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
data/lib/hecks/fqn.rb
CHANGED
|
@@ -6,22 +6,30 @@ module Hecks
|
|
|
6
6
|
class Fqn
|
|
7
7
|
class Invalid < ArgumentError; end
|
|
8
8
|
|
|
9
|
-
attr_reader :realm, :domain, :version, :aggregate, :verb
|
|
9
|
+
attr_reader :realm, :domain, :version, :aggregate, :verb, :kind
|
|
10
10
|
|
|
11
11
|
def self.command(realm:, domain:, aggregate:, command:, version: nil)
|
|
12
12
|
new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: command, kind: :command)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def self.query(realm:, domain:, aggregate: nil,
|
|
15
|
+
def self.query(realm:, domain:, query:, aggregate: nil, version: nil)
|
|
16
16
|
new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: query, kind: :query)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# ONE ORDER-DEPENDENT PARSE PIPELINE: split -> shape-validate -> dispatch
|
|
20
|
+
# on segment count -> split domain/version -> classify kind -> cross-
|
|
21
|
+
# field validate -> construct. Each step consumes locals (segments, verb,
|
|
22
|
+
# kind) the step before it derived; splitting would mean threading all of
|
|
23
|
+
# them back out as parameters/returns between new methods, for no
|
|
24
|
+
# readability gain over reading the pipeline top to bottom once.
|
|
25
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
19
26
|
def self.parse(text)
|
|
20
27
|
head, separator, verb = text.to_s.rpartition(".")
|
|
21
28
|
raise Invalid, "FQN must be Realm::Domain::Aggregate.verb — got #{text.inspect}" if separator.empty?
|
|
22
29
|
|
|
23
30
|
segments = head.split("::", -1)
|
|
24
|
-
|
|
31
|
+
valid_fqn = [1, 2, 3].include?(segments.length) && segments.none?(&:empty?) && !verb.empty?
|
|
32
|
+
raise Invalid, "FQN must be Realm::Domain::Aggregate.verb — got #{text.inspect}" unless valid_fqn
|
|
25
33
|
|
|
26
34
|
realm, domain_spec, aggregate = case segments.length
|
|
27
35
|
when 3 then segments
|
|
@@ -48,7 +56,7 @@ module Hecks
|
|
|
48
56
|
def self.command_name?(name) = /\A[A-Z][A-Za-z0-9]*\z/.match?(name.to_s)
|
|
49
57
|
def self.query_name?(name) = /\A[a-z][a-z0-9_]*\z/.match?(name.to_s)
|
|
50
58
|
|
|
51
|
-
def initialize(realm:, domain:,
|
|
59
|
+
def initialize(realm:, domain:, aggregate:, verb:, kind:, version: nil)
|
|
52
60
|
@realm = realm && segment(realm, "realm")
|
|
53
61
|
@domain = segment(domain, "domain")
|
|
54
62
|
@version = version && segment(version, "version")
|
|
@@ -62,7 +70,6 @@ module Hecks
|
|
|
62
70
|
|
|
63
71
|
def command? = @kind == :command
|
|
64
72
|
def query? = @kind == :query
|
|
65
|
-
def kind = @kind
|
|
66
73
|
|
|
67
74
|
def to_s
|
|
68
75
|
domain = @version ? "#{@domain}@#{@version}" : @domain
|
|
@@ -77,16 +84,21 @@ module Hecks
|
|
|
77
84
|
|
|
78
85
|
def self.split_domain(value)
|
|
79
86
|
domain, version, extra = value.to_s.split("@", 3)
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
if domain.empty? || extra || (value.include?("@") && version.to_s.empty?)
|
|
88
|
+
raise Invalid,
|
|
89
|
+
"FQN domain version is malformed: #{value.inspect}"
|
|
90
|
+
end
|
|
82
91
|
|
|
83
92
|
[domain, version]
|
|
84
93
|
end
|
|
94
|
+
private_class_method :split_domain
|
|
85
95
|
|
|
86
96
|
def segment(value, label)
|
|
87
97
|
text = value.to_s
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
if text.empty? || text.include?("::") || text.include?(".") || text.include?("@")
|
|
99
|
+
raise Invalid,
|
|
100
|
+
"FQN #{label} cannot be empty or contain a separator"
|
|
101
|
+
end
|
|
90
102
|
|
|
91
103
|
text
|
|
92
104
|
end
|
|
@@ -101,7 +101,7 @@ module Hecks
|
|
|
101
101
|
# actually bound to.
|
|
102
102
|
ARRAY_ELEMENT_TYPE = { "arr_num" => :numeric, "arr_str" => :string }.freeze
|
|
103
103
|
|
|
104
|
-
BLOCK_PARAM = "el"
|
|
104
|
+
BLOCK_PARAM = "el".freeze
|
|
105
105
|
|
|
106
106
|
MAX_DEPTH = 3
|
|
107
107
|
|
|
@@ -217,7 +217,9 @@ module Hecks
|
|
|
217
217
|
when :string then string_productions(depth)
|
|
218
218
|
when :boolean then boolean_productions(depth)
|
|
219
219
|
when :array then array_productions(depth)
|
|
220
|
-
|
|
220
|
+
# no recursive producer of nil in this grammar — Find's "not found" is a
|
|
221
|
+
# runtime OUTCOME, not a distinct construct to render as source text
|
|
222
|
+
when :nil_type then []
|
|
221
223
|
else raise ArgumentError, "no production rule for type #{type.inspect}"
|
|
222
224
|
end
|
|
223
225
|
end
|
|
@@ -291,8 +293,12 @@ module Hecks
|
|
|
291
293
|
def resolver_boolean_leaves(depth)
|
|
292
294
|
leaves(:boolean) +
|
|
293
295
|
resolver_numeric_leaves(depth).flat_map { |n| ["#{n}.positive?", "#{n}.negative?", "#{n}.zero?"] } +
|
|
294
|
-
bounded(:string, depth).flat_map
|
|
295
|
-
|
|
296
|
+
bounded(:string, depth).flat_map do |s|
|
|
297
|
+
["#{s}.present?", "#{s}.blank?", "#{s}.match?(/a/)", "#{s}.start_with?(\"a\")", "#{s}.end_with?(\"a\")"]
|
|
298
|
+
end +
|
|
299
|
+
(sample(numeric_array_productions(depth)) + sample(string_array_productions(depth)) + bounded(:array, depth)).map do |x|
|
|
300
|
+
"#{x}.empty?"
|
|
301
|
+
end +
|
|
296
302
|
sample(block_predicate_productions(depth))
|
|
297
303
|
end
|
|
298
304
|
|
|
@@ -386,6 +392,15 @@ module Hecks
|
|
|
386
392
|
# typed at its own top level (`Evaluator.truthy?`), so this is also
|
|
387
393
|
# the set `all_predicates` (below) draws its top-level cases from
|
|
388
394
|
# directly.
|
|
395
|
+
# One declarative table of boolean productions, not several
|
|
396
|
+
# unrelated things bolted together — each `+`-joined term is a
|
|
397
|
+
# distinct grammar rule, and several carry their own comment
|
|
398
|
+
# explaining a specific inclusion/exclusion choice (the
|
|
399
|
+
# `resolver_numeric_leaves`-not-`num` note above, `arr_num`'s non-
|
|
400
|
+
# empty twin, etc). Extracting terms into separate methods would
|
|
401
|
+
# need `sub`/`num`/`str` threaded into each as parameters and
|
|
402
|
+
# would separate every rule from the comment justifying it.
|
|
403
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
389
404
|
def boolean_productions(depth)
|
|
390
405
|
sub = bounded(:boolean, depth - 1)
|
|
391
406
|
num = bounded(:numeric, depth - 1)
|
|
@@ -404,8 +419,11 @@ module Hecks
|
|
|
404
419
|
# documents (`"0 + 0.positive?"` would parse as `0 + (0
|
|
405
420
|
# .positive?)`, not `(0 + 0).positive?`).
|
|
406
421
|
resolver_numeric_leaves(depth - 1).flat_map { |n| ["#{n}.positive?", "#{n}.negative?", "#{n}.zero?"] } +
|
|
407
|
-
(str + sample(numeric_array_productions(depth - 1)) + sample(string_array_productions(depth - 1)) + bounded(:array,
|
|
408
|
-
|
|
422
|
+
(str + sample(numeric_array_productions(depth - 1)) + sample(string_array_productions(depth - 1)) + bounded(:array,
|
|
423
|
+
depth - 1))
|
|
424
|
+
.map do |x|
|
|
425
|
+
"#{x}.empty?"
|
|
426
|
+
end +
|
|
409
427
|
str.map { |s| "#{s}.match?(/a/)" } +
|
|
410
428
|
str.flat_map { |s| ["#{s}.present?", "#{s}.blank?"] } +
|
|
411
429
|
str.flat_map { |s| ["#{s}.start_with?(\"a\")", "#{s}.end_with?(\"a\")"] } +
|
|
@@ -461,8 +479,12 @@ module Hecks
|
|
|
461
479
|
def include_productions(depth)
|
|
462
480
|
str = bounded(:string, depth)
|
|
463
481
|
pairs(str).map { |haystack, needle| "#{haystack}.include?(#{needle})" } +
|
|
464
|
-
sample(numeric_array_productions(depth)).product(bounded(:numeric, depth)).map
|
|
465
|
-
|
|
482
|
+
sample(numeric_array_productions(depth)).product(bounded(:numeric, depth)).map do |arr, needle|
|
|
483
|
+
"#{arr}.include?(#{needle})"
|
|
484
|
+
end +
|
|
485
|
+
sample(string_array_productions(depth)).product(bounded(:string, depth)).map do |arr, needle|
|
|
486
|
+
"#{arr}.include?(#{needle})"
|
|
487
|
+
end
|
|
466
488
|
end
|
|
467
489
|
|
|
468
490
|
# SAMPLED, NOT A FULL CROSS PRODUCT — a full `list.product(list)`
|
|
@@ -103,7 +103,15 @@ module Hecks
|
|
|
103
103
|
FileUtils.mkdir_p(target)
|
|
104
104
|
else
|
|
105
105
|
FileUtils.mkdir_p(File.dirname(target))
|
|
106
|
-
|
|
106
|
+
begin
|
|
107
|
+
FileUtils.cp(path, target)
|
|
108
|
+
rescue Errno::ENOENT
|
|
109
|
+
# A transient sibling (an adapter's atomic-write `.tmp.<pid>`
|
|
110
|
+
# file, caught mid-rename by the glob above) can vanish
|
|
111
|
+
# between listing and copy — under parallel_r-spec two
|
|
112
|
+
# workers share the real example tree. A file that no longer
|
|
113
|
+
# exists was never part of the state this copy needs.
|
|
114
|
+
end
|
|
107
115
|
end
|
|
108
116
|
end
|
|
109
117
|
end
|
|
@@ -160,8 +168,8 @@ module Hecks
|
|
|
160
168
|
# process-unique schema name (`SecureRandom.hex` is already
|
|
161
169
|
# `require`d here for exactly that day) — flagged, not solved,
|
|
162
170
|
# since nothing today calls this from more than one thread.
|
|
163
|
-
FUZZ_POSTGRES_DATABASE = "hecks_fuzz"
|
|
164
|
-
FUZZ_POSTGRES_SCHEMA = "hecks_fuzz"
|
|
171
|
+
FUZZ_POSTGRES_DATABASE = "hecks_fuzz".freeze
|
|
172
|
+
FUZZ_POSTGRES_SCHEMA = "hecks_fuzz".freeze
|
|
165
173
|
|
|
166
174
|
def rebind_to_postgres!(copy)
|
|
167
175
|
require "pg"
|
|
@@ -182,7 +190,7 @@ module Hecks
|
|
|
182
190
|
next if names.empty?
|
|
183
191
|
|
|
184
192
|
world_path = File.join(File.dirname(hecksagon_path), "hecks_fuzz_postgres.world")
|
|
185
|
-
File.write(world_path, names.map
|
|
193
|
+
File.write(world_path, names.map do |name|
|
|
186
194
|
<<~WORLD
|
|
187
195
|
Hecks.world "#{name}" do
|
|
188
196
|
persisted_by("Postgres") do
|
|
@@ -191,7 +199,7 @@ module Hecks
|
|
|
191
199
|
end
|
|
192
200
|
end
|
|
193
201
|
WORLD
|
|
194
|
-
|
|
202
|
+
end.join("\n"))
|
|
195
203
|
end
|
|
196
204
|
|
|
197
205
|
# Any PRE-EXISTING `.world` this copy shipped with (a real
|
|
@@ -277,7 +285,7 @@ module Hecks
|
|
|
277
285
|
# never wires is simply unread, not broken.
|
|
278
286
|
def rewrite_bindings!(copy, adapter_name)
|
|
279
287
|
Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |path|
|
|
280
|
-
lines = File.readlines(path).
|
|
288
|
+
lines = File.readlines(path).grep_v(/\bprojected_by\s*\(?\s*"/)
|
|
281
289
|
File.write(path, lines.join.gsub(/persisted_by\s*\(?\s*"[^"]+"\s*\)?/, "persisted_by(\"#{adapter_name}\")"))
|
|
282
290
|
end
|
|
283
291
|
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
module Properties
|
|
4
|
+
# Dispatch-binding and mutation-recomputation properties: a saga/
|
|
5
|
+
# policy dispatch is bound to the value its own with_spec names, and a
|
|
6
|
+
# command's append/remove/multiply/clamp mutations land on the same
|
|
7
|
+
# after-state an independent recomputation of the SAME rule produces.
|
|
8
|
+
|
|
9
|
+
# Holds dispatch_binding_fidelity and mutations_match_recompute, plus
|
|
10
|
+
# the independently-written re-derivations (#resolve_dispatch_binding,
|
|
11
|
+
# #resolve_trigger_binding, #recompute_append/#remove/#multiply/#clamp)
|
|
12
|
+
# each checks the real dispatch/mutation against.
|
|
13
|
+
module DispatchAndMutations
|
|
14
|
+
# Closes Handler#dispatches and — the same shape, one construct
|
|
15
|
+
# over — Policy#with_spec (Dispatch#command_name/Dispatch#with_spec
|
|
16
|
+
# are not claimable feature strings at all — see FEATURE_COVERAGE's
|
|
17
|
+
# own comment on this entry). history[:saga_dispatches]/[:policy_dispatches]
|
|
18
|
+
# (Registry#saga_dispatch_log/#policy_dispatch_log — additive,
|
|
19
|
+
# Ruby-only, NEVER touching saga_log/reaction_log, the byte-for-
|
|
20
|
+
# byte shape spec/rust_conformance_spec.rb holds Rust to) each
|
|
21
|
+
# carry the RAW inputs a dispatch's own args were resolved from,
|
|
22
|
+
# captured live at the moment the resolution actually ran — a
|
|
23
|
+
# saga's own memory keeps changing across a run, so re-deriving
|
|
24
|
+
# from history[:saga_instances]'s FINAL memory (the only other
|
|
25
|
+
# place it would be visible) would grade the wrong moment,
|
|
26
|
+
# lifecycle_guard_and_given_violations_are_refused's own false
|
|
27
|
+
# positive one item earlier, in a different shape.
|
|
28
|
+
#
|
|
29
|
+
# #resolve_dispatch_binding/#resolve_trigger_binding are SEPARATE,
|
|
30
|
+
# independently-written re-derivations of SagaInterpreter#
|
|
31
|
+
# dispatch_args/PolicyInterpreter#trigger_args's own resolution —
|
|
32
|
+
# never calling either method again, which would only ever agree
|
|
33
|
+
# with itself. Exactly the class of bug this closes: "a wrong
|
|
34
|
+
# argument binding on a fan-out dispatch that produces a perfectly
|
|
35
|
+
# normal-looking log entry (`delivered: true`) and would only ever
|
|
36
|
+
# surface as a downstream assertion failure, if it surfaces at
|
|
37
|
+
# all" — PR #325's own defect class, one level over.
|
|
38
|
+
#
|
|
39
|
+
# Real targets: Settlement (mixed literal/correlation-head/event-
|
|
40
|
+
# payload/memory-fallback bindings across three legs, plus a
|
|
41
|
+
# compensation leg that deliberately omits `reference:`, a field
|
|
42
|
+
# the forward Credit leg carries), ExternalSettlement, Onboarding
|
|
43
|
+
# (no compensation leg, by design — nothing to check there beyond
|
|
44
|
+
# the forward leg's own event-payload binding).
|
|
45
|
+
def dispatch_binding_fidelity(history)
|
|
46
|
+
saga_offenders = history.fetch(:saga_dispatches, []).filter_map do |entry|
|
|
47
|
+
expected = resolve_dispatch_binding(entry)
|
|
48
|
+
next if expected == entry[:args]
|
|
49
|
+
|
|
50
|
+
"#{entry[:process_manager]}##{entry[:instance]} dispatching #{entry[:dispatch]} on #{entry[:on]} — " \
|
|
51
|
+
"bound #{entry[:args].inspect}, but independently re-deriving with_spec's own resolution gives " \
|
|
52
|
+
"#{expected.inspect}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
policy_offenders = history.fetch(:policy_dispatches, []).filter_map do |entry|
|
|
56
|
+
expected = resolve_trigger_binding(entry)
|
|
57
|
+
next if expected == entry[:args]
|
|
58
|
+
|
|
59
|
+
"#{entry[:policy]} on #{entry[:on]} — bound #{entry[:args].inspect}, but independently re-deriving " \
|
|
60
|
+
"with_spec's own resolution gives #{expected.inspect}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
offenders = saga_offenders + policy_offenders
|
|
64
|
+
offenders.empty? || offenders.join("; ")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# SagaInterpreter#dispatch_args's own 4-branch resolution,
|
|
68
|
+
# reproduced independently: a literal, the correlation key itself,
|
|
69
|
+
# the CURRENT triggering event's own payload, or — the fallback —
|
|
70
|
+
# the saga's own carried memory (seeded from the STARTING event's
|
|
71
|
+
# payload, at begin_saga).
|
|
72
|
+
def resolve_dispatch_binding(entry)
|
|
73
|
+
entry[:with_spec].to_h do |key, value|
|
|
74
|
+
resolved = if !value.is_a?(Symbol) then value
|
|
75
|
+
elsif value == entry[:correlation_head] then entry[:instance]
|
|
76
|
+
elsif entry[:event_payload].key?(value) then entry[:event_payload][value]
|
|
77
|
+
else entry[:memory][value]
|
|
78
|
+
end
|
|
79
|
+
[key.to_sym, Runtime::Value.materialize(resolved)]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# PolicyInterpreter#trigger_args's own 2-branch resolution — a
|
|
84
|
+
# policy holds no correlation and no memory, so `payload` (the
|
|
85
|
+
# triggering event's own payload, already merged with a fan-out
|
|
86
|
+
# row's id when there is one) is the WHOLE source.
|
|
87
|
+
def resolve_trigger_binding(entry)
|
|
88
|
+
entry[:with_spec].to_h do |key, value|
|
|
89
|
+
resolved = value.is_a?(Symbol) ? entry[:payload][value] : value
|
|
90
|
+
[key.to_sym, Runtime::Value.materialize(resolved)]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Closes Command#mutations — the last of the five, and the
|
|
95
|
+
# largest: no real corpus entity anywhere uses `append`/`remove`/
|
|
96
|
+
# `multiply`/`clamp` (every real aggregate-owned mutation is
|
|
97
|
+
# aggregate-scoped — Account.Credit's :ledger, LogVisit's
|
|
98
|
+
# :visits); the only entity-owned use of these four ops in the
|
|
99
|
+
# whole repository is spec/fixtures/entity_list_mutations, now a
|
|
100
|
+
# real, bootable, Memory-default domain (no .hecksagon needed at
|
|
101
|
+
# all — a domain with none boots every aggregate against Memory
|
|
102
|
+
# by construction, confirmed live) rather than the raw-Kernel.
|
|
103
|
+
# load-only fixture it was.
|
|
104
|
+
#
|
|
105
|
+
# `history[:mutation_traces]` (Replay's own bounded, additive
|
|
106
|
+
# extension — see #build_mutation_trace's own comment) carries a
|
|
107
|
+
# per-step before/after snapshot of the ENTITY ELEMENT an
|
|
108
|
+
# entity-dispatched command's own mutations acted on, materialized
|
|
109
|
+
# to plain data, plus the step's own raw args — the delta
|
|
110
|
+
# `aggregation_matches_recompute` never had to ask for, because
|
|
111
|
+
# count/median are pure functions of FINAL state and a mutation
|
|
112
|
+
# is not (the same "captured live, not re-derived from final
|
|
113
|
+
# state" lesson item 8's own saga_dispatch_log already learned).
|
|
114
|
+
#
|
|
115
|
+
# #recompute_append/#recompute_remove/#recompute_multiply/
|
|
116
|
+
# #recompute_clamp are SEPARATE, independently-written
|
|
117
|
+
# reproductions of MutationApplier#appended/#removed and
|
|
118
|
+
# CommandRules::Arithmetic#multiply/#clamp — never calling either
|
|
119
|
+
# again, which would only ever agree with itself. `:set`/
|
|
120
|
+
# `:increment`/`:decrement` are out of scope on purpose (the four
|
|
121
|
+
# "vendored, not yet upstream" ops this item exists for); a
|
|
122
|
+
# command mixing them with a recomputable op still gets the
|
|
123
|
+
# recomputable one checked.
|
|
124
|
+
#
|
|
125
|
+
# `:unrecomputable` (never compared, never a finding) covers the
|
|
126
|
+
# generator's own deliberate arg-malforming (`StepBuilder#malform`)
|
|
127
|
+
# landing a non-Numeric amount/non-2-element bounds where
|
|
128
|
+
# multiply/clamp need one — the SAME shape `guard_check`'s own
|
|
129
|
+
# AbsentArgument false positive taught: a step whose raw material
|
|
130
|
+
# doesn't fit the op's own contract is inconclusive, not a claimed
|
|
131
|
+
# mismatch.
|
|
132
|
+
RECOMPUTABLE_MUTATION_OPS = %i[append remove multiply clamp].freeze
|
|
133
|
+
|
|
134
|
+
def mutations_match_recompute(history)
|
|
135
|
+
bluebooks = history.fetch(:bluebooks)
|
|
136
|
+
|
|
137
|
+
offenders = history.fetch(:mutation_traces, []).flat_map do |entry|
|
|
138
|
+
next [] unless entry[:after]
|
|
139
|
+
|
|
140
|
+
command = command_for_verb(bluebooks, entry[:verb])
|
|
141
|
+
next [] unless command
|
|
142
|
+
|
|
143
|
+
command.mutations.select { |m| RECOMPUTABLE_MUTATION_OPS.include?(m.op) }.filter_map do |mutation|
|
|
144
|
+
expected = recompute_mutation(mutation, entry[:before][mutation.target], entry[:args], entry[:before])
|
|
145
|
+
next if expected == :unrecomputable
|
|
146
|
+
|
|
147
|
+
actual = entry[:after][mutation.target]
|
|
148
|
+
next if symbolize_deep(expected) == symbolize_deep(actual)
|
|
149
|
+
|
|
150
|
+
"#{entry[:verb]} — #{mutation.op} on #{mutation.target} — recomputing independently gives " \
|
|
151
|
+
"#{expected.inspect}, but the real dispatch left #{actual.inspect}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
offenders.empty? || offenders.join("; ")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def recompute_mutation(mutation, current, args, before_scope)
|
|
159
|
+
case mutation.op
|
|
160
|
+
when :append then recompute_append(current, mutation.source, before_scope, args)
|
|
161
|
+
when :remove then recompute_remove(current, mutation.source, args)
|
|
162
|
+
when :multiply then recompute_multiply(current, resolve_mutation_source(mutation.source, args))
|
|
163
|
+
when :clamp then recompute_clamp(current, mutation.source)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# `MutationApplier#appended`'s own value-object branch (never the
|
|
168
|
+
# entity_element branch — see #build_mutation_trace's own comment
|
|
169
|
+
# on why an entity-dispatched command's own mutations never reach
|
|
170
|
+
# it), reproduced: the field map resolved the SAME two-tier way
|
|
171
|
+
# (`MutationApplier#resolve_append_source` — a caller-supplied
|
|
172
|
+
# arg, or the entity's own current field), then appended.
|
|
173
|
+
def recompute_append(current, source_map, before_scope, args)
|
|
174
|
+
fields = source_map.transform_values { |source| resolve_mutation_append_field(source, before_scope, args) }
|
|
175
|
+
Array(current) + [symbolize_deep(fields)]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def resolve_mutation_append_field(source, before_scope, args)
|
|
179
|
+
return source unless source.is_a?(Symbol)
|
|
180
|
+
return args[source] if args.key?(source)
|
|
181
|
+
|
|
182
|
+
before_scope[source]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# `MutationApplier#removed`'s own value-equality match, reproduced.
|
|
186
|
+
def recompute_remove(current, source, args)
|
|
187
|
+
target = symbolize_deep(resolve_mutation_source(source, args))
|
|
188
|
+
Array(current).reject { |element| symbolize_deep(element) == target }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# `CommandRules::Arithmetic#multiply`'s own two branches,
|
|
192
|
+
# reproduced on plain materialized data instead of a real Value:
|
|
193
|
+
# a single-numeric-field Hash (the VO-typed case — ListCount, one
|
|
194
|
+
# Integer field) scales that field ; a bare Numeric scales itself.
|
|
195
|
+
# `current ||= 0` — the SAME phantom-field fallback #multiply
|
|
196
|
+
# itself already gives (unaffected by this session's #clamp fix,
|
|
197
|
+
# since #multiply never needed one).
|
|
198
|
+
def recompute_multiply(current, amount)
|
|
199
|
+
return :unrecomputable unless amount.is_a?(Numeric)
|
|
200
|
+
|
|
201
|
+
current ||= 0
|
|
202
|
+
if current.is_a?(Hash)
|
|
203
|
+
field = current.keys.find { |f| current[f].is_a?(Numeric) }
|
|
204
|
+
return :unrecomputable unless field
|
|
205
|
+
|
|
206
|
+
current.merge(field => current[field] * amount)
|
|
207
|
+
elsif current.is_a?(Numeric)
|
|
208
|
+
current * amount
|
|
209
|
+
else
|
|
210
|
+
:unrecomputable
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# `CommandRules::Arithmetic#clamp`'s own two branches, reproduced
|
|
215
|
+
# the same way #recompute_multiply is — including THIS SESSION'S
|
|
216
|
+
# OWN `current ||= 0` fix (command_rules/arithmetic.rb), the one
|
|
217
|
+
# arithmetic op that didn't have it until now. `mutation.source`
|
|
218
|
+
# is always a literal `[min, max]`, never an argument reference
|
|
219
|
+
# (MutationApplier's own comment on why `resolve_source` is
|
|
220
|
+
# skipped for clamp) — so nothing here reads `args` for it at all.
|
|
221
|
+
def recompute_clamp(current, bounds)
|
|
222
|
+
return :unrecomputable unless bounds.is_a?(Array) && bounds.size == 2
|
|
223
|
+
|
|
224
|
+
min, max = bounds
|
|
225
|
+
current ||= 0
|
|
226
|
+
if current.is_a?(Hash)
|
|
227
|
+
field = current.keys.find { |f| current[f].is_a?(Numeric) }
|
|
228
|
+
return :unrecomputable unless field
|
|
229
|
+
|
|
230
|
+
current.merge(field => current[field].clamp(min, max))
|
|
231
|
+
elsif current.is_a?(Numeric)
|
|
232
|
+
current.clamp(min, max)
|
|
233
|
+
else
|
|
234
|
+
:unrecomputable
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# `CommandRules::Arithmetic#resolve_source`, reproduced: a
|
|
239
|
+
# mutation's source is either the NAME OF AN ARGUMENT or a
|
|
240
|
+
# LITERAL, told apart by type.
|
|
241
|
+
def resolve_mutation_source(source, args)
|
|
242
|
+
source.is_a?(Symbol) ? args[source] : source
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# A generated step's own `args` arrive with STRING keys on every
|
|
246
|
+
# nested Hash (the wire/JSON shape `spec/corpus/*.json` already
|
|
247
|
+
# uses) while `history[:mutation_traces]`' own materialized
|
|
248
|
+
# before/after state carries SYMBOL keys throughout (Runtime::
|
|
249
|
+
# Value.materialize's own convention) — two hashes holding the
|
|
250
|
+
# identical fact compare UNEQUAL by Ruby's own `Hash#==` unless
|
|
251
|
+
# both sides are normalized the same way first. Recursive, since
|
|
252
|
+
# an appended/removed element can itself nest a value object
|
|
253
|
+
# (RemoveTag's own `Tag` argument, `{"key"=>..., "value"=>...}`).
|
|
254
|
+
def symbolize_deep(value)
|
|
255
|
+
case value
|
|
256
|
+
when Hash then value.to_h { |key, val| [key.to_sym, symbolize_deep(val)] }
|
|
257
|
+
when Array then value.map { |val| symbolize_deep(val) }
|
|
258
|
+
else value
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|