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
|
@@ -9,19 +9,19 @@ require_relative "../ports/persistence/remote_runtime"
|
|
|
9
9
|
|
|
10
10
|
module Hecks
|
|
11
11
|
module Runtime
|
|
12
|
-
#
|
|
13
|
-
# constructs this
|
|
12
|
+
# **The write-side half of lambda routing** — `Runtime::Loader.boot`
|
|
13
|
+
# constructs this instead of `Dispatcher` when the booted domain's
|
|
14
14
|
# own `.world` declares Lambda routing (see loader.rb's own
|
|
15
15
|
# `dispatcher_class_for`). Matches `Dispatcher`'s public shape
|
|
16
|
-
# closely enough that everything built
|
|
16
|
+
# closely enough that everything built on top of a dispatcher —
|
|
17
17
|
# `Handle`, `AggregateDoor`, `Facade::Surface` — needs no changes
|
|
18
18
|
# at all: `Handle#run`'s entire contract is
|
|
19
|
-
# `@dispatcher.dispatch("#{fqn}.#{command}",
|
|
19
|
+
# `@dispatcher.dispatch("#{fqn}.#{command}", to: @id, with: args).instance.state`,
|
|
20
20
|
# and both classes answer that identically.
|
|
21
21
|
#
|
|
22
|
-
#
|
|
23
|
-
# to a
|
|
24
|
-
# registry's own repositories are
|
|
22
|
+
# **Reads delegate, writes don't**. `query`/`reference_query` hand off
|
|
23
|
+
# to a real `Dispatcher` built over the same registry — since that
|
|
24
|
+
# registry's own repositories are already Lambda-backed
|
|
25
25
|
# (`Adapters::Lambda`, `persisted_by("Lambda")`, Phase 2's other
|
|
26
26
|
# half), the inherited query machinery (`QueryInterpreter`,
|
|
27
27
|
# `Ports::Query::InMemory.execute`) works completely unchanged, no
|
|
@@ -31,49 +31,120 @@ module Hecks
|
|
|
31
31
|
# against incomplete local state and then merely persisted.
|
|
32
32
|
class RemoteDispatcher
|
|
33
33
|
Result = Struct.new(:verb, :instance, :events, keyword_init: true) do
|
|
34
|
+
# Reads the identity of the record the dispatch settled on.
|
|
35
|
+
#
|
|
36
|
+
# @return [String] the settled record's identity
|
|
34
37
|
def id = instance.id
|
|
38
|
+
|
|
39
|
+
# Reads the settled record's attributes as one Hash.
|
|
40
|
+
#
|
|
41
|
+
# @return [Hash{Symbol => Object}] the settled record's attributes, `:id` merged
|
|
42
|
+
# in last
|
|
35
43
|
def state = instance.to_h
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
attr_reader :registry
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
# @param registry [Runtime::Registry] the booted registry this dispatcher fronts;
|
|
49
|
+
# read-side calls (`query`/`reference_query`) delegate to a local `Dispatcher`
|
|
50
|
+
# built over the same registry
|
|
51
|
+
# @param region [String] the AWS region the routed Lambda function lives in
|
|
52
|
+
# @param function [String, nil] the `.world`'s own `dispatched_by("Lambda")`
|
|
53
|
+
# function name, when the deployment stack is not named `hecks-<domain>`; nil
|
|
54
|
+
# resolves the function name from `ENV["DOMAIN_NAME"]` or `registry.root`
|
|
55
|
+
def initialize(registry, region: "us-east-1", function: nil)
|
|
41
56
|
@registry = registry
|
|
42
57
|
# `File.basename(registry.root)`, not `bluebooks.keys.first` —
|
|
43
58
|
# matches `Adapters::Lambda`'s own function-name resolution
|
|
44
59
|
# exactly (see its own comment on why: one merged Lambda per
|
|
45
60
|
# deploy, not one per attached chapter, and `root` is the one
|
|
46
61
|
# signal every bluebook in this registry shares regardless of
|
|
47
|
-
# which one attached it) —
|
|
62
|
+
# which one attached it) — including that same adapter's own
|
|
48
63
|
# `ENV["DOMAIN_NAME"]`-first fix: `root` is always `/var/task`
|
|
49
64
|
# inside a deployed Lambda, giving "task" instead of the real
|
|
50
65
|
# domain name (a real, live AccessDeniedException on
|
|
51
66
|
# "hecks-task" caught this).
|
|
52
|
-
|
|
53
|
-
#
|
|
67
|
+
# `function:` — the `.world`'s own `dispatched_by("Lambda")`
|
|
68
|
+
# naming of which function this is, for a deployment whose stack
|
|
69
|
+
# name isn't `hecks-<domain>` (Client's own comment has the real
|
|
70
|
+
# case). Absent, the resolution above is unchanged.
|
|
71
|
+
@client = Adapters::Lambda::Client.new(domain: ENV["DOMAIN_NAME"] || File.basename(registry.root),
|
|
72
|
+
region: region, function: function)
|
|
73
|
+
# Read-side delegate only (see class comment) — never dispatched
|
|
54
74
|
# through; a real Dispatcher's own `query`/`reference_query`
|
|
55
75
|
# already resolve generically via `registry.repository(...)`,
|
|
56
76
|
# so building one here reuses that instead of duplicating it.
|
|
57
77
|
@local = Dispatcher.new(registry)
|
|
58
78
|
end
|
|
59
79
|
|
|
80
|
+
# Dispatches a command by verb, routing to the local `Dispatcher` or the
|
|
81
|
+
# remote Lambda depending on the aggregate's bound adapter.
|
|
82
|
+
#
|
|
83
|
+
# Same shape as `Dispatcher#dispatch_flat` — everything but
|
|
84
|
+
# `saga_correlation:` is forwarded through unread, `to:`/`with:`
|
|
85
|
+
# included, and lifted out downstream by whichever path actually
|
|
86
|
+
# dispatches (`@local.dispatch_flat` locally, the flat wire form
|
|
87
|
+
# remotely). Not the strict `to:`/`with:`-only door `Dispatcher#
|
|
88
|
+
# dispatch` is — see that class's own comment for why this file
|
|
89
|
+
# never had one.
|
|
90
|
+
#
|
|
91
|
+
# @param verb [String] the fully qualified verb, `"Domain::Aggregate.Command"`
|
|
92
|
+
# or `"Domain::Aggregate.Entity.Command"`
|
|
93
|
+
# @param saga_correlation [Hash, nil] correlation head => value, stamped on every
|
|
94
|
+
# emitted event when a saga leg causes this dispatch; nil otherwise
|
|
95
|
+
# @param args [Hash] the facts, plus optional `:to`/`:with` keys, read the same
|
|
96
|
+
# way `dispatch_flat` reads them
|
|
97
|
+
# @return [RemoteDispatcher::Result] the verb, settled instance and emitted events
|
|
98
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
|
|
99
|
+
# domain or aggregate that is not declared
|
|
100
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
|
|
101
|
+
# locally-routed aggregate refuses the call
|
|
102
|
+
# @raise [Runtime::StaleWrite] if concurrent local writers beat this one through
|
|
103
|
+
# every retry, for a locally-routed aggregate
|
|
104
|
+
# @raise [Runtime::WiringError] if the aggregate's adapter or repository cannot be
|
|
105
|
+
# resolved, or the remote call is accepted but reports no mutation for it
|
|
106
|
+
# @raise [Runtime::RemoteRefusal] if the routed Lambda refuses the call
|
|
60
107
|
def dispatch(verb, saga_correlation: nil, **args)
|
|
108
|
+
dispatch_flat(verb, args.merge(saga_correlation: saga_correlation))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Routes a dispatch to the local `Dispatcher` when the aggregate's bound
|
|
112
|
+
# adapter is not remote-backed (`Ports::Persistence::RemoteRuntime`),
|
|
113
|
+
# otherwise dispatches through the routed Lambda. Same flat-facts wire
|
|
114
|
+
# form as `Dispatcher#dispatch_flat`.
|
|
115
|
+
#
|
|
116
|
+
# @param verb [String] the fully qualified verb, in any shape `dispatch` accepts
|
|
117
|
+
# @param args [Hash] the facts, plus optional Symbol keys `:to`, `:with` and
|
|
118
|
+
# `:saga_correlation`, read as `dispatch`'s keywords of the same names; not mutated
|
|
119
|
+
# @return [RemoteDispatcher::Result] the verb, settled instance and emitted events
|
|
120
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
|
|
121
|
+
# domain or aggregate that is not declared
|
|
122
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
|
|
123
|
+
# locally-routed aggregate refuses the call
|
|
124
|
+
# @raise [Runtime::StaleWrite] if concurrent local writers beat this one through
|
|
125
|
+
# every retry, for a locally-routed aggregate
|
|
126
|
+
# @raise [Runtime::WiringError] if the aggregate's adapter or repository cannot be
|
|
127
|
+
# resolved, or the remote call is accepted but reports no mutation for it
|
|
128
|
+
# @raise [Runtime::RemoteRefusal] if the routed Lambda refuses the call
|
|
129
|
+
def dispatch_flat(verb, args = {})
|
|
130
|
+
args = args.dup
|
|
131
|
+
saga_correlation = args.delete(:saga_correlation)
|
|
61
132
|
domain, aggregate_name, = Naming.split_verb(verb) ||
|
|
62
133
|
raise(UnknownVerb,
|
|
63
|
-
RefusalWording.
|
|
134
|
+
RefusalWording.render_site("UnknownVerb", "not_fully_qualified", verb: verb))
|
|
64
135
|
aggregate = @registry.bluebook(domain)&.aggregate(aggregate_name) ||
|
|
65
136
|
raise(UnknownVerb,
|
|
66
|
-
RefusalWording.
|
|
137
|
+
RefusalWording.render_site("UnknownVerb", "no_aggregate", domain: domain, aggregate: aggregate_name))
|
|
67
138
|
|
|
68
|
-
#
|
|
69
|
-
#
|
|
139
|
+
# Not every aggregate in a lambda-routed domain is itself
|
|
140
|
+
# lambda-bound — Member's real name->email rekey carries a
|
|
70
141
|
# `compute` rule (era_check.rb's own `check_compute_rules!`),
|
|
71
142
|
# which can only ever run against Postgres, permanently. Its
|
|
72
|
-
#
|
|
143
|
+
# own `.hecksagon` bind stays "Postgres" even when
|
|
73
144
|
# `dispatched_by("Lambda")` is on for everything else — checked
|
|
74
|
-
# here by real
|
|
145
|
+
# here by real capability (`Ports::Persistence::RemoteRuntime`,
|
|
75
146
|
# §1), not by comparing the adapter's own name to the string
|
|
76
|
-
# "Lambda" — a bind resolves to whatever adapter
|
|
147
|
+
# "Lambda" — a bind resolves to whatever adapter class actually
|
|
77
148
|
# backs it, and only a class shaped like "the real interpreter
|
|
78
149
|
# lives behind a call boundary" forwards here; anything else
|
|
79
150
|
# (Postgres, Memory, any future local adapter) falls through to
|
|
@@ -82,7 +153,7 @@ module Hecks
|
|
|
82
153
|
# all.
|
|
83
154
|
adapter_name = Ports::Persistence::BindingPolicy.resolve(@registry, domain, aggregate).adapter
|
|
84
155
|
unless @registry.adapter_class(adapter_name) <= Ports::Persistence::RemoteRuntime
|
|
85
|
-
return @local.
|
|
156
|
+
return @local.dispatch_flat(verb, args.merge(saga_correlation: saga_correlation))
|
|
86
157
|
end
|
|
87
158
|
|
|
88
159
|
response = @client.dispatch(verb, args)
|
|
@@ -90,14 +161,14 @@ module Hecks
|
|
|
90
161
|
refusal = response.fetch("refusals", []).find { |r| r["verb"] == verb }
|
|
91
162
|
raise RemoteRefusal, "#{verb} refused: #{refusal['error']}" if refusal
|
|
92
163
|
|
|
93
|
-
#
|
|
164
|
+
# This step's own mutations — `mutations` is one entry per
|
|
94
165
|
# replayed step (rust/host's rehydrate-and-replay design,
|
|
95
166
|
# Phase 1), so `.last` is exactly the step just dispatched.
|
|
96
167
|
# Matched by fully-qualified aggregate name, not just "the
|
|
97
|
-
# first mutation" — a command whose reaction
|
|
168
|
+
# first mutation" — a command whose reaction also mutates a
|
|
98
169
|
# different aggregate (a policy, a saga leg) puts more than
|
|
99
170
|
# one mutation in the same step, and the direct effect of
|
|
100
|
-
#
|
|
171
|
+
# this verb is the one this dispatch's own caller expects
|
|
101
172
|
# `.instance` to be.
|
|
102
173
|
fqn = "#{domain}::#{aggregate.hecks_name}"
|
|
103
174
|
mutation = response.fetch("mutations", []).last&.find { |m| m["aggregate"] == fqn } ||
|
|
@@ -110,16 +181,46 @@ module Hecks
|
|
|
110
181
|
Result.new(verb: verb, instance: instance, events: step_events(response))
|
|
111
182
|
end
|
|
112
183
|
|
|
184
|
+
# Delegates to the local `Dispatcher` built over the same registry — see the
|
|
185
|
+
# class comment on why reads, unlike writes, need no remote-specific logic.
|
|
186
|
+
#
|
|
187
|
+
# @param verb [String, Symbol] the query's verb, in one of `Dispatcher#query`'s
|
|
188
|
+
# three shapes
|
|
189
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
190
|
+
# @return [Array<Hash>] see `Dispatcher#query`'s own return
|
|
191
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
|
|
192
|
+
# domain, aggregate, entity, query or read model that is not declared
|
|
193
|
+
# @raise [Runtime::NotFound] if a read model's root reference names no record
|
|
194
|
+
# @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
|
|
195
|
+
# @raise [KeyError] if a rooted read model is asked without its reference argument
|
|
196
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
113
197
|
def query(verb, **args) = @local.query(verb, **args)
|
|
198
|
+
|
|
199
|
+
# Delegates to the local `Dispatcher` built over the same registry — see the
|
|
200
|
+
# class comment on why reads, unlike writes, need no remote-specific logic.
|
|
201
|
+
#
|
|
202
|
+
# @param verb [String] the fully qualified query verb, `"Domain::Aggregate.Query"`
|
|
203
|
+
# or `"Domain::Aggregate.Entity.Query"`
|
|
204
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
205
|
+
# @return [Array<Hash>] one Hash per matching record, its state with `:id` merged
|
|
206
|
+
# in last; for an entity query, one Hash per matching element
|
|
207
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
|
|
208
|
+
# domain, aggregate, entity or query that is not declared
|
|
209
|
+
# @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
|
|
210
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
114
211
|
def reference_query(verb, **args) = @local.reference_query(verb, **args)
|
|
115
212
|
|
|
116
|
-
#
|
|
213
|
+
# Fetches the full domain's event history, on every call — `{"read":
|
|
117
214
|
# true}` replays the whole journal (Phase 1's `dispatch::read`),
|
|
118
|
-
# so its own `events` array already
|
|
215
|
+
# so its own `events` array already is the complete log, the
|
|
119
216
|
# same thing `@registry.event_log` would answer for a local
|
|
120
217
|
# dispatch. Not cached: `AggregateDoor.events`/`Handle#events`
|
|
121
218
|
# are not called in this codebase's own hot paths today: if that
|
|
122
219
|
# changes, caching belongs here, not in every caller.
|
|
220
|
+
#
|
|
221
|
+
# @return [Array<Runtime::Event>] every event in the routed Lambda's domain
|
|
222
|
+
# journal, oldest first, with `occurred_at` always nil (the kernel is
|
|
223
|
+
# timestamp-free by design)
|
|
123
224
|
def events
|
|
124
225
|
@client.read.fetch("events", []).map { |e| build_event(e) }
|
|
125
226
|
end
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
require_relative "errors"
|
|
2
|
-
require_relative "refusal_wording"
|
|
3
|
-
require_relative "../rendering"
|
|
4
|
-
|
|
5
1
|
module Hecks
|
|
6
2
|
module Runtime
|
|
7
3
|
# The invocation address is not part of a command's domain payload.
|
|
8
4
|
# Aggregate commands carry one receiver identity; entity commands carry
|
|
9
5
|
# the aggregate receiver followed by one identity for every entity hop.
|
|
6
|
+
#
|
|
7
|
+
# Reading a call's shape — what `to:` and `with:` mean, the BUG#7/#17/#18
|
|
8
|
+
# rules — lives in `Runtime::Invocation.from_call` now (invocation.rb).
|
|
9
|
+
# `envelope`/`payload` are kept as thin delegators for any caller still
|
|
10
|
+
# naming them; the dispatcher itself builds an Invocation instead.
|
|
10
11
|
module Routing
|
|
11
12
|
Envelope = Struct.new(:aggregate, :entities, keyword_init: true) do
|
|
13
|
+
# @param aggregate [String, #to_s] the receiving aggregate's identity
|
|
14
|
+
# @param entities [Array<String, #to_s>, nil] the entity-hop identities after the
|
|
15
|
+
# aggregate, in call order; empty for an aggregate-level command
|
|
12
16
|
def initialize(aggregate:, entities: [])
|
|
13
17
|
super(aggregate: aggregate.to_s, entities: Array(entities).map(&:to_s).freeze)
|
|
14
18
|
freeze
|
|
@@ -17,158 +21,36 @@ module Hecks
|
|
|
17
21
|
|
|
18
22
|
module_function
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
aggregate, entities = to.is_a?(Hash) ? parse_envelope_hash(to) : scalar_envelope(to)
|
|
24
|
-
|
|
25
|
-
raise TypeMismatch, "to: must name the receiving aggregate identity" if aggregate.nil? || aggregate.to_s.empty?
|
|
26
|
-
if entities.size != entity_depth
|
|
27
|
-
raise TypeMismatch,
|
|
28
|
-
"to: for an entity command needs #{entity_depth} entity " \
|
|
29
|
-
"#{entity_depth == 1 ? 'identity' : 'identities'} after the aggregate — got #{entities.size}"
|
|
30
|
-
end
|
|
31
|
-
raise TypeMismatch, "to: contains a blank entity identity" if entities.any? do |identity|
|
|
32
|
-
identity.nil? || identity.to_s.empty?
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
Envelope.new(aggregate: aggregate, entities: entities)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# BUG#7 — the non-Hash half of `to:` used to accept ANY Ruby object
|
|
39
|
-
# (`[to, []]`, unconditionally) as though it were a ready-made
|
|
40
|
-
# aggregate identity scalar. That is looser than the JSON step
|
|
41
|
-
# boundary (`bin/run`/`Fuzzing::Replay`/`StepBuilder`) ever needs it
|
|
42
|
-
# to be — every legitimate caller already hands this a `String`
|
|
43
|
-
# (`Naming.identity` canonicalizes every identity to one before it
|
|
44
|
-
# ever reaches a `to:`/legacy-args door) — and looser than Rust's own
|
|
45
|
-
# hand-written mirror of this exact boundary
|
|
46
|
-
# (`rust/src/kernel/routing.rs#RoutingEnvelope::from_json`, its own
|
|
47
|
-
# header: "Generated routers accept this shape while retaining the
|
|
48
|
-
# legacy mixed-args object as a compatibility input during
|
|
49
|
-
# migration"), which refuses anything that is neither a JSON string
|
|
50
|
-
# nor object outright, TypeMismatch, before ever reaching a domain's
|
|
51
|
-
# own command payload.
|
|
24
|
+
# Resolves a `to:` argument into a routing envelope. A thin delegator to
|
|
25
|
+
# `Invocation.route`; the dispatcher itself builds an `Invocation` instead.
|
|
52
26
|
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
private_class_method :scalar_envelope
|
|
80
|
-
|
|
81
|
-
# The Hash-shaped half of `to:` — pulled out of `envelope` because
|
|
82
|
-
# it is a self-contained parse (raises on an unrecognized key, then
|
|
83
|
-
# returns the pair) with no dependency on anything `envelope` does
|
|
84
|
-
# afterward; the validations that follow apply the same way
|
|
85
|
-
# whichever branch produced `aggregate`/`entities`.
|
|
86
|
-
def parse_envelope_hash(to)
|
|
87
|
-
hash = to.transform_keys(&:to_sym)
|
|
88
|
-
unknown = hash.keys - %i[aggregate entity entities]
|
|
89
|
-
raise TypeMismatch, "to: does not recognize #{unknown.sort.join(', ')}" unless unknown.empty?
|
|
90
|
-
|
|
91
|
-
[hash[:aggregate], entity_identities(hash)]
|
|
92
|
-
end
|
|
93
|
-
private_class_method :parse_envelope_hash
|
|
94
|
-
|
|
95
|
-
# `with:` is deliberately strict. Compatibility-only calls still pass
|
|
96
|
-
# loose keyword arguments through the old addressing gate, but a caller
|
|
97
|
-
# choosing the explicit envelope cannot smuggle receiver identity back
|
|
98
|
-
# into the payload.
|
|
99
|
-
def payload(command, with:, legacy:)
|
|
100
|
-
if with && !legacy.empty?
|
|
101
|
-
raise TypeMismatch,
|
|
102
|
-
"dispatch takes command facts in with:, not both with: and loose keyword arguments"
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
return legacy unless with
|
|
106
|
-
raise TypeMismatch, "with: must be a hash of command facts" unless with.is_a?(Hash)
|
|
107
|
-
|
|
108
|
-
offered = with.transform_keys(&:to_sym)
|
|
109
|
-
declared = command.attributes.map { |attribute| attribute.name.to_sym }
|
|
110
|
-
refuse_unknown_facts!(command, offered, declared)
|
|
111
|
-
refuse_absent_facts!(command, offered, declared)
|
|
112
|
-
offered
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# The two `with:` shape checks `payload` runs in sequence — pulled
|
|
116
|
-
# out because each is a self-contained "compute a difference, raise
|
|
117
|
-
# if non-empty" rule with no dependency on the other. Order stays
|
|
118
|
-
# unknown-before-absent, exactly as inline: `payload` calls them in
|
|
119
|
-
# that order, so a fact that is both unknown AND leaves something
|
|
120
|
-
# else absent still raises UnknownArgument first, same as before.
|
|
121
|
-
def refuse_unknown_facts!(command, offered, declared)
|
|
122
|
-
unknown = (offered.keys - declared).sort
|
|
123
|
-
return if unknown.empty?
|
|
124
|
-
|
|
125
|
-
reading = declared.empty? ? "none" : declared.join(", ")
|
|
126
|
-
raise UnknownArgument,
|
|
127
|
-
RefusalWording.render("UnknownArgument", "unknown_args",
|
|
128
|
-
command: command.hecks_name, unknown: unknown.join(", "),
|
|
129
|
-
declared: reading)
|
|
130
|
-
end
|
|
131
|
-
private_class_method :refuse_unknown_facts!
|
|
132
|
-
|
|
133
|
-
def refuse_absent_facts!(command, offered, declared)
|
|
134
|
-
absent = command.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym } - offered.keys
|
|
135
|
-
return if absent.empty?
|
|
136
|
-
|
|
137
|
-
reading = declared.empty? ? "none" : declared.join(", ")
|
|
138
|
-
raise AbsentArgument,
|
|
139
|
-
RefusalWording.render("AbsentArgument", "absent_args",
|
|
140
|
-
command: command.hecks_name, absent: absent.sort.join(", "),
|
|
141
|
-
declared: reading)
|
|
142
|
-
end
|
|
143
|
-
private_class_method :refuse_absent_facts!
|
|
144
|
-
|
|
145
|
-
# BUG#18 — an entity route naming NO entity at all (`entities: []`,
|
|
146
|
-
# or neither `entity:` nor `entities:` present) used to come back
|
|
147
|
-
# `[]` here unremarked, which only ever refused downstream for an
|
|
148
|
-
# ENTITY command (`envelope`'s own `entities.size != entity_depth`
|
|
149
|
-
# check, entity_depth >= 1) — for an AGGREGATE-level command
|
|
150
|
-
# (entity_depth 0), `[].size == 0` trivially satisfied that check,
|
|
151
|
-
# so the degenerate Hash `{aggregate:, entities: []}` reached the
|
|
152
|
-
# command's own validation instead of being refused as malformed
|
|
153
|
-
# routing. Rust's `RoutingEnvelope::from_json` never let it get
|
|
154
|
-
# that far: the Hash branch refuses "entity route requires at
|
|
155
|
-
# least one entity identity" unconditionally, before any
|
|
156
|
-
# entity_depth is known. Refusing it here too, at the same point,
|
|
157
|
-
# closes the gap without touching the scalar branch (a bare
|
|
158
|
-
# aggregate identity, no Hash at all) that every ordinary aggregate
|
|
159
|
-
# dispatch already uses instead of this shape (`Facade::Handle#
|
|
160
|
-
# dispatch`'s own `to: @id`, `CommandRequest`'s own header —
|
|
161
|
-
# "aggregate command: { to: "record-id", ... }" — and, now,
|
|
162
|
-
# `Judge#address`).
|
|
163
|
-
def entity_identities(hash)
|
|
164
|
-
raise TypeMismatch, "to: takes entity: or entities:, not both" if hash.key?(:entities) && hash.key?(:entity)
|
|
165
|
-
|
|
166
|
-
identities = hash.key?(:entities) ? Array(hash[:entities]) : Array(hash[:entity])
|
|
167
|
-
raise TypeMismatch, "to: entity route requires at least one entity identity" if identities.empty?
|
|
168
|
-
|
|
169
|
-
identities
|
|
27
|
+
# @param to [String, Hash, nil] a bare aggregate identity, or a Hash with
|
|
28
|
+
# `aggregate:` and `entity:`/`entities:`; nil for no receiver
|
|
29
|
+
# @param entity_depth [Integer] the number of entity-hop identities the call expects
|
|
30
|
+
# @return [Routing::Envelope, nil] the resolved envelope, or nil when `to` is nil
|
|
31
|
+
# @raise [Runtime::TypeMismatch] if `to` is malformed or its entity count does not
|
|
32
|
+
# match `entity_depth`
|
|
33
|
+
def envelope(to, entity_depth: 0) = Invocation.route(to, entity_depth: entity_depth)
|
|
34
|
+
|
|
35
|
+
# Resolves a command's offered facts into its flat args hash. A thin
|
|
36
|
+
# delegator to `Invocation.facts_for` for any caller still naming it.
|
|
37
|
+
#
|
|
38
|
+
# Loaded by invocation.rb (which requires this file), so `Invocation`
|
|
39
|
+
# is always defined by the time either method runs.
|
|
40
|
+
#
|
|
41
|
+
# @param command [Class] a `Bluebook::Command` subclass (or port-operation class)
|
|
42
|
+
# responding to `hecks_name` and `attributes`
|
|
43
|
+
# @param with [Hash, nil] the command's facts, keyed by attribute name; nil when the
|
|
44
|
+
# caller offers `flat` instead
|
|
45
|
+
# @param flat [Hash] the command's facts as a flat args hash, used when `with` is nil
|
|
46
|
+
# @return [Hash{String, Symbol => Object}] the offered facts by attribute name; an
|
|
47
|
+
# attribute offered as nil is kept as nil
|
|
48
|
+
# @raise [Runtime::TypeMismatch] if both `with` and a non-empty `flat` are given, if
|
|
49
|
+
# `with` is not a Hash, or if `with` names an unknown or omits a required attribute
|
|
50
|
+
def payload(command, with:, flat:)
|
|
51
|
+
facts = Invocation.facts_for(command, with: with, flat: flat)
|
|
52
|
+
Invocation.new(verb: nil, target: nil, facts: facts).to_args
|
|
170
53
|
end
|
|
171
|
-
private_class_method :entity_identities
|
|
172
54
|
end
|
|
173
55
|
end
|
|
174
56
|
end
|
|
@@ -3,27 +3,27 @@ require_relative "../value"
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Runtime
|
|
5
5
|
class SagaInterpreter
|
|
6
|
-
# How a saga decides
|
|
6
|
+
# How a saga decides which conversation an event belongs to — three
|
|
7
7
|
# tiers, each one a lesson.
|
|
8
8
|
module Correlation
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
-
# A
|
|
11
|
+
# A dotted path names the scalar field, rather than asking a value object
|
|
12
12
|
# to stand in for one. `correlates_by :end_to_end` would key a saga on
|
|
13
13
|
# the whole ExternalTransfer::EndToEndReference — and what a non-scalar
|
|
14
|
-
# correlation key even
|
|
14
|
+
# correlation key even is is representation-dependent (the object
|
|
15
15
|
# itself? its serialised text?). `:"end_to_end.value"` reads the one
|
|
16
16
|
# field with a single unambiguous rendering.
|
|
17
17
|
def saga_correlation(process_manager, event)
|
|
18
18
|
path = process_manager.correlates_by.to_s.split(".")
|
|
19
|
-
# A
|
|
20
|
-
# value object's field out of a
|
|
21
|
-
# `reference`
|
|
19
|
+
# A later event may already hold the scalar. `reference.value` digs a
|
|
20
|
+
# value object's field out of a fresh declaration (TransferRequested's
|
|
21
|
+
# `reference` is a TransferReference) — but a downstream event this
|
|
22
22
|
# same value was smuggled through as a passthrough argument
|
|
23
23
|
# (AccountDebited's `reference:`, resolved by `dispatch_args` to the
|
|
24
24
|
# bare correlation string) carries it as a scalar already, with
|
|
25
25
|
# nothing left to dig. `"xfer-1".respond_to?(:[])` is true — String
|
|
26
|
-
# has its
|
|
26
|
+
# has its own `[]` (substring indexing) — so checking for keyed
|
|
27
27
|
# lookup explicitly, rather than "responds to `[]` at all", is what
|
|
28
28
|
# stops the second segment from being read as a symbol index into a
|
|
29
29
|
# string that has already arrived.
|
|
@@ -32,9 +32,9 @@ module Hecks
|
|
|
32
32
|
end
|
|
33
33
|
return value unless value.to_s.empty?
|
|
34
34
|
|
|
35
|
-
#
|
|
35
|
+
# **The stamp** — `deliver_saga_dispatch` marks its own event before this
|
|
36
36
|
# saga's next step ever asks, for a leg whose command declares
|
|
37
|
-
#
|
|
37
|
+
# neither the correlation field itself nor the emitting aggregate's
|
|
38
38
|
# own reference key (the two tiers above). command_interpreter/
|
|
39
39
|
# argument_gate.rb names the old payload-only lookup "the weakest
|
|
40
40
|
# part of the gate" : a correlation key arriving on a command only
|
|
@@ -48,34 +48,32 @@ module Hecks
|
|
|
48
48
|
stamped = event.correlation && event.correlation[process_manager.correlation_head.to_s]
|
|
49
49
|
return stamped unless stamped.nil? || stamped.to_s.empty?
|
|
50
50
|
|
|
51
|
-
# A
|
|
51
|
+
# A self-referencing leg carries the correlation forward under its
|
|
52
52
|
# own emitting record's identity — `event.id`, not a field dug back
|
|
53
|
-
# out of the payload
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
# lines at all): the saga silently stopped advancing, forever, for
|
|
64
|
-
# exactly the leg this tier exists to correlate.
|
|
53
|
+
# out of the payload, because an addressing key is not a fact the
|
|
54
|
+
# payload should carry: routing is kept separate from payload
|
|
55
|
+
# (`to:`/`with:`, the facade's own `Handle#run` always uses it), so
|
|
56
|
+
# `own_key` (the aggregate's own reference-key convention, "wire",
|
|
57
|
+
# "transfer") never rides along in the payload unfiltered. Digging
|
|
58
|
+
# for it there would read an empty Hash for any self-referencing
|
|
59
|
+
# leg with no other declared attributes (`OnboardingCase.Clear`,
|
|
60
|
+
# `.Decline` — no `attribute` lines at all), silently stopping the
|
|
61
|
+
# saga from advancing, forever, for exactly the leg this tier
|
|
62
|
+
# exists to correlate.
|
|
65
63
|
#
|
|
66
64
|
# `event.id` says the identical thing this tier always meant —
|
|
67
65
|
# "the record that just emitted this event, by its own identity" —
|
|
68
66
|
# and unlike a payload dig it is populated by the record itself,
|
|
69
67
|
# not by which dispatch convention the caller happened to use.
|
|
70
68
|
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
69
|
+
# Gated, still — a manually-dispatched command on a wholly
|
|
70
|
+
# unrelated aggregate can share an event name this process_manager happens to
|
|
73
71
|
# handle (`Drawer.Take` also emits "Taken", the same name a
|
|
74
|
-
#
|
|
72
|
+
# saga-dispatched leg uses) with nothing this saga should read as
|
|
75
73
|
# its own conversation. What makes a leg genuinely
|
|
76
74
|
# self-referencing — the one fact worth trusting `event.id`
|
|
77
|
-
# for — is that `correlates_by`'s own head field
|
|
78
|
-
#
|
|
75
|
+
# for — is that `correlates_by`'s own head field is this event's
|
|
76
|
+
# own aggregate's declared identity, not merely a same-shaped
|
|
79
77
|
# name: `OnboardingCase.identity_heads` really does include
|
|
80
78
|
# `:reference`, `correlates_by :"reference.value"`'s own head ;
|
|
81
79
|
# `Drawer.identity_heads` is `[:number]`, nowhere close.
|