hecks 1.4.0 → 1.5.1
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 +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/deploy/fargate.rb +666 -0
- data/lib/hecks/projections/deploy/lambda.rb +2423 -0
- data/lib/hecks/projections/deploy/shared.rb +624 -0
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projections.rb +3 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +47 -10
- data/lib/hecks/projector.rb +101 -19
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +14 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
- data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
|
@@ -49,6 +49,11 @@ module Hecks
|
|
|
49
49
|
#
|
|
50
50
|
# Real target: SafeDepositBox's own Visit — `invariant("a written
|
|
51
51
|
# note is not blank") { !note || !note.text.to_s.empty? }`.
|
|
52
|
+
#
|
|
53
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
54
|
+
# @return [true, String] true if every stored record satisfies its own
|
|
55
|
+
# (and its own entities') declared invariants; otherwise a message
|
|
56
|
+
# naming the record and the invariant it violates
|
|
52
57
|
def stored_records_satisfy_declared_invariants(history)
|
|
53
58
|
bluebooks = history.fetch(:bluebooks)
|
|
54
59
|
|
|
@@ -76,6 +81,12 @@ module Hecks
|
|
|
76
81
|
# a.list? && a.type.to_s == entity.hecks_name }`), independently
|
|
77
82
|
# reapplied here against a stored record's own plain Hash state
|
|
78
83
|
# rather than a live `Instance`.
|
|
84
|
+
# @param owner_construct [Bluebook::Aggregate, Bluebook::Entity] the
|
|
85
|
+
# construct whose own `list_of` entities to check
|
|
86
|
+
# @param owner_state [Hash] `owner_construct`'s own stored state
|
|
87
|
+
# @param key [String] the top-level record key, for the message
|
|
88
|
+
# @return [String, nil] a message naming the first violating piece found,
|
|
89
|
+
# at any nesting depth; `nil` if every piece satisfies its invariants
|
|
79
90
|
def check_piece_invariants(owner_construct, owner_state, key)
|
|
80
91
|
owner_construct.entities.each do |entity|
|
|
81
92
|
next if entity.invariants.empty?
|
|
@@ -122,6 +133,10 @@ module Hecks
|
|
|
122
133
|
# a live or rehydrated instance sitting in a state the procedure
|
|
123
134
|
# never declares is the saga-durability twin of
|
|
124
135
|
# `lifecycle_values_are_declared` above.
|
|
136
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
137
|
+
# @return [true, String] true if every saga instance holds a declared
|
|
138
|
+
# state and its own memory survives a checkpoint round-trip; otherwise a
|
|
139
|
+
# message naming the process manager, correlation, and problem
|
|
125
140
|
def sagas_rehydrate_cleanly(history)
|
|
126
141
|
bluebook = history.fetch(:bluebook)
|
|
127
142
|
process_managers = bluebook.process_managers.to_h { |pm| [pm.name, pm] }
|
|
@@ -163,6 +178,10 @@ module Hecks
|
|
|
163
178
|
# dispatch is the claim then, not "dispatched to zero rows," and a
|
|
164
179
|
# policy that dispatched anyway despite a failing guard is as real
|
|
165
180
|
# a finding as a row it skipped.
|
|
181
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
182
|
+
# @return [true, String] true if every fan-out finding's actual dispatches
|
|
183
|
+
# match its independently computed expected row set; otherwise a message
|
|
184
|
+
# naming the policy, event, and disagreement
|
|
166
185
|
def fanout_dispatches_once_per_matching_row(history)
|
|
167
186
|
offenders = history.fetch(:fan_outs).filter_map do |finding|
|
|
168
187
|
expected = finding[:expected_row_ids]
|
|
@@ -211,6 +230,10 @@ module Hecks
|
|
|
211
230
|
# branch reads as its own precondition."
|
|
212
231
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
213
232
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
233
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
234
|
+
# @return [true, String] true if every eligible count/median report answer
|
|
235
|
+
# matches an independent recomputation; otherwise a message naming the
|
|
236
|
+
# query and the disagreement
|
|
214
237
|
def aggregation_matches_recompute(history)
|
|
215
238
|
bluebook = history.fetch(:bluebook)
|
|
216
239
|
|
|
@@ -257,6 +280,11 @@ module Hecks
|
|
|
257
280
|
# of count/median, nest_rows in place of recompute_median.
|
|
258
281
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
259
282
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
283
|
+
#
|
|
284
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
285
|
+
# @return [true, String] true if every eligible group_by report answer matches
|
|
286
|
+
# an independent recompute; otherwise a semicolon-joined message naming each
|
|
287
|
+
# offending query
|
|
260
288
|
def group_by_matches_recompute(history)
|
|
261
289
|
bluebook = history.fetch(:bluebook)
|
|
262
290
|
|
|
@@ -290,6 +318,11 @@ module Hecks
|
|
|
290
318
|
# per `group_by` field in declared order, leaf is the row with
|
|
291
319
|
# every grouped field stripped (already spent, as the keys that
|
|
292
320
|
# reached it).
|
|
321
|
+
# @param rows [Array<Hash>] materialized, symbol-keyed rows to nest
|
|
322
|
+
# @param fields [Array<Symbol>] the `group_by` fields, in declared order
|
|
323
|
+
# @return [Hash] one level of nesting per field, in order; the leaf under
|
|
324
|
+
# each key path is the first row in that group with every grouped field
|
|
325
|
+
# stripped
|
|
293
326
|
def nest_rows(rows, fields)
|
|
294
327
|
field, *rest = fields
|
|
295
328
|
rows.group_by { |row| row[field] }.transform_values do |group|
|
|
@@ -305,6 +338,16 @@ module Hecks
|
|
|
305
338
|
# finds the matching attribute, then narrowed by the report's own
|
|
306
339
|
# `where` clauses via the same `InMemory.holds?` the interpreter's
|
|
307
340
|
# `execute` calls.
|
|
341
|
+
# @param bluebook [Bluebook::Chapter] the bluebook the report belongs to
|
|
342
|
+
# @param instances [Hash] the snapshot to read rows from (`history[:instances]`
|
|
343
|
+
# shape, or the query's own `instances_at`)
|
|
344
|
+
# @param domain [String] the domain name the reduced aggregate belongs to
|
|
345
|
+
# @param model [Bluebook::ReadModel] the report being recomputed
|
|
346
|
+
# @param reduced_head [Hash] the many-side aggregate head being reduced, from
|
|
347
|
+
# `model.aggregate_heads`
|
|
348
|
+
# @param args [Hash] the query's own arguments, for FK matching and `where`
|
|
349
|
+
# evaluation
|
|
350
|
+
# @return [Array<Hash>] every eligible row's own state, `id:` merged in
|
|
308
351
|
def eligible_rows(bluebook, instances, domain, model, reduced_head, args)
|
|
309
352
|
aggregate = bluebook.aggregate(reduced_head[:aggregate])
|
|
310
353
|
prefix = "#{domain}::#{reduced_head[:aggregate]}#"
|
|
@@ -337,6 +380,11 @@ module Hecks
|
|
|
337
380
|
# → the average of the two middle values, as a Float; empty → nil,
|
|
338
381
|
# never zero, so a caller cannot mistake "nothing to average" for
|
|
339
382
|
# "averaged to zero."
|
|
383
|
+
# @param rows [Array<Hash>] eligible rows, as returned by `#eligible_rows`
|
|
384
|
+
# @param field [Symbol] the field to average
|
|
385
|
+
# @return [Object, Float, nil] the true middle value for an odd count, the
|
|
386
|
+
# Float average of the two middle values for an even count, or `nil` for
|
|
387
|
+
# an empty `rows`
|
|
340
388
|
def recompute_median(rows, field)
|
|
341
389
|
values = rows.map { |state| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, field)) }
|
|
342
390
|
.compact.sort
|
|
@@ -22,6 +22,10 @@ module Hecks
|
|
|
22
22
|
# a coercion bug, a stale string surviving a rename, a default
|
|
23
23
|
# that drifted from the declared set, would all show up here as a
|
|
24
24
|
# value nothing upstream would have predicted.
|
|
25
|
+
#
|
|
26
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
27
|
+
# @return [true, String] true if every instance's lifecycle field holds a declared
|
|
28
|
+
# state; otherwise a semicolon-joined message naming each offending instance
|
|
25
29
|
def lifecycle_values_are_declared(history)
|
|
26
30
|
bluebook = history.fetch(:bluebook)
|
|
27
31
|
declared = {}
|
|
@@ -54,6 +58,11 @@ module Hecks
|
|
|
54
58
|
# handler names would mean the runtime moved state the language
|
|
55
59
|
# never authorized — the same trust ModelCheck's static reachability
|
|
56
60
|
# rests on, checked here against what a run actually did.
|
|
61
|
+
#
|
|
62
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
63
|
+
# @return [true, String] true if every logged advance matches a declared
|
|
64
|
+
# handler edge; otherwise a message naming the process manager and pair
|
|
65
|
+
# that does not
|
|
57
66
|
def saga_advances_follow_declared_handlers(history)
|
|
58
67
|
bluebook = history.fetch(:bluebook)
|
|
59
68
|
edges = Hash.new { |h, k| h[k] = [] }
|
|
@@ -86,6 +95,15 @@ module Hecks
|
|
|
86
95
|
# comparison depended on, anything. Two independent replays, not a
|
|
87
96
|
# cached one compared to itself, so a bug that corrupts the first
|
|
88
97
|
# run's own bookkeeping cannot pass by agreeing with itself.
|
|
98
|
+
#
|
|
99
|
+
# @param domain_path [String] path to the domain directory to boot, such as
|
|
100
|
+
# `"examples/pizzas"`
|
|
101
|
+
# @param steps [Array<Hash>] the step list to replay twice
|
|
102
|
+
# @param adapter [Symbol] persistence adapter to boot with (`:memory`,
|
|
103
|
+
# `:postgres`, or `:postgres_era`)
|
|
104
|
+
# @return [true, String] true if both replays produce identical histories
|
|
105
|
+
# (after stripping declared nondeterministic fields); otherwise a message
|
|
106
|
+
# naming the step count that diverged
|
|
89
107
|
def replay_is_deterministic(domain_path, steps, adapter: :memory)
|
|
90
108
|
first = Replay.call(domain_path, steps, adapter: adapter)
|
|
91
109
|
second = Replay.call(domain_path, steps, adapter: adapter)
|
|
@@ -10,6 +10,8 @@ module Hecks
|
|
|
10
10
|
# capture, one entry per step whose dispatch enqueued at least one
|
|
11
11
|
# row):
|
|
12
12
|
#
|
|
13
|
+
# ## The two checks
|
|
14
|
+
#
|
|
13
15
|
# 1. "delivery is inline by default" — a row this replay's own
|
|
14
16
|
# dispatch enqueued must not still be `pending`/`claimed` once
|
|
15
17
|
# that same call returns (nothing here ever simulates a
|
|
@@ -38,10 +40,9 @@ module Hecks
|
|
|
38
40
|
# it should have dispatched to) is `fanout_dispatches_once_
|
|
39
41
|
# per_matching_row`'s job, not this one's.
|
|
40
42
|
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
# existed: `Fanout.sagas`' own `listens?` (starts_on/ends_on/
|
|
43
|
+
# ## Why a `saga:` row is exempt from check 2
|
|
44
|
+
#
|
|
45
|
+
# Deliberately: `Fanout.sagas`' own `listens?` (starts_on/ends_on/
|
|
45
46
|
# handler_for matching the event name alone) says nothing about
|
|
46
47
|
# whether a correlation resolves or a live instance exists, and
|
|
47
48
|
# `begin_saga`/`end_saga` (saga_interpreter.rb) both have silent,
|
|
@@ -57,11 +58,18 @@ module Hecks
|
|
|
57
58
|
# `saga_log` entries is therefore not a finding — only check 1
|
|
58
59
|
# applies to it.
|
|
59
60
|
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
61
|
+
# ## Not a grammar construct
|
|
62
|
+
#
|
|
63
|
+
# `FEATURE_COVERAGE`'s own `dry_runs_leave_no_trace` precedent: the
|
|
64
|
+
# outbox is a runtime door (`Runtime::Outbox`), not a word a
|
|
65
|
+
# bluebook declares, so there is no feature string here to claim.
|
|
64
66
|
module Outbox
|
|
67
|
+
# Checks every outbox row a replay's history recorded against the
|
|
68
|
+
# outbox's own contract (see this file's own header).
|
|
69
|
+
#
|
|
70
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
71
|
+
# @return [true, String] true if every outbox row satisfies the contract; otherwise
|
|
72
|
+
# a semicolon-joined message naming each offending row
|
|
65
73
|
def outbox_rows_match_reactions(history)
|
|
66
74
|
bluebooks = history.fetch(:bluebooks, {})
|
|
67
75
|
|
|
@@ -72,6 +80,17 @@ module Hecks
|
|
|
72
80
|
offenders.empty? || offenders.join("; ")
|
|
73
81
|
end
|
|
74
82
|
|
|
83
|
+
# Checks one outbox row against the outbox's own contract.
|
|
84
|
+
#
|
|
85
|
+
# @param row [Hash] one `Runtime::Outbox::Row#to_h` entry, `trace[:rows]`'s shape:
|
|
86
|
+
# at least `:status`, `:delivery_id`, `:consumer`, `:event`, and (when failed)
|
|
87
|
+
# `:error`
|
|
88
|
+
# @param trace [Hash] this row's outbox trace entry, one of `history[:outbox_traces]`,
|
|
89
|
+
# carrying `:rows` and `:reactions`
|
|
90
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
91
|
+
# keyed by domain name
|
|
92
|
+
# @return [Array<String>] zero or one offending message naming what this row got
|
|
93
|
+
# wrong; empty when the row is fine or its status names no check
|
|
75
94
|
def outbox_row_offenders(row, trace, bluebooks)
|
|
76
95
|
on = row.dig(:event, :name)
|
|
77
96
|
|
|
@@ -90,9 +109,20 @@ module Hecks
|
|
|
90
109
|
end
|
|
91
110
|
end
|
|
92
111
|
|
|
112
|
+
# Checks one delivered `policy:` outbox row for a missing `reaction_log` entry.
|
|
113
|
+
#
|
|
93
114
|
# See this file's own header for why a `saga:` row is exempt: its
|
|
94
115
|
# own `listens?` gives no such guarantee, unlike a policy's single,
|
|
95
116
|
# deterministic `where` gate.
|
|
117
|
+
#
|
|
118
|
+
# @param row [Hash] the delivered outbox row being checked
|
|
119
|
+
# @param on [String] the row's event name, `row.dig(:event, :name)`
|
|
120
|
+
# @param trace [Hash] this row's outbox trace entry, carrying `:reactions`
|
|
121
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
122
|
+
# keyed by domain name
|
|
123
|
+
# @return [Array<String>] zero or one offending message; empty when the row is
|
|
124
|
+
# not a policy row, the policy is undeclared or fans out, or the policy's
|
|
125
|
+
# `where` does not independently re-evaluate true
|
|
96
126
|
def outbox_delivered_policy_offenders(row, on, trace, bluebooks)
|
|
97
127
|
kind, fqn = row[:consumer].to_s.split(":", 2)
|
|
98
128
|
return [] unless kind == "policy"
|
|
@@ -101,11 +131,14 @@ module Hecks
|
|
|
101
131
|
return [] if trace[:reactions].any? { |entry| entry[:policy] == name && entry[:on] == on }
|
|
102
132
|
|
|
103
133
|
policy = bluebooks[home]&.policies&.find { |candidate| candidate.name == name }
|
|
104
|
-
|
|
105
|
-
return []
|
|
134
|
+
# Nothing declared under this name — inconclusive, not a claimed mismatch.
|
|
135
|
+
return [] unless policy
|
|
136
|
+
# Fan-out row count is fanout_dispatches_once_per_matching_row's job.
|
|
137
|
+
return [] if policy.fans_out?
|
|
106
138
|
|
|
107
139
|
held = independently_re_evaluate_policy_where(policy, row[:event])
|
|
108
|
-
|
|
140
|
+
# False, or inconclusive (the where itself raised) — never a claimed mismatch.
|
|
141
|
+
return [] if held != true
|
|
109
142
|
|
|
110
143
|
["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) drained as delivered, but no matching " \
|
|
111
144
|
"reaction_log entry exists and the policy's own where clause independently re-evaluates true — " \
|
|
@@ -128,6 +161,11 @@ module Hecks
|
|
|
128
161
|
# claimed pass or a claimed mismatch from a resolution this replay
|
|
129
162
|
# cannot actually reproduce" discipline `build_guard_check`'s own
|
|
130
163
|
# rescue clause already follows.
|
|
164
|
+
#
|
|
165
|
+
# @param policy [Bluebook::Policy] the policy whose `where` clause is re-evaluated
|
|
166
|
+
# @param event [Hash] the outbox row's own recorded event, read for `:payload`
|
|
167
|
+
# @return [Boolean, nil] whether `policy`'s `where` holds against `event`'s payload,
|
|
168
|
+
# or nil if the where clause cannot be re-evaluated from it
|
|
131
169
|
def independently_re_evaluate_policy_where(policy, event)
|
|
132
170
|
return true if policy.where.to_s.empty?
|
|
133
171
|
|
|
@@ -21,20 +21,24 @@ module Hecks
|
|
|
21
21
|
# drifted before — an adapter that accepts what the reference says
|
|
22
22
|
# matches nothing, or orders what it refuses to order, shows up
|
|
23
23
|
# here as a finding no self-referential adapter spec could see.
|
|
24
|
-
# M23 — `Replay`
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
24
|
+
# M23 — `Replay` runs the native and reference engines independently
|
|
25
|
+
# (each in its own begin/rescue — see that file's own comment at the
|
|
26
|
+
# capture site), so this property can tell apart "both engines
|
|
27
|
+
# refused" (fine — the ask was genuinely bad, nothing to compare)
|
|
28
|
+
# from "one refused and the other did not" (a real divergence — the
|
|
29
|
+
# two engines disagree about whether the ask was even valid, never
|
|
30
|
+
# mind what it answers). `native_refused`/`reference_refused` are
|
|
31
|
+
# read by key presence, not truthiness — `Replay` only ever adds
|
|
32
|
+
# `:error`/`:reference_error` to an entry when that side actually
|
|
33
|
+
# raised, so an absent key is an unambiguous "this side answered." A
|
|
34
|
+
# read-model ask (no reference twin attempted at all, `asked[:query]`
|
|
35
|
+
# without "::") is skipped entirely, same as always — there is no
|
|
36
|
+
# second engine to disagree with.
|
|
37
|
+
#
|
|
38
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
39
|
+
# @return [true, String] true if every native/reference query answer, and every
|
|
40
|
+
# refusal of one, agrees; otherwise a message naming the offending query, its
|
|
41
|
+
# args, and how the two engines disagreed
|
|
38
42
|
def query_answers_match_reference(history)
|
|
39
43
|
offenders = history.fetch(:queries).filter_map do |asked|
|
|
40
44
|
next unless asked[:query].is_a?(String) && asked[:query].include?("::")
|
|
@@ -83,6 +87,11 @@ module Hecks
|
|
|
83
87
|
# `skipped`/`expected` across method boundaries as params/returns
|
|
84
88
|
# for a sequence that's only ever computed once, in this order.
|
|
85
89
|
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
90
|
+
#
|
|
91
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
92
|
+
# @return [true, String] true if every paged query's real answer matches an
|
|
93
|
+
# independent order/offset/limit recomputation over its eligible rows;
|
|
94
|
+
# otherwise a message naming the query, its args, and the two answers
|
|
86
95
|
def paging_offset_partitions_correctly(history)
|
|
87
96
|
bluebooks = history.fetch(:bluebooks)
|
|
88
97
|
|
|
@@ -119,6 +128,13 @@ module Hecks
|
|
|
119
128
|
# corpus site yet, and the "one many-side head, one aggregate,
|
|
120
129
|
# no FK-join" shape #query_eligible_rows assumes doesn't hold for
|
|
121
130
|
# one.
|
|
131
|
+
#
|
|
132
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
133
|
+
# keyed by domain name
|
|
134
|
+
# @param verb [String] the asked verb, `"Domain::Aggregate.Query"`
|
|
135
|
+
# @return [Bluebook::Query, nil] the declared query, or nil if `verb` names an
|
|
136
|
+
# entity-level query path, or a domain, aggregate, or query not found among
|
|
137
|
+
# `bluebooks`
|
|
122
138
|
def query_for_verb(bluebooks, verb)
|
|
123
139
|
domain, aggregate_name, query_path = Naming.split_verb(verb)
|
|
124
140
|
return nil unless query_path && !query_path.include?(".")
|
|
@@ -128,6 +144,9 @@ module Hecks
|
|
|
128
144
|
aggregate&.query(query_path)
|
|
129
145
|
end
|
|
130
146
|
|
|
147
|
+
# Independently recomputes which of an aggregate's own stored rows a
|
|
148
|
+
# query's `wheres` admit.
|
|
149
|
+
#
|
|
131
150
|
# A query's own rows — unlike #eligible_rows (a ReadModel's
|
|
132
151
|
# reduced/grouped many-side head, possibly FK-joined against a
|
|
133
152
|
# root), a Query always asks about its own owning aggregate
|
|
@@ -149,6 +168,18 @@ module Hecks
|
|
|
149
168
|
# nil, and declared every genuinely-eligible row ineligible — a
|
|
150
169
|
# false property violation against a correct runtime answer,
|
|
151
170
|
# reproducible on an untouched main with this same 4-step script.
|
|
171
|
+
#
|
|
172
|
+
# @param instances [Hash{String => Hash}] `history[:instances]`, or an
|
|
173
|
+
# `:instances_at` snapshot from a replayed query entry
|
|
174
|
+
# @param domain [String] the domain name the target aggregate belongs to
|
|
175
|
+
# @param aggregate_name [String] the target aggregate's own declared name
|
|
176
|
+
# @param wheres [Array<QuerySpecification::Common::WhereClause>] the clauses
|
|
177
|
+
# every returned row must satisfy
|
|
178
|
+
# @param args [Hash] the query's own call args, for a clause whose value is a
|
|
179
|
+
# Symbol naming one
|
|
180
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain, keyed
|
|
181
|
+
# by domain name; needed only to resolve a `/` hop clause
|
|
182
|
+
# @return [Array<Hash>] each admitted row's own state, merged with its `id:`
|
|
152
183
|
def query_eligible_rows(instances, domain, aggregate_name, wheres, args, bluebooks: {})
|
|
153
184
|
aggregate = bluebooks[domain]&.aggregate(aggregate_name)
|
|
154
185
|
prefix = "#{domain}::#{aggregate_name}#"
|
|
@@ -166,6 +197,9 @@ module Hecks
|
|
|
166
197
|
end
|
|
167
198
|
end
|
|
168
199
|
|
|
200
|
+
# Resolves one hop of a `/`-chained clause into a local `in` clause against
|
|
201
|
+
# the target aggregate's own ids.
|
|
202
|
+
#
|
|
169
203
|
# `Runtime::ReferenceHop#fold`, independently restated over the
|
|
170
204
|
# replay's own `:instances_at` snapshot instead of live
|
|
171
205
|
# repositories — the same shape every other recompute in this
|
|
@@ -180,6 +214,20 @@ module Hecks
|
|
|
180
214
|
# live fold builds. A clause with no `/`, or one whose head this
|
|
181
215
|
# aggregate's declarations cannot resolve, passes through
|
|
182
216
|
# untouched and evaluates locally as it always did.
|
|
217
|
+
#
|
|
218
|
+
# @param instances [Hash{String => Hash}] `history[:instances]`, or an
|
|
219
|
+
# `:instances_at` snapshot from a replayed query entry
|
|
220
|
+
# @param domain [String] the domain name `aggregate` belongs to
|
|
221
|
+
# @param aggregate [Bluebook::Aggregate, nil] the aggregate `clause` is
|
|
222
|
+
# evaluated against; nil skips hop resolution entirely
|
|
223
|
+
# @param clause [QuerySpecification::Common::WhereClause] the clause to resolve
|
|
224
|
+
# @param args [Hash] the query's own call args, passed through to the inner
|
|
225
|
+
# hop's own `query_eligible_rows` recursion
|
|
226
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain, keyed
|
|
227
|
+
# by domain name
|
|
228
|
+
# @return [QuerySpecification::Common::WhereClause] `clause` unchanged if it has
|
|
229
|
+
# no resolvable hop head; otherwise a local `in` clause over the matching
|
|
230
|
+
# target ids
|
|
183
231
|
def resolve_hop_clause(instances, domain, aggregate, clause, args, bluebooks)
|
|
184
232
|
return clause unless aggregate && QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes)
|
|
185
233
|
|
|
@@ -194,9 +242,15 @@ module Hecks
|
|
|
194
242
|
QuerySpecification::Common::WhereClause.new(field: hop.attribute.name, op: "in", value: ids)
|
|
195
243
|
end
|
|
196
244
|
|
|
245
|
+
# Resolves a declared limit/offset value against the query's own call args.
|
|
246
|
+
#
|
|
197
247
|
# `QueryInterpreter#resolve_query_value`, reproduced: a declared
|
|
198
248
|
# limit/offset is either a literal or a Symbol naming an argument
|
|
199
249
|
# the caller supplied.
|
|
250
|
+
#
|
|
251
|
+
# @param value [Integer, Symbol] the declared limit/offset value
|
|
252
|
+
# @param args [Hash] the query's own call args
|
|
253
|
+
# @return [Object] `value` unchanged if not a Symbol; otherwise `args[value]`
|
|
200
254
|
def resolve_paging_value(value, args)
|
|
201
255
|
value.is_a?(Symbol) ? args[value] : value
|
|
202
256
|
end
|
|
@@ -22,11 +22,15 @@ module Hecks
|
|
|
22
22
|
# any history a valid domain produces, independent of which seed
|
|
23
23
|
# produced it.
|
|
24
24
|
#
|
|
25
|
+
# ## Property shape
|
|
26
|
+
#
|
|
25
27
|
# Each property is `name => ->(history) { true/false, or a message
|
|
26
28
|
# string naming what broke }` — a truthy return (including `true`)
|
|
27
29
|
# is a pass; a String return is a failure, and the string is the
|
|
28
30
|
# finding. `history` is Replay's return shape.
|
|
29
31
|
#
|
|
32
|
+
# ## Feature coverage
|
|
33
|
+
#
|
|
30
34
|
# Every property declares the language feature it covers, in
|
|
31
35
|
# `FEATURE_COVERAGE` below — a "Construct#attribute" pair spelled
|
|
32
36
|
# exactly as `Bluebook::MetaValidator.grammar_registry` names it,
|
|
@@ -161,18 +165,19 @@ module Hecks
|
|
|
161
165
|
# Not a place to hide a real gap — a feature belongs here only once
|
|
162
166
|
# the specific enforcing code path has been read and confirmed, the
|
|
163
167
|
# same discipline `spec/fuzzing/meta_domain_coverage_spec.rb` demands
|
|
164
|
-
# of `KNOWN_GAPS` in the other direction. `Entity#identified_by`
|
|
165
|
-
#
|
|
166
|
-
# `
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
# unconditionally on both branches
|
|
171
|
-
# (caller-supplied, or composite —
|
|
172
|
-
# doesn't cover), the same way
|
|
173
|
-
# check is unconditional for
|
|
174
|
-
#
|
|
175
|
-
#
|
|
168
|
+
# of `KNOWN_GAPS` in the other direction. `Entity#identified_by`
|
|
169
|
+
# illustrates the discipline: `command_interpreter.rb`'s
|
|
170
|
+
# `AlreadyExists` refusal covers every creating aggregate command
|
|
171
|
+
# uniformly, but nothing else on its own covers an entity's own
|
|
172
|
+
# append — so this entry names the path that actually does,
|
|
173
|
+
# `MutationApplier#check_entity_collision` (command_interpreter/
|
|
174
|
+
# mutation_applier.rb), which runs unconditionally on both branches
|
|
175
|
+
# an entity identity can arrive by (caller-supplied, or composite —
|
|
176
|
+
# the two the auto-mint branch doesn't cover), the same way
|
|
177
|
+
# `command_interpreter#hydrate`'s own check is unconditional for
|
|
178
|
+
# every creating aggregate command. The collision this closes is
|
|
179
|
+
# real, not hypothetical: SafeDepositBox's Visit/KeyIssuance entities
|
|
180
|
+
# reproduce it, confirmed by spec/runtime/safe_deposit_box_spec.rb.
|
|
176
181
|
GUARANTEED_BY_CONSTRUCTION = {
|
|
177
182
|
"Aggregate#attributes" => "every field's pattern/closed-set/type passes through Value.build's one coercion " \
|
|
178
183
|
"door (value/coercion.rb#check_patterns, value/admission.rb) before it can exist " \
|
|
@@ -230,9 +235,13 @@ module Hecks
|
|
|
230
235
|
"Member#pairs" => "one level into ValueObject#rows — same door"
|
|
231
236
|
}.freeze
|
|
232
237
|
|
|
233
|
-
#
|
|
234
|
-
# except determinism, which needs to replay twice itself
|
|
235
|
-
# takes the steps directly rather than a single history.
|
|
238
|
+
# Runs the standard property battery over one replayed history —
|
|
239
|
+
# everything except determinism, which needs to replay twice itself
|
|
240
|
+
# and so takes the steps directly rather than a single history.
|
|
241
|
+
#
|
|
242
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
243
|
+
# @return [Hash{Symbol => true, String}] each property name mapped to `true`
|
|
244
|
+
# (passed) or a message string naming what broke
|
|
236
245
|
def check(history)
|
|
237
246
|
{ lifecycle_values_are_declared: lifecycle_values_are_declared(history),
|
|
238
247
|
saga_advances_follow_declared_handlers: saga_advances_follow_declared_handlers(history),
|
|
@@ -83,6 +83,13 @@ module Hecks
|
|
|
83
83
|
DEFAULT_PATH = File.expand_path("../../../qa/settings.yml", __dir__)
|
|
84
84
|
|
|
85
85
|
class << self
|
|
86
|
+
# Loads and validates `qa/settings.yml` (or `path`), returning a frozen instance.
|
|
87
|
+
#
|
|
88
|
+
# @param path [String] path to the YAML settings file; defaults to `DEFAULT_PATH`
|
|
89
|
+
# @return [Hecks::Fuzzing::QaSettings] the validated, frozen settings
|
|
90
|
+
# @raise [ArgumentError] if `path` does not exist, is not valid YAML, is not a
|
|
91
|
+
# YAML mapping at the top level, is missing a required key, declares an
|
|
92
|
+
# unknown key, or gives a value the wrong type for its dial
|
|
86
93
|
def load(path = DEFAULT_PATH)
|
|
87
94
|
raise ArgumentError, "qa settings file not found: #{path}" unless File.file?(path)
|
|
88
95
|
|
|
@@ -97,6 +104,11 @@ module Hecks
|
|
|
97
104
|
end
|
|
98
105
|
end
|
|
99
106
|
|
|
107
|
+
# @param raw [Hash] parsed YAML settings keyed by symbol, one entry per dial in
|
|
108
|
+
# `EXPECTED_TYPES`
|
|
109
|
+
# @param path [String] path to the settings file, used only in error messages
|
|
110
|
+
# @raise [ArgumentError] if `raw` is missing a required key, declares an unknown
|
|
111
|
+
# key, or gives a value the wrong type for its dial
|
|
100
112
|
def initialize(raw, path)
|
|
101
113
|
missing = EXPECTED_TYPES.keys - raw.keys
|
|
102
114
|
raise ArgumentError, "#{path} is missing #{missing.sort.join(', ')}" if missing.any?
|