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
|
@@ -4,13 +4,14 @@ require_relative "value"
|
|
|
4
4
|
require_relative "refusal_wording"
|
|
5
5
|
require_relative "errors"
|
|
6
6
|
require_relative "identity"
|
|
7
|
+
require_relative "instance"
|
|
7
8
|
|
|
8
9
|
module Hecks
|
|
9
10
|
module Runtime
|
|
10
|
-
#
|
|
11
|
+
# **One entity element, located and mutated** — the walk-and-write half of
|
|
11
12
|
# dispatching into a piece an aggregate holds, factored out of
|
|
12
|
-
# `EntityInterpreter` so a
|
|
13
|
-
# `delegate_to_entity` step) can locate and mutate the
|
|
13
|
+
# `EntityInterpreter` so a second caller (`CommandInterpreter`'s own
|
|
14
|
+
# `delegate_to_entity` step) can locate and mutate the same element the
|
|
14
15
|
# same way, against an aggregate record it already holds in memory
|
|
15
16
|
# rather than one freshly loaded from a repository. `EntityInterpreter`
|
|
16
17
|
# keeps its own dispatch order and Context; this is the part underneath
|
|
@@ -24,13 +25,21 @@ module Hecks
|
|
|
24
25
|
module EntityElement
|
|
25
26
|
module_function
|
|
26
27
|
|
|
27
|
-
#
|
|
28
|
+
# BUG#3 — an addressing value that can never match a stored element,
|
|
29
|
+
# returned from `element_of`'s own `wants` coercion in place of a real
|
|
30
|
+
# `Value` (below). A unique object, never `nil`: a stored, optional
|
|
31
|
+
# element field really can hold `nil`, and comparing that against a
|
|
32
|
+
# bare `nil` sentinel would accidentally "match" it.
|
|
33
|
+
UNMATCHABLE = Object.new.freeze
|
|
34
|
+
private_constant :UNMATCHABLE
|
|
35
|
+
|
|
36
|
+
# **One hop per chain entry**. `container` starts as `instance` (the root
|
|
28
37
|
# aggregate record) and becomes each just-located element in turn —
|
|
29
|
-
# Dispatch's own element is found
|
|
38
|
+
# Dispatch's own element is found inside the Handler element
|
|
30
39
|
# `locate_chain` located the step before, never inside `instance`
|
|
31
|
-
# directly. `owner` is whichever construct's
|
|
40
|
+
# directly. `owner` is whichever construct's own attribute declares
|
|
32
41
|
# the list being searched (Handler declares `dispatches` ; the root
|
|
33
|
-
# aggregate declares `handlers`) — `root_aggregate` stays the
|
|
42
|
+
# aggregate declares `handlers`) — `root_aggregate` stays the root
|
|
34
43
|
# the whole way through instead, passed to `element_of` separately,
|
|
35
44
|
# because coercion (`Value.for_attribute`) resolves value objects
|
|
36
45
|
# against the root's own namespace only ; an entity must never
|
|
@@ -54,7 +63,7 @@ module Hecks
|
|
|
54
63
|
container
|
|
55
64
|
end
|
|
56
65
|
|
|
57
|
-
#
|
|
66
|
+
# One element, matched on every part of its identity — not just the first.
|
|
58
67
|
# A piece's identity may be several paths, the same shape a head's can be,
|
|
59
68
|
# so a dispatch that names the element has to supply every part and every
|
|
60
69
|
# part has to agree with the stored one. `routed_identity`, when given,
|
|
@@ -63,29 +72,58 @@ module Hecks
|
|
|
63
72
|
# which element it means, so re-deriving `wants` from `args` would be
|
|
64
73
|
# redundant at best and wrong if `args` no longer carries that identity
|
|
65
74
|
# at all.
|
|
66
|
-
# Locate, then copy-before-mutate, in that order — see the "
|
|
67
|
-
#
|
|
75
|
+
# Locate, then copy-before-mutate, in that order — see the "one
|
|
76
|
+
# level deeper" comment below on why the copy has to happen exactly
|
|
68
77
|
# where it does (aliasing the adapter's own record otherwise).
|
|
69
78
|
# Splitting resolution from the copy/write-back would separate two
|
|
70
79
|
# halves of one aliasing-safety invariant across method boundaries.
|
|
71
80
|
# rubocop:disable-next Metrics/AbcSize
|
|
72
81
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
73
82
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
83
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
74
84
|
def element_of(root_aggregate, owner, entity, command_name, container, args, routed_identity = nil)
|
|
75
85
|
entity_name = entity.hecks_name
|
|
76
86
|
list_attr = owner.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
|
|
77
|
-
raise(UnknownVerb, RefusalWording.
|
|
78
|
-
|
|
87
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_holds_no_list",
|
|
88
|
+
aggregate: owner.hecks_name, entity: entity_name))
|
|
79
89
|
|
|
80
90
|
wants = unless routed_identity
|
|
81
91
|
entity.identity_paths.map do |path|
|
|
82
92
|
head = path.to_s.split(".").first.to_sym
|
|
83
93
|
raw = args[head] ||
|
|
84
|
-
raise(NotFound, RefusalWording.
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
raise(NotFound, RefusalWording.render_site("NotFound", "entity_element_no_identity",
|
|
95
|
+
command: command_name, entity: entity_name,
|
|
96
|
+
identity: Identity.reading(entity)))
|
|
97
|
+
|
|
98
|
+
# An identity offered for addressing, not for storage
|
|
99
|
+
# (BUG#3, found live by `bin/qa_sweep` — banking fuzz seed
|
|
100
|
+
# 23, `LedgerEntry.Amend sequence: { value: 0 }` against an
|
|
101
|
+
# entry-less ledger). Coercing it all the way to a typed
|
|
102
|
+
# `Value` here ran that type's own invariant before this
|
|
103
|
+
# method ever checks whether any element matches — a
|
|
104
|
+
# `sequence: 0` against `LedgerSequence`'s own "a ledger
|
|
105
|
+
# sequence is positive" invariant raised InvariantViolation,
|
|
106
|
+
# not NotFound, even when (as here) nothing was ever posted
|
|
107
|
+
# at all. Every element actually in the list already
|
|
108
|
+
# satisfied its own type's invariant the moment it was
|
|
109
|
+
# created, so a value that fails it can never equal one —
|
|
110
|
+
# degrading to `UNMATCHABLE` here, instead of propagating,
|
|
111
|
+
# is exactly as safe as the ordinary "no match found" case
|
|
112
|
+
# below, and lines this addressing path up with the two
|
|
113
|
+
# conventions it already disagreed with: Rust's own
|
|
114
|
+
# `extract_id`/`extract_wants` (a raw scalar read, never a
|
|
115
|
+
# typed rebuild — rust/src/generated/*/*.rs) and
|
|
116
|
+
# `Identity.from`'s own raw-comparison convention for a
|
|
117
|
+
# root aggregate's identity (this file's sibling,
|
|
118
|
+
# `identity.rb`). `raw` rides alongside `want` so the
|
|
119
|
+
# eventual NotFound below can still quote what was offered.
|
|
120
|
+
want = begin
|
|
121
|
+
Value.for_attribute(root_aggregate, entity.attribute(head), raw)
|
|
122
|
+
rescue InvariantViolation
|
|
123
|
+
UNMATCHABLE
|
|
124
|
+
end
|
|
87
125
|
|
|
88
|
-
[head, path,
|
|
126
|
+
[head, path, want, raw]
|
|
89
127
|
end
|
|
90
128
|
end
|
|
91
129
|
|
|
@@ -93,29 +131,31 @@ module Hecks
|
|
|
93
131
|
position = if routed_identity
|
|
94
132
|
original.find_index { |element| element_identity(entity, element).to_s == routed_identity.to_s }
|
|
95
133
|
else
|
|
96
|
-
original.find_index
|
|
134
|
+
original.find_index do |el|
|
|
135
|
+
wants.all? { |head, _path, want, _raw| want != UNMATCHABLE && el[head] == want }
|
|
136
|
+
end
|
|
97
137
|
end
|
|
98
138
|
unless position
|
|
99
|
-
raise NotFound, RefusalWording.
|
|
139
|
+
raise NotFound, RefusalWording.render_site(
|
|
100
140
|
"NotFound", "entity_element_missing",
|
|
101
141
|
entity: entity_name, identity: Identity.reading(entity),
|
|
102
|
-
wants: wants&.map { |_h, path,
|
|
142
|
+
wants: wants&.map { |_h, path, _want, raw| Identity.scalar(path, raw) }&.join(", "),
|
|
103
143
|
aggregate: owner.hecks_name,
|
|
104
144
|
parent_id: container.respond_to?(:id) ? container.id.inspect : Rendering.describe(container)
|
|
105
145
|
)
|
|
106
146
|
end
|
|
107
147
|
|
|
108
|
-
#
|
|
148
|
+
# One level deeper than Instance#dup, for the same reason: a list
|
|
109
149
|
# attribute holds Hashes, and `apply_to_element` mutates the found
|
|
110
|
-
# one
|
|
150
|
+
# one in place — the update mechanism for an entity, not a bug. But
|
|
111
151
|
# in place means aliased with the adapter's own record until this
|
|
112
152
|
# copies the array and the target element before handing either
|
|
113
153
|
# back, and writes the fresh array into `container` so the copy is
|
|
114
|
-
# what persists on success and
|
|
154
|
+
# what persists on success and nothing aliased survives a refusal.
|
|
115
155
|
# `container[list_attr.name] = copied` reaches `instance` itself
|
|
116
|
-
# when this is the
|
|
117
|
-
#
|
|
118
|
-
#
|
|
156
|
+
# when this is the first hop, and reaches the (already copied)
|
|
157
|
+
# parent element when it is a later one — either way it is the
|
|
158
|
+
# same already-fresh object `locate_chain` is about to hand back
|
|
119
159
|
# as `container` for the next hop, so nothing further has to
|
|
120
160
|
# propagate a write back up the chain by hand.
|
|
121
161
|
copied = original.dup
|
|
@@ -125,9 +165,9 @@ module Hecks
|
|
|
125
165
|
element
|
|
126
166
|
end
|
|
127
167
|
|
|
128
|
-
#
|
|
129
|
-
# twin of `Identity.of`, reading off the
|
|
130
|
-
# than a dispatch payload. An id is a
|
|
168
|
+
# The element's own identity, joined from its parts — the entity-level
|
|
169
|
+
# twin of `Identity.of`, reading off the stored element (a Hash) rather
|
|
170
|
+
# than a dispatch payload. An id is a scalar, and the path is how it is
|
|
131
171
|
# reached — never by opening a value object and taking whatever single
|
|
132
172
|
# field is inside. That unwrapping is gone from the language: a piece
|
|
133
173
|
# that does not name its fields is refused when the bluebook loads ("an
|
|
@@ -151,10 +191,10 @@ module Hecks
|
|
|
151
191
|
# refuses what it cannot check applying nothing instead. `Member`/
|
|
152
192
|
# `Dispatch` (S17) are the first real callers: both need to
|
|
153
193
|
# append a value-object-typed element (`Pair`/`Binding`) onto a
|
|
154
|
-
# list attribute
|
|
194
|
+
# list attribute they own, once they become entities of
|
|
155
195
|
# `ValueObject`/`ProcessManager` rather than separate aggregates.
|
|
156
196
|
#
|
|
157
|
-
# `:increment`/`:decrement`/`:multiply`
|
|
197
|
+
# `:increment`/`:decrement`/`:multiply` also fixed here, found
|
|
158
198
|
# while proving this method against a real fixture: they wrapped
|
|
159
199
|
# `amount` unconditionally whenever `attribute` existed, the same
|
|
160
200
|
# asymmetric-wrapping shape `MutationApplier#rewrap_arithmetic_
|
|
@@ -174,7 +214,7 @@ module Hecks
|
|
|
174
214
|
# shape here without doing the same there would break that
|
|
175
215
|
# intentional mirroring, which is what lets the two be diffed
|
|
176
216
|
# against each other when one gets a fix the other needs too.
|
|
177
|
-
# rubocop:disable-next Metrics/AbcSize
|
|
217
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
178
218
|
# `pre` — the element as it was before this command (C4.2): every
|
|
179
219
|
# read below goes through it, every write lands on `element`.
|
|
180
220
|
def apply_to_element(rules, aggregate, entity, element, mutation, args, pre = element)
|
|
@@ -203,6 +243,22 @@ module Hecks
|
|
|
203
243
|
element[mutation.target] = rewrap_arithmetic_result(aggregate, attribute, current, result)
|
|
204
244
|
when :clamp
|
|
205
245
|
element[mutation.target] = rules.clamp(pre[mutation.target], mutation.source, mutation.target)
|
|
246
|
+
# `corrects` — BUG#30. `MutationApplier#apply`'s own aggregate-
|
|
247
|
+
# level `:corrects` branch's own comment gives the full reasoning;
|
|
248
|
+
# the same one applies here unchanged: this mutation targets no
|
|
249
|
+
# field on this element at all — its own event name, and whether
|
|
250
|
+
# the owning record has actually emitted it, was already checked
|
|
251
|
+
# once, up front, by `EntityInterpreter#step_enforce_givens`
|
|
252
|
+
# (`CommandRules::Admissibility#enforce_correction_target`, called
|
|
253
|
+
# there against the parent record/root aggregate — see that
|
|
254
|
+
# step's own comment for exactly why). Whatever field a correction
|
|
255
|
+
# actually changes is an ordinary declared `sets`/`increment`/etc.
|
|
256
|
+
# mutation of its own, applied by one of the branches above like
|
|
257
|
+
# any other — `qa/stress_domains/corrections`' own `Entry.Amend`
|
|
258
|
+
# pairs `corrects "EntryRecorded", ...` with a separate `sets
|
|
259
|
+
# :amount`, exactly this shape.
|
|
260
|
+
when :corrects
|
|
261
|
+
nil
|
|
206
262
|
else
|
|
207
263
|
# The aggregate-level twin's own backstop
|
|
208
264
|
# (MutationApplier#apply), for the same reason: applying
|
|
@@ -226,8 +282,8 @@ module Hecks
|
|
|
226
282
|
end
|
|
227
283
|
|
|
228
284
|
# `MutationApplier#resolve_append_source`'s own entity-scoped
|
|
229
|
-
# twin — a caller-supplied
|
|
230
|
-
#
|
|
285
|
+
# twin — a caller-supplied arg first, falling back to the
|
|
286
|
+
# element's own current field (never the parent instance's) when
|
|
231
287
|
# it isn't one.
|
|
232
288
|
def resolve_element_append_source(source, element, args)
|
|
233
289
|
return source unless source.is_a?(Symbol)
|
|
@@ -236,17 +292,28 @@ module Hecks
|
|
|
236
292
|
element[source]
|
|
237
293
|
end
|
|
238
294
|
|
|
239
|
-
# `MutationApplier#appended`'s own entity-scoped twin.
|
|
240
|
-
#
|
|
295
|
+
# `MutationApplier#appended`'s own entity-scoped twin. Usually a
|
|
296
|
+
# value object element — an entity's own list, appended to by an
|
|
241
297
|
# entity-owned command, holds a value object (`Member.pairs`'
|
|
242
298
|
# own `Pair`, `Dispatch.with_spec`'s own `Binding`) the same way
|
|
243
|
-
#
|
|
244
|
-
# list of
|
|
245
|
-
# `
|
|
246
|
-
#
|
|
247
|
-
#
|
|
248
|
-
#
|
|
249
|
-
#
|
|
299
|
+
# most real corpus appends do — but entity-in-entity nesting (a
|
|
300
|
+
# list of another entity, owned by this one) is real now too:
|
|
301
|
+
# `qa/stress_domains/nested_pieces` (`Board.AddCard`, appending a
|
|
302
|
+
# `Card` onto `Board`'s own `cards`) is the first corpus member to
|
|
303
|
+
# do it, the comment this replaces having been written before that
|
|
304
|
+
# domain existed. `element_type` naming an entity rather than a
|
|
305
|
+
# value object falls through to `fields` unchanged, same as
|
|
306
|
+
# before — `MutationApplier#entity_element`'s own identity-minting
|
|
307
|
+
# fallback still isn't mirrored here (nothing in this corpus needs
|
|
308
|
+
# auto-minting at this depth — Card supplies its own identity in
|
|
309
|
+
# the append mapping) — but its collision-checking fallback now is
|
|
310
|
+
# (BUG#145, `check_entity_collision`, below — see its own call
|
|
311
|
+
# site's comment for why "collision-checking a nested entity is its
|
|
312
|
+
# own separate, unfixed question," this comment's own prior wording,
|
|
313
|
+
# stopped being true). BUG#12's fix (below) is likewise shared:
|
|
314
|
+
# every declared attribute the append mapping doesn't name gets its
|
|
315
|
+
# own default the same way a fresh aggregate's own attributes
|
|
316
|
+
# already do (`Instance.defaults`), whichever branch built `fields`.
|
|
250
317
|
def appended_to_element(aggregate, entity, element, mutation, args)
|
|
251
318
|
fields = mutation.source.transform_values { |source| resolve_element_append_source(source, element, args) }
|
|
252
319
|
element_type = entity.attribute(mutation.target)&.type
|
|
@@ -254,18 +321,187 @@ module Hecks
|
|
|
254
321
|
value_object&.attributes&.each do |attribute|
|
|
255
322
|
fields[attribute.name] = Value.scalar(fields[attribute.name]) if fields[attribute.name].is_a?(Value)
|
|
256
323
|
end
|
|
257
|
-
appended =
|
|
324
|
+
appended =
|
|
325
|
+
if value_object
|
|
326
|
+
Value.build(value_object, fields, aggregate)
|
|
327
|
+
else
|
|
328
|
+
# `entity.entities`, not `aggregate.entities` — a piece
|
|
329
|
+
# nested inside a piece is a child of the owning entity
|
|
330
|
+
# (`Card` is `Board.entities`, never `Workspace.entities`;
|
|
331
|
+
# `Behaviour::Entity#entities` answers direct children only,
|
|
332
|
+
# by design — see its own comment), the same lexical-nesting
|
|
333
|
+
# rule `EntityBuilder#entity_impl` builds the tree with in
|
|
334
|
+
# the first place.
|
|
335
|
+
nested_entity = entity.entities.find { |piece| piece.hecks_name == element_type.to_s }
|
|
336
|
+
if nested_entity
|
|
337
|
+
# BUG#145 — `MutationApplier#entity_element`'s own
|
|
338
|
+
# `check_entity_collision` call (one hop up, an aggregate's
|
|
339
|
+
# own entity list) never had a twin here: a nested entity's
|
|
340
|
+
# own identity is always caller-supplied at this depth
|
|
341
|
+
# (`Card.sequence` rides the append mapping directly — no
|
|
342
|
+
# nested-entity auto-mint exists anywhere in this corpus,
|
|
343
|
+
# see this method's own header), so this is unconditional,
|
|
344
|
+
# unlike `#entity_element`'s own auto-mint/collision `if`/
|
|
345
|
+
# `else` split — there is no auto-mint branch here to skip.
|
|
346
|
+
# Found live: `qa/stress_domains/nested_pieces`'s own
|
|
347
|
+
# differential sweep, `Board.AddCard` dispatched twice under
|
|
348
|
+
# the same `sequence` — Ruby silently appended a second
|
|
349
|
+
# `Card`, Rust's generated code (`rust/project/mutations.rb`'s
|
|
350
|
+
# `emit_mutation_line_body`, which never splits an
|
|
351
|
+
# aggregate-owned append from an entity-owned one the way
|
|
352
|
+
# this runtime's two separate methods do) already refused
|
|
353
|
+
# `AlreadyExists` for both depths.
|
|
354
|
+
# `entity` (the owner — `Board`), not `aggregate` (the root
|
|
355
|
+
# — `Workspace`), is what the refusal names as "on {…}" —
|
|
356
|
+
# `check_entity_collision`'s first argument is only ever
|
|
357
|
+
# used for that one naming purpose (`owner.hecks_name`,
|
|
358
|
+
# below), matching Rust's own generated wording exactly
|
|
359
|
+
# (`rust/project/mutations.rb`'s own `collision_guard`
|
|
360
|
+
# passes the entity's declaring construct's name the same
|
|
361
|
+
# way — "a Card already exists on Board", never "…on
|
|
362
|
+
# Workspace").
|
|
363
|
+
check_entity_collision(entity, nested_entity, element[mutation.target], fields)
|
|
364
|
+
fill_declared_defaults(aggregate, nested_entity, fields)
|
|
365
|
+
else
|
|
366
|
+
fields
|
|
367
|
+
end
|
|
368
|
+
end
|
|
258
369
|
Freezer.deep(Array(element[mutation.target]) + [appended])
|
|
259
370
|
end
|
|
260
371
|
|
|
372
|
+
# BUG#12 — an entity created via `sets :list, append: {...}` used
|
|
373
|
+
# to leave any of its own declared attributes the append mapping
|
|
374
|
+
# simply didn't name (an optional field a later, separate command
|
|
375
|
+
# sets — `Board.label`, `Card.note`) absent from the stored hash
|
|
376
|
+
# entirely, not even a `nil` placeholder, until that later command
|
|
377
|
+
# actually ran. `rust/project/json_codec.rb#emit_to_json_flat`'s
|
|
378
|
+
# own header comment documents the opposite as the intended
|
|
379
|
+
# contract for a persisted record: every declared field present,
|
|
380
|
+
# `null` when unset, "because Ruby's own `JSON.generate(state)`
|
|
381
|
+
# round-trip this mirrors does the same" — true for a freshly
|
|
382
|
+
# created aggregate (`Instance.defaults` already fills one key per
|
|
383
|
+
# declared attribute, `default_for` per attribute), never true for
|
|
384
|
+
# an entity minted by an append. This closes that gap the same
|
|
385
|
+
# way: `Instance.default_for` is the same per-attribute default
|
|
386
|
+
# rule (nil with no declared `default:`, a fully-defaulted value
|
|
387
|
+
# object when every one of its own fields has one), reused rather
|
|
388
|
+
# than reimplemented so the two creation paths can never drift on
|
|
389
|
+
# what "the default" means. Additive only — a key `fields` already
|
|
390
|
+
# holds (the append mapping, an auto-minted identity, a lifecycle
|
|
391
|
+
# default) is never overwritten.
|
|
392
|
+
def fill_declared_defaults(aggregate, entity, fields)
|
|
393
|
+
entity.attributes.each do |attribute|
|
|
394
|
+
next if fields.key?(attribute.name)
|
|
395
|
+
|
|
396
|
+
fields[attribute.name] = attribute.list? ? Freezer.deep([]) : Instance.default_for(aggregate, attribute)
|
|
397
|
+
end
|
|
398
|
+
fields
|
|
399
|
+
end
|
|
400
|
+
|
|
261
401
|
# `MutationApplier#removed`'s own entity-scoped twin — matches by
|
|
262
|
-
#
|
|
263
|
-
# never be lost" reasoning that method's own comment gives
|
|
402
|
+
# value equality, element-wise, the same "so a concurrent Add can
|
|
403
|
+
# never be lost" reasoning that method's own comment gives, unless
|
|
404
|
+
# the list this targets is itself entity-typed — see
|
|
405
|
+
# `list_element_match?`, below, which both this and
|
|
406
|
+
# `MutationApplier#removed` now share.
|
|
264
407
|
def removed_from_element(rules, aggregate, entity, element, mutation, args)
|
|
265
408
|
value = rules.resolve_source(mutation.source, args)
|
|
266
409
|
attribute = entity.attribute(mutation.target)
|
|
267
410
|
value = Value.for_attribute(aggregate, attribute, value) if attribute
|
|
268
|
-
Array(element[mutation.target]).reject { |candidate| candidate
|
|
411
|
+
Array(element[mutation.target]).reject { |candidate| list_element_match?(aggregate, attribute, candidate, value) }
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# BUG#32 (QualityControl ledger) — the match rule `remove:` uses
|
|
415
|
+
# against one stored list element. Value equality for a
|
|
416
|
+
# value-object-typed list stays exactly what it always was — an
|
|
417
|
+
# element and `value` are both real `Value`s there, so `==` already
|
|
418
|
+
# compares every field, the "concurrent Add can never be lost"
|
|
419
|
+
# shape `removed`/`removed_from_element`'s own headers describe.
|
|
420
|
+
# An entity-typed list is different in kind, not just in type: a
|
|
421
|
+
# stored element is a plain Hash, never a `Value` (`Entity`'s own
|
|
422
|
+
# header — "an entity must never answer .value_object"), so there
|
|
423
|
+
# is no whole-value shape to compare against at all — only the
|
|
424
|
+
# entity's own identity field, the same field a caller already has
|
|
425
|
+
# to name to address that element any other way
|
|
426
|
+
# (`element_of`'s own `wants`, above). `value` arrives here already
|
|
427
|
+
# coerced against that identity field's declared type
|
|
428
|
+
# (`Coercion#hydrate_entity_identity`, run underneath
|
|
429
|
+
# `Value.for_attribute` before either caller above ever sees it),
|
|
430
|
+
# so this only has to know which field to read off the stored
|
|
431
|
+
# element — `entity.identity_heads`'s own single head, when there
|
|
432
|
+
# is exactly one. A composite identity (more than one head, or
|
|
433
|
+
# none) has no single field a bare `remove:` target could mean —
|
|
434
|
+
# this returns `false` (never a match, the same documented no-op
|
|
435
|
+
# `hydrate_entity_identity`'s own header already commits to) rather
|
|
436
|
+
# than guessing which head, matching the narrow, honest boundary
|
|
437
|
+
# `MutationApplier#check_entity_collision`'s own header draws for
|
|
438
|
+
# entity identity elsewhere in this runtime.
|
|
439
|
+
#
|
|
440
|
+
# Shared by `MutationApplier#removed` (an aggregate's own list) and
|
|
441
|
+
# `#removed_from_element` (a list an entity owns), so the two
|
|
442
|
+
# `remove:` call sites can never quietly disagree on what
|
|
443
|
+
# "matches" means — the same reasoning this file's own header
|
|
444
|
+
# gives for centralizing `locate_chain`/`element_of` once rather
|
|
445
|
+
# than twice.
|
|
446
|
+
def list_element_match?(aggregate, attribute, element, value)
|
|
447
|
+
entity = attribute&.list? ? Value.find_entity(aggregate, attribute.type.to_s) : nil
|
|
448
|
+
return element == value unless entity
|
|
449
|
+
|
|
450
|
+
head = entity.identity_heads.one? ? entity.identity_heads.first : nil
|
|
451
|
+
return false unless head
|
|
452
|
+
|
|
453
|
+
element.is_a?(Hash) && element[head] == value
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# BUG#13 (PR #549) — the same check #hydrate gives every creating
|
|
457
|
+
# aggregate command (`repository.find(id)`,
|
|
458
|
+
# `command_interpreter.rb`), one level down. Originally lived in
|
|
459
|
+
# `MutationApplier` (mutation_applier.rb), called only from
|
|
460
|
+
# `#entity_element` — an aggregate's own entity list (`Workspace.
|
|
461
|
+
# boards`, `Ledger.entries`). Moved here (BUG#145) so `#appended_
|
|
462
|
+
# to_element`, above — an entity's own nested entity list one hop
|
|
463
|
+
# further in (`Board.cards`) — can share it too, rather than
|
|
464
|
+
# reimplementing it a second time the same way `#list_element_
|
|
465
|
+
# match?` already avoids that split for `remove:`.
|
|
466
|
+
#
|
|
467
|
+
# Reached, at the aggregate-owned call site, only on the two
|
|
468
|
+
# branches that do not auto-mint: a caller-supplied identity (the
|
|
469
|
+
# field is already in the append's own field map) or a composite
|
|
470
|
+
# one (`entity.identified_by` is nil for those — Runtime::
|
|
471
|
+
# Identified#derive_identity). Neither used to check the sibling
|
|
472
|
+
# list at all: a second LogVisit with the same date+sequence, or a
|
|
473
|
+
# second IssueKey with the same serial, appended a silent
|
|
474
|
+
# duplicate — worse than an ordinary duplicate row, because
|
|
475
|
+
# `EntityElement#element_of`'s own `find_index` always matches the
|
|
476
|
+
# first match, so the second becomes permanently unaddressable by
|
|
477
|
+
# any later command. At the entity-owned call site (`#appended_to_
|
|
478
|
+
# element`), there is no auto-mint branch at all — every caller
|
|
479
|
+
# reaches this unconditionally, since a nested entity's own
|
|
480
|
+
# identity is always caller-supplied in this corpus.
|
|
481
|
+
#
|
|
482
|
+
# Auto-minted (aggregate-owned) entities never reach here —
|
|
483
|
+
# `identity_heads` for them is still checked at mint time by
|
|
484
|
+
# construction (`current.size + 1` can only repeat if something
|
|
485
|
+
# `remove:`s from the list between mints, which no real domain does
|
|
486
|
+
# today), so they can't be flagged by mistake.
|
|
487
|
+
# `owner` — the declaring construct named in the "…already exists on
|
|
488
|
+
# {owner}" wording: the root aggregate at the aggregate-owned call
|
|
489
|
+
# site (`Workspace`, `Ledger`), the immediately-enclosing entity at
|
|
490
|
+
# the entity-owned one (`Board` — never the root `Workspace` two
|
|
491
|
+
# hops up). Used for that naming purpose only (`owner.hecks_name`) —
|
|
492
|
+
# never for `Value`/namespace resolution, which is why an `Entity`
|
|
493
|
+
# (not just an `Aggregate`) is a valid thing to pass here.
|
|
494
|
+
def check_entity_collision(owner, entity, current, fields)
|
|
495
|
+
heads = entity.identity_heads
|
|
496
|
+
return if heads.empty?
|
|
497
|
+
|
|
498
|
+
collision = Array(current).find { |element| heads.all? { |head| element[head] == fields[head] } }
|
|
499
|
+
return unless collision
|
|
500
|
+
|
|
501
|
+
raise(AlreadyExists, RefusalWording.render_site("AlreadyExists", "entity_duplicate",
|
|
502
|
+
entity: entity.hecks_name, aggregate: owner.hecks_name,
|
|
503
|
+
identity: Identity.reading(entity),
|
|
504
|
+
offered: heads.map { |head| Rendering.describe(fields[head]) }))
|
|
269
505
|
end
|
|
270
506
|
end
|
|
271
507
|
end
|