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
|
@@ -15,27 +15,43 @@ module Hecks
|
|
|
15
15
|
# repository: prefers a native adapter hook (Ports::Query.execute)
|
|
16
16
|
# when the store can answer directly, falling back to interpreting
|
|
17
17
|
# wheres/order_by/limit/offset over every loaded record itself.
|
|
18
|
-
# #reference_call/#reference_interpret are a
|
|
18
|
+
# #reference_call/#reference_interpret are a separate, deliberately
|
|
19
19
|
# naive re-implementation of the same evaluation, used only as the
|
|
20
20
|
# fuzzer's oracle to catch divergence between adapters and this
|
|
21
21
|
# interpreter's own native path.
|
|
22
22
|
class QueryInterpreter
|
|
23
23
|
attr_reader :registry
|
|
24
24
|
|
|
25
|
+
# @param registry [Runtime::Registry] the booted registry queries are answered
|
|
26
|
+
# against
|
|
25
27
|
def initialize(registry)
|
|
26
28
|
@registry = registry
|
|
27
29
|
end
|
|
28
30
|
|
|
31
|
+
# Answers one declared aggregate or entity query, preferring a native adapter
|
|
32
|
+
# hook and falling back to interpreting the query over every loaded record.
|
|
33
|
+
#
|
|
34
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to
|
|
35
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the query is declared on
|
|
36
|
+
# @param query_name [String] the query's declared name, or an entity query's
|
|
37
|
+
# dotted `"Entity.Query"` name
|
|
38
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
39
|
+
# @return [Array<Hash>] one frozen Hash per matching record, its state with
|
|
40
|
+
# `:id` merged in last
|
|
41
|
+
# @raise [Runtime::UnknownVerb] if `query_name` names no declared query
|
|
42
|
+
# @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its
|
|
43
|
+
# declared type
|
|
44
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
29
45
|
def call(domain, aggregate, query_name, args)
|
|
30
46
|
return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
|
|
31
47
|
|
|
32
48
|
declared = declared_query(aggregate, query_name)
|
|
33
49
|
args = normalize_args(aggregate, declared, args)
|
|
34
50
|
declared = TenantScope.apply(declared, args)
|
|
35
|
-
#
|
|
36
|
-
# in `.wheres` and rides through as an ordinary
|
|
37
|
-
# the
|
|
38
|
-
# sub-query against the
|
|
51
|
+
# After TenantScope, so its synthetic clause is already present
|
|
52
|
+
# in `.wheres` and rides through as an ordinary local clause on
|
|
53
|
+
# the outer query. It does not reach the hop's own inner
|
|
54
|
+
# sub-query against the target aggregate — a hop's target may
|
|
39
55
|
# not even declare the same tenant boundary, and propagating one
|
|
40
56
|
# aggregate's tenant scope onto an unrelated aggregate's own
|
|
41
57
|
# query is a real design question of its own, not answered here.
|
|
@@ -51,14 +67,14 @@ module Hecks
|
|
|
51
67
|
if (native = Ports::Query.execute(repository, declared, args,
|
|
52
68
|
context: { domain: domain, aggregate: aggregate, registry: @registry }))
|
|
53
69
|
records = native
|
|
54
|
-
# `record.state.merge(id: record.id)` — id
|
|
70
|
+
# `record.state.merge(id: record.id)` — id last, not first. See
|
|
55
71
|
# Instance#to_h's own comment: an aggregate free to declare its
|
|
56
72
|
# own attribute literally named `id` has that attribute's own
|
|
57
73
|
# wrapped value sitting in `record.state[:id]` already; merging
|
|
58
|
-
# it
|
|
74
|
+
# it over a `{id:}.merge(state)` would let it silently
|
|
59
75
|
# clobber the correct bare identity this row is supposed to
|
|
60
76
|
# carry.
|
|
61
|
-
# A
|
|
77
|
+
# A query row is an answer, not a handle. Mutating one edits
|
|
62
78
|
# nobody's state and silently disagrees with the store.
|
|
63
79
|
return Freezer.deep(records.map { |record| record.state.merge(id: record.id) })
|
|
64
80
|
end
|
|
@@ -66,21 +82,33 @@ module Hecks
|
|
|
66
82
|
Freezer.deep(interpret(repository.all, declared, args, domain: domain))
|
|
67
83
|
end
|
|
68
84
|
|
|
69
|
-
# The
|
|
85
|
+
# The reference answer — this interpreter's own evaluation, never an
|
|
70
86
|
# adapter's native hook. The fuzzer's query oracle replays every
|
|
71
87
|
# generated ask through both paths and treats a difference as a
|
|
72
88
|
# finding: the differential gate the retired cross-runtime harness
|
|
73
89
|
# should always have been, aimed where the divergence actually
|
|
74
90
|
# lives — between the engines inside this one runtime.
|
|
75
91
|
#
|
|
76
|
-
# A hop clause is answered here by its
|
|
92
|
+
# A hop clause is answered here by its own, deliberately naive
|
|
77
93
|
# walk (reference_where_holds?) — never Runtime::ReferenceHop's
|
|
78
|
-
# partition/fold/
|
|
94
|
+
# partition/fold/in-clause. Sharing that algorithm would have made
|
|
79
95
|
# this oracle blind to exactly the code the hop feature adds: every
|
|
80
|
-
#
|
|
96
|
+
# phase of a shared fold would still get diffed against the native
|
|
81
97
|
# adapters, but the fold itself — the empty candidate set, a
|
|
82
98
|
# duplicate id, a dangling reference, a chain's inside-out
|
|
83
99
|
# resolution order — would only ever be compared against itself.
|
|
100
|
+
#
|
|
101
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to
|
|
102
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the query is declared on
|
|
103
|
+
# @param query_name [String] the query's declared name, or an entity query's
|
|
104
|
+
# dotted `"Entity.Query"` name
|
|
105
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
106
|
+
# @return [Array<Hash>] one Hash per matching record, its state with `:id`
|
|
107
|
+
# merged in last
|
|
108
|
+
# @raise [Runtime::UnknownVerb] if `query_name` names no declared query
|
|
109
|
+
# @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its
|
|
110
|
+
# declared type
|
|
111
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
84
112
|
def reference_call(domain, aggregate, query_name, args)
|
|
85
113
|
return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
|
|
86
114
|
|
|
@@ -95,22 +123,22 @@ module Hecks
|
|
|
95
123
|
|
|
96
124
|
def declared_query(aggregate, query_name)
|
|
97
125
|
aggregate.query(query_name) ||
|
|
98
|
-
raise(UnknownVerb, RefusalWording.
|
|
99
|
-
|
|
126
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_query",
|
|
127
|
+
aggregate: aggregate.hecks_name, query: query_name))
|
|
100
128
|
end
|
|
101
129
|
|
|
102
130
|
def interpret(records, declared, args, domain: nil)
|
|
103
131
|
matched = records.select { |r| declared.wheres.all? { |w| where_holds?(w, r, args, domain: domain) } }
|
|
104
132
|
ordered = ordered(matched, declared.order_by, declared.null_semantics)
|
|
105
|
-
#
|
|
133
|
+
# **Offset first, then limit** — the order SQL means by `LIMIT n
|
|
106
134
|
# OFFSET m`, and the order Ports::Query::InMemory#execute already
|
|
107
|
-
# applies (see that file's own comment).
|
|
108
|
-
#
|
|
109
|
-
#
|
|
135
|
+
# applies (see that file's own comment). Without reading
|
|
136
|
+
# `declared.offset`, offset would silently vanish for any query
|
|
137
|
+
# answered here, not just come out reversed.
|
|
110
138
|
skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
|
|
111
139
|
capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
|
|
112
140
|
|
|
113
|
-
# id
|
|
141
|
+
# id last — see the native-path comment above; same clobbering
|
|
114
142
|
# risk for the in-memory reference interpreter's own rows.
|
|
115
143
|
capped.map { |r| r.state.merge(id: r.id) }
|
|
116
144
|
end
|
|
@@ -120,7 +148,7 @@ module Hecks
|
|
|
120
148
|
# reference is answered by reference_where_holds? instead of the
|
|
121
149
|
# plain FieldPath.dig(record, field) `where_holds?` uses (which
|
|
122
150
|
# has no concept of a reference at all — it would just read the
|
|
123
|
-
# raw id straight off the record and compare
|
|
151
|
+
# raw id straight off the record and compare that).
|
|
124
152
|
def reference_interpret(records, declared, args, domain:, shape:)
|
|
125
153
|
matched = records.select do |r|
|
|
126
154
|
declared.wheres.all? do |w|
|
|
@@ -128,20 +156,20 @@ module Hecks
|
|
|
128
156
|
end
|
|
129
157
|
end
|
|
130
158
|
ordered = ordered(matched, declared.order_by, declared.null_semantics)
|
|
131
|
-
#
|
|
159
|
+
# **Offset first, then limit** — same fix, same reasoning, as
|
|
132
160
|
# #interpret's own rows above.
|
|
133
161
|
skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
|
|
134
162
|
capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
|
|
135
163
|
|
|
136
|
-
# id
|
|
164
|
+
# id last — same reasoning, same fix, as interpret's own rows.
|
|
137
165
|
capped.map { |r| r.state.merge(id: r.id) }
|
|
138
166
|
end
|
|
139
167
|
|
|
140
|
-
#
|
|
168
|
+
# **The naive reading of a hop**: not a fold, not an id set — for
|
|
141
169
|
# each candidate row, walk the reference by hand and dig the
|
|
142
170
|
# field out of whatever it actually points at. A nil reference,
|
|
143
171
|
# or one that resolves to nothing (a dangling id), makes the
|
|
144
|
-
#
|
|
172
|
+
# whole clause false outright, whatever the comparator — "points
|
|
145
173
|
# at a client that is not active" is false for a proposal with no
|
|
146
174
|
# client at all, the same way it is false for one whose client
|
|
147
175
|
# really is active; falling through to holds?(clause, nil, args)
|
|
@@ -177,8 +205,8 @@ module Hecks
|
|
|
177
205
|
|
|
178
206
|
ordered = ordered_elements(rows, declared.order_by, declared.null_semantics,
|
|
179
207
|
parent_key, entity.identity_heads)
|
|
180
|
-
#
|
|
181
|
-
# #interpret's own rows above. `entity_rows` is the
|
|
208
|
+
# **Offset first, then limit** — same fix, same reasoning, as
|
|
209
|
+
# #interpret's own rows above. `entity_rows` is the only engine
|
|
182
210
|
# for entity/sub-list queries, so a declared offset here silently
|
|
183
211
|
# vanished for every entity query, not merely one path among
|
|
184
212
|
# several.
|
|
@@ -186,31 +214,28 @@ module Hecks
|
|
|
186
214
|
declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
|
|
187
215
|
end
|
|
188
216
|
|
|
189
|
-
#
|
|
190
|
-
#
|
|
217
|
+
# The three declarations `entity_rows` needs before it can read a
|
|
218
|
+
# single record — the entity itself, its declared query, and the
|
|
191
219
|
# list attribute that holds it on the aggregate. Extracted from
|
|
192
220
|
# `entity_rows` (pure extraction, same lookups, same order, same
|
|
193
221
|
# UnknownVerb refusals) purely to separate "which declarations does
|
|
194
222
|
# this dotted name resolve to" from the row-computation that follows.
|
|
195
223
|
def resolve_entity_query(aggregate, entity_name, query_name)
|
|
196
224
|
entity = aggregate.entities.find { |piece| piece.hecks_name == entity_name } ||
|
|
197
|
-
raise(UnknownVerb, RefusalWording.
|
|
198
|
-
|
|
225
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_unknown",
|
|
226
|
+
aggregate: aggregate.hecks_name, entity: entity_name))
|
|
199
227
|
declared = entity.query(query_name) ||
|
|
200
|
-
raise(UnknownVerb, RefusalWording.
|
|
201
|
-
|
|
228
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_query_missing",
|
|
229
|
+
entity: entity_name, query: query_name))
|
|
202
230
|
list_attr = aggregate.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
|
|
203
|
-
raise(UnknownVerb, RefusalWording.
|
|
204
|
-
|
|
231
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_holds_no_list",
|
|
232
|
+
aggregate: aggregate.hecks_name, entity: entity_name))
|
|
205
233
|
[entity, declared, list_attr]
|
|
206
234
|
end
|
|
207
235
|
|
|
208
|
-
# FieldPath.dig, not a raw `element[clause.field.to_sym]` —
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
# comment below), and a dotted `where` (`where "price.cents" < 100`)
|
|
212
|
-
# needs the same segment-by-segment walk every other query path
|
|
213
|
-
# already gets. Reading only the symbol spelling of the WHOLE
|
|
236
|
+
# FieldPath.dig, not a raw `element[clause.field.to_sym]` — a dotted
|
|
237
|
+
# `where` (`where "price.cents" < 100`) needs the same segment-by-
|
|
238
|
+
# segment walk every other query path already gets. Reading the whole
|
|
214
239
|
# dotted string as one key always missed — `element[:"price.cents"]`
|
|
215
240
|
# is never a real key — so a dotted where on an entity query
|
|
216
241
|
# silently matched nothing, on the only engine entity queries have.
|
|
@@ -218,28 +243,22 @@ module Hecks
|
|
|
218
243
|
holds?(clause, QuerySpecification::FieldPath.dig(element, clause.field), args)
|
|
219
244
|
end
|
|
220
245
|
|
|
221
|
-
# A row's own key
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
# identity is a value object, and `to_s` on one is an
|
|
246
|
+
# A row's own key. A sub-list row is a hydrated entity element (symbol-keyed
|
|
247
|
+
# since every adapter decodes through `Ports::Persistence::StateCodec` and
|
|
248
|
+
# `EntityListCoercion#hydrate_entity_list` symbolizes each element — PR A4
|
|
249
|
+
# removed the string-spelling fallback that coped with the old per-adapter
|
|
250
|
+
# shapes). It rides `comparable` for the same reason a where-clause does : an
|
|
251
|
+
# identity is a value object, and `to_s` on one is an object address — a sort key
|
|
227
252
|
# that differs run to run, which is worse than the store order it replaced.
|
|
228
|
-
|
|
229
|
-
# value is a genuinely-held `false` must not fall through to the
|
|
230
|
-
# other spelling (usually absent) and land on `nil`.
|
|
231
|
-
def cell(row, key)
|
|
232
|
-
sym = key.to_sym
|
|
233
|
-
row.key?(sym) ? row[sym] : row[key.to_s]
|
|
234
|
-
end
|
|
253
|
+
def cell(row, key) = row[key.to_sym]
|
|
235
254
|
|
|
236
|
-
# A sub-list row is identified by its
|
|
255
|
+
# A sub-list row is identified by its parent and then its own key : two
|
|
237
256
|
# entities under different parents can share a sequence, so the parent has
|
|
238
257
|
# to lead or the tie is not broken at all.
|
|
239
258
|
#
|
|
240
|
-
#
|
|
259
|
+
# Every key the piece is known by, in declaration order, for the same
|
|
241
260
|
# reason the parent leads: a part that ties is a part that breaks no tie.
|
|
242
|
-
# This took `identified_by`, which is the
|
|
261
|
+
# This took `identified_by`, which is the single head and is nil the
|
|
243
262
|
# moment an identity has two parts — and `cell(row, nil)` calls
|
|
244
263
|
# `nil.to_sym`, so a query against a composite piece did not sort wrongly,
|
|
245
264
|
# it raised. A piece known by one key sorts exactly as it did.
|
|
@@ -258,12 +277,12 @@ module Hecks
|
|
|
258
277
|
end
|
|
259
278
|
|
|
260
279
|
# The comparator table itself lives in
|
|
261
|
-
# QuerySpecification::Common::Comparison
|
|
262
|
-
# Ports::Query::InMemory#holds?
|
|
263
|
-
# two drifted
|
|
264
|
-
# `comparable` disagreed about value objects with two
|
|
265
|
-
# members. What stays here is how a value is
|
|
266
|
-
# path: the registry is instance state rather than an argument.
|
|
280
|
+
# QuerySpecification::Common::Comparison, shared with
|
|
281
|
+
# Ports::Query::InMemory#holds? rather than each carrying its own
|
|
282
|
+
# copy — two copies once drifted: `none_in_state` reached only one
|
|
283
|
+
# of them, and `comparable` disagreed about value objects with two
|
|
284
|
+
# numeric members. What stays here is how a value is reached for
|
|
285
|
+
# this path: the registry is instance state rather than an argument.
|
|
267
286
|
def holds?(clause, held, args, record: nil, domain: nil)
|
|
268
287
|
QuerySpecification::Common::Comparison.holds?(
|
|
269
288
|
clause.op, comparable(held), comparable(resolve_query_value(clause.value, args)), registry: @registry
|
|
@@ -276,40 +295,40 @@ module Hecks
|
|
|
276
295
|
|
|
277
296
|
# `boundary: false` always (C3.8 — a query's declared argument types
|
|
278
297
|
# name the argument for callers and generators, never a runtime
|
|
279
|
-
# shape checked here). A null required
|
|
280
|
-
# argument, though, is
|
|
298
|
+
# shape checked here). A null required value-object-typed query
|
|
299
|
+
# argument, though, is not a runtime-shape question at all: C3.7
|
|
281
300
|
# says a named query's declared value-object arguments are checked
|
|
282
301
|
# the same way a command argument's own is, so a `nil` offered for
|
|
283
302
|
# a non-optional value-object-typed query attribute
|
|
284
303
|
# (Governance::RoleAssignment.AssignmentsForActor's `actor_id`, say)
|
|
285
|
-
# has to refuse — passing it through unchecked
|
|
286
|
-
#
|
|
304
|
+
# has to refuse — passing it through unchecked would let it through
|
|
305
|
+
# as a silent, unfiltered query instead, a real
|
|
287
306
|
# Ruby/Rust divergence the fuzzer caught (QualityControl BUG#2).
|
|
288
307
|
#
|
|
289
|
-
# `checked_vo?` true is handled by `null_vo_argument!`
|
|
308
|
+
# `checked_vo?` true is handled by `null_vo_argument!` directly,
|
|
290
309
|
# never by routing through `Value.for_attribute(argument: true)`
|
|
291
|
-
# into the shared `Value::Coercion#nil_argument` the
|
|
310
|
+
# into the shared `Value::Coercion#nil_argument` the command door
|
|
292
311
|
# (`Interpreting#coerce_declared_arguments`) still uses — that
|
|
293
|
-
# method builds a null value object from
|
|
294
|
-
#
|
|
312
|
+
# method builds a null value object from zero fields, which
|
|
313
|
+
# succeeds (silently absorbing the null via the type's own field
|
|
295
314
|
# defaults) whenever every field happens to have one
|
|
296
315
|
# (`Lease.Expired`'s `now`, a `LeaseInstant` with a `default: 0`
|
|
297
316
|
# field; `Account.Overdrawn`/`HighBalance`/`StrictlyAbove`/
|
|
298
317
|
# `AtMost`'s `floor`/`cap`, a two-defaulted-field `Money`) and only
|
|
299
318
|
# refuses when a field has none (`Order.CostingLessThan`'s
|
|
300
|
-
# `ceiling`, a defaultless `Price`) — a real
|
|
319
|
+
# `ceiling`, a defaultless `Price`) — a real query-side divergence
|
|
301
320
|
# from Rust, which always refuses `TypeMismatch` on an explicit
|
|
302
321
|
# null argument regardless of any default (QualityControl BUG#36).
|
|
303
322
|
# `null_vo_argument!` instead treats an explicit null exactly the
|
|
304
|
-
# way `Value.fields_for` already treats any other
|
|
323
|
+
# way `Value.fields_for` already treats any other wrong-shaped
|
|
305
324
|
# (non-Hash, non-Value) value offered for that same attribute — a
|
|
306
325
|
# single-field value object auto-wraps into `{field: nil}` (whose
|
|
307
|
-
#
|
|
326
|
+
# own `nil` is a present key, so `Value.build`'s `apply_defaults`
|
|
308
327
|
# never fills it, and `check_required_fields` refuses it exactly
|
|
309
328
|
# as any other missing required field would); a multi-field value
|
|
310
329
|
# object refuses immediately with the same `value_object_shape`
|
|
311
330
|
# wording an ordinary wrong-shaped scalar already gets. Command
|
|
312
|
-
# arguments are deliberately
|
|
331
|
+
# arguments are deliberately untouched — this is a query-only
|
|
313
332
|
# door; `nil_argument`'s own default-absorbing fallback still
|
|
314
333
|
# governs a null command argument exactly as it always has.
|
|
315
334
|
#
|
|
@@ -6,10 +6,10 @@ require_relative "value"
|
|
|
6
6
|
module Hecks
|
|
7
7
|
module Runtime
|
|
8
8
|
# Turns facts selected by a policy or process manager into the same
|
|
9
|
-
# receiver/payload envelope an outside caller uses.
|
|
10
|
-
#
|
|
11
|
-
# compatibility seam that separates
|
|
12
|
-
#
|
|
9
|
+
# receiver/payload envelope an outside caller uses. A reaction declares
|
|
10
|
+
# both receiver identities and facts through one `with:` map, so this is
|
|
11
|
+
# the one compatibility seam that separates them after resolving the
|
|
12
|
+
# declaration and before re-entering the dispatcher.
|
|
13
13
|
module ReactionInvocation
|
|
14
14
|
Target = Struct.new(:aggregate, :entities, :command, keyword_init: true)
|
|
15
15
|
# :facts, not :values — Struct.new already defines #values (every
|
|
@@ -19,10 +19,17 @@ module Hecks
|
|
|
19
19
|
|
|
20
20
|
module_function
|
|
21
21
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
22
|
+
# Reports whether a reaction declared an explicit `with:` projection.
|
|
23
|
+
#
|
|
24
|
+
# An omitted projection and an explicitly empty `with: {}` both hold
|
|
25
|
+
# as the same empty array on the holding IR. Builders preserve
|
|
26
|
+
# declaration presence off-wire, in `@projection_declared`;
|
|
27
|
+
# reconstructed/legacy IR without that ivar falls back to reading
|
|
28
|
+
# presence off whether `with_spec` is non-empty.
|
|
29
|
+
#
|
|
30
|
+
# @param declaration [Bluebook::Policy, Bluebook::DispatchSpec] the reacting
|
|
31
|
+
# declaration to check
|
|
32
|
+
# @return [Boolean] true if the declaration names an explicit `with:` projection
|
|
26
33
|
def projection_declared?(declaration)
|
|
27
34
|
if declaration.instance_variable_defined?(:@projection_declared)
|
|
28
35
|
declaration.instance_variable_get(:@projection_declared)
|
|
@@ -31,11 +38,27 @@ module Hecks
|
|
|
31
38
|
end
|
|
32
39
|
end
|
|
33
40
|
|
|
41
|
+
# Resolves a declared `with:` projection against the scopes and bindings visible to it.
|
|
42
|
+
#
|
|
34
43
|
# A reaction's source names resolve lexically, not globally. Policies
|
|
35
44
|
# supply one event/row scope. Process managers supply current event then
|
|
36
45
|
# opening-event memory, while correlation is an explicit binding ahead
|
|
37
46
|
# of both. Missing names are refused here rather than materialized as nil
|
|
38
47
|
# and accidentally presented as target command facts.
|
|
48
|
+
#
|
|
49
|
+
# @param with_spec [Hash{Symbol => Object}] each target fact name mapped to its source:
|
|
50
|
+
# a Symbol naming a fact visible in `bindings` or `scopes`, or any other value taken
|
|
51
|
+
# as a literal
|
|
52
|
+
# @param scopes [Array<Hecks::Runtime::ReactionInvocation::Scope, Array(String,
|
|
53
|
+
# Hash)>] the named fact scopes to resolve a Symbol source against, checked in order;
|
|
54
|
+
# a bare `[name, facts]` pair is wrapped into a `Scope`
|
|
55
|
+
# @param bindings [Hash] explicit bindings (such as a saga's correlation key), checked
|
|
56
|
+
# before any scope
|
|
57
|
+
# @param label [String] names this resolution in an `UnknownArgument` refusal
|
|
58
|
+
# @return [Hash{Symbol => Object}] `with_spec`'s keys mapped to their resolved,
|
|
59
|
+
# materialized values
|
|
60
|
+
# @raise [Runtime::UnknownArgument] if a Symbol source names a fact visible in no
|
|
61
|
+
# binding and no scope
|
|
39
62
|
def resolve_mapping(with_spec:, scopes:, bindings: {}, label: "reaction")
|
|
40
63
|
normalized_bindings = bindings.transform_keys(&:to_sym)
|
|
41
64
|
normalized_scopes = scopes.map do |scope|
|
|
@@ -49,7 +72,7 @@ module Hecks
|
|
|
49
72
|
end
|
|
50
73
|
end
|
|
51
74
|
|
|
52
|
-
#
|
|
75
|
+
# One `with:` source, resolved — pulled out of resolve_mapping
|
|
53
76
|
# because it is a pure function of its own arguments (a literal, a
|
|
54
77
|
# binding, or a name visible in some scope), with no dependency on
|
|
55
78
|
# anything else resolve_mapping's own to_h block is doing.
|
|
@@ -60,10 +83,10 @@ module Hecks
|
|
|
60
83
|
visible = scopes.find { |scope| scope.facts.key?(source) }
|
|
61
84
|
unless visible
|
|
62
85
|
names = scopes.map(&:name).join(" then ")
|
|
63
|
-
#
|
|
86
|
+
# **What is visible, named**. A refusal that only says which name is
|
|
64
87
|
# missing sent a modeler guessing field after field
|
|
65
88
|
# (`number`, `reference`…) at a fan-out row that is addressed
|
|
66
|
-
# by
|
|
89
|
+
# by one key — `account`, the lowercase aggregate — which
|
|
67
90
|
# nothing else in the domain spells out. The names each scope
|
|
68
91
|
# actually offers are the whole diagnosis; the refusal now
|
|
69
92
|
# lists them, scope by scope.
|
|
@@ -80,12 +103,34 @@ module Hecks
|
|
|
80
103
|
# command attributes enter `with:`, while identities become `to:`.
|
|
81
104
|
# `consumed` accumulates across the aggregate-identity and entity-
|
|
82
105
|
# identity steps below, and refuse_unconsumed! at the end reads the
|
|
83
|
-
#
|
|
106
|
+
# final list — an ordering dependency threaded through one shared
|
|
84
107
|
# local. Already leans on private helpers (identity_for,
|
|
85
108
|
# source_receiver_for, command_facts, refuse_unconsumed!) for every
|
|
86
|
-
# piece that
|
|
109
|
+
# piece that is self-contained; what remains is the sequencing
|
|
87
110
|
# itself, which further splitting would only relocate, not remove.
|
|
88
111
|
# rubocop:disable-next Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
112
|
+
#
|
|
113
|
+
# @param registry [Runtime::Registry] the booted registry to resolve `verb` against
|
|
114
|
+
# @param verb [String] the fully qualified target command verb
|
|
115
|
+
# @param projected [Hash] the facts to send, already resolved (e.g. by
|
|
116
|
+
# `resolve_mapping`) or, for a legacy reaction, the raw event/row payload
|
|
117
|
+
# @param explicit [Boolean] true when the reaction declared its own `with:` projection
|
|
118
|
+
# (`projection_declared?`); false forwards `projected` wholesale as legacy args
|
|
119
|
+
# @param passthrough [Array<String, Symbol>] extra fact names allowed to ride along
|
|
120
|
+
# unconsumed, beyond the receiver identity and declared command facts
|
|
121
|
+
# @param source_receiver [Hash{Symbol => Object}, nil] the triggering event's own
|
|
122
|
+
# `{aggregate:, identity:}`, offered as a same-aggregate receiver when nothing else
|
|
123
|
+
# supplies one; nil when there is no such event to inherit from
|
|
124
|
+
# @return [Hash{Symbol => Object}] `{to:, with:}` for an explicit projection targeting
|
|
125
|
+
# a non-creating command (`with:` only for a creating command); otherwise `projected`
|
|
126
|
+
# (with `to:` merged in when a receiver could be inherited)
|
|
127
|
+
# @raise [Runtime::UnknownVerb] if `verb` does not resolve to a declared command,
|
|
128
|
+
# entity command, or port operation (only when `explicit` is true; a legacy call
|
|
129
|
+
# resolving `verb` only to check inheritance swallows this and forwards unchanged)
|
|
130
|
+
# @raise [Runtime::TypeMismatch] if an explicit projection resolves no receiver
|
|
131
|
+
# identity for the target aggregate or one of its entities
|
|
132
|
+
# @raise [Runtime::UnknownArgument] if an explicit projection's facts include a name
|
|
133
|
+
# that is neither a consumed receiver identity nor a declared command fact
|
|
89
134
|
def build(registry:, verb:, projected:, explicit:, passthrough: [], source_receiver: nil)
|
|
90
135
|
args = projected.transform_keys(&:to_sym)
|
|
91
136
|
unless explicit
|
|
@@ -99,7 +144,7 @@ module Hecks
|
|
|
99
144
|
rescue UnknownVerb
|
|
100
145
|
return args
|
|
101
146
|
end
|
|
102
|
-
#
|
|
147
|
+
# An entity target has no shape this branch can build — an
|
|
103
148
|
# implicit (unprojected) `to:` is always a bare scalar, the
|
|
104
149
|
# aggregate's own identity alone; an entity command's receiver is
|
|
105
150
|
# `{aggregate:, entities:}`, which needs the entity's own
|
|
@@ -167,13 +212,13 @@ module Hecks
|
|
|
167
212
|
|
|
168
213
|
*entity_names, command_name = command_path.split(".")
|
|
169
214
|
|
|
170
|
-
# A
|
|
171
|
-
# entity command uses ("Head.Rest") — checked
|
|
215
|
+
# A port operation, reached by the same two-segment tail shape an
|
|
216
|
+
# entity command uses ("Head.Rest") — checked first, same order
|
|
172
217
|
# `Dispatcher#dispatch` already resolves a live verb in (an
|
|
173
218
|
# aggregate that declared both a port and an entity of the same
|
|
174
219
|
# name would resolve to the port there too; no domain in this
|
|
175
220
|
# corpus does). A port has no `entities` of its own — the
|
|
176
|
-
#
|
|
221
|
+
# receiver is always the aggregate itself — so `Target#entities`
|
|
177
222
|
# stays empty and `#command` holds the `PortOperation`, which
|
|
178
223
|
# answers `#creates?` (always false) the same way an ordinary
|
|
179
224
|
# `Command` does, letting `source_receiver_for` lift a same-
|
|
@@ -215,37 +260,37 @@ module Hecks
|
|
|
215
260
|
private_class_method :aggregate_aliases
|
|
216
261
|
|
|
217
262
|
# Event.id names the aggregate that emitted the event. It can therefore
|
|
218
|
-
# supply only the receiver of a non-creating command on that
|
|
263
|
+
# supply only the receiver of a non-creating command on that same root;
|
|
219
264
|
# it cannot address another aggregate, invent an entity identity, or turn
|
|
220
265
|
# a creation into a mutation. An explicit projected receiver is resolved
|
|
221
266
|
# first and remains authoritative.
|
|
222
267
|
#
|
|
223
|
-
#
|
|
224
|
-
# back is
|
|
268
|
+
# Answers regardless of `target.entities` — the value this method hands
|
|
269
|
+
# back is only ever the root aggregate's own identity (`source_receiver`
|
|
225
270
|
# carries nothing else: `event.aggregate`/`event.id` name the emitting
|
|
226
|
-
#
|
|
271
|
+
# aggregate, never one of its entities). An entity target's own identity
|
|
227
272
|
# is resolved entirely separately, from `args` (`build`'s own `entity_
|
|
228
273
|
# identities` loop) — this method is never consulted for it, so
|
|
229
274
|
# answering for an entity target does not "invent an entity identity"
|
|
230
|
-
# any more than answering for a plain one invents
|
|
275
|
+
# any more than answering for a plain one invents that identity; it was
|
|
231
276
|
# already the one thing this method has ever supplied. `build`'s own
|
|
232
|
-
#
|
|
277
|
+
# explicit branch (below) already applies this correctly either way
|
|
233
278
|
# (`aggregate_identity ||= inherited_receiver`, entity identities read
|
|
234
279
|
# from `args` regardless) — the caller that actually needed a guard
|
|
235
|
-
# here is the
|
|
280
|
+
# here is the implicit one, above, which has no shape to build a
|
|
236
281
|
# `{aggregate:, entities:}` receiver from an inherited scalar alone,
|
|
237
282
|
# and now guards itself for exactly that reason instead of this method
|
|
238
283
|
# doing it on that caller's behalf and, as a side effect, also refusing
|
|
239
|
-
# the
|
|
284
|
+
# the explicit caller's own legitimate case — confirmed missing until
|
|
240
285
|
# `qa/stress_domains/waybill` (BUG#6) exercised it for the first time
|
|
241
286
|
# anywhere in the corpus: a saga dispatching into a nested entity's own
|
|
242
287
|
# command, inheriting its aggregate receiver from the event that
|
|
243
288
|
# triggered it.
|
|
244
289
|
def source_receiver_for(target, source_receiver)
|
|
245
290
|
return nil unless source_receiver
|
|
246
|
-
# `target.command.creates?`
|
|
291
|
+
# `target.command.creates?` alone misreads every entity command —
|
|
247
292
|
# `Behaviour::Command#creates?`'s own comment: "a verb declared on
|
|
248
|
-
# an
|
|
293
|
+
# an entity always acts on that piece... which means `creates?`
|
|
249
294
|
# answers true for every one of them" (it never sets `@references`
|
|
250
295
|
# the way an aggregate-level command's own `reference_to` does,
|
|
251
296
|
# not because it brings anything new into being). `build`'s own
|
|
@@ -253,7 +298,7 @@ module Hecks
|
|
|
253
298
|
# 20-odd lines below, already reads `creates?` correctly for
|
|
254
299
|
# exactly this reason — an entity command is never a genuine
|
|
255
300
|
# creation, whatever `creates?` alone answers — so this checks the
|
|
256
|
-
#
|
|
301
|
+
# same compound condition instead of the bare, misleading half of
|
|
257
302
|
# it. Left unfixed, an entity target's own receiver was refused
|
|
258
303
|
# here even after this method stopped refusing on `target.entities`
|
|
259
304
|
# alone — the second half of BUG#6's own fix.
|