hecks 2.8.0 → 2.9.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/exe/hecks +4 -6
- data/lib/hecks/adapters/driven/claude_code.rb +4 -104
- data/lib/hecks/adapters/driven/d1.rb +44 -237
- data/lib/hecks/adapters/driven/folder.rb +17 -76
- data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
- data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
- data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
- data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
- data/lib/hecks/adapters/driven/heki.rb +13 -42
- data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
- data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
- data/lib/hecks/adapters/driven/lambda.rb +10 -84
- data/lib/hecks/adapters/driven/local_storage.rb +5 -40
- data/lib/hecks/adapters/driven/memory.rb +17 -98
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
- data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
- data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
- data/lib/hecks/adapters/driven/postgres.rb +145 -134
- data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
- data/lib/hecks/adapters/driven/prism.rb +7 -30
- data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
- data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
- data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
- data/lib/hecks/adapters/driven/sqlite.rb +128 -65
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +5 -48
- data/lib/hecks/adapters/driven.rb +5 -36
- data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
- data/lib/hecks/behaviors/dsl.rb +7 -25
- data/lib/hecks/behaviors/expectations.rb +31 -141
- data/lib/hecks/behaviors/ir.rb +7 -20
- data/lib/hecks/behaviors/rspec.rb +6 -17
- data/lib/hecks/behaviors/runner.rb +10 -34
- data/lib/hecks/behaviors.rb +4 -8
- data/lib/hecks/bluebook/aggregate.rb +9 -75
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
- data/lib/hecks/bluebook/assembly/build.rb +4 -15
- data/lib/hecks/bluebook/assembly/contract.rb +15 -136
- data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
- data/lib/hecks/bluebook/assembly/marks.rb +31 -223
- data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
- data/lib/hecks/bluebook/assembly.rb +6 -45
- data/lib/hecks/bluebook/attribute.rb +12 -61
- data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
- data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
- data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
- data/lib/hecks/bluebook/behaviour/command.rb +14 -66
- data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
- data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
- data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
- data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
- data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
- data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
- data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
- data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
- data/lib/hecks/bluebook/capabilities.rb +17 -49
- data/lib/hecks/bluebook/chapter.rb +10 -48
- data/lib/hecks/bluebook/command.rb +23 -81
- data/lib/hecks/bluebook/domain_port.rb +14 -63
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
- data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
- data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -194
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
- data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
- data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
- data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
- data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
- data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
- data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
- data/lib/hecks/bluebook/dsl/world_builder.rb +19 -111
- data/lib/hecks/bluebook/entity.rb +13 -71
- data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
- data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
- data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
- data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
- data/lib/hecks/bluebook/expression/resolver.rb +37 -660
- data/lib/hecks/bluebook/hexagon.rb +5 -28
- data/lib/hecks/bluebook/lifecycle.rb +5 -16
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
- data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
- data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
- data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +43 -305
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
- data/lib/hecks/bluebook/meta_validator.rb +79 -349
- data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
- data/lib/hecks/bluebook/model_check.rb +100 -597
- data/lib/hecks/bluebook/pattern_subset.rb +11 -69
- data/lib/hecks/bluebook/process_manager.rb +13 -83
- data/lib/hecks/bluebook/project_loader.rb +3 -39
- data/lib/hecks/bluebook/project_register.rb +12 -60
- data/lib/hecks/bluebook/query.rb +9 -48
- data/lib/hecks/bluebook/read_model.rb +57 -81
- data/lib/hecks/bluebook/reference.rb +7 -41
- data/lib/hecks/bluebook/smoke_test.rb +8 -98
- data/lib/hecks/bluebook/synthesizer.rb +13 -62
- data/lib/hecks/bluebook/translation.rb +26 -62
- data/lib/hecks/bluebook/value_object.rb +11 -36
- data/lib/hecks/bluebook.rb +5 -22
- data/lib/hecks/cache_dir.rb +4 -23
- data/lib/hecks/cli/document.rb +3 -21
- data/lib/hecks/cli/ir.rb +3 -5
- data/lib/hecks/cli/mcp.rb +2 -3
- data/lib/hecks/cli/mcp_door.rb +19 -114
- data/lib/hecks/cli/model_check.rb +18 -85
- data/lib/hecks/cli/project_cli.rb +7 -40
- data/lib/hecks/cli/project_diagrams.rb +0 -7
- data/lib/hecks/cli/run.rb +12 -58
- data/lib/hecks/cli/smoke_test.rb +2 -16
- data/lib/hecks/cli/stores.rb +1 -2
- data/lib/hecks/cli.rb +18 -24
- data/lib/hecks/construct.rb +8 -34
- data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
- data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
- data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
- data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
- data/lib/hecks/embryonaut_bluebook.rb +4 -71
- data/lib/hecks/facade/cli_door.rb +24 -106
- data/lib/hecks/facade/cli_runner.rb +28 -172
- data/lib/hecks/facade/command_request.rb +13 -33
- data/lib/hecks/facade/handle.rb +34 -121
- data/lib/hecks/facade/json_door.rb +31 -146
- data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
- data/lib/hecks/facade/surface/chapter.rb +10 -54
- data/lib/hecks/facade/surface.rb +8 -23
- data/lib/hecks/facade.rb +10 -25
- data/lib/hecks/forms/app.rb +15 -70
- data/lib/hecks/forms/command_form_renderer.rb +10 -29
- data/lib/hecks/forms/field_renderer.rb +17 -156
- data/lib/hecks/forms/field_shape.rb +30 -222
- data/lib/hecks/forms/html.rb +12 -47
- data/lib/hecks/forms/index_renderer.rb +1 -4
- data/lib/hecks/forms/page.rb +3 -9
- data/lib/hecks/forms/params.rb +14 -60
- data/lib/hecks/forms/port_argument.rb +2 -19
- data/lib/hecks/forms/query_form_renderer.rb +4 -79
- data/lib/hecks/forms/record_renderer.rb +8 -75
- data/lib/hecks/forms/record_table.rb +7 -50
- data/lib/hecks/forms/reference_options.rb +3 -30
- data/lib/hecks/forms/value_object_shape.rb +8 -35
- data/lib/hecks/forms.rb +5 -33
- data/lib/hecks/fqn.rb +5 -12
- data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
- data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
- data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
- data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
- data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
- data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
- data/lib/hecks/framework.rb +11 -80
- data/lib/hecks/freezer.rb +5 -39
- data/lib/hecks/grammar/expression.bluebook +15 -40
- data/lib/hecks/grammar/translation.bluebook +3 -6
- data/lib/hecks/grammar.rb +16 -41
- data/lib/hecks/ir.rb +14 -83
- data/lib/hecks/language/adapter.bluebook +2 -7
- data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
- data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
- data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
- data/lib/hecks/language/bluebook/command.bluebook +27 -144
- data/lib/hecks/language/bluebook/entity.bluebook +24 -97
- data/lib/hecks/language/bluebook/policy.bluebook +16 -67
- data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
- data/lib/hecks/language/bluebook/projection.bluebook +126 -60
- data/lib/hecks/language/bluebook/query.bluebook +12 -43
- data/lib/hecks/language/bluebook/shape.bluebook +23 -73
- data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
- data/lib/hecks/language/bluebook/vocabulary.bluebook +82 -197
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
- data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
- data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
- data/lib/hecks/language/oidc.json +35 -0
- data/lib/hecks/language/port.bluebook +4 -17
- data/lib/hecks/language/translation/translation.bluebook +3 -9
- data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
- data/lib/hecks/language/world/wiring.bluebook +1 -3
- data/lib/hecks/language/world/world.bluebook +5 -7
- data/lib/hecks/literal.rb +8 -50
- data/lib/hecks/mcp_door_scope.rb +9 -90
- data/lib/hecks/mcp_stdio_guard.rb +4 -41
- data/lib/hecks/naming.rb +29 -137
- data/lib/hecks/ports/access_control.rb +7 -23
- data/lib/hecks/ports/agent/answers.rb +5 -23
- data/lib/hecks/ports/agent.rb +47 -172
- data/lib/hecks/ports/authentication.rb +12 -39
- data/lib/hecks/ports/authorization.rb +16 -43
- data/lib/hecks/ports/clock.rb +5 -45
- data/lib/hecks/ports/extraction.rb +7 -12
- data/lib/hecks/ports/identity_assignment.rb +8 -29
- data/lib/hecks/ports/identity_generation.rb +6 -29
- data/lib/hecks/ports/identity_resolution.rb +2 -10
- data/lib/hecks/ports/key_vault.rb +9 -45
- data/lib/hecks/ports/persistence/append_only.rb +61 -64
- data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
- data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
- data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
- data/lib/hecks/ports/persistence/plugin.rb +13 -42
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
- data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
- data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
- data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
- data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
- data/lib/hecks/ports/persistence/state_codec.rb +23 -103
- data/lib/hecks/ports/persistence.rb +7 -14
- data/lib/hecks/ports/projection.rb +42 -84
- data/lib/hecks/ports/query/in_memory.rb +10 -58
- data/lib/hecks/ports/query/ordering.rb +11 -32
- data/lib/hecks/ports/query.rb +14 -37
- data/lib/hecks/ports.rb +2 -8
- data/lib/hecks/projections/bootstrap_table.rb +11 -43
- data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
- data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
- data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
- data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
- data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
- data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
- data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
- data/lib/hecks/projections/deploy/fargate.rb +35 -202
- data/lib/hecks/projections/deploy/lambda.rb +150 -678
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
- data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
- data/lib/hecks/projections/deploy/preview/script.rb +2 -14
- data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
- data/lib/hecks/projections/deploy/preview/template.rb +2 -58
- data/lib/hecks/projections/deploy/preview.rb +2 -67
- data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
- data/lib/hecks/projections/deploy/scripts.rb +4 -57
- data/lib/hecks/projections/deploy/shared.rb +47 -146
- data/lib/hecks/projections/deploy/smoke.rb +4 -61
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
- data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
- data/lib/hecks/projections/deploy/template_diff.rb +5 -57
- data/lib/hecks/projections/diagrams.rb +20 -582
- data/lib/hecks/projections/glossary/html.rb +14 -130
- data/lib/hecks/projections/glossary/markdown.rb +8 -69
- data/lib/hecks/projections/glossary/mermaid.rb +9 -66
- data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
- data/lib/hecks/projections/glossary/sentences.rb +14 -159
- data/lib/hecks/projections/glossary.rb +30 -222
- data/lib/hecks/projections/ir.rb +3 -10
- data/lib/hecks/projections/model/deviations.rb +15 -42
- data/lib/hecks/projections/model.rb +12 -51
- data/lib/hecks/projections/oidc.rb +12 -56
- data/lib/hecks/projections/parser_table.rb +9 -28
- data/lib/hecks/projections/reference.rb +2 -15
- data/lib/hecks/projections/rust_vocabulary.rb +12 -250
- data/lib/hecks/projections/shape.rb +3 -29
- data/lib/hecks/projections/statements.rb +7 -86
- data/lib/hecks/projections/vocabulary.rb +6 -22
- data/lib/hecks/projections.rb +2 -16
- data/lib/hecks/projector/cli_projector.rb +60 -375
- data/lib/hecks/projector/docs_projector.rb +10 -201
- data/lib/hecks/projector/exporter.rb +42 -134
- data/lib/hecks/projector/ir_projector.rb +2 -9
- data/lib/hecks/projector/narrate_projector.rb +15 -189
- data/lib/hecks/projector/target.rb +13 -76
- data/lib/hecks/projector.rb +30 -121
- data/lib/hecks/query_specification/common/comparators.rb +5 -26
- data/lib/hecks/query_specification/common/comparison.rb +35 -143
- data/lib/hecks/query_specification/common/dsl.rb +8 -24
- data/lib/hecks/query_specification/common/null_policy.rb +20 -70
- data/lib/hecks/query_specification/common/options.rb +2 -6
- data/lib/hecks/query_specification/common/where_clause.rb +2 -10
- data/lib/hecks/query_specification/field_path.rb +37 -77
- data/lib/hecks/query_specification/hop_path.rb +12 -76
- data/lib/hecks/release/gem_pin.rb +2 -26
- data/lib/hecks/release/runner/ci_publisher.rb +3 -6
- data/lib/hecks/release/runner/commands.rb +2 -4
- data/lib/hecks/release/runner/gem_publisher.rb +2 -3
- data/lib/hecks/release/runner/npm_publisher.rb +6 -20
- data/lib/hecks/release/runner/preflight.rb +4 -19
- data/lib/hecks/release/runner/tagger.rb +2 -9
- data/lib/hecks/release/runner.rb +3 -32
- data/lib/hecks/rendering.rb +4 -26
- data/lib/hecks/router/namespace_installer.rb +12 -36
- data/lib/hecks/router.rb +4 -8
- data/lib/hecks/runtime/aggregate_lock.rb +8 -33
- data/lib/hecks/runtime/boot_gates.rb +3 -18
- data/lib/hecks/runtime/caller.rb +19 -59
- data/lib/hecks/runtime/capability_graph.rb +5 -21
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
- data/lib/hecks/runtime/command_interpreter.rb +124 -306
- data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
- data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
- data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
- data/lib/hecks/runtime/command_rules/emission.rb +8 -21
- data/lib/hecks/runtime/command_rules/references.rb +23 -202
- data/lib/hecks/runtime/dependency_planning.rb +30 -102
- data/lib/hecks/runtime/dispatcher.rb +82 -241
- data/lib/hecks/runtime/entity_element.rb +40 -462
- data/lib/hecks/runtime/entity_interpreter.rb +64 -219
- data/lib/hecks/runtime/errors.rb +32 -115
- data/lib/hecks/runtime/event.rb +3 -25
- data/lib/hecks/runtime/identity.rb +18 -154
- data/lib/hecks/runtime/instance.rb +18 -150
- data/lib/hecks/runtime/interpreting.rb +11 -70
- data/lib/hecks/runtime/invocation.rb +28 -164
- data/lib/hecks/runtime/loader.rb +48 -186
- data/lib/hecks/runtime/outbox.rb +38 -261
- data/lib/hecks/runtime/policy_interpreter.rb +38 -200
- data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
- data/lib/hecks/runtime/query_interpreter.rb +45 -187
- data/lib/hecks/runtime/reaction_invocation.rb +36 -138
- data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
- data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
- data/lib/hecks/runtime/reference_hop.rb +8 -81
- data/lib/hecks/runtime/refusal_wording.rb +8 -89
- data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
- data/lib/hecks/runtime/registry/verification.rb +52 -225
- data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
- data/lib/hecks/runtime/registry.rb +99 -244
- data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
- data/lib/hecks/runtime/routing.rb +4 -15
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
- data/lib/hecks/runtime/saga_interpreter.rb +79 -263
- data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
- data/lib/hecks/runtime/tenant_check.rb +6 -68
- data/lib/hecks/runtime/tenant_scope.rb +3 -28
- data/lib/hecks/runtime/value/admission.rb +16 -67
- data/lib/hecks/runtime/value/coercion.rb +155 -641
- data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
- data/lib/hecks/runtime/value.rb +12 -84
- data/lib/hecks/runtime.rb +17 -65
- data/lib/hecks/storehouse.rb +141 -568
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
- data/lib/hecks/vendoring/git_environment.rb +2 -8
- data/lib/hecks/vendoring/git_source.rb +4 -10
- data/lib/hecks/vendoring.rb +7 -46
- data/lib/hecks/version.rb +3 -14
- data/lib/hecks.rb +26 -81
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
- metadata +3 -2
|
@@ -2,52 +2,20 @@ module Hecks
|
|
|
2
2
|
module Bluebook
|
|
3
3
|
module DSL
|
|
4
4
|
class BluebookBuilder
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# runs once a chapter is fully assembled (reference targets, event
|
|
8
|
-
# shapes, correlation keys, query hops, projected fields,
|
|
9
|
-
# bidirectional-reference cycles), plus each one's own private
|
|
10
|
-
# helpers. Split out of bluebook_builder.rb (which keeps the DSL
|
|
11
|
-
# surface — chapter/aggregate/read_model/policy declaration,
|
|
12
|
-
# `#build`, and `self.build`) for the same reason
|
|
13
|
-
# AggregateBuilder::Sealing exists: one cohesive concern, filed by
|
|
14
|
-
# responsibility rather than by an arbitrary line count.
|
|
15
|
-
#
|
|
16
|
-
# `extend`ed onto BluebookBuilder rather than `include`d — every
|
|
17
|
-
# one of these is a pure function of its own explicit arguments
|
|
18
|
-
# (an assembled `Bluebook::Chapter`, an aggregate list, ...), never
|
|
19
|
-
# builder-instance state (no `@ivar` read anywhere in this
|
|
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
|
-
# `Runtime::Registry` already uses (`include Verification`), just
|
|
26
|
-
# turning each method into a class (singleton) method instead of
|
|
27
|
-
# an instance method, the same role `def self.foo` fills on a
|
|
28
|
-
# class that keeps its methods in one file.
|
|
5
|
+
# Whole-chapter, cross-aggregate checks that `#build` runs once a chapter is assembled.
|
|
6
|
+
# Extended onto BluebookBuilder: pure functions of their arguments, no builder state.
|
|
29
7
|
module Validation
|
|
30
|
-
# Runs every whole-chapter check against one
|
|
8
|
+
# Runs every whole-chapter check against one assembled chapter.
|
|
9
|
+
# Public so `MetaValidator.judge_deferred!` can call it with no builder instance.
|
|
31
10
|
#
|
|
32
|
-
#
|
|
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
|
|
11
|
+
# @param bluebook [Bluebook::Chapter] the assembled chapter to validate
|
|
43
12
|
# @return [void]
|
|
44
|
-
# @raise [Bluebook::DSL::Malformed] if any
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# manager's `correlates_by` resolves to something other than a scalar field
|
|
13
|
+
# @raise [Bluebook::DSL::Malformed] if any check finds a violation
|
|
14
|
+
# @raise [Bluebook::DSL::ProcessManagerBuilder::InvalidProcessManager] if a
|
|
15
|
+
# `correlates_by` resolves to something other than a scalar field
|
|
48
16
|
def validate_assembled!(bluebook)
|
|
49
|
-
#
|
|
50
|
-
#
|
|
17
|
+
# an attribute type references its Shape, so an undeclared value object
|
|
18
|
+
# fails resolution
|
|
51
19
|
validate_reference_value_objects!(bluebook.aggregates)
|
|
52
20
|
validate_correlation_keys!(bluebook.process_managers, bluebook.aggregates)
|
|
53
21
|
validate_no_bidirectional_references!(bluebook.aggregates)
|
|
@@ -56,40 +24,18 @@ module Hecks
|
|
|
56
24
|
validate_with_projections!(bluebook.policies, bluebook.process_managers, bluebook.aggregates)
|
|
57
25
|
end
|
|
58
26
|
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
# hop can be checked, for exactly the reason
|
|
62
|
-
# validate_no_bidirectional_references! above already gives:
|
|
63
|
-
# `Bluebook.new` just stamped `hecks_owner` on every
|
|
64
|
-
# aggregate, so `Reference#resolve` finally has a chapter to
|
|
65
|
-
# walk. Before this line every target in the file (including
|
|
66
|
-
# ones declared above the aggregate doing the asking) would
|
|
67
|
-
# have resolved to nil.
|
|
27
|
+
# Hops resolve only now: `Bluebook.new` has just stamped `hecks_owner` on
|
|
28
|
+
# every aggregate.
|
|
68
29
|
infer_hop_query_arguments!(bluebook)
|
|
69
30
|
validate_query_hops!(bluebook)
|
|
70
31
|
|
|
71
|
-
#
|
|
72
|
-
# own reference cannot resolve until every aggregate in the
|
|
73
|
-
# chapter is real and owner-stamped (S12, ADR 0025).
|
|
32
|
+
# A `projects` reference needs the same owner-stamped aggregates to resolve (ADR 0025).
|
|
74
33
|
validate_projected_fields!(bluebook)
|
|
75
34
|
|
|
76
35
|
validate_provisions!(bluebook)
|
|
77
36
|
end
|
|
78
37
|
|
|
79
38
|
# 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
39
|
def validate_provisions!(bluebook)
|
|
94
40
|
bluebook.provides.group_by(&:capability).each do |capability, rows|
|
|
95
41
|
contract = Capabilities::CONTRACTS.fetch(capability) do
|
|
@@ -107,17 +53,6 @@ module Hecks
|
|
|
107
53
|
end
|
|
108
54
|
end
|
|
109
55
|
|
|
110
|
-
# Checks that one `provides` row's verb names a real command or query.
|
|
111
|
-
#
|
|
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`, `:query` or `:port_operation`, the kind of
|
|
117
|
-
# verb this row's key 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
56
|
def validate_provided_verb!(bluebook, capability, row, kind)
|
|
122
57
|
return validate_provided_port_operation!(bluebook, capability, row) if kind == :port_operation
|
|
123
58
|
|
|
@@ -129,18 +64,9 @@ module Hecks
|
|
|
129
64
|
"which names no #{kind} this chapter declares (spelled \"Aggregate.#{kind.capitalize}\")"
|
|
130
65
|
end
|
|
131
66
|
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
# verb has the `"Aggregate.Port.Operation"` shape and its aggregate is
|
|
136
|
-
# this chapter's; `Registry#verify!` checks the operation exists.
|
|
137
|
-
#
|
|
138
|
-
# @param bluebook [Bluebook::Chapter] the chapter the row was declared on
|
|
139
|
-
# @param capability [String] the capability the row belongs to
|
|
140
|
-
# @param row [Bluebook::Chapter::Provision] the one declared row to check
|
|
141
|
-
# @return [void]
|
|
142
|
-
# @raise [Bluebook::DSL::Malformed] if the verb is not `"Aggregate.Port.Operation"`
|
|
143
|
-
# or names an aggregate this chapter does not declare
|
|
67
|
+
# Only the verb's shape and aggregate are checkable here: the port is declared
|
|
68
|
+
# in the hecksagon, which attaches after the chapter is built. `Registry#verify!`
|
|
69
|
+
# checks the operation.
|
|
144
70
|
def validate_provided_port_operation!(bluebook, capability, row)
|
|
145
71
|
aggregate_name, port, operation = row.verb.split(".", 3)
|
|
146
72
|
return if bluebook.aggregate(aggregate_name) && port && operation && !operation.include?(".")
|
|
@@ -149,44 +75,13 @@ module Hecks
|
|
|
149
75
|
"which is not spelled \"Aggregate.Port.Operation\" over an aggregate this chapter declares"
|
|
150
76
|
end
|
|
151
77
|
|
|
152
|
-
# Names of the members a `provides` verb of the given kind may address.
|
|
153
|
-
#
|
|
154
|
-
# @param aggregate [Bluebook::Aggregate] the aggregate the verb is scoped to
|
|
155
|
-
# @param kind [Symbol] `:command` or `:query`
|
|
156
|
-
# @return [Array<String>] every command's `hecks_name` when `kind` is `:command`,
|
|
157
|
-
# else every query's declared `name`
|
|
158
78
|
def provided_member_names(aggregate, kind)
|
|
159
79
|
kind == :command ? aggregate.commands.map(&:hecks_name) : aggregate.queries.map(&:name)
|
|
160
80
|
end
|
|
161
81
|
|
|
162
|
-
# Refuses an entity command that names
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
# the refusal message once read "references must target aggregate
|
|
166
|
-
# heads," a claim broader than what this actually checks.
|
|
167
|
-
#
|
|
168
|
-
# `CommandBuilder#reference_to` sets `references` only when the target's
|
|
169
|
-
# bare name equals the owner's ; anything else becomes a reference
|
|
170
|
-
# attribute. So on an aggregate command `references` is always a copy of
|
|
171
|
-
# that aggregate's own name, and looking it up in an index of aggregates
|
|
172
|
-
# is a tautology — that branch never refused anything and structurally
|
|
173
|
-
# could not. Verified across all eight golden chapters before deleting it.
|
|
174
|
-
#
|
|
175
|
-
# On a piece's command the owner is the entity, and an entity is not a
|
|
176
|
-
# head, so what this actually refuses is `reference_to <its own name>`
|
|
177
|
-
# written inside `entity do … end`. A piece is reached through its
|
|
178
|
-
# aggregate ; a command on one addresses the aggregate, never the piece.
|
|
179
|
-
#
|
|
180
|
-
# Reference attributes are the language's business now — offered as the
|
|
181
|
-
# head's own id and resolved as references, so `Aggregate.Reference` and
|
|
182
|
-
# `Command.Reference` refuse an undeclared head with no predicate at all.
|
|
183
|
-
#
|
|
184
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
185
|
-
# @return [void]
|
|
186
|
-
# @raise [Bluebook::DSL::Malformed] if an entity command's `references` names
|
|
187
|
-
# something other than one of this chapter's own aggregate heads — which, given
|
|
188
|
-
# how `CommandBuilder#reference_to` sets it, only happens when the entity command
|
|
189
|
-
# names its own entity as its root
|
|
82
|
+
# Refuses an entity command that names its own entity as its root.
|
|
83
|
+
# `CommandBuilder#reference_to` sets `references` only to the owner's bare name, so on
|
|
84
|
+
# an aggregate command a head lookup is a tautology; only an entity self-reference fails.
|
|
190
85
|
def validate_reference_value_objects!(aggregates)
|
|
191
86
|
heads = aggregates.map(&:hecks_name)
|
|
192
87
|
|
|
@@ -207,27 +102,8 @@ module Hecks
|
|
|
207
102
|
"an entity command is addressed through its aggregate; #{violations.uniq.join('; ')}"
|
|
208
103
|
end
|
|
209
104
|
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
# object with its own attributes, not a label"). No new `event
|
|
213
|
-
# do ... end` construct exists to hand-author and keep in step
|
|
214
|
-
# with every emitting command by hand — an event's own known
|
|
215
|
-
# shape is whichever command(s) declare `emits` for its name, and
|
|
216
|
-
# this is the one thing that has to hold for that convention to
|
|
217
|
-
# mean anything: every command that emits a given name has to
|
|
218
|
-
# agree on what it carries. An event is one fact; a fact does not
|
|
219
|
-
# carry two different truths depending on who is telling it.
|
|
220
|
-
#
|
|
221
|
-
# Structural fields only (name/type/list/optional) — `pattern:`/
|
|
222
|
-
# `admits:`/`default:` are refinements on a field, not a second
|
|
223
|
-
# claim about what the payload holds, so two emitting commands
|
|
224
|
-
# are free to differ there without actually disagreeing about
|
|
225
|
-
# the event's own shape.
|
|
226
|
-
#
|
|
227
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
228
|
-
# @return [void]
|
|
229
|
-
# @raise [Bluebook::DSL::Malformed] if two or more commands `emits` the same event
|
|
230
|
-
# name with different structural shapes
|
|
105
|
+
# Every command that emits a given event must agree on its structural shape
|
|
106
|
+
# (name, type, list, optional); `pattern:`/`admits:`/`default:` may differ (ADR 0025).
|
|
231
107
|
def validate_event_shapes!(aggregates)
|
|
232
108
|
event_emitters(aggregates).each do |event_name, pairs|
|
|
233
109
|
next if pairs.size == 1
|
|
@@ -242,39 +118,10 @@ module Hecks
|
|
|
242
118
|
end
|
|
243
119
|
end
|
|
244
120
|
|
|
245
|
-
#
|
|
246
|
-
#
|
|
247
|
-
#
|
|
248
|
-
#
|
|
249
|
-
# shape per event name, and command references being first-class
|
|
250
|
-
# (`Naming.command_ref`) means the target side is a real
|
|
251
|
-
# resolvable command, not a string that might be a typo.
|
|
252
|
-
#
|
|
253
|
-
# **Same-chapter only, on purpose** — a `with:` whose source event or
|
|
254
|
-
# target command lives outside this chapter (an `across` policy
|
|
255
|
-
# reacting to another domain's event entirely) is silently left
|
|
256
|
-
# unchecked rather than refused: there is nothing here yet to
|
|
257
|
-
# check it against, and "unresolvable" is not the same claim as
|
|
258
|
-
# "wrong."
|
|
259
|
-
#
|
|
260
|
-
# A FOR_EACH policy's source isn't the event at all — a fan-out
|
|
261
|
-
# `with:`'s symbols read the query row `for_each` answers
|
|
262
|
-
# (FreezeAccountsOnSuspension's own comment: "`account` is the
|
|
263
|
-
# key the fan-out merges for each row"), which this has no shape
|
|
264
|
-
# for; the source half is skipped for those, the target half
|
|
265
|
-
# (does the dispatched command actually declare the field) still
|
|
266
|
-
# runs, since that half is true regardless of where the value
|
|
267
|
-
# came from.
|
|
268
|
-
#
|
|
269
|
-
# @param policies [Array<Bluebook::Policy>] every reaction hoisted onto the
|
|
270
|
-
# chapter's own aggregates
|
|
271
|
-
# @param process_managers [Array<Bluebook::ProcessManager>] the chapter's declared
|
|
272
|
-
# process managers
|
|
273
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
274
|
-
# @return [void]
|
|
275
|
-
# @raise [Bluebook::DSL::Malformed] if a policy's or a saga dispatch's `with:`
|
|
276
|
-
# names a field its target command does not declare, or reads a source field
|
|
277
|
-
# its event does not carry (see `check_with_spec!` for the full rule)
|
|
121
|
+
# Checks `with:` projections against the source event's shape and the target's fields.
|
|
122
|
+
# Same-chapter only: an unresolvable cross-chapter source or target is skipped.
|
|
123
|
+
# A for_each policy's source is a query row with no known shape, so only its target
|
|
124
|
+
# is checked.
|
|
278
125
|
def validate_with_projections!(policies, process_managers, aggregates)
|
|
279
126
|
lookup = command_lookup(aggregates)
|
|
280
127
|
heads = correlation_heads(process_managers)
|
|
@@ -299,47 +146,11 @@ module Hecks
|
|
|
299
146
|
end
|
|
300
147
|
end
|
|
301
148
|
|
|
302
|
-
# `
|
|
303
|
-
#
|
|
304
|
-
#
|
|
305
|
-
# saga
|
|
306
|
-
#
|
|
307
|
-
# memory starts as the opening event's payload
|
|
308
|
-
# (`SagaInterpreter#instance = { ..., memory: event.payload }`,
|
|
309
|
-
# never updated after), never the leg's own. Settlement's own
|
|
310
|
-
# comment names exactly this: "the credit leg reads a destination
|
|
311
|
-
# no event carried" — `AccountDebited` never declares `:reference`,
|
|
312
|
-
# only `TransferRequested` (`pm.starts_on`) does, and that is
|
|
313
|
-
# where the value is genuinely still coming from.
|
|
314
|
-
# One closed sequence of per-field checks against a fixed set of
|
|
315
|
-
# legal `with:` sources (target's own attributes, correlation,
|
|
316
|
-
# emitter identity, event/memory shape) — see the comment above
|
|
317
|
-
# for why each source is legal. Extracting the loop body would
|
|
318
|
-
# mean threading ten-plus already-resolved locals (target,
|
|
319
|
-
# source_shape, memory_shape, correlation, identity_sources, ...)
|
|
320
|
-
# into a new method for no gain: each `next`/`raise` already
|
|
321
|
-
# reads as its own rule at its own site.
|
|
322
|
-
#
|
|
323
|
-
# @param command_ref [String] the dotted name of the command `with:` dispatches,
|
|
324
|
-
# such as `"Account.Debit"` or `"Account.Piece.Move"`
|
|
325
|
-
# @param event_name [String, nil] the triggering event's name, or `nil` for a
|
|
326
|
-
# FOR_EACH policy (whose source is a query row, not an event)
|
|
327
|
-
# @param with_spec [Array<Array(Symbol, Object)>] the declared `with:` pairs,
|
|
328
|
-
# field name to source — a Symbol naming a field to read, or a literal value
|
|
329
|
-
# @param lookup [Hash{String => Class}] every command in the chapter, keyed by
|
|
330
|
-
# `"owner.hecks_name"` (`command_lookup`'s own shape)
|
|
331
|
-
# @param label [String] a human-readable description of what declared this
|
|
332
|
-
# `with:`, used only in a raised message
|
|
333
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
334
|
-
# @param correlation_heads [Array<Symbol>] every saga's own `correlates_by` head
|
|
335
|
-
# in this chapter, a legal addressing key alongside a command's own attributes
|
|
336
|
-
# @param process_manager [Bluebook::ProcessManager, nil] the process manager this
|
|
337
|
-
# `with:` dispatches from, or `nil` for a policy's own trigger
|
|
338
|
-
# @return [void]
|
|
339
|
-
# @raise [Bluebook::DSL::Malformed] if `with:` names a field the target command
|
|
340
|
-
# does not declare, or reads a Symbol source that resolves to neither the
|
|
341
|
-
# correlation key, the emitter's identity, nor a field the triggering event or
|
|
342
|
-
# (for a saga leg) the starting event's shape carries
|
|
149
|
+
# Checks one `with:` spec: each key must be a field the target command accepts,
|
|
150
|
+
# and each Symbol source must be the correlation key, the emitter's identity, or a
|
|
151
|
+
# field of the event's shape. A saga leg also falls back to the opening event's shape,
|
|
152
|
+
# since saga memory starts as that payload. One closed sequence of per-field checks;
|
|
153
|
+
# splitting it threads many locals.
|
|
343
154
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
344
155
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
345
156
|
def check_with_spec!(command_ref, event_name, with_spec, lookup, label, aggregates, correlation_heads,
|
|
@@ -348,16 +159,9 @@ module Hecks
|
|
|
348
159
|
source_shape = event_name && event_shape_for(event_name, aggregates)
|
|
349
160
|
memory_shape = process_manager && event_shape_for(process_manager.starts_on, aggregates)
|
|
350
161
|
correlation = process_manager&.correlates_by && process_manager.correlation_head
|
|
351
|
-
# A policy's source also carries the emitter's
|
|
352
|
-
# `PolicyInterpreter#emitter_identity
|
|
353
|
-
#
|
|
354
|
-
# identity (it arrives through `reference_to`, not an
|
|
355
|
-
# `attribute`), so before this a policy on `KnightCaptured` could
|
|
356
|
-
# not spell `with: { label: :label }` at all — "reads :label off
|
|
357
|
-
# KnightCaptured, which does not declare it" — and chess's
|
|
358
|
-
# AdvancePly grew optional, unread attributes just to survive a
|
|
359
|
-
# wholesale forward. Policies only: a saga leg's own source is
|
|
360
|
-
# `SagaInterpreter#dispatch_args`, which merges no such thing.
|
|
162
|
+
# A policy's source also carries the emitter's identity
|
|
163
|
+
# (`PolicyInterpreter#emitter_identity`); a saga leg's source
|
|
164
|
+
# (`SagaInterpreter#dispatch_args`) merges no such thing.
|
|
361
165
|
identity_sources = process_manager.nil? && event_name ? event_identity_heads_for(event_name, aggregates) : []
|
|
362
166
|
|
|
363
167
|
with_spec.each do |field, source|
|
|
@@ -380,34 +184,9 @@ module Hecks
|
|
|
380
184
|
end
|
|
381
185
|
end
|
|
382
186
|
|
|
383
|
-
#
|
|
384
|
-
#
|
|
385
|
-
#
|
|
386
|
-
# `attributes` — `CommandBuilder#reference_to`'s self-reference
|
|
387
|
-
# branch sets `command.references` instead (S2), and mints no new
|
|
388
|
-
# field at all. What addresses it is not one name but the same
|
|
389
|
-
# set `CommandInterpreter::ArgumentGate#refuse_unknown_arguments`
|
|
390
|
-
# already accepts at dispatch time — `:id`, the owning aggregate's
|
|
391
|
-
# own `identity_heads` (real corpus proof — `Account.Debit`
|
|
392
|
-
# dispatched everywhere as `number: ...`, `Account`'s own
|
|
393
|
-
# `identified_by`), and `Naming.reference_key(command.references)`
|
|
394
|
-
# (real corpus proof — `FreezeAccountsOnSuspension`'s `for_each`
|
|
395
|
-
# fan-out, whose own comment reads "`account` is the key the
|
|
396
|
-
# fan-out merges for each row it answers"). Both are simultaneously
|
|
397
|
-
# legal there, not context-dependent alternatives, so both are
|
|
398
|
-
# legal here : this mirrors that gate rather than re-deriving a
|
|
399
|
-
# narrower rule that would refuse one of two real, already-shipped
|
|
400
|
-
# dispatch conventions.
|
|
401
|
-
#
|
|
402
|
-
# @param command [Class<Bluebook::Command>] the command `with:` dispatches
|
|
403
|
-
# @param field [Symbol] the `with:` key to check
|
|
404
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates,
|
|
405
|
-
# searched when `field` might name `command`'s own reference key
|
|
406
|
-
# @param correlation_heads [Array<Symbol>] every saga's own `correlates_by` head
|
|
407
|
-
# in this chapter
|
|
408
|
-
# @return [Boolean] `true` when `field` is one of `command`'s own attributes,
|
|
409
|
-
# `:id`, a correlation head, or the identity/reference key of the aggregate
|
|
410
|
-
# `command.references` names
|
|
187
|
+
# Mirrors `ArgumentGate#refuse_unknown_arguments`: `:id`, the owning aggregate's
|
|
188
|
+
# `identity_heads` and `Naming.reference_key(command.references)` are all legal keys
|
|
189
|
+
# beside the command's own attributes.
|
|
411
190
|
def command_declares?(command, field, aggregates, correlation_heads)
|
|
412
191
|
return true if command.attributes.any? { |a| a.name == field }
|
|
413
192
|
return true if field == :id
|
|
@@ -420,38 +199,14 @@ module Hecks
|
|
|
420
199
|
referenced.identity_heads.include?(field) || Naming.reference_key(command.references) == field
|
|
421
200
|
end
|
|
422
201
|
|
|
423
|
-
#
|
|
424
|
-
#
|
|
425
|
-
# saga in this domain's own `correlates_by` head, carried through
|
|
426
|
-
# every dispatch as pure passthrough (Settlement's own comment:
|
|
427
|
-
# "`reference:` carries the correlation forward... this is pure
|
|
428
|
-
# passthrough, not an addressing key"). A command declaring none of
|
|
429
|
-
# its attributes named this is not a gap; the correlation key rides
|
|
430
|
-
# through commands that never read it, same as it does at runtime.
|
|
431
|
-
#
|
|
432
|
-
# @param process_managers [Array<Bluebook::ProcessManager>] the chapter's declared
|
|
433
|
-
# process managers
|
|
434
|
-
# @return [Array<Symbol>] the `correlation_head` of every process manager that
|
|
435
|
-
# declares a `correlates_by`
|
|
202
|
+
# A saga's `correlates_by` head is a legal `with:` key: correlation rides
|
|
203
|
+
# through commands that never read it.
|
|
436
204
|
def correlation_heads(process_managers)
|
|
437
205
|
process_managers.filter_map { |pm| pm.correlates_by && pm.correlation_head }
|
|
438
206
|
end
|
|
439
207
|
|
|
440
|
-
# Yields every command
|
|
441
|
-
#
|
|
442
|
-
# Every command this chapter declares, an aggregate's own and
|
|
443
|
-
# every entity nested inside one, paired with a name for what
|
|
444
|
-
# declares it — shared by `validate_event_shapes!` and
|
|
445
|
-
# `validate_with_projections!`'s own command lookup, the same
|
|
446
|
-
# reach `HecksagonBuilder#commands_in` needs one level up (S8).
|
|
447
|
-
#
|
|
448
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
449
|
-
# @yieldparam owner [String] the declaring aggregate's `hecks_name`, or
|
|
450
|
-
# `"Aggregate.Entity"` for a command declared on a nested entity
|
|
451
|
-
# @yieldparam command [Class<Bluebook::Command>] the command class
|
|
452
|
-
# @return [Enumerator<Array(String, Class)>] an enumerator over the same pairs
|
|
453
|
-
# when no block is given; otherwise the value of the underlying `each`, unused
|
|
454
|
-
# by any caller
|
|
208
|
+
# Yields every command the chapter declares with its owner's name
|
|
209
|
+
# ("Aggregate.Entity" for entities).
|
|
455
210
|
def each_command(aggregates)
|
|
456
211
|
return enum_for(:each_command, aggregates) unless block_given?
|
|
457
212
|
|
|
@@ -463,116 +218,41 @@ module Hecks
|
|
|
463
218
|
end
|
|
464
219
|
end
|
|
465
220
|
|
|
466
|
-
#
|
|
467
|
-
#
|
|
468
|
-
# **Not memoised** — caching this as `@event_emitters ||=` on the builder
|
|
469
|
-
# instance would be safe for a one-file chapter but wrong for one split
|
|
470
|
-
# across several: the first file's `build()` call would compute and cache
|
|
471
|
-
# it from whatever `@aggregates` held at that moment, and every later
|
|
472
|
-
# file's own validation would keep reading that same stale snapshot,
|
|
473
|
-
# silently missing any command a later file adds. Recomputed fresh every
|
|
474
|
-
# call instead — this walks the whole chapter once per `#build`, not a
|
|
475
|
-
# hot path worth memoising at that cost.
|
|
476
|
-
#
|
|
477
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
478
|
-
# @return [Hash{String => Array<Array(String, Class)>}] every event name mapped
|
|
479
|
-
# to the `[owner, command]` pairs (`each_command`'s own shape) that `emits` it;
|
|
480
|
-
# an unqueried event name answers `[]`
|
|
221
|
+
# Not memoised: a chapter split across files is validated once per file, and a
|
|
222
|
+
# cached index would miss the commands later files add.
|
|
481
223
|
def event_emitters(aggregates)
|
|
482
224
|
each_command(aggregates).with_object(Hash.new { |h, k| h[k] = [] }) do |(owner, command), index|
|
|
483
225
|
command.emits.each { |event_name| index[event_name] << [owner, command] }
|
|
484
226
|
end
|
|
485
227
|
end
|
|
486
228
|
|
|
487
|
-
# The comparable shape of
|
|
488
|
-
#
|
|
489
|
-
#
|
|
490
|
-
#
|
|
491
|
-
# field through two different, locally-scoped wrapper value
|
|
492
|
-
# objects (e.g. one aggregate's own `value: SomeText` vs
|
|
493
|
-
# another's `value: OtherText`, exactly the per-aggregate "own
|
|
494
|
-
# text VO" convention every aggregate in this grammar already
|
|
495
|
-
# follows for everything from `RuleText` to `FieldRef`) without
|
|
496
|
-
# actually disagreeing about the event's shape — comparing
|
|
497
|
-
# `a.type` by name would flag that as a violation for no real
|
|
498
|
-
# reason: an event is one fact, and two isomorphic wrapper types
|
|
499
|
-
# tell an identical one. So a value-object type is unwrapped to
|
|
500
|
-
# its own attribute shape (recursively — a wrapper could itself
|
|
501
|
-
# wrap another) before comparing, and only a primitive type
|
|
502
|
-
# (nothing left to unwrap) or two VOs that truly differ once
|
|
503
|
-
# unwrapped still counts as a real mismatch. `owner` carries the
|
|
504
|
-
# type's `value_object` lookup — a command's own attributes only
|
|
505
|
-
# know their type's name, never the aggregate that declared it,
|
|
506
|
-
# and two sibling aggregates in one chapter each keep a
|
|
507
|
-
# same-named VO private to themselves, so the unwrap has to ask
|
|
508
|
-
# the same aggregate the field's own command belongs to, never a
|
|
509
|
-
# neighbor's.
|
|
510
|
-
#
|
|
511
|
-
# @param command [Class<Bluebook::Command>] the command whose attribute shape
|
|
512
|
-
# to compute
|
|
513
|
-
# @param owner [Bluebook::Aggregate, nil] the aggregate whose `value_object`
|
|
514
|
-
# lookup resolves any wrapper type by name; `nil` when the owner could not
|
|
515
|
-
# be resolved, in which case nothing is unwrapped
|
|
516
|
-
# @return [Array<Array>] one `[name, unwrapped_type, list, optional]` tuple per
|
|
517
|
-
# attribute, sorted; `unwrapped_type` is a type name String for a primitive
|
|
518
|
-
# or an unresolved type, or a recursively-unwrapped Array of the same shape
|
|
519
|
-
# for a value object
|
|
229
|
+
# The comparable shape of a command's attributes, value objects unwrapped.
|
|
230
|
+
# Structural, not nominal: aggregates may type a field with differently named
|
|
231
|
+
# wrapper value objects without disagreeing about the event. `owner` supplies the
|
|
232
|
+
# `value_object` lookup, since same-named value objects are private to each aggregate.
|
|
520
233
|
def event_shape(command, owner)
|
|
521
234
|
command.attributes.map { |a| [a.name, unwrap_shape(owner, a.type.to_s), a.list?, a.optional?] }.sort
|
|
522
235
|
end
|
|
523
236
|
|
|
524
|
-
# Recursively unwraps a type name to a primitive or a value object's own shape.
|
|
525
|
-
#
|
|
526
|
-
# @param owner [Bluebook::Aggregate, nil] the aggregate whose `value_object`
|
|
527
|
-
# lookup resolves `type_name`; `nil` answers `type_name` unchanged
|
|
528
|
-
# @param type_name [String] the attribute type to unwrap
|
|
529
|
-
# @param seen [Array<String>] type names already unwrapped on this path, so a
|
|
530
|
-
# self-referential value object bottoms out on its own name instead of
|
|
531
|
-
# recursing forever
|
|
532
|
-
# @return [String, Array<Array>] `type_name` itself when `owner` is `nil`, when
|
|
533
|
-
# it is already a primitive, when it is already in `seen`, or when it names no
|
|
534
|
-
# value object `owner` declares; otherwise the same `[name, unwrapped_type,
|
|
535
|
-
# list, optional]` shape `event_shape` returns, one tuple per member
|
|
536
237
|
def unwrap_shape(owner, type_name, seen = [])
|
|
537
|
-
# owner couldn't be resolved, so compare by name: nothing left to unwrap
|
|
538
238
|
return type_name if owner.nil?
|
|
539
239
|
return type_name if Attribute::PRIMITIVES.include?(type_name)
|
|
540
|
-
# a self-referential
|
|
240
|
+
# a self-referential value object bottoms out on its own name
|
|
541
241
|
return type_name if seen.include?(type_name)
|
|
542
242
|
|
|
543
243
|
shape = owner.value_object(type_name)
|
|
544
|
-
# not this owner's own VO (a reference type, say): nothing further to unwrap
|
|
545
244
|
return type_name unless shape
|
|
546
245
|
|
|
547
246
|
shape.attributes.map { |a| [a.name, unwrap_shape(owner, a.type.to_s, seen + [type_name]), a.list?, a.optional?] }.sort
|
|
548
247
|
end
|
|
549
248
|
|
|
550
|
-
#
|
|
551
|
-
#
|
|
552
|
-
# `owner` (from `each_command`) is a plain string — the aggregate's
|
|
553
|
-
# `hecks_name` alone, or `"Aggregate.Entity"` for an entity's own
|
|
554
|
-
# command. Either way the value objects a command's fields can be
|
|
249
|
+
# The value objects a command's fields can be
|
|
555
250
|
# typed with are the aggregate's own (`Entity` carries no
|
|
556
|
-
# `value_object` lookup
|
|
557
|
-
# already resolves hop/type lookups only at the aggregate level,
|
|
558
|
-
# e.g. `validate_hop_tail!`'s `target.value_object(type)`), so only
|
|
559
|
-
# the first segment ever matters here.
|
|
560
|
-
#
|
|
561
|
-
# @param owner [String] an `each_command` owner string, such as `"Game"` or
|
|
562
|
-
# `"Game.Knight"`
|
|
563
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
564
|
-
# @return [Bluebook::Aggregate, nil] the aggregate whose `hecks_name` matches
|
|
565
|
-
# `owner`'s first dotted segment, or `nil` if none does
|
|
251
|
+
# `value_object` lookup), so only the owner string's first segment matters.
|
|
566
252
|
def owner_aggregate(owner, aggregates)
|
|
567
253
|
aggregates.find { |a| a.hecks_name == owner.to_s.split(".").first }
|
|
568
254
|
end
|
|
569
255
|
|
|
570
|
-
# The comparable attribute shape of whichever command first emits `event_name`.
|
|
571
|
-
#
|
|
572
|
-
# @param event_name [String, Symbol] the event name to look up
|
|
573
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
574
|
-
# @return [Array<Array>, nil] the first emitting command's `event_shape`, or `nil`
|
|
575
|
-
# if no command in the chapter emits this event
|
|
576
256
|
def event_shape_for(event_name, aggregates)
|
|
577
257
|
pairs = event_emitters(aggregates).fetch(event_name.to_s, [])
|
|
578
258
|
return nil if pairs.empty?
|
|
@@ -581,16 +261,8 @@ module Hecks
|
|
|
581
261
|
event_shape(command, owner_aggregate(owner_name, aggregates))
|
|
582
262
|
end
|
|
583
263
|
|
|
584
|
-
#
|
|
585
|
-
#
|
|
586
|
-
# (`Event#id` is the parent's), so an owner spelled "Game.Knight"
|
|
587
|
-
# answers Game's heads.
|
|
588
|
-
#
|
|
589
|
-
# @param event_name [String, Symbol] the event name to look up
|
|
590
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
591
|
-
# @return [Array<Symbol>] the emitting aggregate's own `identity_heads`, plus the
|
|
592
|
-
# emitting entity's own `identity_heads` when the event is an entity's; `[]` if
|
|
593
|
-
# no command in the chapter emits this event, or its owner cannot be resolved
|
|
264
|
+
# An entity's event is stamped with its owning aggregate's identity, so
|
|
265
|
+
# "Game.Knight" answers Game's heads.
|
|
594
266
|
def event_identity_heads_for(event_name, aggregates)
|
|
595
267
|
pairs = event_emitters(aggregates).fetch(event_name.to_s, [])
|
|
596
268
|
return [] if pairs.empty?
|
|
@@ -600,57 +272,23 @@ module Hecks
|
|
|
600
272
|
return [] unless aggregate
|
|
601
273
|
|
|
602
274
|
heads = aggregate.identity_heads.map(&:to_sym)
|
|
603
|
-
# An entity's event also carries the piece's own identity
|
|
604
|
-
#
|
|
605
|
-
# (`Emission#emit`: `payload: args`), so `id`-shaped heads are
|
|
606
|
-
# genuinely there at runtime even though no `attribute` line on
|
|
607
|
-
# the entity command declares them.
|
|
275
|
+
# An entity's event also carries the piece's own identity: the args a piece
|
|
276
|
+
# was addressed by are its payload (`Emission#emit`).
|
|
608
277
|
entity_names = owner_name.to_s.split(".").drop(1)
|
|
609
278
|
entity = entity_names.reduce(aggregate) { |owner, name| owner&.entities&.find { |e| e.hecks_name == name } }
|
|
610
279
|
heads + (entity ? entity.identity_heads.map(&:to_sym) : [])
|
|
611
280
|
end
|
|
612
281
|
|
|
613
|
-
# Every command in the chapter, keyed by its fully dotted address.
|
|
614
|
-
#
|
|
615
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
616
|
-
# @return [Hash{String => Class}] `"owner.hecks_name"` (`each_command`'s owner
|
|
617
|
-
# string joined with the command's own `hecks_name`) mapped to the command class
|
|
618
282
|
def command_lookup(aggregates)
|
|
619
283
|
each_command(aggregates).with_object({}) do |(owner, command), index|
|
|
620
284
|
index["#{owner}.#{command.hecks_name}"] = command
|
|
621
285
|
end
|
|
622
286
|
end
|
|
623
287
|
|
|
624
|
-
# A reference ring
|
|
625
|
-
#
|
|
626
|
-
#
|
|
627
|
-
#
|
|
628
|
-
# alone ; a ring back to where it started means no aggregate in
|
|
629
|
-
# it is a boundary anyone can reason about without the rest of
|
|
630
|
-
# the ring, and the whole ring is really one aggregate wearing
|
|
631
|
-
# several names.
|
|
632
|
-
#
|
|
633
|
-
# Checked at the bluebook level, not inside `AggregateBuilder`
|
|
634
|
-
# itself, because seeing a cycle needs every end declared — an
|
|
635
|
-
# aggregate finishes building long before it can know whether
|
|
636
|
-
# some later aggregate in the same file points back at it.
|
|
637
|
-
#
|
|
638
|
-
# Acyclic within a chapter (ADR 0025, "References") — catches any
|
|
639
|
-
# ring, however long (A -> B -> C -> A), not only a direct pair
|
|
640
|
-
# (A -> B -> A), the same DFS coloring a reference graph needs
|
|
641
|
-
# for any cycle. A cross-chapter reference is unreachable here
|
|
642
|
-
# rather than unchecked: `Reference#resolve` is scoped to its
|
|
643
|
-
# own chapter by construction, so a target this chapter never
|
|
644
|
-
# declares is a dangling name, not an edge — `edges.key?` below
|
|
645
|
-
# is what keeps the walk from ever leaving this chapter's own
|
|
646
|
-
# aggregates. Self-reference stays legal (`parent.parent.name`
|
|
647
|
-
# for a hierarchy is real and safe) — excluded the same way a
|
|
648
|
-
# direct pair is.
|
|
649
|
-
#
|
|
650
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
651
|
-
# @return [void]
|
|
652
|
-
# @raise [Bluebook::DSL::Malformed] if the chapter's own aggregate-to-aggregate
|
|
653
|
-
# reference graph contains a cycle
|
|
288
|
+
# A reference ring means no aggregate in it is a consistency boundary. Checked at
|
|
289
|
+
# chapter level because seeing a cycle needs every end declared (ADR 0025). Catches
|
|
290
|
+
# any ring length; self-reference stays legal, and a cross-chapter target is a
|
|
291
|
+
# dangling name, not an edge.
|
|
654
292
|
def validate_no_bidirectional_references!(aggregates)
|
|
655
293
|
edges = aggregates.to_h do |aggregate|
|
|
656
294
|
[aggregate.hecks_name, aggregate.reference_targets.uniq.reject { |target| target == aggregate.hecks_name }]
|
|
@@ -667,14 +305,8 @@ module Hecks
|
|
|
667
305
|
"through a query instead of a reference pointing back"
|
|
668
306
|
end
|
|
669
307
|
|
|
670
|
-
# Plain DFS with a visiting/done coloring
|
|
671
|
-
#
|
|
672
|
-
# itself (in the order it closes), or nil.
|
|
673
|
-
#
|
|
674
|
-
# @param edges [Hash{String => Array<String>}] each aggregate's `hecks_name`
|
|
675
|
-
# mapped to the names of the aggregates it references
|
|
676
|
-
# @return [Array<String>, nil] the aggregate names forming a cycle, in the order
|
|
677
|
-
# the walk closes it, or `nil` if the graph has none
|
|
308
|
+
# Plain DFS with a visiting/done coloring; returns the ring in the order it
|
|
309
|
+
# closes, or nil.
|
|
678
310
|
def find_reference_cycle(edges)
|
|
679
311
|
state = {}
|
|
680
312
|
|
|
@@ -686,17 +318,6 @@ module Hecks
|
|
|
686
318
|
nil
|
|
687
319
|
end
|
|
688
320
|
|
|
689
|
-
# One DFS step of `find_reference_cycle`, from `node`.
|
|
690
|
-
#
|
|
691
|
-
# @param node [String] the aggregate name to walk from
|
|
692
|
-
# @param edges [Hash{String => Array<String>}] the whole reference graph, as
|
|
693
|
-
# `find_reference_cycle` receives it
|
|
694
|
-
# @param state [Hash{String => Symbol}] each visited node's `:visiting` or
|
|
695
|
-
# `:done` color, mutated in place across the whole walk
|
|
696
|
-
# @param path [Array<String>] the nodes visited on the current path, mutated
|
|
697
|
-
# in place as the walk descends and backtracks
|
|
698
|
-
# @return [Array<String>, nil] the cycle closing back on `node`, or `nil` if
|
|
699
|
-
# no cycle is reachable from here
|
|
700
321
|
def reference_cycle_from(node, edges, state, path)
|
|
701
322
|
return nil if state[node] == :done
|
|
702
323
|
return path[path.index(node)..] if state[node] == :visiting
|
|
@@ -718,36 +339,9 @@ module Hecks
|
|
|
718
339
|
end
|
|
719
340
|
|
|
720
341
|
# Resolves and checks every `where` hop deferred at aggregate-seal time.
|
|
721
|
-
#
|
|
722
|
-
#
|
|
723
|
-
#
|
|
724
|
-
# its own references and deferred it here, unable to check
|
|
725
|
-
# further: it cannot yet resolve what the reference points at.
|
|
726
|
-
# This runs once every aggregate exists in one chapter, so it
|
|
727
|
-
# can.
|
|
728
|
-
#
|
|
729
|
-
# Only `where` clauses ever reach here — a hop on ORDER BY is
|
|
730
|
-
# refused outright, immediately, back in seal_query_field
|
|
731
|
-
# itself (that answer never needed the target's shape).
|
|
732
|
-
#
|
|
733
|
-
# An entity's own queries do reach `EntityBuilder#reference_to` —
|
|
734
|
-
# tier-1 sealing (`AggregateBuilder#query_surfaces`) already
|
|
735
|
-
# recognises a hop on an entity's own field and defers it exactly
|
|
736
|
-
# like an aggregate's — but nothing walks entity queries at tier 2
|
|
737
|
-
# to check the deferral, so a bad hop, or even a well-formed one,
|
|
738
|
-
# would otherwise build silently and then match nothing at runtime
|
|
739
|
-
# (`QueryInterpreter#entity_rows` reads an element's fields by
|
|
740
|
-
# literal hash key, never follows a reference). Refused outright
|
|
741
|
-
# here instead of taught to follow the hop for real: no corpus
|
|
742
|
-
# member needs an entity query to cross a reference yet, and a
|
|
743
|
-
# named refusal beats a runtime that resolves nothing while
|
|
744
|
-
# looking like it might.
|
|
745
|
-
#
|
|
746
|
-
# @param bluebook [Bluebook::Chapter] the fully assembled chapter to check
|
|
747
|
-
# @return [void]
|
|
748
|
-
# @raise [Bluebook::DSL::Malformed] if an aggregate's `where` hop resolves to
|
|
749
|
-
# nothing, chains too deep, or lands somewhere illegal, or if an entity's own
|
|
750
|
-
# query hops through a reference at all
|
|
342
|
+
# An entity query that hops through a reference is refused outright: nothing follows
|
|
343
|
+
# the hop at runtime (`QueryInterpreter#entity_rows` reads fields by literal key),
|
|
344
|
+
# so it would match nothing.
|
|
751
345
|
def validate_query_hops!(bluebook)
|
|
752
346
|
bluebook.aggregates.each do |aggregate|
|
|
753
347
|
aggregate.queries.each do |query|
|
|
@@ -762,19 +356,8 @@ module Hecks
|
|
|
762
356
|
end
|
|
763
357
|
end
|
|
764
358
|
|
|
765
|
-
# Mints an implicit query attribute for
|
|
766
|
-
#
|
|
767
|
-
# The chapter-wide half of AggregateBuilder's local query-argument
|
|
768
|
-
# inference. A hop cannot resolve while its aggregate is still being
|
|
769
|
-
# built; here every Reference has an owner and target, so a symbolic
|
|
770
|
-
# comparison can inherit the type of the scalar it compares without a
|
|
771
|
-
# duplicate query-local declaration.
|
|
772
|
-
#
|
|
773
|
-
# @param bluebook [Bluebook::Chapter] the fully assembled chapter to infer
|
|
774
|
-
# query arguments over
|
|
775
|
-
# @return [void]
|
|
776
|
-
# @raise [Bluebook::DSL::Malformed] if a hop's reference has no `declared_in`
|
|
777
|
-
# aggregate to resolve its target through
|
|
359
|
+
# Mints an implicit query attribute for each symbolic hop comparison, typed from
|
|
360
|
+
# the scalar it compares against.
|
|
778
361
|
def infer_hop_query_arguments!(bluebook)
|
|
779
362
|
bluebook.aggregates.each do |aggregate|
|
|
780
363
|
aggregate.queries.each do |query|
|
|
@@ -796,23 +379,6 @@ module Hecks
|
|
|
796
379
|
end
|
|
797
380
|
end
|
|
798
381
|
|
|
799
|
-
# Builds the query attribute a resolved hop plan implies for `name`.
|
|
800
|
-
#
|
|
801
|
-
# The leaf `infer_hop_query_arguments!` infers for one resolved
|
|
802
|
-
# hop plan — a pure function of `plan` and the symbolic `name`
|
|
803
|
-
# it is naming, pulled out because it is a self-contained
|
|
804
|
-
# computation with no dependency on the enclosing loop's own
|
|
805
|
-
# iteration state (it neither reads nor mutates anything about
|
|
806
|
-
# `bluebook`/`aggregate`/`query` beyond what `plan` already
|
|
807
|
-
# carries).
|
|
808
|
-
#
|
|
809
|
-
# @param name [Symbol] the symbolic comparison value the query names, and the
|
|
810
|
-
# attribute name to mint
|
|
811
|
-
# @param plan [QuerySpecification::HopPath::Plan] the resolved hop chain whose
|
|
812
|
-
# target's shape `name` is inferred against
|
|
813
|
-
# @return [Bluebook::Attribute, nil] a `String`-typed attribute when the tail
|
|
814
|
-
# lands on the target's lifecycle field; otherwise an attribute copying the
|
|
815
|
-
# resolved leaf's type and list-ness; `nil` when the tail resolves to nothing
|
|
816
382
|
def inferred_hop_leaf(name, plan)
|
|
817
383
|
target = plan.hops.last.target
|
|
818
384
|
head, *nested = plan.tail.to_s.split(".")
|
|
@@ -827,14 +393,6 @@ module Hecks
|
|
|
827
393
|
end
|
|
828
394
|
end
|
|
829
395
|
|
|
830
|
-
# Refuses any `where` clause on an entity's own query that hops through a reference.
|
|
831
|
-
#
|
|
832
|
-
# @param aggregate [Bluebook::Aggregate] the aggregate `entity` is nested under,
|
|
833
|
-
# named only in a raised message
|
|
834
|
-
# @param entity [Class<Bluebook::Entity>] the entity whose queries to check
|
|
835
|
-
# @return [void]
|
|
836
|
-
# @raise [Bluebook::DSL::Malformed] if any of `entity`'s queries has a `where`
|
|
837
|
-
# clause that hops through one of the entity's own references
|
|
838
396
|
def refuse_entity_query_hops!(aggregate, entity)
|
|
839
397
|
entity.queries.each do |query|
|
|
840
398
|
query.wheres.each do |clause|
|
|
@@ -849,27 +407,13 @@ module Hecks
|
|
|
849
407
|
end
|
|
850
408
|
end
|
|
851
409
|
|
|
852
|
-
# Resolves one `where` hop's target and checks where its tail lands.
|
|
853
|
-
#
|
|
854
|
-
# @param aggregate [Bluebook::Aggregate] the querying aggregate, named in a
|
|
855
|
-
# raised message
|
|
856
|
-
# @param query [Bluebook::Query] the query `clause` belongs to, named in a
|
|
857
|
-
# raised message
|
|
858
|
-
# @param clause [QuerySpecification::Common::WhereClause] the `where` clause whose
|
|
859
|
-
# `field` hops through one of `aggregate`'s own references
|
|
860
|
-
# @return [void]
|
|
861
|
-
# @raise [Bluebook::DSL::Malformed] if the hop's target is not declared in this
|
|
862
|
-
# chapter, the hop chain exceeds `QuerySpecification::HopPath::MAX_HOPS`, or
|
|
863
|
-
# the resolved tail fails `validate_hop_tail!`'s own checks
|
|
864
410
|
def validate_hop_clause!(aggregate, query, clause)
|
|
865
411
|
plan = QuerySpecification::HopPath.plan(clause.field, aggregate.attributes)
|
|
866
412
|
|
|
867
413
|
case plan.refusal
|
|
868
414
|
when :unresolvable
|
|
869
|
-
# HopPath.plan pushes even an unresolved hop onto `hops`
|
|
870
|
-
#
|
|
871
|
-
# real, known at declaration, independent of whether
|
|
872
|
-
# `resolve` succeeded — is always here to name.
|
|
415
|
+
# HopPath.plan pushes even an unresolved hop onto `hops`, so `target_name`
|
|
416
|
+
# is always there.
|
|
873
417
|
raise Malformed,
|
|
874
418
|
"#{aggregate.hecks_name}.#{query.hecks_name} asks about #{clause.field}, " \
|
|
875
419
|
"which hops to #{plan.hops.last.target_name}, which this chapter never " \
|
|
@@ -888,26 +432,8 @@ module Hecks
|
|
|
888
432
|
validate_hop_tail!(aggregate, query, clause, target, plan.tail)
|
|
889
433
|
end
|
|
890
434
|
|
|
891
|
-
#
|
|
892
|
-
#
|
|
893
|
-
# The same three-way answer seal_query_field gives for its own
|
|
894
|
-
# aggregate's fields — landing on a real scalar (fine), landing
|
|
895
|
-
# on a value object (refused by name), or naming nothing at all
|
|
896
|
-
# (refused by name) — asked instead of the hop's target aggregate,
|
|
897
|
-
# since that is whose shape the tail actually has to answer for.
|
|
898
|
-
#
|
|
899
|
-
# @param aggregate [Bluebook::Aggregate] the querying aggregate, named in a
|
|
900
|
-
# raised message
|
|
901
|
-
# @param query [Bluebook::Query] the query `clause` belongs to, named in a
|
|
902
|
-
# raised message
|
|
903
|
-
# @param clause [QuerySpecification::Common::WhereClause] the `where` clause the
|
|
904
|
-
# hop was resolved from, named in a raised message
|
|
905
|
-
# @param target [Bluebook::Aggregate] the aggregate the hop resolved to
|
|
906
|
-
# @param tail [String] the dotted field path remaining after the hop, such as
|
|
907
|
-
# `"status"` or `"amount.currency"`
|
|
908
|
-
# @return [void]
|
|
909
|
-
# @raise [Bluebook::DSL::Malformed] if `tail` lands on a value object rather
|
|
910
|
-
# than a scalar, or names no field `target` declares
|
|
435
|
+
# Same three-way answer as `seal_query_field` (scalar, value object, or
|
|
436
|
+
# nothing), asked of the hop's target.
|
|
911
437
|
def validate_hop_tail!(aggregate, query, clause, target, tail)
|
|
912
438
|
name, *nested = tail.to_s.split(".")
|
|
913
439
|
attribute = target.attributes.find { |candidate| candidate.name.to_s == name }
|
|
@@ -933,30 +459,8 @@ module Hecks
|
|
|
933
459
|
"not exist matches nothing and refuses nothing"
|
|
934
460
|
end
|
|
935
461
|
|
|
936
|
-
#
|
|
937
|
-
#
|
|
938
|
-
# A where hop with an ordered comparator is legitimate ("client
|
|
939
|
-
# whose balance > 500") — AggregateBuilder#seal_ordered_comparator
|
|
940
|
-
# already deferred this exact check for the same reason every
|
|
941
|
-
# other hop check is deferred, and this is where it gets asked,
|
|
942
|
-
# against the hop's target instead of the querying aggregate.
|
|
943
|
-
#
|
|
944
|
-
# @param aggregate [Bluebook::Aggregate] the querying aggregate, named in a
|
|
945
|
-
# raised message
|
|
946
|
-
# @param query [Bluebook::Query] the query `clause` belongs to, named in a
|
|
947
|
-
# raised message
|
|
948
|
-
# @param clause [QuerySpecification::Common::WhereClause] the `where` clause
|
|
949
|
-
# whose `op` and `field` are named in a raised message
|
|
950
|
-
# @param target [Bluebook::Aggregate] the aggregate the hop resolved to, named
|
|
951
|
-
# in a raised message
|
|
952
|
-
# @param attribute [Bluebook::Attribute, nil] the tail's root attribute on
|
|
953
|
-
# `target`, or `nil` when the tail lands on the lifecycle field
|
|
954
|
-
# @param nested [Array<String>] the tail's remaining dotted segments past
|
|
955
|
-
# `attribute`, `[]` for a bare field
|
|
956
|
-
# @return [void]
|
|
957
|
-
# @raise [Bluebook::DSL::Malformed] if `clause.op` is one of
|
|
958
|
-
# `AggregateBuilder::ORDERED_COMPARATORS` and the field it lands on, after
|
|
959
|
-
# `attribute`/`nested`, is not numeric
|
|
462
|
+
# An ordered comparator over a hopped field must land on a number; the check
|
|
463
|
+
# `AggregateBuilder#seal_ordered_comparator` deferred.
|
|
960
464
|
def validate_hop_comparator!(aggregate, query, clause, target, attribute, nested)
|
|
961
465
|
return unless AggregateBuilder::ORDERED_COMPARATORS.include?(clause.op.to_s.to_sym)
|
|
962
466
|
return if attribute &&
|
|
@@ -970,26 +474,9 @@ module Hecks
|
|
|
970
474
|
"anything else the adapters answer differently or not at all"
|
|
971
475
|
end
|
|
972
476
|
|
|
973
|
-
#
|
|
974
|
-
# `
|
|
975
|
-
#
|
|
976
|
-
# `reference_to` on this aggregate); this checks the reference
|
|
977
|
-
# actually resolves to a real aggregate in this chapter, and
|
|
978
|
-
# that aggregate really declares `remote_field` as a scalar.
|
|
979
|
-
#
|
|
980
|
-
# Reuses `QuerySpecification::HopPath` rather than re-deriving
|
|
981
|
-
# hop resolution a second way — `"reference/remote_field"` is
|
|
982
|
-
# the same single-hop shape a query's own `/`-spelled hop
|
|
983
|
-
# resolves, even though `projects`'s own DSL spelling is dotted
|
|
984
|
-
# (`from: :"customer.status"`): two constructs, two spellings,
|
|
985
|
-
# one resolution primitive. A single hop can never reach
|
|
986
|
-
# HopPath::MAX_HOPS, so :too_deep is structurally unreachable
|
|
987
|
-
# here and is not special-cased.
|
|
988
|
-
#
|
|
989
|
-
# @param bluebook [Bluebook::Chapter] the fully assembled chapter to check
|
|
990
|
-
# @return [void]
|
|
991
|
-
# @raise [Bluebook::DSL::Malformed] if any aggregate's `projects` field fails
|
|
992
|
-
# `validate_projected_field!`'s own checks
|
|
477
|
+
# Checks each `projects` reference resolves to an aggregate declaring
|
|
478
|
+
# `remote_field` as a scalar. A single hop never reaches HopPath::MAX_HOPS, so
|
|
479
|
+
# :too_deep is not special-cased.
|
|
993
480
|
def validate_projected_fields!(bluebook)
|
|
994
481
|
bluebook.aggregates.each do |aggregate|
|
|
995
482
|
aggregate.projected_fields.each { |field| validate_projected_field!(aggregate, field) }
|
|
@@ -997,22 +484,8 @@ module Hecks
|
|
|
997
484
|
end
|
|
998
485
|
|
|
999
486
|
# Checks that one `projects` field's reference resolves to a real scalar.
|
|
1000
|
-
#
|
|
1001
|
-
#
|
|
1002
|
-
# hop plan, each already explained by its own comment above
|
|
1003
|
-
# (the lifecycle fallback, the chained-projection fallback, the
|
|
1004
|
-
# final scalar check) — a fixed, closed sequence "resolve, then
|
|
1005
|
-
# check each way this could legitimately or illegitimately
|
|
1006
|
-
# land," not several unrelated concerns. Splitting it would
|
|
1007
|
-
# scatter `plan`/`target`/`remote_attribute` across new methods
|
|
1008
|
-
# that would each need most of them anyway.
|
|
1009
|
-
#
|
|
1010
|
-
# @param aggregate [Bluebook::Aggregate] the aggregate that declared `field`
|
|
1011
|
-
# @param field [Bluebook::ProjectedField] the declared `projects` field to check
|
|
1012
|
-
# @return [void]
|
|
1013
|
-
# @raise [Bluebook::DSL::Malformed] if `field.reference` hops to an aggregate
|
|
1014
|
-
# this chapter never declares, or `field.remote_field` names nothing on the
|
|
1015
|
-
# target, or names something other than a scalar
|
|
487
|
+
# One closed sequence over a single hop plan; splitting it scatters `plan`/`target`/
|
|
488
|
+
# `remote_attribute` across methods that each need most of them.
|
|
1016
489
|
# rubocop:disable-next Metrics/AbcSize
|
|
1017
490
|
def validate_projected_field!(aggregate, field)
|
|
1018
491
|
plan = QuerySpecification::HopPath.plan("#{field.reference}/#{field.remote_field}", aggregate.attributes)
|
|
@@ -1027,30 +500,13 @@ module Hecks
|
|
|
1027
500
|
target = plan.hops.last.target
|
|
1028
501
|
remote_attribute = target.attributes.find { |candidate| candidate.name.to_s == plan.tail }
|
|
1029
502
|
|
|
1030
|
-
#
|
|
1031
|
-
#
|
|
1032
|
-
# :status`, never a plain `attribute` — the same fallback
|
|
1033
|
-
# validate_hop_tail! already gives a query's own hop tail. A
|
|
1034
|
-
# lifecycle field is always a plain string by construction ;
|
|
1035
|
-
# nothing further to check once it matches by name.
|
|
503
|
+
# A lifecycle field is a plain string by construction, so a name match is enough (as in
|
|
504
|
+
# `validate_hop_tail!`).
|
|
1036
505
|
return if remote_attribute.nil? && target.lifecycle&.field.to_s == plan.tail
|
|
1037
506
|
|
|
1038
|
-
# A projection may chain through another projection
|
|
1039
|
-
#
|
|
1040
|
-
#
|
|
1041
|
-
# separate list from `attributes`, so a match there is
|
|
1042
|
-
# invisible to the check above even though it names a real,
|
|
1043
|
-
# always-current, stored field. `Transfer.projects
|
|
1044
|
-
# :source_customer_status, from: :"source.customer_status"`
|
|
1045
|
-
# reads Account's own already-projected `customer_status`
|
|
1046
|
-
# this way — Account is one hop from Customer, Transfer is
|
|
1047
|
-
# one hop from Account, and neither aggregate needs to know
|
|
1048
|
-
# about the other's target two hops away. A projected
|
|
1049
|
-
# field's remote value is always a scalar by construction
|
|
1050
|
-
# (`RebuildSweep.remote_value` never copies a reference, a
|
|
1051
|
-
# value object, or a list), so nothing further to check once
|
|
1052
|
-
# it matches by name — same reasoning the lifecycle
|
|
1053
|
-
# fallback just above already applies.
|
|
507
|
+
# A projection may chain through another projection; `projected_fields` is
|
|
508
|
+
# separate from `attributes`, and a projected value is always a scalar by
|
|
509
|
+
# construction, so a name match is enough.
|
|
1054
510
|
return if remote_attribute.nil? && target.projected_fields.any? { |f| f.name.to_s == plan.tail }
|
|
1055
511
|
|
|
1056
512
|
unless remote_attribute
|
|
@@ -1067,45 +523,14 @@ module Hecks
|
|
|
1067
523
|
"value, never a reference, a value object, or a list"
|
|
1068
524
|
end
|
|
1069
525
|
|
|
1070
|
-
# Decides whether a declared attribute is a plain, single scalar value.
|
|
1071
|
-
#
|
|
1072
|
-
# @param target [Bluebook::Aggregate] the aggregate `attribute` is declared on,
|
|
1073
|
-
# whose `value_object` lookup resolves `attribute`'s type
|
|
1074
|
-
# @param attribute [Bluebook::Attribute] the attribute to check
|
|
1075
|
-
# @return [Boolean] `true` when `attribute` is not a list, not a reference, and
|
|
1076
|
-
# its type names no value object `target` declares
|
|
1077
526
|
def projectable_scalar?(target, attribute)
|
|
1078
527
|
!attribute.list? && !attribute.reference? && target.value_object(attribute.type).nil?
|
|
1079
528
|
end
|
|
1080
529
|
|
|
1081
|
-
# `correlates_by`
|
|
1082
|
-
#
|
|
1083
|
-
#
|
|
1084
|
-
#
|
|
1085
|
-
# the question open. It cannot go further: a process manager is built
|
|
1086
|
-
# in isolation, before this chapter's aggregates exist to check
|
|
1087
|
-
# against. Here, with the whole document assembled, the dotted path
|
|
1088
|
-
# is walked for real — against whichever command actually emits an
|
|
1089
|
-
# event this process manager reacts to — so a path that still lands
|
|
1090
|
-
# on a value object is refused before the runtime ever has to decide
|
|
1091
|
-
# what a non-scalar correlation key even means: a saga keys off this
|
|
1092
|
-
# value directly, and a value object carries no guaranteed-stable
|
|
1093
|
-
# identity to key on the way a scalar does.
|
|
1094
|
-
#
|
|
1095
|
-
# A command that does not declare the path's first segment at all is
|
|
1096
|
-
# silently skipped, not refused — correlation has two other fallback
|
|
1097
|
-
# tiers below the payload dig (a correlation stamp, then the emitting
|
|
1098
|
-
# aggregate's own reference key; saga_interpreter/correlation.rb), so
|
|
1099
|
-
# an absent field is not this check's business. Only a field that
|
|
1100
|
-
# resolves, and resolves to something other than a scalar, is.
|
|
1101
|
-
#
|
|
1102
|
-
# @param process_managers [Array<Bluebook::ProcessManager>] the chapter's
|
|
1103
|
-
# declared process managers
|
|
1104
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
1105
|
-
# @return [void]
|
|
1106
|
-
# @raise [Bluebook::DSL::ProcessManagerBuilder::InvalidProcessManager] if a
|
|
1107
|
-
# process manager's `correlates_by` path resolves, on any command that emits
|
|
1108
|
-
# an event it reacts to, to something other than a scalar
|
|
530
|
+
# `correlates_by` must land on a scalar: the dotted path is walked against each
|
|
531
|
+
# command that emits an event the process manager reacts to. A command lacking the
|
|
532
|
+
# first segment is skipped, since correlation has fallback tiers
|
|
533
|
+
# (saga_interpreter/correlation.rb).
|
|
1109
534
|
def validate_correlation_keys!(process_managers, aggregates)
|
|
1110
535
|
process_managers.each do |pm|
|
|
1111
536
|
next unless pm.correlates_by
|
|
@@ -1118,15 +543,6 @@ module Hecks
|
|
|
1118
543
|
end
|
|
1119
544
|
end
|
|
1120
545
|
|
|
1121
|
-
# Finds why a process manager's `correlates_by` path is not a scalar, if it isn't.
|
|
1122
|
-
#
|
|
1123
|
-
# @param process_manager [Bluebook::ProcessManager] the process manager whose
|
|
1124
|
-
# `correlates_by` to check
|
|
1125
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
1126
|
-
# @return [String, nil] the reason the path is not a scalar, on the first
|
|
1127
|
-
# emitting command whose first segment resolves; `nil` if every command that
|
|
1128
|
-
# emits a reacted-to event either omits the first segment, or resolves to
|
|
1129
|
-
# a scalar
|
|
1130
546
|
def correlation_key_violation(process_manager, aggregates)
|
|
1131
547
|
head, *rest = process_manager.correlates_by.to_s.split(".")
|
|
1132
548
|
events = reacted_events(process_manager)
|
|
@@ -1142,12 +558,6 @@ module Hecks
|
|
|
1142
558
|
nil
|
|
1143
559
|
end
|
|
1144
560
|
|
|
1145
|
-
# The demodulised names of every event a process manager reacts to.
|
|
1146
|
-
#
|
|
1147
|
-
# @param process_manager [Bluebook::ProcessManager] the process manager whose
|
|
1148
|
-
# `starts_on`, `ends_on` and handler event types to collect
|
|
1149
|
-
# @return [Array<String>] the unique demodulised event names, `REFUSED` (the
|
|
1150
|
-
# compensation trigger, not a real event) excluded
|
|
1151
561
|
def reacted_events(process_manager)
|
|
1152
562
|
([process_manager.starts_on, process_manager.ends_on] + process_manager.handlers.map(&:event_type))
|
|
1153
563
|
.compact
|
|
@@ -1156,13 +566,6 @@ module Hecks
|
|
|
1156
566
|
.uniq
|
|
1157
567
|
end
|
|
1158
568
|
|
|
1159
|
-
# Every command across the chapter that emits one of the given events.
|
|
1160
|
-
#
|
|
1161
|
-
# @param events [Array<String>] the demodulised event names to match against
|
|
1162
|
-
# each command's own `emits`
|
|
1163
|
-
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
1164
|
-
# @return [Array<Array(Bluebook::Aggregate, Class)>] one `[aggregate, command]`
|
|
1165
|
-
# pair per matching command, an aggregate's own or one of its entities'
|
|
1166
569
|
def emitting_commands(events, aggregates)
|
|
1167
570
|
aggregates.flat_map do |aggregate|
|
|
1168
571
|
commands = aggregate.commands + aggregate.entities.flat_map(&:commands)
|
|
@@ -1171,16 +574,6 @@ module Hecks
|
|
|
1171
574
|
end
|
|
1172
575
|
end
|
|
1173
576
|
|
|
1174
|
-
# Decides whether one command attribute can serve as a correlation key.
|
|
1175
|
-
#
|
|
1176
|
-
# @param owner [Bluebook::Aggregate] the aggregate whose `value_object` lookup
|
|
1177
|
-
# resolves `attribute`'s type, for `walk_scalar`
|
|
1178
|
-
# @param attribute [Bluebook::Attribute] the command attribute the correlation
|
|
1179
|
-
# path's first segment named
|
|
1180
|
-
# @param segments [Array<String>] the path's remaining dotted segments, `[]` for
|
|
1181
|
-
# a bare field
|
|
1182
|
-
# @return [String, nil] why `attribute` cannot serve as a correlation key, or
|
|
1183
|
-
# `nil` if it can — see `walk_scalar` for what a non-`nil` reason means
|
|
1184
577
|
def list_or_scalar_violation(owner, attribute, segments)
|
|
1185
578
|
if attribute.list?
|
|
1186
579
|
return "#{attribute.name} is a list — a correlation key must name one instance's own field, " \
|
|
@@ -1190,23 +583,8 @@ module Hecks
|
|
|
1190
583
|
walk_scalar(owner, attribute.type.to_s, segments)
|
|
1191
584
|
end
|
|
1192
585
|
|
|
1193
|
-
# Walks the remaining dotted segments through nested value objects
|
|
1194
|
-
#
|
|
1195
|
-
# step either bottoms out at a real scalar (nil — no violation) or
|
|
1196
|
-
# names why it cannot: still a value object with no more path left,
|
|
1197
|
-
# a value object this domain never declared, a field that value
|
|
1198
|
-
# object does not have, or a segment left over after already
|
|
1199
|
-
# reaching a scalar.
|
|
1200
|
-
#
|
|
1201
|
-
# @param owner [Bluebook::Aggregate] the aggregate whose `value_object` lookup
|
|
1202
|
-
# resolves each stepped-into type name
|
|
1203
|
-
# @param type_name [String] the type to check or step into
|
|
1204
|
-
# @param segments [Array<String>] the dotted path segments remaining to walk
|
|
1205
|
-
# @return [String, nil] `nil` when the walk bottoms out cleanly on a scalar;
|
|
1206
|
-
# otherwise why it cannot: `type_name` is a value object with no segments
|
|
1207
|
-
# left, `type_name` is already a scalar with segments left over, `type_name`
|
|
1208
|
-
# names no value object `owner` declares, the next segment names no field of
|
|
1209
|
-
# that value object, or that field is a list
|
|
586
|
+
# Walks the remaining dotted segments through nested value objects; nil means the walk
|
|
587
|
+
# bottomed out on a scalar, otherwise the string says why it cannot.
|
|
1210
588
|
def walk_scalar(owner, type_name, segments)
|
|
1211
589
|
if segments.empty?
|
|
1212
590
|
return nil if Attribute::PRIMITIVES.include?(type_name)
|