hecks 1.2.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 +60 -2
- 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/postgres_era.adapter +5 -0
- 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 +145 -0
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +51 -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 +27 -3
- 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 +122 -86
- 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 +301 -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 +183 -20
- 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 +361 -0
- 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 +124 -0
- data/lib/hecks/fuzzing/form_census.rb +199 -0
- 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 +226 -38
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
- data/lib/hecks/fuzzing/properties/guards.rb +129 -26
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
- data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +84 -39
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +224 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +676 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +39 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
- data/lib/hecks/fuzzing/value_generator.rb +66 -14
- data/lib/hecks/fuzzing.rb +11 -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 +365 -15
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +89 -21
- 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 +160 -58
- 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 +44 -6
- 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 +338 -83
- 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 +29 -6
- 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 +250 -0
- data/lib/hecks/projections/glossary/markdown.rb +105 -0
- data/lib/hecks/projections/glossary/mermaid.rb +110 -0
- data/lib/hecks/projections/glossary/page.css +271 -0
- data/lib/hecks/projections/glossary/page.js +72 -0
- data/lib/hecks/projections/glossary/sections.rb +17 -0
- data/lib/hecks/projections/glossary/sentences.rb +205 -0
- data/lib/hecks/projections/glossary.rb +214 -286
- 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 +15 -22
- 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 +132 -24
- 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 +64 -67
- 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 +94 -19
- 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 +282 -46
- data/lib/hecks/runtime/entity_interpreter.rb +160 -69
- 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 +111 -56
- data/lib/hecks/runtime/reaction_invocation.rb +76 -9
- 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 -112
- 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 +56 -27
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -88
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +93 -56
- 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 +379 -226
- data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
- data/lib/hecks/runtime/value.rb +28 -23
- 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 +207 -5
- 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 +43 -2
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
require_relative "errors"
|
|
2
|
+
require_relative "../deprecation"
|
|
2
3
|
require_relative "refusal_wording"
|
|
3
4
|
require_relative "caller"
|
|
4
|
-
require_relative "
|
|
5
|
+
require_relative "invocation"
|
|
5
6
|
require_relative "command_rules"
|
|
6
7
|
require_relative "command_interpreter"
|
|
7
8
|
require_relative "entity_interpreter"
|
|
@@ -25,6 +26,8 @@ module Hecks
|
|
|
25
26
|
MAX_REACTION_DEPTH = 5
|
|
26
27
|
|
|
27
28
|
Result = Struct.new(:verb, :instance, :events, :execution_plan, :persistence_outcome, keyword_init: true) do
|
|
29
|
+
# Reads the identity of the record the dispatch settled on.
|
|
30
|
+
#
|
|
28
31
|
# `instance` is nil for a port operation dispatched by verb (below)
|
|
29
32
|
# — nothing was hydrated or saved, the same reason
|
|
30
33
|
# `PortOperationInterpreter#emit`'s own comment gives for sourcing
|
|
@@ -33,7 +36,15 @@ module Hecks
|
|
|
33
36
|
# asks this Result for `.id`/`.state` made a category error the
|
|
34
37
|
# domain itself already told it about (there is no record here),
|
|
35
38
|
# not a crash-worthy one.
|
|
39
|
+
#
|
|
40
|
+
# @return [String, nil] the record's identity; nil for a port operation, which
|
|
41
|
+
# hydrates no record
|
|
36
42
|
def id = instance&.id
|
|
43
|
+
|
|
44
|
+
# Reads the settled record's attributes as one Hash, for the same nil-safe reason as `id`.
|
|
45
|
+
#
|
|
46
|
+
# @return [Hash{Symbol => Object}, nil] the record's state with `:id` merged in
|
|
47
|
+
# last; nil for a port operation, which hydrates no record
|
|
37
48
|
def state = instance&.to_h
|
|
38
49
|
|
|
39
50
|
def to_s
|
|
@@ -46,6 +57,8 @@ module Hecks
|
|
|
46
57
|
|
|
47
58
|
attr_reader :registry
|
|
48
59
|
|
|
60
|
+
# @param registry [Runtime::Registry] the booted registry every interpreter reads; its
|
|
61
|
+
# outbox is attached to this dispatcher's policy and saga interpreters
|
|
49
62
|
def initialize(registry)
|
|
50
63
|
@registry = registry
|
|
51
64
|
rules = CommandRules.new(registry)
|
|
@@ -56,7 +69,7 @@ module Hecks
|
|
|
56
69
|
@read_models = ReadModelInterpreter.new(registry)
|
|
57
70
|
@policies = PolicyInterpreter.new(registry, door: self)
|
|
58
71
|
@sagas = SagaInterpreter.new(registry, door: self)
|
|
59
|
-
#
|
|
72
|
+
# The relay is the registry's, not this dispatcher's — the
|
|
60
73
|
# interpreters enqueue through `@registry.outbox` from inside
|
|
61
74
|
# the save transaction, and this dispatcher drains through the
|
|
62
75
|
# same object, so there is exactly one relay per registry no
|
|
@@ -65,20 +78,128 @@ module Hecks
|
|
|
65
78
|
@registry.outbox.attach(policies: @policies, sagas: @sagas)
|
|
66
79
|
end
|
|
67
80
|
|
|
81
|
+
# Exposes the registry's outbox relay, the object this dispatcher drains reactions through.
|
|
82
|
+
#
|
|
68
83
|
# `runtime.outbox.rows`, `.rows(status: "claimed")`, `.redrive!`,
|
|
69
84
|
# `.log` — see `Runtime::Outbox`.
|
|
85
|
+
#
|
|
86
|
+
# @return [Runtime::Outbox::Relay] the registry's one relay, shared by every dispatcher
|
|
87
|
+
# fronting that registry
|
|
70
88
|
def outbox = @registry.outbox
|
|
71
89
|
|
|
90
|
+
# Exposes every event emitted through this registry since boot or the last reset.
|
|
91
|
+
#
|
|
92
|
+
# @return [Array<Runtime::Event>] the registry's live event log, oldest first
|
|
72
93
|
def events = @registry.event_log
|
|
73
94
|
|
|
95
|
+
# Exposes one record per policy reaction that was delivered, refused or left undelivered.
|
|
96
|
+
#
|
|
97
|
+
# @return [Array<Hash{Symbol => Object}>] the registry's live reaction log, oldest first
|
|
74
98
|
def reactions = @registry.reaction_log
|
|
75
99
|
|
|
100
|
+
# Exposes one record per process-manager step: a start, an advance, a delivery, a refusal.
|
|
101
|
+
#
|
|
102
|
+
# @return [Array<Hash{Symbol => Object}>] the registry's live saga log, oldest first
|
|
76
103
|
def sagas = @registry.saga_log
|
|
104
|
+
|
|
105
|
+
# Exposes the raw inputs each saga dispatch bound its arguments from, a Ruby-only log.
|
|
106
|
+
#
|
|
107
|
+
# @return [Array<Hash{Symbol => Object}>] the registry's live saga dispatch log, oldest first
|
|
77
108
|
def saga_dispatches = @registry.saga_dispatch_log
|
|
109
|
+
|
|
110
|
+
# Exposes the raw inputs each policy trigger bound its arguments from, a Ruby-only log.
|
|
111
|
+
#
|
|
112
|
+
# @return [Array<Hash{Symbol => Object}>] the registry's live policy dispatch log, with
|
|
113
|
+
# keys `:policy`, `:on`, `:payload`, `:with_spec` and `:args`
|
|
78
114
|
def policy_dispatches = @registry.policy_dispatch_log
|
|
115
|
+
|
|
116
|
+
# Lists every verb the loaded bluebooks declare.
|
|
117
|
+
#
|
|
118
|
+
# @return [Array<String>] the verbs of every loaded bluebook, sorted
|
|
79
119
|
def verbs = @registry.verbs
|
|
80
120
|
|
|
121
|
+
# Runs one command, entity command or port operation, then every policy and saga reaction
|
|
122
|
+
# its events are owed.
|
|
123
|
+
#
|
|
124
|
+
# Loose keyword facts are deprecated (roadmap I3) — `dispatch(verb,
|
|
125
|
+
# amount: 5)` still works, and warns once per call site; pass
|
|
126
|
+
# `with: { amount: 5 }` (and the receiver in `to:`) instead. The
|
|
127
|
+
# keyword door closes in `LEGACY_ARGS_REMOVAL`. `bin/codemod_legacy_
|
|
128
|
+
# dispatch_args` rewrites existing callers.
|
|
129
|
+
#
|
|
130
|
+
# @param verb [String] the fully qualified verb: `"Domain::Aggregate.Command"`,
|
|
131
|
+
# `"Domain::Aggregate.Entity.Command"` or `"Domain::Aggregate.Port.Operation"`
|
|
132
|
+
# @param to [String, Hash, nil] the receiver: an aggregate identity, or an entity route
|
|
133
|
+
# Hash with `:aggregate` and one of `:entity`/`:entities`; nil when the facts carry
|
|
134
|
+
# the identity themselves
|
|
135
|
+
# @param with [Hash, nil] the command's facts, keyed by argument name (String or Symbol);
|
|
136
|
+
# may not be combined with loose keyword facts
|
|
137
|
+
# @param saga_correlation [Hash, nil] correlation head => value, stamped on every
|
|
138
|
+
# emitted event when a saga leg causes this dispatch; nil otherwise
|
|
139
|
+
# @param legacy_args [Hash{Symbol => Object}] deprecated loose keyword facts
|
|
140
|
+
# @return [Runtime::Dispatcher::Result] the verb, settled instance (nil for a port
|
|
141
|
+
# operation), emitted events, execution plan and persistence outcome
|
|
142
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
|
|
143
|
+
# aggregate, command, entity or port operation that is not declared
|
|
144
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the domain refuses
|
|
145
|
+
# the call (`GivenNotMet`, `TypeMismatch`, `Unauthorized`, `NotFound`, …)
|
|
146
|
+
# @raise [Runtime::StaleWrite] if concurrent writers beat this one through every retry
|
|
147
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
148
|
+
# @raise [Deprecation::Error] if loose keyword facts are given where
|
|
149
|
+
# `Deprecation.raise_on!(:legacy_dispatch_args)` applies
|
|
81
150
|
def dispatch(verb, to: nil, with: nil, saga_correlation: nil, **legacy_args)
|
|
151
|
+
Dispatcher.deprecate_loose_facts(legacy_args)
|
|
152
|
+
dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, legacy_args: legacy_args)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
LEGACY_ARGS_REMOVAL = "1.5.0".freeze
|
|
156
|
+
LEGACY_ARGS_WARNING =
|
|
157
|
+
"passing command facts to dispatch as loose keyword arguments is deprecated and will be removed in " \
|
|
158
|
+
"hecks #{LEGACY_ARGS_REMOVAL} — pass them as `with: { ... }`, with the receiver identity in `to:` " \
|
|
159
|
+
"(bin/codemod_legacy_dispatch_args rewrites existing callers)".freeze
|
|
160
|
+
|
|
161
|
+
# Warns, once per call site, that a dispatch passed its facts as loose keyword arguments.
|
|
162
|
+
#
|
|
163
|
+
# @param legacy_args [Hash] the loose keyword facts a dispatch received; empty means
|
|
164
|
+
# nothing deprecated was used, and nothing is warned
|
|
165
|
+
# @return [nil] always, whether or not a warning was issued
|
|
166
|
+
# @raise [Deprecation::Error] if `legacy_args` is not empty where
|
|
167
|
+
# `Deprecation.raise_on!(:legacy_dispatch_args)` applies
|
|
168
|
+
def self.deprecate_loose_facts(legacy_args)
|
|
169
|
+
Deprecation.call(:legacy_dispatch_args, LEGACY_ARGS_WARNING) unless legacy_args.empty?
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Dispatches a verb whose receiver and facts arrive together in one flat Hash.
|
|
173
|
+
#
|
|
174
|
+
# **The flat-facts wire form** — one Hash, not keywords, and not
|
|
175
|
+
# deprecated: the shape `spec/corpus/*.json` steps, the Rust kernel's
|
|
176
|
+
# `cli.rs` contract, a reaction without a `with:` projection, and the
|
|
177
|
+
# self-hosted meta-domain all carry. Routes exactly as
|
|
178
|
+
# `dispatch(verb, **args)` does: a Symbol `:to`, `:with` or
|
|
179
|
+
# `:saga_correlation` key is lifted out as that keyword, everything
|
|
180
|
+
# else is a fact (so a String "to" key stays a fact).
|
|
181
|
+
# Framework code that replays data calls this; application code
|
|
182
|
+
# calls `dispatch(verb, to:, with:)`.
|
|
183
|
+
#
|
|
184
|
+
# @param verb [String] the fully qualified verb, in any shape `dispatch` accepts
|
|
185
|
+
# @param args [Hash] the facts, plus optional Symbol keys `:to`, `:with` and
|
|
186
|
+
# `:saga_correlation`, read as `dispatch`'s keywords of the same names; not mutated
|
|
187
|
+
# @return [Runtime::Dispatcher::Result] the same result `dispatch` returns
|
|
188
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
|
|
189
|
+
# aggregate, command, entity or port operation that is not declared
|
|
190
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the domain refuses
|
|
191
|
+
# the call
|
|
192
|
+
# @raise [Runtime::StaleWrite] if concurrent writers beat this one through every retry
|
|
193
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
194
|
+
def dispatch_flat(verb, args = {})
|
|
195
|
+
facts = args.dup
|
|
196
|
+
to = facts.delete(:to)
|
|
197
|
+
with = facts.delete(:with)
|
|
198
|
+
saga_correlation = facts.delete(:saga_correlation)
|
|
199
|
+
dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, legacy_args: facts)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def dispatch_invocation(verb, to:, with:, saga_correlation:, legacy_args:)
|
|
82
203
|
domain, aggregate_name, command_name = parse(verb)
|
|
83
204
|
aggregate = resolve_aggregate(domain, aggregate_name, verb)
|
|
84
205
|
|
|
@@ -86,7 +207,7 @@ module Hecks
|
|
|
86
207
|
if command_name.include?(".")
|
|
87
208
|
head, sub = command_name.split(".", 2)
|
|
88
209
|
port = aggregate.port(head)
|
|
89
|
-
# A
|
|
210
|
+
# A port operation, reached by the same verb shape an entity
|
|
90
211
|
# command already uses ("Domain::Aggregate.Head.Rest") — ports
|
|
91
212
|
# are checked first, so an aggregate that ever declared both a
|
|
92
213
|
# port and an entity of the same name would resolve to the
|
|
@@ -99,34 +220,35 @@ module Hecks
|
|
|
99
220
|
# path.
|
|
100
221
|
if port
|
|
101
222
|
operation = port.operation(sub) ||
|
|
102
|
-
raise(UnknownVerb, RefusalWording.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
223
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "port_no_operation",
|
|
224
|
+
port: head, operation: sub))
|
|
225
|
+
invocation = Invocation.from_call(verb, to: to, with: with, legacy: legacy_args,
|
|
226
|
+
receiver: :port, aggregate: aggregate) { operation }
|
|
227
|
+
[nil, @port_ops.call(domain, aggregate, operation, invocation), nil, nil, :enqueue]
|
|
106
228
|
else
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
229
|
+
resolution = nil
|
|
230
|
+
invocation = Invocation.from_call(verb, to: to, with: with, legacy: legacy_args,
|
|
231
|
+
receiver: :entity, entity_depth: command_name.count(".")) do
|
|
232
|
+
(resolution = EntityInterpreter::Resolution.of(aggregate, command_name)).command
|
|
233
|
+
end
|
|
234
|
+
@entities.call(domain, aggregate, resolution, invocation)
|
|
110
235
|
end
|
|
111
236
|
else
|
|
112
|
-
command = aggregate
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
args = Routing.payload(command, with: with, legacy: legacy_args)
|
|
116
|
-
route = Routing.envelope(to)
|
|
117
|
-
@commands.call(domain, aggregate, command, args, saga_correlation, route: route)
|
|
237
|
+
command = command_of(aggregate, aggregate_name, command_name)
|
|
238
|
+
invocation = Invocation.from_call(verb, to: to, with: with, legacy: legacy_args) { command }
|
|
239
|
+
@commands.call(domain, aggregate, command, invocation, saga_correlation)
|
|
118
240
|
end
|
|
119
241
|
|
|
120
|
-
# Correlation is
|
|
121
|
-
# it is part of the transaction, known from this method's
|
|
122
|
-
# argument before a single event exists.
|
|
123
|
-
#
|
|
124
|
-
#
|
|
242
|
+
# Correlation is set when each event is constructed, not merged on
|
|
243
|
+
# here — it is part of the transaction, known from this method's
|
|
244
|
+
# own argument before a single event exists. Stamping it onto
|
|
245
|
+
# already-emitted events would keep an event mutable after it had
|
|
246
|
+
# happened.
|
|
125
247
|
#
|
|
126
|
-
# The ordering
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
248
|
+
# The ordering that depends on it holds for the same reason:
|
|
249
|
+
# `SagaInterpreter#advance` runs on this domain's `announced`
|
|
250
|
+
# events within this very call, and finds the correlation already
|
|
251
|
+
# there because it was never absent.
|
|
130
252
|
|
|
131
253
|
react(announced, domain, aggregate, outbox_rows)
|
|
132
254
|
|
|
@@ -134,7 +256,12 @@ module Hecks
|
|
|
134
256
|
execution_plan: execution_plan, persistence_outcome: persistence_outcome)
|
|
135
257
|
end
|
|
136
258
|
|
|
137
|
-
#
|
|
259
|
+
# **The one body both doors run** — `dispatch` (keywords) and
|
|
260
|
+
# `dispatch_flat` (one Hash) differ only in how the call's parts are
|
|
261
|
+
# spelled, never in what happens next.
|
|
262
|
+
private :dispatch_invocation
|
|
263
|
+
|
|
264
|
+
# **Everything owed because `announced` committed** — policies first,
|
|
138
265
|
# then sagas, the order this method always ran them in. The
|
|
139
266
|
# command/entity interpreters hand back the outbox rows they
|
|
140
267
|
# enqueued inside the save transaction (`Interpreting#
|
|
@@ -152,9 +279,11 @@ module Hecks
|
|
|
152
279
|
end
|
|
153
280
|
private :react
|
|
154
281
|
|
|
155
|
-
#
|
|
282
|
+
# Answers whether a command would succeed right now, without saving, emitting or reacting.
|
|
283
|
+
#
|
|
284
|
+
# "If this were dispatched right now, would it succeed" — the same
|
|
156
285
|
# pipeline #dispatch itself runs (arguments coerced, givens checked,
|
|
157
|
-
# mutations applied
|
|
286
|
+
# mutations applied in memory, ensures checked against the settled
|
|
158
287
|
# result), except `step_save`/`step_emit` never run, and neither do
|
|
159
288
|
# policies or sagas afterward: nothing here is committed, so nothing
|
|
160
289
|
# should react to it. Built for exactly the shape a whole-board
|
|
@@ -164,15 +293,26 @@ module Hecks
|
|
|
164
293
|
# own move purely to trigger the check, which then had to avoid
|
|
165
294
|
# interfering with the very position being tested).
|
|
166
295
|
#
|
|
167
|
-
#
|
|
296
|
+
# Raises the same refusals #dispatch does — a DomainRefusal
|
|
168
297
|
# subclass propagates normally, so a caller checking "would this be
|
|
169
298
|
# legal" writes the identical rescue clause a real dispatch already
|
|
170
299
|
# needs; this returns `true` only when nothing was refused.
|
|
171
300
|
#
|
|
172
|
-
#
|
|
301
|
+
# **Never a port verb** — `PortOperationInterpreter`'s own side effects
|
|
173
302
|
# (an external gateway call, say) have no meaningful in-memory-only
|
|
174
303
|
# form, so this refuses one outright rather than silently running
|
|
175
304
|
# it for real, which "dry" would otherwise quietly lie about.
|
|
305
|
+
#
|
|
306
|
+
# @param verb [String] the fully qualified aggregate or entity command verb
|
|
307
|
+
# @param args [Hash{Symbol => Object}] the command's facts as flat keywords; a key named
|
|
308
|
+
# `to` or `with` is an ordinary fact here, never routing (BUG#131)
|
|
309
|
+
# @return [true] whenever nothing refused; a refusal is raised, never returned as false
|
|
310
|
+
# @raise [Runtime::WiringError] if the verb names a port operation, or the aggregate's
|
|
311
|
+
# repository cannot be resolved
|
|
312
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
|
|
313
|
+
# aggregate, command or entity that is not declared
|
|
314
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` that the real dispatch
|
|
315
|
+
# would raise
|
|
176
316
|
def dry_run?(verb, **args)
|
|
177
317
|
domain, aggregate_name, command_name = parse(verb)
|
|
178
318
|
aggregate = resolve_aggregate(domain, aggregate_name, verb)
|
|
@@ -185,18 +325,25 @@ module Hecks
|
|
|
185
325
|
"only for aggregate and entity commands"
|
|
186
326
|
end
|
|
187
327
|
|
|
188
|
-
|
|
328
|
+
# `to:`/`with:` are not keywords of this method — a key named
|
|
329
|
+
# either is an ordinary fact here (BUG#131), so both go in as nil.
|
|
330
|
+
resolution = nil
|
|
331
|
+
invocation = Invocation.from_call(verb, to: nil, with: nil, legacy: args, receiver: :entity) do
|
|
332
|
+
(resolution = EntityInterpreter::Resolution.of(aggregate, command_name)).command
|
|
333
|
+
end
|
|
334
|
+
@entities.call(domain, aggregate, resolution, invocation, dry_run: true)
|
|
189
335
|
else
|
|
190
|
-
command = aggregate
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
@commands.call(domain, aggregate, command, args, dry_run: true)
|
|
336
|
+
command = command_of(aggregate, aggregate_name, command_name)
|
|
337
|
+
invocation = Invocation.from_call(verb, to: nil, with: nil, legacy: args) { command }
|
|
338
|
+
@commands.call(domain, aggregate, command, invocation, dry_run: true)
|
|
194
339
|
end
|
|
195
340
|
|
|
196
341
|
true
|
|
197
342
|
end
|
|
198
343
|
|
|
199
|
-
#
|
|
344
|
+
# Runs one port operation named by its parts, then the reactions its events are owed.
|
|
345
|
+
#
|
|
346
|
+
# The door an adapter outside the bluebook calls through — never the
|
|
200
347
|
# domain itself. `port_name`/`operation_name` are separate arguments
|
|
201
348
|
# rather than one packed verb string on purpose: there is no established
|
|
202
349
|
# wire spelling for "domain, aggregate, port, operation" yet, and
|
|
@@ -205,66 +352,69 @@ module Hecks
|
|
|
205
352
|
# No adapter-to-port binding lookup happens here — that is
|
|
206
353
|
# `Hecks.adapter`'s existing job (unchanged by this), and wiring "which
|
|
207
354
|
# adapter may call this port" through is the next piece, not this one.
|
|
355
|
+
#
|
|
356
|
+
# @param domain [String, Symbol] name of the domain the aggregate belongs to
|
|
357
|
+
# @param aggregate_name [String, Symbol] name of the aggregate that declares the port
|
|
358
|
+
# @param port_name [String] name of the port, as the aggregate declares it
|
|
359
|
+
# @param operation_name [String] name of the operation on that port
|
|
360
|
+
# @param to [String, Hash, nil] the receiving aggregate's identity; when nil it is read
|
|
361
|
+
# from the facts, by the operation's reference or identity attribute
|
|
362
|
+
# @param with [Hash, nil] the operation's facts, keyed by argument name
|
|
363
|
+
# @param legacy_args [Hash{Symbol => Object}] deprecated loose keyword facts
|
|
364
|
+
# @return [Array<Runtime::Event>] the events the operation announced: one per declared
|
|
365
|
+
# `emits` for an inbound operation, the one answering or refusing event for an
|
|
366
|
+
# outbound one
|
|
367
|
+
# @raise [Runtime::UnknownVerb] if the domain, aggregate, port or operation is not declared
|
|
368
|
+
# @raise [Runtime::TypeMismatch] if no receiving identity can be found, or `to:`/`with:`
|
|
369
|
+
# is malformed
|
|
370
|
+
# @raise [Runtime::NotFound] if the receiving aggregate record does not exist
|
|
371
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
372
|
+
# @raise [Deprecation::Error] if loose keyword facts are given where
|
|
373
|
+
# `Deprecation.raise_on!(:legacy_dispatch_args)` applies
|
|
208
374
|
def dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, **legacy_args)
|
|
375
|
+
Dispatcher.deprecate_loose_facts(legacy_args)
|
|
209
376
|
aggregate = resolve_aggregate(domain, aggregate_name, "#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}")
|
|
210
377
|
port = aggregate.port(port_name) ||
|
|
211
378
|
raise(UnknownVerb, "#{aggregate_name} has no port #{port_name.inspect}")
|
|
212
379
|
operation = port.operation(operation_name) ||
|
|
213
380
|
raise(UnknownVerb, "#{port_name} has no operation #{operation_name.inspect}")
|
|
214
381
|
|
|
215
|
-
|
|
216
|
-
|
|
382
|
+
invocation = Invocation.from_call("#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}",
|
|
383
|
+
to: to, with: with, legacy: legacy_args,
|
|
384
|
+
receiver: :port, aggregate: aggregate) { operation }
|
|
385
|
+
announced = @port_ops.call(domain, aggregate, operation, invocation)
|
|
217
386
|
|
|
218
387
|
react(announced, domain, aggregate, :enqueue)
|
|
219
388
|
|
|
220
389
|
announced
|
|
221
390
|
end
|
|
222
391
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
# READ, NOT deleted — unlike the Reference-attribute branch
|
|
242
|
-
# above, this is a genuine declared operation attribute (Rust's
|
|
243
|
-
# own comment: "declare only external facts with attribute"),
|
|
244
|
-
# not synthetic routing-only state; the operation's own
|
|
245
|
-
# attributes still expect to find it in the payload a few steps
|
|
246
|
-
# later (refuse_absent_arguments), and a real, live
|
|
247
|
-
# AbsentArgument confirmed this the hard way before `[]`
|
|
248
|
-
# replaced `delete`.
|
|
249
|
-
identity_name = Array(aggregate.identified_by).first
|
|
250
|
-
to = legacy[identity_name] if identity_name && legacy.key?(identity_name)
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
route = Routing.envelope(to)
|
|
254
|
-
raise TypeMismatch, "#{operation.hecks_name} requires its receiving aggregate in to:" unless route
|
|
255
|
-
|
|
256
|
-
[route, Routing.payload(operation, with: with, legacy: legacy)]
|
|
257
|
-
end
|
|
258
|
-
private :port_invocation
|
|
259
|
-
|
|
392
|
+
# Answers a declared query: an aggregate query, an entity query, or a read model.
|
|
393
|
+
#
|
|
394
|
+
# The verb's shape picks the interpreter. `"Domain.ReadModel"` — no `::` before the
|
|
395
|
+
# dot — is a read model; `"Domain::Aggregate.Query"` is an aggregate query, and
|
|
396
|
+
# `"Domain::Aggregate.Entity.Query"` an entity query.
|
|
397
|
+
#
|
|
398
|
+
# @param verb [String, Symbol] the query's verb, in one of the three shapes above
|
|
399
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
400
|
+
# @return [Array<Hash>] for an aggregate query, one deep-frozen Hash per matching record,
|
|
401
|
+
# its state with `:id` merged in last; for an entity query, one Hash per matching
|
|
402
|
+
# element with the parent's reference key merged in first; for a read model, a
|
|
403
|
+
# one-element Array holding a Hash of head name to projected rows
|
|
404
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
|
|
405
|
+
# aggregate, entity, query or read model that is not declared
|
|
406
|
+
# @raise [Runtime::NotFound] if a read model's root reference names no record
|
|
407
|
+
# @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
|
|
408
|
+
# @raise [KeyError] if a rooted read model is asked without its reference argument
|
|
409
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
260
410
|
def query(verb, **args)
|
|
261
411
|
domain, query_name = verb.to_s.split(".", 2)
|
|
262
412
|
if query_name && !domain.include?("::")
|
|
263
413
|
bluebook = @registry.bluebook(domain) ||
|
|
264
|
-
raise(UnknownVerb, RefusalWording.
|
|
414
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_domain", domain: domain, verb: verb))
|
|
265
415
|
model = bluebook.read_model(query_name) ||
|
|
266
|
-
raise(UnknownVerb, RefusalWording.
|
|
267
|
-
|
|
416
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_read_model",
|
|
417
|
+
domain: domain, query: query_name))
|
|
268
418
|
return @read_models.call(domain, model, args)
|
|
269
419
|
end
|
|
270
420
|
|
|
@@ -274,10 +424,22 @@ module Hecks
|
|
|
274
424
|
@queries.call(domain, aggregate, query_name, args)
|
|
275
425
|
end
|
|
276
426
|
|
|
277
|
-
#
|
|
278
|
-
#
|
|
279
|
-
#
|
|
280
|
-
#
|
|
427
|
+
# Answers an aggregate or entity query through the reference interpreter alone.
|
|
428
|
+
#
|
|
429
|
+
# The same ask as `#query`, never answered by the bound adapter's
|
|
430
|
+
# native hook. Read models have no reference twin, so only the
|
|
431
|
+
# `"Domain::Aggregate.Query"` forms answer here; the fuzzer's
|
|
432
|
+
# query oracle diffs this against `#query`'s answer.
|
|
433
|
+
#
|
|
434
|
+
# @param verb [String] the fully qualified query verb, `"Domain::Aggregate.Query"` or
|
|
435
|
+
# `"Domain::Aggregate.Entity.Query"`
|
|
436
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
437
|
+
# @return [Array<Hash>] one Hash per matching record, its state with `:id` merged in
|
|
438
|
+
# last; for an entity query, one Hash per matching element
|
|
439
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
|
|
440
|
+
# aggregate, entity or query that is not declared
|
|
441
|
+
# @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
|
|
442
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
281
443
|
def reference_query(verb, **args)
|
|
282
444
|
domain, aggregate_name, query_name = parse(verb)
|
|
283
445
|
aggregate = resolve_aggregate(domain, aggregate_name, verb)
|
|
@@ -285,7 +447,10 @@ module Hecks
|
|
|
285
447
|
@queries.reference_call(domain, aggregate, query_name, args)
|
|
286
448
|
end
|
|
287
449
|
|
|
288
|
-
#
|
|
450
|
+
# Dispatches a reaction's command one level deeper in the cascade, as the system rather
|
|
451
|
+
# than as the triggering caller.
|
|
452
|
+
#
|
|
453
|
+
# A reaction is the system acting, not the caller who happened to be
|
|
289
454
|
# on the stack when the triggering command ran — the ambient caller
|
|
290
455
|
# is cleared for the reaction's own dispatch, so a triggering
|
|
291
456
|
# caller's role can neither satisfy nor block a reaction command it
|
|
@@ -296,39 +461,67 @@ module Hecks
|
|
|
296
461
|
# dispatching through it (a Puma worker pool, say), so a plain ivar
|
|
297
462
|
# here is exactly the known Puma-concurrency bug class: two
|
|
298
463
|
# concurrent top-level dispatches on different threads would
|
|
299
|
-
# increment/decrement the
|
|
464
|
+
# increment/decrement the same counter, letting one thread's nested
|
|
300
465
|
# reaction depth leak into another thread's unrelated dispatch. A
|
|
301
466
|
# `Mutex` is not the answer either — a reaction cascade re-enters
|
|
302
|
-
# `reenter` on the
|
|
467
|
+
# `reenter` on the same thread (see `SagaInterpreter#advance_saga`'s
|
|
303
468
|
# own comment on why a non-reentrant `Mutex` can't guard this).
|
|
304
469
|
# `Thread.current`-backed, saved/restored around the call with a
|
|
305
470
|
# plain local + `ensure`, is the same idiom `Runtime::Caller`
|
|
306
471
|
# (`caller.rb`) already established for exactly this shape of
|
|
307
472
|
# per-thread ambient state.
|
|
473
|
+
#
|
|
474
|
+
# The depth is not checked here: a reacting interpreter asks
|
|
475
|
+
# `reaction_depth_reached?` first and records an undelivered reaction
|
|
476
|
+
# instead of calling this.
|
|
477
|
+
#
|
|
478
|
+
# @param verb [String] the fully qualified verb of the reaction's target command
|
|
479
|
+
# @param saga_correlation [Hash{String => Object}, nil] correlation head => value when a
|
|
480
|
+
# saga leg dispatches; nil for a policy
|
|
481
|
+
# @param args [Hash{Symbol => Object}] the flat facts, read exactly as `dispatch_flat`
|
|
482
|
+
# reads them, so `:to` and `:with` keys route rather than count as facts
|
|
483
|
+
# @return [Runtime::Dispatcher::Result] the result of the nested dispatch
|
|
484
|
+
# @raise [Runtime::UnknownVerb] if the verb names nothing declared
|
|
485
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the target
|
|
486
|
+
# refuses; the policy and saga interpreters rescue these as recorded outcomes
|
|
308
487
|
def reenter(verb, saga_correlation: nil, **args)
|
|
309
488
|
depth = Thread.current[:hecks_reaction_depth].to_i
|
|
310
489
|
Thread.current[:hecks_reaction_depth] = depth + 1
|
|
311
|
-
Caller.without {
|
|
490
|
+
Caller.without { dispatch_flat(verb, args.merge(saga_correlation: saga_correlation)) }
|
|
312
491
|
ensure
|
|
313
492
|
Thread.current[:hecks_reaction_depth] = depth
|
|
314
493
|
end
|
|
315
494
|
|
|
495
|
+
# Answers whether the calling thread's reaction cascade is as deep as it may go.
|
|
496
|
+
#
|
|
497
|
+
# @return [Boolean] true once this thread's nested `reenter` calls number
|
|
498
|
+
# `MAX_REACTION_DEPTH` or more; other threads' cascades are not counted
|
|
316
499
|
def reaction_depth_reached? = Thread.current[:hecks_reaction_depth].to_i >= MAX_REACTION_DEPTH
|
|
500
|
+
|
|
501
|
+
# Reads the cascade limit, for the reason an interpreter records when it declines to react.
|
|
502
|
+
#
|
|
503
|
+
# @return [Integer] `MAX_REACTION_DEPTH`, the number of nested reactions allowed
|
|
317
504
|
def max_reaction_depth = MAX_REACTION_DEPTH
|
|
318
505
|
|
|
319
506
|
private
|
|
320
507
|
|
|
321
508
|
def parse(verb)
|
|
322
509
|
Naming.split_verb(verb) ||
|
|
323
|
-
raise(UnknownVerb, RefusalWording.
|
|
510
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "not_fully_qualified", verb: verb))
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def command_of(aggregate, aggregate_name, command_name)
|
|
514
|
+
aggregate.command(command_name) ||
|
|
515
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "aggregate_no_command",
|
|
516
|
+
aggregate: aggregate_name, command: command_name))
|
|
324
517
|
end
|
|
325
518
|
|
|
326
519
|
def resolve_aggregate(domain, aggregate_name, verb)
|
|
327
520
|
bluebook = @registry.bluebook(domain) ||
|
|
328
|
-
raise(UnknownVerb, RefusalWording.
|
|
521
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_domain", domain: domain, verb: verb))
|
|
329
522
|
bluebook.aggregate(aggregate_name) ||
|
|
330
|
-
raise(UnknownVerb, RefusalWording.
|
|
331
|
-
|
|
523
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_aggregate",
|
|
524
|
+
domain: domain, aggregate: aggregate_name))
|
|
332
525
|
end
|
|
333
526
|
end
|
|
334
527
|
end
|