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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "../nondeterministic"
|
|
2
|
+
|
|
1
3
|
module Hecks
|
|
2
4
|
module Fuzzing
|
|
3
5
|
module Properties
|
|
@@ -12,14 +14,18 @@ module Hecks
|
|
|
12
14
|
# trustworthy in the first place.
|
|
13
15
|
module LifecycleAndReplay
|
|
14
16
|
# Every lifecycle field a replay leaves an instance holding is one
|
|
15
|
-
# of the aggregate's
|
|
17
|
+
# of the aggregate's own declared states — the full set, not just
|
|
16
18
|
# `Lifecycle#states`' default+targets (see ModelCheck.full_states'
|
|
17
19
|
# own comment on that hole). The tie to M2 is direct: the model
|
|
18
|
-
# checker proves which states a domain's
|
|
19
|
-
# produce ; this proves a
|
|
20
|
+
# checker proves which states a domain's own declarations can ever
|
|
21
|
+
# produce ; this proves a real run never produced anything else —
|
|
20
22
|
# a coercion bug, a stale string surviving a rename, a default
|
|
21
23
|
# that drifted from the declared set, would all show up here as a
|
|
22
24
|
# value nothing upstream would have predicted.
|
|
25
|
+
#
|
|
26
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
27
|
+
# @return [true, String] true if every instance's lifecycle field holds a declared
|
|
28
|
+
# state; otherwise a semicolon-joined message naming each offending instance
|
|
23
29
|
def lifecycle_values_are_declared(history)
|
|
24
30
|
bluebook = history.fetch(:bluebook)
|
|
25
31
|
declared = {}
|
|
@@ -44,14 +50,19 @@ module Hecks
|
|
|
44
50
|
end
|
|
45
51
|
|
|
46
52
|
# Every saga advance a replay actually logged moved along an edge
|
|
47
|
-
# the process manager
|
|
53
|
+
# the process manager declared — `(from, to)` pairs that appear in
|
|
48
54
|
# `saga_log` with `advanced: true` must be a `(handler.from_state,
|
|
49
55
|
# handler.to_state)` pair some handler on that PM declares
|
|
50
|
-
# (compensation edges included ; a
|
|
56
|
+
# (compensation edges included ; a refused-triggered advance is a
|
|
51
57
|
# handler like any other). A saga that advanced along a pair no
|
|
52
58
|
# handler names would mean the runtime moved state the language
|
|
53
59
|
# never authorized — the same trust ModelCheck's static reachability
|
|
54
60
|
# rests on, checked here against what a run actually did.
|
|
61
|
+
#
|
|
62
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
63
|
+
# @return [true, String] true if every logged advance matches a declared
|
|
64
|
+
# handler edge; otherwise a message naming the process manager and pair
|
|
65
|
+
# that does not
|
|
55
66
|
def saga_advances_follow_declared_handlers(history)
|
|
56
67
|
bluebook = history.fetch(:bluebook)
|
|
57
68
|
edges = Hash.new { |h, k| h[k] = [] }
|
|
@@ -72,56 +83,50 @@ module Hecks
|
|
|
72
83
|
offenders.empty? || offenders.join("; ")
|
|
73
84
|
end
|
|
74
85
|
|
|
75
|
-
#
|
|
86
|
+
# The foundational one. `Hecks::Runtime` mints nothing — every
|
|
76
87
|
# identity is declared and derived, never invented (see
|
|
77
|
-
# command_interpreter.rb's own "
|
|
88
|
+
# command_interpreter.rb's own "nothing is minted" — a random hex,
|
|
78
89
|
# a counter, anything not reproducible from the payload, was
|
|
79
90
|
# refused out of the runtime specifically because it broke this).
|
|
80
|
-
# So the
|
|
81
|
-
#
|
|
91
|
+
# So the same steps, replayed against a fresh boot, must produce
|
|
92
|
+
# byte-identical events, refusals, and instances — any drift here
|
|
82
93
|
# is nondeterminism the runtime promised not to have: a wall-clock
|
|
83
94
|
# read that leaked into compared state, a Hash iteration order a
|
|
84
95
|
# comparison depended on, anything. Two independent replays, not a
|
|
85
|
-
# cached one compared to itself, so a bug that corrupts the
|
|
96
|
+
# cached one compared to itself, so a bug that corrupts the first
|
|
86
97
|
# run's own bookkeeping cannot pass by agreeing with itself.
|
|
98
|
+
#
|
|
99
|
+
# @param domain_path [String] path to the domain directory to boot, such as
|
|
100
|
+
# `"examples/pizzas"`
|
|
101
|
+
# @param steps [Array<Hash>] the step list to replay twice
|
|
102
|
+
# @param adapter [Symbol] persistence adapter to boot with (`:memory`,
|
|
103
|
+
# `:postgres`, or `:postgres_era`)
|
|
104
|
+
# @return [true, String] true if both replays produce identical histories
|
|
105
|
+
# (after stripping declared nondeterministic fields); otherwise a message
|
|
106
|
+
# naming the step count that diverged
|
|
87
107
|
def replay_is_deterministic(domain_path, steps, adapter: :memory)
|
|
88
108
|
first = Replay.call(domain_path, steps, adapter: adapter)
|
|
89
109
|
second = Replay.call(domain_path, steps, adapter: adapter)
|
|
90
110
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
# `row[:event][:occurred_at]` — the outbox row's own `event:` field
|
|
101
|
-
# is `Outbox.serialize_event`'s `event.to_h.merge(correlation:...)`,
|
|
102
|
-
# the FULL `Event#to_h`, unlike `history[:events]` (this file's own
|
|
103
|
-
# `events = runtime.events.map { {name:, aggregate:, id:, payload:}
|
|
104
|
-
# }`, above) which has ALWAYS deliberately left `occurred_at` OFF
|
|
105
|
-
# the compared surface for exactly this reason: a wall-clock read,
|
|
106
|
-
# never reproducible byte-for-byte between two independent
|
|
107
|
-
# replaying processes a second apart. Stripped here the same way,
|
|
108
|
-
# for the one place it newly reappears.
|
|
109
|
-
#
|
|
110
|
-
# Both stripped from each `outbox_traces` row before comparing
|
|
111
|
-
# rather than dropping `outbox_traces` wholesale: everything else
|
|
112
|
-
# on a row (status, consumer, kind, the event's own name/
|
|
113
|
-
# aggregate/id/payload) IS reproducible from the steps alone and
|
|
111
|
+
# What leaves this comparison, and why, is declared once in
|
|
112
|
+
# `Nondeterministic::FIELDS`: the `outbox_row` group (per-enqueue
|
|
113
|
+
# uuids), the `event` group on each row's full `Event#to_h`
|
|
114
|
+
# (`occurred_at`, which `history[:events]` never carried), and the
|
|
115
|
+
# `history` group (live IR objects). Stripped per `outbox_traces`
|
|
116
|
+
# row rather than dropping `outbox_traces` wholesale: everything
|
|
117
|
+
# else on a row (status, consumer, kind, the event's own name/
|
|
118
|
+
# aggregate/id/payload) is reproducible from the steps alone and
|
|
114
119
|
# stays checked.
|
|
115
120
|
strip_outbox_nondeterminism = lambda do |history|
|
|
116
121
|
traces = Array(history[:outbox_traces]).map do |trace|
|
|
117
122
|
trace.merge(rows: trace[:rows].map do |row|
|
|
118
|
-
|
|
123
|
+
Nondeterministic.strip(row, :outbox_row).merge(event: Nondeterministic.strip(row[:event], :event))
|
|
119
124
|
end)
|
|
120
125
|
end
|
|
121
126
|
history.merge(outbox_traces: traces)
|
|
122
127
|
end
|
|
123
128
|
|
|
124
|
-
comparable = ->(history) { strip_outbox_nondeterminism.call(history)
|
|
129
|
+
comparable = ->(history) { Nondeterministic.strip(strip_outbox_nondeterminism.call(history), :history) }
|
|
125
130
|
return true if comparable.call(first) == comparable.call(second)
|
|
126
131
|
|
|
127
132
|
"two replays of the same #{steps.length} steps produced different histories"
|
|
@@ -3,65 +3,73 @@ require_relative "../../bluebook/expression/evaluator"
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Fuzzing
|
|
5
5
|
module Properties
|
|
6
|
-
#
|
|
6
|
+
# The transactional outbox's own contract (`Runtime::Outbox`, that
|
|
7
7
|
# file's own header), held to the history a replay actually produced
|
|
8
8
|
# rather than trusted. Two facts, each independently checkable from
|
|
9
9
|
# `history[:outbox_traces]` (`Replay#call`'s own before/after
|
|
10
10
|
# capture, one entry per step whose dispatch enqueued at least one
|
|
11
11
|
# row):
|
|
12
12
|
#
|
|
13
|
-
#
|
|
13
|
+
# ## The two checks
|
|
14
|
+
#
|
|
15
|
+
# 1. "delivery is inline by default" — a row this replay's own
|
|
14
16
|
# dispatch enqueued must not still be `pending`/`claimed` once
|
|
15
|
-
# that
|
|
17
|
+
# that same call returns (nothing here ever simulates a
|
|
16
18
|
# crash), and must not be `failed` either — `deliver_row`'s
|
|
17
19
|
# own rescue only reaches `failed` for a genuine defect in the
|
|
18
|
-
#
|
|
20
|
+
# relay's own consumer resolution (a row naming a policy/
|
|
19
21
|
# process_manager `run_consumer`'s own independent registry
|
|
20
22
|
# lookup cannot find — `WiringError`), never an ordinary
|
|
21
23
|
# domain refusal (`PolicyInterpreter#deliver`/`SagaInterpreter#
|
|
22
|
-
# advance` both rescue those
|
|
24
|
+
# advance` both rescue those themselves, recording `delivered:
|
|
23
25
|
# false` on the reaction/saga log and letting `run_consumer`
|
|
24
26
|
# return normally). Both checked for every row, `saga:` and
|
|
25
27
|
# `policy:` alike.
|
|
26
28
|
#
|
|
27
|
-
# 2. A `policy:`
|
|
29
|
+
# 2. A `policy:` row specifically — `PolicyInterpreter#deliver`
|
|
28
30
|
# returns `nil` (no `reaction_log` entry appended at all)
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# every
|
|
31
|
+
# exactly when its own `where` (or, for a fan-out policy, the
|
|
32
|
+
# same `where`, gating the whole `for_each`) does not hold;
|
|
33
|
+
# every other outcome (delivered, refused, a defect,
|
|
32
34
|
# reaction-depth-reached) is still a non-nil record `#react`
|
|
33
|
-
# appends. So a `delivered` policy row with
|
|
34
|
-
# `reaction_log` entry is legitimate
|
|
35
|
-
# own `where`, independently
|
|
35
|
+
# appends. So a `delivered` policy row with no matching
|
|
36
|
+
# `reaction_log` entry is legitimate only when that policy's
|
|
37
|
+
# own `where`, independently re-evaluated here against the
|
|
36
38
|
# row's own recorded event, genuinely does not hold. A
|
|
37
|
-
# `for_each` policy's own fan-out correctness (how
|
|
39
|
+
# `for_each` policy's own fan-out correctness (how many rows
|
|
38
40
|
# it should have dispatched to) is `fanout_dispatches_once_
|
|
39
41
|
# per_matching_row`'s job, not this one's.
|
|
40
42
|
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# whether a CORRELATION resolves or a LIVE INSTANCE exists, and
|
|
43
|
+
# ## Why a `saga:` row is exempt from check 2
|
|
44
|
+
#
|
|
45
|
+
# Deliberately: `Fanout.sagas`' own `listens?` (starts_on/ends_on/
|
|
46
|
+
# handler_for matching the event name alone) says nothing about
|
|
47
|
+
# whether a correlation resolves or a live instance exists, and
|
|
47
48
|
# `begin_saga`/`end_saga` (saga_interpreter.rb) both have silent,
|
|
48
|
-
# perfectly ordinary no-op paths that append
|
|
49
|
+
# perfectly ordinary no-op paths that append nothing to `saga_log`
|
|
49
50
|
# — `begin_saga` when an instance under that correlation already
|
|
50
|
-
# exists, `end_saga` when
|
|
51
|
+
# exists, `end_saga` when no live instance exists to end (an
|
|
51
52
|
# `AccountOpened` fired by opening an account directly, bypassing
|
|
52
53
|
# the onboarding flow whose `ends_on` names that same event,
|
|
53
54
|
# reproduces this exactly: `Fanout.listens?` enqueues the row
|
|
54
|
-
# because the event
|
|
55
|
+
# because the event name matches `ends_on`, `end_saga` finds
|
|
55
56
|
# nothing under that correlation to delete, and neither logs a
|
|
56
57
|
# word). A `saga:` row draining to `delivered` with zero matching
|
|
57
|
-
# `saga_log` entries is therefore
|
|
58
|
+
# `saga_log` entries is therefore not a finding — only check 1
|
|
58
59
|
# applies to it.
|
|
59
60
|
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
61
|
+
# ## Not a grammar construct
|
|
62
|
+
#
|
|
63
|
+
# `FEATURE_COVERAGE`'s own `dry_runs_leave_no_trace` precedent: the
|
|
64
|
+
# outbox is a runtime door (`Runtime::Outbox`), not a word a
|
|
65
|
+
# bluebook declares, so there is no feature string here to claim.
|
|
64
66
|
module Outbox
|
|
67
|
+
# Checks every outbox row a replay's history recorded against the
|
|
68
|
+
# outbox's own contract (see this file's own header).
|
|
69
|
+
#
|
|
70
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
71
|
+
# @return [true, String] true if every outbox row satisfies the contract; otherwise
|
|
72
|
+
# a semicolon-joined message naming each offending row
|
|
65
73
|
def outbox_rows_match_reactions(history)
|
|
66
74
|
bluebooks = history.fetch(:bluebooks, {})
|
|
67
75
|
|
|
@@ -72,6 +80,17 @@ module Hecks
|
|
|
72
80
|
offenders.empty? || offenders.join("; ")
|
|
73
81
|
end
|
|
74
82
|
|
|
83
|
+
# Checks one outbox row against the outbox's own contract.
|
|
84
|
+
#
|
|
85
|
+
# @param row [Hash] one `Runtime::Outbox::Row#to_h` entry, `trace[:rows]`'s shape:
|
|
86
|
+
# at least `:status`, `:delivery_id`, `:consumer`, `:event`, and (when failed)
|
|
87
|
+
# `:error`
|
|
88
|
+
# @param trace [Hash] this row's outbox trace entry, one of `history[:outbox_traces]`,
|
|
89
|
+
# carrying `:rows` and `:reactions`
|
|
90
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
91
|
+
# keyed by domain name
|
|
92
|
+
# @return [Array<String>] zero or one offending message naming what this row got
|
|
93
|
+
# wrong; empty when the row is fine or its status names no check
|
|
75
94
|
def outbox_row_offenders(row, trace, bluebooks)
|
|
76
95
|
on = row.dig(:event, :name)
|
|
77
96
|
|
|
@@ -90,9 +109,20 @@ module Hecks
|
|
|
90
109
|
end
|
|
91
110
|
end
|
|
92
111
|
|
|
112
|
+
# Checks one delivered `policy:` outbox row for a missing `reaction_log` entry.
|
|
113
|
+
#
|
|
93
114
|
# See this file's own header for why a `saga:` row is exempt: its
|
|
94
115
|
# own `listens?` gives no such guarantee, unlike a policy's single,
|
|
95
116
|
# deterministic `where` gate.
|
|
117
|
+
#
|
|
118
|
+
# @param row [Hash] the delivered outbox row being checked
|
|
119
|
+
# @param on [String] the row's event name, `row.dig(:event, :name)`
|
|
120
|
+
# @param trace [Hash] this row's outbox trace entry, carrying `:reactions`
|
|
121
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
122
|
+
# keyed by domain name
|
|
123
|
+
# @return [Array<String>] zero or one offending message; empty when the row is
|
|
124
|
+
# not a policy row, the policy is undeclared or fans out, or the policy's
|
|
125
|
+
# `where` does not independently re-evaluate true
|
|
96
126
|
def outbox_delivered_policy_offenders(row, on, trace, bluebooks)
|
|
97
127
|
kind, fqn = row[:consumer].to_s.split(":", 2)
|
|
98
128
|
return [] unless kind == "policy"
|
|
@@ -101,11 +131,14 @@ module Hecks
|
|
|
101
131
|
return [] if trace[:reactions].any? { |entry| entry[:policy] == name && entry[:on] == on }
|
|
102
132
|
|
|
103
133
|
policy = bluebooks[home]&.policies&.find { |candidate| candidate.name == name }
|
|
104
|
-
|
|
105
|
-
return []
|
|
134
|
+
# Nothing declared under this name — inconclusive, not a claimed mismatch.
|
|
135
|
+
return [] unless policy
|
|
136
|
+
# Fan-out row count is fanout_dispatches_once_per_matching_row's job.
|
|
137
|
+
return [] if policy.fans_out?
|
|
106
138
|
|
|
107
139
|
held = independently_re_evaluate_policy_where(policy, row[:event])
|
|
108
|
-
|
|
140
|
+
# False, or inconclusive (the where itself raised) — never a claimed mismatch.
|
|
141
|
+
return [] if held != true
|
|
109
142
|
|
|
110
143
|
["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) drained as delivered, but no matching " \
|
|
111
144
|
"reaction_log entry exists and the policy's own where clause independently re-evaluates true — " \
|
|
@@ -117,17 +150,22 @@ module Hecks
|
|
|
117
150
|
# with itself (the same rule `resolve_dispatch_binding`'s own
|
|
118
151
|
# comment states). `Evaluator.call` (the raw-string entry, parsed
|
|
119
152
|
# and cached — never `call_rule`, which needs the policy's own
|
|
120
|
-
#
|
|
153
|
+
# build-time `where_rule` AST, an object this history has no
|
|
121
154
|
# reason to carry) is the exact same call `Replay#fan_out_finding`
|
|
122
155
|
# already makes for the identical fact one property over
|
|
123
156
|
# (`policy.where.to_s.empty? || Evaluator.call(policy.where, {},
|
|
124
157
|
# payload)`), reused rather than re-derived a second, slightly
|
|
125
158
|
# different way. `rescue`d to `nil`, not `false`: a where clause
|
|
126
159
|
# that cannot be re-evaluated from the row's own recorded payload
|
|
127
|
-
# alone is
|
|
160
|
+
# alone is inconclusive, not proof either way — the same "never a
|
|
128
161
|
# claimed pass or a claimed mismatch from a resolution this replay
|
|
129
162
|
# cannot actually reproduce" discipline `build_guard_check`'s own
|
|
130
163
|
# rescue clause already follows.
|
|
164
|
+
#
|
|
165
|
+
# @param policy [Bluebook::Policy] the policy whose `where` clause is re-evaluated
|
|
166
|
+
# @param event [Hash] the outbox row's own recorded event, read for `:payload`
|
|
167
|
+
# @return [Boolean, nil] whether `policy`'s `where` holds against `event`'s payload,
|
|
168
|
+
# or nil if the where clause cannot be re-evaluated from it
|
|
131
169
|
def independently_re_evaluate_policy_where(policy, event)
|
|
132
170
|
return true if policy.where.to_s.empty?
|
|
133
171
|
|
|
@@ -10,7 +10,7 @@ module Hecks
|
|
|
10
10
|
# (#query_for_verb, #query_eligible_rows, #resolve_hop_clause) other
|
|
11
11
|
# property modules in this directory also call.
|
|
12
12
|
module Querying
|
|
13
|
-
#
|
|
13
|
+
# The query oracle — differential testing within the one runtime,
|
|
14
14
|
# the shape the retired cross-runtime harness should always have
|
|
15
15
|
# been. Every generated ask was answered twice at the same instant
|
|
16
16
|
# (Replay records both): once through whatever the aggregate is
|
|
@@ -18,23 +18,27 @@ module Hecks
|
|
|
18
18
|
# a SQL binding would compile it), once through the reference
|
|
19
19
|
# interpreter's own evaluation. The two are separate, live
|
|
20
20
|
# implementations of the same comparator vocabulary, and they have
|
|
21
|
-
# drifted before — an adapter that
|
|
21
|
+
# drifted before — an adapter that accepts what the reference says
|
|
22
22
|
# matches nothing, or orders what it refuses to order, shows up
|
|
23
23
|
# here as a finding no self-referential adapter spec could see.
|
|
24
|
-
# M23 — `Replay`
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
24
|
+
# M23 — `Replay` runs the native and reference engines independently
|
|
25
|
+
# (each in its own begin/rescue — see that file's own comment at the
|
|
26
|
+
# capture site), so this property can tell apart "both engines
|
|
27
|
+
# refused" (fine — the ask was genuinely bad, nothing to compare)
|
|
28
|
+
# from "one refused and the other did not" (a real divergence — the
|
|
29
|
+
# two engines disagree about whether the ask was even valid, never
|
|
30
|
+
# mind what it answers). `native_refused`/`reference_refused` are
|
|
31
|
+
# read by key presence, not truthiness — `Replay` only ever adds
|
|
32
|
+
# `:error`/`:reference_error` to an entry when that side actually
|
|
33
|
+
# raised, so an absent key is an unambiguous "this side answered." A
|
|
34
|
+
# read-model ask (no reference twin attempted at all, `asked[:query]`
|
|
35
|
+
# without "::") is skipped entirely, same as always — there is no
|
|
36
|
+
# second engine to disagree with.
|
|
37
|
+
#
|
|
38
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
39
|
+
# @return [true, String] true if every native/reference query answer, and every
|
|
40
|
+
# refusal of one, agrees; otherwise a message naming the offending query, its
|
|
41
|
+
# args, and how the two engines disagreed
|
|
38
42
|
def query_answers_match_reference(history)
|
|
39
43
|
offenders = history.fetch(:queries).filter_map do |asked|
|
|
40
44
|
next unless asked[:query].is_a?(String) && asked[:query].include?("::")
|
|
@@ -59,16 +63,16 @@ module Hecks
|
|
|
59
63
|
offenders.empty? || offenders.join("; ")
|
|
60
64
|
end
|
|
61
65
|
|
|
62
|
-
#
|
|
66
|
+
# The same "two engines, compared" shape query_answers_match_reference
|
|
63
67
|
# already uses, aimed squarely at Query#options' offset/limit pair —
|
|
64
|
-
# but recomputed from history[:instances] directly, a
|
|
68
|
+
# but recomputed from history[:instances] directly, a third,
|
|
65
69
|
# independent computation, rather than comparing QueryInterpreter's
|
|
66
70
|
# own native and reference paths against each other (which could
|
|
67
71
|
# share the identical bug neither implementation happened to hit —
|
|
68
|
-
# see #4's own fix, which touched
|
|
72
|
+
# see #4's own fix, which touched both #interpret and
|
|
69
73
|
# #reference_interpret at once). `order_by` declared alongside
|
|
70
74
|
# `offset` or `limit` names a genuinely paged query. Ports::Query::
|
|
71
|
-
# Ordering.apply is the
|
|
75
|
+
# Ordering.apply is the same engine QueryInterpreter#ordered calls,
|
|
72
76
|
# reused here rather than re-derived, so this oracle cannot drift
|
|
73
77
|
# from what "in order" means without the interpreter drifting the
|
|
74
78
|
# identical way — only the offset-then-limit .drop/.first slice
|
|
@@ -83,6 +87,11 @@ module Hecks
|
|
|
83
87
|
# `skipped`/`expected` across method boundaries as params/returns
|
|
84
88
|
# for a sequence that's only ever computed once, in this order.
|
|
85
89
|
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
90
|
+
#
|
|
91
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
92
|
+
# @return [true, String] true if every paged query's real answer matches an
|
|
93
|
+
# independent order/offset/limit recomputation over its eligible rows;
|
|
94
|
+
# otherwise a message naming the query, its args, and the two answers
|
|
86
95
|
def paging_offset_partitions_correctly(history)
|
|
87
96
|
bluebooks = history.fetch(:bluebooks)
|
|
88
97
|
|
|
@@ -112,13 +121,20 @@ module Hecks
|
|
|
112
121
|
offenders.empty? || offenders.join("; ")
|
|
113
122
|
end
|
|
114
123
|
|
|
115
|
-
#
|
|
124
|
+
# The declared Query itself, resolved from a replayed verb — the
|
|
116
125
|
# same shape #command_for_verb resolves a command by, one
|
|
117
126
|
# construct over. Entity-level queries (a dotted query_path) are
|
|
118
127
|
# out of scope here — paging on an entity's own list has no real
|
|
119
128
|
# corpus site yet, and the "one many-side head, one aggregate,
|
|
120
129
|
# no FK-join" shape #query_eligible_rows assumes doesn't hold for
|
|
121
130
|
# one.
|
|
131
|
+
#
|
|
132
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
133
|
+
# keyed by domain name
|
|
134
|
+
# @param verb [String] the asked verb, `"Domain::Aggregate.Query"`
|
|
135
|
+
# @return [Bluebook::Query, nil] the declared query, or nil if `verb` names an
|
|
136
|
+
# entity-level query path, or a domain, aggregate, or query not found among
|
|
137
|
+
# `bluebooks`
|
|
122
138
|
def query_for_verb(bluebooks, verb)
|
|
123
139
|
domain, aggregate_name, query_path = Naming.split_verb(verb)
|
|
124
140
|
return nil unless query_path && !query_path.include?(".")
|
|
@@ -128,27 +144,42 @@ module Hecks
|
|
|
128
144
|
aggregate&.query(query_path)
|
|
129
145
|
end
|
|
130
146
|
|
|
131
|
-
#
|
|
147
|
+
# Independently recomputes which of an aggregate's own stored rows a
|
|
148
|
+
# query's `wheres` admit.
|
|
149
|
+
#
|
|
150
|
+
# A query's own rows — unlike #eligible_rows (a ReadModel's
|
|
132
151
|
# reduced/grouped many-side head, possibly FK-joined against a
|
|
133
|
-
# root), a Query always asks about its
|
|
152
|
+
# root), a Query always asks about its own owning aggregate
|
|
134
153
|
# directly ; no join, no reference_target. `id:` merged in the
|
|
135
154
|
# same way #eligible_rows' own rows are, since a stable sort
|
|
136
155
|
# (Ordering.apply's own `identity:`) and the real answer's own
|
|
137
156
|
# `record.state.merge(id: record.id)` both need it.
|
|
138
|
-
# `bluebooks:` — needed
|
|
157
|
+
# `bluebooks:` — needed only to recognise and resolve a `/` hop
|
|
139
158
|
# clause (`engagement/client/status`, hop_chain.bluebook's own
|
|
140
|
-
# PricedAboveViaEngagement): a hop's head names one of the
|
|
159
|
+
# PricedAboveViaEngagement): a hop's head names one of the owning
|
|
141
160
|
# aggregate's declared references, and only the declaration graph
|
|
142
161
|
# can say which attribute that is and which aggregate it targets.
|
|
143
162
|
# A local clause never consults it. Latent gap this closed, found
|
|
144
163
|
# by the fuzzer itself the first time a generated sequence ever
|
|
145
|
-
# built a full hop chain
|
|
164
|
+
# built a full hop chain and had its paged query answer a row
|
|
146
165
|
# (seed 1, the moment scalar_value_objects.bluebook joined the
|
|
147
166
|
# fixtures corpus and shifted every seeded draw): the recompute
|
|
148
|
-
# dug `engagement/client/status` as a
|
|
167
|
+
# dug `engagement/client/status` as a local dotted path, found
|
|
149
168
|
# nil, and declared every genuinely-eligible row ineligible — a
|
|
150
169
|
# false property violation against a correct runtime answer,
|
|
151
170
|
# reproducible on an untouched main with this same 4-step script.
|
|
171
|
+
#
|
|
172
|
+
# @param instances [Hash{String => Hash}] `history[:instances]`, or an
|
|
173
|
+
# `:instances_at` snapshot from a replayed query entry
|
|
174
|
+
# @param domain [String] the domain name the target aggregate belongs to
|
|
175
|
+
# @param aggregate_name [String] the target aggregate's own declared name
|
|
176
|
+
# @param wheres [Array<QuerySpecification::Common::WhereClause>] the clauses
|
|
177
|
+
# every returned row must satisfy
|
|
178
|
+
# @param args [Hash] the query's own call args, for a clause whose value is a
|
|
179
|
+
# Symbol naming one
|
|
180
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain, keyed
|
|
181
|
+
# by domain name; needed only to resolve a `/` hop clause
|
|
182
|
+
# @return [Array<Hash>] each admitted row's own state, merged with its `id:`
|
|
152
183
|
def query_eligible_rows(instances, domain, aggregate_name, wheres, args, bluebooks: {})
|
|
153
184
|
aggregate = bluebooks[domain]&.aggregate(aggregate_name)
|
|
154
185
|
prefix = "#{domain}::#{aggregate_name}#"
|
|
@@ -166,6 +197,9 @@ module Hecks
|
|
|
166
197
|
end
|
|
167
198
|
end
|
|
168
199
|
|
|
200
|
+
# Resolves one hop of a `/`-chained clause into a local `in` clause against
|
|
201
|
+
# the target aggregate's own ids.
|
|
202
|
+
#
|
|
169
203
|
# `Runtime::ReferenceHop#fold`, independently restated over the
|
|
170
204
|
# replay's own `:instances_at` snapshot instead of live
|
|
171
205
|
# repositories — the same shape every other recompute in this
|
|
@@ -173,13 +207,27 @@ module Hecks
|
|
|
173
207
|
# would be checking the runtime against itself). One hop peels
|
|
174
208
|
# off the head (`HopPath.next_hop`, the identical one-step
|
|
175
209
|
# primitive the live fold uses), the inner clause recurses
|
|
176
|
-
# through `query_eligible_rows` against the
|
|
210
|
+
# through `query_eligible_rows` against the target's own
|
|
177
211
|
# snapshot rows (so a multi-hop tail resolves hop by hop, exactly
|
|
178
212
|
# as the live path's own recursion does), and the ids that
|
|
179
213
|
# answered fold back as the same local `in` membership clause the
|
|
180
214
|
# live fold builds. A clause with no `/`, or one whose head this
|
|
181
215
|
# aggregate's declarations cannot resolve, passes through
|
|
182
216
|
# untouched and evaluates locally as it always did.
|
|
217
|
+
#
|
|
218
|
+
# @param instances [Hash{String => Hash}] `history[:instances]`, or an
|
|
219
|
+
# `:instances_at` snapshot from a replayed query entry
|
|
220
|
+
# @param domain [String] the domain name `aggregate` belongs to
|
|
221
|
+
# @param aggregate [Bluebook::Aggregate, nil] the aggregate `clause` is
|
|
222
|
+
# evaluated against; nil skips hop resolution entirely
|
|
223
|
+
# @param clause [QuerySpecification::Common::WhereClause] the clause to resolve
|
|
224
|
+
# @param args [Hash] the query's own call args, passed through to the inner
|
|
225
|
+
# hop's own `query_eligible_rows` recursion
|
|
226
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain, keyed
|
|
227
|
+
# by domain name
|
|
228
|
+
# @return [QuerySpecification::Common::WhereClause] `clause` unchanged if it has
|
|
229
|
+
# no resolvable hop head; otherwise a local `in` clause over the matching
|
|
230
|
+
# target ids
|
|
183
231
|
def resolve_hop_clause(instances, domain, aggregate, clause, args, bluebooks)
|
|
184
232
|
return clause unless aggregate && QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes)
|
|
185
233
|
|
|
@@ -194,9 +242,15 @@ module Hecks
|
|
|
194
242
|
QuerySpecification::Common::WhereClause.new(field: hop.attribute.name, op: "in", value: ids)
|
|
195
243
|
end
|
|
196
244
|
|
|
245
|
+
# Resolves a declared limit/offset value against the query's own call args.
|
|
246
|
+
#
|
|
197
247
|
# `QueryInterpreter#resolve_query_value`, reproduced: a declared
|
|
198
248
|
# limit/offset is either a literal or a Symbol naming an argument
|
|
199
249
|
# the caller supplied.
|
|
250
|
+
#
|
|
251
|
+
# @param value [Integer, Symbol] the declared limit/offset value
|
|
252
|
+
# @param args [Hash] the query's own call args
|
|
253
|
+
# @return [Object] `value` unchanged if not a Symbol; otherwise `args[value]`
|
|
200
254
|
def resolve_paging_value(value, args)
|
|
201
255
|
value.is_a?(Symbol) ? args[value] : value
|
|
202
256
|
end
|