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
|
@@ -77,16 +77,16 @@ module Hecks
|
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
# `.find`/`.all`/`.count`/`.events`/`.repository` — the
|
|
80
|
+
# `.find`/`.all`/`.count`/`.events`/`.repository` — the same read/CRUD
|
|
81
81
|
# surface `Facade::Surface::AggregateDoor` gives a plain `Hecks
|
|
82
82
|
# .boot`, missing here until now: `install_namespace_entry` above
|
|
83
|
-
# installs
|
|
83
|
+
# installs one method per declared verb, so an aggregate with no
|
|
84
84
|
# commands or queries of its own shape (or simply never asked for
|
|
85
85
|
# a `find`-shaped query) had no way to look up one record by id
|
|
86
86
|
# through the router surface at all — real gap, hit live building
|
|
87
87
|
# a `List` aggregate meant to be read this way. Grouped by
|
|
88
88
|
# (realm, domain, aggregate) rather than installed per-verb,
|
|
89
|
-
# because unlike a command or query this is the
|
|
89
|
+
# because unlike a command or query this is the same five methods
|
|
90
90
|
# regardless of which verb happened to trigger this aggregate's
|
|
91
91
|
# own namespace module into existing.
|
|
92
92
|
def install_aggregate_doors!
|
data/lib/hecks/router.rb
CHANGED
|
@@ -47,7 +47,7 @@ module Hecks
|
|
|
47
47
|
entry = resolve(address)
|
|
48
48
|
raise WrongVerbKind, "#{address.inspect} names a query; use #query" unless entry.command?
|
|
49
49
|
|
|
50
|
-
entry.dispatcher.
|
|
50
|
+
entry.dispatcher.dispatch_flat(local_verb(entry), args)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def query(address, **args)
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Runtime
|
|
3
|
-
# A
|
|
4
|
-
# mechanism for every adapter that does
|
|
3
|
+
# **A process-wide, striped mutex registry** — the concurrency-control
|
|
4
|
+
# mechanism for every adapter that does not declare
|
|
5
5
|
# `:optimistic_concurrency` (Heki, Memory today; see
|
|
6
6
|
# `CommandInterpreter#call`/`EntityInterpreter#call`, which choose
|
|
7
7
|
# between this and Postgres's CAS+retry purely off
|
|
8
8
|
# `repository.capabilities`).
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# Why a lock suffices here and CAS is not needed: both adapters hold
|
|
11
11
|
# process-local data. `Adapters::Memory.tenant_capable?`'s own comment
|
|
12
12
|
# states the confirmed fact this relies on — two `Runtime.boot` calls
|
|
13
|
-
# get two entirely separate adapter instances; there is never a
|
|
14
|
-
#
|
|
15
|
-
# possibly other
|
|
13
|
+
# get two entirely separate adapter instances; there is never a second
|
|
14
|
+
# process writing the same Heki file or the same Memory Hash, only
|
|
15
|
+
# possibly other threads within this one process. A `Mutex` held for
|
|
16
16
|
# the full hydrate-through-save critical section closes the identical
|
|
17
17
|
# lost-update gap CAS closes for Postgres, with no schema, no version
|
|
18
18
|
# column, and no retry loop — the second thread simply doesn't start
|
|
19
19
|
# its own hydrate until the first thread's save has landed.
|
|
20
20
|
#
|
|
21
|
-
#
|
|
22
|
-
# id]`, so two dispatches against two
|
|
21
|
+
# **Striped, not one global lock**: keyed by `[domain, aggregate.hecks_name,
|
|
22
|
+
# id]`, so two dispatches against two different records never block
|
|
23
23
|
# each other. The registry Hash itself is guarded by its own top-level
|
|
24
24
|
# Mutex only for the moment a new per-key Mutex is created — two
|
|
25
|
-
# threads locking
|
|
25
|
+
# threads locking different keys for the first time never wait on one
|
|
26
26
|
# another beyond that brief creation window.
|
|
27
27
|
module AggregateLock
|
|
28
28
|
@registry_lock = Mutex.new
|
|
@@ -31,9 +31,9 @@ module Hecks
|
|
|
31
31
|
class << self
|
|
32
32
|
# `AggregateLock.for(domain, aggregate, id).synchronize { ... }`
|
|
33
33
|
# `id: nil` — identity could not be resolved yet (see
|
|
34
|
-
# `Identity.best_effort`) — locks by aggregate
|
|
34
|
+
# `Identity.best_effort`) — locks by aggregate type alone, coarser
|
|
35
35
|
# (every record of this aggregate serializes against every other)
|
|
36
|
-
# but still correct: it can only ever make dispatch
|
|
36
|
+
# but still correct: it can only ever make dispatch more
|
|
37
37
|
# conservative than a resolved id would.
|
|
38
38
|
def for(domain, aggregate, id = nil)
|
|
39
39
|
key = id.nil? ? [domain.to_s, aggregate.hecks_name] : [domain.to_s, aggregate.hecks_name, id.to_s]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Runtime
|
|
3
3
|
# A boot's own small set of phase-tagged, conditionally-present gates —
|
|
4
|
-
# ADR 0031. Registration is instance-scoped,
|
|
5
|
-
#
|
|
4
|
+
# ADR 0031. Registration is instance-scoped, one per `Loader.boot`
|
|
5
|
+
# call, never a module-level singleton: a process that boots more than
|
|
6
6
|
# one registry in its lifetime (every spec suite does) must never let
|
|
7
7
|
# one boot's capability profile leak into the next boot's gate list.
|
|
8
8
|
#
|
|
@@ -13,7 +13,7 @@ module Hecks
|
|
|
13
13
|
# order — today exactly one gate per phase, so ordering among
|
|
14
14
|
# same-phase gates has never been exercised.
|
|
15
15
|
#
|
|
16
|
-
# This is deliberately
|
|
16
|
+
# This is deliberately not the same registry `Hecks::Projector` uses
|
|
17
17
|
# (ADR 0027) — that one is a process-wide, static IR-in/artifact-out
|
|
18
18
|
# registry with no bindings and no live-state mutation; this one is
|
|
19
19
|
# per-boot and gates real I/O (a Postgres mint, a saga-store read).
|
data/lib/hecks/runtime/caller.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Runtime
|
|
3
|
-
#
|
|
3
|
+
# Who is dispatching, bound for the duration of a block — the ambient
|
|
4
4
|
# counterpart to `Runtime.with_registry`, but `Thread.current`-backed
|
|
5
|
-
# rather than a plain module ivar: registry-binding happens at
|
|
6
|
-
# time (single-threaded by construction), this happens at
|
|
5
|
+
# rather than a plain module ivar: registry-binding happens at boot
|
|
6
|
+
# time (single-threaded by construction), this happens at dispatch
|
|
7
7
|
# time, which a future per-request caller (docs/rails-integration.md's
|
|
8
8
|
# still-unbuilt `WebDoor`) needs to bind safely under concurrency.
|
|
9
9
|
#
|
|
@@ -12,15 +12,15 @@ module Hecks
|
|
|
12
12
|
# worker thread mid-dispatch today, so this is a fact worth naming
|
|
13
13
|
# rather than a gap worth solving.
|
|
14
14
|
module Caller
|
|
15
|
-
# `actor_id` is
|
|
15
|
+
# `actor_id` is optional, on top of the role that has always been
|
|
16
16
|
# here — a caller that states only a role (every caller before this)
|
|
17
17
|
# is checked the way it always was, string equality against the
|
|
18
|
-
# command's own `role`. A caller that also names
|
|
18
|
+
# command's own `role`. A caller that also names who it is lets
|
|
19
19
|
# `CommandRules::Authorization` check a real Governance
|
|
20
20
|
# `RoleAssignment` instead, once the command's domain has Governance
|
|
21
21
|
# attached — see that module's own header for the full split.
|
|
22
22
|
#
|
|
23
|
-
# `as_of` and `scope` are
|
|
23
|
+
# `as_of` and `scope` are both optional too, and both self-asserted
|
|
24
24
|
# by the caller rather than derived from the command — deliberately:
|
|
25
25
|
# `as_of` is filled at the door from `Ports::Clock.now`, never
|
|
26
26
|
# inside the interpreter (see `Ports::Clock`'s own header), so
|
|
@@ -31,7 +31,7 @@ module Hecks
|
|
|
31
31
|
# keeps it an application-boundary fact instead, the same shape
|
|
32
32
|
# `role`/`actor_id` already are. The tradeoff: a caller states what
|
|
33
33
|
# scope it is acting in, and `holds_role?` verifies a live grant
|
|
34
|
-
# exists for
|
|
34
|
+
# exists for that scope — it does not independently confirm the
|
|
35
35
|
# scope matches whatever the command's own target data belongs to.
|
|
36
36
|
Current = Struct.new(:role, :actor_id, :as_of, :scope, keyword_init: true)
|
|
37
37
|
|
|
@@ -49,7 +49,7 @@ module Hecks
|
|
|
49
49
|
Thread.current[:hecks_caller] = previous
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
# A
|
|
52
|
+
# A reaction is the system acting, not the caller who happened to be
|
|
53
53
|
# on the stack when the triggering command ran — `Dispatcher#reenter`
|
|
54
54
|
# clears the ambient caller around a reaction's own dispatch so a
|
|
55
55
|
# triggering caller's role can neither satisfy nor block a reaction
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Runtime
|
|
3
|
-
#
|
|
3
|
+
# Which ports a boot can actually fulfill, read off the registry it already
|
|
4
4
|
# holds. `registry.ports` names every port a domain declared a dependency
|
|
5
5
|
# on ; `registry.adapters` names every adapter wired to implement one — the
|
|
6
6
|
# same `adapter.port == port.name` match `Ports::Extraction` and
|
|
7
7
|
# `Ports::IdentityGeneration` already make for themselves, one at a time,
|
|
8
8
|
# each time they resolve. This is that same question asked once, for
|
|
9
|
-
# every port at once, so a gap in the wiring is something a caller can
|
|
9
|
+
# every port at once, so a gap in the wiring is something a caller can ask
|
|
10
10
|
# about rather than something a live dispatch discovers by refusing.
|
|
11
11
|
#
|
|
12
12
|
# `cycles` answers `[]`, always, and honestly: nothing in this port model
|
|
@@ -11,18 +11,18 @@ module Hecks
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
13
|
# Anything else used to ride along in the payload untouched —
|
|
14
|
-
# normalize_args walks the
|
|
14
|
+
# normalize_args walks the declared attributes, so a name the command
|
|
15
15
|
# never had was simply never looked at. A misspelled argument did
|
|
16
16
|
# nothing, in silence.
|
|
17
17
|
#
|
|
18
|
-
# The keys that are legitimately not attributes are the ones that
|
|
18
|
+
# The keys that are legitimately not attributes are the ones that address
|
|
19
19
|
# the aggregate rather than describe it : `id`, whatever the aggregate is
|
|
20
20
|
# identified by, and the reference key of the root a command reaches
|
|
21
21
|
# through. Refusing those would refuse every dispatch there is.
|
|
22
22
|
#
|
|
23
23
|
# `extra_identity_heads:` — EntityInterpreter's own callers only. An
|
|
24
24
|
# entity dispatch addresses not just the root aggregate but every
|
|
25
|
-
# entity
|
|
25
|
+
# entity along the chain it walks to reach the piece the command
|
|
26
26
|
# actually belongs to (`Handler.Dispatch.Bind` is two hops), and each
|
|
27
27
|
# hop's own `identity_heads` is addressing the same way the root's
|
|
28
28
|
# are — `element_of` (entity_element.rb) reads them straight out of
|
|
@@ -33,24 +33,24 @@ module Hecks
|
|
|
33
33
|
addressing = [:id, *aggregate.identity_heads, *extra_identity_heads, reference_key(command)] +
|
|
34
34
|
correlation_keys(domain)
|
|
35
35
|
known = (command.attributes.map(&:name) + addressing).compact.map(&:to_sym)
|
|
36
|
-
#
|
|
36
|
+
# Sorted. Payload order is whatever the caller happened to write, and
|
|
37
37
|
# refusal wording is contract — pinned byte-for-byte by the corpus, so
|
|
38
38
|
# it cannot depend on hash iteration order.
|
|
39
39
|
unknown = (args.keys.map(&:to_sym) - known).sort
|
|
40
40
|
return if unknown.empty?
|
|
41
41
|
|
|
42
42
|
raise UnknownArgument,
|
|
43
|
-
RefusalWording.
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
RefusalWording.render_site("UnknownArgument", "unknown_args",
|
|
44
|
+
command: command.hecks_name, unknown: unknown,
|
|
45
|
+
declared: declared_names(command))
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# And it takes
|
|
48
|
+
# And it takes all of them. The other half of the same sentence, missing
|
|
49
49
|
# until fuzz went looking : a name the command never declared was refused,
|
|
50
|
-
# while a name it
|
|
50
|
+
# while a name it did declare could simply be left out.
|
|
51
51
|
#
|
|
52
52
|
# `Customer.Register` without its `name` used to be refused — but by
|
|
53
|
-
#
|
|
53
|
+
# accident, and with a lie for a message. `then_set :name, to: :name` found
|
|
54
54
|
# nothing to resolve, passed the literal symbol on, and coercion reported
|
|
55
55
|
# `name is a PersonName — pass its fields as an object`, which describes a
|
|
56
56
|
# mistake the caller did not make. The real mistake — an argument simply
|
|
@@ -60,16 +60,16 @@ module Hecks
|
|
|
60
60
|
# all eight chapters, zero — so there is no optional argument for this to
|
|
61
61
|
# step on. Every declared attribute is a fact the command needs.
|
|
62
62
|
#
|
|
63
|
-
# `aggregate:` —
|
|
63
|
+
# `aggregate:` — present only for a port operation. An aggregate
|
|
64
64
|
# command's own self-address never reaches this list at all
|
|
65
65
|
# (`CommandBuilder#reference_to`'s bare self-reference mints no
|
|
66
66
|
# attribute to be absent) — but `PortOperationBuilder#reference_to`
|
|
67
|
-
#
|
|
67
|
+
# always mints one (this file's own header on `PortOperation`: "no
|
|
68
68
|
# creates?/acts_on distinction to protect"), because an operation
|
|
69
|
-
# historically had no
|
|
69
|
+
# historically had no other way to say which record it addressed.
|
|
70
70
|
# `to:` is that other way now (`Dispatcher#port_invocation`
|
|
71
71
|
# promotes the identity attribute's own value out of the payload
|
|
72
|
-
# and into routing) — which left the attribute still
|
|
72
|
+
# and into routing) — which left the attribute still declared,
|
|
73
73
|
# still non-optional, and now never present in `args` at all: every
|
|
74
74
|
# operation with an identity attribute refused its own well-formed
|
|
75
75
|
# calls, dispatched exactly the way `to:` intends. Exempted here on
|
|
@@ -80,19 +80,19 @@ module Hecks
|
|
|
80
80
|
exempt = aggregate && command.respond_to?(:identity_attribute) &&
|
|
81
81
|
command.identity_attribute(aggregate.hecks_name)&.name
|
|
82
82
|
required = command.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym } - [exempt]
|
|
83
|
-
#
|
|
83
|
+
# Sorted, for the same reason the unknown list is : refusal wording is
|
|
84
84
|
# contract, and a pinned wording cannot depend on the order a set
|
|
85
85
|
# difference happens to be computed in.
|
|
86
86
|
absent = (required - given).sort
|
|
87
87
|
return if absent.empty?
|
|
88
88
|
|
|
89
89
|
raise AbsentArgument,
|
|
90
|
-
RefusalWording.
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
RefusalWording.render_site("AbsentArgument", "absent_args",
|
|
91
|
+
command: command.hecks_name, absent: absent,
|
|
92
|
+
declared: declared_names(command))
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
# A
|
|
95
|
+
# **A command that declares nothing still has to say so**. `Account
|
|
96
96
|
# .Freeze` is `reference_to Account` and no attributes at all, so
|
|
97
97
|
# `{declared}` rendered empty and the sentence trailed off mid-
|
|
98
98
|
# clause : "Freeze does not declare standing — it takes ". Read
|
|
@@ -104,17 +104,14 @@ module Hecks
|
|
|
104
104
|
# one attribute renders exactly as it did before, because refusal
|
|
105
105
|
# wording is contract and pinned byte-for-byte by the corpus (see
|
|
106
106
|
# `refuse_unknown_arguments`' own note on sorting for why).
|
|
107
|
-
def
|
|
108
|
-
declared = command.attributes.map(&:name)
|
|
109
|
-
declared.empty? ? "none" : declared.join(", ")
|
|
110
|
-
end
|
|
107
|
+
def declared_names(command) = command.attributes.map(&:name)
|
|
111
108
|
|
|
112
|
-
# What a process manager correlates by is
|
|
109
|
+
# What a process manager correlates by is routing, not description. A saga
|
|
113
110
|
# threads its correlation key through every leg it dispatches so the event
|
|
114
111
|
# each leg emits carries it and the next step can be correlated — so the key
|
|
115
112
|
# arrives on commands that never declare it, and legitimately.
|
|
116
113
|
#
|
|
117
|
-
# This is the weakest part of the gate. Correlation is the
|
|
114
|
+
# This is the weakest part of the gate. Correlation is the saga's business,
|
|
118
115
|
# and the better shape is for the saga to stamp its own key onto the event
|
|
119
116
|
# it caused rather than smuggle it through the command's payload. Until it
|
|
120
117
|
# does, refusing the key here would break every saga in the corpus.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "../value"
|
|
2
|
+
require_relative "../entity_element"
|
|
2
3
|
|
|
3
4
|
module Hecks
|
|
4
5
|
module Runtime
|
|
@@ -18,19 +19,19 @@ module Hecks
|
|
|
18
19
|
end
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
# A
|
|
22
|
+
# A case statement over a closed, declared set — every mutation op
|
|
22
23
|
# the grammar can emit gets its own branch, including the `else`
|
|
23
24
|
# backstop for the day a new op reaches this method undeclared (see
|
|
24
25
|
# its own comment). Splitting each branch into its own method would
|
|
25
26
|
# not reduce what a reader has to hold at once (each op's own
|
|
26
|
-
# comment already explains why
|
|
27
|
+
# comment already explains why it is shaped the way it is) and
|
|
27
28
|
# would obscure that the set is closed and exhaustive.
|
|
28
29
|
# rubocop:disable Lint/DuplicateBranch -- :delegate and :corrects
|
|
29
30
|
# both no-op here, for two unrelated documented reasons (see each
|
|
30
31
|
# branch's own comment below); merging would blur that distinction.
|
|
31
32
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
32
33
|
#
|
|
33
|
-
# `pre` —
|
|
34
|
+
# `pre` — the pre-dispatch state (C4.2): every source below reads
|
|
34
35
|
# it, every target is written to `instance`. A command's effects
|
|
35
36
|
# are one update set; declaration order carries no meaning, and
|
|
36
37
|
# build refuses a field written twice (`CommandBuilder#
|
|
@@ -53,7 +54,7 @@ module Hecks
|
|
|
53
54
|
current = pre[mutation.target]
|
|
54
55
|
# Vendored fix, not (yet) upstream hecks (migration plan
|
|
55
56
|
# task 9): see #rewrap_arithmetic_result's own comment below
|
|
56
|
-
# -- `amount` is wrapped
|
|
57
|
+
# -- `amount` is wrapped only when `current` already is, not
|
|
57
58
|
# merely because the target attribute exists.
|
|
58
59
|
amount = Value.for_attribute(aggregate, attribute, amount) if attribute && current.is_a?(Value)
|
|
59
60
|
result = @rules.arithmetic(current, amount, mutation.target, @rules.sign_of(mutation.op))
|
|
@@ -85,22 +86,22 @@ module Hecks
|
|
|
85
86
|
when :clamp
|
|
86
87
|
instance[mutation.target] = @rules.clamp(pre[mutation.target], mutation.source, mutation.target)
|
|
87
88
|
# `delegates_to` — CommandBuilder#delegates_to's own comment gives
|
|
88
|
-
# the full reasoning for storing it as a mutation at all. A
|
|
89
|
-
# no-op here, not a gap: it targets no field on
|
|
90
|
-
# `CommandInterpreter#step_delegate_to_entity`, a
|
|
89
|
+
# the full reasoning for storing it as a mutation at all. A real
|
|
90
|
+
# no-op here, not a gap: it targets no field on this instance —
|
|
91
|
+
# `CommandInterpreter#step_delegate_to_entity`, a later step in
|
|
91
92
|
# the same dispatch, is what actually applies it, against the
|
|
92
93
|
# target entity element `EntityElement.apply_to_element` reaches,
|
|
93
94
|
# never through this method.
|
|
94
95
|
when :delegate
|
|
95
96
|
nil
|
|
96
97
|
# `corrects` — CommandBuilder#corrects_impl's own comment gives
|
|
97
|
-
# the full reasoning for storing it as a mutation at all. A
|
|
98
|
-
# no-op here too: it targets no field on
|
|
99
|
-
# its own event name, and whether
|
|
98
|
+
# the full reasoning for storing it as a mutation at all. A real
|
|
99
|
+
# no-op here too: it targets no field on this instance either —
|
|
100
|
+
# its own event name, and whether this record has actually
|
|
100
101
|
# emitted it, is checked once, up front, by
|
|
101
102
|
# CommandRules::Admissibility#enforce_correction_target, not
|
|
102
103
|
# here. Whatever field this correction actually changes is an
|
|
103
|
-
#
|
|
104
|
+
# ordinary declared (or, for `reverses: true`, derived — see
|
|
104
105
|
# AggregateBuilder#seal_correction_targets) mutation of its own,
|
|
105
106
|
# applied by one of the branches above like any other.
|
|
106
107
|
when :corrects
|
|
@@ -119,9 +120,9 @@ module Hecks
|
|
|
119
120
|
# rubocop:enable Lint/DuplicateBranch
|
|
120
121
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
121
122
|
|
|
122
|
-
# A
|
|
123
|
-
# name something the
|
|
124
|
-
# back to the
|
|
123
|
+
# A caller-supplied arg, first -- an append's own field can also
|
|
124
|
+
# name something the subject already knows about itself, falling
|
|
125
|
+
# back to the aggregate's own current field when it isn't one — a
|
|
125
126
|
# caller appending at the end without computing or supplying a
|
|
126
127
|
# position (`position: :next_position`, a field the command never
|
|
127
128
|
# declares as an argument at all). `args.key?`, not a truthiness
|
|
@@ -148,9 +149,9 @@ module Hecks
|
|
|
148
149
|
value_object = aggregate.value_object(element_type)
|
|
149
150
|
value_object&.attributes&.each do |attribute|
|
|
150
151
|
held = fields[attribute.name]
|
|
151
|
-
# A
|
|
152
|
+
# A single-field value unwraps to its scalar so it bridges
|
|
152
153
|
# into the element's own (differently named) wrapper; a
|
|
153
|
-
#
|
|
154
|
+
# multi-field one (a `state(:en_passant_square)` Square
|
|
154
155
|
# copied off the record) has no scalar to stand in for it
|
|
155
156
|
# and is handed across whole — `Value.for_attribute` keeps a
|
|
156
157
|
# value of the element field's own type as it is.
|
|
@@ -164,7 +165,7 @@ module Hecks
|
|
|
164
165
|
fields)
|
|
165
166
|
end
|
|
166
167
|
|
|
167
|
-
#
|
|
168
|
+
# Frozen, like every other value the domain hands back. An
|
|
168
169
|
# appended list used to come back mutable, so a caller could
|
|
169
170
|
# push straight into an aggregate's own state after the
|
|
170
171
|
# dispatch had finished.
|
|
@@ -173,42 +174,49 @@ module Hecks
|
|
|
173
174
|
|
|
174
175
|
# Vendored addition, not (yet) upstream hecks (migration plan
|
|
175
176
|
# task 4): the removal counterpart to #appended -- matches by
|
|
176
|
-
#
|
|
177
|
-
# bluebook's own words: "so a concurrent Add can never be lost")
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
177
|
+
# value equality, element-wise, no read-modify-write (plan.
|
|
178
|
+
# bluebook's own words: "so a concurrent Add can never be lost")
|
|
179
|
+
# -- unless the target list is itself entity-typed (BUG#32,
|
|
180
|
+
# QualityControl ledger), in which case `EntityElement.
|
|
181
|
+
# list_element_match?` matches by the entity's own identity
|
|
182
|
+
# field instead -- see that method's own comment for the full
|
|
183
|
+
# "why identity, not whole-value equality" reasoning. `mutation.
|
|
184
|
+
# source` is a single field reference (`:dependency`), unlike
|
|
185
|
+
# append's field-map -- resolved and Value-coerced the same way
|
|
186
|
+
# increment/decrement already coerce their own amount, so the
|
|
187
|
+
# comparison is against a like-shaped Value, not a raw scalar
|
|
188
|
+
# against a wrapped one.
|
|
183
189
|
def removed(instance, aggregate, mutation, args)
|
|
184
190
|
value = @rules.resolve_source(mutation.source, args)
|
|
185
191
|
attribute = aggregate.attribute(mutation.target)
|
|
186
192
|
value = Value.for_attribute(aggregate, attribute, value) if attribute
|
|
187
|
-
Array(instance[mutation.target]).reject
|
|
193
|
+
Array(instance[mutation.target]).reject do |element|
|
|
194
|
+
EntityElement.list_element_match?(aggregate, attribute, element, value)
|
|
195
|
+
end
|
|
188
196
|
end
|
|
189
197
|
|
|
190
198
|
# Vendored fix, not (yet) upstream hecks (migration plan
|
|
191
199
|
# task 9): #apply's `:increment`/`:decrement`/`:multiply`
|
|
192
200
|
# branches used to wrap `amount` into a `Value` unconditionally
|
|
193
201
|
# whenever the target attribute existed, never checking whether
|
|
194
|
-
# `current` (the field's
|
|
195
|
-
# `instance[mutation.target]`) was
|
|
202
|
+
# `current` (the field's own existing value, read straight off
|
|
203
|
+
# `instance[mutation.target]`) was also wrapped -- the two sides
|
|
196
204
|
# of the same arithmetic call could disagree on Value-ness. On a
|
|
197
|
-
#
|
|
205
|
+
# phantom-created field this is the common case, not an edge
|
|
198
206
|
# one: `Instance.defaults`/`#default_for` leaves a VO-typed
|
|
199
207
|
# attribute with no declared `default:` genuinely absent (nil),
|
|
200
208
|
# and `CommandRules::Arithmetic#arithmetic`/`#multiply`'s own
|
|
201
|
-
# `current ||= 0` then turns that nil into a
|
|
209
|
+
# `current ||= 0` then turns that nil into a raw, unwrapped
|
|
202
210
|
# Integer `0` -- so `current.is_a?(Value) && amount.is_a?(Value)`
|
|
203
211
|
# read false even though `amount` (correctly wrapped by the old
|
|
204
212
|
# unconditional line) genuinely held a valid, correctly-typed
|
|
205
213
|
# number, and the primitive path's `unless amount.is_a?(Numeric)`
|
|
206
|
-
# guard refused it as a
|
|
214
|
+
# guard refused it as a type mismatch the caller never made --
|
|
207
215
|
# an artifact of this method's own asymmetric wrapping, not bad
|
|
208
216
|
# input.
|
|
209
217
|
#
|
|
210
|
-
# Fixed at the call site (above) by wrapping `amount`
|
|
211
|
-
# `current` is
|
|
218
|
+
# Fixed at the call site (above) by wrapping `amount` only when
|
|
219
|
+
# `current` is already a `Value` -- so an established VO-typed
|
|
212
220
|
# field (a multi-field Money balance, say, already hydrated from
|
|
213
221
|
# a prior save) keeps going through
|
|
214
222
|
# `Arithmetic#arithmetic_value_object` exactly as before (the
|
|
@@ -217,7 +225,7 @@ module Hecks
|
|
|
217
225
|
# `current` and raw `amount` both take the plain-Numeric path
|
|
218
226
|
# together. This method closes the other half: the plain-Numeric
|
|
219
227
|
# path returns a bare Ruby number, and if the attribute is
|
|
220
|
-
# itself VO-typed (the norm), that raw result needs the
|
|
228
|
+
# itself VO-typed (the norm), that raw result needs the same
|
|
221
229
|
# wrap `:set` already gives its own resolved value (`Value.for`)
|
|
222
230
|
# before it's stored, so a field's stored shape doesn't depend
|
|
223
231
|
# on which dispatch happened to mutate it first. A no-op
|
|
@@ -243,13 +251,23 @@ module Hecks
|
|
|
243
251
|
attribute = entity.attribute(entity.identified_by)
|
|
244
252
|
fields[entity.identified_by] = Value.from_identifier(aggregate, attribute, next_identity(current, entity))
|
|
245
253
|
else
|
|
246
|
-
check_entity_collision(aggregate, entity, current, fields)
|
|
254
|
+
EntityElement.check_entity_collision(aggregate, entity, current, fields)
|
|
247
255
|
end
|
|
248
256
|
fields[entity.lifecycle.field] ||= entity.lifecycle.default if entity.lifecycle
|
|
249
|
-
|
|
257
|
+
# BUG#12 — every one of this entity's own declared attributes
|
|
258
|
+
# the append mapping (and the identity/lifecycle filling just
|
|
259
|
+
# above) didn't already touch gets its own default, the same
|
|
260
|
+
# way a fresh aggregate's own attributes already do
|
|
261
|
+
# (`Instance.defaults`) — see `EntityElement#
|
|
262
|
+
# fill_declared_defaults`'s own comment for the full reasoning;
|
|
263
|
+
# shared rather than reimplemented so this aggregate-level
|
|
264
|
+
# entity creation and `EntityElement#appended_to_element`'s
|
|
265
|
+
# entity-nested-in-entity one can never drift on what "the
|
|
266
|
+
# default" means.
|
|
267
|
+
EntityElement.fill_declared_defaults(aggregate, entity, fields)
|
|
250
268
|
end
|
|
251
269
|
|
|
252
|
-
#
|
|
270
|
+
# The minted identity is one past the highest held (C4.5) — not
|
|
253
271
|
# `size + 1`, which repeats an identity the moment the list has
|
|
254
272
|
# ever shrunk. One strategy for the language, so the IR declares
|
|
255
273
|
# none; the Rust generators mint by the same rule.
|
|
@@ -258,37 +276,16 @@ module Hecks
|
|
|
258
276
|
held.max.to_i + 1
|
|
259
277
|
end
|
|
260
278
|
|
|
261
|
-
#
|
|
262
|
-
#
|
|
263
|
-
#
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
# than an ordinary duplicate row, because EntityInterpreter#element_of's
|
|
272
|
-
# `find_index` always matches the FIRST match, so the second becomes
|
|
273
|
-
# permanently unaddressable by any later command.
|
|
274
|
-
#
|
|
275
|
-
# Auto-minted entities never reach here — `identity_heads` for them
|
|
276
|
-
# is still checked at mint time by construction (`current.size + 1`
|
|
277
|
-
# can only repeat if something `remove:`s from the list between
|
|
278
|
-
# mints, which no real domain does today), so they can't be flagged
|
|
279
|
-
# by mistake.
|
|
280
|
-
def check_entity_collision(aggregate, entity, current, fields)
|
|
281
|
-
heads = entity.identity_heads
|
|
282
|
-
return if heads.empty?
|
|
283
|
-
|
|
284
|
-
collision = Array(current).find { |element| heads.all? { |head| element[head] == fields[head] } }
|
|
285
|
-
return unless collision
|
|
286
|
-
|
|
287
|
-
raise(AlreadyExists, RefusalWording.render("AlreadyExists", "entity_duplicate",
|
|
288
|
-
entity: entity.hecks_name, aggregate: aggregate.hecks_name,
|
|
289
|
-
identity: Identity.reading(entity),
|
|
290
|
-
offered: heads.map { |head| Rendering.describe(fields[head]) }.join(", ")))
|
|
291
|
-
end
|
|
279
|
+
# Moved to `EntityElement.check_entity_collision` (entity_element.rb)
|
|
280
|
+
# — BUG#145. Used to live here, called only from `#entity_element`
|
|
281
|
+
# above (an aggregate's own entity list, e.g. `Workspace.boards`).
|
|
282
|
+
# `EntityElement#appended_to_element`'s own nested-entity branch (an
|
|
283
|
+
# entity's own entity list one hop further in, e.g. `Board.cards`)
|
|
284
|
+
# needs the exact same guard — see that method's own call site and
|
|
285
|
+
# comment for why a caller-supplied nested identity was silently
|
|
286
|
+
# duplicating before this moved. Pure relocation, not a behavior
|
|
287
|
+
# change here: the check's own doc comment (heads/composite/
|
|
288
|
+
# auto-mint reasoning) now lives with the code, in entity_element.rb.
|
|
292
289
|
end
|
|
293
290
|
end
|
|
294
291
|
end
|