hecks 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
data/lib/hecks/fuzzing/replay.rb
CHANGED
|
@@ -9,17 +9,17 @@ require_relative "../ports/query/in_memory"
|
|
|
9
9
|
|
|
10
10
|
module Hecks
|
|
11
11
|
module Fuzzing
|
|
12
|
-
# A step list, replayed
|
|
12
|
+
# A step list, replayed in-process against a fresh boot — the same
|
|
13
13
|
# copy-to-tmp-and-reset preamble SequenceGenerator#call uses, and the
|
|
14
14
|
# same observable surface bin/run prints (instances, events,
|
|
15
15
|
# refusals, reactions, sagas, queries), but returned as data rather
|
|
16
16
|
# than JSON on stdout.
|
|
17
17
|
#
|
|
18
|
-
#
|
|
18
|
+
# Not what SequenceGenerator itself dispatches through while
|
|
19
19
|
# generating — that inline execution feeds the picker's own
|
|
20
20
|
# known_ids tracking and stays exactly as it is. This exists for
|
|
21
|
-
# everything
|
|
22
|
-
# me what happened": bin/fuzz recomputing a shrink candidate's
|
|
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
23
|
# event count (removing a step changes what the sequence actually
|
|
24
24
|
# produces, so a shrunk candidate cannot reuse the original claim),
|
|
25
25
|
# and the declared-property checks in properties.rb. Both want it
|
|
@@ -35,36 +35,36 @@ module Hecks
|
|
|
35
35
|
module Replay
|
|
36
36
|
module_function
|
|
37
37
|
|
|
38
|
-
#
|
|
38
|
+
# The ad hoc filter's own comparator roster — read directly from
|
|
39
39
|
# QuerySpecification::Common::COMPARATORS (the same nine names
|
|
40
40
|
# Vocabulary::QueryComparator declares), never re-typed. A
|
|
41
41
|
# declared bluebook query never sees an `op:` outside this set —
|
|
42
|
-
# `admits: "Vocabulary::QueryComparator"` refuses one at
|
|
42
|
+
# `admits: "Vocabulary::QueryComparator"` refuses one at declare
|
|
43
43
|
# time — but a `"filter"`-shaped query step (below) has no
|
|
44
44
|
# declare-time gate at all, so this method gates it here instead.
|
|
45
45
|
#
|
|
46
|
-
#
|
|
46
|
+
# Not rust/src/kernel/query_comparators.rs's own ground truth —
|
|
47
47
|
# that hand-maintained Rust enum is missing `none_in_state` (the
|
|
48
48
|
# 9th comparator, added after the enum was written) and has
|
|
49
49
|
# already drifted; do not treat it as authoritative until item #9
|
|
50
50
|
# of the whole-project table-unification survey closes that gap.
|
|
51
51
|
FILTER_COMPARATORS = Hecks::QuerySpecification::Common::COMPARATORS.map(&:to_s).freeze
|
|
52
52
|
|
|
53
|
-
#
|
|
53
|
+
# The two classes `#enforce_givens`/`#enforce_lifecycle_guard`
|
|
54
54
|
# themselves ever raise — see Admissibility's own doc comment,
|
|
55
|
-
# `command_rules/admissibility.rb`. Any
|
|
55
|
+
# `command_rules/admissibility.rb`. Any other DOMAIN_REFUSAL a step
|
|
56
56
|
# raises (TypeMismatch, EnsuresNotMet, InvariantViolation, ...)
|
|
57
|
-
# proves the guard itself did
|
|
57
|
+
# proves the guard itself did not fire, since it runs first in
|
|
58
58
|
# DISPATCH_ORDER.
|
|
59
59
|
GUARD_REFUSAL_CLASSES = [Runtime::GivenNotMet, Runtime::LifecycleRefused].freeze
|
|
60
60
|
|
|
61
61
|
# One tightly ordered loop over steps, with several "oracle" snapshots
|
|
62
62
|
# (reaction_mark, fan_out_snapshot, guard_check, mutation_trace) that
|
|
63
|
-
# must be taken at very specific points
|
|
64
|
-
# fan_out_snapshot's own comment above for the real
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
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
68
|
# would let a future editor silently reorder a snapshot relative to
|
|
69
69
|
# `dispatch` without any single method looking wrong — the ordering
|
|
70
70
|
# invariant is only visible with the whole sequence in one place.
|
|
@@ -72,27 +72,43 @@ module Hecks
|
|
|
72
72
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
73
73
|
# rubocop:disable-next Metrics/MethodLength
|
|
74
74
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
75
|
-
# `self_consistency:` —
|
|
75
|
+
# `self_consistency:` — off by default, same "existing callers see no
|
|
76
76
|
# change" contract `adapter:` already has. `bin/qa_sweep` is the one
|
|
77
77
|
# real caller that opts in (gated by `QualityControlDials::
|
|
78
78
|
# SELF_CONSISTENCY_CHECKS`/`--self-consistency`): `bin/fuzz`,
|
|
79
79
|
# `Properties.check`'s own callers, and every existing spec keep
|
|
80
80
|
# calling this with no second axis of comparison at all, exactly as
|
|
81
|
-
# before. Computed
|
|
81
|
+
# before. Computed here, not by a caller reading `runtime` back out
|
|
82
82
|
# afterward — `runtime` and the whole `IsolatedBoot` tmp directory
|
|
83
83
|
# go out of scope the moment this method returns (see this file's
|
|
84
84
|
# own header), so `Hecks::Fuzzing::SelfConsistency.check` has to run
|
|
85
85
|
# while both are still alive, against the exact same repositories
|
|
86
86
|
# this replay's own dispatch loop just wrote to.
|
|
87
87
|
#
|
|
88
|
-
# `database:`/`schema:` —
|
|
88
|
+
# `database:`/`schema:` — only meaningful, and required, for
|
|
89
89
|
# `adapter: :postgres_era` — see `IsolatedBoot#rebind_to_postgres_era!`'s
|
|
90
90
|
# own header for why that one mode takes caller-owned connection
|
|
91
91
|
# identity rather than a shared default the way `:postgres` does.
|
|
92
92
|
# Forwarded straight through, unchanged, exactly like `adapter:`
|
|
93
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`
|
|
94
110
|
def call(domain_path, steps, adapter: :memory, database: nil, schema: nil, self_consistency: false)
|
|
95
|
-
# See isolated_boot.rb's own header: resets data/
|
|
111
|
+
# See isolated_boot.rb's own header: resets data/ and rebinds
|
|
96
112
|
# persistence to the chosen adapter (Memory by default), since a
|
|
97
113
|
# Postgres-bound domain's real store lives outside the copied
|
|
98
114
|
# directory and cannot be reached by resetting data/ alone.
|
|
@@ -108,7 +124,7 @@ module Hecks
|
|
|
108
124
|
mutation_traces = []
|
|
109
125
|
outbox_traces = []
|
|
110
126
|
|
|
111
|
-
#
|
|
127
|
+
# Every aggregate a `for_each` could ever query, resolved once —
|
|
112
128
|
# `[domain, aggregate_name]` pairs, gleaned from every loaded
|
|
113
129
|
# bluebook's own fanning-out policies. Empty for every domain
|
|
114
130
|
# with no `for_each` at all (every example this corpus ships
|
|
@@ -126,17 +142,17 @@ module Hecks
|
|
|
126
142
|
args = (step["args"] || {}).transform_keys(&:to_sym)
|
|
127
143
|
|
|
128
144
|
if (question = step["query"])
|
|
129
|
-
#
|
|
145
|
+
# **The ad hoc, single-comparator filter** — a "query" step whose
|
|
130
146
|
# own value is a Hash, not a name: `{aggregate:, field:, op:,
|
|
131
|
-
# value:}`, the
|
|
147
|
+
# value:}`, the same wire shape kernel/cli.rs's new object-
|
|
132
148
|
# form "query" step reads on the Rust side (that file's own
|
|
133
149
|
# header explains why this shape exists at all: it bypasses
|
|
134
150
|
# the bluebook query DSL entirely, so it needs no generated
|
|
135
151
|
# per-domain codegen to prove for real). Answered here by
|
|
136
|
-
# calling `Ports::Query::InMemory`
|
|
152
|
+
# calling `Ports::Query::InMemory` directly — the real
|
|
137
153
|
# production comparator engine, not a second, hand-rewritten
|
|
138
154
|
# copy of it — against the raw repository, never through
|
|
139
|
-
# `runtime.query`, which only ever resolves a
|
|
155
|
+
# `runtime.query`, which only ever resolves a named, declared
|
|
140
156
|
# ask.
|
|
141
157
|
if question.is_a?(Hash)
|
|
142
158
|
begin
|
|
@@ -148,16 +164,16 @@ module Hecks
|
|
|
148
164
|
next
|
|
149
165
|
end
|
|
150
166
|
|
|
151
|
-
#
|
|
152
|
-
#
|
|
167
|
+
# **The query oracle** — two independent engines, each run and
|
|
168
|
+
# caught on its own, never a single shared `begin`/`rescue`
|
|
153
169
|
# wrapping both calls. A shared begin/rescue meant `runtime.
|
|
154
|
-
# query` raising (native refuses) short-circuited
|
|
170
|
+
# query` raising (native refuses) short-circuited before
|
|
155
171
|
# `runtime.reference_query` ever ran at all — the entry
|
|
156
172
|
# recorded only `error:`, with no `reference_rows` and no
|
|
157
173
|
# record of what the reference interpreter would have
|
|
158
174
|
# answered — and `runtime.reference_query` raising instead
|
|
159
175
|
# (reference refuses, native already succeeded) landed in the
|
|
160
|
-
#
|
|
176
|
+
# same rescue, discarding the native `rows` this begin block
|
|
161
177
|
# had already computed and recording the whole ask as an
|
|
162
178
|
# ordinary refusal. Either way, "one engine refused and the
|
|
163
179
|
# other did not" — a real divergence, exactly the shape a
|
|
@@ -202,20 +218,20 @@ module Hecks
|
|
|
202
218
|
|
|
203
219
|
# `{"dry_run": verb, "args": …}` — `Dispatcher#dry_run?`: the command
|
|
204
220
|
# evaluated hypothetically, nothing saved or emitted, no reaction.
|
|
205
|
-
# Recorded, never a refusal: a refused dry run is an
|
|
221
|
+
# Recorded, never a refusal: a refused dry run is an answer.
|
|
206
222
|
#
|
|
207
|
-
# `dry_runs`
|
|
208
|
-
# shape it always had, and the
|
|
223
|
+
# `dry_runs` stays exactly `{verb:, ok:, error?:}` — the same
|
|
224
|
+
# shape it always had, and the same shape `kernel/cli.rs`'s own
|
|
209
225
|
# `dry_run` answers (`{"verb", "ok"}` or `{"verb", "ok": false,
|
|
210
226
|
# "error"}`, that function's own doc comment) — `spec/rust_
|
|
211
227
|
# conformance_spec.rb` compares this array against the compiled
|
|
212
228
|
# binary's own verbatim, so it can never carry a key Rust's own
|
|
213
229
|
# answer does not. The role-gated binding (`as_step_caller`,
|
|
214
|
-
# the
|
|
215
|
-
# applies to the dry-run call itself — only what gets
|
|
230
|
+
# the same `role:`/`actor_id:` a real dispatch gets below) still
|
|
231
|
+
# applies to the dry-run call itself — only what gets recorded
|
|
216
232
|
# about it is unchanged.
|
|
217
233
|
#
|
|
218
|
-
# `dry_run_traces` —
|
|
234
|
+
# `dry_run_traces` — a separate, parallel array (same order,
|
|
219
235
|
# not merged into `dry_runs` above) carrying `before:`/`after:`
|
|
220
236
|
# snapshots of the whole observable store (every instance, the
|
|
221
237
|
# event count) on either side of the hypothetical call, so
|
|
@@ -239,32 +255,32 @@ module Hecks
|
|
|
239
255
|
end
|
|
240
256
|
|
|
241
257
|
begin
|
|
242
|
-
#
|
|
243
|
-
# dispatch, so any reaction this
|
|
258
|
+
# **The fan-out oracle's own low-water mark** — taken before
|
|
259
|
+
# dispatch, so any reaction this one step's own announced
|
|
244
260
|
# events produce (`reaction_log` grows in place, the same
|
|
245
261
|
# Array `runtime.reactions` already exposes) can be sliced
|
|
246
|
-
# out after, and matched against an
|
|
262
|
+
# out after, and matched against an independent recomputation
|
|
247
263
|
# of what a `for_each` policy should have fanned out over —
|
|
248
264
|
# the query oracle's own shape (two engines, compared, never
|
|
249
265
|
# one graded against itself), aimed at fan-out instead of a
|
|
250
266
|
# named ask.
|
|
251
267
|
reaction_mark = runtime.reactions.size
|
|
252
268
|
|
|
253
|
-
#
|
|
269
|
+
# **The outbox oracle's own low-water marks** — taken before
|
|
254
270
|
# dispatch, same idiom as `reaction_mark` right above:
|
|
255
271
|
# `saga_log_mark` slices `runtime.sagas` (a single flat
|
|
256
272
|
# array, safe to index into directly) the identical way
|
|
257
273
|
# `reaction_mark` already slices `runtime.reactions`.
|
|
258
|
-
# `outbox_before_ids` is a
|
|
274
|
+
# `outbox_before_ids` is a set of delivery_ids, not a
|
|
259
275
|
# size — `runtime.outbox.rows` concatenates every bound
|
|
260
|
-
# repository's own array in a
|
|
276
|
+
# repository's own array in a fixed per-store order
|
|
261
277
|
# (`Outbox::Relay#rows`, `stores.flat_map`), so a row a
|
|
262
|
-
#
|
|
263
|
-
# earlier-iterated store would land in the
|
|
278
|
+
# different step's dispatch enqueues into an
|
|
279
|
+
# earlier-iterated store would land in the middle of
|
|
264
280
|
# that concatenated list, not at its tail — a plain
|
|
265
281
|
# "grew from N to M, take the tail" slice (the shape
|
|
266
282
|
# `reaction_mark`/`saga_log_mark` both get away with,
|
|
267
|
-
# since `reaction_log`/`saga_log` are each already
|
|
283
|
+
# since `reaction_log`/`saga_log` are each already one
|
|
268
284
|
# flat array irrespective of domain) would silently miss
|
|
269
285
|
# or misattribute rows the moment more than one
|
|
270
286
|
# repository has an outbox. `delivery_id` is unique per
|
|
@@ -276,12 +292,12 @@ module Hecks
|
|
|
276
292
|
saga_log_mark = runtime.sagas.size
|
|
277
293
|
outbox_before_ids = runtime.outbox.rows.map(&:delivery_id)
|
|
278
294
|
|
|
279
|
-
#
|
|
280
|
-
#
|
|
281
|
-
# `deliver_for_each` runs its query
|
|
282
|
-
# this
|
|
283
|
-
# oracle that re-reads the live repository
|
|
284
|
-
# answers sees whatever the fan-out's
|
|
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
|
|
285
301
|
# commands already mutated (an Account a `Review` leg just
|
|
286
302
|
# moved out of "open," say), not what the query actually
|
|
287
303
|
# matched. A measured bug, not a hypothetical one — this
|
|
@@ -295,37 +311,37 @@ module Hecks
|
|
|
295
311
|
runtime.registry.repository(fdomain, aggregate).all.to_h { |record| [record.id, record.state.dup] }
|
|
296
312
|
end
|
|
297
313
|
|
|
298
|
-
#
|
|
314
|
+
# **The guard oracle's own pre-dispatch read** — same idiom,
|
|
299
315
|
# same placement, same reason as fan_out_snapshot right
|
|
300
316
|
# above: `Admissibility#enforce_givens` (which itself calls
|
|
301
317
|
# `#enforce_lifecycle_guard` when `declaring:` is passed)
|
|
302
|
-
# is called a
|
|
318
|
+
# is called a second time here, independently, against the
|
|
303
319
|
# record exactly as CommandInterpreter#hydrate's own acting
|
|
304
320
|
# branch would find it (`repository.find(id).dup` — the
|
|
305
321
|
# identical three-tier id fallback, Identity.of/.from,
|
|
306
|
-
# reproduced read-only) —
|
|
322
|
+
# reproduced read-only) — before this step's real dispatch
|
|
307
323
|
# can mutate anything a cross-aggregate given dereferences
|
|
308
324
|
# (`customer.status`). A pure predicate read, side-effect
|
|
309
325
|
# free, so calling it twice changes nothing this step
|
|
310
326
|
# itself observes.
|
|
311
327
|
guard_check = build_guard_check(runtime, step["verb"], args)
|
|
312
328
|
|
|
313
|
-
#
|
|
314
|
-
# idiom again: an
|
|
329
|
+
# **The mutation oracle's own pre-dispatch read** — same
|
|
330
|
+
# idiom again: an entity-dispatched command's own
|
|
315
331
|
# `append`/`remove`/`multiply`/`clamp` mutations (S17's
|
|
316
332
|
# fixture, spec/fixtures/entity_list_mutations, now a real
|
|
317
|
-
# bootable domain) act on the entity's
|
|
333
|
+
# bootable domain) act on the entity's own attributes, so
|
|
318
334
|
# the element addressed by this step's own identity args
|
|
319
|
-
# is snapshotted
|
|
335
|
+
# is snapshotted before dispatch, materialized to plain
|
|
320
336
|
# data — `nil` for anything out of scope (an aggregate-
|
|
321
337
|
# level command, an entity command with no mutations at
|
|
322
338
|
# all, or one whose identity args don't resolve).
|
|
323
339
|
mutation_trace = build_mutation_trace(runtime, step["verb"], args)
|
|
324
340
|
|
|
325
|
-
# `role:`/`actor_id:` —
|
|
341
|
+
# `role:`/`actor_id:` — optional per-step keys, absent on every
|
|
326
342
|
# one of the 231 existing `spec/corpus/*.json` steps (their own
|
|
327
343
|
# unwrapped `runtime.dispatch` call, unchanged, so nothing
|
|
328
|
-
# already pinned changes behavior). Binds the
|
|
344
|
+
# already pinned changes behavior). Binds the same ambient
|
|
329
345
|
# caller `refuse_role_mismatch` reads (`Hecks.as_caller`,
|
|
330
346
|
# `Runtime::Caller.as`) for exactly the one dispatch this
|
|
331
347
|
# step makes, then unbinds — mirrors `Caller.as`'s own
|
|
@@ -334,12 +350,12 @@ module Hecks
|
|
|
334
350
|
# sibling `kernel/cli.rs` already read (its own comment on
|
|
335
351
|
# the key): with it, a Governance-attached domain runs the
|
|
336
352
|
# real `holds_role?` lookup instead of the string fallback.
|
|
337
|
-
result = as_step_caller(step) { runtime.
|
|
353
|
+
result = as_step_caller(step) { runtime.dispatch_flat(step["verb"], args) }
|
|
338
354
|
|
|
339
355
|
fan_outs.concat(fan_out_findings(runtime, fan_out_snapshot, result.events, runtime.reactions[reaction_mark..]))
|
|
340
356
|
|
|
341
|
-
#
|
|
342
|
-
#
|
|
357
|
+
# **The outbox oracle's own capture** — every outbox row this
|
|
358
|
+
# step's own dispatch newly wrote (across every bound
|
|
343
359
|
# repository, including any a reaction cascade touched —
|
|
344
360
|
# `outbox_before_ids` was taken before `dispatch`, which
|
|
345
361
|
# is the same call that runs the whole cascade
|
|
@@ -358,15 +374,15 @@ module Hecks
|
|
|
358
374
|
end
|
|
359
375
|
|
|
360
376
|
guard_checks << guard_check.merge(actual_refused: false, actual_kind: nil) if guard_check
|
|
361
|
-
#
|
|
377
|
+
# After — only on success ; a refused step mutated nothing,
|
|
362
378
|
# so there is no "after" to compare (and #build_mutation_
|
|
363
379
|
# trace already skipped anything with no mutations to
|
|
364
380
|
# trace in the first place).
|
|
365
381
|
mutation_traces << mutation_trace.merge(after: read_mutation_after(runtime, mutation_trace)) if mutation_trace
|
|
366
382
|
rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
|
|
367
|
-
# `kind:` — the
|
|
383
|
+
# `kind:` — the raised class, not re-derived from the message.
|
|
368
384
|
# `GivenNotMet`/`EnsuresNotMet` share their exact wording
|
|
369
|
-
# ("<command> refused — <description>") with
|
|
385
|
+
# ("<command> refused — <description>") with four other
|
|
370
386
|
# refusal templates (Vocabulary's own LifecycleRefused/
|
|
371
387
|
# TypeMismatch/Unauthorized entries) — a property that told
|
|
372
388
|
# a guard refusal apart by pattern-matching the string alone
|
|
@@ -374,21 +390,21 @@ module Hecks
|
|
|
374
390
|
# one of those. The class is unambiguous where the string
|
|
375
391
|
# is not.
|
|
376
392
|
refusals << { verb: step["verb"], error: e.message, kind: refusal_kind(e) }
|
|
377
|
-
#
|
|
393
|
+
# Only a refusal raised by the guard itself counts here —
|
|
378
394
|
# measured, not assumed: a step whose args were simply
|
|
379
395
|
# incomplete (AbsentArgument, from normalize_args — which
|
|
380
|
-
# runs
|
|
396
|
+
# runs before enforce_givens in DISPATCH_ORDER) never
|
|
381
397
|
# reached the guard at all, and this oracle's own first
|
|
382
398
|
# live run against real generated pizzas data caught
|
|
383
399
|
# exactly that case as a false positive (a malformed-args
|
|
384
400
|
# step the generator deliberately produces, `amount:`
|
|
385
401
|
# dropped entirely) before this comment existed. Whether a
|
|
386
|
-
# refusal from a stage
|
|
402
|
+
# refusal from a stage after enforce_givens (TypeMismatch
|
|
387
403
|
# on a mutation, EnsuresNotMet, InvariantViolation) proves
|
|
388
404
|
# the guard passed can't be told apart from a same-shaped
|
|
389
|
-
# refusal from a stage
|
|
405
|
+
# refusal from a stage before it by class alone (TypeMismatch
|
|
390
406
|
# can come from either), so anything that isn't one of the
|
|
391
|
-
# two guard classes is left
|
|
407
|
+
# two guard classes is left out of guard_checks entirely —
|
|
392
408
|
# inconclusive, not a claimed pass.
|
|
393
409
|
if guard_check && GUARD_REFUSAL_CLASSES.include?(e.class)
|
|
394
410
|
guard_checks << guard_check.merge(actual_refused: true,
|
|
@@ -403,9 +419,9 @@ module Hecks
|
|
|
403
419
|
{ name: event.name, aggregate: event.aggregate, id: event.id, payload: event.payload }
|
|
404
420
|
end
|
|
405
421
|
|
|
406
|
-
#
|
|
422
|
+
# The live process-manager store, materialised to inert data —
|
|
407
423
|
# `{ pm_name => { correlation => { state:, memory: } } }`, the
|
|
408
|
-
#
|
|
424
|
+
# same shape SagaInterpreter#checkpoint hands its persistence
|
|
409
425
|
# adapter (state plus a `Value.materialize`d memory, which is
|
|
410
426
|
# exactly what `deep_copy` there serialises). Captured here
|
|
411
427
|
# because Replay returns the history, not the runtime, and the
|
|
@@ -413,7 +429,7 @@ module Hecks
|
|
|
413
429
|
# property (Properties.sagas_rehydrate_cleanly) reads this rather
|
|
414
430
|
# than reaching into a store the Memory rebind leaves as the
|
|
415
431
|
# no-op NULL_SAGA_STORE. Materialised, not raw, so the history
|
|
416
|
-
# stays plain data
|
|
432
|
+
# stays plain data and the round-trip check sees exactly the
|
|
417
433
|
# bytes a real adapter would have persisted.
|
|
418
434
|
saga_instances = runtime.registry.saga_instances.each_with_object({}) do |(pm_name, conversations), out|
|
|
419
435
|
out[pm_name] = conversations.each_with_object({}) do |(correlation, instance), rows|
|
|
@@ -427,16 +443,16 @@ module Hecks
|
|
|
427
443
|
# Free: no second boot, just the object the first one already
|
|
428
444
|
# built.
|
|
429
445
|
#
|
|
430
|
-
# `bluebook:` (singular) stays the
|
|
446
|
+
# `bluebook:` (singular) stays the first-loaded chapter — every
|
|
431
447
|
# existing property scopes itself to "only what we have the
|
|
432
448
|
# grammar for" against exactly this one, deliberately (see
|
|
433
449
|
# lifecycle_values_are_declared's own comment). `bluebooks:`
|
|
434
|
-
# (plural) is the
|
|
450
|
+
# (plural) is the full map, keyed by domain name — a domain
|
|
435
451
|
# under fuzz commonly composes more than one bluebook (banking
|
|
436
452
|
# alone loads Banking + Governance + Identity), and a refusal
|
|
437
|
-
# or an event can legitimately come from
|
|
453
|
+
# or an event can legitimately come from any of them, not only
|
|
438
454
|
# whichever one happened to load first. A property that needs
|
|
439
|
-
# to resolve a verb back to its
|
|
455
|
+
# to resolve a verb back to its own declaring bluebook — not
|
|
440
456
|
# "the" bluebook — reads this instead.
|
|
441
457
|
history = { instances: instances, events: events, refusals: refusals,
|
|
442
458
|
reactions: runtime.reactions, sagas: runtime.sagas, saga_instances: saga_instances,
|
|
@@ -447,9 +463,9 @@ module Hecks
|
|
|
447
463
|
bluebook: runtime.registry.bluebooks.values.first,
|
|
448
464
|
bluebooks: runtime.registry.bluebooks.dup }
|
|
449
465
|
|
|
450
|
-
# `runtime`
|
|
466
|
+
# `runtime` is still live here — this is the one and only place
|
|
451
467
|
# it is. See `SelfConsistency`'s own header for why this needs
|
|
452
|
-
# to happen
|
|
468
|
+
# to happen now, against the same registry/repositories this
|
|
453
469
|
# replay's own dispatch loop just populated, not a second boot.
|
|
454
470
|
history[:self_consistency] = SelfConsistency.check(runtime, history) if self_consistency
|
|
455
471
|
|
|
@@ -457,22 +473,29 @@ module Hecks
|
|
|
457
473
|
end
|
|
458
474
|
end
|
|
459
475
|
|
|
476
|
+
# Runs the block with this step's `role:`/`actor_id:` bound as the
|
|
477
|
+
# ambient caller, if it declares one.
|
|
478
|
+
#
|
|
460
479
|
# A step with no `role:` dispatches exactly as every corpus step
|
|
461
480
|
# always has — bare, no caller bound at all (`Caller.current` nil,
|
|
462
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
|
|
463
486
|
def as_step_caller(step, &)
|
|
464
487
|
return yield unless step["role"]
|
|
465
488
|
|
|
466
489
|
Hecks.as_caller(role: step["role"], actor_id: step["actor_id"], &)
|
|
467
490
|
end
|
|
468
491
|
|
|
469
|
-
#
|
|
492
|
+
# The guard oracle's own resolution — "which record, if any, is
|
|
470
493
|
# this step about, and would enforce_givens/enforce_lifecycle_guard
|
|
471
|
-
# have refused it against that record's
|
|
494
|
+
# have refused it against that record's pre-dispatch state" —
|
|
472
495
|
# reproduced read-only from already-public pieces
|
|
473
496
|
# (Naming.split_verb, registry.bluebook/.aggregate/.command,
|
|
474
497
|
# Runtime::Identity.of/.from, repository.find), the exact same
|
|
475
|
-
# three-tier fallback CommandInterpreter#hydrate's
|
|
498
|
+
# three-tier fallback CommandInterpreter#hydrate's own acting
|
|
476
499
|
# branch uses, minus its creating/duplicate-checking logic (a
|
|
477
500
|
# creating command has no pre-existing record to snapshot, and
|
|
478
501
|
# every real target this closes — Debit/CloseAccount/Credit/
|
|
@@ -489,25 +512,29 @@ module Hecks
|
|
|
489
512
|
#
|
|
490
513
|
# Never lets a resolution surprise (a malformed verb, a dangling
|
|
491
514
|
# reference) become the step's own real dispatch outcome — this
|
|
492
|
-
# is a
|
|
515
|
+
# is a separate, best-effort read, not part of the step's own
|
|
493
516
|
# control flow.
|
|
494
|
-
#
|
|
495
|
-
#
|
|
496
|
-
#
|
|
497
|
-
#
|
|
498
|
-
#
|
|
499
|
-
#
|
|
500
|
-
#
|
|
501
|
-
#
|
|
502
|
-
# the ones that existed when the query actually ran. Found live:
|
|
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:
|
|
503
525
|
# `Banking.accounts_by_kind`, asked as literally the first step of a
|
|
504
526
|
# 3-step script, correctly answered against zero accounts (none
|
|
505
527
|
# existed yet) while `group_by_matches_recompute`'s own independent
|
|
506
|
-
# recompute claimed "1 eligible row" — the
|
|
528
|
+
# recompute claimed "1 eligible row" — the one account the script's
|
|
507
529
|
# later two steps went on to create. Each query step now carries
|
|
508
530
|
# its own `instances_at:` snapshot, taken at the moment it ran, so
|
|
509
531
|
# every property that recomputes against "the eligible rows" reads
|
|
510
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"`
|
|
511
538
|
def snapshot_instances(runtime)
|
|
512
539
|
instances = {}
|
|
513
540
|
runtime.registry.bluebooks.each do |domain_name, bluebook|
|
|
@@ -530,6 +557,15 @@ module Hecks
|
|
|
530
557
|
# rubocop:disable-next Metrics/AbcSize
|
|
531
558
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
532
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)
|
|
533
569
|
def build_guard_check(runtime, verb, args)
|
|
534
570
|
domain_name, aggregate_name, command_name = Naming.split_verb(verb)
|
|
535
571
|
return nil unless command_name && !command_name.include?(".")
|
|
@@ -538,7 +574,7 @@ module Hecks
|
|
|
538
574
|
command = aggregate&.command(command_name)
|
|
539
575
|
return nil unless aggregate && command && !command.creates?
|
|
540
576
|
|
|
541
|
-
#
|
|
577
|
+
# Nothing to check, genuinely — not "nothing THIS reproduces yet".
|
|
542
578
|
# A transition-only guard (no per-command `from:`, no `given`,
|
|
543
579
|
# only an aggregate `lifecycle do transition ... end` block
|
|
544
580
|
# naming this command — `Admit`/`Reject`'s own shape) still
|
|
@@ -561,20 +597,20 @@ module Hecks
|
|
|
561
597
|
recomputed_kind = begin
|
|
562
598
|
rules.enforce_givens(record.dup, command, args, domain: domain_name, declaring: aggregate)
|
|
563
599
|
|
|
564
|
-
# A
|
|
565
|
-
# (just above) only ever checks a per-
|
|
600
|
+
# A second, separate DISPATCH_ORDER step — `enforce_givens`
|
|
601
|
+
# (just above) only ever checks a per-command `from:` clause
|
|
566
602
|
# (its own trailing `enforce_lifecycle_guard(declaring, ...)
|
|
567
603
|
# if declaring` call) — the aggregate's own `lifecycle do
|
|
568
|
-
# transition "X" => Y, from: Z end` block is a
|
|
604
|
+
# transition "X" => Y, from: Z end` block is a wholly separate
|
|
569
605
|
# method (`admissible_transition`), called as its own later
|
|
570
606
|
# DISPATCH_ORDER step (`:enforce_givens` then
|
|
571
607
|
# `:admissible_transition` — Vocabulary.symbols
|
|
572
608
|
# ("AggregateDispatchOrder")), not reached from inside
|
|
573
609
|
# `enforce_givens` at all. Missing this call meant a command
|
|
574
|
-
# declared with
|
|
610
|
+
# declared with no per-command `from:` of its own — every real
|
|
575
611
|
# transition-guarded command in this corpus, `Admit`/`Reject`
|
|
576
612
|
# included — always recomputed "admitted" no matter the
|
|
577
|
-
# record's actual state, because the
|
|
613
|
+
# record's actual state, because the one check that would
|
|
578
614
|
# have refused it was never run. Found live: `Expression::
|
|
579
615
|
# Expression.Admit`, fuzzed against `lib/hecks/grammar`
|
|
580
616
|
# (a domain the property's own hand-verification — Banking,
|
|
@@ -594,22 +630,22 @@ module Hecks
|
|
|
594
630
|
nil
|
|
595
631
|
end
|
|
596
632
|
|
|
597
|
-
#
|
|
598
|
-
# purpose, to
|
|
633
|
+
# The mutation oracle's own pre-dispatch read — scoped, on
|
|
634
|
+
# purpose, to entity-dispatched commands only (a dotted
|
|
599
635
|
# command_name): the one place `append`/`remove`/`multiply`/
|
|
600
|
-
# `clamp` are known to act on an entity's
|
|
636
|
+
# `clamp` are known to act on an entity's own attributes
|
|
601
637
|
# (spec/fixtures/entity_list_mutations' own TaggedList — `tags`
|
|
602
638
|
# a value-object list, `count` a VO-typed scalar), never on
|
|
603
|
-
#
|
|
639
|
+
# another nested entity list — so this never needs to reproduce
|
|
604
640
|
# `MutationApplier#entity_element`'s own auto-mint/collision logic
|
|
605
|
-
# (item 1's own fix) at all. An aggregate-level command whose
|
|
606
|
-
# mutation appends an
|
|
607
|
-
# LogVisit`) is a
|
|
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
|
|
608
644
|
# collision property, not this one.
|
|
609
645
|
#
|
|
610
646
|
# `nil` for anything out of scope: an aggregate-level command, an
|
|
611
647
|
# entity command with no mutations at all, or one whose identity
|
|
612
|
-
# args (parent
|
|
648
|
+
# args (parent or element) don't resolve.
|
|
613
649
|
# Same shape as build_guard_check just above: one early-return chain
|
|
614
650
|
# resolving the entity/element this step's args address (see the
|
|
615
651
|
# comment above), each step depending on the previous one's
|
|
@@ -618,6 +654,14 @@ module Hecks
|
|
|
618
654
|
# rubocop:disable-next Metrics/AbcSize
|
|
619
655
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
620
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
|
|
621
665
|
def build_mutation_trace(runtime, verb, args)
|
|
622
666
|
domain_name, aggregate_name, command_name = Naming.split_verb(verb)
|
|
623
667
|
return nil unless command_name&.include?(".")
|
|
@@ -660,12 +704,22 @@ module Hecks
|
|
|
660
704
|
nil
|
|
661
705
|
end
|
|
662
706
|
|
|
663
|
-
#
|
|
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
|
|
664
711
|
# position (an append could have changed the array's own length
|
|
665
712
|
# or order relative to it). `nil` if it somehow vanished (not
|
|
666
713
|
# expected for any op this fixture declares — none of them
|
|
667
714
|
# remove the acted-on element itself — but a property comparing
|
|
668
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
|
|
669
723
|
def read_mutation_after(runtime, trace)
|
|
670
724
|
aggregate = runtime.registry.bluebook(trace[:domain])&.aggregate(trace[:aggregate])
|
|
671
725
|
return nil unless aggregate
|
|
@@ -681,10 +735,10 @@ module Hecks
|
|
|
681
735
|
nil
|
|
682
736
|
end
|
|
683
737
|
|
|
684
|
-
#
|
|
738
|
+
# The fan-out oracle — one finding per (event, for_each policy) this
|
|
685
739
|
# step's own announced events could have triggered, independent of
|
|
686
|
-
# `PolicyInterpreter#deliver_for_each`: the
|
|
687
|
-
# every given/ensures already runs through, but the
|
|
740
|
+
# `PolicyInterpreter#deliver_for_each`: the same `where` evaluator
|
|
741
|
+
# every given/ensures already runs through, but the query answered
|
|
688
742
|
# by `Ports::Query::InMemory.holds?` directly against the live
|
|
689
743
|
# repository (`Replay.run_filter`'s own idiom), never by calling
|
|
690
744
|
# `QueryInterpreter` — sharing that call would make this oracle
|
|
@@ -696,10 +750,19 @@ module Hecks
|
|
|
696
750
|
# apart. Recomputed once per event, not once per policy-and-event,
|
|
697
751
|
# because a `Chapter` de-duplicates on nothing this loop cannot
|
|
698
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
|
|
699
762
|
def fan_out_findings(runtime, snapshot, announced, reactions_since)
|
|
700
763
|
announced.each_with_object([]) do |event, findings|
|
|
701
764
|
# `event.aggregate` is domain-qualified ("Banking::Account" —
|
|
702
|
-
# see command_rules/emission.rb's own Event.new) — the
|
|
765
|
+
# see command_rules/emission.rb's own Event.new) — the same
|
|
703
766
|
# source `PolicyInterpreter#policies_for` reads, split the
|
|
704
767
|
# same two ways: `Naming.demodulise` for the emitting
|
|
705
768
|
# aggregate's bare name, plain `split("::")` for the domain.
|
|
@@ -718,6 +781,19 @@ module Hecks
|
|
|
718
781
|
end
|
|
719
782
|
end
|
|
720
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}`
|
|
721
797
|
def fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
|
|
722
798
|
payload = event.payload.transform_keys(&:to_sym)
|
|
723
799
|
held = policy.where.to_s.empty? ||
|
|
@@ -731,8 +807,8 @@ module Hecks
|
|
|
731
807
|
{ policy: policy.name, on: event.name, expected_row_ids: expected, actual_row_ids: actual }
|
|
732
808
|
end
|
|
733
809
|
|
|
734
|
-
#
|
|
735
|
-
# answered against the
|
|
810
|
+
# The independent recomputation — `policy.for_each`'s declared query,
|
|
811
|
+
# answered against the pre-dispatch snapshot (see the snapshot's
|
|
736
812
|
# own comment at its capture site: the real fan-out's query runs
|
|
737
813
|
# synchronously, before its own dispatched commands can mutate
|
|
738
814
|
# anything the query would have matched, so this has to read the
|
|
@@ -743,6 +819,19 @@ module Hecks
|
|
|
743
819
|
# the triggering event's own payload (the same binding
|
|
744
820
|
# `OpenForCustomer`'s `customer_id: :customer_id` relies on); a
|
|
745
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
|
|
746
835
|
def expected_fan_out_rows(runtime, snapshot, policy, domain, payload)
|
|
747
836
|
query_domain, aggregate_name, query_name = policy.for_each_route(domain)
|
|
748
837
|
aggregate = runtime.registry.bluebook(query_domain)&.aggregate(aggregate_name)
|
|
@@ -760,38 +849,52 @@ module Hecks
|
|
|
760
849
|
matched.keys.map(&:to_s).sort
|
|
761
850
|
end
|
|
762
851
|
|
|
763
|
-
#
|
|
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
|
|
764
868
|
# kernel/cli.rs's own `run_filter`, deliberately calling the exact
|
|
765
|
-
#
|
|
869
|
+
# same production module that method's Rust port stands in for
|
|
766
870
|
# (`Ports::Query::InMemory`, lib/hecks/ports/query/in_memory.rb)
|
|
767
871
|
# rather than re-deriving comparator behavior by hand. `field` walks
|
|
768
872
|
# through `QuerySpecification::FieldPath.dig` (the same reading a
|
|
769
873
|
# declared where-clause gets), `comparable`/`holds?` are the same
|
|
770
874
|
# two calls `InMemory.execute` itself makes per candidate record —
|
|
771
875
|
# this is that method's own filter/select step, inlined, because
|
|
772
|
-
# there is no
|
|
876
|
+
# there is no declared `Query` object here to hand `execute` (an ad
|
|
773
877
|
# hoc filter has no `order_by`/`limit`/`offset` at all, so nothing
|
|
774
878
|
# about `execute`'s own ordering/paging logic even applies).
|
|
775
879
|
# Sorted by id ascending regardless — `Ports::Query::Ordering`'s own
|
|
776
880
|
# header explains why an ask with no declared order still needs
|
|
777
881
|
# this tier ("the identity tier is what makes an ask total").
|
|
778
|
-
#
|
|
779
|
-
#
|
|
780
|
-
#
|
|
781
|
-
#
|
|
782
|
-
#
|
|
783
|
-
#
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
|
788
891
|
def run_filter(runtime, filter)
|
|
789
892
|
aggregate_ref = filter["aggregate"].to_s
|
|
790
893
|
field = filter["field"].to_s
|
|
791
894
|
op = filter["op"].to_s
|
|
792
895
|
value = filter["value"]
|
|
793
896
|
|
|
794
|
-
# A malformed ad-hoc ask is a
|
|
897
|
+
# A malformed ad-hoc ask is a fault (C8.3), not a bare RuntimeError.
|
|
795
898
|
unless FILTER_COMPARATORS.include?(op)
|
|
796
899
|
raise Bluebook::Expression::EvaluationError, "unknown query comparator #{op.inspect}"
|
|
797
900
|
end
|
|
@@ -810,12 +913,17 @@ module Hecks
|
|
|
810
913
|
matched.sort_by { |record| record.id.to_s }.map { |record| { id: record.id }.merge(record.state) }
|
|
811
914
|
end
|
|
812
915
|
|
|
813
|
-
#
|
|
814
|
-
#
|
|
815
|
-
#
|
|
816
|
-
#
|
|
817
|
-
# of any of them being
|
|
818
|
-
# the same way that Rust
|
|
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"`
|
|
819
927
|
def filter_label(filter) = "filter #{filter['aggregate']}.#{filter['field']} #{filter['op']}"
|
|
820
928
|
end
|
|
821
929
|
end
|