hecks 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require_relative "errors"
|
|
2
2
|
require_relative "refusal_wording"
|
|
3
3
|
require_relative "caller"
|
|
4
|
-
require_relative "
|
|
4
|
+
require_relative "invocation"
|
|
5
5
|
require_relative "command_rules"
|
|
6
6
|
require_relative "command_interpreter"
|
|
7
7
|
require_relative "entity_interpreter"
|
|
@@ -25,6 +25,8 @@ module Hecks
|
|
|
25
25
|
MAX_REACTION_DEPTH = 5
|
|
26
26
|
|
|
27
27
|
Result = Struct.new(:verb, :instance, :events, :execution_plan, :persistence_outcome, keyword_init: true) do
|
|
28
|
+
# Reads the identity of the record the dispatch settled on.
|
|
29
|
+
#
|
|
28
30
|
# `instance` is nil for a port operation dispatched by verb (below)
|
|
29
31
|
# — nothing was hydrated or saved, the same reason
|
|
30
32
|
# `PortOperationInterpreter#emit`'s own comment gives for sourcing
|
|
@@ -33,7 +35,15 @@ module Hecks
|
|
|
33
35
|
# asks this Result for `.id`/`.state` made a category error the
|
|
34
36
|
# domain itself already told it about (there is no record here),
|
|
35
37
|
# not a crash-worthy one.
|
|
38
|
+
#
|
|
39
|
+
# @return [String, nil] the record's identity; nil for a port operation, which
|
|
40
|
+
# hydrates no record
|
|
36
41
|
def id = instance&.id
|
|
42
|
+
|
|
43
|
+
# Reads the settled record's attributes as one Hash, for the same nil-safe reason as `id`.
|
|
44
|
+
#
|
|
45
|
+
# @return [Hash{Symbol => Object}, nil] the record's state with `:id` merged in
|
|
46
|
+
# last; nil for a port operation, which hydrates no record
|
|
37
47
|
def state = instance&.to_h
|
|
38
48
|
|
|
39
49
|
def to_s
|
|
@@ -46,6 +56,8 @@ module Hecks
|
|
|
46
56
|
|
|
47
57
|
attr_reader :registry
|
|
48
58
|
|
|
59
|
+
# @param registry [Runtime::Registry] the booted registry every interpreter reads; its
|
|
60
|
+
# outbox is attached to this dispatcher's policy and saga interpreters
|
|
49
61
|
def initialize(registry)
|
|
50
62
|
@registry = registry
|
|
51
63
|
rules = CommandRules.new(registry)
|
|
@@ -56,7 +68,7 @@ module Hecks
|
|
|
56
68
|
@read_models = ReadModelInterpreter.new(registry)
|
|
57
69
|
@policies = PolicyInterpreter.new(registry, door: self)
|
|
58
70
|
@sagas = SagaInterpreter.new(registry, door: self)
|
|
59
|
-
#
|
|
71
|
+
# The relay is the registry's, not this dispatcher's — the
|
|
60
72
|
# interpreters enqueue through `@registry.outbox` from inside
|
|
61
73
|
# the save transaction, and this dispatcher drains through the
|
|
62
74
|
# same object, so there is exactly one relay per registry no
|
|
@@ -65,20 +77,108 @@ module Hecks
|
|
|
65
77
|
@registry.outbox.attach(policies: @policies, sagas: @sagas)
|
|
66
78
|
end
|
|
67
79
|
|
|
80
|
+
# Exposes the registry's outbox relay, the object this dispatcher drains reactions through.
|
|
81
|
+
#
|
|
68
82
|
# `runtime.outbox.rows`, `.rows(status: "claimed")`, `.redrive!`,
|
|
69
83
|
# `.log` — see `Runtime::Outbox`.
|
|
84
|
+
#
|
|
85
|
+
# @return [Runtime::Outbox::Relay] the registry's one relay, shared by every dispatcher
|
|
86
|
+
# fronting that registry
|
|
70
87
|
def outbox = @registry.outbox
|
|
71
88
|
|
|
89
|
+
# Exposes every event emitted through this registry since boot or the last reset.
|
|
90
|
+
#
|
|
91
|
+
# @return [Array<Runtime::Event>] the registry's live event log, oldest first
|
|
72
92
|
def events = @registry.event_log
|
|
73
93
|
|
|
94
|
+
# Exposes one record per policy reaction that was delivered, refused or left undelivered.
|
|
95
|
+
#
|
|
96
|
+
# @return [Array<Hash{Symbol => Object}>] the registry's live reaction log, oldest first
|
|
74
97
|
def reactions = @registry.reaction_log
|
|
75
98
|
|
|
99
|
+
# Exposes one record per process-manager step: a start, an advance, a delivery, a refusal.
|
|
100
|
+
#
|
|
101
|
+
# @return [Array<Hash{Symbol => Object}>] the registry's live saga log, oldest first
|
|
76
102
|
def sagas = @registry.saga_log
|
|
103
|
+
|
|
104
|
+
# Exposes the raw inputs each saga dispatch bound its arguments from, a Ruby-only log.
|
|
105
|
+
#
|
|
106
|
+
# @return [Array<Hash{Symbol => Object}>] the registry's live saga dispatch log, oldest first
|
|
77
107
|
def saga_dispatches = @registry.saga_dispatch_log
|
|
108
|
+
|
|
109
|
+
# Exposes the raw inputs each policy trigger bound its arguments from, a Ruby-only log.
|
|
110
|
+
#
|
|
111
|
+
# @return [Array<Hash{Symbol => Object}>] the registry's live policy dispatch log, with
|
|
112
|
+
# keys `:policy`, `:on`, `:payload`, `:with_spec` and `:args`
|
|
78
113
|
def policy_dispatches = @registry.policy_dispatch_log
|
|
114
|
+
|
|
115
|
+
# Lists every verb the loaded bluebooks declare.
|
|
116
|
+
#
|
|
117
|
+
# @return [Array<String>] the verbs of every loaded bluebook, sorted
|
|
79
118
|
def verbs = @registry.verbs
|
|
80
119
|
|
|
81
|
-
|
|
120
|
+
# Runs one command, entity command or port operation, then every policy and saga reaction
|
|
121
|
+
# its events are owed.
|
|
122
|
+
#
|
|
123
|
+
# The receiver in `to:`, the facts in `with:`, and nothing else.
|
|
124
|
+
# Loose keyword facts — `dispatch(verb, amount: 5)`, one bag holding
|
|
125
|
+
# both the route and the payload — were deprecated in 1.3.x and are
|
|
126
|
+
# gone: Ruby now refuses them itself, by name ("unknown keyword:
|
|
127
|
+
# :amount"). Code holding a bag of data rather than written keywords
|
|
128
|
+
# calls `dispatch_flat` below; that door is not going anywhere.
|
|
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
|
+
# @param saga_correlation [Hash, nil] correlation head => value, stamped on every
|
|
137
|
+
# emitted event when a saga leg causes this dispatch; nil otherwise
|
|
138
|
+
# @return [Runtime::Dispatcher::Result] the verb, settled instance (nil for a port
|
|
139
|
+
# operation), emitted events, execution plan and persistence outcome
|
|
140
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
|
|
141
|
+
# aggregate, command, entity or port operation that is not declared
|
|
142
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the domain refuses
|
|
143
|
+
# the call (`GivenNotMet`, `TypeMismatch`, `Unauthorized`, `NotFound`, …)
|
|
144
|
+
# @raise [Runtime::StaleWrite] if concurrent writers beat this one through every retry
|
|
145
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
146
|
+
def dispatch(verb, to: nil, with: nil, saga_correlation: nil)
|
|
147
|
+
dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, flat: {})
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Dispatches a verb whose receiver and facts arrive together in one flat Hash.
|
|
151
|
+
#
|
|
152
|
+
# **The flat-facts wire form** — one Hash, not keywords: the shape
|
|
153
|
+
# `spec/corpus/*.json` steps, the Rust kernel's `cli.rs` contract, a
|
|
154
|
+
# reaction without a `with:` projection, and the self-hosted
|
|
155
|
+
# meta-domain all carry, where the receiver's identity is one of the
|
|
156
|
+
# keys because that is how the wire spells it. Routes exactly as the
|
|
157
|
+
# removed keyword door did: a Symbol `:to`, `:with` or
|
|
158
|
+
# `:saga_correlation` key is lifted out as that keyword, everything
|
|
159
|
+
# else is a fact (so a String "to" key stays a fact, as it did).
|
|
160
|
+
# Framework code that replays data calls this; application code
|
|
161
|
+
# calls `dispatch(verb, to:, with:)`.
|
|
162
|
+
#
|
|
163
|
+
# @param verb [String] the fully qualified verb, in any shape `dispatch` accepts
|
|
164
|
+
# @param args [Hash] the facts, plus optional Symbol keys `:to`, `:with` and
|
|
165
|
+
# `:saga_correlation`, read as `dispatch`'s keywords of the same names; not mutated
|
|
166
|
+
# @return [Runtime::Dispatcher::Result] the same result `dispatch` returns
|
|
167
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
|
|
168
|
+
# aggregate, command, entity or port operation that is not declared
|
|
169
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the domain refuses
|
|
170
|
+
# the call
|
|
171
|
+
# @raise [Runtime::StaleWrite] if concurrent writers beat this one through every retry
|
|
172
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
173
|
+
def dispatch_flat(verb, args = {})
|
|
174
|
+
facts = args.dup
|
|
175
|
+
to = facts.delete(:to)
|
|
176
|
+
with = facts.delete(:with)
|
|
177
|
+
saga_correlation = facts.delete(:saga_correlation)
|
|
178
|
+
dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, flat: facts)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def dispatch_invocation(verb, to:, with:, saga_correlation:, flat:)
|
|
82
182
|
domain, aggregate_name, command_name = parse(verb)
|
|
83
183
|
aggregate = resolve_aggregate(domain, aggregate_name, verb)
|
|
84
184
|
|
|
@@ -86,7 +186,7 @@ module Hecks
|
|
|
86
186
|
if command_name.include?(".")
|
|
87
187
|
head, sub = command_name.split(".", 2)
|
|
88
188
|
port = aggregate.port(head)
|
|
89
|
-
# A
|
|
189
|
+
# A port operation, reached by the same verb shape an entity
|
|
90
190
|
# command already uses ("Domain::Aggregate.Head.Rest") — ports
|
|
91
191
|
# are checked first, so an aggregate that ever declared both a
|
|
92
192
|
# port and an entity of the same name would resolve to the
|
|
@@ -99,34 +199,35 @@ module Hecks
|
|
|
99
199
|
# path.
|
|
100
200
|
if port
|
|
101
201
|
operation = port.operation(sub) ||
|
|
102
|
-
raise(UnknownVerb, RefusalWording.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
202
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "port_no_operation",
|
|
203
|
+
port: head, operation: sub))
|
|
204
|
+
invocation = Invocation.from_call(verb, to: to, with: with, flat: flat,
|
|
205
|
+
receiver: :port, aggregate: aggregate) { operation }
|
|
206
|
+
[nil, @port_ops.call(domain, aggregate, operation, invocation), nil, nil, :enqueue]
|
|
106
207
|
else
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
208
|
+
resolution = nil
|
|
209
|
+
invocation = Invocation.from_call(verb, to: to, with: with, flat: flat,
|
|
210
|
+
receiver: :entity, entity_depth: command_name.count(".")) do
|
|
211
|
+
(resolution = EntityInterpreter::Resolution.of(aggregate, command_name)).command
|
|
212
|
+
end
|
|
213
|
+
@entities.call(domain, aggregate, resolution, invocation)
|
|
110
214
|
end
|
|
111
215
|
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)
|
|
216
|
+
command = command_of(aggregate, aggregate_name, command_name)
|
|
217
|
+
invocation = Invocation.from_call(verb, to: to, with: with, flat: flat) { command }
|
|
218
|
+
@commands.call(domain, aggregate, command, invocation, saga_correlation)
|
|
118
219
|
end
|
|
119
220
|
|
|
120
|
-
# Correlation is
|
|
121
|
-
# it is part of the transaction, known from this method's
|
|
122
|
-
# argument before a single event exists.
|
|
123
|
-
#
|
|
124
|
-
#
|
|
221
|
+
# Correlation is set when each event is constructed, not merged on
|
|
222
|
+
# here — it is part of the transaction, known from this method's
|
|
223
|
+
# own argument before a single event exists. Stamping it onto
|
|
224
|
+
# already-emitted events would keep an event mutable after it had
|
|
225
|
+
# happened.
|
|
125
226
|
#
|
|
126
|
-
# The ordering
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
227
|
+
# The ordering that depends on it holds for the same reason:
|
|
228
|
+
# `SagaInterpreter#advance` runs on this domain's `announced`
|
|
229
|
+
# events within this very call, and finds the correlation already
|
|
230
|
+
# there because it was never absent.
|
|
130
231
|
|
|
131
232
|
react(announced, domain, aggregate, outbox_rows)
|
|
132
233
|
|
|
@@ -134,7 +235,12 @@ module Hecks
|
|
|
134
235
|
execution_plan: execution_plan, persistence_outcome: persistence_outcome)
|
|
135
236
|
end
|
|
136
237
|
|
|
137
|
-
#
|
|
238
|
+
# **The one body both doors run** — `dispatch` (keywords) and
|
|
239
|
+
# `dispatch_flat` (one Hash) differ only in how the call's parts are
|
|
240
|
+
# spelled, never in what happens next.
|
|
241
|
+
private :dispatch_invocation
|
|
242
|
+
|
|
243
|
+
# **Everything owed because `announced` committed** — policies first,
|
|
138
244
|
# then sagas, the order this method always ran them in. The
|
|
139
245
|
# command/entity interpreters hand back the outbox rows they
|
|
140
246
|
# enqueued inside the save transaction (`Interpreting#
|
|
@@ -152,9 +258,11 @@ module Hecks
|
|
|
152
258
|
end
|
|
153
259
|
private :react
|
|
154
260
|
|
|
155
|
-
#
|
|
261
|
+
# Answers whether a command would succeed right now, without saving, emitting or reacting.
|
|
262
|
+
#
|
|
263
|
+
# "If this were dispatched right now, would it succeed" — the same
|
|
156
264
|
# pipeline #dispatch itself runs (arguments coerced, givens checked,
|
|
157
|
-
# mutations applied
|
|
265
|
+
# mutations applied in memory, ensures checked against the settled
|
|
158
266
|
# result), except `step_save`/`step_emit` never run, and neither do
|
|
159
267
|
# policies or sagas afterward: nothing here is committed, so nothing
|
|
160
268
|
# should react to it. Built for exactly the shape a whole-board
|
|
@@ -164,15 +272,26 @@ module Hecks
|
|
|
164
272
|
# own move purely to trigger the check, which then had to avoid
|
|
165
273
|
# interfering with the very position being tested).
|
|
166
274
|
#
|
|
167
|
-
#
|
|
275
|
+
# Raises the same refusals #dispatch does — a DomainRefusal
|
|
168
276
|
# subclass propagates normally, so a caller checking "would this be
|
|
169
277
|
# legal" writes the identical rescue clause a real dispatch already
|
|
170
278
|
# needs; this returns `true` only when nothing was refused.
|
|
171
279
|
#
|
|
172
|
-
#
|
|
280
|
+
# **Never a port verb** — `PortOperationInterpreter`'s own side effects
|
|
173
281
|
# (an external gateway call, say) have no meaningful in-memory-only
|
|
174
282
|
# form, so this refuses one outright rather than silently running
|
|
175
283
|
# it for real, which "dry" would otherwise quietly lie about.
|
|
284
|
+
#
|
|
285
|
+
# @param verb [String] the fully qualified aggregate or entity command verb
|
|
286
|
+
# @param args [Hash{Symbol => Object}] the command's facts as flat keywords; a key named
|
|
287
|
+
# `to` or `with` is an ordinary fact here, never routing (BUG#131)
|
|
288
|
+
# @return [true] whenever nothing refused; a refusal is raised, never returned as false
|
|
289
|
+
# @raise [Runtime::WiringError] if the verb names a port operation, or the aggregate's
|
|
290
|
+
# repository cannot be resolved
|
|
291
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
|
|
292
|
+
# aggregate, command or entity that is not declared
|
|
293
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` that the real dispatch
|
|
294
|
+
# would raise
|
|
176
295
|
def dry_run?(verb, **args)
|
|
177
296
|
domain, aggregate_name, command_name = parse(verb)
|
|
178
297
|
aggregate = resolve_aggregate(domain, aggregate_name, verb)
|
|
@@ -185,18 +304,25 @@ module Hecks
|
|
|
185
304
|
"only for aggregate and entity commands"
|
|
186
305
|
end
|
|
187
306
|
|
|
188
|
-
|
|
307
|
+
# `to:`/`with:` are not keywords of this method — a key named
|
|
308
|
+
# either is an ordinary fact here (BUG#131), so both go in as nil.
|
|
309
|
+
resolution = nil
|
|
310
|
+
invocation = Invocation.from_call(verb, to: nil, with: nil, flat: args, receiver: :entity) do
|
|
311
|
+
(resolution = EntityInterpreter::Resolution.of(aggregate, command_name)).command
|
|
312
|
+
end
|
|
313
|
+
@entities.call(domain, aggregate, resolution, invocation, dry_run: true)
|
|
189
314
|
else
|
|
190
|
-
command = aggregate
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
@commands.call(domain, aggregate, command, args, dry_run: true)
|
|
315
|
+
command = command_of(aggregate, aggregate_name, command_name)
|
|
316
|
+
invocation = Invocation.from_call(verb, to: nil, with: nil, flat: args) { command }
|
|
317
|
+
@commands.call(domain, aggregate, command, invocation, dry_run: true)
|
|
194
318
|
end
|
|
195
319
|
|
|
196
320
|
true
|
|
197
321
|
end
|
|
198
322
|
|
|
199
|
-
#
|
|
323
|
+
# Runs one port operation named by its parts, then the reactions its events are owed.
|
|
324
|
+
#
|
|
325
|
+
# The door an adapter outside the bluebook calls through — never the
|
|
200
326
|
# domain itself. `port_name`/`operation_name` are separate arguments
|
|
201
327
|
# rather than one packed verb string on purpose: there is no established
|
|
202
328
|
# wire spelling for "domain, aggregate, port, operation" yet, and
|
|
@@ -205,66 +331,68 @@ module Hecks
|
|
|
205
331
|
# No adapter-to-port binding lookup happens here — that is
|
|
206
332
|
# `Hecks.adapter`'s existing job (unchanged by this), and wiring "which
|
|
207
333
|
# adapter may call this port" through is the next piece, not this one.
|
|
208
|
-
|
|
334
|
+
#
|
|
335
|
+
# @param domain [String, Symbol] name of the domain the aggregate belongs to
|
|
336
|
+
# @param aggregate_name [String, Symbol] name of the aggregate that declares the port
|
|
337
|
+
# @param port_name [String] name of the port, as the aggregate declares it
|
|
338
|
+
# @param operation_name [String] name of the operation on that port
|
|
339
|
+
# @param to [String, Hash, nil] the receiving aggregate's identity; when nil it is read
|
|
340
|
+
# from the facts, by the operation's reference or identity attribute
|
|
341
|
+
# @param with [Hash, nil] the operation's facts, keyed by argument name
|
|
342
|
+
# @param flat [Hash] the wire form `dispatch_flat` takes, for the driving adapter
|
|
343
|
+
# holding a decoded webhook rather than written keywords; it is where the operation's
|
|
344
|
+
# own reference attribute is read from and lifted into `to:` when `to:` is nil
|
|
345
|
+
# @return [Array<Runtime::Event>] the events the operation announced: one per declared
|
|
346
|
+
# `emits` for an inbound operation, the one answering or refusing event for an
|
|
347
|
+
# outbound one
|
|
348
|
+
# @raise [Runtime::UnknownVerb] if the domain, aggregate, port or operation is not declared
|
|
349
|
+
# @raise [Runtime::TypeMismatch] if no receiving identity can be found, or `to:`/`with:`
|
|
350
|
+
# is malformed
|
|
351
|
+
# @raise [Runtime::NotFound] if the receiving aggregate record does not exist
|
|
352
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
353
|
+
def dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, flat: {})
|
|
209
354
|
aggregate = resolve_aggregate(domain, aggregate_name, "#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}")
|
|
210
355
|
port = aggregate.port(port_name) ||
|
|
211
356
|
raise(UnknownVerb, "#{aggregate_name} has no port #{port_name.inspect}")
|
|
212
357
|
operation = port.operation(operation_name) ||
|
|
213
358
|
raise(UnknownVerb, "#{port_name} has no operation #{operation_name.inspect}")
|
|
214
359
|
|
|
215
|
-
|
|
216
|
-
|
|
360
|
+
invocation = Invocation.from_call("#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}",
|
|
361
|
+
to: to, with: with, flat: flat,
|
|
362
|
+
receiver: :port, aggregate: aggregate) { operation }
|
|
363
|
+
announced = @port_ops.call(domain, aggregate, operation, invocation)
|
|
217
364
|
|
|
218
365
|
react(announced, domain, aggregate, :enqueue)
|
|
219
366
|
|
|
220
367
|
announced
|
|
221
368
|
end
|
|
222
369
|
|
|
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
|
-
|
|
370
|
+
# Answers a declared query: an aggregate query, an entity query, or a read model.
|
|
371
|
+
#
|
|
372
|
+
# The verb's shape picks the interpreter. `"Domain.ReadModel"` — no `::` before the
|
|
373
|
+
# dot — is a read model; `"Domain::Aggregate.Query"` is an aggregate query, and
|
|
374
|
+
# `"Domain::Aggregate.Entity.Query"` an entity query.
|
|
375
|
+
#
|
|
376
|
+
# @param verb [String, Symbol] the query's verb, in one of the three shapes above
|
|
377
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
378
|
+
# @return [Array<Hash>] for an aggregate query, one deep-frozen Hash per matching record,
|
|
379
|
+
# its state with `:id` merged in last; for an entity query, one Hash per matching
|
|
380
|
+
# element with the parent's reference key merged in first; for a read model, a
|
|
381
|
+
# one-element Array holding a Hash of head name to projected rows
|
|
382
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
|
|
383
|
+
# aggregate, entity, query or read model that is not declared
|
|
384
|
+
# @raise [Runtime::NotFound] if a read model's root reference names no record
|
|
385
|
+
# @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
|
|
386
|
+
# @raise [KeyError] if a rooted read model is asked without its reference argument
|
|
387
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
260
388
|
def query(verb, **args)
|
|
261
389
|
domain, query_name = verb.to_s.split(".", 2)
|
|
262
390
|
if query_name && !domain.include?("::")
|
|
263
391
|
bluebook = @registry.bluebook(domain) ||
|
|
264
|
-
raise(UnknownVerb, RefusalWording.
|
|
392
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_domain", domain: domain, verb: verb))
|
|
265
393
|
model = bluebook.read_model(query_name) ||
|
|
266
|
-
raise(UnknownVerb, RefusalWording.
|
|
267
|
-
|
|
394
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_read_model",
|
|
395
|
+
domain: domain, query: query_name))
|
|
268
396
|
return @read_models.call(domain, model, args)
|
|
269
397
|
end
|
|
270
398
|
|
|
@@ -274,10 +402,22 @@ module Hecks
|
|
|
274
402
|
@queries.call(domain, aggregate, query_name, args)
|
|
275
403
|
end
|
|
276
404
|
|
|
277
|
-
#
|
|
278
|
-
#
|
|
279
|
-
#
|
|
280
|
-
#
|
|
405
|
+
# Answers an aggregate or entity query through the reference interpreter alone.
|
|
406
|
+
#
|
|
407
|
+
# The same ask as `#query`, never answered by the bound adapter's
|
|
408
|
+
# native hook. Read models have no reference twin, so only the
|
|
409
|
+
# `"Domain::Aggregate.Query"` forms answer here; the fuzzer's
|
|
410
|
+
# query oracle diffs this against `#query`'s answer.
|
|
411
|
+
#
|
|
412
|
+
# @param verb [String] the fully qualified query verb, `"Domain::Aggregate.Query"` or
|
|
413
|
+
# `"Domain::Aggregate.Entity.Query"`
|
|
414
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
415
|
+
# @return [Array<Hash>] one Hash per matching record, its state with `:id` merged in
|
|
416
|
+
# last; for an entity query, one Hash per matching element
|
|
417
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
|
|
418
|
+
# aggregate, entity or query that is not declared
|
|
419
|
+
# @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
|
|
420
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
281
421
|
def reference_query(verb, **args)
|
|
282
422
|
domain, aggregate_name, query_name = parse(verb)
|
|
283
423
|
aggregate = resolve_aggregate(domain, aggregate_name, verb)
|
|
@@ -285,7 +425,10 @@ module Hecks
|
|
|
285
425
|
@queries.reference_call(domain, aggregate, query_name, args)
|
|
286
426
|
end
|
|
287
427
|
|
|
288
|
-
#
|
|
428
|
+
# Dispatches a reaction's command one level deeper in the cascade, as the system rather
|
|
429
|
+
# than as the triggering caller.
|
|
430
|
+
#
|
|
431
|
+
# A reaction is the system acting, not the caller who happened to be
|
|
289
432
|
# on the stack when the triggering command ran — the ambient caller
|
|
290
433
|
# is cleared for the reaction's own dispatch, so a triggering
|
|
291
434
|
# caller's role can neither satisfy nor block a reaction command it
|
|
@@ -296,39 +439,67 @@ module Hecks
|
|
|
296
439
|
# dispatching through it (a Puma worker pool, say), so a plain ivar
|
|
297
440
|
# here is exactly the known Puma-concurrency bug class: two
|
|
298
441
|
# concurrent top-level dispatches on different threads would
|
|
299
|
-
# increment/decrement the
|
|
442
|
+
# increment/decrement the same counter, letting one thread's nested
|
|
300
443
|
# reaction depth leak into another thread's unrelated dispatch. A
|
|
301
444
|
# `Mutex` is not the answer either — a reaction cascade re-enters
|
|
302
|
-
# `reenter` on the
|
|
445
|
+
# `reenter` on the same thread (see `SagaInterpreter#advance_saga`'s
|
|
303
446
|
# own comment on why a non-reentrant `Mutex` can't guard this).
|
|
304
447
|
# `Thread.current`-backed, saved/restored around the call with a
|
|
305
448
|
# plain local + `ensure`, is the same idiom `Runtime::Caller`
|
|
306
449
|
# (`caller.rb`) already established for exactly this shape of
|
|
307
450
|
# per-thread ambient state.
|
|
451
|
+
#
|
|
452
|
+
# The depth is not checked here: a reacting interpreter asks
|
|
453
|
+
# `reaction_depth_reached?` first and records an undelivered reaction
|
|
454
|
+
# instead of calling this.
|
|
455
|
+
#
|
|
456
|
+
# @param verb [String] the fully qualified verb of the reaction's target command
|
|
457
|
+
# @param saga_correlation [Hash{String => Object}, nil] correlation head => value when a
|
|
458
|
+
# saga leg dispatches; nil for a policy
|
|
459
|
+
# @param args [Hash{Symbol => Object}] the flat facts, read exactly as `dispatch_flat`
|
|
460
|
+
# reads them, so `:to` and `:with` keys route rather than count as facts
|
|
461
|
+
# @return [Runtime::Dispatcher::Result] the result of the nested dispatch
|
|
462
|
+
# @raise [Runtime::UnknownVerb] if the verb names nothing declared
|
|
463
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the target
|
|
464
|
+
# refuses; the policy and saga interpreters rescue these as recorded outcomes
|
|
308
465
|
def reenter(verb, saga_correlation: nil, **args)
|
|
309
466
|
depth = Thread.current[:hecks_reaction_depth].to_i
|
|
310
467
|
Thread.current[:hecks_reaction_depth] = depth + 1
|
|
311
|
-
Caller.without {
|
|
468
|
+
Caller.without { dispatch_flat(verb, args.merge(saga_correlation: saga_correlation)) }
|
|
312
469
|
ensure
|
|
313
470
|
Thread.current[:hecks_reaction_depth] = depth
|
|
314
471
|
end
|
|
315
472
|
|
|
473
|
+
# Answers whether the calling thread's reaction cascade is as deep as it may go.
|
|
474
|
+
#
|
|
475
|
+
# @return [Boolean] true once this thread's nested `reenter` calls number
|
|
476
|
+
# `MAX_REACTION_DEPTH` or more; other threads' cascades are not counted
|
|
316
477
|
def reaction_depth_reached? = Thread.current[:hecks_reaction_depth].to_i >= MAX_REACTION_DEPTH
|
|
478
|
+
|
|
479
|
+
# Reads the cascade limit, for the reason an interpreter records when it declines to react.
|
|
480
|
+
#
|
|
481
|
+
# @return [Integer] `MAX_REACTION_DEPTH`, the number of nested reactions allowed
|
|
317
482
|
def max_reaction_depth = MAX_REACTION_DEPTH
|
|
318
483
|
|
|
319
484
|
private
|
|
320
485
|
|
|
321
486
|
def parse(verb)
|
|
322
487
|
Naming.split_verb(verb) ||
|
|
323
|
-
raise(UnknownVerb, RefusalWording.
|
|
488
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "not_fully_qualified", verb: verb))
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def command_of(aggregate, aggregate_name, command_name)
|
|
492
|
+
aggregate.command(command_name) ||
|
|
493
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "aggregate_no_command",
|
|
494
|
+
aggregate: aggregate_name, command: command_name))
|
|
324
495
|
end
|
|
325
496
|
|
|
326
497
|
def resolve_aggregate(domain, aggregate_name, verb)
|
|
327
498
|
bluebook = @registry.bluebook(domain) ||
|
|
328
|
-
raise(UnknownVerb, RefusalWording.
|
|
499
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_domain", domain: domain, verb: verb))
|
|
329
500
|
bluebook.aggregate(aggregate_name) ||
|
|
330
|
-
raise(UnknownVerb, RefusalWording.
|
|
331
|
-
|
|
501
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_aggregate",
|
|
502
|
+
domain: domain, aggregate: aggregate_name))
|
|
332
503
|
end
|
|
333
504
|
end
|
|
334
505
|
end
|