hecks 2.7.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 +11 -0
- 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 +18 -99
- 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 +15 -37
- 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 +32 -142
- 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 +17 -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 +38 -26
- 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 +17 -65
- 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 -189
- 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 -106
- 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 +54 -268
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -308
- 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 +10 -91
- data/lib/hecks/bluebook/model_check.rb +102 -600
- 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 +84 -0
- data/lib/hecks/cli/document.rb +86 -0
- data/lib/hecks/cli/ir.rb +45 -0
- data/lib/hecks/cli/mcp.rb +31 -0
- data/lib/hecks/cli/mcp_door.rb +366 -0
- data/lib/hecks/cli/model_check.rb +203 -0
- data/lib/hecks/cli/project_cli.rb +90 -0
- data/lib/hecks/cli/project_diagrams.rb +36 -0
- data/lib/hecks/cli/run.rb +173 -0
- data/lib/hecks/cli/smoke_test.rb +45 -0
- data/lib/hecks/cli/stores.rb +53 -0
- data/lib/hecks/cli.rb +160 -0
- 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 +98 -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 +95 -0
- 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 +62 -65
- 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 +7 -26
- 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 +22 -122
- 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 +151 -679
- 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 +9 -16
- 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 +46 -138
- 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 +93 -0
- data/lib/hecks/release/runner/commands.rb +66 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +43 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +75 -0
- data/lib/hecks/release/runner/preflight.rb +104 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +80 -0
- data/lib/hecks/release/runner.rb +215 -0
- 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 +125 -307
- 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 +65 -220
- 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 +139 -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 +103 -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 +146 -567
- 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/vocabulary.rb +5 -0
- data/lib/hecks.rb +32 -82
- 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 +30 -59
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
data/lib/hecks/fuzzing/replay.rb
DELETED
|
@@ -1,930 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
require "tmpdir"
|
|
3
|
-
require_relative "isolated_boot"
|
|
4
|
-
require_relative "self_consistency"
|
|
5
|
-
require_relative "../query_specification/common/comparators"
|
|
6
|
-
require_relative "../query_specification/common/where_clause"
|
|
7
|
-
require_relative "../query_specification/field_path"
|
|
8
|
-
require_relative "../ports/query/in_memory"
|
|
9
|
-
|
|
10
|
-
module Hecks
|
|
11
|
-
module Fuzzing
|
|
12
|
-
# A step list, replayed in-process against a fresh boot — the same
|
|
13
|
-
# copy-to-tmp-and-reset preamble SequenceGenerator#call uses, and the
|
|
14
|
-
# same observable surface bin/run prints (instances, events,
|
|
15
|
-
# refusals, reactions, sagas, queries), but returned as data rather
|
|
16
|
-
# than JSON on stdout.
|
|
17
|
-
#
|
|
18
|
-
# Not what SequenceGenerator itself dispatches through while
|
|
19
|
-
# generating — that inline execution feeds the picker's own
|
|
20
|
-
# known_ids tracking and stays exactly as it is. This exists for
|
|
21
|
-
# everything else that needs "given a step list, boot fresh and tell
|
|
22
|
-
# me what happened": bin/fuzz recomputing a shrink candidate's true
|
|
23
|
-
# event count (removing a step changes what the sequence actually
|
|
24
|
-
# produces, so a shrunk candidate cannot reuse the original claim),
|
|
25
|
-
# and the declared-property checks in properties.rb. Both want it
|
|
26
|
-
# in-process — a property or a shrink-candidate check is a question
|
|
27
|
-
# this boot can answer itself, and paying for a subprocess and a
|
|
28
|
-
# second boot to ask it would be ~100x the cost for nothing.
|
|
29
|
-
#
|
|
30
|
-
# Refuses the same way SequenceGenerator's own safe_call does: a
|
|
31
|
-
# DOMAIN_REFUSAL or an EvaluationError is the domain declining a
|
|
32
|
-
# step, recorded and not fatal to the replay. Anything else
|
|
33
|
-
# propagates — a step that breaks the interpreter is a defect, not
|
|
34
|
-
# an observation to fold quietly into the history.
|
|
35
|
-
module Replay
|
|
36
|
-
module_function
|
|
37
|
-
|
|
38
|
-
# The ad hoc filter's own comparator roster — read directly from
|
|
39
|
-
# QuerySpecification::Common::COMPARATORS (the same nine names
|
|
40
|
-
# Vocabulary::QueryComparator declares), never re-typed. A
|
|
41
|
-
# declared bluebook query never sees an `op:` outside this set —
|
|
42
|
-
# `admits: "Vocabulary::QueryComparator"` refuses one at declare
|
|
43
|
-
# time — but a `"filter"`-shaped query step (below) has no
|
|
44
|
-
# declare-time gate at all, so this method gates it here instead.
|
|
45
|
-
#
|
|
46
|
-
# Not rust/src/kernel/query_comparators.rs's own ground truth —
|
|
47
|
-
# that hand-maintained Rust enum is missing `none_in_state` (the
|
|
48
|
-
# 9th comparator, added after the enum was written) and has
|
|
49
|
-
# already drifted; do not treat it as authoritative until item #9
|
|
50
|
-
# of the whole-project table-unification survey closes that gap.
|
|
51
|
-
FILTER_COMPARATORS = Hecks::QuerySpecification::Common::COMPARATORS.map(&:to_s).freeze
|
|
52
|
-
|
|
53
|
-
# The two classes `#enforce_givens`/`#enforce_lifecycle_guard`
|
|
54
|
-
# themselves ever raise — see Admissibility's own doc comment,
|
|
55
|
-
# `command_rules/admissibility.rb`. Any other DOMAIN_REFUSAL a step
|
|
56
|
-
# raises (TypeMismatch, EnsuresNotMet, InvariantViolation, ...)
|
|
57
|
-
# proves the guard itself did not fire, since it runs first in
|
|
58
|
-
# DISPATCH_ORDER.
|
|
59
|
-
GUARD_REFUSAL_CLASSES = [Runtime::GivenNotMet, Runtime::LifecycleRefused].freeze
|
|
60
|
-
|
|
61
|
-
# One tightly ordered loop over steps, with several "oracle" snapshots
|
|
62
|
-
# (reaction_mark, fan_out_snapshot, guard_check, mutation_trace) that
|
|
63
|
-
# must be taken at very specific points relative to dispatch — see
|
|
64
|
-
# fan_out_snapshot's own comment above for the real bug this exact
|
|
65
|
-
# before/after ordering fixes. Splitting this into smaller methods
|
|
66
|
-
# would mean threading five-plus oracle-state locals across method
|
|
67
|
-
# boundaries as parameters/return values, and
|
|
68
|
-
# would let a future editor silently reorder a snapshot relative to
|
|
69
|
-
# `dispatch` without any single method looking wrong — the ordering
|
|
70
|
-
# invariant is only visible with the whole sequence in one place.
|
|
71
|
-
# rubocop:disable-next Metrics/AbcSize
|
|
72
|
-
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
73
|
-
# rubocop:disable-next Metrics/MethodLength
|
|
74
|
-
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
75
|
-
# `self_consistency:` — off by default, same "existing callers see no
|
|
76
|
-
# change" contract `adapter:` already has. `bin/qa_sweep` is the one
|
|
77
|
-
# real caller that opts in (gated by `QualityControlDials::
|
|
78
|
-
# SELF_CONSISTENCY_CHECKS`/`--self-consistency`): `bin/fuzz`,
|
|
79
|
-
# `Properties.check`'s own callers, and every existing spec keep
|
|
80
|
-
# calling this with no second axis of comparison at all, exactly as
|
|
81
|
-
# before. Computed here, not by a caller reading `runtime` back out
|
|
82
|
-
# afterward — `runtime` and the whole `IsolatedBoot` tmp directory
|
|
83
|
-
# go out of scope the moment this method returns (see this file's
|
|
84
|
-
# own header), so `Hecks::Fuzzing::SelfConsistency.check` has to run
|
|
85
|
-
# while both are still alive, against the exact same repositories
|
|
86
|
-
# this replay's own dispatch loop just wrote to.
|
|
87
|
-
#
|
|
88
|
-
# `database:`/`schema:` — only meaningful, and required, for
|
|
89
|
-
# `adapter: :postgres_era` — see `IsolatedBoot#rebind_to_postgres_era!`'s
|
|
90
|
-
# own header for why that one mode takes caller-owned connection
|
|
91
|
-
# identity rather than a shared default the way `:postgres` does.
|
|
92
|
-
# Forwarded straight through, unchanged, exactly like `adapter:`
|
|
93
|
-
# itself already was.
|
|
94
|
-
#
|
|
95
|
-
# @param domain_path [String] filesystem path to the domain directory to replay
|
|
96
|
-
# @param steps [Array<Hash>] the step list to dispatch, any-keyed (normalized to
|
|
97
|
-
# String keys internally)
|
|
98
|
-
# @param adapter [Symbol] persistence adapter to boot the copy with
|
|
99
|
-
# @param database [String, nil] PostgresEra database name; required, only meaningful
|
|
100
|
-
# when `adapter: :postgres_era`
|
|
101
|
-
# @param schema [String, nil] PostgresEra schema name; required, only meaningful
|
|
102
|
-
# when `adapter: :postgres_era`
|
|
103
|
-
# @param self_consistency [Boolean] whether to run `SelfConsistency.check` against
|
|
104
|
-
# this replay's own runtime before returning
|
|
105
|
-
# @return [Hash] the replay history: `:instances`, `:events`, `:refusals`,
|
|
106
|
-
# `:reactions`, `:sagas`, `:saga_instances`, `:queries`, `:dry_runs`,
|
|
107
|
-
# `:dry_run_traces`, `:fan_outs`, `:guard_checks`, `:mutation_traces`,
|
|
108
|
-
# `:outbox_traces`, `:saga_dispatches`, `:policy_dispatches`, `:bluebook`,
|
|
109
|
-
# `:bluebooks`, and (when `self_consistency:` is true) `:self_consistency`
|
|
110
|
-
def call(domain_path, steps, adapter: :memory, database: nil, schema: nil, self_consistency: false)
|
|
111
|
-
# See isolated_boot.rb's own header: resets data/ and rebinds
|
|
112
|
-
# persistence to the chosen adapter (Memory by default), since a
|
|
113
|
-
# Postgres-bound domain's real store lives outside the copied
|
|
114
|
-
# directory and cannot be reached by resetting data/ alone.
|
|
115
|
-
IsolatedBoot.call(domain_path, adapter: adapter, database: database, schema: schema) do |copy|
|
|
116
|
-
runtime = Hecks.boot(copy)
|
|
117
|
-
|
|
118
|
-
refusals = []
|
|
119
|
-
queries = []
|
|
120
|
-
dry_runs = []
|
|
121
|
-
dry_run_traces = []
|
|
122
|
-
fan_outs = []
|
|
123
|
-
guard_checks = []
|
|
124
|
-
mutation_traces = []
|
|
125
|
-
outbox_traces = []
|
|
126
|
-
|
|
127
|
-
# Every aggregate a `for_each` could ever query, resolved once —
|
|
128
|
-
# `[domain, aggregate_name]` pairs, gleaned from every loaded
|
|
129
|
-
# bluebook's own fanning-out policies. Empty for every domain
|
|
130
|
-
# with no `for_each` at all (every example this corpus ships
|
|
131
|
-
# today), so the snapshot below costs nothing until a domain
|
|
132
|
-
# actually declares one.
|
|
133
|
-
fan_out_targets = runtime.registry.bluebooks.each_with_object({}) do |(domain, bluebook), targets|
|
|
134
|
-
bluebook.policies.select(&:fans_out?).each do |policy|
|
|
135
|
-
query_domain, aggregate_name, = policy.for_each_route(domain)
|
|
136
|
-
targets[[query_domain, aggregate_name]] ||= runtime.registry.bluebook(query_domain)&.aggregate(aggregate_name)
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
steps.each do |step|
|
|
141
|
-
step = step.transform_keys(&:to_s)
|
|
142
|
-
args = (step["args"] || {}).transform_keys(&:to_sym)
|
|
143
|
-
|
|
144
|
-
if (question = step["query"])
|
|
145
|
-
# **The ad hoc, single-comparator filter** — a "query" step whose
|
|
146
|
-
# own value is a Hash, not a name: `{aggregate:, field:, op:,
|
|
147
|
-
# value:}`, the same wire shape kernel/cli.rs's new object-
|
|
148
|
-
# form "query" step reads on the Rust side (that file's own
|
|
149
|
-
# header explains why this shape exists at all: it bypasses
|
|
150
|
-
# the bluebook query DSL entirely, so it needs no generated
|
|
151
|
-
# per-domain codegen to prove for real). Answered here by
|
|
152
|
-
# calling `Ports::Query::InMemory` directly — the real
|
|
153
|
-
# production comparator engine, not a second, hand-rewritten
|
|
154
|
-
# copy of it — against the raw repository, never through
|
|
155
|
-
# `runtime.query`, which only ever resolves a named, declared
|
|
156
|
-
# ask.
|
|
157
|
-
if question.is_a?(Hash)
|
|
158
|
-
begin
|
|
159
|
-
rows = run_filter(runtime, question)
|
|
160
|
-
queries << { query: question, rows: rows, instances_at: snapshot_instances(runtime) }
|
|
161
|
-
rescue StandardError => e
|
|
162
|
-
refusals << { verb: filter_label(question), error: e.message, kind: refusal_kind(e) }
|
|
163
|
-
end
|
|
164
|
-
next
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# **The query oracle** — two independent engines, each run and
|
|
168
|
-
# caught on its own, never a single shared `begin`/`rescue`
|
|
169
|
-
# wrapping both calls. A shared begin/rescue meant `runtime.
|
|
170
|
-
# query` raising (native refuses) short-circuited before
|
|
171
|
-
# `runtime.reference_query` ever ran at all — the entry
|
|
172
|
-
# recorded only `error:`, with no `reference_rows` and no
|
|
173
|
-
# record of what the reference interpreter would have
|
|
174
|
-
# answered — and `runtime.reference_query` raising instead
|
|
175
|
-
# (reference refuses, native already succeeded) landed in the
|
|
176
|
-
# same rescue, discarding the native `rows` this begin block
|
|
177
|
-
# had already computed and recording the whole ask as an
|
|
178
|
-
# ordinary refusal. Either way, "one engine refused and the
|
|
179
|
-
# other did not" — a real divergence, exactly the shape a
|
|
180
|
-
# differential oracle exists to catch — read as "refused,
|
|
181
|
-
# nothing to compare" and vanished. Each engine's own
|
|
182
|
-
# success/failure is captured here independently instead, so
|
|
183
|
-
# `Properties.query_answers_match_reference` can tell "both
|
|
184
|
-
# refused" (fine) apart from "one refused, the other did not"
|
|
185
|
-
# (a finding) rather than having every refusal-shaped run
|
|
186
|
-
# skipped uniformly.
|
|
187
|
-
native_rows = native_error = nil
|
|
188
|
-
begin
|
|
189
|
-
native_rows = runtime.query(question, **args)
|
|
190
|
-
rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
|
|
191
|
-
native_error = e
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# Read-model asks (bare domain form, no "::") have no
|
|
195
|
-
# reference twin at all — never attempted, not "attempted and
|
|
196
|
-
# agreed."
|
|
197
|
-
has_reference = question.include?("::")
|
|
198
|
-
reference_rows = reference_error = nil
|
|
199
|
-
if has_reference
|
|
200
|
-
begin
|
|
201
|
-
reference_rows = runtime.reference_query(question, **args)
|
|
202
|
-
rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
|
|
203
|
-
reference_error = e
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
entry = { query: question, args: args, rows: native_rows, instances_at: snapshot_instances(runtime) }
|
|
208
|
-
entry[:error] = native_error.message if native_error
|
|
209
|
-
if has_reference
|
|
210
|
-
entry[:reference_rows] = reference_rows
|
|
211
|
-
entry[:reference_error] = reference_error.message if reference_error
|
|
212
|
-
end
|
|
213
|
-
queries << entry
|
|
214
|
-
|
|
215
|
-
refusals << { verb: question, error: native_error.message, kind: refusal_kind(native_error) } if native_error
|
|
216
|
-
next
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
# `{"dry_run": verb, "args": …}` — `Dispatcher#dry_run?`: the command
|
|
220
|
-
# evaluated hypothetically, nothing saved or emitted, no reaction.
|
|
221
|
-
# Recorded, never a refusal: a refused dry run is an answer.
|
|
222
|
-
#
|
|
223
|
-
# `dry_runs` stays exactly `{verb:, ok:, error?:}` — the same
|
|
224
|
-
# shape it always had, and the same shape `kernel/cli.rs`'s own
|
|
225
|
-
# `dry_run` answers (`{"verb", "ok"}` or `{"verb", "ok": false,
|
|
226
|
-
# "error"}`, that function's own doc comment) — `spec/rust_
|
|
227
|
-
# conformance_spec.rb` compares this array against the compiled
|
|
228
|
-
# binary's own verbatim, so it can never carry a key Rust's own
|
|
229
|
-
# answer does not. The role-gated binding (`as_step_caller`,
|
|
230
|
-
# the same `role:`/`actor_id:` a real dispatch gets below) still
|
|
231
|
-
# applies to the dry-run call itself — only what gets recorded
|
|
232
|
-
# about it is unchanged.
|
|
233
|
-
#
|
|
234
|
-
# `dry_run_traces` — a separate, parallel array (same order,
|
|
235
|
-
# not merged into `dry_runs` above) carrying `before:`/`after:`
|
|
236
|
-
# snapshots of the whole observable store (every instance, the
|
|
237
|
-
# event count) on either side of the hypothetical call, so
|
|
238
|
-
# `Properties.dry_runs_leave_no_trace` can hold `Dispatcher
|
|
239
|
-
# #dry_run?`'s own contract to the store rather than trusting
|
|
240
|
-
# it. Ruby's own oracle data — Rust has nothing to compare it
|
|
241
|
-
# against, so it stays out of the compared surface entirely.
|
|
242
|
-
if (hypothetical = step["dry_run"])
|
|
243
|
-
before = { instances: snapshot_instances(runtime), events: runtime.events.size }
|
|
244
|
-
entry = { verb: hypothetical }
|
|
245
|
-
begin
|
|
246
|
-
as_step_caller(step) { runtime.dry_run?(hypothetical, **args) }
|
|
247
|
-
entry[:ok] = true
|
|
248
|
-
rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
|
|
249
|
-
entry.merge!(ok: false, error: e.message)
|
|
250
|
-
end
|
|
251
|
-
after = { instances: snapshot_instances(runtime), events: runtime.events.size }
|
|
252
|
-
dry_runs << entry
|
|
253
|
-
dry_run_traces << entry.merge(before: before, after: after)
|
|
254
|
-
next
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
begin
|
|
258
|
-
# **The fan-out oracle's own low-water mark** — taken before
|
|
259
|
-
# dispatch, so any reaction this one step's own announced
|
|
260
|
-
# events produce (`reaction_log` grows in place, the same
|
|
261
|
-
# Array `runtime.reactions` already exposes) can be sliced
|
|
262
|
-
# out after, and matched against an independent recomputation
|
|
263
|
-
# of what a `for_each` policy should have fanned out over —
|
|
264
|
-
# the query oracle's own shape (two engines, compared, never
|
|
265
|
-
# one graded against itself), aimed at fan-out instead of a
|
|
266
|
-
# named ask.
|
|
267
|
-
reaction_mark = runtime.reactions.size
|
|
268
|
-
|
|
269
|
-
# **The outbox oracle's own low-water marks** — taken before
|
|
270
|
-
# dispatch, same idiom as `reaction_mark` right above:
|
|
271
|
-
# `saga_log_mark` slices `runtime.sagas` (a single flat
|
|
272
|
-
# array, safe to index into directly) the identical way
|
|
273
|
-
# `reaction_mark` already slices `runtime.reactions`.
|
|
274
|
-
# `outbox_before_ids` is a set of delivery_ids, not a
|
|
275
|
-
# size — `runtime.outbox.rows` concatenates every bound
|
|
276
|
-
# repository's own array in a fixed per-store order
|
|
277
|
-
# (`Outbox::Relay#rows`, `stores.flat_map`), so a row a
|
|
278
|
-
# different step's dispatch enqueues into an
|
|
279
|
-
# earlier-iterated store would land in the middle of
|
|
280
|
-
# that concatenated list, not at its tail — a plain
|
|
281
|
-
# "grew from N to M, take the tail" slice (the shape
|
|
282
|
-
# `reaction_mark`/`saga_log_mark` both get away with,
|
|
283
|
-
# since `reaction_log`/`saga_log` are each already one
|
|
284
|
-
# flat array irrespective of domain) would silently miss
|
|
285
|
-
# or misattribute rows the moment more than one
|
|
286
|
-
# repository has an outbox. `delivery_id` is unique per
|
|
287
|
-
# store by construction (`Row#to_h`'s own header;
|
|
288
|
-
# `hecks_outbox`'s `UNIQUE` column in the Postgres/Sqlite
|
|
289
|
-
# DDL, `outbox_enqueue`'s own de-dup check in Memory), so
|
|
290
|
-
# membership in this set is exactly "existed before this
|
|
291
|
-
# step's own dispatch ran."
|
|
292
|
-
saga_log_mark = runtime.sagas.size
|
|
293
|
-
outbox_before_ids = runtime.outbox.rows.map(&:delivery_id)
|
|
294
|
-
|
|
295
|
-
# The snapshot a `for_each` query would have seen — taken
|
|
296
|
-
# before this step's own dispatch, not after. The real
|
|
297
|
-
# `deliver_for_each` runs its query synchronously, inside
|
|
298
|
-
# this same dispatch, before this call even returns — so an
|
|
299
|
-
# oracle that re-reads the live repository after `dispatch`
|
|
300
|
-
# answers sees whatever the fan-out's own dispatched
|
|
301
|
-
# commands already mutated (an Account a `Review` leg just
|
|
302
|
-
# moved out of "open," say), not what the query actually
|
|
303
|
-
# matched. A measured bug, not a hypothetical one — this
|
|
304
|
-
# exact ordering is what `spec/fuzzing/fan_out_spec.rb`'s
|
|
305
|
-
# own adversarial run against a live `for_each` policy
|
|
306
|
-
# caught the first time this shipped without it.
|
|
307
|
-
fan_out_snapshot = fan_out_targets.each_with_object({}) do |((fdomain, faggregate_name), aggregate), snap|
|
|
308
|
-
next unless aggregate
|
|
309
|
-
|
|
310
|
-
snap[[fdomain, faggregate_name]] =
|
|
311
|
-
runtime.registry.repository(fdomain, aggregate).all.to_h { |record| [record.id, record.state.dup] }
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
# **The guard oracle's own pre-dispatch read** — same idiom,
|
|
315
|
-
# same placement, same reason as fan_out_snapshot right
|
|
316
|
-
# above: `Admissibility#enforce_givens` (which itself calls
|
|
317
|
-
# `#enforce_lifecycle_guard` when `declaring:` is passed)
|
|
318
|
-
# is called a second time here, independently, against the
|
|
319
|
-
# record exactly as CommandInterpreter#hydrate's own acting
|
|
320
|
-
# branch would find it (`repository.find(id).dup` — the
|
|
321
|
-
# identical three-tier id fallback, Identity.of/.from,
|
|
322
|
-
# reproduced read-only) — before this step's real dispatch
|
|
323
|
-
# can mutate anything a cross-aggregate given dereferences
|
|
324
|
-
# (`customer.status`). A pure predicate read, side-effect
|
|
325
|
-
# free, so calling it twice changes nothing this step
|
|
326
|
-
# itself observes.
|
|
327
|
-
guard_check = build_guard_check(runtime, step["verb"], args)
|
|
328
|
-
|
|
329
|
-
# **The mutation oracle's own pre-dispatch read** — same
|
|
330
|
-
# idiom again: an entity-dispatched command's own
|
|
331
|
-
# `append`/`remove`/`multiply`/`clamp` mutations (S17's
|
|
332
|
-
# fixture, spec/fixtures/entity_list_mutations, now a real
|
|
333
|
-
# bootable domain) act on the entity's own attributes, so
|
|
334
|
-
# the element addressed by this step's own identity args
|
|
335
|
-
# is snapshotted before dispatch, materialized to plain
|
|
336
|
-
# data — `nil` for anything out of scope (an aggregate-
|
|
337
|
-
# level command, an entity command with no mutations at
|
|
338
|
-
# all, or one whose identity args don't resolve).
|
|
339
|
-
mutation_trace = build_mutation_trace(runtime, step["verb"], args)
|
|
340
|
-
|
|
341
|
-
# `role:`/`actor_id:` — optional per-step keys, absent on every
|
|
342
|
-
# one of the 231 existing `spec/corpus/*.json` steps (their own
|
|
343
|
-
# unwrapped `runtime.dispatch` call, unchanged, so nothing
|
|
344
|
-
# already pinned changes behavior). Binds the same ambient
|
|
345
|
-
# caller `refuse_role_mismatch` reads (`Hecks.as_caller`,
|
|
346
|
-
# `Runtime::Caller.as`) for exactly the one dispatch this
|
|
347
|
-
# step makes, then unbinds — mirrors `Caller.as`'s own
|
|
348
|
-
# `ensure`-restore, so back-to-back steps with different (or
|
|
349
|
-
# no) `role:` never leak into each other. `actor_id:` is the
|
|
350
|
-
# sibling `kernel/cli.rs` already read (its own comment on
|
|
351
|
-
# the key): with it, a Governance-attached domain runs the
|
|
352
|
-
# real `holds_role?` lookup instead of the string fallback.
|
|
353
|
-
result = as_step_caller(step) { runtime.dispatch_flat(step["verb"], args) }
|
|
354
|
-
|
|
355
|
-
fan_outs.concat(fan_out_findings(runtime, fan_out_snapshot, result.events, runtime.reactions[reaction_mark..]))
|
|
356
|
-
|
|
357
|
-
# **The outbox oracle's own capture** — every outbox row this
|
|
358
|
-
# step's own dispatch newly wrote (across every bound
|
|
359
|
-
# repository, including any a reaction cascade touched —
|
|
360
|
-
# `outbox_before_ids` was taken before `dispatch`, which
|
|
361
|
-
# is the same call that runs the whole cascade
|
|
362
|
-
# synchronously, `reenter` included), paired with the
|
|
363
|
-
# `reaction_log`/`saga_log` rows that same dispatch
|
|
364
|
-
# produced. Skipped entirely when empty — a step whose
|
|
365
|
-
# own aggregate has no outbox enqueues nothing here, and
|
|
366
|
-
# there is nothing for `Properties.outbox_rows_match_
|
|
367
|
-
# reactions` to check for it (its own reactions, if any,
|
|
368
|
-
# went through the direct, pre-outbox path instead).
|
|
369
|
-
outbox_new_rows = runtime.outbox.rows.reject { |row| outbox_before_ids.include?(row.delivery_id) }
|
|
370
|
-
if outbox_new_rows.any?
|
|
371
|
-
outbox_traces << { verb: step["verb"], rows: outbox_new_rows.map(&:to_h),
|
|
372
|
-
reactions: runtime.reactions[reaction_mark..].dup,
|
|
373
|
-
sagas: runtime.sagas[saga_log_mark..].dup }
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
guard_checks << guard_check.merge(actual_refused: false, actual_kind: nil) if guard_check
|
|
377
|
-
# After — only on success ; a refused step mutated nothing,
|
|
378
|
-
# so there is no "after" to compare (and #build_mutation_
|
|
379
|
-
# trace already skipped anything with no mutations to
|
|
380
|
-
# trace in the first place).
|
|
381
|
-
mutation_traces << mutation_trace.merge(after: read_mutation_after(runtime, mutation_trace)) if mutation_trace
|
|
382
|
-
rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
|
|
383
|
-
# `kind:` — the raised class, not re-derived from the message.
|
|
384
|
-
# `GivenNotMet`/`EnsuresNotMet` share their exact wording
|
|
385
|
-
# ("<command> refused — <description>") with four other
|
|
386
|
-
# refusal templates (Vocabulary's own LifecycleRefused/
|
|
387
|
-
# TypeMismatch/Unauthorized entries) — a property that told
|
|
388
|
-
# a guard refusal apart by pattern-matching the string alone
|
|
389
|
-
# would misread every one of those as a guard, or a guard as
|
|
390
|
-
# one of those. The class is unambiguous where the string
|
|
391
|
-
# is not.
|
|
392
|
-
refusals << { verb: step["verb"], error: e.message, kind: refusal_kind(e) }
|
|
393
|
-
# Only a refusal raised by the guard itself counts here —
|
|
394
|
-
# measured, not assumed: a step whose args were simply
|
|
395
|
-
# incomplete (AbsentArgument, from normalize_args — which
|
|
396
|
-
# runs before enforce_givens in DISPATCH_ORDER) never
|
|
397
|
-
# reached the guard at all, and this oracle's own first
|
|
398
|
-
# live run against real generated pizzas data caught
|
|
399
|
-
# exactly that case as a false positive (a malformed-args
|
|
400
|
-
# step the generator deliberately produces, `amount:`
|
|
401
|
-
# dropped entirely) before this comment existed. Whether a
|
|
402
|
-
# refusal from a stage after enforce_givens (TypeMismatch
|
|
403
|
-
# on a mutation, EnsuresNotMet, InvariantViolation) proves
|
|
404
|
-
# the guard passed can't be told apart from a same-shaped
|
|
405
|
-
# refusal from a stage before it by class alone (TypeMismatch
|
|
406
|
-
# can come from either), so anything that isn't one of the
|
|
407
|
-
# two guard classes is left out of guard_checks entirely —
|
|
408
|
-
# inconclusive, not a claimed pass.
|
|
409
|
-
if guard_check && GUARD_REFUSAL_CLASSES.include?(e.class)
|
|
410
|
-
guard_checks << guard_check.merge(actual_refused: true,
|
|
411
|
-
actual_kind: e.class.name)
|
|
412
|
-
end
|
|
413
|
-
end
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
instances = snapshot_instances(runtime)
|
|
417
|
-
|
|
418
|
-
events = runtime.events.map do |event|
|
|
419
|
-
{ name: event.name, aggregate: event.aggregate, id: event.id, payload: event.payload }
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
# The live process-manager store, materialised to inert data —
|
|
423
|
-
# `{ pm_name => { correlation => { state:, memory: } } }`, the
|
|
424
|
-
# same shape SagaInterpreter#checkpoint hands its persistence
|
|
425
|
-
# adapter (state plus a `Value.materialize`d memory, which is
|
|
426
|
-
# exactly what `deep_copy` there serialises). Captured here
|
|
427
|
-
# because Replay returns the history, not the runtime, and the
|
|
428
|
-
# runtime goes out of scope with the boot — so the saga-durability
|
|
429
|
-
# property (Properties.sagas_rehydrate_cleanly) reads this rather
|
|
430
|
-
# than reaching into a store the Memory rebind leaves as the
|
|
431
|
-
# no-op NULL_SAGA_STORE. Materialised, not raw, so the history
|
|
432
|
-
# stays plain data and the round-trip check sees exactly the
|
|
433
|
-
# bytes a real adapter would have persisted.
|
|
434
|
-
saga_instances = runtime.registry.saga_instances.each_with_object({}) do |(pm_name, conversations), out|
|
|
435
|
-
out[pm_name] = conversations.each_with_object({}) do |(correlation, instance), rows|
|
|
436
|
-
rows[correlation] = { state: instance[:state], memory: Runtime::Value.materialize(instance[:memory]) }
|
|
437
|
-
end
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
# The booted chapter rides along — the boot already happened, and
|
|
441
|
-
# properties.rb's lifecycle/saga checks need the declared IR
|
|
442
|
-
# (state sets, handler graphs) beside the history it produced.
|
|
443
|
-
# Free: no second boot, just the object the first one already
|
|
444
|
-
# built.
|
|
445
|
-
#
|
|
446
|
-
# `bluebook:` (singular) stays the first-loaded chapter — every
|
|
447
|
-
# existing property scopes itself to "only what we have the
|
|
448
|
-
# grammar for" against exactly this one, deliberately (see
|
|
449
|
-
# lifecycle_values_are_declared's own comment). `bluebooks:`
|
|
450
|
-
# (plural) is the full map, keyed by domain name — a domain
|
|
451
|
-
# under fuzz commonly composes more than one bluebook (banking
|
|
452
|
-
# alone loads Banking + Governance + Identity), and a refusal
|
|
453
|
-
# or an event can legitimately come from any of them, not only
|
|
454
|
-
# whichever one happened to load first. A property that needs
|
|
455
|
-
# to resolve a verb back to its own declaring bluebook — not
|
|
456
|
-
# "the" bluebook — reads this instead.
|
|
457
|
-
history = { instances: instances, events: events, refusals: refusals,
|
|
458
|
-
reactions: runtime.reactions, sagas: runtime.sagas, saga_instances: saga_instances,
|
|
459
|
-
queries: queries, dry_runs: dry_runs, dry_run_traces: dry_run_traces,
|
|
460
|
-
fan_outs: fan_outs, guard_checks: guard_checks,
|
|
461
|
-
mutation_traces: mutation_traces, outbox_traces: outbox_traces,
|
|
462
|
-
saga_dispatches: runtime.saga_dispatches, policy_dispatches: runtime.policy_dispatches,
|
|
463
|
-
bluebook: runtime.registry.bluebooks.values.first,
|
|
464
|
-
bluebooks: runtime.registry.bluebooks.dup }
|
|
465
|
-
|
|
466
|
-
# `runtime` is still live here — this is the one and only place
|
|
467
|
-
# it is. See `SelfConsistency`'s own header for why this needs
|
|
468
|
-
# to happen now, against the same registry/repositories this
|
|
469
|
-
# replay's own dispatch loop just populated, not a second boot.
|
|
470
|
-
history[:self_consistency] = SelfConsistency.check(runtime, history) if self_consistency
|
|
471
|
-
|
|
472
|
-
history
|
|
473
|
-
end
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
# Runs the block with this step's `role:`/`actor_id:` bound as the
|
|
477
|
-
# ambient caller, if it declares one.
|
|
478
|
-
#
|
|
479
|
-
# A step with no `role:` dispatches exactly as every corpus step
|
|
480
|
-
# always has — bare, no caller bound at all (`Caller.current` nil,
|
|
481
|
-
# so `refuse_role_mismatch` returns before checking anything).
|
|
482
|
-
#
|
|
483
|
-
# @param step [Hash] a String-keyed step hash, read for `"role"`/`"actor_id"`
|
|
484
|
-
# @yield the step's own dispatch or dry-run call
|
|
485
|
-
# @return [Object] whatever the block returns
|
|
486
|
-
def as_step_caller(step, &)
|
|
487
|
-
return yield unless step["role"]
|
|
488
|
-
|
|
489
|
-
Hecks.as_caller(role: step["role"], actor_id: step["actor_id"], &)
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
# The guard oracle's own resolution — "which record, if any, is
|
|
493
|
-
# this step about, and would enforce_givens/enforce_lifecycle_guard
|
|
494
|
-
# have refused it against that record's pre-dispatch state" —
|
|
495
|
-
# reproduced read-only from already-public pieces
|
|
496
|
-
# (Naming.split_verb, registry.bluebook/.aggregate/.command,
|
|
497
|
-
# Runtime::Identity.of/.from, repository.find), the exact same
|
|
498
|
-
# three-tier fallback CommandInterpreter#hydrate's own acting
|
|
499
|
-
# branch uses, minus its creating/duplicate-checking logic (a
|
|
500
|
-
# creating command has no pre-existing record to snapshot, and
|
|
501
|
-
# every real target this closes — Debit/CloseAccount/Credit/
|
|
502
|
-
# FreezeAccount — already acts on one).
|
|
503
|
-
#
|
|
504
|
-
# `nil` for anything out of scope: an entity/port verb (a dotted
|
|
505
|
-
# command_name — EntityInterpreter's own enforce_givens call,
|
|
506
|
-
# `parent:`-shaped, is a different call signature this does not
|
|
507
|
-
# reproduce), a creating command, an unresolvable id, or a command
|
|
508
|
-
# that declares neither `givens` nor `from` at all (nothing to
|
|
509
|
-
# check — logging a guaranteed, tautological pass would be noise,
|
|
510
|
-
# not a finding, the same reason `aggregation_matches_recompute`
|
|
511
|
-
# skips a read model with no count/median declared).
|
|
512
|
-
#
|
|
513
|
-
# Never lets a resolution surprise (a malformed verb, a dangling
|
|
514
|
-
# reference) become the step's own real dispatch outcome — this
|
|
515
|
-
# is a separate, best-effort read, not part of the step's own
|
|
516
|
-
# control flow.
|
|
517
|
-
# Every persisted record, keyed the way `query_eligible_rows`/
|
|
518
|
-
# `#eligible_rows` (properties.rb) already expect. Called once per
|
|
519
|
-
# query step (see `call`, above), not only once at the very end,
|
|
520
|
-
# because a query asked at step 1 of a script whose later steps go on
|
|
521
|
-
# to create more records needs to be checked, by every property that
|
|
522
|
-
# independently recomputes "the eligible rows," against the state as
|
|
523
|
-
# of that query — not the final snapshot after the whole replay.
|
|
524
|
-
# Found live:
|
|
525
|
-
# `Banking.accounts_by_kind`, asked as literally the first step of a
|
|
526
|
-
# 3-step script, correctly answered against zero accounts (none
|
|
527
|
-
# existed yet) while `group_by_matches_recompute`'s own independent
|
|
528
|
-
# recompute claimed "1 eligible row" — the one account the script's
|
|
529
|
-
# later two steps went on to create. Each query step now carries
|
|
530
|
-
# its own `instances_at:` snapshot, taken at the moment it ran, so
|
|
531
|
-
# every property that recomputes against "the eligible rows" reads
|
|
532
|
-
# the state as that query actually saw it, not a shared final one.
|
|
533
|
-
#
|
|
534
|
-
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
535
|
-
# to read every persisted record from
|
|
536
|
-
# @return [Hash{String => Hash}] every persisted record's state, keyed by
|
|
537
|
-
# `"Domain::Aggregate#id"`
|
|
538
|
-
def snapshot_instances(runtime)
|
|
539
|
-
instances = {}
|
|
540
|
-
runtime.registry.bluebooks.each do |domain_name, bluebook|
|
|
541
|
-
bluebook.aggregates.each do |aggregate|
|
|
542
|
-
runtime.registry.repository(domain_name, aggregate).all.each do |record|
|
|
543
|
-
instances["#{domain_name}::#{aggregate.name}##{record.id}"] = record.state
|
|
544
|
-
end
|
|
545
|
-
end
|
|
546
|
-
end
|
|
547
|
-
instances
|
|
548
|
-
end
|
|
549
|
-
|
|
550
|
-
# One early-return chain resolving "which record, if any" (see the
|
|
551
|
-
# long comment above), feeding a two-stage guard replay
|
|
552
|
-
# (enforce_givens then admissible_transition) that must run in that
|
|
553
|
-
# order to mirror the real DISPATCH_ORDER. Splitting the resolution
|
|
554
|
-
# out would still require passing domain_name/aggregate_name/
|
|
555
|
-
# aggregate/command/record back in, for no gain — every early
|
|
556
|
-
# return already reads as "nothing to check" at its own site.
|
|
557
|
-
# rubocop:disable-next Metrics/AbcSize
|
|
558
|
-
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
559
|
-
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
560
|
-
#
|
|
561
|
-
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
562
|
-
# to resolve the step's own verb and record against
|
|
563
|
-
# @param verb [String] the step's own command verb
|
|
564
|
-
# @param args [Hash{Symbol => Object}] the step's own args, symbol-keyed
|
|
565
|
-
# @return [Hash, nil] `{verb:, domain:, aggregate:, command:, id:, recomputed_refused:,
|
|
566
|
-
# recomputed_kind:}` if there was a record and a guard to check; nil for anything
|
|
567
|
-
# out of scope (an entity/port verb, a creating command, an unresolvable id, or a
|
|
568
|
-
# command with nothing to check)
|
|
569
|
-
def build_guard_check(runtime, verb, args)
|
|
570
|
-
domain_name, aggregate_name, command_name = Naming.split_verb(verb)
|
|
571
|
-
return nil unless command_name && !command_name.include?(".")
|
|
572
|
-
|
|
573
|
-
aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
|
|
574
|
-
command = aggregate&.command(command_name)
|
|
575
|
-
return nil unless aggregate && command && !command.creates?
|
|
576
|
-
|
|
577
|
-
# Nothing to check, genuinely — not "nothing THIS reproduces yet".
|
|
578
|
-
# A transition-only guard (no per-command `from:`, no `given`,
|
|
579
|
-
# only an aggregate `lifecycle do transition ... end` block
|
|
580
|
-
# naming this command — `Admit`/`Reject`'s own shape) still
|
|
581
|
-
# counts as something to check now that `admissible_transition`
|
|
582
|
-
# is reproduced below; skipping it here would just move the
|
|
583
|
-
# exact gap that call was added to close one line earlier.
|
|
584
|
-
has_transition = aggregate.lifecycle&.transitions_for(command.hecks_name)&.any?
|
|
585
|
-
return nil if command.givens.empty? && !command.from && !has_transition
|
|
586
|
-
|
|
587
|
-
reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
|
|
588
|
-
id = Runtime::Identity.of(aggregate, args) ||
|
|
589
|
-
Runtime::Identity.from(aggregate, args, :id) ||
|
|
590
|
-
(reference_key && Runtime::Identity.from(aggregate, args, reference_key))
|
|
591
|
-
return nil unless id
|
|
592
|
-
|
|
593
|
-
record = runtime.registry.repository(domain_name, aggregate).find(id)
|
|
594
|
-
return nil unless record
|
|
595
|
-
|
|
596
|
-
rules = Runtime::CommandRules.new(runtime.registry)
|
|
597
|
-
recomputed_kind = begin
|
|
598
|
-
rules.enforce_givens(record.dup, command, args, domain: domain_name, declaring: aggregate)
|
|
599
|
-
|
|
600
|
-
# A second, separate DISPATCH_ORDER step — `enforce_givens`
|
|
601
|
-
# (just above) only ever checks a per-command `from:` clause
|
|
602
|
-
# (its own trailing `enforce_lifecycle_guard(declaring, ...)
|
|
603
|
-
# if declaring` call) — the aggregate's own `lifecycle do
|
|
604
|
-
# transition "X" => Y, from: Z end` block is a wholly separate
|
|
605
|
-
# method (`admissible_transition`), called as its own later
|
|
606
|
-
# DISPATCH_ORDER step (`:enforce_givens` then
|
|
607
|
-
# `:admissible_transition` — Vocabulary.symbols
|
|
608
|
-
# ("AggregateDispatchOrder")), not reached from inside
|
|
609
|
-
# `enforce_givens` at all. Missing this call meant a command
|
|
610
|
-
# declared with no per-command `from:` of its own — every real
|
|
611
|
-
# transition-guarded command in this corpus, `Admit`/`Reject`
|
|
612
|
-
# included — always recomputed "admitted" no matter the
|
|
613
|
-
# record's actual state, because the one check that would
|
|
614
|
-
# have refused it was never run. Found live: `Expression::
|
|
615
|
-
# Expression.Admit`, fuzzed against `lib/hecks/grammar`
|
|
616
|
-
# (a domain the property's own hand-verification — Banking,
|
|
617
|
-
# Pizzas — never happened to exercise a transition-guarded,
|
|
618
|
-
# no-per-command-`from:` command against). Called only when
|
|
619
|
-
# `enforce_givens` didn't already refuse, mirroring the real
|
|
620
|
-
# pipeline's own "first refusal wins" order exactly.
|
|
621
|
-
rules.admissible_transition(aggregate, command, record.dup)
|
|
622
|
-
nil
|
|
623
|
-
rescue *GUARD_REFUSAL_CLASSES => e
|
|
624
|
-
e.class.name
|
|
625
|
-
end
|
|
626
|
-
|
|
627
|
-
{ verb: verb, domain: domain_name, aggregate: aggregate_name, command: command_name, id: id,
|
|
628
|
-
recomputed_refused: !recomputed_kind.nil?, recomputed_kind: recomputed_kind }
|
|
629
|
-
rescue StandardError
|
|
630
|
-
nil
|
|
631
|
-
end
|
|
632
|
-
|
|
633
|
-
# The mutation oracle's own pre-dispatch read — scoped, on
|
|
634
|
-
# purpose, to entity-dispatched commands only (a dotted
|
|
635
|
-
# command_name): the one place `append`/`remove`/`multiply`/
|
|
636
|
-
# `clamp` are known to act on an entity's own attributes
|
|
637
|
-
# (spec/fixtures/entity_list_mutations' own TaggedList — `tags`
|
|
638
|
-
# a value-object list, `count` a VO-typed scalar), never on
|
|
639
|
-
# another nested entity list — so this never needs to reproduce
|
|
640
|
-
# `MutationApplier#entity_element`'s own auto-mint/collision logic
|
|
641
|
-
# (item 1's own fix) at all. An aggregate-level command whose own
|
|
642
|
-
# mutation appends an entity (`Board.AddList`, `SafeDepositBox.
|
|
643
|
-
# LogVisit`) is a different, already-covered case — item 1's own
|
|
644
|
-
# collision property, not this one.
|
|
645
|
-
#
|
|
646
|
-
# `nil` for anything out of scope: an aggregate-level command, an
|
|
647
|
-
# entity command with no mutations at all, or one whose identity
|
|
648
|
-
# args (parent or element) don't resolve.
|
|
649
|
-
# Same shape as build_guard_check just above: one early-return chain
|
|
650
|
-
# resolving the entity/element this step's args address (see the
|
|
651
|
-
# comment above), each step depending on the previous one's
|
|
652
|
-
# resolved value. Threading those locals out to smaller methods
|
|
653
|
-
# would cost more than the cop gains.
|
|
654
|
-
# rubocop:disable-next Metrics/AbcSize
|
|
655
|
-
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
656
|
-
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
657
|
-
#
|
|
658
|
-
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
659
|
-
# to resolve the step's own verb and record against
|
|
660
|
-
# @param verb [String] the step's own command verb
|
|
661
|
-
# @param args [Hash{Symbol => Object}] the step's own args, symbol-keyed
|
|
662
|
-
# @return [Hash, nil] `{verb:, domain:, aggregate:, command:, parent_id:, list_attr:,
|
|
663
|
-
# element_wants:, before:, args:}` if this is an entity-dispatched command with
|
|
664
|
-
# mutations acting on a resolvable element; nil for anything out of scope
|
|
665
|
-
def build_mutation_trace(runtime, verb, args)
|
|
666
|
-
domain_name, aggregate_name, command_name = Naming.split_verb(verb)
|
|
667
|
-
return nil unless command_name&.include?(".")
|
|
668
|
-
|
|
669
|
-
aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
|
|
670
|
-
return nil unless aggregate
|
|
671
|
-
|
|
672
|
-
entity_name, entity_command_name = command_name.split(".", 2)
|
|
673
|
-
entity = aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
|
|
674
|
-
command = entity&.command(entity_command_name)
|
|
675
|
-
return nil unless command&.mutations&.any?
|
|
676
|
-
|
|
677
|
-
reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
|
|
678
|
-
parent_id = Runtime::Identity.of(aggregate, args) ||
|
|
679
|
-
Runtime::Identity.from(aggregate, args, :id) ||
|
|
680
|
-
(reference_key && Runtime::Identity.from(aggregate, args, reference_key))
|
|
681
|
-
return nil unless parent_id
|
|
682
|
-
|
|
683
|
-
record = runtime.registry.repository(domain_name, aggregate).find(parent_id)
|
|
684
|
-
return nil unless record
|
|
685
|
-
|
|
686
|
-
list_attr = aggregate.attributes.find { |a| a.list? && a.type.to_s == entity.hecks_name }
|
|
687
|
-
return nil unless list_attr
|
|
688
|
-
|
|
689
|
-
wants = entity.identity_paths.map do |path|
|
|
690
|
-
head = path.to_s.split(".").first.to_sym
|
|
691
|
-
raw = args[head]
|
|
692
|
-
return nil if raw.nil?
|
|
693
|
-
|
|
694
|
-
[head, Runtime::Value.for_attribute(aggregate, entity.attribute(head), raw)]
|
|
695
|
-
end
|
|
696
|
-
|
|
697
|
-
element = Array(record.state[list_attr.name]).find { |el| wants.all? { |head, want| el[head] == want } }
|
|
698
|
-
return nil unless element
|
|
699
|
-
|
|
700
|
-
{ verb: verb, domain: domain_name, aggregate: aggregate_name, command: command_name,
|
|
701
|
-
parent_id: parent_id, list_attr: list_attr.name, element_wants: wants,
|
|
702
|
-
before: Runtime::Value.materialize(element), args: args }
|
|
703
|
-
rescue StandardError
|
|
704
|
-
nil
|
|
705
|
-
end
|
|
706
|
-
|
|
707
|
-
# Re-reads the same entity element, after dispatch, for comparison against
|
|
708
|
-
# `trace[:before]`.
|
|
709
|
-
#
|
|
710
|
-
# The same element, re-located, after dispatch — by identity, not
|
|
711
|
-
# position (an append could have changed the array's own length
|
|
712
|
-
# or order relative to it). `nil` if it somehow vanished (not
|
|
713
|
-
# expected for any op this fixture declares — none of them
|
|
714
|
-
# remove the acted-on element itself — but a property comparing
|
|
715
|
-
# against `nil` fails loudly rather than crashing this replay).
|
|
716
|
-
#
|
|
717
|
-
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
718
|
-
# dispatch just ran against
|
|
719
|
-
# @param trace [Hash] the mutation trace `build_mutation_trace` built for this step,
|
|
720
|
-
# read for `:domain`, `:aggregate`, `:parent_id`, `:list_attr`, `:element_wants`
|
|
721
|
-
# @return [Hash, nil] the element's materialized state after dispatch, or nil if
|
|
722
|
-
# the parent record, its aggregate, or the element itself can no longer be found
|
|
723
|
-
def read_mutation_after(runtime, trace)
|
|
724
|
-
aggregate = runtime.registry.bluebook(trace[:domain])&.aggregate(trace[:aggregate])
|
|
725
|
-
return nil unless aggregate
|
|
726
|
-
|
|
727
|
-
record = runtime.registry.repository(trace[:domain], aggregate).find(trace[:parent_id])
|
|
728
|
-
return nil unless record
|
|
729
|
-
|
|
730
|
-
element = Array(record.state[trace[:list_attr]]).find do |el|
|
|
731
|
-
trace[:element_wants].all? { |head, want| el[head] == want }
|
|
732
|
-
end
|
|
733
|
-
element && Runtime::Value.materialize(element)
|
|
734
|
-
rescue StandardError
|
|
735
|
-
nil
|
|
736
|
-
end
|
|
737
|
-
|
|
738
|
-
# The fan-out oracle — one finding per (event, for_each policy) this
|
|
739
|
-
# step's own announced events could have triggered, independent of
|
|
740
|
-
# `PolicyInterpreter#deliver_for_each`: the same `where` evaluator
|
|
741
|
-
# every given/ensures already runs through, but the query answered
|
|
742
|
-
# by `Ports::Query::InMemory.holds?` directly against the live
|
|
743
|
-
# repository (`Replay.run_filter`'s own idiom), never by calling
|
|
744
|
-
# `QueryInterpreter` — sharing that call would make this oracle
|
|
745
|
-
# blind to exactly the code the fan-out feature adds.
|
|
746
|
-
#
|
|
747
|
-
# `expected_row_ids` is `nil` when `where` did not hold — no dispatch
|
|
748
|
-
# is the claim, not "dispatched to zero rows," so a property
|
|
749
|
-
# comparing this against the reaction log needs to tell the two
|
|
750
|
-
# apart. Recomputed once per event, not once per policy-and-event,
|
|
751
|
-
# because a `Chapter` de-duplicates on nothing this loop cannot
|
|
752
|
-
# cheaply repeat.
|
|
753
|
-
#
|
|
754
|
-
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
755
|
-
# @param snapshot [Hash{Array(String, String) => Hash}] the pre-dispatch `for_each`
|
|
756
|
-
# query snapshot, keyed by `[query_domain, aggregate_name]`
|
|
757
|
-
# @param announced [Array<Runtime::Event>] the events this step's own dispatch produced
|
|
758
|
-
# @param reactions_since [Array<Hash>] the reaction-log rows this step's own
|
|
759
|
-
# dispatch appended
|
|
760
|
-
# @return [Array<Hash>] one `{policy:, on:, expected_row_ids:, actual_row_ids:}`
|
|
761
|
-
# finding per (event, fanning-out policy) pair
|
|
762
|
-
def fan_out_findings(runtime, snapshot, announced, reactions_since)
|
|
763
|
-
announced.each_with_object([]) do |event, findings|
|
|
764
|
-
# `event.aggregate` is domain-qualified ("Banking::Account" —
|
|
765
|
-
# see command_rules/emission.rb's own Event.new) — the same
|
|
766
|
-
# source `PolicyInterpreter#policies_for` reads, split the
|
|
767
|
-
# same two ways: `Naming.demodulise` for the emitting
|
|
768
|
-
# aggregate's bare name, plain `split("::")` for the domain.
|
|
769
|
-
domain = event.aggregate.to_s.split("::").first
|
|
770
|
-
bluebook = runtime.registry.bluebook(domain)
|
|
771
|
-
next unless bluebook
|
|
772
|
-
|
|
773
|
-
emitting = Naming.demodulise(event.aggregate)
|
|
774
|
-
|
|
775
|
-
bluebook.policies.each do |policy|
|
|
776
|
-
next unless policy.fans_out? && policy.event_name == event.name
|
|
777
|
-
next unless policy.event_qualifier.nil? || policy.event_qualifier == emitting
|
|
778
|
-
|
|
779
|
-
findings << fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
|
|
780
|
-
end
|
|
781
|
-
end
|
|
782
|
-
end
|
|
783
|
-
|
|
784
|
-
# Builds one fan-out finding, comparing one `for_each` policy's independently
|
|
785
|
-
# recomputed expected rows against what actually reacted for one event.
|
|
786
|
-
#
|
|
787
|
-
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
788
|
-
# @param snapshot [Hash{Array(String, String) => Hash}] the pre-dispatch `for_each`
|
|
789
|
-
# query snapshot, keyed by `[query_domain, aggregate_name]`
|
|
790
|
-
# @param policy [Bluebook::Policy] the fanning-out policy this event could trigger
|
|
791
|
-
# @param event [Runtime::Event] the announced event being checked
|
|
792
|
-
# @param domain [String] the emitting event's own domain name
|
|
793
|
-
# @param reactions_since [Array<Hash>] the reaction-log rows this step's own
|
|
794
|
-
# dispatch appended
|
|
795
|
-
# @return [Hash{Symbol => Object}] `{policy: String, on: String,
|
|
796
|
-
# expected_row_ids: Array<String>, nil, actual_row_ids: Array}`
|
|
797
|
-
def fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
|
|
798
|
-
payload = event.payload.transform_keys(&:to_sym)
|
|
799
|
-
held = policy.where.to_s.empty? ||
|
|
800
|
-
Bluebook::Expression::Evaluator.call(policy.where, {}, payload)
|
|
801
|
-
|
|
802
|
-
expected = held ? expected_fan_out_rows(runtime, snapshot, policy, domain, payload) : nil
|
|
803
|
-
|
|
804
|
-
actual = reactions_since.select { |r| r[:policy] == policy.name && r[:on] == event.name }
|
|
805
|
-
.filter_map { |r| r[:for_row] }
|
|
806
|
-
|
|
807
|
-
{ policy: policy.name, on: event.name, expected_row_ids: expected, actual_row_ids: actual }
|
|
808
|
-
end
|
|
809
|
-
|
|
810
|
-
# The independent recomputation — `policy.for_each`'s declared query,
|
|
811
|
-
# answered against the pre-dispatch snapshot (see the snapshot's
|
|
812
|
-
# own comment at its capture site: the real fan-out's query runs
|
|
813
|
-
# synchronously, before its own dispatched commands can mutate
|
|
814
|
-
# anything the query would have matched, so this has to read the
|
|
815
|
-
# same "before" state or it grades the wrong moment). Comparators
|
|
816
|
-
# are `Ports::Query::InMemory.holds?`, never `QueryInterpreter` —
|
|
817
|
-
# sharing that call would make this oracle blind to exactly the
|
|
818
|
-
# code the fan-out feature adds. A `Symbol` where-value binds to
|
|
819
|
-
# the triggering event's own payload (the same binding
|
|
820
|
-
# `OpenForCustomer`'s `customer_id: :customer_id` relies on); a
|
|
821
|
-
# literal is compared as declared.
|
|
822
|
-
#
|
|
823
|
-
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
824
|
-
# to resolve the target query against
|
|
825
|
-
# @param snapshot [Hash{Array(String, String) => Hash}] the pre-dispatch `for_each`
|
|
826
|
-
# query snapshot, keyed by `[query_domain, aggregate_name]`
|
|
827
|
-
# @param policy [Bluebook::Policy] the fanning-out policy whose `for_each` route
|
|
828
|
-
# names the target query
|
|
829
|
-
# @param domain [String] the emitting event's own domain name, resolved through
|
|
830
|
-
# `policy.for_each_route`
|
|
831
|
-
# @param payload [Hash] the triggering event's own payload, symbol-keyed; binds any
|
|
832
|
-
# `Symbol` where-value
|
|
833
|
-
# @return [Array<String>] matching row ids, as strings, sorted; `[]` if the target
|
|
834
|
-
# query cannot be resolved
|
|
835
|
-
def expected_fan_out_rows(runtime, snapshot, policy, domain, payload)
|
|
836
|
-
query_domain, aggregate_name, query_name = policy.for_each_route(domain)
|
|
837
|
-
aggregate = runtime.registry.bluebook(query_domain)&.aggregate(aggregate_name)
|
|
838
|
-
query = aggregate&.query(query_name)
|
|
839
|
-
return [] unless query
|
|
840
|
-
|
|
841
|
-
rows = snapshot[[query_domain, aggregate_name]] || {}
|
|
842
|
-
matched = rows.select do |_id, state|
|
|
843
|
-
query.wheres.all? do |clause|
|
|
844
|
-
held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, clause.field))
|
|
845
|
-
Ports::Query::InMemory.holds?(clause, held, payload)
|
|
846
|
-
end
|
|
847
|
-
end
|
|
848
|
-
|
|
849
|
-
matched.keys.map(&:to_s).sort
|
|
850
|
-
end
|
|
851
|
-
|
|
852
|
-
# Names the outcome class a recorded refusal row should carry.
|
|
853
|
-
#
|
|
854
|
-
# The outcome class a recorded refusal row names (C8.2/C8.3,
|
|
855
|
-
# docs/semantics/bluebook-semantics.md): a domain refusal is its own
|
|
856
|
-
# class; an evaluation fault — the language refusing to interpret a
|
|
857
|
-
# broken rule or input — is `"Fault"`, the same word the Rust kernel
|
|
858
|
-
# emits (`Refusal::Fault`), never a refusal class and never a raw
|
|
859
|
-
# Ruby exception name.
|
|
860
|
-
#
|
|
861
|
-
# @param error [StandardError] the raised error a step's refusal was rescued as
|
|
862
|
-
# @return [String] `"Fault"` for an evaluation fault, otherwise `error`'s own class name
|
|
863
|
-
def refusal_kind(error)
|
|
864
|
-
error.is_a?(Bluebook::Expression::EvaluationError) ? "Fault" : error.class.name
|
|
865
|
-
end
|
|
866
|
-
|
|
867
|
-
# Answers one ad hoc filter step for real — the mirror image of
|
|
868
|
-
# kernel/cli.rs's own `run_filter`, deliberately calling the exact
|
|
869
|
-
# same production module that method's Rust port stands in for
|
|
870
|
-
# (`Ports::Query::InMemory`, lib/hecks/ports/query/in_memory.rb)
|
|
871
|
-
# rather than re-deriving comparator behavior by hand. `field` walks
|
|
872
|
-
# through `QuerySpecification::FieldPath.dig` (the same reading a
|
|
873
|
-
# declared where-clause gets), `comparable`/`holds?` are the same
|
|
874
|
-
# two calls `InMemory.execute` itself makes per candidate record —
|
|
875
|
-
# this is that method's own filter/select step, inlined, because
|
|
876
|
-
# there is no declared `Query` object here to hand `execute` (an ad
|
|
877
|
-
# hoc filter has no `order_by`/`limit`/`offset` at all, so nothing
|
|
878
|
-
# about `execute`'s own ordering/paging logic even applies).
|
|
879
|
-
# Sorted by id ascending regardless — `Ports::Query::Ordering`'s own
|
|
880
|
-
# header explains why an ask with no declared order still needs
|
|
881
|
-
# this tier ("the identity tier is what makes an ask total").
|
|
882
|
-
#
|
|
883
|
-
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
884
|
-
# to filter against
|
|
885
|
-
# @param filter [Hash] the ad hoc filter step's own Hash value: String-keyed
|
|
886
|
-
# `"aggregate"`, `"field"`, `"op"`, `"value"`
|
|
887
|
-
# @return [Array<Hash>] matching records, sorted by id ascending, each `{id:}`
|
|
888
|
-
# merged with the record's own state
|
|
889
|
-
# @raise [Bluebook::Expression::EvaluationError] if `op` names no known comparator,
|
|
890
|
-
# or `"aggregate"` names no loaded aggregate
|
|
891
|
-
def run_filter(runtime, filter)
|
|
892
|
-
aggregate_ref = filter["aggregate"].to_s
|
|
893
|
-
field = filter["field"].to_s
|
|
894
|
-
op = filter["op"].to_s
|
|
895
|
-
value = filter["value"]
|
|
896
|
-
|
|
897
|
-
# A malformed ad-hoc ask is a fault (C8.3), not a bare RuntimeError.
|
|
898
|
-
unless FILTER_COMPARATORS.include?(op)
|
|
899
|
-
raise Bluebook::Expression::EvaluationError, "unknown query comparator #{op.inspect}"
|
|
900
|
-
end
|
|
901
|
-
|
|
902
|
-
domain_name, aggregate_name = aggregate_ref.split("::", 2)
|
|
903
|
-
aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
|
|
904
|
-
raise Bluebook::Expression::EvaluationError, "unknown aggregate #{aggregate_ref.inspect}" unless aggregate
|
|
905
|
-
|
|
906
|
-
clause = QuerySpecification::Common::WhereClause.new(field: field, op: op, value: value)
|
|
907
|
-
records = runtime.registry.repository(domain_name, aggregate).all
|
|
908
|
-
matched = records.select do |record|
|
|
909
|
-
held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(record, field))
|
|
910
|
-
Ports::Query::InMemory.holds?(clause, held, {})
|
|
911
|
-
end
|
|
912
|
-
|
|
913
|
-
matched.sort_by { |record| record.id.to_s }.map { |record| { id: record.id }.merge(record.state) }
|
|
914
|
-
end
|
|
915
|
-
|
|
916
|
-
# Builds the `refusals` entry's own "verb" column for a refused ad hoc filter.
|
|
917
|
-
#
|
|
918
|
-
# There is no real verb to report (a filter step carries none), so this
|
|
919
|
-
# builds the same descriptive label kernel/cli.rs's own `filter_label`
|
|
920
|
-
# builds from the same three raw fields, tolerant of any of them being
|
|
921
|
-
# missing (Ruby's own nil-to-"" interpolation) the same way that Rust
|
|
922
|
-
# port is.
|
|
923
|
-
#
|
|
924
|
-
# @param filter [Hash] the ad hoc filter step's own Hash value, read for
|
|
925
|
-
# `"aggregate"`, `"field"`, `"op"`
|
|
926
|
-
# @return [String] a descriptive label such as `"filter Banking::Account.status eq"`
|
|
927
|
-
def filter_label(filter) = "filter #{filter['aggregate']}.#{filter['field']} #{filter['op']}"
|
|
928
|
-
end
|
|
929
|
-
end
|
|
930
|
-
end
|