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
|
@@ -6,44 +6,44 @@ 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
|
-
#
|
|
18
|
+
# Three checks, run on the same already-generated sequence and its
|
|
19
19
|
# resulting live state — no second fuzzing pass, no re-dispatch
|
|
20
20
|
# through the command layer:
|
|
21
21
|
#
|
|
22
22
|
# 1. `check_rehydration` — does reloading an aggregate from its
|
|
23
23
|
# own durable journal reproduce exactly the state a live dispatch
|
|
24
24
|
# already produced?
|
|
25
|
-
# 2. `check_idempotency` — does replaying that
|
|
25
|
+
# 2. `check_idempotency` — does replaying that same journal a
|
|
26
26
|
# second time change anything? (a variant of #1, but a different
|
|
27
27
|
# failure mode: no leaked state between applications, not just
|
|
28
28
|
# "cold load works once.")
|
|
29
29
|
# 3. `check_value_object_round_trip` — does every value object the
|
|
30
30
|
# sequence actually built survive `to_json` then rebuilt back?
|
|
31
31
|
#
|
|
32
|
-
#
|
|
32
|
+
# **The rehydration path, found, not guessed**. hecks is not event-sourced
|
|
33
33
|
# at the aggregate level — there is no `AccountOpened`-shaped log a
|
|
34
|
-
# `CommandInterpreter` folds to rebuild state. What there
|
|
34
|
+
# `CommandInterpreter` folds to rebuild state. What there is, real and
|
|
35
35
|
# already shipping, is `Ports::Persistence::AppendOnly` (lib/hecks/
|
|
36
36
|
# ports/persistence/append_only.rb): every adapter accepts the same
|
|
37
|
-
# `Entry` stream (`operation`, `id`, the
|
|
37
|
+
# `Entry` stream (`operation`, `id`, the full state after that
|
|
38
38
|
# command — not a delta) and answers `#entries`; `#recover!` — "an
|
|
39
39
|
# append is durable before a projection is attempted; replaying the
|
|
40
40
|
# log restores a snapshot/table after a crash in that small window" —
|
|
41
|
-
# is called on
|
|
42
|
-
# (`RepositoryFactory.build`'s own `recover: true` default). That
|
|
41
|
+
# is called on every repository this runtime ever builds
|
|
42
|
+
# (`RepositoryFactory.build`'s own `recover: true` default). That is
|
|
43
43
|
# the production cold-rehydration path. Reusing `#recover!` directly
|
|
44
|
-
# against the
|
|
44
|
+
# against the live adapter would prove nothing, though: `Fuzzing::
|
|
45
45
|
# Replay` runs against `Adapters::Memory` (`IsolatedBoot`'s own
|
|
46
|
-
# default), and Memory's own `Entry#state` is a
|
|
46
|
+
# default), and Memory's own `Entry#state` is a shallow `instance.
|
|
47
47
|
# state.dup` — the exact same `Runtime::Value` objects a command
|
|
48
48
|
# produced ride along unchanged, so folding them straight back through
|
|
49
49
|
# Memory's own `#project` is a tautology that can never fail (`Value#
|
|
@@ -51,16 +51,16 @@ module Hecks
|
|
|
51
51
|
# branch passes an already-typed value straight through, no
|
|
52
52
|
# rebuilding at all).
|
|
53
53
|
#
|
|
54
|
-
# So `cold_read`, below, feeds the
|
|
55
|
-
# Heki` instead — a
|
|
54
|
+
# So `cold_read`, below, feeds the same entries through `Adapters::
|
|
55
|
+
# Heki` instead — a real, already-shipped, disk-backed adapter
|
|
56
56
|
# (examples/banking's own `persisted_by("Heki")`), in a throwaway
|
|
57
57
|
# directory. Writing forces every value through `JSON.generate`
|
|
58
58
|
# (Heki's own journal line, its own compressed snapshot); reading
|
|
59
|
-
# back through a
|
|
59
|
+
# back through a fresh `Heki` instance (unmemoized `@store`) forces
|
|
60
60
|
# `read_snapshot`/`replay_journal` — real `JSON.parse`, real
|
|
61
61
|
# `Zlib::Inflate`, real bytes off a real filesystem — which is what
|
|
62
62
|
# makes `Instance.hydrate_with_defaults` → `Value.hydrate` →
|
|
63
|
-
# `Value.for_attribute` actually
|
|
63
|
+
# `Value.for_attribute` actually rebuild every value object from raw
|
|
64
64
|
# data via `Value.build`, the same coercion/validation path a real
|
|
65
65
|
# restart takes, rather than pass the live object through unchanged.
|
|
66
66
|
# This is the exact mechanism `AppendOnly#recover!` names in its own
|
|
@@ -70,12 +70,20 @@ module Hecks
|
|
|
70
70
|
module SelfConsistency
|
|
71
71
|
module_function
|
|
72
72
|
|
|
73
|
-
#
|
|
73
|
+
# Every Heki this pass opens is guarded like a runtime repository
|
|
74
|
+
# (Phase 2, Track A, PR A4). These adapters are built directly, not
|
|
75
|
+
# through `RepositoryFactory.build`, so without this a cold read here
|
|
76
|
+
# could hand `Runtime::Instance` undecoded state and the codec's
|
|
77
|
+
# guarantee would have a bypass in exactly the check that exists to
|
|
78
|
+
# compare stored state against live state.
|
|
79
|
+
def guarded_heki(**) = Ports::Persistence::CodecBoundary.guard!(Adapters::Heki.new(**))
|
|
80
|
+
|
|
81
|
+
# The whole pass — called once, with the runtime still live (inside
|
|
74
82
|
# `Replay.call`'s own `IsolatedBoot.call` block, before the tmp
|
|
75
83
|
# directory and its adapters go out of scope) and the `history`
|
|
76
84
|
# `Replay.call` is about to return. Nothing here boots a second
|
|
77
85
|
# runtime or dispatches a single command; every check below reads
|
|
78
|
-
# data this
|
|
86
|
+
# data this one replay already produced. Just the three checks
|
|
79
87
|
# below, run and collected — kept as three independently callable
|
|
80
88
|
# methods (not fused into one shared fold) so a spec proving one
|
|
81
89
|
# check can fire never has to reason about the other two.
|
|
@@ -86,12 +94,12 @@ module Hecks
|
|
|
86
94
|
saga_redelivery_idempotency: check_saga_idempotency(runtime, history) }
|
|
87
95
|
end
|
|
88
96
|
|
|
89
|
-
#
|
|
97
|
+
# Check 1 — rehydrate-from-journal == live state.
|
|
90
98
|
def check_rehydration(runtime)
|
|
91
99
|
each_touched_repository(runtime).filter_map do |domain_name, aggregate, repository, entries|
|
|
92
100
|
live = snapshot(repository)
|
|
93
101
|
Dir.mktmpdir("hecks-self-consistency") do |tmp|
|
|
94
|
-
writer =
|
|
102
|
+
writer = guarded_heki(aggregate: aggregate, root: tmp)
|
|
95
103
|
rehydrated = fold!(writer, tmp, aggregate, entries)
|
|
96
104
|
next if rehydrated == live
|
|
97
105
|
|
|
@@ -101,8 +109,8 @@ module Hecks
|
|
|
101
109
|
end
|
|
102
110
|
end
|
|
103
111
|
|
|
104
|
-
#
|
|
105
|
-
#
|
|
112
|
+
# Check 2 — replay idempotency: folding the same entries into the
|
|
113
|
+
# same durable store a second time must change nothing. A variant
|
|
106
114
|
# of check 1, worth stating separately — this catches a
|
|
107
115
|
# replay-specific bug (leaked state between applications, a
|
|
108
116
|
# double-applied effect) that a single, one-shot cold read could
|
|
@@ -110,7 +118,7 @@ module Hecks
|
|
|
110
118
|
def check_idempotency(runtime)
|
|
111
119
|
each_touched_repository(runtime).filter_map do |domain_name, aggregate, repository, entries|
|
|
112
120
|
Dir.mktmpdir("hecks-self-consistency") do |tmp|
|
|
113
|
-
writer =
|
|
121
|
+
writer = guarded_heki(aggregate: aggregate, root: tmp)
|
|
114
122
|
once = fold!(writer, tmp, aggregate, entries)
|
|
115
123
|
twice = fold!(writer, tmp, aggregate, entries)
|
|
116
124
|
next if once == twice
|
|
@@ -121,9 +129,9 @@ module Hecks
|
|
|
121
129
|
end
|
|
122
130
|
end
|
|
123
131
|
|
|
124
|
-
#
|
|
132
|
+
# Check 3 — every `Runtime::Value` the sequence actually built
|
|
125
133
|
# (walked out of the replay's own `instances`/`events`/`queries`,
|
|
126
|
-
# never a hand-picked example), round-tripped through the
|
|
134
|
+
# never a hand-picked example), round-tripped through the real
|
|
127
135
|
# serialize/deserialize pair: `Value#to_json` (JSON.generate(to_h),
|
|
128
136
|
# value.rb) out, `Value.build` (value/coercion.rb — the same
|
|
129
137
|
# constructor a command argument's own raw JSON goes through) back
|
|
@@ -132,16 +140,16 @@ module Hecks
|
|
|
132
140
|
# `from_json` per closed set/value object); `Value.build` is the
|
|
133
141
|
# actual Ruby door a raw, untyped Hash becomes a validated,
|
|
134
142
|
# admitted, invariant-checked value object through.
|
|
135
|
-
# `aggregate:`
|
|
143
|
+
# `aggregate:` threaded alongside every value found, not dropped —
|
|
136
144
|
# `Value.build(value_object, fields, aggregate)`'s third argument is
|
|
137
|
-
# what lets `normalize_composite_fields` resolve a
|
|
145
|
+
# what lets `normalize_composite_fields` resolve a nested composite
|
|
138
146
|
# field's own type by name (`value_object_for(aggregate, type)`).
|
|
139
147
|
# Building with `aggregate: nil` (this method's first version, live-
|
|
140
148
|
# tested against `examples/pizzas` while this was being written)
|
|
141
149
|
# silently skips that step entirely — `Pizza`'s own `price_cents`/
|
|
142
|
-
# `size` fields round-tripped back as bare,
|
|
150
|
+
# `size` fields round-tripped back as bare, string-keyed Hashes
|
|
143
151
|
# instead of rebuilt `Money`/`PizzaSize` value objects, a false
|
|
144
|
-
#
|
|
152
|
+
# positive this check would have reported as a real bug on every
|
|
145
153
|
# single sweep. Resolved from `history[:instances]`' own key
|
|
146
154
|
# (`"Domain::Aggregate#id"`, `Replay#snapshot_instances`) and
|
|
147
155
|
# `history[:events]`' own `event[:aggregate]` (`"Domain::Aggregate"`,
|
|
@@ -150,7 +158,7 @@ module Hecks
|
|
|
150
158
|
# off `history` rather than a second lookup. `history[:queries]`'
|
|
151
159
|
# own rows have no single owning aggregate reliably named on the
|
|
152
160
|
# entry itself (a cross-aggregate read model, a `for_each` target),
|
|
153
|
-
# so they are left
|
|
161
|
+
# so they are left out of this walk rather than risk the same false
|
|
154
162
|
# positive `nil` already produced once — `instances` and `events`
|
|
155
163
|
# alone already reach every value object a generated sequence
|
|
156
164
|
# actually persisted or announced.
|
|
@@ -186,12 +194,12 @@ module Hecks
|
|
|
186
194
|
end
|
|
187
195
|
end
|
|
188
196
|
|
|
189
|
-
#
|
|
190
|
-
# read an
|
|
191
|
-
# ever exercised the
|
|
197
|
+
# Check 4 — saga cold-rehydration (angle-10). Checks 1/2 above cold-
|
|
198
|
+
# read an aggregate's own journal through Heki; nothing in this file
|
|
199
|
+
# ever exercised the other durable store `SagaInterpreter#checkpoint`
|
|
192
200
|
# writes through — `Ports::Persistence::NullSagaStore`'s own header
|
|
193
201
|
# calls Heki's `SagaStore` (`adapters/driven/heki/saga_store.rb`) the
|
|
194
|
-
#
|
|
202
|
+
# optional saga-persistence capability, and it is real and already
|
|
195
203
|
# shipping, just never fuzzed: `Registry#rehydrate_sagas!` — the
|
|
196
204
|
# production "process just restarted" path — folds exactly what
|
|
197
205
|
# `each_saga` yields back into `@saga_instances`, and until now
|
|
@@ -201,26 +209,26 @@ module Hecks
|
|
|
201
209
|
# rather than folding it into checks 1/2's own aggregate walk.
|
|
202
210
|
#
|
|
203
211
|
# `history[:saga_instances]` (`replay.rb`'s own `saga_instances`
|
|
204
|
-
# local, built once at the very end of a replay) is the
|
|
212
|
+
# local, built once at the very end of a replay) is the same
|
|
205
213
|
# materialized `{pm_name => {correlation => {state:, memory:}}}`
|
|
206
214
|
# shape `SagaInterpreter#checkpoint` itself hands a real adapter —
|
|
207
215
|
# read from `history`, not re-derived from the (by-now-live, already
|
|
208
216
|
# mutated by whatever `check_saga_idempotency` ran first, see that
|
|
209
217
|
# method's own header) `runtime.registry.saga_instances`. Written
|
|
210
|
-
# through a
|
|
218
|
+
# through a real `Adapters::Heki` (a throwaway tmpdir, one per
|
|
211
219
|
# process manager so two process managers with correlations that
|
|
212
220
|
# happen to collide as strings never share a store), read back
|
|
213
|
-
# through a
|
|
221
|
+
# through a fresh instance (unmemoized `@store`/`@saga_store`, same
|
|
214
222
|
# reason `fold!` above uses one) — forcing the identical
|
|
215
223
|
# `JSON.generate`/`JSON.parse` boundary a real crash-then-restart
|
|
216
224
|
# takes, not a live-object pass-through.
|
|
217
225
|
#
|
|
218
|
-
#
|
|
226
|
+
# One finding per (domain, process manager) — every correlation this
|
|
219
227
|
# process manager's own `history[:saga_instances]` entry holds,
|
|
220
228
|
# compared as a whole Hash — the same aggregate-granularity (not
|
|
221
229
|
# per-record) `check_rehydration` already reports at.
|
|
222
230
|
#
|
|
223
|
-
# `completed_compensations` is
|
|
231
|
+
# `completed_compensations` is deliberately out of scope — `history[
|
|
224
232
|
# :saga_instances]` never captures it (`replay.rb`'s own comment:
|
|
225
233
|
# only `state`/`memory` are threaded through, since a saga's
|
|
226
234
|
# in-flight compensation ledger is a fact about a leg still running,
|
|
@@ -237,7 +245,7 @@ module Hecks
|
|
|
237
245
|
next unless anchor
|
|
238
246
|
|
|
239
247
|
Dir.mktmpdir("hecks-self-consistency-saga") do |tmp|
|
|
240
|
-
writer =
|
|
248
|
+
writer = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
|
|
241
249
|
persisted.each do |correlation, saga|
|
|
242
250
|
writer.save_saga(process_manager: process_manager.name, correlation: correlation.to_s,
|
|
243
251
|
state: saga[:state], memory: saga[:memory], completed_compensations: [])
|
|
@@ -253,21 +261,21 @@ module Hecks
|
|
|
253
261
|
end
|
|
254
262
|
end
|
|
255
263
|
|
|
256
|
-
#
|
|
264
|
+
# Check 5 — redelivery idempotency of the checkpoint-then-load path.
|
|
257
265
|
# `check_saga_rehydration` above proves cold-reading a checkpoint
|
|
258
|
-
# reproduces the same
|
|
266
|
+
# reproduces the same data; this proves the other half of a real
|
|
259
267
|
# crash/restart — a message an at-least-once delivery mechanism (an
|
|
260
268
|
# outbox redrive, a queue redelivery) hands the rehydrated saga a
|
|
261
|
-
#
|
|
269
|
+
# second time — does not silently re-advance it. There is no flag
|
|
262
270
|
# for this in production (`SagaInterpreter#unwind`'s own comment:
|
|
263
271
|
# "the check is the guard") — the (event, current state) lookup
|
|
264
|
-
# `handler_for` performs is the
|
|
272
|
+
# `handler_for` performs is the entire mechanism, and it has never
|
|
265
273
|
# been exercised against a state this practice loaded from cold
|
|
266
274
|
# storage rather than one still sitting in a live process's memory.
|
|
267
275
|
#
|
|
268
|
-
#
|
|
276
|
+
# One (process manager, correlation) tested, using a fresh
|
|
269
277
|
# `Runtime::SagaInterpreter` sharing `runtime`'s own `registry` and
|
|
270
|
-
# `door: runtime` — the identical two objects the
|
|
278
|
+
# `door: runtime` — the identical two objects the dispatcher's own
|
|
271
279
|
# `@sagas` was built from (`dispatcher.rb`'s own `SagaInterpreter.
|
|
272
280
|
# new(registry, door: self)`) — not a hand-rolled re-implementation
|
|
273
281
|
# of `advance_saga`'s own state-guard. `only: process_manager` scopes
|
|
@@ -275,13 +283,13 @@ module Hecks
|
|
|
275
283
|
# keyword the outbox relay already uses to run one consumer alone
|
|
276
284
|
# (`Runtime::Outbox::Relay#run_consumer`).
|
|
277
285
|
#
|
|
278
|
-
#
|
|
286
|
+
# Which event to redeliver — `runtime.registry.saga_log`'s own last
|
|
279
287
|
# `advanced: true` row for this (process manager, correlation) names
|
|
280
|
-
# the event
|
|
288
|
+
# the event by name only; the real `Runtime::Event` object (payload,
|
|
281
289
|
# aggregate, id, `correlation` — everything `saga_correlation`/
|
|
282
290
|
# `dispatch_args` actually read) lives in `runtime.events`, still
|
|
283
|
-
# live for exactly this reason (this file's own header: "runtime
|
|
284
|
-
#
|
|
291
|
+
# live for exactly this reason (this file's own header: "runtime is
|
|
292
|
+
# still live here"). Matched back by name plus `saga_correlation`
|
|
285
293
|
# itself (`Runtime::SagaInterpreter::Correlation`, `private`) —
|
|
286
294
|
# reused via `send` rather than reproduced, because reproducing its
|
|
287
295
|
# three-tier fallback (a dotted payload field, a stamped passthrough,
|
|
@@ -291,25 +299,25 @@ module Hecks
|
|
|
291
299
|
# its own `saga_log` row's `on:` is the synthetic `REFUSED` trigger
|
|
292
300
|
# name, never a real domain event, so there is nothing to redeliver.
|
|
293
301
|
#
|
|
294
|
-
#
|
|
295
|
-
# `saga_instances[pm][correlation]` slot is overwritten,
|
|
296
|
-
# with whatever a cold Heki read of the
|
|
302
|
+
# Simulating "just restarted" — the live registry's own in-memory
|
|
303
|
+
# `saga_instances[pm][correlation]` slot is overwritten, in place,
|
|
304
|
+
# with whatever a cold Heki read of the same checkpoint answers
|
|
297
305
|
# (exactly what `Registry#rehydrate_sagas!` does for real on every
|
|
298
306
|
# boot), the redelivery is driven through the real interpreter, and
|
|
299
307
|
# the slot is put back — `ensure`d — once this correlation's own
|
|
300
|
-
# check is done. Safe
|
|
308
|
+
# check is done. Safe only because `check`/`Replay.call` run this,
|
|
301
309
|
# synchronously, single-threaded, as the very last thing before
|
|
302
310
|
# `runtime` and its whole tmp directory go out of scope for good;
|
|
303
|
-
# nothing downstream of this method ever reads the
|
|
311
|
+
# nothing downstream of this method ever reads the live registry
|
|
304
312
|
# again (`check_saga_rehydration`, `check_rehydration`, `check_
|
|
305
313
|
# idempotency`, `check_value_object_round_trip` all read `history`'s
|
|
306
314
|
# own frozen snapshot instead, never `runtime.registry` — so calling
|
|
307
315
|
# order relative to this method's own mutation doesn't matter).
|
|
308
316
|
#
|
|
309
|
-
#
|
|
310
|
-
# — a leg whose own `from:`/`to:` are the
|
|
317
|
+
# The assertion is about `state`/`memory`, not "did a dispatch fire"
|
|
318
|
+
# — a leg whose own `from:`/`to:` are the same state (every existing
|
|
311
319
|
# saga's own starts_on self-transition, `waybill.bluebook`'s own leg
|
|
312
|
-
# 1/2) is
|
|
320
|
+
# 1/2) is expected to re-run on redelivery with no visible state
|
|
313
321
|
# change at all; that is a property of the declared handler graph,
|
|
314
322
|
# not a rehydration defect, and asserting against it here would
|
|
315
323
|
# manufacture a false positive on every saga this corpus has. A
|
|
@@ -340,18 +348,18 @@ module Hecks
|
|
|
340
348
|
|
|
341
349
|
# ── Rust-side self-consistency ───────────────────────────────────
|
|
342
350
|
#
|
|
343
|
-
#
|
|
344
|
-
# `kernel/cli.rs`'s `run` accepts an
|
|
345
|
-
# ("the exact 'Domain::Aggregate#id' -> state shape
|
|
346
|
-
# 'instances' output already produces... lets a
|
|
351
|
+
# The compiled binary's own rehydration door, already shipping —
|
|
352
|
+
# `kernel/cli.rs`'s `run` accepts an optional top-level `"seed"` key
|
|
353
|
+
# ("the exact 'Domain::Aggregate#id' -> state shape this run's own
|
|
354
|
+
# 'instances' output already produces... lets a host seed prior
|
|
347
355
|
# state back in instead of replaying `steps` from scratch every
|
|
348
356
|
# invocation"). `Store::from_seed`/`Store::instances` are that
|
|
349
357
|
# mechanism's own two halves — `rust/host` (docs/implemented/
|
|
350
358
|
# 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
|
|
359
|
+
# today. `rust_seed_round_trip` exercises exactly that: a second,
|
|
360
|
+
# independent invocation of the same binary, `"steps": []` (nothing
|
|
353
361
|
# new dispatched — this is rehydration, not re-dispatch), seeded
|
|
354
|
-
# with whatever `"instances"` a
|
|
362
|
+
# with whatever `"instances"` a prior invocation already produced.
|
|
355
363
|
# `differ` (unused, kept in the signature — see below) is a
|
|
356
364
|
# `RustConformanceHelpers`-including instance (`bin/qa_sweep`'s own
|
|
357
365
|
# `Differ`), the same argument `check_rust_rehydration`/`check_
|
|
@@ -359,25 +367,25 @@ module Hecks
|
|
|
359
367
|
# rather than dropped from all three signatures at once so a
|
|
360
368
|
# future differential-style reduction has a door already open,
|
|
361
369
|
# without this module ever `require`ing `spec/support/` itself.
|
|
362
|
-
#
|
|
363
|
-
#
|
|
370
|
+
# Neither `strip_emitted_flags!` nor any other differential-only
|
|
371
|
+
# reduction runs here — that reduction exists so a Rust-only
|
|
364
372
|
# bookkeeping field (`emitted_<event>`, docs/decisions/0049) never
|
|
365
373
|
# counts against Ruby, which has no equivalent field to agree with
|
|
366
374
|
# at all (`RustConformanceHelpers#strip_emitted_flags!`'s own
|
|
367
375
|
# comment). This check has no Ruby side to spare — it is asking the
|
|
368
|
-
# Rust binary whether it agrees with
|
|
376
|
+
# Rust binary whether it agrees with itself, so `emitted_*` fields
|
|
369
377
|
# are exactly as real a fact to compare as any other. Stripping them
|
|
370
378
|
# here (an earlier version of this method did) silently deleted
|
|
371
|
-
# them from the
|
|
372
|
-
# present on the
|
|
373
|
-
# asymmetric comparison that reported
|
|
379
|
+
# them from the returned seed-round-trip result while leaving them
|
|
380
|
+
# present on the original `seed_instances` a caller passes in — an
|
|
381
|
+
# asymmetric comparison that reported every record carrying one as
|
|
374
382
|
# a rehydration divergence, unconditionally, on every domain that
|
|
375
383
|
# has one at all. Found live against `examples/banking`
|
|
376
384
|
# (`Banking::Account`'s own `corrects` reaction) while this
|
|
377
385
|
# integration was being written, by comparing this method's own
|
|
378
|
-
# answer against the compiled binary's
|
|
386
|
+
# answer against the compiled binary's raw stdout for the identical
|
|
379
387
|
# seed call: the raw round trip preserved `emitted_fee_applied`
|
|
380
|
-
# correctly; only
|
|
388
|
+
# correctly; only this method's own stripping dropped it. `spec/
|
|
381
389
|
# self_consistency_rust_spec.rb`'s own "banking" example pins the
|
|
382
390
|
# regression against a real domain going forward.
|
|
383
391
|
def rust_seed_round_trip(binary, _differ, seed_instances)
|
|
@@ -391,7 +399,7 @@ module Hecks
|
|
|
391
399
|
parsed["instances"]
|
|
392
400
|
end
|
|
393
401
|
|
|
394
|
-
#
|
|
402
|
+
# Check 1, Rust side — seeding a fresh invocation with a prior
|
|
395
403
|
# invocation's own live `"instances"` must reproduce that same
|
|
396
404
|
# state, unchanged. `live_instances` is `rust_output["instances"]`
|
|
397
405
|
# — the exact same value `bin/qa_sweep`'s own differential compare
|
|
@@ -403,8 +411,8 @@ module Hecks
|
|
|
403
411
|
[{ field: "rust_rehydration", live: live_instances, rehydrated: rehydrated }]
|
|
404
412
|
end
|
|
405
413
|
|
|
406
|
-
#
|
|
407
|
-
# already produced, a
|
|
414
|
+
# Check 2, Rust side — seeding with what a first seed round trip
|
|
415
|
+
# already produced, a second time, must not drift any further. The
|
|
408
416
|
# same "replay it again, byte for byte" claim `check_idempotency`
|
|
409
417
|
# proves for Ruby, aimed at the one rehydration door this compiled
|
|
410
418
|
# binary actually has.
|
|
@@ -418,7 +426,7 @@ module Hecks
|
|
|
418
426
|
|
|
419
427
|
# ── shared plumbing ─────────────────────────────────────────────
|
|
420
428
|
|
|
421
|
-
#
|
|
429
|
+
# Every [domain, aggregate] pair this sequence actually wrote to —
|
|
422
430
|
# an aggregate with an empty `#entries` never had anything dispatch
|
|
423
431
|
# against it this run, so there is nothing to rehydrate and no
|
|
424
432
|
# finding a "clean, nothing touched" report would mean anything
|
|
@@ -442,12 +450,12 @@ module Hecks
|
|
|
442
450
|
repository.all.to_h { |record| [record.id.to_s, Runtime::Value.materialize(record.state)] }
|
|
443
451
|
end
|
|
444
452
|
|
|
445
|
-
#
|
|
453
|
+
# Every [domain, process manager] pair any loaded bluebook declares —
|
|
446
454
|
# regardless of whether this replay's own `history[:saga_instances]`
|
|
447
455
|
# ever touched it (mirrors `each_touched_repository`'s own walk one
|
|
448
456
|
# level up; the "did anything actually persist" filter lives in each
|
|
449
457
|
# check's own caller, same as that method's `entries.empty?` guard).
|
|
450
|
-
# An empty return here
|
|
458
|
+
# An empty return here is the "domain declares no process manager"
|
|
451
459
|
# skip `check_saga_rehydration`/`check_saga_idempotency` both need —
|
|
452
460
|
# `filter_map`/`flat_map` over an empty Array already answers `[]`,
|
|
453
461
|
# identical to "ran and found nothing," which is deliberate: neither
|
|
@@ -461,7 +469,7 @@ module Hecks
|
|
|
461
469
|
found
|
|
462
470
|
end
|
|
463
471
|
|
|
464
|
-
#
|
|
472
|
+
# The live-side ground truth, key-shape-normalized (see `deep_
|
|
465
473
|
# stringify_keys`'s own comment) so it compares fairly against a
|
|
466
474
|
# real Heki round trip's own shallow-symbolize convention.
|
|
467
475
|
def normalize_saga_rows(persisted)
|
|
@@ -470,38 +478,38 @@ module Hecks
|
|
|
470
478
|
end
|
|
471
479
|
end
|
|
472
480
|
|
|
473
|
-
# A
|
|
481
|
+
# A fresh `Adapters::Heki` at the same `tmp`/`domain` — unmemoized
|
|
474
482
|
# `@store`/`@saga_store`, so `#each_saga` is forced back through
|
|
475
483
|
# `read_snapshot`/`replay_journal`, real bytes off real disk, not
|
|
476
484
|
# whatever the writer that just wrote them still holds in its own
|
|
477
485
|
# process memory (the same reason `fold!`, above, opens a second
|
|
478
486
|
# `Adapters::Heki` instance rather than reading its own writer back).
|
|
479
487
|
def cold_read_saga_rows(anchor, tmp, domain_name)
|
|
480
|
-
reader =
|
|
488
|
+
reader = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
|
|
481
489
|
reader.each_saga.with_object({}) do |(_pm, correlation, state, memory, _completed), rows|
|
|
482
490
|
rows[correlation] = { state: state, memory: deep_stringify_keys(memory) }
|
|
483
491
|
end
|
|
484
492
|
end
|
|
485
493
|
|
|
486
|
-
# `SagaStore#each_saga`
|
|
487
|
-
#
|
|
494
|
+
# `SagaStore#each_saga` only ever symbolizes `memory`'s own top-level
|
|
495
|
+
# keys (`heki/saga_store.rb`'s own `each_saga`, one level deep) —
|
|
488
496
|
# `Registry::SagaPersistence#warn_stalled_saga` already documents
|
|
489
497
|
# this exact asymmetry for the one reserved key production code
|
|
490
|
-
# cares about (`SAGA_PENDING_DISPATCH_KEY`). A
|
|
498
|
+
# cares about (`SAGA_PENDING_DISPATCH_KEY`). A nested composite
|
|
491
499
|
# memory field (any saga whose starting event carries a value
|
|
492
500
|
# 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
|
|
501
|
+
# `ConsignmentRequested` alone has three) comes back with string
|
|
502
|
+
# keys at every level below the top, while `history[:saga_instances]`
|
|
503
|
+
# 's own `Runtime::Value.materialize` call produces symbol keys
|
|
496
504
|
# throughout. That asymmetry is Heki's own documented, accepted
|
|
497
505
|
# storage convention — an "opaque, adapter-agnostic JSON blob"
|
|
498
506
|
# (`SagaInterpreter#checkpoint`'s own comment), never a typed
|
|
499
|
-
# rebuild the way an
|
|
507
|
+
# rebuild the way an aggregate's own composite fields get on cold
|
|
500
508
|
# read (this file's own header: there is no VO schema to rebuild
|
|
501
509
|
# against for a saga's memory blob at all) — not a rehydration
|
|
502
510
|
# defect this check exists to find. Recursively re-stringifying
|
|
503
|
-
#
|
|
504
|
-
# shape difference apart from an
|
|
511
|
+
# both sides before comparing is what tells that known, accepted
|
|
512
|
+
# shape difference apart from an actual data-loss bug (a dropped
|
|
505
513
|
# key, a changed value, a missing field) — exactly the kind (b)'s
|
|
506
514
|
# own seeded fixture in `spec/fuzzing/self_consistency_saga_spec.rb`
|
|
507
515
|
# proves this still catches.
|
|
@@ -513,7 +521,7 @@ module Hecks
|
|
|
513
521
|
end
|
|
514
522
|
end
|
|
515
523
|
|
|
516
|
-
#
|
|
524
|
+
# The real, already-announced event this correlation's current
|
|
517
525
|
# checkpoint came from — walked back out of `runtime.registry.
|
|
518
526
|
# saga_log`'s own `advanced: true` rows (newest first), skipping the
|
|
519
527
|
# synthetic `REFUSED` trigger (`Runtime::SagaInterpreter::REFUSED`
|
|
@@ -530,7 +538,7 @@ module Hecks
|
|
|
530
538
|
# payload field, a stamped passthrough, a self-identifying
|
|
531
539
|
# `event.id`) here rather than reusing it would be exactly the
|
|
532
540
|
# "hand-rolled approximation" this check exists to avoid; `send` on
|
|
533
|
-
# an interpreter sharing this
|
|
541
|
+
# an interpreter sharing this same `runtime`'s own registry is the
|
|
534
542
|
# real thing, not a copy of it.
|
|
535
543
|
def last_advancing_event(runtime, interpreter, process_manager, correlation)
|
|
536
544
|
entry = runtime.registry.saga_log.reverse_each.find do |row|
|
|
@@ -544,20 +552,24 @@ module Hecks
|
|
|
544
552
|
end
|
|
545
553
|
end
|
|
546
554
|
|
|
547
|
-
#
|
|
555
|
+
# One (process manager, correlation)'s own redelivery check — pulled
|
|
548
556
|
# out of `check_saga_idempotency` itself so that method's own
|
|
549
557
|
# `flat_map`/`filter_map` walk stays readable; every local this
|
|
550
558
|
# shares with its caller (`interpreter`, `anchor`) is passed in
|
|
551
|
-
# rather than re-derived.
|
|
559
|
+
# rather than re-derived. The `saga_log` mark/restore pair (BUG#39
|
|
560
|
+
# fix) belongs right where it guards `interpreter.advance`, not in
|
|
561
|
+
# a helper a reader would have to jump to just to see what is and
|
|
562
|
+
# isn't being restored around that one call.
|
|
552
563
|
def check_one_saga_redelivery(runtime, interpreter, domain_name, process_manager, anchor,
|
|
553
564
|
correlation, saga, redelivery)
|
|
565
|
+
# rubocop:disable-next Metrics/BlockLength
|
|
554
566
|
Dir.mktmpdir("hecks-self-consistency-saga") do |tmp|
|
|
555
|
-
writer =
|
|
567
|
+
writer = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
|
|
556
568
|
writer.save_saga(process_manager: process_manager.name, correlation: correlation.to_s,
|
|
557
569
|
state: saga[:state], memory: saga[:memory], completed_compensations: [])
|
|
558
570
|
|
|
559
|
-
rehydrated =
|
|
560
|
-
|
|
571
|
+
rehydrated = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
|
|
572
|
+
.each_saga.find { |_pm, corr, *| corr == correlation.to_s }
|
|
561
573
|
next unless rehydrated
|
|
562
574
|
|
|
563
575
|
_pm, _corr, state, memory, compensations = rehydrated
|
|
@@ -565,6 +577,17 @@ module Hecks
|
|
|
565
577
|
|
|
566
578
|
saga_instances = runtime.registry.saga_instances[process_manager.name]
|
|
567
579
|
original = saga_instances[correlation]
|
|
580
|
+
|
|
581
|
+
# `saga_log` is `runtime.sagas`/`history[:sagas]` itself, by
|
|
582
|
+
# reference (`Dispatcher#sagas` — `@registry.saga_log`).
|
|
583
|
+
# `interpreter.advance` below is a real `advance_saga` dispatch,
|
|
584
|
+
# which appends its own row to this same array unconditionally —
|
|
585
|
+
# a true fact about this probe, but an unrestored append leaks
|
|
586
|
+
# into the primary trace (BUG#39). Marked here, sliced back off
|
|
587
|
+
# in `ensure`, same restore-what-I-mutated idiom as
|
|
588
|
+
# `saga_instances[correlation]` below.
|
|
589
|
+
saga_log = runtime.registry.saga_log
|
|
590
|
+
saga_log_mark = saga_log.size
|
|
568
591
|
begin
|
|
569
592
|
saga_instances[correlation] = { state: state, memory: memory, completed_compensations: compensations || [] }
|
|
570
593
|
interpreter.advance(redelivery, domain_name, only: process_manager)
|
|
@@ -576,6 +599,8 @@ module Hecks
|
|
|
576
599
|
{ field: "saga_redelivery_idempotency", domain: domain_name, process_manager: process_manager.name,
|
|
577
600
|
correlation: correlation, on: redelivery.name, before: before, after: after_shape }
|
|
578
601
|
ensure
|
|
602
|
+
saga_log.slice!(saga_log_mark..) if saga_log.size > saga_log_mark
|
|
603
|
+
|
|
579
604
|
if original
|
|
580
605
|
saga_instances[correlation] = original
|
|
581
606
|
else
|
|
@@ -585,13 +610,13 @@ module Hecks
|
|
|
585
610
|
end
|
|
586
611
|
end
|
|
587
612
|
|
|
588
|
-
#
|
|
589
|
-
# adapter at `tmp`),
|
|
590
|
-
# instance at the
|
|
613
|
+
# One fold of `entries` into `writer` (a real, already-open `Heki`
|
|
614
|
+
# adapter at `tmp`), then a cold read back through a brand new `Heki`
|
|
615
|
+
# instance at the same path — `@store` on a fresh instance starts
|
|
591
616
|
# unmemoized, so `#all` below is forced through `#read` →
|
|
592
617
|
# `#read_snapshot`/`#replay_journal`, real disk bytes, not whatever
|
|
593
618
|
# `writer` still holds cached in its own process memory. Called
|
|
594
|
-
#
|
|
619
|
+
# twice in a row against the same `writer` (see `check`/
|
|
595
620
|
# `check_idempotency` above) is exactly "replay the same journal a
|
|
596
621
|
# second time" — `writer` already holds everything the first fold
|
|
597
622
|
# wrote, so a second fold re-applies the identical operations on
|
|
@@ -602,29 +627,33 @@ module Hecks
|
|
|
602
627
|
writer.project(entry)
|
|
603
628
|
end
|
|
604
629
|
|
|
605
|
-
|
|
606
|
-
|
|
630
|
+
guarded_heki(aggregate: aggregate, root: tmp).all
|
|
631
|
+
.to_h do |record|
|
|
632
|
+
[
|
|
633
|
+
record.id.to_s, Runtime::Value.materialize(record.state)
|
|
634
|
+
]
|
|
635
|
+
end
|
|
607
636
|
end
|
|
608
637
|
|
|
609
|
-
#
|
|
638
|
+
# Recurses through a `Value`'s own fields via `#[]`, not `#to_h` —
|
|
610
639
|
# `#to_h` already materializes every field (`Value.materialize`),
|
|
611
640
|
# which would hide a nested `Value` from this walk before it ever
|
|
612
|
-
# got here. In practice a value object's
|
|
641
|
+
# got here. In practice a value object's own composite fields
|
|
613
642
|
# (`Coercion#normalize_composite_fields`) are validated but stored
|
|
614
643
|
# as plain, already-materialized Hashes, not re-wrapped `Value`
|
|
615
644
|
# instances — confirmed live, not assumed — so this recursion finds
|
|
616
|
-
# nothing further past the field it started from
|
|
645
|
+
# nothing further past the field it started from for today's
|
|
617
646
|
# coercion pipeline specifically. Kept anyway, not dead code: the
|
|
618
647
|
# generic `Hash`/`Array` branches below reach the exact same nested
|
|
619
648
|
# data through `node[attribute.name]` regardless, and a future
|
|
620
|
-
# change that
|
|
649
|
+
# change that does start wrapping composite fields as real `Value`
|
|
621
650
|
# instances would be walked correctly here with no change needed.
|
|
622
|
-
# `seen` is a `compare_by_identity` Hash: the same
|
|
651
|
+
# `seen` is a `compare_by_identity` Hash: the same instance can legitimately
|
|
623
652
|
# appear more than once (an aggregate's live state and an event
|
|
624
653
|
# payload both reference the exact same frozen object), and
|
|
625
654
|
# checking it twice would just waste time, never change the
|
|
626
655
|
# answer — identity, not `Value#==`, is the right notion of
|
|
627
|
-
# "already found" here (two
|
|
656
|
+
# "already found" here (two different value objects that happen to
|
|
628
657
|
# hold equal fields are still two separate round trips to prove).
|
|
629
658
|
def walk_value_objects(node, found, seen, aggregate)
|
|
630
659
|
case node
|