hecks 1.3.0 → 1.4.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 +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +5 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +23 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +170 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +180 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +37 -18
- data/lib/hecks/fuzzing/form_census.rb +35 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
- data/lib/hecks/fuzzing/properties/guards.rb +42 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +64 -40
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +114 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +141 -112
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
- data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +23 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
- data/lib/hecks/fuzzing/value_generator.rb +14 -14
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +25 -25
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +3 -3
- data/lib/hecks/projections/glossary/markdown.rb +3 -3
- data/lib/hecks/projections/glossary/mermaid.rb +1 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +5 -5
- data/lib/hecks/projections/glossary.rb +15 -15
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +11 -11
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +35 -35
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +151 -67
- data/lib/hecks/runtime/entity_interpreter.rb +108 -82
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +55 -64
- data/lib/hecks/runtime/reaction_invocation.rb +20 -20
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +45 -28
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -153
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +64 -64
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +318 -127
- data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
- data/lib/hecks/runtime/value.rb +21 -21
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +22 -2
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
require_relative "errors"
|
|
2
|
-
require_relative "refusal_wording"
|
|
3
|
-
require_relative "../rendering"
|
|
4
|
-
|
|
5
1
|
module Hecks
|
|
6
2
|
module Runtime
|
|
7
3
|
# The invocation address is not part of a command's domain payload.
|
|
8
4
|
# Aggregate commands carry one receiver identity; entity commands carry
|
|
9
5
|
# the aggregate receiver followed by one identity for every entity hop.
|
|
6
|
+
#
|
|
7
|
+
# Reading a call's shape — what `to:` and `with:` mean, the BUG#7/#17/#18
|
|
8
|
+
# rules — lives in `Runtime::Invocation.from_call` now (invocation.rb).
|
|
9
|
+
# `envelope`/`payload` are kept as thin delegators for any caller still
|
|
10
|
+
# naming them; the dispatcher itself builds an Invocation instead.
|
|
10
11
|
module Routing
|
|
11
12
|
Envelope = Struct.new(:aggregate, :entities, keyword_init: true) do
|
|
12
13
|
def initialize(aggregate:, entities: [])
|
|
@@ -17,158 +18,14 @@ module Hecks
|
|
|
17
18
|
|
|
18
19
|
module_function
|
|
19
20
|
|
|
20
|
-
def envelope(to, entity_depth: 0)
|
|
21
|
-
return nil if to.nil?
|
|
22
|
-
|
|
23
|
-
aggregate, entities = to.is_a?(Hash) ? parse_envelope_hash(to) : scalar_envelope(to)
|
|
24
|
-
|
|
25
|
-
raise TypeMismatch, "to: must name the receiving aggregate identity" if aggregate.nil? || aggregate.to_s.empty?
|
|
26
|
-
if entities.size != entity_depth
|
|
27
|
-
raise TypeMismatch,
|
|
28
|
-
"to: for an entity command needs #{entity_depth} entity " \
|
|
29
|
-
"#{entity_depth == 1 ? 'identity' : 'identities'} after the aggregate — got #{entities.size}"
|
|
30
|
-
end
|
|
31
|
-
raise TypeMismatch, "to: contains a blank entity identity" if entities.any? do |identity|
|
|
32
|
-
identity.nil? || identity.to_s.empty?
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
Envelope.new(aggregate: aggregate, entities: entities)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# BUG#7 — the non-Hash half of `to:` used to accept ANY Ruby object
|
|
39
|
-
# (`[to, []]`, unconditionally) as though it were a ready-made
|
|
40
|
-
# aggregate identity scalar. That is looser than the JSON step
|
|
41
|
-
# boundary (`bin/run`/`Fuzzing::Replay`/`StepBuilder`) ever needs it
|
|
42
|
-
# to be — every legitimate caller already hands this a `String`
|
|
43
|
-
# (`Naming.identity` canonicalizes every identity to one before it
|
|
44
|
-
# ever reaches a `to:`/legacy-args door) — and looser than Rust's own
|
|
45
|
-
# hand-written mirror of this exact boundary
|
|
46
|
-
# (`rust/src/kernel/routing.rs#RoutingEnvelope::from_json`, its own
|
|
47
|
-
# header: "Generated routers accept this shape while retaining the
|
|
48
|
-
# legacy mixed-args object as a compatibility input during
|
|
49
|
-
# migration"), which refuses anything that is neither a JSON string
|
|
50
|
-
# nor object outright, TypeMismatch, before ever reaching a domain's
|
|
51
|
-
# own command payload.
|
|
52
|
-
#
|
|
53
|
-
# The gap surfaced live on `examples/roster` — the first domain in
|
|
54
|
-
# this corpus to declare a command attribute literally named `to`
|
|
55
|
-
# (`Roster::Roster.Mark`, deliberately, per that bluebook's own
|
|
56
|
-
# header comment). `bin/qa_sweep`'s legacy-args dispatch convention
|
|
57
|
-
# (`runtime.dispatch(verb, **symbolize(args))`, `step_builder.rb`)
|
|
58
|
-
# flattens a command's own declared fact and its routing target into
|
|
59
|
-
# ONE Ruby kwargs hash — completely ordinary for every other domain,
|
|
60
|
-
# since Ruby's keyword-argument binding only steals a key that
|
|
61
|
-
# collides with `dispatch`'s own `to:`/`with:`/`saga_correlation:`
|
|
62
|
-
# parameter names. `Mark`'s `to` does collide, so a fuzzer-corrupted
|
|
63
|
-
# scalar offered for it (an out-of-range Integer, from
|
|
64
|
-
# `InvalidValueGenerator.corrupt`) was routed here as the AGGREGATE
|
|
65
|
-
# IDENTITY instead of the domain's own required argument — accepted
|
|
66
|
-
# unconditionally, leaving `Mark`'s own `to` fact absent from the
|
|
67
|
-
# payload entirely. Ruby refused `AbsentArgument` ("Mark was not
|
|
68
|
-
# given to — it takes to"); Rust's stricter `RoutingEnvelope::
|
|
69
|
-
# from_json` refuses the malformed scalar itself, TypeMismatch,
|
|
70
|
-
# before the domain payload is ever examined — the observed
|
|
71
|
-
# divergence. Tightening this branch to Rust's own contract (a
|
|
72
|
-
# scalar `to:` must be a `String`) makes both refuse the same way,
|
|
73
|
-
# for the same reason, at the same step.
|
|
74
|
-
def scalar_envelope(to)
|
|
75
|
-
return [to, []] if to.is_a?(String)
|
|
76
|
-
|
|
77
|
-
raise TypeMismatch, "to: must be a string aggregate identity or an entity route, got #{Rendering.describe(to)}"
|
|
78
|
-
end
|
|
79
|
-
private_class_method :scalar_envelope
|
|
80
|
-
|
|
81
|
-
# The Hash-shaped half of `to:` — pulled out of `envelope` because
|
|
82
|
-
# it is a self-contained parse (raises on an unrecognized key, then
|
|
83
|
-
# returns the pair) with no dependency on anything `envelope` does
|
|
84
|
-
# afterward; the validations that follow apply the same way
|
|
85
|
-
# whichever branch produced `aggregate`/`entities`.
|
|
86
|
-
def parse_envelope_hash(to)
|
|
87
|
-
hash = to.transform_keys(&:to_sym)
|
|
88
|
-
unknown = hash.keys - %i[aggregate entity entities]
|
|
89
|
-
raise TypeMismatch, "to: does not recognize #{unknown.sort.join(', ')}" unless unknown.empty?
|
|
90
|
-
|
|
91
|
-
[hash[:aggregate], entity_identities(hash)]
|
|
92
|
-
end
|
|
93
|
-
private_class_method :parse_envelope_hash
|
|
21
|
+
def envelope(to, entity_depth: 0) = Invocation.route(to, entity_depth: entity_depth)
|
|
94
22
|
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
# choosing the explicit envelope cannot smuggle receiver identity back
|
|
98
|
-
# into the payload.
|
|
23
|
+
# Loaded by invocation.rb (which requires this file), so `Invocation`
|
|
24
|
+
# is always defined by the time either method runs.
|
|
99
25
|
def payload(command, with:, legacy:)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
"dispatch takes command facts in with:, not both with: and loose keyword arguments"
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
return legacy unless with
|
|
106
|
-
raise TypeMismatch, "with: must be a hash of command facts" unless with.is_a?(Hash)
|
|
107
|
-
|
|
108
|
-
offered = with.transform_keys(&:to_sym)
|
|
109
|
-
declared = command.attributes.map { |attribute| attribute.name.to_sym }
|
|
110
|
-
refuse_unknown_facts!(command, offered, declared)
|
|
111
|
-
refuse_absent_facts!(command, offered, declared)
|
|
112
|
-
offered
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# The two `with:` shape checks `payload` runs in sequence — pulled
|
|
116
|
-
# out because each is a self-contained "compute a difference, raise
|
|
117
|
-
# if non-empty" rule with no dependency on the other. Order stays
|
|
118
|
-
# unknown-before-absent, exactly as inline: `payload` calls them in
|
|
119
|
-
# that order, so a fact that is both unknown AND leaves something
|
|
120
|
-
# else absent still raises UnknownArgument first, same as before.
|
|
121
|
-
def refuse_unknown_facts!(command, offered, declared)
|
|
122
|
-
unknown = (offered.keys - declared).sort
|
|
123
|
-
return if unknown.empty?
|
|
124
|
-
|
|
125
|
-
reading = declared.empty? ? "none" : declared.join(", ")
|
|
126
|
-
raise UnknownArgument,
|
|
127
|
-
RefusalWording.render("UnknownArgument", "unknown_args",
|
|
128
|
-
command: command.hecks_name, unknown: unknown.join(", "),
|
|
129
|
-
declared: reading)
|
|
130
|
-
end
|
|
131
|
-
private_class_method :refuse_unknown_facts!
|
|
132
|
-
|
|
133
|
-
def refuse_absent_facts!(command, offered, declared)
|
|
134
|
-
absent = command.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym } - offered.keys
|
|
135
|
-
return if absent.empty?
|
|
136
|
-
|
|
137
|
-
reading = declared.empty? ? "none" : declared.join(", ")
|
|
138
|
-
raise AbsentArgument,
|
|
139
|
-
RefusalWording.render("AbsentArgument", "absent_args",
|
|
140
|
-
command: command.hecks_name, absent: absent.sort.join(", "),
|
|
141
|
-
declared: reading)
|
|
142
|
-
end
|
|
143
|
-
private_class_method :refuse_absent_facts!
|
|
144
|
-
|
|
145
|
-
# BUG#18 — an entity route naming NO entity at all (`entities: []`,
|
|
146
|
-
# or neither `entity:` nor `entities:` present) used to come back
|
|
147
|
-
# `[]` here unremarked, which only ever refused downstream for an
|
|
148
|
-
# ENTITY command (`envelope`'s own `entities.size != entity_depth`
|
|
149
|
-
# check, entity_depth >= 1) — for an AGGREGATE-level command
|
|
150
|
-
# (entity_depth 0), `[].size == 0` trivially satisfied that check,
|
|
151
|
-
# so the degenerate Hash `{aggregate:, entities: []}` reached the
|
|
152
|
-
# command's own validation instead of being refused as malformed
|
|
153
|
-
# routing. Rust's `RoutingEnvelope::from_json` never let it get
|
|
154
|
-
# that far: the Hash branch refuses "entity route requires at
|
|
155
|
-
# least one entity identity" unconditionally, before any
|
|
156
|
-
# entity_depth is known. Refusing it here too, at the same point,
|
|
157
|
-
# closes the gap without touching the scalar branch (a bare
|
|
158
|
-
# aggregate identity, no Hash at all) that every ordinary aggregate
|
|
159
|
-
# dispatch already uses instead of this shape (`Facade::Handle#
|
|
160
|
-
# dispatch`'s own `to: @id`, `CommandRequest`'s own header —
|
|
161
|
-
# "aggregate command: { to: "record-id", ... }" — and, now,
|
|
162
|
-
# `Judge#address`).
|
|
163
|
-
def entity_identities(hash)
|
|
164
|
-
raise TypeMismatch, "to: takes entity: or entities:, not both" if hash.key?(:entities) && hash.key?(:entity)
|
|
165
|
-
|
|
166
|
-
identities = hash.key?(:entities) ? Array(hash[:entities]) : Array(hash[:entity])
|
|
167
|
-
raise TypeMismatch, "to: entity route requires at least one entity identity" if identities.empty?
|
|
168
|
-
|
|
169
|
-
identities
|
|
26
|
+
facts = Invocation.facts_for(command, with: with, legacy: legacy)
|
|
27
|
+
Invocation.new(verb: nil, target: nil, facts: facts).to_args
|
|
170
28
|
end
|
|
171
|
-
private_class_method :entity_identities
|
|
172
29
|
end
|
|
173
30
|
end
|
|
174
31
|
end
|
|
@@ -3,27 +3,27 @@ require_relative "../value"
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Runtime
|
|
5
5
|
class SagaInterpreter
|
|
6
|
-
# How a saga decides
|
|
6
|
+
# How a saga decides which conversation an event belongs to — three
|
|
7
7
|
# tiers, each one a lesson.
|
|
8
8
|
module Correlation
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
-
# A
|
|
11
|
+
# A dotted path names the scalar field, rather than asking a value object
|
|
12
12
|
# to stand in for one. `correlates_by :end_to_end` would key a saga on
|
|
13
13
|
# the whole ExternalTransfer::EndToEndReference — and what a non-scalar
|
|
14
|
-
# correlation key even
|
|
14
|
+
# correlation key even is is representation-dependent (the object
|
|
15
15
|
# itself? its serialised text?). `:"end_to_end.value"` reads the one
|
|
16
16
|
# field with a single unambiguous rendering.
|
|
17
17
|
def saga_correlation(process_manager, event)
|
|
18
18
|
path = process_manager.correlates_by.to_s.split(".")
|
|
19
|
-
# A
|
|
20
|
-
# value object's field out of a
|
|
21
|
-
# `reference`
|
|
19
|
+
# A later event may already hold the scalar. `reference.value` digs a
|
|
20
|
+
# value object's field out of a fresh declaration (TransferRequested's
|
|
21
|
+
# `reference` is a TransferReference) — but a downstream event this
|
|
22
22
|
# same value was smuggled through as a passthrough argument
|
|
23
23
|
# (AccountDebited's `reference:`, resolved by `dispatch_args` to the
|
|
24
24
|
# bare correlation string) carries it as a scalar already, with
|
|
25
25
|
# nothing left to dig. `"xfer-1".respond_to?(:[])` is true — String
|
|
26
|
-
# has its
|
|
26
|
+
# has its own `[]` (substring indexing) — so checking for keyed
|
|
27
27
|
# lookup explicitly, rather than "responds to `[]` at all", is what
|
|
28
28
|
# stops the second segment from being read as a symbol index into a
|
|
29
29
|
# string that has already arrived.
|
|
@@ -32,9 +32,9 @@ module Hecks
|
|
|
32
32
|
end
|
|
33
33
|
return value unless value.to_s.empty?
|
|
34
34
|
|
|
35
|
-
#
|
|
35
|
+
# **The stamp** — `deliver_saga_dispatch` marks its own event before this
|
|
36
36
|
# saga's next step ever asks, for a leg whose command declares
|
|
37
|
-
#
|
|
37
|
+
# neither the correlation field itself nor the emitting aggregate's
|
|
38
38
|
# own reference key (the two tiers above). command_interpreter/
|
|
39
39
|
# argument_gate.rb names the old payload-only lookup "the weakest
|
|
40
40
|
# part of the gate" : a correlation key arriving on a command only
|
|
@@ -48,17 +48,17 @@ module Hecks
|
|
|
48
48
|
stamped = event.correlation && event.correlation[process_manager.correlation_head.to_s]
|
|
49
49
|
return stamped unless stamped.nil? || stamped.to_s.empty?
|
|
50
50
|
|
|
51
|
-
# A
|
|
51
|
+
# A self-referencing leg carries the correlation forward under its
|
|
52
52
|
# own emitting record's identity — `event.id`, not a field dug back
|
|
53
53
|
# out of the payload. This used to read `event.payload[own_key]`
|
|
54
54
|
# (`own_key` the aggregate's own reference-key convention, "wire",
|
|
55
|
-
# "transfer"), which only ever held a value because
|
|
55
|
+
# "transfer"), which only ever held a value because legacy dispatch
|
|
56
56
|
# left the self-addressing key riding along in the payload
|
|
57
57
|
# unfiltered. Routing separated from payload (`to:`/`with:`, the
|
|
58
58
|
# facade's own `Handle#run` always uses it) closed exactly that
|
|
59
59
|
# leak — correctly, since an addressing key is not a fact the
|
|
60
60
|
# payload should carry — which left this tier reading an empty
|
|
61
|
-
# Hash for any self-referencing leg with no
|
|
61
|
+
# Hash for any self-referencing leg with no other declared
|
|
62
62
|
# attributes (`OnboardingCase.Clear`, `.Decline` — no `attribute`
|
|
63
63
|
# lines at all): the saga silently stopped advancing, forever, for
|
|
64
64
|
# exactly the leg this tier exists to correlate.
|
|
@@ -68,14 +68,14 @@ module Hecks
|
|
|
68
68
|
# and unlike a payload dig it is populated by the record itself,
|
|
69
69
|
# not by which dispatch convention the caller happened to use.
|
|
70
70
|
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
71
|
+
# Gated, still — a manually-dispatched command on a wholly
|
|
72
|
+
# unrelated aggregate can share an event name this process_manager happens to
|
|
73
73
|
# handle (`Drawer.Take` also emits "Taken", the same name a
|
|
74
|
-
#
|
|
74
|
+
# saga-dispatched leg uses) with nothing this saga should read as
|
|
75
75
|
# its own conversation. What makes a leg genuinely
|
|
76
76
|
# self-referencing — the one fact worth trusting `event.id`
|
|
77
|
-
# for — is that `correlates_by`'s own head field
|
|
78
|
-
#
|
|
77
|
+
# for — is that `correlates_by`'s own head field is this event's
|
|
78
|
+
# own aggregate's declared identity, not merely a same-shaped
|
|
79
79
|
# name: `OnboardingCase.identity_heads` really does include
|
|
80
80
|
# `:reference`, `correlates_by :"reference.value"`'s own head ;
|
|
81
81
|
# `Drawer.identity_heads` is `[:number]`, nowhere close.
|
|
@@ -49,26 +49,26 @@ module Hecks
|
|
|
49
49
|
|
|
50
50
|
private
|
|
51
51
|
|
|
52
|
-
#
|
|
53
|
-
# holds `saga_mutex` across
|
|
52
|
+
# The checkpoint write, shared by every mutation site below —
|
|
53
|
+
# holds `saga_mutex` across both the in-memory Hash mutation and
|
|
54
54
|
# the persistence write (§7), not just the Hash mutation alone:
|
|
55
|
-
# two threads racing the
|
|
55
|
+
# two threads racing the same (process_manager, correlation) key
|
|
56
56
|
# could otherwise interleave their writes out of order, silently
|
|
57
57
|
# reordering a saga's own transition history — worse for the
|
|
58
58
|
# adapters with no locking of their own (Heki) than for Postgres.
|
|
59
59
|
# `deep_copy` guards against the exact shape of bug PR #175 itself
|
|
60
60
|
# already found once (over-freezing a live, still-mutated Hash) —
|
|
61
|
-
# never hand a persistence adapter the
|
|
61
|
+
# never hand a persistence adapter the same object `advance_saga`/
|
|
62
62
|
# `unwind` go on to mutate in place; round-tripping through JSON
|
|
63
63
|
# is also what guarantees the value is safe for every adapter that
|
|
64
64
|
# itself calls `JSON.generate` on it.
|
|
65
65
|
# `pending:` — see saga_pending_dispatch.rb. Injected into the
|
|
66
|
-
#
|
|
66
|
+
# written copy of memory only, never into `instance[:memory]`
|
|
67
67
|
# itself: every other reader of a live instance's memory
|
|
68
68
|
# (`dispatch_args`'s "opening event memory" scope, the fuzzer's
|
|
69
69
|
# own round-trip/shape checks, `saga_spec.rb`'s exact-equality
|
|
70
70
|
# assertion against a fresh instance's seeded memory) sees exactly
|
|
71
|
-
# what it always did. The marker exists
|
|
71
|
+
# what it always did. The marker exists only in the persisted
|
|
72
72
|
# blob, and only for as long as a dispatch cascade is genuinely
|
|
73
73
|
# in flight for this instance.
|
|
74
74
|
def checkpoint(process_manager, correlation, instance, domain, pending: nil)
|
|
@@ -82,8 +82,8 @@ module Hecks
|
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
# `deep_copy` is `JSON.parse(JSON.generate(hash), ...)`, which
|
|
85
|
-
# only accepts an
|
|
86
|
-
# is an
|
|
85
|
+
# only accepts an object at the top level — `completed_compensations`
|
|
86
|
+
# is an array, so it gets its own wrap-and-unwrap rather than a
|
|
87
87
|
# second, parallel `deep_copy_array` reimplementing the same
|
|
88
88
|
# round-trip. `|| []` — an instance from before this field existed
|
|
89
89
|
# (or one that has never completed a compensable leg) rehydrates
|
|
@@ -105,15 +105,15 @@ module Hecks
|
|
|
105
105
|
created = @registry.saga_mutex.synchronize do
|
|
106
106
|
next false if @registry.saga_instances[process_manager.name].key?(correlation)
|
|
107
107
|
|
|
108
|
-
# `.dup`,
|
|
109
|
-
# as a
|
|
108
|
+
# `.dup`, not the same object — a fresh saga's own memory starts
|
|
109
|
+
# as a copy of the starting event's own payload, never the
|
|
110
110
|
# payload itself. A saga's own memory is meant to be written
|
|
111
111
|
# into over its lifetime (remember-style, growing beyond what
|
|
112
112
|
# the starting event carried) ; the payload it was seeded from
|
|
113
|
-
# is a fact about something that
|
|
113
|
+
# is a fact about something that already happened, logged and
|
|
114
114
|
# emitted before the saga ever saw it. Sharing the one Hash
|
|
115
115
|
# object between them means a write into the saga's own memory
|
|
116
|
-
# is silently
|
|
116
|
+
# is silently also a write into an already-emitted event's own
|
|
117
117
|
# payload — retroactively adding a field nothing announced.
|
|
118
118
|
# `event.payload` is deep-frozen by `Event#emit!` by the time
|
|
119
119
|
# this runs, so a naive in-place write here would raise
|
|
@@ -132,11 +132,11 @@ module Hecks
|
|
|
132
132
|
instance: correlation, born: true, state: process_manager.states.first }
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
-
#
|
|
136
|
-
#
|
|
135
|
+
# The mutex covers only the state-check-and-mutate-and-checkpoint
|
|
136
|
+
# step, never the dispatch cascade that follows — `deliver_saga_
|
|
137
137
|
# dispatch` calls `@door.reenter`, which can recursively re-enter
|
|
138
|
-
#
|
|
139
|
-
# or itself again) on the
|
|
138
|
+
# this same interpreter (a saga's own leg triggering another saga,
|
|
139
|
+
# or itself again) on the same thread, and `Mutex` is not
|
|
140
140
|
# reentrant: holding it across that call would deadlock the
|
|
141
141
|
# thread against itself the moment any real chain did that.
|
|
142
142
|
def advance_saga(process_manager, event, domain)
|
|
@@ -156,7 +156,7 @@ module Hecks
|
|
|
156
156
|
@registry.saga_log << record.merge(advanced: false, reason: "no conversation remembers #{correlation.inspect}")
|
|
157
157
|
next false
|
|
158
158
|
end
|
|
159
|
-
#
|
|
159
|
+
# The leg is chosen by (event, current state) — C10.3. Read
|
|
160
160
|
# under the mutex, against the state this instance holds right
|
|
161
161
|
# now, so two legs on the same event from different states each
|
|
162
162
|
# answer exactly when their own state is current.
|
|
@@ -189,7 +189,7 @@ module Hecks
|
|
|
189
189
|
{ on: event.name, from: from_state, to: to_state, dispatches: handler.dispatches.map(&:command_name) }
|
|
190
190
|
end
|
|
191
191
|
|
|
192
|
-
#
|
|
192
|
+
# The shared tail of `advance_saga` and `unwind` — both are "guard,
|
|
193
193
|
# mutate, checkpoint-with-pending" under the mutex (kept separate
|
|
194
194
|
# per caller: `advance_saga`'s own guard also has to handle "no
|
|
195
195
|
# instance at all", `unwind`'s doesn't), then this: log the real
|
|
@@ -197,11 +197,11 @@ module Hecks
|
|
|
197
197
|
# pending marker once that cascade — however it ended — is done.
|
|
198
198
|
def settle_transition(process_manager, event, handler, instance, correlation, domain, record, pre_state,
|
|
199
199
|
drain_compensations: false)
|
|
200
|
-
# `from:`/`to:` are the
|
|
200
|
+
# `from:`/`to:` are the instance's own real pre/post state — read
|
|
201
201
|
# back from `instance` itself, never re-derived from `handler.
|
|
202
202
|
# from_state`/`handler.to_state` a second time. `Properties.saga_
|
|
203
203
|
# advances_follow_declared_handlers` (fuzzing/properties.rb) builds
|
|
204
|
-
# its
|
|
204
|
+
# its own "declared edges" list from this same handler object (via
|
|
205
205
|
# `process_manager.handlers`), so a log entry that just echoed `handler.
|
|
206
206
|
# from_state`/`handler.to_state` back could never disagree with
|
|
207
207
|
# that list no matter what the runtime actually did — the entry
|
|
@@ -213,10 +213,10 @@ module Hecks
|
|
|
213
213
|
# real mismatch instead of vanishing into a tautology.
|
|
214
214
|
@registry.saga_log << record.merge(advanced: true, from: pre_state, to: instance[:state])
|
|
215
215
|
|
|
216
|
-
#
|
|
217
|
-
# own call (`drain_compensations: true`): every leg
|
|
216
|
+
# **Derived compensation first, newest-first** — only for `unwind`'s
|
|
217
|
+
# own call (`drain_compensations: true`): every leg this instance
|
|
218
218
|
# actually completed that declared its own `compensates`, popped
|
|
219
|
-
# and dispatched in reverse completion order,
|
|
219
|
+
# and dispatched in reverse completion order, before any
|
|
220
220
|
# hand-written `on :refused` dispatches below — coexistence, not
|
|
221
221
|
# replacement. Drained (not just read) as it fires: a saga's own
|
|
222
222
|
# `on :refused` handler is guarded against re-entry by `unwind`'s
|
|
@@ -234,19 +234,19 @@ module Hecks
|
|
|
234
234
|
deliver_saga_dispatch(process_manager, spec, event, instance, correlation, domain)
|
|
235
235
|
end
|
|
236
236
|
|
|
237
|
-
#
|
|
237
|
+
# **The clear** — guarded by the same identity check `end_saga`'s own
|
|
238
238
|
# `.delete` return value implies: `deliver_saga_dispatch`'s
|
|
239
|
-
# `@door.reenter` can synchronously trigger this
|
|
239
|
+
# `@door.reenter` can synchronously trigger this same correlation's
|
|
240
240
|
# `ends_on` event as a nested reaction (a leg's own dispatch is
|
|
241
241
|
# what makes the saga's terminal event fire), which deletes this
|
|
242
242
|
# row from the store before this line ever runs. Writing the
|
|
243
|
-
# clear unconditionally would
|
|
243
|
+
# clear unconditionally would resurrect a legitimately-ended saga
|
|
244
244
|
# — this diff's own first attempt did exactly that, caught by
|
|
245
245
|
# `saga_durability_spec.rb`'s "deletes the checkpoint once a saga
|
|
246
246
|
# genuinely ends" — so this only re-checkpoints when `instance`
|
|
247
|
-
# is still
|
|
247
|
+
# is still the same object `@saga_instances` holds for this
|
|
248
248
|
# correlation (`.equal?`, not `==`: a fresh saga reborn under the
|
|
249
|
-
# same correlation between then and now is a
|
|
249
|
+
# same correlation between then and now is a different instance,
|
|
250
250
|
# and writing this stale one's state onto that one's row would be
|
|
251
251
|
# its own corruption). Under the mutex — dispatching is over by
|
|
252
252
|
# now, so this is not the reentrancy hazard `advance_saga`'s own
|
|
@@ -264,9 +264,9 @@ module Hecks
|
|
|
264
264
|
args = dispatch_args(process_manager, spec, event, instance, correlation)
|
|
265
265
|
record = { process_manager: process_manager.name, instance: correlation, dispatch: spec.command_name }
|
|
266
266
|
|
|
267
|
-
#
|
|
267
|
+
# The raw inputs `args` was resolved from, captured alongside the
|
|
268
268
|
# result — never re-derived from history[:saga_instances] later
|
|
269
|
-
# (that only ever holds the
|
|
269
|
+
# (that only ever holds the final memory, after every step has
|
|
270
270
|
# run; this dispatch's own memory, at the moment it actually
|
|
271
271
|
# fired, is a different fact for a saga whose memory keeps
|
|
272
272
|
# changing). `spec.with_spec.empty?` skipped: nothing declared
|
|
@@ -282,7 +282,7 @@ module Hecks
|
|
|
282
282
|
end
|
|
283
283
|
|
|
284
284
|
if @door.reaction_depth_reached?
|
|
285
|
-
#
|
|
285
|
+
# The ceiling is not a domain decision either — same reasoning as a
|
|
286
286
|
# crash, below — but unlike a crash there is nothing ambiguous
|
|
287
287
|
# about it: the leg unambiguously did not run, so it unwinds
|
|
288
288
|
# exactly like a refusal instead of stranding the instance for a
|
|
@@ -301,18 +301,18 @@ module Hecks
|
|
|
301
301
|
attempt = 0
|
|
302
302
|
compensation_recorded = false
|
|
303
303
|
begin
|
|
304
|
-
#
|
|
305
|
-
# returns — `@door.reenter` can recursively
|
|
306
|
-
# saga interpreter (the event
|
|
307
|
-
#
|
|
308
|
-
#
|
|
309
|
-
# "after reenter succeeds" would be too late for a
|
|
304
|
+
# Recorded before dispatching, not after `@door.reenter`
|
|
305
|
+
# returns — `@door.reenter` can recursively re-enter this same
|
|
306
|
+
# saga interpreter (the event this dispatch emits triggers a
|
|
307
|
+
# later handler, which can itself refuse and unwind) entirely
|
|
308
|
+
# within this one call, before it ever returns here. Recording
|
|
309
|
+
# "after reenter succeeds" would be too late for a nested
|
|
310
310
|
# refusal to ever see this leg's own compensation — found
|
|
311
311
|
# live: Settlement's own AccountDebited handler refuses
|
|
312
|
-
# Account.Credit and unwinds from
|
|
312
|
+
# Account.Credit and unwinds from inside Account.Debit's own
|
|
313
313
|
# `reenter` call, so "delivered: true, then record" left the
|
|
314
314
|
# ledger empty at the exact moment it was needed. Popped back
|
|
315
|
-
# off in the rescues below if
|
|
315
|
+
# off in the rescues below if this leg's own attempt is the
|
|
316
316
|
# one that failed — never left recorded for a refusal that
|
|
317
317
|
# was never this leg's own to compensate.
|
|
318
318
|
if spec.compensates && !compensation_recorded
|
|
@@ -336,7 +336,7 @@ module Hecks
|
|
|
336
336
|
rescue *DOMAIN_REFUSALS => e
|
|
337
337
|
unrecord_compensation(instance, correlation, domain, process_manager) if compensation_recorded
|
|
338
338
|
# Same rule as the policy interpreter : a refusal by the target is
|
|
339
|
-
# a recorded outcome, and the leg that raised it
|
|
339
|
+
# a recorded outcome, and the leg that raised it unwinds — see
|
|
340
340
|
# `unwind`'s own comment for why the procedure runs its
|
|
341
341
|
# compensation here rather than leaving the money (or whatever
|
|
342
342
|
# else a leg moved) sitting out.
|
|
@@ -345,13 +345,13 @@ module Hecks
|
|
|
345
345
|
rescue StandardError => e
|
|
346
346
|
unrecord_compensation(instance, correlation, domain, process_manager) if compensation_recorded
|
|
347
347
|
compensation_recorded = false
|
|
348
|
-
# A
|
|
348
|
+
# A defect, not a refusal — see PolicyInterpreter#deliver's own
|
|
349
349
|
# comment for the full reasoning: the same DOMAIN_REFUSALS split,
|
|
350
350
|
# and the same "the triggering command already succeeded and
|
|
351
351
|
# persisted by the time this runs" fact that makes catching it
|
|
352
352
|
# here safe rather than reckless.
|
|
353
353
|
#
|
|
354
|
-
#
|
|
354
|
+
# Unlike a refusal, a crash is not a decision the domain made, so
|
|
355
355
|
# it does not unwind on the first failure — MAX_DEFECT_RETRIES
|
|
356
356
|
# gives a transient failure (a DB timeout, a race, a cold start)
|
|
357
357
|
# a chance to clear on its own, retrying the identical dispatch,
|
|
@@ -381,36 +381,36 @@ module Hecks
|
|
|
381
381
|
end
|
|
382
382
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
383
383
|
|
|
384
|
-
#
|
|
384
|
+
# The rollback half of `deliver_saga_dispatch`'s own speculative
|
|
385
385
|
# pre-record (that method's own comment for why it has to be
|
|
386
|
-
# speculative) —
|
|
386
|
+
# speculative) — this leg's own attempt is the one that failed,
|
|
387
387
|
# so whatever was just pushed for it was never actually earned.
|
|
388
388
|
# `.pop`, not a search-and-delete: nothing else can have pushed
|
|
389
|
-
#
|
|
389
|
+
# after this leg's own entry without this leg's own `@door.
|
|
390
390
|
# reenter` call having already returned (the recursive re-entry
|
|
391
|
-
# this whole mechanism exists for only ever runs
|
|
391
|
+
# this whole mechanism exists for only ever runs between this
|
|
392
392
|
# push and this leg's own return, and a nested refusal that
|
|
393
393
|
# consumed it already popped it itself — this rollback only ever
|
|
394
|
-
# runs for
|
|
394
|
+
# runs for this leg's own, still-present entry).
|
|
395
395
|
def unrecord_compensation(instance, correlation, domain, process_manager)
|
|
396
396
|
instance[:completed_compensations].pop
|
|
397
397
|
checkpoint(process_manager, correlation, instance, domain)
|
|
398
398
|
end
|
|
399
399
|
|
|
400
|
-
# A refused leg
|
|
400
|
+
# A refused leg unwinds — the procedure runs the leg declared `on :refused`,
|
|
401
401
|
# which is where the compensation lives. So does a leg that hit the
|
|
402
402
|
# reaction-depth ceiling, and so does a leg that crashed and stayed
|
|
403
403
|
# crashing through MAX_DEFECT_RETRIES — see `deliver_saga_dispatch`'s own
|
|
404
404
|
# comments for why each of those is safe to route here.
|
|
405
405
|
#
|
|
406
|
-
# Until this existed a refusal was
|
|
406
|
+
# Until this existed a refusal was recorded and nothing else happened. The
|
|
407
407
|
# wire's thousand was taken from the source, refused by the destination, and
|
|
408
408
|
# sat nowhere until a human dispatched the reversal by hand ; banking's
|
|
409
409
|
# settlement left a debit standing with no credit and no compensation at all.
|
|
410
410
|
# Both bluebooks had written the compensating leg. Nothing armed it.
|
|
411
411
|
#
|
|
412
|
-
# A compensation that is itself refused does
|
|
413
|
-
# flag to stop it: the state moves to the compensating leg's to_state
|
|
412
|
+
# A compensation that is itself refused does not unwind again, and needs no
|
|
413
|
+
# flag to stop it: the state moves to the compensating leg's to_state before
|
|
414
414
|
# its dispatches run, so a second refusal finds the instance no longer in
|
|
415
415
|
# from_state and records that instead. The check is the guard.
|
|
416
416
|
def unwind(process_manager, event, instance, correlation, domain)
|
|
@@ -442,7 +442,7 @@ module Hecks
|
|
|
442
442
|
|
|
443
443
|
# See `settle_transition`'s own comment on `pre_state`/
|
|
444
444
|
# `instance[:state]` — the real observed transition, not a
|
|
445
|
-
# second read of the
|
|
445
|
+
# second read of the same handler object `Properties.saga_
|
|
446
446
|
# advances_follow_declared_handlers` checks this log against.
|
|
447
447
|
# `drain_compensations: true` — only `unwind`'s own call site
|
|
448
448
|
# fires derived compensation; `advance_saga`'s own call never
|
|
@@ -451,16 +451,16 @@ module Hecks
|
|
|
451
451
|
drain_compensations: true)
|
|
452
452
|
end
|
|
453
453
|
|
|
454
|
-
# A
|
|
454
|
+
# A derived compensation — `entry[:args]` is already resolved
|
|
455
455
|
# (`record_completed_compensation`'s own comment for why), so this
|
|
456
456
|
# skips `dispatch_args` entirely and goes straight to delivery,
|
|
457
|
-
# through the
|
|
457
|
+
# through the same retry-on-defect path an ordinary forward leg
|
|
458
458
|
# uses. Never re-enters `unwind` on its own failure — a
|
|
459
459
|
# compensation that itself refuses is a real, pre-existing gap
|
|
460
460
|
# this feature makes visible rather than closes (see this file's
|
|
461
461
|
# own class-level notes); `compensation_failed: true` tags it
|
|
462
462
|
# distinctly in the log instead of recording it identically to an
|
|
463
|
-
# ordinary failed delivery, and every
|
|
463
|
+
# ordinary failed delivery, and every other completed compensation
|
|
464
464
|
# still queued still gets its own attempt.
|
|
465
465
|
def deliver_derived_compensation(process_manager, entry, correlation, domain)
|
|
466
466
|
record = { process_manager: process_manager.name, instance: correlation, dispatch: entry[:command_name] }
|
|
@@ -506,40 +506,40 @@ module Hecks
|
|
|
506
506
|
)
|
|
507
507
|
end
|
|
508
508
|
|
|
509
|
-
# BUG#6 —
|
|
509
|
+
# BUG#6 — unconditionally the saga's own home domain, never inferred
|
|
510
510
|
# from `command_name`'s own shape. This used to guess: a leftover
|
|
511
511
|
# `::` after `Naming.command_ref`'s own rewrite was read as "already
|
|
512
512
|
# domain-qualified" and left alone. That heuristic cannot actually
|
|
513
513
|
# tell a genuinely cross-domain reference (`Banking::Account::
|
|
514
|
-
# Debit` -> one `::` survives) apart from a
|
|
514
|
+
# Debit` -> one `::` survives) apart from a same-domain entity
|
|
515
515
|
# command reference (`Manifest::Slot::Fill` -> one `::` survives
|
|
516
516
|
# too, for an unrelated reason — entity nesting, not a domain
|
|
517
517
|
# qualifier) — both collapse to the identical "one `::` left" shape,
|
|
518
518
|
# and the string alone carries no further signal to split them
|
|
519
519
|
# (confirmed against `Naming.command_ref`'s own rewrite: it only
|
|
520
|
-
# ever strips the
|
|
520
|
+
# ever strips the last `::`, so the count of what remains is blind
|
|
521
521
|
# to why it's there). Picking the cross-domain reading unconditionally
|
|
522
522
|
# left `qa/stress_domains/waybill`'s own `Packing` saga dispatching
|
|
523
|
-
# `Manifest::Slot::Fill` — an entity command in its
|
|
523
|
+
# `Manifest::Slot::Fill` — an entity command in its own domain —
|
|
524
524
|
# unprefixed, so `Naming.split_verb` read "Manifest" as a domain
|
|
525
525
|
# name instead of this chapter's own aggregate, and the dispatch
|
|
526
526
|
# failed with `UnknownVerb`, silently recorded as an ordinary
|
|
527
527
|
# domain refusal rather than surfacing as the real bug it is.
|
|
528
528
|
#
|
|
529
|
-
#
|
|
529
|
+
# The fix mirrors `PolicyInterpreter#deliver`'s own mechanism,
|
|
530
530
|
# which never had this bug: a policy's cross-domain target is a
|
|
531
|
-
#
|
|
531
|
+
# separate, explicit field (`Policy#target_domain`, set only by the
|
|
532
532
|
# `across` keyword) — `deliver` unconditionally builds
|
|
533
533
|
# `"#{policy.target_domain || domain}::#{policy.trigger_command}"`,
|
|
534
|
-
# never asking whether `trigger_command`
|
|
534
|
+
# never asking whether `trigger_command` looks already-qualified.
|
|
535
535
|
# A saga's own `dispatch`/`compensates` has no such explicit field
|
|
536
|
-
# and no keyword to set one — and, confirmed against the
|
|
536
|
+
# and no keyword to set one — and, confirmed against the entire
|
|
537
537
|
# corpus (banking's Onboarding/Settlement/ExternalSettlement,
|
|
538
538
|
# quality_control's BugCiWatch, and this domain's own Packing),
|
|
539
539
|
# no saga anywhere ever dispatches genuinely cross-domain: "every
|
|
540
540
|
# command a saga fires lands inside its own bluebook chapter"
|
|
541
541
|
# (`Projections::Diagrams#saga_diagram`'s own comment, written
|
|
542
|
-
# independently of this fix and still true). So the home domain
|
|
542
|
+
# independently of this fix and still true). So the home domain is
|
|
543
543
|
# the only explicit context a saga dispatch ever has — this applies
|
|
544
544
|
# it the same way `deliver` applies its own default (no `across`)
|
|
545
545
|
# case, without inventing a keyword nothing in the corpus needs.
|