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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
module Properties
|
|
4
|
+
# Angle-9 — `corrects` (retroactive correction) had exactly one
|
|
5
|
+
# declaration anywhere in the corpus (`examples/banking/bluebook/
|
|
6
|
+
# deposit_accounts.bluebook:353`, aggregate-level) and no property
|
|
7
|
+
# anywhere in this file ever checked it, and no `FEATURE_COVERAGE`
|
|
8
|
+
# claim either — confirmed absent before this file existed. `qa/
|
|
9
|
+
# stress_domains/corrections` gives it its first real coverage; see
|
|
10
|
+
# that domain's own NOTES.md for what it found (an entity-level
|
|
11
|
+
# `corrects` crashes Ruby at dispatch outright, and Rust's own
|
|
12
|
+
# generated code has no admissibility check for it at all — neither
|
|
13
|
+
# engine can be compared on the untested combination this property
|
|
14
|
+
# was written to watch, which is itself the headline finding).
|
|
15
|
+
module Corrections
|
|
16
|
+
# Every event named by a `corrects` mutation, on any command a
|
|
17
|
+
# booted history's own bluebooks declare — aggregate-level, or
|
|
18
|
+
# nested inside an entity at any depth (`each_command_including_
|
|
19
|
+
# entities`, below, is the same recursive walk `SequenceGenerator::
|
|
20
|
+
# Catalog#each_entity_chain` already uses, for the identical reason:
|
|
21
|
+
# `Aggregate#entities`/`Entity#entities` nest, ADR 0026, S17). For
|
|
22
|
+
# every such command, every event this history actually recorded
|
|
23
|
+
# under one of the command's own `emits` names must have an event
|
|
24
|
+
# named by the command's own `corrects` target — same aggregate-
|
|
25
|
+
# qualified name, same id — appearing strictly earlier in the same
|
|
26
|
+
# history.
|
|
27
|
+
#
|
|
28
|
+
# `history[:events]` is already in occurrence order (`Replay.call`'s
|
|
29
|
+
# own `runtime.events`, appended as each step dispatches) — "earlier"
|
|
30
|
+
# is therefore "earlier in this array," no timestamp comparison
|
|
31
|
+
# needed, and no per-step attribution back to which command produced
|
|
32
|
+
# which event is needed either: an event's own declared name already
|
|
33
|
+
# identifies the one command in its aggregate that can produce it
|
|
34
|
+
# (`AggregateBuilder::Sealing#seal_correction_targets`'s own
|
|
35
|
+
# `emitted_by` hash reads the identical fact, one level shallower).
|
|
36
|
+
#
|
|
37
|
+
# Why this cannot be `GUARANTEED_BY_CONSTRUCTION` the way the
|
|
38
|
+
# aggregate-level case almost is: `CommandRules::Admissibility#
|
|
39
|
+
# enforce_correction_target` (the dispatch-time check) and
|
|
40
|
+
# `AggregateBuilder::Sealing#seal_correction_targets` (the build-time
|
|
41
|
+
# check) both exist only for an aggregate-level `corrects` —
|
|
42
|
+
# `EntityInterpreter#step_enforce_givens` never calls the former at
|
|
43
|
+
# all, and the latter walks only `@commands` (the aggregate's own
|
|
44
|
+
# top-level list), never `@entities`. An entity-level `corrects`
|
|
45
|
+
# mutation is invisible to both doors today — this property is the
|
|
46
|
+
# only thing anywhere, on either engine, that would ever catch one
|
|
47
|
+
# going wrong.
|
|
48
|
+
def corrections_reference_an_emitted_event(history)
|
|
49
|
+
violations = []
|
|
50
|
+
|
|
51
|
+
(history[:bluebooks] || {}).each do |domain, bluebook|
|
|
52
|
+
bluebook.aggregates.each do |aggregate|
|
|
53
|
+
aggregate_key = "#{domain}::#{aggregate.hecks_name}"
|
|
54
|
+
|
|
55
|
+
each_command_including_entities(aggregate) do |command|
|
|
56
|
+
corrects_mutations = command.mutations.select { |mutation| mutation.op == :corrects }
|
|
57
|
+
next if corrects_mutations.empty?
|
|
58
|
+
|
|
59
|
+
corrects_mutations.each do |mutation|
|
|
60
|
+
corrected_event = mutation.target.to_s
|
|
61
|
+
|
|
62
|
+
command.emits.each do |produced_event_name|
|
|
63
|
+
violations.concat(unmatched_corrections(history[:events], aggregate_key,
|
|
64
|
+
produced_event_name.to_s, corrected_event,
|
|
65
|
+
command.hecks_name))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
violations.empty? || violations.uniq.join("; ")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def unmatched_corrections(events, aggregate_key, produced_event_name, corrected_event, command_name)
|
|
76
|
+
own_events = events.each_with_index.select do |event, _index|
|
|
77
|
+
event[:name] == produced_event_name && event[:aggregate] == aggregate_key
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
own_events.filter_map do |event, index|
|
|
81
|
+
preceding = events.first(index)
|
|
82
|
+
next if preceding.any? do |earlier|
|
|
83
|
+
earlier[:name] == corrected_event && earlier[:aggregate] == aggregate_key &&
|
|
84
|
+
earlier[:id].to_s == event[:id].to_s
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
"#{command_name} (#{aggregate_key}##{event[:id]}) emitted #{produced_event_name}, claiming to " \
|
|
88
|
+
"correct #{corrected_event}, but no #{corrected_event} for the same aggregate/id appears " \
|
|
89
|
+
"earlier in this history"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def each_command_including_entities(owner, &block)
|
|
94
|
+
owner.commands.each(&block)
|
|
95
|
+
owner.entities.each { |entity| each_command_including_entities(entity, &block) }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Fuzzing
|
|
3
3
|
module Properties
|
|
4
|
+
# **A dry run leaves no trace** — `Dispatcher#dry_run?`'s whole contract
|
|
5
|
+
# ("the command evaluated hypothetically, nothing saved or emitted,
|
|
6
|
+
# no reaction"), held to the store rather than trusted: `Replay`
|
|
7
|
+
# snapshots every instance and the event count on either side of
|
|
8
|
+
# each `{"dry_run": …}` step, in a separate `dry_run_traces` array
|
|
9
|
+
# (same order as `history[:dry_runs]`, never merged into it — that
|
|
10
|
+
# array stays the exact `{verb:, ok:, error?:}` shape the compiled
|
|
11
|
+
# Rust binary's own `dry_run` answers, so `spec/rust_conformance_
|
|
12
|
+
# spec.rb`'s direct comparison against it never sees a key Rust
|
|
13
|
+
# doesn't have), and this is the comparison. Refused or accepted
|
|
14
|
+
# makes no difference — a hypothetical that was refused had even
|
|
15
|
+
# less business writing anything. Entries without the snapshots (a
|
|
16
|
+
# hand-built history, an older corpus) are skipped, not failed: no
|
|
17
|
+
# claim, no finding.
|
|
18
|
+
module DryRuns
|
|
19
|
+
def dry_runs_leave_no_trace(history)
|
|
20
|
+
offenders = Array(history[:dry_run_traces]).filter_map do |entry|
|
|
21
|
+
before = entry[:before]
|
|
22
|
+
after = entry[:after]
|
|
23
|
+
next unless before && after
|
|
24
|
+
|
|
25
|
+
traces = []
|
|
26
|
+
traces << "events #{before[:events]} -> #{after[:events]}" unless before[:events] == after[:events]
|
|
27
|
+
traces << "instances changed" unless before[:instances] == after[:instances]
|
|
28
|
+
next if traces.empty?
|
|
29
|
+
|
|
30
|
+
"dry run of #{entry[:verb]} (ok: #{entry[:ok]}) left a trace: #{traces.join(', ')}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
offenders.empty? || offenders.join("; ")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
4
37
|
# Dispatch-binding and mutation-recomputation properties: a saga/
|
|
5
38
|
# policy dispatch is bound to the value its own with_spec names, and a
|
|
6
39
|
# command's append/remove/multiply/clamp mutations land on the same
|
|
7
|
-
# after-state an independent recomputation of the
|
|
40
|
+
# after-state an independent recomputation of the same rule produces.
|
|
8
41
|
|
|
9
42
|
# Holds dispatch_binding_fidelity and mutations_match_recompute, plus
|
|
10
43
|
# the independently-written re-derivations (#resolve_dispatch_binding,
|
|
@@ -16,17 +49,17 @@ module Hecks
|
|
|
16
49
|
# are not claimable feature strings at all — see FEATURE_COVERAGE's
|
|
17
50
|
# own comment on this entry). history[:saga_dispatches]/[:policy_dispatches]
|
|
18
51
|
# (Registry#saga_dispatch_log/#policy_dispatch_log — additive,
|
|
19
|
-
# Ruby-only,
|
|
52
|
+
# Ruby-only, never touching saga_log/reaction_log, the byte-for-
|
|
20
53
|
# byte shape spec/rust_conformance_spec.rb holds Rust to) each
|
|
21
|
-
# carry the
|
|
54
|
+
# carry the raw inputs a dispatch's own args were resolved from,
|
|
22
55
|
# captured live at the moment the resolution actually ran — a
|
|
23
56
|
# saga's own memory keeps changing across a run, so re-deriving
|
|
24
|
-
# from history[:saga_instances]'s
|
|
57
|
+
# from history[:saga_instances]'s final memory (the only other
|
|
25
58
|
# place it would be visible) would grade the wrong moment,
|
|
26
59
|
# lifecycle_guard_and_given_violations_are_refused's own false
|
|
27
60
|
# positive one item earlier, in a different shape.
|
|
28
61
|
#
|
|
29
|
-
# #resolve_dispatch_binding/#resolve_trigger_binding are
|
|
62
|
+
# #resolve_dispatch_binding/#resolve_trigger_binding are separate,
|
|
30
63
|
# independently-written re-derivations of SagaInterpreter#
|
|
31
64
|
# dispatch_args/PolicyInterpreter#trigger_args's own resolution —
|
|
32
65
|
# never calling either method again, which would only ever agree
|
|
@@ -66,8 +99,8 @@ module Hecks
|
|
|
66
99
|
|
|
67
100
|
# SagaInterpreter#dispatch_args's own 4-branch resolution,
|
|
68
101
|
# reproduced independently: a literal, the correlation key itself,
|
|
69
|
-
# the
|
|
70
|
-
# the saga's own carried memory (seeded from the
|
|
102
|
+
# the current triggering event's own payload, or — the fallback —
|
|
103
|
+
# the saga's own carried memory (seeded from the starting event's
|
|
71
104
|
# payload, at begin_saga).
|
|
72
105
|
def resolve_dispatch_binding(entry)
|
|
73
106
|
entry[:with_spec].to_h do |key, value|
|
|
@@ -83,7 +116,7 @@ module Hecks
|
|
|
83
116
|
# PolicyInterpreter#trigger_args's own 2-branch resolution — a
|
|
84
117
|
# policy holds no correlation and no memory, so `payload` (the
|
|
85
118
|
# triggering event's own payload, already merged with a fan-out
|
|
86
|
-
# row's id when there is one) is the
|
|
119
|
+
# row's id when there is one) is the whole source.
|
|
87
120
|
def resolve_trigger_binding(entry)
|
|
88
121
|
entry[:with_spec].to_h do |key, value|
|
|
89
122
|
resolved = value.is_a?(Symbol) ? entry[:payload][value] : value
|
|
@@ -104,32 +137,77 @@ module Hecks
|
|
|
104
137
|
#
|
|
105
138
|
# `history[:mutation_traces]` (Replay's own bounded, additive
|
|
106
139
|
# extension — see #build_mutation_trace's own comment) carries a
|
|
107
|
-
# per-step before/after snapshot of the
|
|
140
|
+
# per-step before/after snapshot of the entity element an
|
|
108
141
|
# entity-dispatched command's own mutations acted on, materialized
|
|
109
142
|
# to plain data, plus the step's own raw args — the delta
|
|
110
143
|
# `aggregation_matches_recompute` never had to ask for, because
|
|
111
|
-
# count/median are pure functions of
|
|
144
|
+
# count/median are pure functions of final state and a mutation
|
|
112
145
|
# is not (the same "captured live, not re-derived from final
|
|
113
146
|
# state" lesson item 8's own saga_dispatch_log already learned).
|
|
114
147
|
#
|
|
115
148
|
# #recompute_append/#recompute_remove/#recompute_multiply/
|
|
116
|
-
# #recompute_clamp are
|
|
117
|
-
# reproductions of
|
|
149
|
+
# #recompute_clamp/#recompute_set are separate, independently-
|
|
150
|
+
# written reproductions of EntityElement#appended_to_element/
|
|
151
|
+
# #removed_from_element/#apply_to_element's own `:set` branch and
|
|
118
152
|
# CommandRules::Arithmetic#multiply/#clamp — never calling either
|
|
119
|
-
# again, which would only ever agree with itself.
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
153
|
+
# again, which would only ever agree with itself. Every one of
|
|
154
|
+
# these five reproduces the entity-scoped applier specifically
|
|
155
|
+
# (`EntityElement#apply_to_element`, entity_element.rb), never
|
|
156
|
+
# the aggregate-level `MutationApplier#apply` (mutation_applier.rb)
|
|
157
|
+
# — `build_mutation_trace` (replay.rb) only ever captures an
|
|
158
|
+
# entity-owned command's own mutation in the first place (that
|
|
159
|
+
# method's own `command_name&.include?(".")` guard), so there is
|
|
160
|
+
# no aggregate-level trace for this property to ever compare
|
|
161
|
+
# against `owner_for_verb`'s own comment draws the identical
|
|
162
|
+
# aggregate/owner distinction for `:append`. `:increment`/
|
|
163
|
+
# `:decrement` remain out of scope (CommandRules::Arithmetic#
|
|
164
|
+
# arithmetic's own VO-vs-scalar branching and INT64_RANGE overflow
|
|
165
|
+
# checks are a materially larger reproduction than the other five
|
|
166
|
+
# ops combined — scoped out of this pass, not forgotten; see
|
|
167
|
+
# docs/decisions/0056); a command mixing them with a recomputable
|
|
168
|
+
# op still gets the recomputable one checked.
|
|
169
|
+
#
|
|
170
|
+
# `:set` closes a real, separate gap from the other four — this is
|
|
171
|
+
# not "the fifth op of a symmetrical set." `self_consistency.rb`'s
|
|
172
|
+
# own rehydration/idempotency checks (lib/hecks/fuzzing/
|
|
173
|
+
# self_consistency.rb) can never catch a bug in `EntityElement#
|
|
174
|
+
# apply_to_element`'s `:set` branch (entity_element.rb) no matter
|
|
175
|
+
# how much they run: both the "live" state they snapshot and the
|
|
176
|
+
# "rehydrated" state they fold from `Ports::Persistence::
|
|
177
|
+
# AppendOnly`'s own journal trace back to the same single
|
|
178
|
+
# `step_apply_mutations` call (entity_interpreter.rb) — the
|
|
179
|
+
# journal holds the full POST-mutation state, not a delta (that
|
|
180
|
+
# file's own header), so a wrong `:set` result is already baked
|
|
181
|
+
# into both sides of that comparison before either one runs. A
|
|
182
|
+
# Ruby/Rust differential check is subject to the identical
|
|
183
|
+
# structural blind spot whenever Rust's own generated `:set`
|
|
184
|
+
# handling was derived from — and so shares — the same
|
|
185
|
+
# misunderstanding Ruby's implementation has. `#recompute_set`
|
|
186
|
+
# is a genuinely third computation, independent of both: it
|
|
187
|
+
# re-derives the expected value from the mutation's own declared
|
|
188
|
+
# `source` (an argument or a literal — matching `apply_to_
|
|
189
|
+
# element`'s own `resolve_source`, which unlike the aggregate-
|
|
190
|
+
# level `apply` never special-cases a `StateRef` source at all)
|
|
191
|
+
# and the step's own captured `args`, then compares against what
|
|
192
|
+
# the real dispatch actually stored — closing exactly the class
|
|
193
|
+
# of bug the self-consistency and (whenever Rust's codegen shares
|
|
194
|
+
# a ruby-derived misunderstanding) differential checks cannot see.
|
|
195
|
+
# `spec/fuzzing/mutation_set_self_correctness_spec.rb` proves this
|
|
196
|
+
# concretely: a defect planted directly in `EntityElement#apply_
|
|
197
|
+
# to_element`'s `:set` branch leaves `SelfConsistency.check`
|
|
198
|
+
# completely clean while `mutations_match_recompute` names it.
|
|
124
199
|
#
|
|
125
200
|
# `:unrecomputable` (never compared, never a finding) covers the
|
|
126
201
|
# generator's own deliberate arg-malforming (`StepBuilder#malform`)
|
|
127
202
|
# landing a non-Numeric amount/non-2-element bounds where
|
|
128
|
-
# multiply/clamp need one — the
|
|
203
|
+
# multiply/clamp need one — the same shape `guard_check`'s own
|
|
129
204
|
# AbsentArgument false positive taught: a step whose raw material
|
|
130
205
|
# doesn't fit the op's own contract is inconclusive, not a claimed
|
|
131
|
-
# mismatch.
|
|
132
|
-
|
|
206
|
+
# mismatch. `#recompute_set` returns it for the same reason,
|
|
207
|
+
# whenever `Value.for_attribute`'s own coercion door raises on the
|
|
208
|
+
# resolved raw source (malformed fuzzer input, never a real
|
|
209
|
+
# corpus shape).
|
|
210
|
+
RECOMPUTABLE_MUTATION_OPS = %i[append remove multiply clamp set].freeze
|
|
133
211
|
|
|
134
212
|
def mutations_match_recompute(history)
|
|
135
213
|
bluebooks = history.fetch(:bluebooks)
|
|
@@ -140,8 +218,14 @@ module Hecks
|
|
|
140
218
|
command = command_for_verb(bluebooks, entry[:verb])
|
|
141
219
|
next [] unless command
|
|
142
220
|
|
|
221
|
+
aggregate = aggregate_for_verb(bluebooks, entry[:verb])
|
|
222
|
+
next [] unless aggregate
|
|
223
|
+
|
|
224
|
+
owner = owner_for_verb(bluebooks, entry[:verb]) || aggregate
|
|
225
|
+
|
|
143
226
|
command.mutations.select { |m| RECOMPUTABLE_MUTATION_OPS.include?(m.op) }.filter_map do |mutation|
|
|
144
|
-
expected = recompute_mutation(mutation, entry[:before][mutation.target], entry[:args], entry[:before]
|
|
227
|
+
expected = recompute_mutation(mutation, entry[:before][mutation.target], entry[:args], entry[:before],
|
|
228
|
+
aggregate, command, owner)
|
|
145
229
|
next if expected == :unrecomputable
|
|
146
230
|
|
|
147
231
|
actual = entry[:after][mutation.target]
|
|
@@ -155,31 +239,227 @@ module Hecks
|
|
|
155
239
|
offenders.empty? || offenders.join("; ")
|
|
156
240
|
end
|
|
157
241
|
|
|
158
|
-
|
|
242
|
+
# `command_for_verb`'s own root-aggregate half (Guards) —
|
|
243
|
+
# re-derived independently rather than read off `entry[:domain]`/
|
|
244
|
+
# `entry[:aggregate]` (present on a real `build_mutation_trace`
|
|
245
|
+
# entry, but not on every hand-built fixture this property is
|
|
246
|
+
# tested against) so this works from `entry[:verb]` alone, the
|
|
247
|
+
# one field every entry always carries. BUG#5's fix needs the
|
|
248
|
+
# root aggregate specifically — `Value.for_attribute` resolves a
|
|
249
|
+
# value-object type against the root's own namespace only, the
|
|
250
|
+
# same reason `EntityElement#locate_chain` threads `root_aggregate`
|
|
251
|
+
# through every hop separately from each hop's own `owner`.
|
|
252
|
+
def aggregate_for_verb(bluebooks, verb)
|
|
253
|
+
domain_name, aggregate_name, = Naming.split_verb(verb)
|
|
254
|
+
return nil unless domain_name
|
|
255
|
+
|
|
256
|
+
bluebooks[domain_name]&.aggregate(aggregate_name)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# `mutation.target`'s own declaring construct — the root
|
|
260
|
+
# aggregate for an aggregate-owned command (`AddSlot`'s own
|
|
261
|
+
# `:slots`), or the entity a dot-shaped command belongs to
|
|
262
|
+
# (`Board.AddCard`'s own `:cards`, declared on `Board`, not on
|
|
263
|
+
# `Workspace`) — needed only by BUG#12's own recompute-side fix
|
|
264
|
+
# (`recompute_append`, below): `entity.attribute(mutation.target)`
|
|
265
|
+
# has to be asked of whichever construct actually declares it,
|
|
266
|
+
# never the root aggregate unconditionally, the same distinction
|
|
267
|
+
# `EntityElement#locate_chain` draws between `root_aggregate` and
|
|
268
|
+
# each hop's own `owner`. Re-derived independently from
|
|
269
|
+
# `entry[:verb]` alone, the same reasoning `aggregate_for_verb`'s
|
|
270
|
+
# own comment gives.
|
|
271
|
+
def owner_for_verb(bluebooks, verb)
|
|
272
|
+
domain_name, aggregate_name, command_path = Naming.split_verb(verb)
|
|
273
|
+
return nil unless command_path
|
|
274
|
+
|
|
275
|
+
aggregate = bluebooks[domain_name]&.aggregate(aggregate_name)
|
|
276
|
+
return nil unless aggregate
|
|
277
|
+
return aggregate unless command_path.include?(".")
|
|
278
|
+
|
|
279
|
+
entity_name, = command_path.split(".", 2)
|
|
280
|
+
aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def recompute_mutation(mutation, current, args, before_scope, aggregate, command, owner = aggregate)
|
|
159
284
|
case mutation.op
|
|
160
|
-
when :append
|
|
285
|
+
when :append
|
|
286
|
+
recompute_append(current, mutation.source, before_scope, args, aggregate, command, owner, mutation.target)
|
|
161
287
|
when :remove then recompute_remove(current, mutation.source, args)
|
|
162
288
|
when :multiply then recompute_multiply(current, resolve_mutation_source(mutation.source, args))
|
|
163
289
|
when :clamp then recompute_clamp(current, mutation.source)
|
|
290
|
+
when :set then recompute_set(mutation.source, args, aggregate, owner, mutation.target)
|
|
164
291
|
end
|
|
165
292
|
end
|
|
166
293
|
|
|
167
|
-
# `
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
#
|
|
294
|
+
# `EntityElement#apply_to_element`'s own `:set` branch (entity_
|
|
295
|
+
# element.rb), reproduced independently — not `MutationApplier#
|
|
296
|
+
# apply`'s aggregate-level twin (mutation_applier.rb), a
|
|
297
|
+
# different method with a different shape: `build_mutation_trace`
|
|
298
|
+
# (replay.rb) only ever captures an entity-owned command's own
|
|
299
|
+
# mutation (`command_name&.include?(".")`, that method's own
|
|
300
|
+
# header), so `mutations_match_recompute` can only ever be
|
|
301
|
+
# checking `apply_to_element`'s branch, never `apply`'s — the
|
|
302
|
+
# same distinction `owner_for_verb`'s own comment already draws
|
|
303
|
+
# for `:append`. Confirmed by reading `apply_to_element` directly:
|
|
304
|
+
# its own `:set` branch resolves the source through `rules.
|
|
305
|
+
# resolve_source` unconditionally (no `StateRef` branch at all —
|
|
306
|
+
# unlike the aggregate-level `apply`, an entity-owned `sets` has
|
|
307
|
+
# no declared corpus site using `state(:x)` today, so this
|
|
308
|
+
# reproduces what ships, not a hypothetical), reads the
|
|
309
|
+
# attribute off `entity.attribute(mutation.target)` (the owning
|
|
310
|
+
# entity, `owner` here — `Board`, never `Workspace`), and coerces
|
|
311
|
+
# through `Value.for_attribute(aggregate, attribute, value)`
|
|
312
|
+
# (the root aggregate, for value-object namespace resolution
|
|
313
|
+
# only — `value_object_for(aggregate, attribute.type)` — the
|
|
314
|
+
# same aggregate/owner split `owner_for_verb`'s own comment
|
|
315
|
+
# explains for `recompute_append`). An earlier version of this
|
|
316
|
+
# method mirrored `MutationApplier#apply`'s `:set` branch instead
|
|
317
|
+
# (StateRef-aware, coerced against `aggregate.attribute` rather
|
|
318
|
+
# than `owner.attribute`) and false-positived on every real
|
|
319
|
+
# entity-owned `sets` in the corpus — `NestedPieces::Workspace.
|
|
320
|
+
# Board.Label` (`sets :label`) has no `:label` attribute on
|
|
321
|
+
# `Workspace` at all, so `Value.for(aggregate, :label, raw)`
|
|
322
|
+
# silently passed the raw, uncoerced String through instead of
|
|
323
|
+
# wrapping it as `BoardLabel`, and the comparison below then
|
|
324
|
+
# disagreed with the real, correctly-coerced `after` state on
|
|
325
|
+
# every single run — caught immediately by running this against
|
|
326
|
+
# `qa/stress_domains/nested_pieces` before this comment existed,
|
|
327
|
+
# not by inspection alone.
|
|
328
|
+
#
|
|
329
|
+
# Reusing `Value.for_attribute` for the coercion step, rather
|
|
330
|
+
# than re-deriving it, is the same "coercion is its own already-
|
|
331
|
+
# guaranteed door" reasoning `GUARANTEED_BY_CONSTRUCTION` states
|
|
332
|
+
# and `#coerce_recompute_append_arg` already leans on for BUG#5 —
|
|
333
|
+
# what this property exists to check is the source resolution and
|
|
334
|
+
# routing (did the right raw value, from the right source, land
|
|
335
|
+
# on the right target?), not whether `Value.for_attribute` itself
|
|
336
|
+
# coerces correctly (a separate, already-enforced concern).
|
|
337
|
+
# Rescued broadly: a `:set` mutation trace is only ever captured
|
|
338
|
+
# after a real, already-admitted dispatch (`Replay#build_
|
|
339
|
+
# mutation_trace`'s own header), so a raise here means this
|
|
340
|
+
# recomputation resolved the wrong raw material, not that the
|
|
341
|
+
# real dispatch was itself malformed — `:unrecomputable`, not a
|
|
342
|
+
# crash, the same discipline every other branch in this method
|
|
343
|
+
# already follows for the generator's own deliberate malforming.
|
|
344
|
+
def recompute_set(source, args, aggregate, owner, target)
|
|
345
|
+
raw = resolve_mutation_source(source, args)
|
|
346
|
+
attribute = owner&.attribute(target)
|
|
347
|
+
coerced = attribute ? Runtime::Value.for_attribute(aggregate, attribute, raw) : raw
|
|
348
|
+
Runtime::Value.materialize(coerced)
|
|
349
|
+
rescue StandardError
|
|
350
|
+
:unrecomputable
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# `EntityElement#appended_to_element`'s own field-mapping half,
|
|
354
|
+
# reproduced: the field map resolved the same two-tier way
|
|
171
355
|
# (`MutationApplier#resolve_append_source` — a caller-supplied
|
|
172
|
-
# arg, or the entity's own current field), then appended.
|
|
173
|
-
|
|
174
|
-
|
|
356
|
+
# arg, or the entity's own current field), then appended. (An
|
|
357
|
+
# entity-dispatched command's own mutations do reach here —
|
|
358
|
+
# `#build_mutation_trace`'s own comment describing them as never
|
|
359
|
+
# reaching "the entity_element branch" means `MutationApplier#
|
|
360
|
+
# appended`'s own aggregate-level entity_element fallback
|
|
361
|
+
# specifically, which really is unreached from here; entity-owned
|
|
362
|
+
# append dispatches go through `EntityElement#appended_to_element`
|
|
363
|
+
# instead, and do reach this method.)
|
|
364
|
+
#
|
|
365
|
+
# BUG#5 — an entity-owned `:append` whose target field is itself
|
|
366
|
+
# value-object-typed (`Board.AddCard`'s own `sets :cards, append:
|
|
367
|
+
# { sequence: :sequence }`, `CardSequence`-typed). A caller-
|
|
368
|
+
# supplied arg reaches the real applier (`EntityElement#
|
|
369
|
+
# appended_to_element`) already coerced: `Interpreting#
|
|
370
|
+
# coerce_declared_arguments` runs `Value.for_attribute` over
|
|
371
|
+
# every arg the acting command itself declares, before dispatch
|
|
372
|
+
# ever reaches a mutation applier at all — independent of, and
|
|
373
|
+
# earlier than, anything `appended_to_element`'s own value_object
|
|
374
|
+
# check does. `before_scope[source]` (the entity's own current
|
|
375
|
+
# field) needs no such re-coercion here: it's already the
|
|
376
|
+
# materialized shape `build_mutation_trace` snapshotted it in
|
|
377
|
+
# (`Value.materialize`, same as `entry[:after]`), not a raw value
|
|
378
|
+
# sitting behind a live `Value`.
|
|
379
|
+
#
|
|
380
|
+
# BUG#12 — `owner`/`target` (new here) let this also reproduce
|
|
381
|
+
# `EntityElement#fill_declared_defaults`'s own entity-nested-in-
|
|
382
|
+
# entity fallback (`appended_to_element`'s `else` branch, when
|
|
383
|
+
# the appended element is itself an entity — `Card`, nested
|
|
384
|
+
# inside `Board` — not a value object): `owner.attribute(target)
|
|
385
|
+
# &.type` names the appended element's own type; when that names
|
|
386
|
+
# an entity of `aggregate` rather than a value object, every one
|
|
387
|
+
# of its own declared attributes `fields` doesn't already hold
|
|
388
|
+
# gets `Instance.default_for`'s own default — reused, not
|
|
389
|
+
# reimplemented, for the identical "never agree with itself"
|
|
390
|
+
# reason BUG#5's own coercion re-derivation above already gives:
|
|
391
|
+
# `Instance.default_for` is pre-existing, independently-tested
|
|
392
|
+
# machinery (an ordinary aggregate's own creation already runs
|
|
393
|
+
# through it via `Instance.defaults`), not the new glue
|
|
394
|
+
# (`fill_declared_defaults` itself) this property exists to
|
|
395
|
+
# catch a drift in.
|
|
396
|
+
def recompute_append(current, source_map, before_scope, args, aggregate, command, owner = aggregate, target = nil)
|
|
397
|
+
fields = source_map.transform_values do |source|
|
|
398
|
+
resolve_mutation_append_field(source, before_scope, args, aggregate, command)
|
|
399
|
+
end
|
|
400
|
+
fill_recompute_declared_defaults(aggregate, owner, target, fields)
|
|
175
401
|
Array(current) + [symbolize_deep(fields)]
|
|
176
402
|
end
|
|
177
403
|
|
|
178
|
-
|
|
404
|
+
# BUG#12's own recompute-side half — see `#recompute_append`'s
|
|
405
|
+
# own comment above for why this exists and why it reuses
|
|
406
|
+
# `Instance.default_for` rather than calling `EntityElement#
|
|
407
|
+
# fill_declared_defaults` again. A no-op whenever `target` names
|
|
408
|
+
# no attribute at all (every RECOMPUTABLE_MUTATION_OPS caller but
|
|
409
|
+
# `:append` passes no `target`) or `target`'s own declared type
|
|
410
|
+
# isn't an entity nested directly under `owner` (a value object,
|
|
411
|
+
# or nothing declared at all — `owner.attribute` answering `nil`
|
|
412
|
+
# for a target the DSL itself would already have refused at
|
|
413
|
+
# build time). `owner.entities`, not `aggregate.entities` — a
|
|
414
|
+
# piece nested inside a piece is a child of the owning entity
|
|
415
|
+
# (`Card` is `Board.entities`, never `Workspace.entities`), the
|
|
416
|
+
# same distinction `EntityElement#appended_to_element`'s own fix
|
|
417
|
+
# draws.
|
|
418
|
+
def fill_recompute_declared_defaults(aggregate, owner, target, fields)
|
|
419
|
+
return fields unless target
|
|
420
|
+
|
|
421
|
+
element_type = owner&.attribute(target)&.type
|
|
422
|
+
entity = element_type && owner.entities.find { |piece| piece.hecks_name == element_type.to_s }
|
|
423
|
+
return fields unless entity
|
|
424
|
+
|
|
425
|
+
entity.attributes.each do |attribute|
|
|
426
|
+
next if fields.key?(attribute.name)
|
|
427
|
+
|
|
428
|
+
fields[attribute.name] = attribute.list? ? [] : Runtime::Instance.default_for(aggregate, attribute)
|
|
429
|
+
end
|
|
430
|
+
fields
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def resolve_mutation_append_field(source, before_scope, args, aggregate, command)
|
|
179
434
|
return source unless source.is_a?(Symbol)
|
|
180
|
-
return
|
|
435
|
+
return before_scope[source] unless args.key?(source)
|
|
436
|
+
|
|
437
|
+
coerce_recompute_append_arg(aggregate, command, source, args[source])
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# `Interpreting#coerce_declared_arguments`'s own coercion,
|
|
441
|
+
# reproduced independently (never calling it again, the same
|
|
442
|
+
# "never agree with itself" rule this whole module's header
|
|
443
|
+
# comment gives) — a raw arg is coerced only when its own name
|
|
444
|
+
# (`source`) is one of the acting command's own declared
|
|
445
|
+
# attributes, exactly the condition that method checks before a
|
|
446
|
+
# real dispatch ever coerces it either. `command.attribute(source)`
|
|
447
|
+
# answering `nil` (a source that names no declared attribute —
|
|
448
|
+
# never possible for `coerce_declared_arguments` to have touched
|
|
449
|
+
# it in the real dispatch either) leaves `raw` exactly as it
|
|
450
|
+
# arrived, the same as every bare-scalar append this already
|
|
451
|
+
# handled correctly before BUG#5's fix.
|
|
452
|
+
#
|
|
453
|
+
# `Value.materialize`d immediately after coercing — matching the
|
|
454
|
+
# plain-data shape `entry[:before]`/`entry[:after]` already carry
|
|
455
|
+
# throughout this whole property, so the eventual `symbolize_deep`
|
|
456
|
+
# comparison is always materialized-against-materialized, never a
|
|
457
|
+
# live `Value` against a Hash.
|
|
458
|
+
def coerce_recompute_append_arg(aggregate, command, source, raw)
|
|
459
|
+
attribute = command.attribute(source)
|
|
460
|
+
return raw unless attribute
|
|
181
461
|
|
|
182
|
-
|
|
462
|
+
Runtime::Value.materialize(Runtime::Value.for_attribute(aggregate, attribute, raw, argument: true))
|
|
183
463
|
end
|
|
184
464
|
|
|
185
465
|
# `MutationApplier#removed`'s own value-equality match, reproduced.
|
|
@@ -192,7 +472,7 @@ module Hecks
|
|
|
192
472
|
# reproduced on plain materialized data instead of a real Value:
|
|
193
473
|
# a single-numeric-field Hash (the VO-typed case — ListCount, one
|
|
194
474
|
# Integer field) scales that field ; a bare Numeric scales itself.
|
|
195
|
-
# `current ||= 0` — the
|
|
475
|
+
# `current ||= 0` — the same phantom-field fallback #multiply
|
|
196
476
|
# itself already gives (unaffected by this session's #clamp fix,
|
|
197
477
|
# since #multiply never needed one).
|
|
198
478
|
def recompute_multiply(current, amount)
|
|
@@ -212,8 +492,8 @@ module Hecks
|
|
|
212
492
|
end
|
|
213
493
|
|
|
214
494
|
# `CommandRules::Arithmetic#clamp`'s own two branches, reproduced
|
|
215
|
-
# the same way #recompute_multiply is — including
|
|
216
|
-
#
|
|
495
|
+
# the same way #recompute_multiply is — including this session's
|
|
496
|
+
# own `current ||= 0` fix (command_rules/arithmetic.rb), the one
|
|
217
497
|
# arithmetic op that didn't have it until now. `mutation.source`
|
|
218
498
|
# is always a literal `[min, max]`, never an argument reference
|
|
219
499
|
# (MutationApplier's own comment on why `resolve_source` is
|
|
@@ -236,18 +516,18 @@ module Hecks
|
|
|
236
516
|
end
|
|
237
517
|
|
|
238
518
|
# `CommandRules::Arithmetic#resolve_source`, reproduced: a
|
|
239
|
-
# mutation's source is either the
|
|
240
|
-
#
|
|
519
|
+
# mutation's source is either the name of an argument or a
|
|
520
|
+
# literal, told apart by type.
|
|
241
521
|
def resolve_mutation_source(source, args)
|
|
242
522
|
source.is_a?(Symbol) ? args[source] : source
|
|
243
523
|
end
|
|
244
524
|
|
|
245
|
-
# A generated step's own `args` arrive with
|
|
525
|
+
# A generated step's own `args` arrive with string keys on every
|
|
246
526
|
# nested Hash (the wire/JSON shape `spec/corpus/*.json` already
|
|
247
527
|
# uses) while `history[:mutation_traces]`' own materialized
|
|
248
|
-
# before/after state carries
|
|
528
|
+
# before/after state carries symbol keys throughout (Runtime::
|
|
249
529
|
# Value.materialize's own convention) — two hashes holding the
|
|
250
|
-
# identical fact compare
|
|
530
|
+
# identical fact compare unequal by Ruby's own `Hash#==` unless
|
|
251
531
|
# both sides are normalized the same way first. Recursive, since
|
|
252
532
|
# an appended/removed element can itself nest a value object
|
|
253
533
|
# (RemoveTag's own `Tag` argument, `{"key"=>..., "value"=>...}`).
|