hecks 1.3.0 → 1.5.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 +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- 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 +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -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 +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- 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/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 +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -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 +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- 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 +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- 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 +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- 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 +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- 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 +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- 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 +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -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 +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- 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 +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- 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/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -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 +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- 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 +34 -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 +95 -6
- 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 +48 -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 +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -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 +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -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 +61 -15
- 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 +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- 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 +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
|
@@ -6,61 +6,64 @@ require_relative "../runtime/saga_interpreter"
|
|
|
6
6
|
|
|
7
7
|
module Hecks
|
|
8
8
|
module Fuzzing
|
|
9
|
-
# A
|
|
10
|
-
#
|
|
9
|
+
# **A second comparison axis** — every other check this practice runs is
|
|
10
|
+
# differential: Ruby vs the compiled Rust conformance binary, on the
|
|
11
11
|
# same generated sequence (`bin/qa_sweep`'s own `diff_ruby_vs_rust`,
|
|
12
12
|
# `Properties.check`). Differential comparison structurally cannot
|
|
13
|
-
# catch a bug where both engines are wrong the
|
|
13
|
+
# catch a bug where both engines are wrong the same way, or where a
|
|
14
14
|
# single engine is self-inconsistent with nothing to differentially
|
|
15
|
-
# compare it against. This module asks a
|
|
16
|
-
# engine at a time: does it agree with
|
|
15
|
+
# compare it against. This module asks a different question of one
|
|
16
|
+
# engine at a time: does it agree with itself?
|
|
17
17
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
18
|
+
# ## Three checks
|
|
19
|
+
#
|
|
20
|
+
# Run on the same already-generated sequence and its resulting live
|
|
21
|
+
# state — no second fuzzing pass, no re-dispatch through the command
|
|
22
|
+
# layer:
|
|
21
23
|
#
|
|
22
24
|
# 1. `check_rehydration` — does reloading an aggregate from its
|
|
23
25
|
# own durable journal reproduce exactly the state a live dispatch
|
|
24
26
|
# already produced?
|
|
25
|
-
# 2. `check_idempotency` — does replaying that
|
|
27
|
+
# 2. `check_idempotency` — does replaying that same journal a
|
|
26
28
|
# second time change anything? (a variant of #1, but a different
|
|
27
29
|
# failure mode: no leaked state between applications, not just
|
|
28
30
|
# "cold load works once.")
|
|
29
31
|
# 3. `check_value_object_round_trip` — does every value object the
|
|
30
32
|
# sequence actually built survive `to_json` then rebuilt back?
|
|
31
33
|
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# `
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
34
|
+
# ## The rehydration path, found, not guessed
|
|
35
|
+
#
|
|
36
|
+
# hecks is not event-sourced at the aggregate level — there is no
|
|
37
|
+
# `AccountOpened`-shaped log a `CommandInterpreter` folds to rebuild
|
|
38
|
+
# state. What there is, real and already shipping, is
|
|
39
|
+
# `Ports::Persistence::AppendOnly` (lib/hecks/ports/persistence/
|
|
40
|
+
# append_only.rb): every adapter accepts the same `Entry` stream
|
|
41
|
+
# (`operation`, `id`, the full state after that command — not a
|
|
42
|
+
# delta) and answers `#entries`; `#recover!` — "an append is durable
|
|
43
|
+
# before a projection is attempted; replaying the log restores a
|
|
44
|
+
# snapshot/table after a crash in that small window" — is called on
|
|
45
|
+
# every repository this runtime ever builds (`RepositoryFactory
|
|
46
|
+
# .build`'s own `recover: true` default). That is the production
|
|
47
|
+
# cold-rehydration path. Reusing `#recover!` directly against the
|
|
48
|
+
# live adapter would prove nothing, though: `Fuzzing::Replay` runs
|
|
49
|
+
# against `Adapters::Memory` (`IsolatedBoot`'s own default), and
|
|
50
|
+
# Memory's own `Entry#state` is a shallow `instance.state.dup` — the
|
|
51
|
+
# exact same `Runtime::Value` objects a command produced ride along
|
|
52
|
+
# unchanged, so folding them straight back through Memory's own
|
|
53
|
+
# `#project` is a tautology that can never fail (`Value#for_attribute`
|
|
54
|
+
# 's own `value.is_a?(self) && value.type_name == ...` branch passes
|
|
55
|
+
# an already-typed value straight through, no rebuilding at all).
|
|
53
56
|
#
|
|
54
|
-
# So `
|
|
55
|
-
#
|
|
57
|
+
# So `fold!`, below, feeds the same entries through `Adapters::Heki`
|
|
58
|
+
# instead — a real, already-shipped, disk-backed adapter
|
|
56
59
|
# (examples/banking's own `persisted_by("Heki")`), in a throwaway
|
|
57
60
|
# directory. Writing forces every value through `JSON.generate`
|
|
58
61
|
# (Heki's own journal line, its own compressed snapshot); reading
|
|
59
|
-
# back through a
|
|
62
|
+
# back through a fresh `Heki` instance (unmemoized `@store`) forces
|
|
60
63
|
# `read_snapshot`/`replay_journal` — real `JSON.parse`, real
|
|
61
64
|
# `Zlib::Inflate`, real bytes off a real filesystem — which is what
|
|
62
65
|
# makes `Instance.hydrate_with_defaults` → `Value.hydrate` →
|
|
63
|
-
# `Value.for_attribute` actually
|
|
66
|
+
# `Value.for_attribute` actually rebuild every value object from raw
|
|
64
67
|
# data via `Value.build`, the same coercion/validation path a real
|
|
65
68
|
# restart takes, rather than pass the live object through unchanged.
|
|
66
69
|
# This is the exact mechanism `AppendOnly#recover!` names in its own
|
|
@@ -70,15 +73,36 @@ module Hecks
|
|
|
70
73
|
module SelfConsistency
|
|
71
74
|
module_function
|
|
72
75
|
|
|
73
|
-
#
|
|
76
|
+
# Every Heki this pass opens is guarded like a runtime repository
|
|
77
|
+
# (Phase 2, Track A, PR A4). These adapters are built directly, not
|
|
78
|
+
# through `RepositoryFactory.build`, so without this a cold read here
|
|
79
|
+
# could hand `Runtime::Instance` undecoded state and the codec's
|
|
80
|
+
# guarantee would have a bypass in exactly the check that exists to
|
|
81
|
+
# compare stored state against live state.
|
|
82
|
+
#
|
|
83
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate this store holds
|
|
84
|
+
# @param settings [Hash] adapter settings, forwarded to `Adapters::Heki.new`
|
|
85
|
+
# @param root [String, nil] the directory this store reads/writes under
|
|
86
|
+
# @return [Adapters::Heki] a fresh, codec-boundary-guarded Heki instance
|
|
87
|
+
def guarded_heki(**) = Ports::Persistence::CodecBoundary.guard!(Adapters::Heki.new(**))
|
|
88
|
+
|
|
89
|
+
# The whole pass — called once, with the runtime still live (inside
|
|
74
90
|
# `Replay.call`'s own `IsolatedBoot.call` block, before the tmp
|
|
75
91
|
# directory and its adapters go out of scope) and the `history`
|
|
76
92
|
# `Replay.call` is about to return. Nothing here boots a second
|
|
77
93
|
# runtime or dispatches a single command; every check below reads
|
|
78
|
-
# data this
|
|
94
|
+
# data this one replay already produced. Just the three checks
|
|
79
95
|
# below, run and collected — kept as three independently callable
|
|
80
96
|
# methods (not fused into one shared fold) so a spec proving one
|
|
81
97
|
# check can fire never has to reason about the other two.
|
|
98
|
+
#
|
|
99
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
100
|
+
# booted runtime this replay just dispatched through
|
|
101
|
+
# @param history [Hash] the replayed history `Replay.call` is about to return
|
|
102
|
+
# @return [Hash{Symbol => Array<Hash>}] the five checks' own findings, keyed
|
|
103
|
+
# `:rehydration`, `:idempotency`, `:value_object_round_trip`,
|
|
104
|
+
# `:saga_rehydration`, `:saga_redelivery_idempotency` — each value an empty
|
|
105
|
+
# Array when that check found nothing
|
|
82
106
|
def check(runtime, history)
|
|
83
107
|
{ rehydration: check_rehydration(runtime), idempotency: check_idempotency(runtime),
|
|
84
108
|
value_object_round_trip: check_value_object_round_trip(history),
|
|
@@ -86,12 +110,18 @@ module Hecks
|
|
|
86
110
|
saga_redelivery_idempotency: check_saga_idempotency(runtime, history) }
|
|
87
111
|
end
|
|
88
112
|
|
|
89
|
-
#
|
|
113
|
+
# Check 1 — rehydrate-from-journal == live state.
|
|
114
|
+
#
|
|
115
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
116
|
+
# booted runtime this replay just dispatched through
|
|
117
|
+
# @return [Array<Hash>] one divergence Hash (`:field`, `:domain`, `:aggregate`,
|
|
118
|
+
# `:live`, `:rehydrated`) per aggregate whose cold-read state disagrees with its
|
|
119
|
+
# live state; empty when every touched aggregate rehydrates cleanly
|
|
90
120
|
def check_rehydration(runtime)
|
|
91
121
|
each_touched_repository(runtime).filter_map do |domain_name, aggregate, repository, entries|
|
|
92
122
|
live = snapshot(repository)
|
|
93
123
|
Dir.mktmpdir("hecks-self-consistency") do |tmp|
|
|
94
|
-
writer =
|
|
124
|
+
writer = guarded_heki(aggregate: aggregate, root: tmp)
|
|
95
125
|
rehydrated = fold!(writer, tmp, aggregate, entries)
|
|
96
126
|
next if rehydrated == live
|
|
97
127
|
|
|
@@ -101,16 +131,22 @@ module Hecks
|
|
|
101
131
|
end
|
|
102
132
|
end
|
|
103
133
|
|
|
104
|
-
#
|
|
105
|
-
#
|
|
134
|
+
# Check 2 — replay idempotency: folding the same entries into the
|
|
135
|
+
# same durable store a second time must change nothing. A variant
|
|
106
136
|
# of check 1, worth stating separately — this catches a
|
|
107
137
|
# replay-specific bug (leaked state between applications, a
|
|
108
138
|
# double-applied effect) that a single, one-shot cold read could
|
|
109
139
|
# never see, even one that already agrees with live state.
|
|
140
|
+
#
|
|
141
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
142
|
+
# booted runtime this replay just dispatched through
|
|
143
|
+
# @return [Array<Hash>] one divergence Hash (`:field`, `:domain`, `:aggregate`,
|
|
144
|
+
# `:once`, `:twice`) per aggregate whose second fold disagrees with its first;
|
|
145
|
+
# empty when every touched aggregate folds idempotently
|
|
110
146
|
def check_idempotency(runtime)
|
|
111
147
|
each_touched_repository(runtime).filter_map do |domain_name, aggregate, repository, entries|
|
|
112
148
|
Dir.mktmpdir("hecks-self-consistency") do |tmp|
|
|
113
|
-
writer =
|
|
149
|
+
writer = guarded_heki(aggregate: aggregate, root: tmp)
|
|
114
150
|
once = fold!(writer, tmp, aggregate, entries)
|
|
115
151
|
twice = fold!(writer, tmp, aggregate, entries)
|
|
116
152
|
next if once == twice
|
|
@@ -121,9 +157,9 @@ module Hecks
|
|
|
121
157
|
end
|
|
122
158
|
end
|
|
123
159
|
|
|
124
|
-
#
|
|
160
|
+
# Check 3 — every `Runtime::Value` the sequence actually built
|
|
125
161
|
# (walked out of the replay's own `instances`/`events`/`queries`,
|
|
126
|
-
# never a hand-picked example), round-tripped through the
|
|
162
|
+
# never a hand-picked example), round-tripped through the real
|
|
127
163
|
# serialize/deserialize pair: `Value#to_json` (JSON.generate(to_h),
|
|
128
164
|
# value.rb) out, `Value.build` (value/coercion.rb — the same
|
|
129
165
|
# constructor a command argument's own raw JSON goes through) back
|
|
@@ -132,16 +168,16 @@ module Hecks
|
|
|
132
168
|
# `from_json` per closed set/value object); `Value.build` is the
|
|
133
169
|
# actual Ruby door a raw, untyped Hash becomes a validated,
|
|
134
170
|
# admitted, invariant-checked value object through.
|
|
135
|
-
# `aggregate:`
|
|
171
|
+
# `aggregate:` threaded alongside every value found, not dropped —
|
|
136
172
|
# `Value.build(value_object, fields, aggregate)`'s third argument is
|
|
137
|
-
# what lets `normalize_composite_fields` resolve a
|
|
173
|
+
# what lets `normalize_composite_fields` resolve a nested composite
|
|
138
174
|
# field's own type by name (`value_object_for(aggregate, type)`).
|
|
139
175
|
# Building with `aggregate: nil` (this method's first version, live-
|
|
140
176
|
# tested against `examples/pizzas` while this was being written)
|
|
141
177
|
# silently skips that step entirely — `Pizza`'s own `price_cents`/
|
|
142
|
-
# `size` fields round-tripped back as bare,
|
|
178
|
+
# `size` fields round-tripped back as bare, string-keyed Hashes
|
|
143
179
|
# instead of rebuilt `Money`/`PizzaSize` value objects, a false
|
|
144
|
-
#
|
|
180
|
+
# positive this check would have reported as a real bug on every
|
|
145
181
|
# single sweep. Resolved from `history[:instances]`' own key
|
|
146
182
|
# (`"Domain::Aggregate#id"`, `Replay#snapshot_instances`) and
|
|
147
183
|
# `history[:events]`' own `event[:aggregate]` (`"Domain::Aggregate"`,
|
|
@@ -150,10 +186,16 @@ module Hecks
|
|
|
150
186
|
# off `history` rather than a second lookup. `history[:queries]`'
|
|
151
187
|
# own rows have no single owning aggregate reliably named on the
|
|
152
188
|
# entry itself (a cross-aggregate read model, a `for_each` target),
|
|
153
|
-
# so they are left
|
|
189
|
+
# so they are left out of this walk rather than risk the same false
|
|
154
190
|
# positive `nil` already produced once — `instances` and `events`
|
|
155
191
|
# alone already reach every value object a generated sequence
|
|
156
192
|
# actually persisted or announced.
|
|
193
|
+
#
|
|
194
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
195
|
+
# @return [Array<Hash>] one divergence Hash (`:field`, `:type`, `:original`, and
|
|
196
|
+
# either `:rehydrated` or `:error`) per value object whose `to_json`-then-`build`
|
|
197
|
+
# round trip disagrees with (or raises on) the original; empty when every value
|
|
198
|
+
# object found round-trips cleanly
|
|
157
199
|
def check_value_object_round_trip(history)
|
|
158
200
|
bluebooks = history[:bluebooks] || {}
|
|
159
201
|
seen = {}.compare_by_identity
|
|
@@ -186,12 +228,12 @@ module Hecks
|
|
|
186
228
|
end
|
|
187
229
|
end
|
|
188
230
|
|
|
189
|
-
#
|
|
190
|
-
# read an
|
|
191
|
-
# ever exercised the
|
|
231
|
+
# Check 4 — saga cold-rehydration (angle-10). Checks 1/2 above cold-
|
|
232
|
+
# read an aggregate's own journal through Heki; nothing in this file
|
|
233
|
+
# ever exercised the other durable store `SagaInterpreter#checkpoint`
|
|
192
234
|
# writes through — `Ports::Persistence::NullSagaStore`'s own header
|
|
193
235
|
# calls Heki's `SagaStore` (`adapters/driven/heki/saga_store.rb`) the
|
|
194
|
-
#
|
|
236
|
+
# optional saga-persistence capability, and it is real and already
|
|
195
237
|
# shipping, just never fuzzed: `Registry#rehydrate_sagas!` — the
|
|
196
238
|
# production "process just restarted" path — folds exactly what
|
|
197
239
|
# `each_saga` yields back into `@saga_instances`, and until now
|
|
@@ -201,32 +243,40 @@ module Hecks
|
|
|
201
243
|
# rather than folding it into checks 1/2's own aggregate walk.
|
|
202
244
|
#
|
|
203
245
|
# `history[:saga_instances]` (`replay.rb`'s own `saga_instances`
|
|
204
|
-
# local, built once at the very end of a replay) is the
|
|
246
|
+
# local, built once at the very end of a replay) is the same
|
|
205
247
|
# materialized `{pm_name => {correlation => {state:, memory:}}}`
|
|
206
248
|
# shape `SagaInterpreter#checkpoint` itself hands a real adapter —
|
|
207
249
|
# read from `history`, not re-derived from the (by-now-live, already
|
|
208
250
|
# mutated by whatever `check_saga_idempotency` ran first, see that
|
|
209
251
|
# method's own header) `runtime.registry.saga_instances`. Written
|
|
210
|
-
# through a
|
|
252
|
+
# through a real `Adapters::Heki` (a throwaway tmpdir, one per
|
|
211
253
|
# process manager so two process managers with correlations that
|
|
212
254
|
# happen to collide as strings never share a store), read back
|
|
213
|
-
# through a
|
|
255
|
+
# through a fresh instance (unmemoized `@store`/`@saga_store`, same
|
|
214
256
|
# reason `fold!` above uses one) — forcing the identical
|
|
215
257
|
# `JSON.generate`/`JSON.parse` boundary a real crash-then-restart
|
|
216
258
|
# takes, not a live-object pass-through.
|
|
217
259
|
#
|
|
218
|
-
#
|
|
260
|
+
# One finding per (domain, process manager) — every correlation this
|
|
219
261
|
# process manager's own `history[:saga_instances]` entry holds,
|
|
220
262
|
# compared as a whole Hash — the same aggregate-granularity (not
|
|
221
263
|
# per-record) `check_rehydration` already reports at.
|
|
222
264
|
#
|
|
223
|
-
# `completed_compensations` is
|
|
265
|
+
# `completed_compensations` is deliberately out of scope — `history[
|
|
224
266
|
# :saga_instances]` never captures it (`replay.rb`'s own comment:
|
|
225
267
|
# only `state`/`memory` are threaded through, since a saga's
|
|
226
268
|
# in-flight compensation ledger is a fact about a leg still running,
|
|
227
269
|
# not the settled snapshot this history exists to describe), so
|
|
228
270
|
# there is no ground truth to compare it against here. Written as an
|
|
229
271
|
# empty array on the way in and never read back on the way out.
|
|
272
|
+
#
|
|
273
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
274
|
+
# booted runtime this replay just dispatched through
|
|
275
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
276
|
+
# @return [Array<Hash>] one divergence Hash (`:field`, `:domain`,
|
|
277
|
+
# `:process_manager`, `:live`, `:rehydrated`) per process manager whose cold-read
|
|
278
|
+
# saga instances disagree with the ones this replay persisted; empty when every
|
|
279
|
+
# process manager with persisted instances rehydrates cleanly
|
|
230
280
|
def check_saga_rehydration(runtime, history)
|
|
231
281
|
saga_instances = history[:saga_instances] || {}
|
|
232
282
|
each_domain_process_manager(runtime).filter_map do |domain_name, process_manager|
|
|
@@ -237,7 +287,7 @@ module Hecks
|
|
|
237
287
|
next unless anchor
|
|
238
288
|
|
|
239
289
|
Dir.mktmpdir("hecks-self-consistency-saga") do |tmp|
|
|
240
|
-
writer =
|
|
290
|
+
writer = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
|
|
241
291
|
persisted.each do |correlation, saga|
|
|
242
292
|
writer.save_saga(process_manager: process_manager.name, correlation: correlation.to_s,
|
|
243
293
|
state: saga[:state], memory: saga[:memory], completed_compensations: [])
|
|
@@ -253,21 +303,21 @@ module Hecks
|
|
|
253
303
|
end
|
|
254
304
|
end
|
|
255
305
|
|
|
256
|
-
#
|
|
306
|
+
# Check 5 — redelivery idempotency of the checkpoint-then-load path.
|
|
257
307
|
# `check_saga_rehydration` above proves cold-reading a checkpoint
|
|
258
|
-
# reproduces the same
|
|
308
|
+
# reproduces the same data; this proves the other half of a real
|
|
259
309
|
# crash/restart — a message an at-least-once delivery mechanism (an
|
|
260
310
|
# outbox redrive, a queue redelivery) hands the rehydrated saga a
|
|
261
|
-
#
|
|
311
|
+
# second time — does not silently re-advance it. There is no flag
|
|
262
312
|
# for this in production (`SagaInterpreter#unwind`'s own comment:
|
|
263
313
|
# "the check is the guard") — the (event, current state) lookup
|
|
264
|
-
# `handler_for` performs is the
|
|
314
|
+
# `handler_for` performs is the entire mechanism, and it has never
|
|
265
315
|
# been exercised against a state this practice loaded from cold
|
|
266
316
|
# storage rather than one still sitting in a live process's memory.
|
|
267
317
|
#
|
|
268
|
-
#
|
|
318
|
+
# One (process manager, correlation) tested, using a fresh
|
|
269
319
|
# `Runtime::SagaInterpreter` sharing `runtime`'s own `registry` and
|
|
270
|
-
# `door: runtime` — the identical two objects the
|
|
320
|
+
# `door: runtime` — the identical two objects the dispatcher's own
|
|
271
321
|
# `@sagas` was built from (`dispatcher.rb`'s own `SagaInterpreter.
|
|
272
322
|
# new(registry, door: self)`) — not a hand-rolled re-implementation
|
|
273
323
|
# of `advance_saga`'s own state-guard. `only: process_manager` scopes
|
|
@@ -275,13 +325,13 @@ module Hecks
|
|
|
275
325
|
# keyword the outbox relay already uses to run one consumer alone
|
|
276
326
|
# (`Runtime::Outbox::Relay#run_consumer`).
|
|
277
327
|
#
|
|
278
|
-
#
|
|
328
|
+
# Which event to redeliver — `runtime.registry.saga_log`'s own last
|
|
279
329
|
# `advanced: true` row for this (process manager, correlation) names
|
|
280
|
-
# the event
|
|
330
|
+
# the event by name only; the real `Runtime::Event` object (payload,
|
|
281
331
|
# aggregate, id, `correlation` — everything `saga_correlation`/
|
|
282
332
|
# `dispatch_args` actually read) lives in `runtime.events`, still
|
|
283
|
-
# live for exactly this reason (this file's own header: "runtime
|
|
284
|
-
#
|
|
333
|
+
# live for exactly this reason (this file's own header: "runtime is
|
|
334
|
+
# still live here"). Matched back by name plus `saga_correlation`
|
|
285
335
|
# itself (`Runtime::SagaInterpreter::Correlation`, `private`) —
|
|
286
336
|
# reused via `send` rather than reproduced, because reproducing its
|
|
287
337
|
# three-tier fallback (a dotted payload field, a stamped passthrough,
|
|
@@ -291,25 +341,25 @@ module Hecks
|
|
|
291
341
|
# its own `saga_log` row's `on:` is the synthetic `REFUSED` trigger
|
|
292
342
|
# name, never a real domain event, so there is nothing to redeliver.
|
|
293
343
|
#
|
|
294
|
-
#
|
|
295
|
-
# `saga_instances[pm][correlation]` slot is overwritten,
|
|
296
|
-
# with whatever a cold Heki read of the
|
|
344
|
+
# Simulating "just restarted" — the live registry's own in-memory
|
|
345
|
+
# `saga_instances[pm][correlation]` slot is overwritten, in place,
|
|
346
|
+
# with whatever a cold Heki read of the same checkpoint answers
|
|
297
347
|
# (exactly what `Registry#rehydrate_sagas!` does for real on every
|
|
298
348
|
# boot), the redelivery is driven through the real interpreter, and
|
|
299
349
|
# the slot is put back — `ensure`d — once this correlation's own
|
|
300
|
-
# check is done. Safe
|
|
350
|
+
# check is done. Safe only because `check`/`Replay.call` run this,
|
|
301
351
|
# synchronously, single-threaded, as the very last thing before
|
|
302
352
|
# `runtime` and its whole tmp directory go out of scope for good;
|
|
303
|
-
# nothing downstream of this method ever reads the
|
|
353
|
+
# nothing downstream of this method ever reads the live registry
|
|
304
354
|
# again (`check_saga_rehydration`, `check_rehydration`, `check_
|
|
305
355
|
# idempotency`, `check_value_object_round_trip` all read `history`'s
|
|
306
356
|
# own frozen snapshot instead, never `runtime.registry` — so calling
|
|
307
357
|
# order relative to this method's own mutation doesn't matter).
|
|
308
358
|
#
|
|
309
|
-
#
|
|
310
|
-
# — a leg whose own `from:`/`to:` are the
|
|
359
|
+
# The assertion is about `state`/`memory`, not "did a dispatch fire"
|
|
360
|
+
# — a leg whose own `from:`/`to:` are the same state (every existing
|
|
311
361
|
# saga's own starts_on self-transition, `waybill.bluebook`'s own leg
|
|
312
|
-
# 1/2) is
|
|
362
|
+
# 1/2) is expected to re-run on redelivery with no visible state
|
|
313
363
|
# change at all; that is a property of the declared handler graph,
|
|
314
364
|
# not a rehydration defect, and asserting against it here would
|
|
315
365
|
# manufacture a false positive on every saga this corpus has. A
|
|
@@ -317,6 +367,14 @@ module Hecks
|
|
|
317
367
|
# the redelivered event name (the ordinary, expected case once a
|
|
318
368
|
# saga has moved past the leg that produced its own current
|
|
319
369
|
# checkpoint) is exactly what this proves stays put.
|
|
370
|
+
#
|
|
371
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
372
|
+
# booted runtime this replay just dispatched through
|
|
373
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
374
|
+
# @return [Array<Hash>] one divergence Hash (`:field`, `:domain`,
|
|
375
|
+
# `:process_manager`, `:correlation`, `:on`, `:before`, `:after`) per correlation
|
|
376
|
+
# whose redelivered event visibly changed state/memory on a rehydrated
|
|
377
|
+
# checkpoint; empty when every redelivered correlation stayed put
|
|
320
378
|
def check_saga_idempotency(runtime, history)
|
|
321
379
|
saga_instances = history[:saga_instances] || {}
|
|
322
380
|
interpreter = Runtime::SagaInterpreter.new(runtime.registry, door: runtime)
|
|
@@ -340,18 +398,18 @@ module Hecks
|
|
|
340
398
|
|
|
341
399
|
# ── Rust-side self-consistency ───────────────────────────────────
|
|
342
400
|
#
|
|
343
|
-
#
|
|
344
|
-
# `kernel/cli.rs`'s `run` accepts an
|
|
345
|
-
# ("the exact 'Domain::Aggregate#id' -> state shape
|
|
346
|
-
# 'instances' output already produces... lets a
|
|
401
|
+
# The compiled binary's own rehydration door, already shipping —
|
|
402
|
+
# `kernel/cli.rs`'s `run` accepts an optional top-level `"seed"` key
|
|
403
|
+
# ("the exact 'Domain::Aggregate#id' -> state shape this run's own
|
|
404
|
+
# 'instances' output already produces... lets a host seed prior
|
|
347
405
|
# state back in instead of replaying `steps` from scratch every
|
|
348
406
|
# invocation"). `Store::from_seed`/`Store::instances` are that
|
|
349
407
|
# mechanism's own two halves — `rust/host` (docs/implemented/
|
|
350
408
|
# decisions/0012) depends on them being true inverses for real,
|
|
351
|
-
# today. `rust_seed_round_trip` exercises exactly that: a
|
|
352
|
-
# independent invocation of the
|
|
409
|
+
# today. `rust_seed_round_trip` exercises exactly that: a second,
|
|
410
|
+
# independent invocation of the same binary, `"steps": []` (nothing
|
|
353
411
|
# new dispatched — this is rehydration, not re-dispatch), seeded
|
|
354
|
-
# with whatever `"instances"` a
|
|
412
|
+
# with whatever `"instances"` a prior invocation already produced.
|
|
355
413
|
# `differ` (unused, kept in the signature — see below) is a
|
|
356
414
|
# `RustConformanceHelpers`-including instance (`bin/qa_sweep`'s own
|
|
357
415
|
# `Differ`), the same argument `check_rust_rehydration`/`check_
|
|
@@ -359,27 +417,33 @@ module Hecks
|
|
|
359
417
|
# rather than dropped from all three signatures at once so a
|
|
360
418
|
# future differential-style reduction has a door already open,
|
|
361
419
|
# without this module ever `require`ing `spec/support/` itself.
|
|
362
|
-
#
|
|
363
|
-
#
|
|
420
|
+
# Neither `strip_emitted_flags!` nor any other differential-only
|
|
421
|
+
# reduction runs here — that reduction exists so a Rust-only
|
|
364
422
|
# bookkeeping field (`emitted_<event>`, docs/decisions/0049) never
|
|
365
423
|
# counts against Ruby, which has no equivalent field to agree with
|
|
366
424
|
# at all (`RustConformanceHelpers#strip_emitted_flags!`'s own
|
|
367
425
|
# comment). This check has no Ruby side to spare — it is asking the
|
|
368
|
-
# Rust binary whether it agrees with
|
|
426
|
+
# Rust binary whether it agrees with itself, so `emitted_*` fields
|
|
369
427
|
# are exactly as real a fact to compare as any other. Stripping them
|
|
370
|
-
# here
|
|
371
|
-
#
|
|
372
|
-
#
|
|
373
|
-
#
|
|
374
|
-
#
|
|
375
|
-
#
|
|
376
|
-
#
|
|
377
|
-
#
|
|
378
|
-
#
|
|
379
|
-
#
|
|
380
|
-
#
|
|
381
|
-
#
|
|
382
|
-
#
|
|
428
|
+
# here would silently delete them from the returned seed-round-trip
|
|
429
|
+
# result while leaving them present on the original `seed_instances`
|
|
430
|
+
# a caller passes in — an asymmetric comparison that would report
|
|
431
|
+
# every record carrying one as a rehydration divergence,
|
|
432
|
+
# unconditionally, on every domain that has one at all. Confirmed
|
|
433
|
+
# live against `examples/banking` (`Banking::Account`'s own
|
|
434
|
+
# `corrects` reaction) by comparing this method's own answer against
|
|
435
|
+
# the compiled binary's raw stdout for the identical seed call: the
|
|
436
|
+
# raw round trip preserves `emitted_fee_applied` correctly. `spec/
|
|
437
|
+
# self_consistency_rust_spec.rb`'s own "banking" example pins this
|
|
438
|
+
# against a real domain going forward.
|
|
439
|
+
#
|
|
440
|
+
# @param binary [String] path to the compiled Rust conformance binary
|
|
441
|
+
# @param _differ [Object] a `RustConformanceHelpers`-including instance, unused here
|
|
442
|
+
# @param seed_instances [Hash] the `"instances"` shape to seed the fresh invocation
|
|
443
|
+
# with, in place of replaying `steps`
|
|
444
|
+
# @return [Hash] the fresh invocation's own `"instances"`, or a
|
|
445
|
+
# `{"__self_consistency_error__" => String}` Hash if the binary exited nonzero or
|
|
446
|
+
# the binary itself reported an `"error"`
|
|
383
447
|
def rust_seed_round_trip(binary, _differ, seed_instances)
|
|
384
448
|
stdout, status = Open3.capture2(binary, stdin_data: JSON.generate({ "steps" => [], "seed" => seed_instances }))
|
|
385
449
|
return { "__self_consistency_error__" => "rust binary exited #{status.exitstatus}: #{stdout}" } \
|
|
@@ -391,11 +455,18 @@ module Hecks
|
|
|
391
455
|
parsed["instances"]
|
|
392
456
|
end
|
|
393
457
|
|
|
394
|
-
#
|
|
458
|
+
# Check 1, Rust side — seeding a fresh invocation with a prior
|
|
395
459
|
# invocation's own live `"instances"` must reproduce that same
|
|
396
460
|
# state, unchanged. `live_instances` is `rust_output["instances"]`
|
|
397
461
|
# — the exact same value `bin/qa_sweep`'s own differential compare
|
|
398
462
|
# already diffed against Ruby, reused here rather than re-derived.
|
|
463
|
+
#
|
|
464
|
+
# @param binary [String] path to the compiled Rust conformance binary
|
|
465
|
+
# @param differ [Object] a `RustConformanceHelpers`-including instance, forwarded to
|
|
466
|
+
# `rust_seed_round_trip` unused
|
|
467
|
+
# @param live_instances [Hash] the prior invocation's own `"instances"` output
|
|
468
|
+
# @return [Array<Hash>] a single divergence Hash (`:field`, `:live`, `:rehydrated`)
|
|
469
|
+
# if seeding with `live_instances` fails to reproduce it; empty otherwise
|
|
399
470
|
def check_rust_rehydration(binary, differ, live_instances)
|
|
400
471
|
rehydrated = rust_seed_round_trip(binary, differ, live_instances)
|
|
401
472
|
return [] if rehydrated == live_instances
|
|
@@ -403,11 +474,19 @@ module Hecks
|
|
|
403
474
|
[{ field: "rust_rehydration", live: live_instances, rehydrated: rehydrated }]
|
|
404
475
|
end
|
|
405
476
|
|
|
406
|
-
#
|
|
407
|
-
# already produced, a
|
|
477
|
+
# Check 2, Rust side — seeding with what a first seed round trip
|
|
478
|
+
# already produced, a second time, must not drift any further. The
|
|
408
479
|
# same "replay it again, byte for byte" claim `check_idempotency`
|
|
409
480
|
# proves for Ruby, aimed at the one rehydration door this compiled
|
|
410
481
|
# binary actually has.
|
|
482
|
+
#
|
|
483
|
+
# @param binary [String] path to the compiled Rust conformance binary
|
|
484
|
+
# @param differ [Object] a `RustConformanceHelpers`-including instance, forwarded to
|
|
485
|
+
# `rust_seed_round_trip` unused
|
|
486
|
+
# @param live_instances [Hash] the prior invocation's own `"instances"` output to
|
|
487
|
+
# seed the first round trip with
|
|
488
|
+
# @return [Array<Hash>] a single divergence Hash (`:field`, `:once`, `:twice`) if a
|
|
489
|
+
# second seed round trip disagrees with the first; empty otherwise
|
|
411
490
|
def check_rust_idempotency(binary, differ, live_instances)
|
|
412
491
|
once = rust_seed_round_trip(binary, differ, live_instances)
|
|
413
492
|
twice = rust_seed_round_trip(binary, differ, once)
|
|
@@ -418,12 +497,18 @@ module Hecks
|
|
|
418
497
|
|
|
419
498
|
# ── shared plumbing ─────────────────────────────────────────────
|
|
420
499
|
|
|
421
|
-
#
|
|
500
|
+
# Every [domain, aggregate] pair this sequence actually wrote to —
|
|
422
501
|
# an aggregate with an empty `#entries` never had anything dispatch
|
|
423
502
|
# against it this run, so there is nothing to rehydrate and no
|
|
424
503
|
# finding a "clean, nothing touched" report would mean anything
|
|
425
504
|
# for. Mirrors `Replay#snapshot_instances`' own
|
|
426
505
|
# `bluebooks.each { aggregates.each { repository(...) } }` walk.
|
|
506
|
+
#
|
|
507
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
508
|
+
# booted runtime this replay just dispatched through
|
|
509
|
+
# @return [Array<Array(String, Bluebook::Aggregate, Ports::Persistence::AppendOnly,
|
|
510
|
+
# Array<Ports::Persistence::Entry>)>] one `[domain_name, aggregate, repository,
|
|
511
|
+
# entries]` tuple per aggregate with at least one journal entry
|
|
427
512
|
def each_touched_repository(runtime)
|
|
428
513
|
found = []
|
|
429
514
|
runtime.registry.bluebooks.each do |domain_name, bluebook|
|
|
@@ -438,21 +523,32 @@ module Hecks
|
|
|
438
523
|
found
|
|
439
524
|
end
|
|
440
525
|
|
|
526
|
+
# Materializes a repository's own stored records, keyed by id, for comparing
|
|
527
|
+
# against a cold read of the same aggregate.
|
|
528
|
+
#
|
|
529
|
+
# @param repository [Ports::Persistence::AppendOnly] the repository to read
|
|
530
|
+
# @return [Hash{String => Object}] every stored record's id (stringified) mapped to
|
|
531
|
+
# its materialized state
|
|
441
532
|
def snapshot(repository)
|
|
442
533
|
repository.all.to_h { |record| [record.id.to_s, Runtime::Value.materialize(record.state)] }
|
|
443
534
|
end
|
|
444
535
|
|
|
445
|
-
#
|
|
536
|
+
# Every [domain, process manager] pair any loaded bluebook declares —
|
|
446
537
|
# regardless of whether this replay's own `history[:saga_instances]`
|
|
447
538
|
# ever touched it (mirrors `each_touched_repository`'s own walk one
|
|
448
539
|
# level up; the "did anything actually persist" filter lives in each
|
|
449
540
|
# check's own caller, same as that method's `entries.empty?` guard).
|
|
450
|
-
# An empty return here
|
|
541
|
+
# An empty return here is the "domain declares no process manager"
|
|
451
542
|
# skip `check_saga_rehydration`/`check_saga_idempotency` both need —
|
|
452
543
|
# `filter_map`/`flat_map` over an empty Array already answers `[]`,
|
|
453
544
|
# identical to "ran and found nothing," which is deliberate: neither
|
|
454
545
|
# check has a positive "passed" artifact to report either way (see
|
|
455
546
|
# this file's own header on why silence is never a claimed pass).
|
|
547
|
+
#
|
|
548
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
549
|
+
# booted runtime this replay just dispatched through
|
|
550
|
+
# @return [Array<Array(String, Bluebook::ProcessManager)>] one `[domain_name,
|
|
551
|
+
# process_manager]` tuple per process manager any loaded bluebook declares
|
|
456
552
|
def each_domain_process_manager(runtime)
|
|
457
553
|
found = []
|
|
458
554
|
runtime.registry.bluebooks.each do |domain_name, bluebook|
|
|
@@ -461,50 +557,67 @@ module Hecks
|
|
|
461
557
|
found
|
|
462
558
|
end
|
|
463
559
|
|
|
464
|
-
#
|
|
560
|
+
# The live-side ground truth, key-shape-normalized (see `deep_
|
|
465
561
|
# stringify_keys`'s own comment) so it compares fairly against a
|
|
466
562
|
# real Heki round trip's own shallow-symbolize convention.
|
|
563
|
+
#
|
|
564
|
+
# @param persisted [Hash{String, Symbol => Hash}] `history[:saga_instances]`'s own
|
|
565
|
+
# per-process-manager entry, keyed by correlation, each holding `:state`/`:memory`
|
|
566
|
+
# @return [Hash{String => Hash}] the same rows, keyed by stringified correlation,
|
|
567
|
+
# with `:memory` recursively string-keyed
|
|
467
568
|
def normalize_saga_rows(persisted)
|
|
468
569
|
persisted.each_with_object({}) do |(correlation, saga), rows|
|
|
469
570
|
rows[correlation.to_s] = { state: saga[:state], memory: deep_stringify_keys(saga[:memory]) }
|
|
470
571
|
end
|
|
471
572
|
end
|
|
472
573
|
|
|
473
|
-
# A
|
|
574
|
+
# A fresh `Adapters::Heki` at the same `tmp`/`domain` — unmemoized
|
|
474
575
|
# `@store`/`@saga_store`, so `#each_saga` is forced back through
|
|
475
576
|
# `read_snapshot`/`replay_journal`, real bytes off real disk, not
|
|
476
577
|
# whatever the writer that just wrote them still holds in its own
|
|
477
|
-
# process memory (the same reason `fold!`,
|
|
578
|
+
# process memory (the same reason `fold!`, below, opens a second
|
|
478
579
|
# `Adapters::Heki` instance rather than reading its own writer back).
|
|
580
|
+
#
|
|
581
|
+
# @param anchor [Bluebook::Aggregate] any aggregate of the owning domain, used only
|
|
582
|
+
# to open a `Heki` store at the right domain/root
|
|
583
|
+
# @param tmp [String] the throwaway directory the saga store was written into
|
|
584
|
+
# @param domain_name [String] the owning domain's name, threaded into the store's
|
|
585
|
+
# own `settings`
|
|
586
|
+
# @return [Hash{String => Hash}] every persisted correlation's `:state`/`:memory`,
|
|
587
|
+
# with `:memory` recursively string-keyed
|
|
479
588
|
def cold_read_saga_rows(anchor, tmp, domain_name)
|
|
480
|
-
reader =
|
|
589
|
+
reader = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
|
|
481
590
|
reader.each_saga.with_object({}) do |(_pm, correlation, state, memory, _completed), rows|
|
|
482
591
|
rows[correlation] = { state: state, memory: deep_stringify_keys(memory) }
|
|
483
592
|
end
|
|
484
593
|
end
|
|
485
594
|
|
|
486
|
-
# `SagaStore#each_saga`
|
|
487
|
-
#
|
|
595
|
+
# `SagaStore#each_saga` only ever symbolizes `memory`'s own top-level
|
|
596
|
+
# keys (`heki/saga_store.rb`'s own `each_saga`, one level deep) —
|
|
488
597
|
# `Registry::SagaPersistence#warn_stalled_saga` already documents
|
|
489
598
|
# this exact asymmetry for the one reserved key production code
|
|
490
|
-
# cares about (`SAGA_PENDING_DISPATCH_KEY`). A
|
|
599
|
+
# cares about (`SAGA_PENDING_DISPATCH_KEY`). A nested composite
|
|
491
600
|
# memory field (any saga whose starting event carries a value
|
|
492
601
|
# object, which is most of them — `waybill.bluebook`'s own
|
|
493
|
-
# `ConsignmentRequested` alone has three) comes back with
|
|
494
|
-
# keys at every level
|
|
495
|
-
# 's own `Runtime::Value.materialize` call produces
|
|
602
|
+
# `ConsignmentRequested` alone has three) comes back with string
|
|
603
|
+
# keys at every level below the top, while `history[:saga_instances]`
|
|
604
|
+
# 's own `Runtime::Value.materialize` call produces symbol keys
|
|
496
605
|
# throughout. That asymmetry is Heki's own documented, accepted
|
|
497
606
|
# storage convention — an "opaque, adapter-agnostic JSON blob"
|
|
498
607
|
# (`SagaInterpreter#checkpoint`'s own comment), never a typed
|
|
499
|
-
# rebuild the way an
|
|
608
|
+
# rebuild the way an aggregate's own composite fields get on cold
|
|
500
609
|
# read (this file's own header: there is no VO schema to rebuild
|
|
501
610
|
# against for a saga's memory blob at all) — not a rehydration
|
|
502
611
|
# defect this check exists to find. Recursively re-stringifying
|
|
503
|
-
#
|
|
504
|
-
# shape difference apart from an
|
|
612
|
+
# both sides before comparing is what tells that known, accepted
|
|
613
|
+
# shape difference apart from an actual data-loss bug (a dropped
|
|
505
614
|
# key, a changed value, a missing field) — exactly the kind (b)'s
|
|
506
615
|
# own seeded fixture in `spec/fuzzing/self_consistency_saga_spec.rb`
|
|
507
616
|
# proves this still catches.
|
|
617
|
+
#
|
|
618
|
+
# @param value [Object] any value; only Hash and Array are recursed into
|
|
619
|
+
# @return [Object] `value` with every Hash key (at every nesting level) replaced by
|
|
620
|
+
# its own `to_s`; anything else is returned unchanged
|
|
508
621
|
def deep_stringify_keys(value)
|
|
509
622
|
case value
|
|
510
623
|
when Hash then value.each_with_object({}) { |(k, v), h| h[k.to_s] = deep_stringify_keys(v) }
|
|
@@ -513,7 +626,7 @@ module Hecks
|
|
|
513
626
|
end
|
|
514
627
|
end
|
|
515
628
|
|
|
516
|
-
#
|
|
629
|
+
# The real, already-announced event this correlation's current
|
|
517
630
|
# checkpoint came from — walked back out of `runtime.registry.
|
|
518
631
|
# saga_log`'s own `advanced: true` rows (newest first), skipping the
|
|
519
632
|
# synthetic `REFUSED` trigger (`Runtime::SagaInterpreter::REFUSED`
|
|
@@ -530,8 +643,19 @@ module Hecks
|
|
|
530
643
|
# payload field, a stamped passthrough, a self-identifying
|
|
531
644
|
# `event.id`) here rather than reusing it would be exactly the
|
|
532
645
|
# "hand-rolled approximation" this check exists to avoid; `send` on
|
|
533
|
-
# an interpreter sharing this
|
|
646
|
+
# an interpreter sharing this same `runtime`'s own registry is the
|
|
534
647
|
# real thing, not a copy of it.
|
|
648
|
+
#
|
|
649
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
650
|
+
# booted runtime this replay just dispatched through
|
|
651
|
+
# @param interpreter [Runtime::SagaInterpreter] the interpreter whose (private)
|
|
652
|
+
# `saga_correlation` re-derives this conversation's own correlation
|
|
653
|
+
# @param process_manager [Bluebook::ProcessManager] the process manager whose saga
|
|
654
|
+
# log is walked
|
|
655
|
+
# @param correlation [Object] the correlation identifier to match, as
|
|
656
|
+
# `saga_correlation` produces it — typically a String
|
|
657
|
+
# @return [Runtime::Event, nil] the real domain event this correlation's current
|
|
658
|
+
# checkpoint last advanced on, or nil if it was only ever `begin_saga`'d
|
|
535
659
|
def last_advancing_event(runtime, interpreter, process_manager, correlation)
|
|
536
660
|
entry = runtime.registry.saga_log.reverse_each.find do |row|
|
|
537
661
|
row[:process_manager] == process_manager.name && row[:instance] == correlation &&
|
|
@@ -544,20 +668,39 @@ module Hecks
|
|
|
544
668
|
end
|
|
545
669
|
end
|
|
546
670
|
|
|
547
|
-
#
|
|
671
|
+
# One (process manager, correlation)'s own redelivery check — pulled
|
|
548
672
|
# out of `check_saga_idempotency` itself so that method's own
|
|
549
673
|
# `flat_map`/`filter_map` walk stays readable; every local this
|
|
550
674
|
# shares with its caller (`interpreter`, `anchor`) is passed in
|
|
551
|
-
# rather than re-derived.
|
|
675
|
+
# rather than re-derived. The `saga_log` mark/restore pair (BUG#39
|
|
676
|
+
# fix) belongs right where it guards `interpreter.advance`, not in
|
|
677
|
+
# a helper a reader would have to jump to just to see what is and
|
|
678
|
+
# isn't being restored around that one call.
|
|
679
|
+
#
|
|
680
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
681
|
+
# booted runtime this replay just dispatched through
|
|
682
|
+
# @param interpreter [Runtime::SagaInterpreter] the interpreter to redeliver through
|
|
683
|
+
# @param domain_name [String] the owning domain's name
|
|
684
|
+
# @param process_manager [Bluebook::ProcessManager] the process manager under test
|
|
685
|
+
# @param anchor [Bluebook::Aggregate] any aggregate of the owning domain, used only
|
|
686
|
+
# to open a `Heki` store at the right domain/root
|
|
687
|
+
# @param correlation [Object] the conversation's own correlation identifier
|
|
688
|
+
# @param saga [Hash] this correlation's persisted `:state`/`:memory`
|
|
689
|
+
# @param redelivery [Runtime::Event] the real domain event to redeliver
|
|
690
|
+
# @return [Hash, nil] a divergence Hash (`:field`, `:domain`, `:process_manager`,
|
|
691
|
+
# `:correlation`, `:on`, `:before`, `:after`) if the redelivery visibly changed
|
|
692
|
+
# state/memory; nil if it left the checkpoint unchanged, or if the cold-read
|
|
693
|
+
# checkpoint could not be found at all
|
|
552
694
|
def check_one_saga_redelivery(runtime, interpreter, domain_name, process_manager, anchor,
|
|
553
695
|
correlation, saga, redelivery)
|
|
696
|
+
# rubocop:disable-next Metrics/BlockLength
|
|
554
697
|
Dir.mktmpdir("hecks-self-consistency-saga") do |tmp|
|
|
555
|
-
writer =
|
|
698
|
+
writer = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
|
|
556
699
|
writer.save_saga(process_manager: process_manager.name, correlation: correlation.to_s,
|
|
557
700
|
state: saga[:state], memory: saga[:memory], completed_compensations: [])
|
|
558
701
|
|
|
559
|
-
rehydrated =
|
|
560
|
-
|
|
702
|
+
rehydrated = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
|
|
703
|
+
.each_saga.find { |_pm, corr, *| corr == correlation.to_s }
|
|
561
704
|
next unless rehydrated
|
|
562
705
|
|
|
563
706
|
_pm, _corr, state, memory, compensations = rehydrated
|
|
@@ -565,6 +708,17 @@ module Hecks
|
|
|
565
708
|
|
|
566
709
|
saga_instances = runtime.registry.saga_instances[process_manager.name]
|
|
567
710
|
original = saga_instances[correlation]
|
|
711
|
+
|
|
712
|
+
# `saga_log` is `runtime.sagas`/`history[:sagas]` itself, by
|
|
713
|
+
# reference (`Dispatcher#sagas` — `@registry.saga_log`).
|
|
714
|
+
# `interpreter.advance` below is a real `advance_saga` dispatch,
|
|
715
|
+
# which appends its own row to this same array unconditionally —
|
|
716
|
+
# a true fact about this probe, but an unrestored append leaks
|
|
717
|
+
# into the primary trace (BUG#39). Marked here, sliced back off
|
|
718
|
+
# in `ensure`, same restore-what-I-mutated idiom as
|
|
719
|
+
# `saga_instances[correlation]` below.
|
|
720
|
+
saga_log = runtime.registry.saga_log
|
|
721
|
+
saga_log_mark = saga_log.size
|
|
568
722
|
begin
|
|
569
723
|
saga_instances[correlation] = { state: state, memory: memory, completed_compensations: compensations || [] }
|
|
570
724
|
interpreter.advance(redelivery, domain_name, only: process_manager)
|
|
@@ -576,6 +730,8 @@ module Hecks
|
|
|
576
730
|
{ field: "saga_redelivery_idempotency", domain: domain_name, process_manager: process_manager.name,
|
|
577
731
|
correlation: correlation, on: redelivery.name, before: before, after: after_shape }
|
|
578
732
|
ensure
|
|
733
|
+
saga_log.slice!(saga_log_mark..) if saga_log.size > saga_log_mark
|
|
734
|
+
|
|
579
735
|
if original
|
|
580
736
|
saga_instances[correlation] = original
|
|
581
737
|
else
|
|
@@ -585,47 +741,69 @@ module Hecks
|
|
|
585
741
|
end
|
|
586
742
|
end
|
|
587
743
|
|
|
588
|
-
#
|
|
589
|
-
# adapter at `tmp`),
|
|
590
|
-
# instance at the
|
|
744
|
+
# One fold of `entries` into `writer` (a real, already-open `Heki`
|
|
745
|
+
# adapter at `tmp`), then a cold read back through a brand new `Heki`
|
|
746
|
+
# instance at the same path — `@store` on a fresh instance starts
|
|
591
747
|
# unmemoized, so `#all` below is forced through `#read` →
|
|
592
748
|
# `#read_snapshot`/`#replay_journal`, real disk bytes, not whatever
|
|
593
749
|
# `writer` still holds cached in its own process memory. Called
|
|
594
|
-
#
|
|
750
|
+
# twice in a row against the same `writer` (see `check`/
|
|
595
751
|
# `check_idempotency` above) is exactly "replay the same journal a
|
|
596
752
|
# second time" — `writer` already holds everything the first fold
|
|
597
753
|
# wrote, so a second fold re-applies the identical operations on
|
|
598
754
|
# top, and the two cold reads either agree (idempotent) or don't.
|
|
755
|
+
#
|
|
756
|
+
# @param writer [Adapters::Heki] the open store to append `entries` into
|
|
757
|
+
# @param tmp [String] the throwaway directory `writer` was opened against
|
|
758
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `entries` belongs to
|
|
759
|
+
# @param entries [Array<Ports::Persistence::Entry>] the journal entries to fold in
|
|
760
|
+
# @return [Hash{String => Object}] every stored record's id (stringified) mapped to
|
|
761
|
+
# its materialized state, read back cold
|
|
599
762
|
def fold!(writer, tmp, aggregate, entries)
|
|
600
763
|
entries.each do |entry|
|
|
601
764
|
writer.append(entry)
|
|
602
765
|
writer.project(entry)
|
|
603
766
|
end
|
|
604
767
|
|
|
605
|
-
|
|
606
|
-
|
|
768
|
+
guarded_heki(aggregate: aggregate, root: tmp).all
|
|
769
|
+
.to_h do |record|
|
|
770
|
+
[
|
|
771
|
+
record.id.to_s, Runtime::Value.materialize(record.state)
|
|
772
|
+
]
|
|
773
|
+
end
|
|
607
774
|
end
|
|
608
775
|
|
|
609
|
-
#
|
|
776
|
+
# Recurses through a `Value`'s own fields via `#[]`, not `#to_h` —
|
|
610
777
|
# `#to_h` already materializes every field (`Value.materialize`),
|
|
611
778
|
# which would hide a nested `Value` from this walk before it ever
|
|
612
|
-
# got here. In practice a value object's
|
|
779
|
+
# got here. In practice a value object's own composite fields
|
|
613
780
|
# (`Coercion#normalize_composite_fields`) are validated but stored
|
|
614
781
|
# as plain, already-materialized Hashes, not re-wrapped `Value`
|
|
615
782
|
# instances — confirmed live, not assumed — so this recursion finds
|
|
616
|
-
# nothing further past the field it started from
|
|
783
|
+
# nothing further past the field it started from for today's
|
|
617
784
|
# coercion pipeline specifically. Kept anyway, not dead code: the
|
|
618
785
|
# generic `Hash`/`Array` branches below reach the exact same nested
|
|
619
786
|
# data through `node[attribute.name]` regardless, and a future
|
|
620
|
-
# change that
|
|
787
|
+
# change that does start wrapping composite fields as real `Value`
|
|
621
788
|
# instances would be walked correctly here with no change needed.
|
|
622
|
-
# `seen` is a `compare_by_identity` Hash: the same
|
|
789
|
+
# `seen` is a `compare_by_identity` Hash: the same instance can legitimately
|
|
623
790
|
# appear more than once (an aggregate's live state and an event
|
|
624
791
|
# payload both reference the exact same frozen object), and
|
|
625
792
|
# checking it twice would just waste time, never change the
|
|
626
793
|
# answer — identity, not `Value#==`, is the right notion of
|
|
627
|
-
# "already found" here (two
|
|
794
|
+
# "already found" here (two different value objects that happen to
|
|
628
795
|
# hold equal fields are still two separate round trips to prove).
|
|
796
|
+
#
|
|
797
|
+
# @param node [Object] the value to walk; a `Runtime::Value` is recorded and
|
|
798
|
+
# recursed into by field, a Hash or Array is recursed into by value/element,
|
|
799
|
+
# anything else is ignored
|
|
800
|
+
# @param found [Array<Array(Runtime::Value, Bluebook::Aggregate, nil)>] accumulator
|
|
801
|
+
# this method appends `[value, aggregate]` pairs to, in place
|
|
802
|
+
# @param seen [Hash] a `compare_by_identity` Hash used as a found-by-identity set,
|
|
803
|
+
# mutated in place
|
|
804
|
+
# @param aggregate [Bluebook::Aggregate, nil] the aggregate `node` belongs to, carried
|
|
805
|
+
# along unchanged for every value found beneath it
|
|
806
|
+
# @return [void]
|
|
629
807
|
def walk_value_objects(node, found, seen, aggregate)
|
|
630
808
|
case node
|
|
631
809
|
when Runtime::Value
|