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
|
@@ -2,8 +2,8 @@ module Hecks
|
|
|
2
2
|
module Bluebook
|
|
3
3
|
module DSL
|
|
4
4
|
class BluebookBuilder
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# The self.validate_*/self.correlation_*/self.event_*/self.walk_*
|
|
6
|
+
# cluster — every whole-chapter, cross-aggregate check `#build`
|
|
7
7
|
# runs once a chapter is fully assembled (reference targets, event
|
|
8
8
|
# shapes, correlation keys, query hops, projected fields,
|
|
9
9
|
# bidirectional-reference cycles), plus each one's own private
|
|
@@ -17,25 +17,34 @@ module Hecks
|
|
|
17
17
|
# one of these is a pure function of its own explicit arguments
|
|
18
18
|
# (an assembled `Bluebook::Chapter`, an aggregate list, ...), never
|
|
19
19
|
# builder-instance state (no `@ivar` read anywhere in this
|
|
20
|
-
# module
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
20
|
+
# module), and every caller reaches these as
|
|
21
|
+
# `BluebookBuilder.validate_assembled!(...)` class-level calls,
|
|
22
|
+
# `#build`'s own `self.class.validate_assembled!` included.
|
|
23
|
+
# `extend` is `include`'s class-level counterpart: the same "one
|
|
24
|
+
# class, its methods, filed across files" pattern
|
|
25
25
|
# `Runtime::Registry` already uses (`include Verification`), just
|
|
26
26
|
# turning each method into a class (singleton) method instead of
|
|
27
|
-
# an instance method,
|
|
28
|
-
#
|
|
27
|
+
# an instance method, the same role `def self.foo` fills on a
|
|
28
|
+
# class that keeps its methods in one file.
|
|
29
29
|
module Validation
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
30
|
+
# Runs every whole-chapter check against one fully assembled chapter.
|
|
31
|
+
#
|
|
32
|
+
# **Every whole-chapter check, in one place** — a pure function of an
|
|
33
|
+
# assembled `Bluebook::Chapter`, callable both from `#build` (right
|
|
34
|
+
# after assembly) and from `MetaValidator.judge_deferred!` (once, on
|
|
35
|
+
# a chapter whose files have all loaded — see `#build`'s own comment
|
|
36
|
+
# for why that split exists at all). Public, not
|
|
37
|
+
# `private_class_method`'d, for exactly that second caller —
|
|
38
|
+
# `MetaValidator` needs to reach this with no builder instance in
|
|
39
|
+
# hand, only the chapter `judge_deferred!` already read back out of
|
|
40
|
+
# the registry.
|
|
41
|
+
#
|
|
42
|
+
# @param bluebook [Bluebook::Chapter] the fully assembled chapter to validate
|
|
43
|
+
# @return [void]
|
|
44
|
+
# @raise [Bluebook::DSL::Malformed] if any of the reference, event-shape, `with:`,
|
|
45
|
+
# query-hop, projected-field or provision checks below finds a violation
|
|
46
|
+
# @raise [Bluebook::DSL::ProcessManagerBuilder::InvalidProcessManager] if a process
|
|
47
|
+
# manager's `correlates_by` resolves to something other than a scalar field
|
|
39
48
|
def validate_assembled!(bluebook)
|
|
40
49
|
# moved to the language: an attribute type is a reference to its Shape,
|
|
41
50
|
# so an undeclared value object fails reference resolution
|
|
@@ -49,12 +58,12 @@ module Hecks
|
|
|
49
58
|
|
|
50
59
|
# Every hop AggregateBuilder#seal_query_field recognised and
|
|
51
60
|
# deferred gets checked for real here — the earliest point a
|
|
52
|
-
# hop
|
|
61
|
+
# hop can be checked, for exactly the reason
|
|
53
62
|
# validate_no_bidirectional_references! above already gives:
|
|
54
63
|
# `Bluebook.new` just stamped `hecks_owner` on every
|
|
55
64
|
# aggregate, so `Reference#resolve` finally has a chapter to
|
|
56
65
|
# walk. Before this line every target in the file (including
|
|
57
|
-
# ones declared
|
|
66
|
+
# ones declared above the aggregate doing the asking) would
|
|
58
67
|
# have resolved to nil.
|
|
59
68
|
infer_hop_query_arguments!(bluebook)
|
|
60
69
|
validate_query_hops!(bluebook)
|
|
@@ -63,29 +72,99 @@ module Hecks
|
|
|
63
72
|
# own reference cannot resolve until every aggregate in the
|
|
64
73
|
# chapter is real and owner-stamped (S12, ADR 0025).
|
|
65
74
|
validate_projected_fields!(bluebook)
|
|
75
|
+
|
|
76
|
+
validate_provisions!(bluebook)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Checks every `provides` row against `Capabilities::CONTRACTS`.
|
|
80
|
+
#
|
|
81
|
+
# **What a declared capability must name**. A `provides` row is only
|
|
82
|
+
# worth trusting in place of a name check if it is checked: an
|
|
83
|
+
# unknown capability, a missing or extra key, or a verb that is
|
|
84
|
+
# not this chapter's own command/query of the right kind would
|
|
85
|
+
# otherwise wire a role check to nothing, in silence.
|
|
86
|
+
#
|
|
87
|
+
# @param bluebook [Bluebook::Chapter] the chapter whose `provides` rows to check
|
|
88
|
+
# @return [void]
|
|
89
|
+
# @raise [Bluebook::DSL::Malformed] if a row names a capability
|
|
90
|
+
# `Capabilities::CONTRACTS` does not know, its keys do not match the
|
|
91
|
+
# capability's contract exactly, or a verb names no real command/query
|
|
92
|
+
# of the declared kind
|
|
93
|
+
def validate_provisions!(bluebook)
|
|
94
|
+
bluebook.provides.group_by(&:capability).each do |capability, rows|
|
|
95
|
+
contract = Capabilities::CONTRACTS.fetch(capability) do
|
|
96
|
+
raise Malformed, "#{bluebook.name} provides #{capability.inspect}, which is no capability the " \
|
|
97
|
+
"language knows — known: #{Capabilities::CONTRACTS.keys.sort.join(', ')}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
keys = rows.map { |row| row.key.to_sym }
|
|
101
|
+
unless keys.sort == contract.keys.sort
|
|
102
|
+
raise Malformed, "#{bluebook.name} provides #{capability.inspect} with #{keys.join(', ')}, but " \
|
|
103
|
+
"#{capability} needs exactly #{contract.keys.join(', ')}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
rows.each { |row| validate_provided_verb!(bluebook, capability, row, contract.fetch(row.key.to_sym)) }
|
|
107
|
+
end
|
|
66
108
|
end
|
|
67
109
|
|
|
68
|
-
#
|
|
110
|
+
# Checks that one `provides` row's verb names a real command or query.
|
|
69
111
|
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
112
|
+
# @param bluebook [Bluebook::Chapter] the chapter the row was declared on
|
|
113
|
+
# @param capability [String] the capability the row belongs to, such as
|
|
114
|
+
# `"authorization"`
|
|
115
|
+
# @param row [Bluebook::Chapter::Provision] the one declared row to check
|
|
116
|
+
# @param kind [Symbol] `:command` or `:query`, the kind of verb this row's key
|
|
117
|
+
# must name (from `Capabilities::CONTRACTS`)
|
|
118
|
+
# @return [void]
|
|
119
|
+
# @raise [Bluebook::DSL::Malformed] if `row.verb` does not split into a real
|
|
120
|
+
# aggregate and a member of the right kind that aggregate declares
|
|
121
|
+
def validate_provided_verb!(bluebook, capability, row, kind)
|
|
122
|
+
aggregate_name, member = row.verb.split(".", 2)
|
|
123
|
+
aggregate = bluebook.aggregate(aggregate_name)
|
|
124
|
+
return if aggregate && member && provided_member_names(aggregate, kind).include?(member)
|
|
125
|
+
|
|
126
|
+
raise Malformed, "#{bluebook.name} provides #{capability.inspect} #{row.key}: #{row.verb.inspect}, " \
|
|
127
|
+
"which names no #{kind} this chapter declares (spelled \"Aggregate.#{kind.capitalize}\")"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Names of the members a `provides` verb of the given kind may address.
|
|
73
131
|
#
|
|
74
|
-
#
|
|
132
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the verb is scoped to
|
|
133
|
+
# @param kind [Symbol] `:command` or `:query`
|
|
134
|
+
# @return [Array<String>] every command's `hecks_name` when `kind` is `:command`,
|
|
135
|
+
# else every query's declared `name`
|
|
136
|
+
def provided_member_names(aggregate, kind)
|
|
137
|
+
kind == :command ? aggregate.commands.map(&:hecks_name) : aggregate.queries.map(&:name)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Refuses an entity command that names itself as its own root.
|
|
141
|
+
#
|
|
142
|
+
# That is the whole of what is left here, and it needs saying plainly:
|
|
143
|
+
# the refusal message once read "references must target aggregate
|
|
144
|
+
# heads," a claim broader than what this actually checks.
|
|
145
|
+
#
|
|
146
|
+
# `CommandBuilder#reference_to` sets `references` only when the target's
|
|
75
147
|
# bare name equals the owner's ; anything else becomes a reference
|
|
76
|
-
#
|
|
148
|
+
# attribute. So on an aggregate command `references` is always a copy of
|
|
77
149
|
# that aggregate's own name, and looking it up in an index of aggregates
|
|
78
|
-
# is a
|
|
150
|
+
# is a tautology — that branch never refused anything and structurally
|
|
79
151
|
# could not. Verified across all eight golden chapters before deleting it.
|
|
80
152
|
#
|
|
81
|
-
# On a
|
|
153
|
+
# On a piece's command the owner is the entity, and an entity is not a
|
|
82
154
|
# head, so what this actually refuses is `reference_to <its own name>`
|
|
83
|
-
# written inside `entity do … end`. A piece is reached
|
|
155
|
+
# written inside `entity do … end`. A piece is reached through its
|
|
84
156
|
# aggregate ; a command on one addresses the aggregate, never the piece.
|
|
85
157
|
#
|
|
86
|
-
# Reference
|
|
158
|
+
# Reference attributes are the language's business now — offered as the
|
|
87
159
|
# head's own id and resolved as references, so `Aggregate.Reference` and
|
|
88
160
|
# `Command.Reference` refuse an undeclared head with no predicate at all.
|
|
161
|
+
#
|
|
162
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
163
|
+
# @return [void]
|
|
164
|
+
# @raise [Bluebook::DSL::Malformed] if an entity command's `references` names
|
|
165
|
+
# something other than one of this chapter's own aggregate heads — which, given
|
|
166
|
+
# how `CommandBuilder#reference_to` sets it, only happens when the entity command
|
|
167
|
+
# names its own entity as its root
|
|
89
168
|
def validate_reference_value_objects!(aggregates)
|
|
90
169
|
heads = aggregates.map(&:hecks_name)
|
|
91
170
|
|
|
@@ -106,22 +185,27 @@ module Hecks
|
|
|
106
185
|
"an entity command is addressed through its aggregate; #{violations.uniq.join('; ')}"
|
|
107
186
|
end
|
|
108
187
|
|
|
109
|
-
#
|
|
188
|
+
# Events are first-class by convention, not by declaration (ADR
|
|
110
189
|
# 0025, "events and reactions" — "a domain event is a value
|
|
111
190
|
# object with its own attributes, not a label"). No new `event
|
|
112
191
|
# do ... end` construct exists to hand-author and keep in step
|
|
113
192
|
# with every emitting command by hand — an event's own known
|
|
114
|
-
# shape
|
|
115
|
-
# this is the
|
|
193
|
+
# shape is whichever command(s) declare `emits` for its name, and
|
|
194
|
+
# this is the one thing that has to hold for that convention to
|
|
116
195
|
# mean anything: every command that emits a given name has to
|
|
117
196
|
# agree on what it carries. An event is one fact; a fact does not
|
|
118
197
|
# carry two different truths depending on who is telling it.
|
|
119
198
|
#
|
|
120
|
-
#
|
|
121
|
-
# `admits:`/`default:` are refinements
|
|
199
|
+
# Structural fields only (name/type/list/optional) — `pattern:`/
|
|
200
|
+
# `admits:`/`default:` are refinements on a field, not a second
|
|
122
201
|
# claim about what the payload holds, so two emitting commands
|
|
123
202
|
# are free to differ there without actually disagreeing about
|
|
124
203
|
# the event's own shape.
|
|
204
|
+
#
|
|
205
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
206
|
+
# @return [void]
|
|
207
|
+
# @raise [Bluebook::DSL::Malformed] if two or more commands `emits` the same event
|
|
208
|
+
# name with different structural shapes
|
|
125
209
|
def validate_event_shapes!(aggregates)
|
|
126
210
|
event_emitters(aggregates).each do |event_name, pairs|
|
|
127
211
|
next if pairs.size == 1
|
|
@@ -136,29 +220,39 @@ module Hecks
|
|
|
136
220
|
end
|
|
137
221
|
end
|
|
138
222
|
|
|
139
|
-
#
|
|
223
|
+
# The "expensive half" the ADR names: "with: { account: :account }
|
|
140
224
|
# projecting into a reaction that has no declared contract ...
|
|
141
225
|
# breaks at dispatch rather than at load." Checked here, now that
|
|
142
226
|
# `validate_event_shapes!` (above) guarantees at most one real
|
|
143
227
|
# shape per event name, and command references being first-class
|
|
144
|
-
# (`Naming.command_ref`) means the
|
|
228
|
+
# (`Naming.command_ref`) means the target side is a real
|
|
145
229
|
# resolvable command, not a string that might be a typo.
|
|
146
230
|
#
|
|
147
|
-
#
|
|
231
|
+
# **Same-chapter only, on purpose** — a `with:` whose source event or
|
|
148
232
|
# target command lives outside this chapter (an `across` policy
|
|
149
233
|
# reacting to another domain's event entirely) is silently left
|
|
150
234
|
# unchecked rather than refused: there is nothing here yet to
|
|
151
235
|
# check it against, and "unresolvable" is not the same claim as
|
|
152
236
|
# "wrong."
|
|
153
237
|
#
|
|
154
|
-
# A FOR_EACH
|
|
155
|
-
# `with:`'s symbols read the
|
|
238
|
+
# A FOR_EACH policy's source isn't the event at all — a fan-out
|
|
239
|
+
# `with:`'s symbols read the query row `for_each` answers
|
|
156
240
|
# (FreezeAccountsOnSuspension's own comment: "`account` is the
|
|
157
241
|
# key the fan-out merges for each row"), which this has no shape
|
|
158
|
-
# for; the
|
|
242
|
+
# for; the source half is skipped for those, the target half
|
|
159
243
|
# (does the dispatched command actually declare the field) still
|
|
160
244
|
# runs, since that half is true regardless of where the value
|
|
161
245
|
# came from.
|
|
246
|
+
#
|
|
247
|
+
# @param policies [Array<Bluebook::Policy>] every reaction hoisted onto the
|
|
248
|
+
# chapter's own aggregates
|
|
249
|
+
# @param process_managers [Array<Bluebook::ProcessManager>] the chapter's declared
|
|
250
|
+
# process managers
|
|
251
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
252
|
+
# @return [void]
|
|
253
|
+
# @raise [Bluebook::DSL::Malformed] if a policy's or a saga dispatch's `with:`
|
|
254
|
+
# names a field its target command does not declare, or reads a source field
|
|
255
|
+
# its event does not carry (see `check_with_spec!` for the full rule)
|
|
162
256
|
def validate_with_projections!(policies, process_managers, aggregates)
|
|
163
257
|
lookup = command_lookup(aggregates)
|
|
164
258
|
heads = correlation_heads(process_managers)
|
|
@@ -184,11 +278,11 @@ module Hecks
|
|
|
184
278
|
end
|
|
185
279
|
|
|
186
280
|
# `process_manager:` is present only for a process manager's own dispatch — a
|
|
187
|
-
# saga leg's source symbol resolves against the
|
|
281
|
+
# saga leg's source symbol resolves against the current triggering
|
|
188
282
|
# event first, same as a policy, but falls all the way back to the
|
|
189
|
-
# saga's own
|
|
283
|
+
# saga's own memory when the current event does not carry it
|
|
190
284
|
# (`SagaInterpreter#dispatch_args`, its own last `else`) — and
|
|
191
|
-
# memory starts as the
|
|
285
|
+
# memory starts as the opening event's payload
|
|
192
286
|
# (`SagaInterpreter#instance = { ..., memory: event.payload }`,
|
|
193
287
|
# never updated after), never the leg's own. Settlement's own
|
|
194
288
|
# comment names exactly this: "the credit leg reads a destination
|
|
@@ -203,6 +297,27 @@ module Hecks
|
|
|
203
297
|
# source_shape, memory_shape, correlation, identity_sources, ...)
|
|
204
298
|
# into a new method for no gain: each `next`/`raise` already
|
|
205
299
|
# reads as its own rule at its own site.
|
|
300
|
+
#
|
|
301
|
+
# @param command_ref [String] the dotted name of the command `with:` dispatches,
|
|
302
|
+
# such as `"Account.Debit"` or `"Account.Piece.Move"`
|
|
303
|
+
# @param event_name [String, nil] the triggering event's name, or `nil` for a
|
|
304
|
+
# FOR_EACH policy (whose source is a query row, not an event)
|
|
305
|
+
# @param with_spec [Array<Array(Symbol, Object)>] the declared `with:` pairs,
|
|
306
|
+
# field name to source — a Symbol naming a field to read, or a literal value
|
|
307
|
+
# @param lookup [Hash{String => Class}] every command in the chapter, keyed by
|
|
308
|
+
# `"owner.hecks_name"` (`command_lookup`'s own shape)
|
|
309
|
+
# @param label [String] a human-readable description of what declared this
|
|
310
|
+
# `with:`, used only in a raised message
|
|
311
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
312
|
+
# @param correlation_heads [Array<Symbol>] every saga's own `correlates_by` head
|
|
313
|
+
# in this chapter, a legal addressing key alongside a command's own attributes
|
|
314
|
+
# @param process_manager [Bluebook::ProcessManager, nil] the process manager this
|
|
315
|
+
# `with:` dispatches from, or `nil` for a policy's own trigger
|
|
316
|
+
# @return [void]
|
|
317
|
+
# @raise [Bluebook::DSL::Malformed] if `with:` names a field the target command
|
|
318
|
+
# does not declare, or reads a Symbol source that resolves to neither the
|
|
319
|
+
# correlation key, the emitter's identity, nor a field the triggering event or
|
|
320
|
+
# (for a saga leg) the starting event's shape carries
|
|
206
321
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
207
322
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
208
323
|
def check_with_spec!(command_ref, event_name, with_spec, lookup, label, aggregates, correlation_heads,
|
|
@@ -211,7 +326,7 @@ module Hecks
|
|
|
211
326
|
source_shape = event_name && event_shape_for(event_name, aggregates)
|
|
212
327
|
memory_shape = process_manager && event_shape_for(process_manager.starts_on, aggregates)
|
|
213
328
|
correlation = process_manager&.correlates_by && process_manager.correlation_head
|
|
214
|
-
# A
|
|
329
|
+
# A policy's source also carries the emitter's own identity —
|
|
215
330
|
# `PolicyInterpreter#emitter_identity`, the runtime half of this.
|
|
216
331
|
# An entity command's event never declares its aggregate's
|
|
217
332
|
# identity (it arrives through `reference_to`, not an
|
|
@@ -243,15 +358,17 @@ module Hecks
|
|
|
243
358
|
end
|
|
244
359
|
end
|
|
245
360
|
|
|
246
|
-
#
|
|
361
|
+
# Decides whether `field` is a legal `with:` target on `command`.
|
|
362
|
+
#
|
|
363
|
+
# A command's own `reference_to` (bare, no `as:`) never lands in
|
|
247
364
|
# `attributes` — `CommandBuilder#reference_to`'s self-reference
|
|
248
365
|
# branch sets `command.references` instead (S2), and mints no new
|
|
249
|
-
# field at all. What addresses it is not one name but the
|
|
250
|
-
#
|
|
366
|
+
# field at all. What addresses it is not one name but the same
|
|
367
|
+
# set `CommandInterpreter::ArgumentGate#refuse_unknown_arguments`
|
|
251
368
|
# already accepts at dispatch time — `:id`, the owning aggregate's
|
|
252
369
|
# own `identity_heads` (real corpus proof — `Account.Debit`
|
|
253
370
|
# dispatched everywhere as `number: ...`, `Account`'s own
|
|
254
|
-
# `identified_by`),
|
|
371
|
+
# `identified_by`), and `Naming.reference_key(command.references)`
|
|
255
372
|
# (real corpus proof — `FreezeAccountsOnSuspension`'s `for_each`
|
|
256
373
|
# fan-out, whose own comment reads "`account` is the key the
|
|
257
374
|
# fan-out merges for each row it answers"). Both are simultaneously
|
|
@@ -259,6 +376,16 @@ module Hecks
|
|
|
259
376
|
# legal here : this mirrors that gate rather than re-deriving a
|
|
260
377
|
# narrower rule that would refuse one of two real, already-shipped
|
|
261
378
|
# dispatch conventions.
|
|
379
|
+
#
|
|
380
|
+
# @param command [Class<Bluebook::Command>] the command `with:` dispatches
|
|
381
|
+
# @param field [Symbol] the `with:` key to check
|
|
382
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates,
|
|
383
|
+
# searched when `field` might name `command`'s own reference key
|
|
384
|
+
# @param correlation_heads [Array<Symbol>] every saga's own `correlates_by` head
|
|
385
|
+
# in this chapter
|
|
386
|
+
# @return [Boolean] `true` when `field` is one of `command`'s own attributes,
|
|
387
|
+
# `:id`, a correlation head, or the identity/reference key of the aggregate
|
|
388
|
+
# `command.references` names
|
|
262
389
|
def command_declares?(command, field, aggregates, correlation_heads)
|
|
263
390
|
return true if command.attributes.any? { |a| a.name == field }
|
|
264
391
|
return true if field == :id
|
|
@@ -271,23 +398,38 @@ module Hecks
|
|
|
271
398
|
referenced.identity_heads.include?(field) || Naming.reference_key(command.references) == field
|
|
272
399
|
end
|
|
273
400
|
|
|
274
|
-
#
|
|
401
|
+
# The fourth addressing key `ArgumentGate#refuse_unknown_arguments`
|
|
275
402
|
# accepts, alongside `:id`/`identity_heads`/`reference_key` — every
|
|
276
|
-
# saga in
|
|
403
|
+
# saga in this domain's own `correlates_by` head, carried through
|
|
277
404
|
# every dispatch as pure passthrough (Settlement's own comment:
|
|
278
405
|
# "`reference:` carries the correlation forward... this is pure
|
|
279
406
|
# passthrough, not an addressing key"). A command declaring none of
|
|
280
407
|
# its attributes named this is not a gap; the correlation key rides
|
|
281
408
|
# through commands that never read it, same as it does at runtime.
|
|
409
|
+
#
|
|
410
|
+
# @param process_managers [Array<Bluebook::ProcessManager>] the chapter's declared
|
|
411
|
+
# process managers
|
|
412
|
+
# @return [Array<Symbol>] the `correlation_head` of every process manager that
|
|
413
|
+
# declares a `correlates_by`
|
|
282
414
|
def correlation_heads(process_managers)
|
|
283
415
|
process_managers.filter_map { |pm| pm.correlates_by && pm.correlation_head }
|
|
284
416
|
end
|
|
285
417
|
|
|
286
|
-
#
|
|
418
|
+
# Yields every command this chapter declares, paired with its owner's name.
|
|
419
|
+
#
|
|
420
|
+
# Every command this chapter declares, an aggregate's own and
|
|
287
421
|
# every entity nested inside one, paired with a name for what
|
|
288
422
|
# declares it — shared by `validate_event_shapes!` and
|
|
289
423
|
# `validate_with_projections!`'s own command lookup, the same
|
|
290
424
|
# reach `HecksagonBuilder#commands_in` needs one level up (S8).
|
|
425
|
+
#
|
|
426
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
427
|
+
# @yieldparam owner [String] the declaring aggregate's `hecks_name`, or
|
|
428
|
+
# `"Aggregate.Entity"` for a command declared on a nested entity
|
|
429
|
+
# @yieldparam command [Class<Bluebook::Command>] the command class
|
|
430
|
+
# @return [Enumerator<Array(String, Class)>] an enumerator over the same pairs
|
|
431
|
+
# when no block is given; otherwise the value of the underlying `each`, unused
|
|
432
|
+
# by any caller
|
|
291
433
|
def each_command(aggregates)
|
|
292
434
|
return enum_for(:each_command, aggregates) unless block_given?
|
|
293
435
|
|
|
@@ -299,70 +441,116 @@ module Hecks
|
|
|
299
441
|
end
|
|
300
442
|
end
|
|
301
443
|
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
#
|
|
308
|
-
#
|
|
309
|
-
#
|
|
310
|
-
#
|
|
444
|
+
# Groups every command in the chapter by the event name(s) it emits.
|
|
445
|
+
#
|
|
446
|
+
# **Not memoised** — caching this as `@event_emitters ||=` on the builder
|
|
447
|
+
# instance would be safe for a one-file chapter but wrong for one split
|
|
448
|
+
# across several: the first file's `build()` call would compute and cache
|
|
449
|
+
# it from whatever `@aggregates` held at that moment, and every later
|
|
450
|
+
# file's own validation would keep reading that same stale snapshot,
|
|
451
|
+
# silently missing any command a later file adds. Recomputed fresh every
|
|
452
|
+
# call instead — this walks the whole chapter once per `#build`, not a
|
|
453
|
+
# hot path worth memoising at that cost.
|
|
454
|
+
#
|
|
455
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
456
|
+
# @return [Hash{String => Array<Array(String, Class)>}] every event name mapped
|
|
457
|
+
# to the `[owner, command]` pairs (`each_command`'s own shape) that `emits` it;
|
|
458
|
+
# an unqueried event name answers `[]`
|
|
311
459
|
def event_emitters(aggregates)
|
|
312
460
|
each_command(aggregates).with_object(Hash.new { |h, k| h[k] = [] }) do |(owner, command), index|
|
|
313
461
|
command.emits.each { |event_name| index[event_name] << [owner, command] }
|
|
314
462
|
end
|
|
315
463
|
end
|
|
316
464
|
|
|
317
|
-
#
|
|
465
|
+
# The comparable shape of one command's attributes, value objects unwrapped.
|
|
466
|
+
#
|
|
467
|
+
# **Structural, not nominal**. Two commands on two different
|
|
318
468
|
# aggregates that both `emits "SameEvent"` are free to type a
|
|
319
|
-
# field through two
|
|
469
|
+
# field through two different, locally-scoped wrapper value
|
|
320
470
|
# objects (e.g. one aggregate's own `value: SomeText` vs
|
|
321
471
|
# another's `value: OtherText`, exactly the per-aggregate "own
|
|
322
472
|
# text VO" convention every aggregate in this grammar already
|
|
323
473
|
# follows for everything from `RuleText` to `FieldRef`) without
|
|
324
474
|
# actually disagreeing about the event's shape — comparing
|
|
325
|
-
# `a.type` by
|
|
475
|
+
# `a.type` by name would flag that as a violation for no real
|
|
326
476
|
# reason: an event is one fact, and two isomorphic wrapper types
|
|
327
477
|
# tell an identical one. So a value-object type is unwrapped to
|
|
328
|
-
# its
|
|
478
|
+
# its own attribute shape (recursively — a wrapper could itself
|
|
329
479
|
# wrap another) before comparing, and only a primitive type
|
|
330
480
|
# (nothing left to unwrap) or two VOs that truly differ once
|
|
331
481
|
# unwrapped still counts as a real mismatch. `owner` carries the
|
|
332
482
|
# type's `value_object` lookup — a command's own attributes only
|
|
333
|
-
# know their type's
|
|
483
|
+
# know their type's name, never the aggregate that declared it,
|
|
334
484
|
# and two sibling aggregates in one chapter each keep a
|
|
335
485
|
# same-named VO private to themselves, so the unwrap has to ask
|
|
336
|
-
# the
|
|
486
|
+
# the same aggregate the field's own command belongs to, never a
|
|
337
487
|
# neighbor's.
|
|
488
|
+
#
|
|
489
|
+
# @param command [Class<Bluebook::Command>] the command whose attribute shape
|
|
490
|
+
# to compute
|
|
491
|
+
# @param owner [Bluebook::Aggregate, nil] the aggregate whose `value_object`
|
|
492
|
+
# lookup resolves any wrapper type by name; `nil` when the owner could not
|
|
493
|
+
# be resolved, in which case nothing is unwrapped
|
|
494
|
+
# @return [Array<Array>] one `[name, unwrapped_type, list, optional]` tuple per
|
|
495
|
+
# attribute, sorted; `unwrapped_type` is a type name String for a primitive
|
|
496
|
+
# or an unresolved type, or a recursively-unwrapped Array of the same shape
|
|
497
|
+
# for a value object
|
|
338
498
|
def event_shape(command, owner)
|
|
339
499
|
command.attributes.map { |a| [a.name, unwrap_shape(owner, a.type.to_s), a.list?, a.optional?] }.sort
|
|
340
500
|
end
|
|
341
501
|
|
|
502
|
+
# Recursively unwraps a type name to a primitive or a value object's own shape.
|
|
503
|
+
#
|
|
504
|
+
# @param owner [Bluebook::Aggregate, nil] the aggregate whose `value_object`
|
|
505
|
+
# lookup resolves `type_name`; `nil` answers `type_name` unchanged
|
|
506
|
+
# @param type_name [String] the attribute type to unwrap
|
|
507
|
+
# @param seen [Array<String>] type names already unwrapped on this path, so a
|
|
508
|
+
# self-referential value object bottoms out on its own name instead of
|
|
509
|
+
# recursing forever
|
|
510
|
+
# @return [String, Array<Array>] `type_name` itself when `owner` is `nil`, when
|
|
511
|
+
# it is already a primitive, when it is already in `seen`, or when it names no
|
|
512
|
+
# value object `owner` declares; otherwise the same `[name, unwrapped_type,
|
|
513
|
+
# list, optional]` shape `event_shape` returns, one tuple per member
|
|
342
514
|
def unwrap_shape(owner, type_name, seen = [])
|
|
343
|
-
|
|
515
|
+
# owner couldn't be resolved, so compare by name: nothing left to unwrap
|
|
516
|
+
return type_name if owner.nil?
|
|
344
517
|
return type_name if Attribute::PRIMITIVES.include?(type_name)
|
|
345
518
|
# a self-referential VO bottoms out on its own name, not an infinite unwrap
|
|
346
519
|
return type_name if seen.include?(type_name)
|
|
347
520
|
|
|
348
521
|
shape = owner.value_object(type_name)
|
|
349
|
-
|
|
522
|
+
# not this owner's own VO (a reference type, say): nothing further to unwrap
|
|
523
|
+
return type_name unless shape
|
|
350
524
|
|
|
351
525
|
shape.attributes.map { |a| [a.name, unwrap_shape(owner, a.type.to_s, seen + [type_name]), a.list?, a.optional?] }.sort
|
|
352
526
|
end
|
|
353
527
|
|
|
354
|
-
#
|
|
528
|
+
# Finds the aggregate an `each_command` owner string names.
|
|
529
|
+
#
|
|
530
|
+
# `owner` (from `each_command`) is a plain string — the aggregate's
|
|
355
531
|
# `hecks_name` alone, or `"Aggregate.Entity"` for an entity's own
|
|
356
|
-
# command. Either way the
|
|
357
|
-
# typed with are the
|
|
532
|
+
# command. Either way the value objects a command's fields can be
|
|
533
|
+
# typed with are the aggregate's own (`Entity` carries no
|
|
358
534
|
# `value_object` lookup of its own — the whole rest of this file
|
|
359
535
|
# already resolves hop/type lookups only at the aggregate level,
|
|
360
536
|
# e.g. `validate_hop_tail!`'s `target.value_object(type)`), so only
|
|
361
537
|
# the first segment ever matters here.
|
|
538
|
+
#
|
|
539
|
+
# @param owner [String] an `each_command` owner string, such as `"Game"` or
|
|
540
|
+
# `"Game.Knight"`
|
|
541
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
542
|
+
# @return [Bluebook::Aggregate, nil] the aggregate whose `hecks_name` matches
|
|
543
|
+
# `owner`'s first dotted segment, or `nil` if none does
|
|
362
544
|
def owner_aggregate(owner, aggregates)
|
|
363
545
|
aggregates.find { |a| a.hecks_name == owner.to_s.split(".").first }
|
|
364
546
|
end
|
|
365
547
|
|
|
548
|
+
# The comparable attribute shape of whichever command first emits `event_name`.
|
|
549
|
+
#
|
|
550
|
+
# @param event_name [String, Symbol] the event name to look up
|
|
551
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
552
|
+
# @return [Array<Array>, nil] the first emitting command's `event_shape`, or `nil`
|
|
553
|
+
# if no command in the chapter emits this event
|
|
366
554
|
def event_shape_for(event_name, aggregates)
|
|
367
555
|
pairs = event_emitters(aggregates).fetch(event_name.to_s, [])
|
|
368
556
|
return nil if pairs.empty?
|
|
@@ -372,9 +560,15 @@ module Hecks
|
|
|
372
560
|
end
|
|
373
561
|
|
|
374
562
|
# The identity heads of the aggregate that emits `event_name` — an
|
|
375
|
-
# entity's event is stamped with its
|
|
563
|
+
# entity's event is stamped with its owning aggregate's identity
|
|
376
564
|
# (`Event#id` is the parent's), so an owner spelled "Game.Knight"
|
|
377
565
|
# answers Game's heads.
|
|
566
|
+
#
|
|
567
|
+
# @param event_name [String, Symbol] the event name to look up
|
|
568
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
569
|
+
# @return [Array<Symbol>] the emitting aggregate's own `identity_heads`, plus the
|
|
570
|
+
# emitting entity's own `identity_heads` when the event is an entity's; `[]` if
|
|
571
|
+
# no command in the chapter emits this event, or its owner cannot be resolved
|
|
378
572
|
def event_identity_heads_for(event_name, aggregates)
|
|
379
573
|
pairs = event_emitters(aggregates).fetch(event_name.to_s, [])
|
|
380
574
|
return [] if pairs.empty?
|
|
@@ -384,7 +578,7 @@ module Hecks
|
|
|
384
578
|
return [] unless aggregate
|
|
385
579
|
|
|
386
580
|
heads = aggregate.identity_heads.map(&:to_sym)
|
|
387
|
-
#
|
|
581
|
+
# An entity's event also carries the piece's own identity — the
|
|
388
582
|
# args a piece was addressed by are the args its event announces
|
|
389
583
|
# (`Emission#emit`: `payload: args`), so `id`-shaped heads are
|
|
390
584
|
# genuinely there at runtime even though no `attribute` line on
|
|
@@ -394,15 +588,20 @@ module Hecks
|
|
|
394
588
|
heads + (entity ? entity.identity_heads.map(&:to_sym) : [])
|
|
395
589
|
end
|
|
396
590
|
|
|
591
|
+
# Every command in the chapter, keyed by its fully dotted address.
|
|
592
|
+
#
|
|
593
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
594
|
+
# @return [Hash{String => Class}] `"owner.hecks_name"` (`each_command`'s owner
|
|
595
|
+
# string joined with the command's own `hecks_name`) mapped to the command class
|
|
397
596
|
def command_lookup(aggregates)
|
|
398
597
|
each_command(aggregates).with_object({}) do |(owner, command), index|
|
|
399
598
|
index["#{owner}.#{command.hecks_name}"] = command
|
|
400
599
|
end
|
|
401
600
|
end
|
|
402
601
|
|
|
403
|
-
# A
|
|
602
|
+
# A reference ring is not a modelling choice, it is a missing one
|
|
404
603
|
# — a DDD aggregate is a consistency boundary precisely because
|
|
405
|
-
# something outside it can only ever point
|
|
604
|
+
# something outside it can only ever point in, by id, never the
|
|
406
605
|
# other way. A caller must be able to reason about one aggregate
|
|
407
606
|
# alone ; a ring back to where it started means no aggregate in
|
|
408
607
|
# it is a boundary anyone can reason about without the rest of
|
|
@@ -414,18 +613,22 @@ module Hecks
|
|
|
414
613
|
# aggregate finishes building long before it can know whether
|
|
415
614
|
# some later aggregate in the same file points back at it.
|
|
416
615
|
#
|
|
417
|
-
#
|
|
418
|
-
#
|
|
419
|
-
#
|
|
420
|
-
#
|
|
421
|
-
#
|
|
422
|
-
#
|
|
423
|
-
#
|
|
424
|
-
#
|
|
425
|
-
#
|
|
426
|
-
#
|
|
427
|
-
#
|
|
428
|
-
#
|
|
616
|
+
# Acyclic within a chapter (ADR 0025, "References") — catches any
|
|
617
|
+
# ring, however long (A -> B -> C -> A), not only a direct pair
|
|
618
|
+
# (A -> B -> A), the same DFS coloring a reference graph needs
|
|
619
|
+
# for any cycle. A cross-chapter reference is unreachable here
|
|
620
|
+
# rather than unchecked: `Reference#resolve` is scoped to its
|
|
621
|
+
# own chapter by construction, so a target this chapter never
|
|
622
|
+
# declares is a dangling name, not an edge — `edges.key?` below
|
|
623
|
+
# is what keeps the walk from ever leaving this chapter's own
|
|
624
|
+
# aggregates. Self-reference stays legal (`parent.parent.name`
|
|
625
|
+
# for a hierarchy is real and safe) — excluded the same way a
|
|
626
|
+
# direct pair is.
|
|
627
|
+
#
|
|
628
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
629
|
+
# @return [void]
|
|
630
|
+
# @raise [Bluebook::DSL::Malformed] if the chapter's own aggregate-to-aggregate
|
|
631
|
+
# reference graph contains a cycle
|
|
429
632
|
def validate_no_bidirectional_references!(aggregates)
|
|
430
633
|
edges = aggregates.to_h do |aggregate|
|
|
431
634
|
[aggregate.hecks_name, aggregate.reference_targets.uniq.reject { |target| target == aggregate.hecks_name }]
|
|
@@ -443,8 +646,13 @@ module Hecks
|
|
|
443
646
|
end
|
|
444
647
|
|
|
445
648
|
# Plain DFS with a visiting/done coloring, over the reference
|
|
446
|
-
# graph
|
|
649
|
+
# graph this chapter's own aggregates declare. Returns the ring
|
|
447
650
|
# itself (in the order it closes), or nil.
|
|
651
|
+
#
|
|
652
|
+
# @param edges [Hash{String => Array<String>}] each aggregate's `hecks_name`
|
|
653
|
+
# mapped to the names of the aggregates it references
|
|
654
|
+
# @return [Array<String>, nil] the aggregate names forming a cycle, in the order
|
|
655
|
+
# the walk closes it, or `nil` if the graph has none
|
|
448
656
|
def find_reference_cycle(edges)
|
|
449
657
|
state = {}
|
|
450
658
|
|
|
@@ -456,6 +664,17 @@ module Hecks
|
|
|
456
664
|
nil
|
|
457
665
|
end
|
|
458
666
|
|
|
667
|
+
# One DFS step of `find_reference_cycle`, from `node`.
|
|
668
|
+
#
|
|
669
|
+
# @param node [String] the aggregate name to walk from
|
|
670
|
+
# @param edges [Hash{String => Array<String>}] the whole reference graph, as
|
|
671
|
+
# `find_reference_cycle` receives it
|
|
672
|
+
# @param state [Hash{String => Symbol}] each visited node's `:visiting` or
|
|
673
|
+
# `:done` color, mutated in place across the whole walk
|
|
674
|
+
# @param path [Array<String>] the nodes visited on the current path, mutated
|
|
675
|
+
# in place as the walk descends and backtracks
|
|
676
|
+
# @return [Array<String>, nil] the cycle closing back on `node`, or `nil` if
|
|
677
|
+
# no cycle is reachable from here
|
|
459
678
|
def reference_cycle_from(node, edges, state, path)
|
|
460
679
|
return nil if state[node] == :done
|
|
461
680
|
return path[path.index(node)..] if state[node] == :visiting
|
|
@@ -464,7 +683,8 @@ module Hecks
|
|
|
464
683
|
path.push(node)
|
|
465
684
|
|
|
466
685
|
edges[node].each do |target|
|
|
467
|
-
|
|
686
|
+
# a name this chapter never declares is dangling, not an edge
|
|
687
|
+
next unless edges.key?(target)
|
|
468
688
|
|
|
469
689
|
found = reference_cycle_from(target, edges, state, path)
|
|
470
690
|
return found if found
|
|
@@ -475,31 +695,37 @@ module Hecks
|
|
|
475
695
|
nil
|
|
476
696
|
end
|
|
477
697
|
|
|
478
|
-
#
|
|
479
|
-
#
|
|
698
|
+
# Resolves and checks every `where` hop deferred at aggregate-seal time.
|
|
699
|
+
#
|
|
700
|
+
# **The other half of a hop** — AggregateBuilder#seal_query_field
|
|
701
|
+
# recognised the head of a dotted where-field that names one of
|
|
480
702
|
# its own references and deferred it here, unable to check
|
|
481
|
-
# further: it cannot yet resolve what the reference points
|
|
703
|
+
# further: it cannot yet resolve what the reference points at.
|
|
482
704
|
# This runs once every aggregate exists in one chapter, so it
|
|
483
705
|
# can.
|
|
484
706
|
#
|
|
485
|
-
# Only
|
|
707
|
+
# Only `where` clauses ever reach here — a hop on ORDER BY is
|
|
486
708
|
# refused outright, immediately, back in seal_query_field
|
|
487
709
|
# itself (that answer never needed the target's shape).
|
|
488
710
|
#
|
|
489
|
-
#
|
|
490
|
-
#
|
|
491
|
-
#
|
|
492
|
-
#
|
|
493
|
-
#
|
|
494
|
-
#
|
|
495
|
-
# to check the deferral — a bad hop, or even a well-formed one,
|
|
496
|
-
# built silently and then matched nothing at runtime
|
|
711
|
+
# An entity's own queries do reach `EntityBuilder#reference_to` —
|
|
712
|
+
# tier-1 sealing (`AggregateBuilder#query_surfaces`) already
|
|
713
|
+
# recognises a hop on an entity's own field and defers it exactly
|
|
714
|
+
# like an aggregate's — but nothing walks entity queries at tier 2
|
|
715
|
+
# to check the deferral, so a bad hop, or even a well-formed one,
|
|
716
|
+
# would otherwise build silently and then match nothing at runtime
|
|
497
717
|
# (`QueryInterpreter#entity_rows` reads an element's fields by
|
|
498
718
|
# literal hash key, never follows a reference). Refused outright
|
|
499
719
|
# here instead of taught to follow the hop for real: no corpus
|
|
500
720
|
# member needs an entity query to cross a reference yet, and a
|
|
501
721
|
# named refusal beats a runtime that resolves nothing while
|
|
502
722
|
# looking like it might.
|
|
723
|
+
#
|
|
724
|
+
# @param bluebook [Bluebook::Chapter] the fully assembled chapter to check
|
|
725
|
+
# @return [void]
|
|
726
|
+
# @raise [Bluebook::DSL::Malformed] if an aggregate's `where` hop resolves to
|
|
727
|
+
# nothing, chains too deep, or lands somewhere illegal, or if an entity's own
|
|
728
|
+
# query hops through a reference at all
|
|
503
729
|
def validate_query_hops!(bluebook)
|
|
504
730
|
bluebook.aggregates.each do |aggregate|
|
|
505
731
|
aggregate.queries.each do |query|
|
|
@@ -514,11 +740,19 @@ module Hecks
|
|
|
514
740
|
end
|
|
515
741
|
end
|
|
516
742
|
|
|
743
|
+
# Mints an implicit query attribute for every unresolved symbolic hop comparison.
|
|
744
|
+
#
|
|
517
745
|
# The chapter-wide half of AggregateBuilder's local query-argument
|
|
518
746
|
# inference. A hop cannot resolve while its aggregate is still being
|
|
519
747
|
# built; here every Reference has an owner and target, so a symbolic
|
|
520
748
|
# comparison can inherit the type of the scalar it compares without a
|
|
521
749
|
# duplicate query-local declaration.
|
|
750
|
+
#
|
|
751
|
+
# @param bluebook [Bluebook::Chapter] the fully assembled chapter to infer
|
|
752
|
+
# query arguments over
|
|
753
|
+
# @return [void]
|
|
754
|
+
# @raise [Bluebook::DSL::Malformed] if a hop's reference has no `declared_in`
|
|
755
|
+
# aggregate to resolve its target through
|
|
522
756
|
def infer_hop_query_arguments!(bluebook)
|
|
523
757
|
bluebook.aggregates.each do |aggregate|
|
|
524
758
|
aggregate.queries.each do |query|
|
|
@@ -540,13 +774,23 @@ module Hecks
|
|
|
540
774
|
end
|
|
541
775
|
end
|
|
542
776
|
|
|
543
|
-
#
|
|
777
|
+
# Builds the query attribute a resolved hop plan implies for `name`.
|
|
778
|
+
#
|
|
779
|
+
# The leaf `infer_hop_query_arguments!` infers for one resolved
|
|
544
780
|
# hop plan — a pure function of `plan` and the symbolic `name`
|
|
545
781
|
# it is naming, pulled out because it is a self-contained
|
|
546
782
|
# computation with no dependency on the enclosing loop's own
|
|
547
783
|
# iteration state (it neither reads nor mutates anything about
|
|
548
784
|
# `bluebook`/`aggregate`/`query` beyond what `plan` already
|
|
549
785
|
# carries).
|
|
786
|
+
#
|
|
787
|
+
# @param name [Symbol] the symbolic comparison value the query names, and the
|
|
788
|
+
# attribute name to mint
|
|
789
|
+
# @param plan [QuerySpecification::HopPath::Plan] the resolved hop chain whose
|
|
790
|
+
# target's shape `name` is inferred against
|
|
791
|
+
# @return [Bluebook::Attribute, nil] a `String`-typed attribute when the tail
|
|
792
|
+
# lands on the target's lifecycle field; otherwise an attribute copying the
|
|
793
|
+
# resolved leaf's type and list-ness; `nil` when the tail resolves to nothing
|
|
550
794
|
def inferred_hop_leaf(name, plan)
|
|
551
795
|
target = plan.hops.last.target
|
|
552
796
|
head, *nested = plan.tail.to_s.split(".")
|
|
@@ -561,6 +805,14 @@ module Hecks
|
|
|
561
805
|
end
|
|
562
806
|
end
|
|
563
807
|
|
|
808
|
+
# Refuses any `where` clause on an entity's own query that hops through a reference.
|
|
809
|
+
#
|
|
810
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `entity` is nested under,
|
|
811
|
+
# named only in a raised message
|
|
812
|
+
# @param entity [Class<Bluebook::Entity>] the entity whose queries to check
|
|
813
|
+
# @return [void]
|
|
814
|
+
# @raise [Bluebook::DSL::Malformed] if any of `entity`'s queries has a `where`
|
|
815
|
+
# clause that hops through one of the entity's own references
|
|
564
816
|
def refuse_entity_query_hops!(aggregate, entity)
|
|
565
817
|
entity.queries.each do |query|
|
|
566
818
|
query.wheres.each do |clause|
|
|
@@ -575,6 +827,18 @@ module Hecks
|
|
|
575
827
|
end
|
|
576
828
|
end
|
|
577
829
|
|
|
830
|
+
# Resolves one `where` hop's target and checks where its tail lands.
|
|
831
|
+
#
|
|
832
|
+
# @param aggregate [Bluebook::Aggregate] the querying aggregate, named in a
|
|
833
|
+
# raised message
|
|
834
|
+
# @param query [Bluebook::Query] the query `clause` belongs to, named in a
|
|
835
|
+
# raised message
|
|
836
|
+
# @param clause [QuerySpecification::Common::WhereClause] the `where` clause whose
|
|
837
|
+
# `field` hops through one of `aggregate`'s own references
|
|
838
|
+
# @return [void]
|
|
839
|
+
# @raise [Bluebook::DSL::Malformed] if the hop's target is not declared in this
|
|
840
|
+
# chapter, the hop chain exceeds `QuerySpecification::HopPath::MAX_HOPS`, or
|
|
841
|
+
# the resolved tail fails `validate_hop_tail!`'s own checks
|
|
578
842
|
def validate_hop_clause!(aggregate, query, clause)
|
|
579
843
|
plan = QuerySpecification::HopPath.plan(clause.field, aggregate.attributes)
|
|
580
844
|
|
|
@@ -602,11 +866,26 @@ module Hecks
|
|
|
602
866
|
validate_hop_tail!(aggregate, query, clause, target, plan.tail)
|
|
603
867
|
end
|
|
604
868
|
|
|
605
|
-
#
|
|
869
|
+
# Checks that a resolved hop's tail lands on a real scalar field of its target.
|
|
870
|
+
#
|
|
871
|
+
# The same three-way answer seal_query_field gives for its own
|
|
606
872
|
# aggregate's fields — landing on a real scalar (fine), landing
|
|
607
873
|
# on a value object (refused by name), or naming nothing at all
|
|
608
|
-
# (refused by name) — asked instead of the hop's
|
|
874
|
+
# (refused by name) — asked instead of the hop's target aggregate,
|
|
609
875
|
# since that is whose shape the tail actually has to answer for.
|
|
876
|
+
#
|
|
877
|
+
# @param aggregate [Bluebook::Aggregate] the querying aggregate, named in a
|
|
878
|
+
# raised message
|
|
879
|
+
# @param query [Bluebook::Query] the query `clause` belongs to, named in a
|
|
880
|
+
# raised message
|
|
881
|
+
# @param clause [QuerySpecification::Common::WhereClause] the `where` clause the
|
|
882
|
+
# hop was resolved from, named in a raised message
|
|
883
|
+
# @param target [Bluebook::Aggregate] the aggregate the hop resolved to
|
|
884
|
+
# @param tail [String] the dotted field path remaining after the hop, such as
|
|
885
|
+
# `"status"` or `"amount.currency"`
|
|
886
|
+
# @return [void]
|
|
887
|
+
# @raise [Bluebook::DSL::Malformed] if `tail` lands on a value object rather
|
|
888
|
+
# than a scalar, or names no field `target` declares
|
|
610
889
|
def validate_hop_tail!(aggregate, query, clause, target, tail)
|
|
611
890
|
name, *nested = tail.to_s.split(".")
|
|
612
891
|
attribute = target.attributes.find { |candidate| candidate.name.to_s == name }
|
|
@@ -632,11 +911,30 @@ module Hecks
|
|
|
632
911
|
"not exist matches nothing and refuses nothing"
|
|
633
912
|
end
|
|
634
913
|
|
|
635
|
-
#
|
|
914
|
+
# Checks that an ordered comparator over a hopped field lands on a number.
|
|
915
|
+
#
|
|
916
|
+
# A where hop with an ordered comparator is legitimate ("client
|
|
636
917
|
# whose balance > 500") — AggregateBuilder#seal_ordered_comparator
|
|
637
918
|
# already deferred this exact check for the same reason every
|
|
638
919
|
# other hop check is deferred, and this is where it gets asked,
|
|
639
|
-
# against the hop's
|
|
920
|
+
# against the hop's target instead of the querying aggregate.
|
|
921
|
+
#
|
|
922
|
+
# @param aggregate [Bluebook::Aggregate] the querying aggregate, named in a
|
|
923
|
+
# raised message
|
|
924
|
+
# @param query [Bluebook::Query] the query `clause` belongs to, named in a
|
|
925
|
+
# raised message
|
|
926
|
+
# @param clause [QuerySpecification::Common::WhereClause] the `where` clause
|
|
927
|
+
# whose `op` and `field` are named in a raised message
|
|
928
|
+
# @param target [Bluebook::Aggregate] the aggregate the hop resolved to, named
|
|
929
|
+
# in a raised message
|
|
930
|
+
# @param attribute [Bluebook::Attribute, nil] the tail's root attribute on
|
|
931
|
+
# `target`, or `nil` when the tail lands on the lifecycle field
|
|
932
|
+
# @param nested [Array<String>] the tail's remaining dotted segments past
|
|
933
|
+
# `attribute`, `[]` for a bare field
|
|
934
|
+
# @return [void]
|
|
935
|
+
# @raise [Bluebook::DSL::Malformed] if `clause.op` is one of
|
|
936
|
+
# `AggregateBuilder::ORDERED_COMPARATORS` and the field it lands on, after
|
|
937
|
+
# `attribute`/`nested`, is not numeric
|
|
640
938
|
def validate_hop_comparator!(aggregate, query, clause, target, attribute, nested)
|
|
641
939
|
return unless AggregateBuilder::ORDERED_COMPARATORS.include?(clause.op.to_s.to_sym)
|
|
642
940
|
return if attribute &&
|
|
@@ -650,10 +948,10 @@ module Hecks
|
|
|
650
948
|
"anything else the adapters answer differently or not at all"
|
|
651
949
|
end
|
|
652
950
|
|
|
653
|
-
#
|
|
951
|
+
# The target half of `projects` validation (S12, ADR 0025) —
|
|
654
952
|
# `AggregateBuilder#seal_projected_fields` already checked the
|
|
655
|
-
#
|
|
656
|
-
# `reference_to` on
|
|
953
|
+
# local half at declare time (the reference names a real
|
|
954
|
+
# `reference_to` on this aggregate); this checks the reference
|
|
657
955
|
# actually resolves to a real aggregate in this chapter, and
|
|
658
956
|
# that aggregate really declares `remote_field` as a scalar.
|
|
659
957
|
#
|
|
@@ -665,13 +963,20 @@ module Hecks
|
|
|
665
963
|
# one resolution primitive. A single hop can never reach
|
|
666
964
|
# HopPath::MAX_HOPS, so :too_deep is structurally unreachable
|
|
667
965
|
# here and is not special-cased.
|
|
966
|
+
#
|
|
967
|
+
# @param bluebook [Bluebook::Chapter] the fully assembled chapter to check
|
|
968
|
+
# @return [void]
|
|
969
|
+
# @raise [Bluebook::DSL::Malformed] if any aggregate's `projects` field fails
|
|
970
|
+
# `validate_projected_field!`'s own checks
|
|
668
971
|
def validate_projected_fields!(bluebook)
|
|
669
972
|
bluebook.aggregates.each do |aggregate|
|
|
670
973
|
aggregate.projected_fields.each { |field| validate_projected_field!(aggregate, field) }
|
|
671
974
|
end
|
|
672
975
|
end
|
|
673
976
|
|
|
674
|
-
#
|
|
977
|
+
# Checks that one `projects` field's reference resolves to a real scalar.
|
|
978
|
+
#
|
|
979
|
+
# A linear decision tree of validation rules over one resolved
|
|
675
980
|
# hop plan, each already explained by its own comment above
|
|
676
981
|
# (the lifecycle fallback, the chained-projection fallback, the
|
|
677
982
|
# final scalar check) — a fixed, closed sequence "resolve, then
|
|
@@ -679,6 +984,13 @@ module Hecks
|
|
|
679
984
|
# land," not several unrelated concerns. Splitting it would
|
|
680
985
|
# scatter `plan`/`target`/`remote_attribute` across new methods
|
|
681
986
|
# that would each need most of them anyway.
|
|
987
|
+
#
|
|
988
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that declared `field`
|
|
989
|
+
# @param field [Bluebook::ProjectedField] the declared `projects` field to check
|
|
990
|
+
# @return [void]
|
|
991
|
+
# @raise [Bluebook::DSL::Malformed] if `field.reference` hops to an aggregate
|
|
992
|
+
# this chapter never declares, or `field.remote_field` names nothing on the
|
|
993
|
+
# target, or names something other than a scalar
|
|
682
994
|
# rubocop:disable-next Metrics/AbcSize
|
|
683
995
|
def validate_projected_field!(aggregate, field)
|
|
684
996
|
plan = QuerySpecification::HopPath.plan("#{field.reference}/#{field.remote_field}", aggregate.attributes)
|
|
@@ -693,17 +1005,17 @@ module Hecks
|
|
|
693
1005
|
target = plan.hops.last.target
|
|
694
1006
|
remote_attribute = target.attributes.find { |candidate| candidate.name.to_s == plan.tail }
|
|
695
1007
|
|
|
696
|
-
#
|
|
697
|
-
#
|
|
1008
|
+
# The worked example itself (ADR 0025) reads through a
|
|
1009
|
+
# lifecycle field — banking's Customer.status is `lifecycle
|
|
698
1010
|
# :status`, never a plain `attribute` — the same fallback
|
|
699
1011
|
# validate_hop_tail! already gives a query's own hop tail. A
|
|
700
1012
|
# lifecycle field is always a plain string by construction ;
|
|
701
1013
|
# nothing further to check once it matches by name.
|
|
702
1014
|
return if remote_attribute.nil? && target.lifecycle&.field.to_s == plan.tail
|
|
703
1015
|
|
|
704
|
-
# A
|
|
1016
|
+
# A projection may chain through another projection (S12, ADR
|
|
705
1017
|
# 0025's own boundary rule, followed through) — `target`'s
|
|
706
|
-
#
|
|
1018
|
+
# own projected fields live in `projected_fields`, a
|
|
707
1019
|
# separate list from `attributes`, so a match there is
|
|
708
1020
|
# invisible to the check above even though it names a real,
|
|
709
1021
|
# always-current, stored field. `Transfer.projects
|
|
@@ -733,14 +1045,21 @@ module Hecks
|
|
|
733
1045
|
"value, never a reference, a value object, or a list"
|
|
734
1046
|
end
|
|
735
1047
|
|
|
1048
|
+
# Decides whether a declared attribute is a plain, single scalar value.
|
|
1049
|
+
#
|
|
1050
|
+
# @param target [Bluebook::Aggregate] the aggregate `attribute` is declared on,
|
|
1051
|
+
# whose `value_object` lookup resolves `attribute`'s type
|
|
1052
|
+
# @param attribute [Bluebook::Attribute] the attribute to check
|
|
1053
|
+
# @return [Boolean] `true` when `attribute` is not a list, not a reference, and
|
|
1054
|
+
# its type names no value object `target` declares
|
|
736
1055
|
def projectable_scalar?(target, attribute)
|
|
737
1056
|
!attribute.list? && !attribute.reference? && target.value_object(attribute.type).nil?
|
|
738
1057
|
end
|
|
739
1058
|
|
|
740
|
-
# `correlates_by`
|
|
1059
|
+
# `correlates_by` names a scalar, now checked rather than trusted.
|
|
741
1060
|
#
|
|
742
1061
|
# ProcessManagerBuilder#validate! already refuses a bare, undotted
|
|
743
|
-
# spelling — a
|
|
1062
|
+
# spelling — a syntactic guarantee that the declaration cannot leave
|
|
744
1063
|
# the question open. It cannot go further: a process manager is built
|
|
745
1064
|
# in isolation, before this chapter's aggregates exist to check
|
|
746
1065
|
# against. Here, with the whole document assembled, the dotted path
|
|
@@ -757,6 +1076,14 @@ module Hecks
|
|
|
757
1076
|
# aggregate's own reference key; saga_interpreter/correlation.rb), so
|
|
758
1077
|
# an absent field is not this check's business. Only a field that
|
|
759
1078
|
# resolves, and resolves to something other than a scalar, is.
|
|
1079
|
+
#
|
|
1080
|
+
# @param process_managers [Array<Bluebook::ProcessManager>] the chapter's
|
|
1081
|
+
# declared process managers
|
|
1082
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
1083
|
+
# @return [void]
|
|
1084
|
+
# @raise [Bluebook::DSL::ProcessManagerBuilder::InvalidProcessManager] if a
|
|
1085
|
+
# process manager's `correlates_by` path resolves, on any command that emits
|
|
1086
|
+
# an event it reacts to, to something other than a scalar
|
|
760
1087
|
def validate_correlation_keys!(process_managers, aggregates)
|
|
761
1088
|
process_managers.each do |pm|
|
|
762
1089
|
next unless pm.correlates_by
|
|
@@ -769,6 +1096,15 @@ module Hecks
|
|
|
769
1096
|
end
|
|
770
1097
|
end
|
|
771
1098
|
|
|
1099
|
+
# Finds why a process manager's `correlates_by` path is not a scalar, if it isn't.
|
|
1100
|
+
#
|
|
1101
|
+
# @param process_manager [Bluebook::ProcessManager] the process manager whose
|
|
1102
|
+
# `correlates_by` to check
|
|
1103
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
1104
|
+
# @return [String, nil] the reason the path is not a scalar, on the first
|
|
1105
|
+
# emitting command whose first segment resolves; `nil` if every command that
|
|
1106
|
+
# emits a reacted-to event either omits the first segment, or resolves to
|
|
1107
|
+
# a scalar
|
|
772
1108
|
def correlation_key_violation(process_manager, aggregates)
|
|
773
1109
|
head, *rest = process_manager.correlates_by.to_s.split(".")
|
|
774
1110
|
events = reacted_events(process_manager)
|
|
@@ -784,6 +1120,12 @@ module Hecks
|
|
|
784
1120
|
nil
|
|
785
1121
|
end
|
|
786
1122
|
|
|
1123
|
+
# The demodulised names of every event a process manager reacts to.
|
|
1124
|
+
#
|
|
1125
|
+
# @param process_manager [Bluebook::ProcessManager] the process manager whose
|
|
1126
|
+
# `starts_on`, `ends_on` and handler event types to collect
|
|
1127
|
+
# @return [Array<String>] the unique demodulised event names, `REFUSED` (the
|
|
1128
|
+
# compensation trigger, not a real event) excluded
|
|
787
1129
|
def reacted_events(process_manager)
|
|
788
1130
|
([process_manager.starts_on, process_manager.ends_on] + process_manager.handlers.map(&:event_type))
|
|
789
1131
|
.compact
|
|
@@ -792,6 +1134,13 @@ module Hecks
|
|
|
792
1134
|
.uniq
|
|
793
1135
|
end
|
|
794
1136
|
|
|
1137
|
+
# Every command across the chapter that emits one of the given events.
|
|
1138
|
+
#
|
|
1139
|
+
# @param events [Array<String>] the demodulised event names to match against
|
|
1140
|
+
# each command's own `emits`
|
|
1141
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
1142
|
+
# @return [Array<Array(Bluebook::Aggregate, Class)>] one `[aggregate, command]`
|
|
1143
|
+
# pair per matching command, an aggregate's own or one of its entities'
|
|
795
1144
|
def emitting_commands(events, aggregates)
|
|
796
1145
|
aggregates.flat_map do |aggregate|
|
|
797
1146
|
commands = aggregate.commands + aggregate.entities.flat_map(&:commands)
|
|
@@ -800,6 +1149,16 @@ module Hecks
|
|
|
800
1149
|
end
|
|
801
1150
|
end
|
|
802
1151
|
|
|
1152
|
+
# Decides whether one command attribute can serve as a correlation key.
|
|
1153
|
+
#
|
|
1154
|
+
# @param owner [Bluebook::Aggregate] the aggregate whose `value_object` lookup
|
|
1155
|
+
# resolves `attribute`'s type, for `walk_scalar`
|
|
1156
|
+
# @param attribute [Bluebook::Attribute] the command attribute the correlation
|
|
1157
|
+
# path's first segment named
|
|
1158
|
+
# @param segments [Array<String>] the path's remaining dotted segments, `[]` for
|
|
1159
|
+
# a bare field
|
|
1160
|
+
# @return [String, nil] why `attribute` cannot serve as a correlation key, or
|
|
1161
|
+
# `nil` if it can — see `walk_scalar` for what a non-`nil` reason means
|
|
803
1162
|
def list_or_scalar_violation(owner, attribute, segments)
|
|
804
1163
|
if attribute.list?
|
|
805
1164
|
return "#{attribute.name} is a list — a correlation key must name one instance's own field, " \
|
|
@@ -816,6 +1175,16 @@ module Hecks
|
|
|
816
1175
|
# a value object this domain never declared, a field that value
|
|
817
1176
|
# object does not have, or a segment left over after already
|
|
818
1177
|
# reaching a scalar.
|
|
1178
|
+
#
|
|
1179
|
+
# @param owner [Bluebook::Aggregate] the aggregate whose `value_object` lookup
|
|
1180
|
+
# resolves each stepped-into type name
|
|
1181
|
+
# @param type_name [String] the type to check or step into
|
|
1182
|
+
# @param segments [Array<String>] the dotted path segments remaining to walk
|
|
1183
|
+
# @return [String, nil] `nil` when the walk bottoms out cleanly on a scalar;
|
|
1184
|
+
# otherwise why it cannot: `type_name` is a value object with no segments
|
|
1185
|
+
# left, `type_name` is already a scalar with segments left over, `type_name`
|
|
1186
|
+
# names no value object `owner` declares, the next segment names no field of
|
|
1187
|
+
# that value object, or that field is a list
|
|
819
1188
|
def walk_scalar(owner, type_name, segments)
|
|
820
1189
|
if segments.empty?
|
|
821
1190
|
return nil if Attribute::PRIMITIVES.include?(type_name)
|