hecks 1.2.0 → 1.4.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 +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -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/postgres_era.adapter +5 -0
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- 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.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +51 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -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 +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- 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 +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- 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 +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- 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 +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +301 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- 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 +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -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 -20
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +124 -0
- data/lib/hecks/fuzzing/form_census.rb +199 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
- data/lib/hecks/fuzzing/properties/guards.rb +129 -26
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
- data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +84 -39
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +224 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +676 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +39 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
- data/lib/hecks/fuzzing/value_generator.rb +66 -14
- data/lib/hecks/fuzzing.rb +11 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- 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 +365 -15
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +89 -21
- data/lib/hecks/ports/access_control.rb +58 -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 +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- 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 +160 -58
- 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 +28 -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 +44 -6
- 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 +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
- 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 +42 -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 +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -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 +29 -6
- 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 +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +250 -0
- data/lib/hecks/projections/glossary/markdown.rb +105 -0
- data/lib/hecks/projections/glossary/mermaid.rb +110 -0
- data/lib/hecks/projections/glossary/page.css +271 -0
- data/lib/hecks/projections/glossary/page.js +72 -0
- data/lib/hecks/projections/glossary/sections.rb +17 -0
- data/lib/hecks/projections/glossary/sentences.rb +205 -0
- data/lib/hecks/projections/glossary.rb +214 -286
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +15 -22
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +132 -24
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- 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 +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- 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 +94 -19
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +282 -46
- data/lib/hecks/runtime/entity_interpreter.rb +160 -69
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +111 -56
- data/lib/hecks/runtime/reaction_invocation.rb +76 -9
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -112
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +56 -27
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -88
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +93 -56
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +379 -226
- data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
- data/lib/hecks/runtime/value.rb +28 -23
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +207 -5
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +43 -2
|
@@ -12,18 +12,18 @@ module Hecks
|
|
|
12
12
|
# verb-to-declaration lookups (#command_for_verb,
|
|
13
13
|
# #effective_guard_descriptions) each resolves a refusal against.
|
|
14
14
|
module Guards
|
|
15
|
-
# `Query#options`'
|
|
15
|
+
# `Query#options`' other half — TenantScope.apply's own contract
|
|
16
16
|
# (tenant_scope.rb), independently restated as a property rather
|
|
17
17
|
# than exercised only through whatever the generator happens to
|
|
18
|
-
# try.
|
|
18
|
+
# try. Not closed by the generator here on purpose: SafeDepositBox.
|
|
19
19
|
# Rented — the only real corpus query declaring `authorize` at
|
|
20
|
-
# all — declares
|
|
20
|
+
# all — declares zero attributes of its own, so StepBuilder#args_for
|
|
21
21
|
# always hands it `{}` and TenantScope.apply refuses every
|
|
22
22
|
# generated attempt, unconditionally (confirmed: no successful ask
|
|
23
23
|
# against an authorize-bearing query reaches this property via the
|
|
24
24
|
# standard battery today). Extending the generator to invent a
|
|
25
25
|
# `tenant:` value ran into a separate, real finding along the way —
|
|
26
|
-
# SafeDepositBox is
|
|
26
|
+
# SafeDepositBox is composite-identified (`identified_by` is nil
|
|
27
27
|
# for it — Runtime::Identified#derive_identity), so the generator's
|
|
28
28
|
# existing `known_ids` pool (keyed by `aggregate.identified_by ||
|
|
29
29
|
# "id"`) tracks a stray, never-real scalar for it rather than its
|
|
@@ -37,10 +37,10 @@ module Hecks
|
|
|
37
37
|
# where a `tenant:` arg came from.
|
|
38
38
|
#
|
|
39
39
|
# Two claims, matching TenantScope.apply's own two branches: every
|
|
40
|
-
#
|
|
40
|
+
# successful answer's own tenant field agrees with the tenant arg
|
|
41
41
|
# given (the WhereClause TenantScope injects is a Symbol reference
|
|
42
|
-
# into args, resolved dynamically — this checks the
|
|
43
|
-
# re-deriving that resolution) ; every ask
|
|
42
|
+
# into args, resolved dynamically — this checks the outcome, not
|
|
43
|
+
# re-deriving that resolution) ; every ask missing a required
|
|
44
44
|
# tenant: refuses with the declared wording, never succeeds. A
|
|
45
45
|
# refusal for an unrelated reason with the tenant arg present is
|
|
46
46
|
# not this property's claim either way — skipped, not graded.
|
|
@@ -92,14 +92,14 @@ module Hecks
|
|
|
92
92
|
offenders.empty? || offenders.join("; ")
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
#
|
|
96
|
-
#
|
|
95
|
+
# Every given/ensures refusal a run actually raised names a rule
|
|
96
|
+
# the command actually declares. `GivenNotMet`/`EnsuresNotMet` both
|
|
97
97
|
# quote their guard's own `description` verbatim
|
|
98
98
|
# (command_rules/admissibility.rb: `"#{command.hecks_name} refused
|
|
99
|
-
# — #{given.description}"`) — the
|
|
99
|
+
# — #{given.description}"`) — the same text `behavior.bluebook`'s
|
|
100
100
|
# own `Rule`/Command.Ensure hold as `Rule#description`, so a
|
|
101
101
|
# refusal whose quoted text is not among the refusing command's
|
|
102
|
-
#
|
|
102
|
+
# own `guard_descriptions` (Behaviour::Command, both givens and
|
|
103
103
|
# ensures) is either a stale message surviving a renamed rule, a
|
|
104
104
|
# rule firing against the wrong command's own guard set, or the
|
|
105
105
|
# wording drifting out from under the declaration it is supposed
|
|
@@ -107,7 +107,7 @@ module Hecks
|
|
|
107
107
|
# guards, some through a cross-aggregate dereference) are exactly
|
|
108
108
|
# the surface this exists to hold to its word.
|
|
109
109
|
#
|
|
110
|
-
# `kind:` is what tells a guard refusal apart from the
|
|
110
|
+
# `kind:` is what tells a guard refusal apart from the four other
|
|
111
111
|
# `RefusalWording` templates sharing the identical "X refused — Y"
|
|
112
112
|
# shape (LifecycleRefused/transition_blocked, both TypeMismatch
|
|
113
113
|
# object-reference templates, Unauthorized/role_mismatch) — see
|
|
@@ -138,16 +138,119 @@ module Hecks
|
|
|
138
138
|
offenders.empty? || offenders.join("; ")
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
#
|
|
141
|
+
# Angle-8's own write-side half. `authorize_scopes_or_refuses`
|
|
142
|
+
# (above) enforces `TenantScope.apply`'s boundary, and that
|
|
143
|
+
# boundary exists only for queries/read models — `authorize
|
|
144
|
+
# policy, tenant: field` is a word `QuerySpecification::Common::
|
|
145
|
+
# DSL#authorize_impl` grants to `QueryBuilder`/`ReadModelBuilder`
|
|
146
|
+
# alone; `CommandBuilder` never includes that module, so no
|
|
147
|
+
# bluebook can declare it on a command at all (confirmed by
|
|
148
|
+
# reading the grammar directly, not inferred). A write that
|
|
149
|
+
# carries a `reference_to` from one tenant-scoped record into
|
|
150
|
+
# another's is checked by nothing at dispatch time: `TenantScope`
|
|
151
|
+
# never runs for a command, and no runtime `given`/`ensures`
|
|
152
|
+
# anywhere in this corpus reads a cross-aggregate tenant field
|
|
153
|
+
# either. `qa/stress_domains/tenant_ledger` exists to give this
|
|
154
|
+
# property a real place to fire.
|
|
155
|
+
#
|
|
156
|
+
# The rule: an aggregate's own declared tenant field is whichever
|
|
157
|
+
# field one of its own queries names in `authorize policy, tenant:
|
|
158
|
+
# :field` — the exact same declaration `authorize_scopes_or_
|
|
159
|
+
# refuses` reads off a query above, reused here to name a field
|
|
160
|
+
# on the aggregate itself that stores the tenant it belongs to.
|
|
161
|
+
# For every stored record (`history[:instances]` — a refused
|
|
162
|
+
# dispatch never writes one, so "a refusal is correct behaviour,
|
|
163
|
+
# not a finding" holds by construction, the same way `history
|
|
164
|
+
# [:instances]` already guarantees this for `stored_records_
|
|
165
|
+
# satisfy_declared_invariants`) whose own aggregate declares a
|
|
166
|
+
# tenant field, walk every `reference_to`-typed attribute it
|
|
167
|
+
# carries (`Bluebook::Reference` — "a reference IS the id",
|
|
168
|
+
# value/coercion.rb's own header, so the stored value is always a
|
|
169
|
+
# plain id, never a nested payload) pointing at another aggregate
|
|
170
|
+
# that also declares a tenant field: if the referenced record's
|
|
171
|
+
# own tenant value disagrees with the referencing record's own
|
|
172
|
+
# tenant value, the write crossed a tenant boundary and nothing
|
|
173
|
+
# refused it — a finding.
|
|
174
|
+
#
|
|
175
|
+
# A dangling/unresolvable reference is skipped — a different,
|
|
176
|
+
# existence-shaped property's claim, not this one's (the same
|
|
177
|
+
# "inconclusive, not a claimed pass" restraint `lifecycle_guard_
|
|
178
|
+
# and_given_violations_are_refused` already documents for a
|
|
179
|
+
# differently-shaped case). Comparison goes through `Ports::
|
|
180
|
+
# Query::InMemory.comparable` (the same normalization `authorize_
|
|
181
|
+
# scopes_or_refuses` already applies to a query row's own tenant
|
|
182
|
+
# field, just above) rather than `Runtime::Value#==` directly —
|
|
183
|
+
# two single-attribute value objects with the same scalar but
|
|
184
|
+
# different declared names (`LedgerRegion`/`TransferRegion`, this
|
|
185
|
+
# domain's own pair — a value object is always declared inside
|
|
186
|
+
# the aggregate that owns it, so two independently tenant-scoped
|
|
187
|
+
# aggregates can never share one) compare unequal under `Value#==`
|
|
188
|
+
# (`type_name` is part of that equality) despite meaning the
|
|
189
|
+
# identical tenant, which would make every same-tenant write a
|
|
190
|
+
# false positive.
|
|
191
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
192
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
193
|
+
def commands_respect_tenant_scope(history)
|
|
194
|
+
bluebooks = history.fetch(:bluebooks)
|
|
195
|
+
instances = history.fetch(:instances)
|
|
196
|
+
|
|
197
|
+
offenders = instances.flat_map do |key, state|
|
|
198
|
+
domain_name = key.split("::").first
|
|
199
|
+
aggregate_name = key.split("::").last.split("#").first
|
|
200
|
+
aggregate = bluebooks[domain_name]&.aggregate(aggregate_name)
|
|
201
|
+
next [] unless aggregate
|
|
202
|
+
|
|
203
|
+
own_tenant_field = tenant_field_for(aggregate)
|
|
204
|
+
next [] unless own_tenant_field && state.key?(own_tenant_field)
|
|
205
|
+
|
|
206
|
+
own_tenant = Ports::Query::InMemory.comparable(state[own_tenant_field])
|
|
207
|
+
|
|
208
|
+
aggregate.attributes.filter_map do |attribute|
|
|
209
|
+
next unless attribute.type.is_a?(Bluebook::Reference)
|
|
210
|
+
|
|
211
|
+
target = bluebooks[domain_name]&.aggregate(attribute.type.target_name)
|
|
212
|
+
target_tenant_field = target && tenant_field_for(target)
|
|
213
|
+
next unless target_tenant_field
|
|
214
|
+
|
|
215
|
+
target_id = state[attribute.name]
|
|
216
|
+
next unless target_id
|
|
217
|
+
|
|
218
|
+
target_state = instances["#{domain_name}::#{target.name}##{target_id}"]
|
|
219
|
+
next unless target_state&.key?(target_tenant_field)
|
|
220
|
+
|
|
221
|
+
target_tenant = Ports::Query::InMemory.comparable(target_state[target_tenant_field])
|
|
222
|
+
next if target_tenant == own_tenant
|
|
223
|
+
|
|
224
|
+
"#{key} (#{own_tenant_field}: #{own_tenant.inspect}) references #{attribute.name}: #{target_id.inspect}, " \
|
|
225
|
+
"but #{domain_name}::#{target.name}##{target_id} carries #{target_tenant_field}: " \
|
|
226
|
+
"#{target_tenant.inspect} — a cross-tenant write nothing refused"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
offenders.empty? || offenders.join("; ")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# The field an aggregate's own query names as tenant-scoping —
|
|
234
|
+
# shared by `commands_respect_tenant_scope` above for both sides
|
|
235
|
+
# of a `reference_to`. `nil` for an aggregate with no `authorize
|
|
236
|
+
# ..., tenant:` on any of its own queries — not every aggregate
|
|
237
|
+
# is tenant-scoped, and one that isn't has nothing for this
|
|
238
|
+
# property to check either side of.
|
|
239
|
+
def tenant_field_for(aggregate)
|
|
240
|
+
authorization = aggregate.queries.filter_map(&:authorization).find(&:tenant)
|
|
241
|
+
authorization&.tenant&.to_sym
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# A declared process manager's own command — `command.hecks_name`,
|
|
142
245
|
# or an entity's own if the verb's second component is itself
|
|
143
246
|
# dotted (`Aggregate.Entity.Command`, the same two shapes
|
|
144
247
|
# `Dispatcher#dispatch` itself branches on). Shared by the guard
|
|
145
248
|
# property above and available for anything else that needs to go
|
|
146
249
|
# from a replayed verb back to its declaration.
|
|
147
250
|
#
|
|
148
|
-
#
|
|
251
|
+
# Resolved against `bluebooks` (the full map, `history[:bluebooks]`
|
|
149
252
|
# — every loaded domain, keyed by name), never a single assumed
|
|
150
|
-
# bluebook: a verb names its
|
|
253
|
+
# bluebook: a verb names its own domain (`Naming.split_verb`'s
|
|
151
254
|
# first element), and that domain is not always the one Replay
|
|
152
255
|
# happens to expose as `history[:bluebook]`. A fuzz run against
|
|
153
256
|
# `lib/hecks/grammar` (Expression + Translation, in load
|
|
@@ -155,14 +258,14 @@ module Hecks
|
|
|
155
258
|
# refusal read as "no declared command resolves that verb" purely
|
|
156
259
|
# because `history[:bluebook]` was Expression, not Translation; the
|
|
157
260
|
# refusal was real, this property's own domain resolution was not.
|
|
158
|
-
# A
|
|
261
|
+
# A delegating door refuses with its target's own words. `delegates_to`
|
|
159
262
|
# (CommandBuilder#delegates_to_impl) hands the whole dispatch to one
|
|
160
263
|
# entity command, and that command's given is what refuses — raised
|
|
161
264
|
# back through the door, in the door's name (chess: `Game.MoveKnight
|
|
162
265
|
# refused — "it is that color's turn"`, a given Knight.Move declares
|
|
163
266
|
# and MoveKnight, a pure passthrough, never could). Read the door's
|
|
164
267
|
# own guards first, then every delegation target's; an offence is
|
|
165
|
-
# only a description
|
|
268
|
+
# only a description neither declares. Found live mining chess's
|
|
166
269
|
# history: every refused move through a door read as undeclared.
|
|
167
270
|
def effective_guard_descriptions(bluebooks, verb, command)
|
|
168
271
|
own = command.guard_descriptions
|
|
@@ -193,9 +296,9 @@ module Hecks
|
|
|
193
296
|
end
|
|
194
297
|
end
|
|
195
298
|
|
|
196
|
-
# `guard_refusals_are_declared`'s
|
|
299
|
+
# `guard_refusals_are_declared`'s own opposite direction. That
|
|
197
300
|
# property is passive and one-directional — for a refusal that
|
|
198
|
-
#
|
|
301
|
+
# already happened, is the quoted text real declared text? It says
|
|
199
302
|
# nothing about a guard that should have refused and silently did
|
|
200
303
|
# not — a call site that stopped calling enforce_givens/enforce_
|
|
201
304
|
# lifecycle_guard would never appear in history[:refusals] at all,
|
|
@@ -203,7 +306,7 @@ module Hecks
|
|
|
203
306
|
#
|
|
204
307
|
# This one calls Admissibility#enforce_givens (which itself folds
|
|
205
308
|
# in #enforce_lifecycle_guard whenever `declaring:` is passed)
|
|
206
|
-
#
|
|
309
|
+
# directly, against Replay's own pre-dispatch snapshot
|
|
207
310
|
# (history[:guard_checks], one bounded, additive extension — see
|
|
208
311
|
# that file's own comment at the capture site) — an independent
|
|
209
312
|
# recomputation, not grading production against itself, the same
|
|
@@ -213,27 +316,27 @@ module Hecks
|
|
|
213
316
|
# could mutate anything a cross-aggregate given dereferences) is
|
|
214
317
|
# compared against `actual_refused` (GivenNotMet/LifecycleRefused
|
|
215
318
|
# specifically — Replay's own comment on GUARD_REFUSAL_CLASSES
|
|
216
|
-
# explains why
|
|
319
|
+
# explains why any other refusal class, or an outright success,
|
|
217
320
|
# both count as "the guard did not fire," since enforce_givens
|
|
218
|
-
# runs
|
|
321
|
+
# runs first in DISPATCH_ORDER).
|
|
219
322
|
#
|
|
220
323
|
# Aggregate#preconditions closes for free alongside this — a
|
|
221
324
|
# no-block `given` reference (CommandBuilder#given) pushes the
|
|
222
|
-
#
|
|
325
|
+
# same Given struct object `enforce_givens` already iterates
|
|
223
326
|
# command.givens for, so there is no separate runtime path a
|
|
224
327
|
# property could exercise beyond what this already reaches.
|
|
225
328
|
# Entity#preconditions closes the identical way, one level down
|
|
226
329
|
# (ADR 0028) — a piece's own bare `given` reference pushes the
|
|
227
|
-
#
|
|
330
|
+
# same Given struct onto its own referencing command's givens,
|
|
228
331
|
# so LedgerEntry's own Amend/Reverse (banking) already exercise
|
|
229
332
|
# this through the exact mechanism above, no separate path.
|
|
230
333
|
#
|
|
231
334
|
# Real targets: Account.Debit/CloseAccount (`from:` guards),
|
|
232
335
|
# Credit/Debit (the named-once `given("customer is active")`
|
|
233
336
|
# precondition) — FreezeAccount deliberately references the
|
|
234
|
-
#
|
|
337
|
+
# different named precondition `"customer is not closed"` instead
|
|
235
338
|
# (a suspended customer must still be freezable), so it is not a
|
|
236
|
-
# `"customer is active"` example, just the same
|
|
339
|
+
# `"customer is active"` example, just the same mechanism.
|
|
237
340
|
def lifecycle_guard_and_given_violations_are_refused(history)
|
|
238
341
|
offenders = history.fetch(:guard_checks).filter_map do |check|
|
|
239
342
|
next if check[:recomputed_refused] == check[:actual_refused]
|
|
@@ -14,11 +14,11 @@ module Hecks
|
|
|
14
14
|
# (#check_piece_invariants, #eligible_rows, #nest_rows,
|
|
15
15
|
# #recompute_median) each leans on.
|
|
16
16
|
module InvariantsAndAggregation
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# admissibility.rb) checks these
|
|
20
|
-
#
|
|
21
|
-
# now re-checked a record
|
|
17
|
+
# Every stored record still satisfies its own aggregate's declared
|
|
18
|
+
# invariants — Admissibility#enforce_invariants (command_rules/
|
|
19
|
+
# admissibility.rb) checks these after every command's mutations,
|
|
20
|
+
# before save, the same point `ensures` is checked. Nothing until
|
|
21
|
+
# now re-checked a record after a whole replay finished, independent
|
|
22
22
|
# of whichever call site was supposed to have refused a violation
|
|
23
23
|
# in the first place — a record failing its own declared invariant
|
|
24
24
|
# here is proof a violating write landed anyway: the call site
|
|
@@ -27,9 +27,9 @@ module Hecks
|
|
|
27
27
|
#
|
|
28
28
|
# `history[:instances]` entries are already plain, symbol-keyed
|
|
29
29
|
# state Hashes (Replay.call's own `record.state`) — called against
|
|
30
|
-
# Evaluator.call the
|
|
30
|
+
# Evaluator.call the same way ValueObject::Builder#build already
|
|
31
31
|
# does for a VO's own invariants (value/coercion.rb), no GuardState
|
|
32
|
-
# wrapper needed the way enforce_invariants' own
|
|
32
|
+
# wrapper needed the way enforce_invariants' own live call uses one
|
|
33
33
|
# (GuardState exists for `parent.`/projected-field dereferencing
|
|
34
34
|
# mid-dispatch; a stored record's own scalar fields need none of
|
|
35
35
|
# that to re-check a same-aggregate invariant against itself).
|
|
@@ -39,8 +39,8 @@ module Hecks
|
|
|
39
39
|
#
|
|
40
40
|
# Entity#invariants (round 7) closes here too, not for free —
|
|
41
41
|
# `stored_records_satisfy_declared_invariants` only ever checked
|
|
42
|
-
# the
|
|
43
|
-
# checked against every
|
|
42
|
+
# the aggregate's own flat state; a piece's own invariant is
|
|
43
|
+
# checked against every element of a `list_of` field, a genuinely
|
|
44
44
|
# different walk `check_piece_invariants` below makes,
|
|
45
45
|
# independently of `Admissibility#check_entity_invariants` (the
|
|
46
46
|
# live enforcement path this property exists to catch drifting
|
|
@@ -70,11 +70,11 @@ module Hecks
|
|
|
70
70
|
offenders.empty? || offenders.join("; ")
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
# A
|
|
74
|
-
# `list_of` field holds — the
|
|
73
|
+
# A piece's own invariant, checked against every element a
|
|
74
|
+
# `list_of` field holds — the same lookup `Admissibility#
|
|
75
75
|
# check_entity_invariants` makes (`owner.attributes.find { |a|
|
|
76
76
|
# a.list? && a.type.to_s == entity.hecks_name }`), independently
|
|
77
|
-
# reapplied here against a
|
|
77
|
+
# reapplied here against a stored record's own plain Hash state
|
|
78
78
|
# rather than a live `Instance`.
|
|
79
79
|
def check_piece_invariants(owner_construct, owner_state, key)
|
|
80
80
|
owner_construct.entities.each do |entity|
|
|
@@ -99,23 +99,23 @@ module Hecks
|
|
|
99
99
|
nil
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
# A
|
|
103
|
-
#
|
|
102
|
+
# A saga instance's own checkpoint survives being written and read
|
|
103
|
+
# back — the durability contract `SagaInterpreter#checkpoint` makes
|
|
104
104
|
# (`state:` plus a `deep_copy`d `memory:`, handed to whatever
|
|
105
105
|
# adapter answers `save_saga`) and `Registry#rehydrate_sagas!`
|
|
106
106
|
# promises to restore on the next boot (`each_saga` yielding
|
|
107
107
|
# `[pm, correlation, state, memory]` back into `saga_instances`).
|
|
108
|
-
# `Replay` captures the
|
|
108
|
+
# `Replay` captures the live store already materialised the same
|
|
109
109
|
# way `checkpoint` itself does (`Value.materialize`, not raw
|
|
110
110
|
# `Runtime::Value`s — see its own comment); this property pushes
|
|
111
|
-
# that captured memory through the
|
|
111
|
+
# that captured memory through the same `JSON.generate` then
|
|
112
112
|
# `JSON.parse(symbolize_names: true)` round-trip `checkpoint`'s own
|
|
113
113
|
# `deep_copy` performs (mirrored here rather than called — a
|
|
114
114
|
# private instance method with no registry to hand it) and checks
|
|
115
115
|
# it comes back byte-identical. A memory holding anything that
|
|
116
116
|
# round-trip cannot carry faithfully — a bare Symbol leaf, a
|
|
117
117
|
# non-JSON type a future field introduces — is corruption the
|
|
118
|
-
# durable path would introduce on a
|
|
118
|
+
# durable path would introduce on a real restart, caught here
|
|
119
119
|
# without needing one.
|
|
120
120
|
#
|
|
121
121
|
# `declares_state?` (Behaviour::ProcessManager) is the other half:
|
|
@@ -150,11 +150,11 @@ module Hecks
|
|
|
150
150
|
offenders.empty? || offenders.join("; ")
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
# A `for_each`
|
|
154
|
-
#
|
|
153
|
+
# A `for_each` policy dispatches exactly once per row its declared
|
|
154
|
+
# query answers — never once for the triggering event regardless of
|
|
155
155
|
# row count, never skipping a matched row, never firing on a row a
|
|
156
|
-
# concurrent mutation only made match
|
|
157
|
-
# computes the expected row-id set
|
|
156
|
+
# concurrent mutation only made match after the fact. `Replay`
|
|
157
|
+
# computes the expected row-id set independently, at the same
|
|
158
158
|
# instant the real dispatch runs (`Replay.expected_fan_out_rows`,
|
|
159
159
|
# the query oracle's own shape aimed at fan-out: two engines
|
|
160
160
|
# compared, never one graded against itself), and records it
|
|
@@ -183,21 +183,21 @@ module Hecks
|
|
|
183
183
|
offenders.empty? || offenders.join("; ")
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
-
# A `count`/`median`
|
|
187
|
-
#
|
|
186
|
+
# A `count`/`median` report's reduced scalar matches the same
|
|
187
|
+
# reduction done independently, over the same eligible rows —
|
|
188
188
|
# `ReadModelInterpreter#project`'s own FK-join (root first, then
|
|
189
189
|
# each many-side head matched against it) and `#median` (odd →
|
|
190
190
|
# the true middle, even → the average of the two middles as a
|
|
191
191
|
# Float, empty → `nil`; `count` is the filtered length, empty →
|
|
192
192
|
# `0`), reproduced here in plain Ruby against `history[:instances]`
|
|
193
193
|
# rather than a live registry — `FieldPath.dig` +
|
|
194
|
-
# `Ports::Query::InMemory.comparable`/`.holds?` are the
|
|
194
|
+
# `Ports::Query::InMemory.comparable`/`.holds?` are the same two
|
|
195
195
|
# calls the interpreter itself makes to read a field and judge a
|
|
196
196
|
# `where`, called here rather than re-derived, so this oracle
|
|
197
197
|
# cannot drift from what "read a field" or "a clause holds" mean
|
|
198
198
|
# without the interpreter drifting the identical way.
|
|
199
199
|
#
|
|
200
|
-
# Only a report whose `:query` is answered by the
|
|
200
|
+
# Only a report whose `:query` is answered by the same bluebook
|
|
201
201
|
# `history[:bluebook]` carries (the bare `Domain.report_name`
|
|
202
202
|
# form, `domain == bluebook.name`) is checked — the same "only
|
|
203
203
|
# what we have the grammar for" scope `lifecycle_values_are_declared`
|
|
@@ -206,7 +206,7 @@ module Hecks
|
|
|
206
206
|
# ("only a report answered by this bluebook, only a count/median
|
|
207
207
|
# report, only one with a reduced many-side head") each gating the
|
|
208
208
|
# next, ending in one independent recomputation compared against
|
|
209
|
-
# the live answer. The guards are what make this oracle
|
|
209
|
+
# the live answer. The guards are what make this oracle scoped
|
|
210
210
|
# correctly, not incidental complexity — narrower than "every
|
|
211
211
|
# branch reads as its own precondition."
|
|
212
212
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
@@ -239,14 +239,14 @@ module Hecks
|
|
|
239
239
|
end
|
|
240
240
|
|
|
241
241
|
# `aggregation_matches_recompute`'s own shape, extended from
|
|
242
|
-
# reducing a many-side head to a scalar (count/median) to
|
|
242
|
+
# reducing a many-side head to a scalar (count/median) to nesting
|
|
243
243
|
# it — `ReadModelInterpreter#group_by_target`/`#nest`, reproduced
|
|
244
244
|
# here in plain Ruby against `history[:instances]` the same way
|
|
245
245
|
# `eligible_rows` already reproduces the FK-join and `where`
|
|
246
246
|
# narrowing count/median share. `Value.materialize_unwrapped` is
|
|
247
|
-
# the
|
|
247
|
+
# the same call `#project` makes before nesting (a single-field
|
|
248
248
|
# value object recurses to its bare scalar — a real grouping key
|
|
249
|
-
# has to
|
|
249
|
+
# has to be one) — called here rather than re-derived, so this
|
|
250
250
|
# oracle cannot drift from what "the group key" means without the
|
|
251
251
|
# interpreter drifting the identical way.
|
|
252
252
|
#
|
|
@@ -298,17 +298,17 @@ module Hecks
|
|
|
298
298
|
end
|
|
299
299
|
end
|
|
300
300
|
|
|
301
|
-
#
|
|
301
|
+
# The eligible rows a `count`/`median` head reduces — every
|
|
302
302
|
# instance of the reduced head's own aggregate, FK-matched against
|
|
303
303
|
# the report's root reference (if it has one; a rootless report has
|
|
304
304
|
# none to match) exactly the way `ReadModelInterpreter#reference_fields`
|
|
305
305
|
# finds the matching attribute, then narrowed by the report's own
|
|
306
|
-
# `where` clauses via the
|
|
306
|
+
# `where` clauses via the same `InMemory.holds?` the interpreter's
|
|
307
307
|
# `execute` calls.
|
|
308
308
|
def eligible_rows(bluebook, instances, domain, model, reduced_head, args)
|
|
309
309
|
aggregate = bluebook.aggregate(reduced_head[:aggregate])
|
|
310
310
|
prefix = "#{domain}::#{reduced_head[:aggregate]}#"
|
|
311
|
-
# `id:`
|
|
311
|
+
# `id:` merged in, the same `record.to_h` (`@state.merge(id:
|
|
312
312
|
# @id)`) every live head row carries — count/median never read
|
|
313
313
|
# it, but group_by_matches_recompute's own independent nesting
|
|
314
314
|
# does, the same way ReadModelInterpreter#row(record) = record.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "../nondeterministic"
|
|
2
|
+
|
|
1
3
|
module Hecks
|
|
2
4
|
module Fuzzing
|
|
3
5
|
module Properties
|
|
@@ -12,11 +14,11 @@ 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.
|
|
@@ -44,10 +46,10 @@ module Hecks
|
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
# Every saga advance a replay actually logged moved along an edge
|
|
47
|
-
# the process manager
|
|
49
|
+
# the process manager declared — `(from, to)` pairs that appear in
|
|
48
50
|
# `saga_log` with `advanced: true` must be a `(handler.from_state,
|
|
49
51
|
# handler.to_state)` pair some handler on that PM declares
|
|
50
|
-
# (compensation edges included ; a
|
|
52
|
+
# (compensation edges included ; a refused-triggered advance is a
|
|
51
53
|
# handler like any other). A saga that advanced along a pair no
|
|
52
54
|
# handler names would mean the runtime moved state the language
|
|
53
55
|
# never authorized — the same trust ModelCheck's static reachability
|
|
@@ -72,23 +74,41 @@ module Hecks
|
|
|
72
74
|
offenders.empty? || offenders.join("; ")
|
|
73
75
|
end
|
|
74
76
|
|
|
75
|
-
#
|
|
77
|
+
# The foundational one. `Hecks::Runtime` mints nothing — every
|
|
76
78
|
# identity is declared and derived, never invented (see
|
|
77
|
-
# command_interpreter.rb's own "
|
|
79
|
+
# command_interpreter.rb's own "nothing is minted" — a random hex,
|
|
78
80
|
# a counter, anything not reproducible from the payload, was
|
|
79
81
|
# refused out of the runtime specifically because it broke this).
|
|
80
|
-
# So the
|
|
81
|
-
#
|
|
82
|
+
# So the same steps, replayed against a fresh boot, must produce
|
|
83
|
+
# byte-identical events, refusals, and instances — any drift here
|
|
82
84
|
# is nondeterminism the runtime promised not to have: a wall-clock
|
|
83
85
|
# read that leaked into compared state, a Hash iteration order a
|
|
84
86
|
# comparison depended on, anything. Two independent replays, not a
|
|
85
|
-
# cached one compared to itself, so a bug that corrupts the
|
|
87
|
+
# cached one compared to itself, so a bug that corrupts the first
|
|
86
88
|
# run's own bookkeeping cannot pass by agreeing with itself.
|
|
87
89
|
def replay_is_deterministic(domain_path, steps, adapter: :memory)
|
|
88
90
|
first = Replay.call(domain_path, steps, adapter: adapter)
|
|
89
91
|
second = Replay.call(domain_path, steps, adapter: adapter)
|
|
90
92
|
|
|
91
|
-
|
|
93
|
+
# What leaves this comparison, and why, is declared once in
|
|
94
|
+
# `Nondeterministic::FIELDS`: the `outbox_row` group (per-enqueue
|
|
95
|
+
# uuids), the `event` group on each row's full `Event#to_h`
|
|
96
|
+
# (`occurred_at`, which `history[:events]` never carried), and the
|
|
97
|
+
# `history` group (live IR objects). Stripped per `outbox_traces`
|
|
98
|
+
# row rather than dropping `outbox_traces` wholesale: everything
|
|
99
|
+
# else on a row (status, consumer, kind, the event's own name/
|
|
100
|
+
# aggregate/id/payload) is reproducible from the steps alone and
|
|
101
|
+
# stays checked.
|
|
102
|
+
strip_outbox_nondeterminism = lambda do |history|
|
|
103
|
+
traces = Array(history[:outbox_traces]).map do |trace|
|
|
104
|
+
trace.merge(rows: trace[:rows].map do |row|
|
|
105
|
+
Nondeterministic.strip(row, :outbox_row).merge(event: Nondeterministic.strip(row[:event], :event))
|
|
106
|
+
end)
|
|
107
|
+
end
|
|
108
|
+
history.merge(outbox_traces: traces)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
comparable = ->(history) { Nondeterministic.strip(strip_outbox_nondeterminism.call(history), :history) }
|
|
92
112
|
return true if comparable.call(first) == comparable.call(second)
|
|
93
113
|
|
|
94
114
|
"two replays of the same #{steps.length} steps produced different histories"
|