hecks 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
|
@@ -8,10 +8,10 @@ require_relative "instance"
|
|
|
8
8
|
|
|
9
9
|
module Hecks
|
|
10
10
|
module Runtime
|
|
11
|
-
#
|
|
11
|
+
# **One entity element, located and mutated** — the walk-and-write half of
|
|
12
12
|
# dispatching into a piece an aggregate holds, factored out of
|
|
13
|
-
# `EntityInterpreter` so a
|
|
14
|
-
# `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
|
|
15
15
|
# same way, against an aggregate record it already holds in memory
|
|
16
16
|
# rather than one freshly loaded from a repository. `EntityInterpreter`
|
|
17
17
|
# keeps its own dispatch order and Context; this is the part underneath
|
|
@@ -27,19 +27,22 @@ module Hecks
|
|
|
27
27
|
|
|
28
28
|
# BUG#3 — an addressing value that can never match a stored element,
|
|
29
29
|
# returned from `element_of`'s own `wants` coercion in place of a real
|
|
30
|
-
# `Value` (below). A unique object, never `nil`: a stored,
|
|
31
|
-
# element field really can hold `nil`, and comparing
|
|
30
|
+
# `Value` (below). A unique object, never `nil`: a stored, optional
|
|
31
|
+
# element field really can hold `nil`, and comparing that against a
|
|
32
32
|
# bare `nil` sentinel would accidentally "match" it.
|
|
33
33
|
UNMATCHABLE = Object.new.freeze
|
|
34
34
|
private_constant :UNMATCHABLE
|
|
35
35
|
|
|
36
|
-
#
|
|
36
|
+
# Walks `chain`, one hop per entry, and returns the located element (or
|
|
37
|
+
# `instance` itself, when `chain` is empty).
|
|
38
|
+
#
|
|
39
|
+
# **One hop per chain entry**. `container` starts as `instance` (the root
|
|
37
40
|
# aggregate record) and becomes each just-located element in turn —
|
|
38
|
-
# Dispatch's own element is found
|
|
41
|
+
# Dispatch's own element is found inside the Handler element
|
|
39
42
|
# `locate_chain` located the step before, never inside `instance`
|
|
40
|
-
# directly. `owner` is whichever construct's
|
|
43
|
+
# directly. `owner` is whichever construct's own attribute declares
|
|
41
44
|
# the list being searched (Handler declares `dispatches` ; the root
|
|
42
|
-
# aggregate declares `handlers`) — `root_aggregate` stays the
|
|
45
|
+
# aggregate declares `handlers`) — `root_aggregate` stays the root
|
|
43
46
|
# the whole way through instead, passed to `element_of` separately,
|
|
44
47
|
# because coercion (`Value.for_attribute`) resolves value objects
|
|
45
48
|
# against the root's own namespace only ; an entity must never
|
|
@@ -52,6 +55,22 @@ module Hecks
|
|
|
52
55
|
# split's own consequence: an entity's own identity may arrive via
|
|
53
56
|
# `to:` rather than duplicated into `args`, so each hop is offered
|
|
54
57
|
# its routed identity ahead of falling back to `args`.
|
|
58
|
+
#
|
|
59
|
+
# @param root_aggregate [Bluebook::Aggregate] the root aggregate record's
|
|
60
|
+
# own construct, held constant across every hop for coercion
|
|
61
|
+
# @param chain [Array<Bluebook::Entity>] the entity chain to walk, root-first
|
|
62
|
+
# @param instance [Runtime::Instance] the root aggregate record to walk from
|
|
63
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments,
|
|
64
|
+
# read for each hop's own identity when `route` does not supply it
|
|
65
|
+
# @param command_name [String] the command name, quoted in a refusal
|
|
66
|
+
# @param route [Runtime::Routing::Envelope, nil] the call's resolved routing
|
|
67
|
+
# envelope, if any; its own `entities` supply each hop's identity first
|
|
68
|
+
# @return [Runtime::Instance, Hash{Symbol => Object}] the located element;
|
|
69
|
+
# `instance` itself, unchanged, when `chain` is empty
|
|
70
|
+
# @raise [Runtime::UnknownVerb] if a hop's owner declares no list attribute
|
|
71
|
+
# for that entity
|
|
72
|
+
# @raise [Runtime::NotFound] if a hop's identity is absent from `args`, or no
|
|
73
|
+
# element matches it
|
|
55
74
|
def locate_chain(root_aggregate, chain, instance, args, command_name, route = nil)
|
|
56
75
|
container = instance
|
|
57
76
|
owner = root_aggregate
|
|
@@ -63,7 +82,7 @@ module Hecks
|
|
|
63
82
|
container
|
|
64
83
|
end
|
|
65
84
|
|
|
66
|
-
#
|
|
85
|
+
# One element, matched on every part of its identity — not just the first.
|
|
67
86
|
# A piece's identity may be several paths, the same shape a head's can be,
|
|
68
87
|
# so a dispatch that names the element has to supply every part and every
|
|
69
88
|
# part has to agree with the stored one. `routed_identity`, when given,
|
|
@@ -72,8 +91,8 @@ module Hecks
|
|
|
72
91
|
# which element it means, so re-deriving `wants` from `args` would be
|
|
73
92
|
# redundant at best and wrong if `args` no longer carries that identity
|
|
74
93
|
# at all.
|
|
75
|
-
# Locate, then copy-before-mutate, in that order — see the "
|
|
76
|
-
#
|
|
94
|
+
# Locate, then copy-before-mutate, in that order — see the "one
|
|
95
|
+
# level deeper" comment below on why the copy has to happen exactly
|
|
77
96
|
# where it does (aliasing the adapter's own record otherwise).
|
|
78
97
|
# Splitting resolution from the copy/write-back would separate two
|
|
79
98
|
# halves of one aliasing-safety invariant across method boundaries.
|
|
@@ -81,30 +100,48 @@ module Hecks
|
|
|
81
100
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
82
101
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
83
102
|
# rubocop:disable-next Metrics/MethodLength
|
|
103
|
+
#
|
|
104
|
+
# @param root_aggregate [Bluebook::Aggregate] the root aggregate record's own
|
|
105
|
+
# construct, used for value-object coercion
|
|
106
|
+
# @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose own
|
|
107
|
+
# attribute declares the list `entity` is searched in
|
|
108
|
+
# @param entity [Bluebook::Entity] the entity type being located
|
|
109
|
+
# @param command_name [String] the command name, quoted in a refusal
|
|
110
|
+
# @param container [Runtime::Instance, Hash{Symbol => Object}] the record or
|
|
111
|
+
# element holding the list to search
|
|
112
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments,
|
|
113
|
+
# read for the element's own identity when `routed_identity` is nil
|
|
114
|
+
# @param routed_identity [String, nil] the routing envelope's own identity
|
|
115
|
+
# string for this hop, matched directly instead of deriving one from `args`
|
|
116
|
+
# @return [Hash{Symbol => Object}] a fresh copy of the located element; the
|
|
117
|
+
# owning list inside `container` is replaced with a fresh copy too
|
|
118
|
+
# @raise [Runtime::UnknownVerb] if `owner` declares no list attribute for `entity`
|
|
119
|
+
# @raise [Runtime::NotFound] if an identity part is absent from `args`
|
|
120
|
+
# (`routed_identity` nil only), or no element matches
|
|
84
121
|
def element_of(root_aggregate, owner, entity, command_name, container, args, routed_identity = nil)
|
|
85
122
|
entity_name = entity.hecks_name
|
|
86
123
|
list_attr = owner.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
|
|
87
|
-
raise(UnknownVerb, RefusalWording.
|
|
88
|
-
|
|
124
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_holds_no_list",
|
|
125
|
+
aggregate: owner.hecks_name, entity: entity_name))
|
|
89
126
|
|
|
90
127
|
wants = unless routed_identity
|
|
91
128
|
entity.identity_paths.map do |path|
|
|
92
129
|
head = path.to_s.split(".").first.to_sym
|
|
93
130
|
raw = args[head] ||
|
|
94
|
-
raise(NotFound, RefusalWording.
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
raise(NotFound, RefusalWording.render_site("NotFound", "entity_element_no_identity",
|
|
132
|
+
command: command_name, entity: entity_name,
|
|
133
|
+
identity: Identity.reading(entity)))
|
|
97
134
|
|
|
98
|
-
#
|
|
135
|
+
# An identity offered for addressing, not for storage
|
|
99
136
|
# (BUG#3, found live by `bin/qa_sweep` — banking fuzz seed
|
|
100
137
|
# 23, `LedgerEntry.Amend sequence: { value: 0 }` against an
|
|
101
138
|
# entry-less ledger). Coercing it all the way to a typed
|
|
102
|
-
# `Value` here ran that type's own invariant
|
|
139
|
+
# `Value` here ran that type's own invariant before this
|
|
103
140
|
# method ever checks whether any element matches — a
|
|
104
141
|
# `sequence: 0` against `LedgerSequence`'s own "a ledger
|
|
105
142
|
# sequence is positive" invariant raised InvariantViolation,
|
|
106
143
|
# not NotFound, even when (as here) nothing was ever posted
|
|
107
|
-
# at all. Every element actually
|
|
144
|
+
# at all. Every element actually in the list already
|
|
108
145
|
# satisfied its own type's invariant the moment it was
|
|
109
146
|
# created, so a value that fails it can never equal one —
|
|
110
147
|
# degrading to `UNMATCHABLE` here, instead of propagating,
|
|
@@ -114,7 +151,7 @@ module Hecks
|
|
|
114
151
|
# `extract_id`/`extract_wants` (a raw scalar read, never a
|
|
115
152
|
# typed rebuild — rust/src/generated/*/*.rs) and
|
|
116
153
|
# `Identity.from`'s own raw-comparison convention for a
|
|
117
|
-
#
|
|
154
|
+
# root aggregate's identity (this file's sibling,
|
|
118
155
|
# `identity.rb`). `raw` rides alongside `want` so the
|
|
119
156
|
# eventual NotFound below can still quote what was offered.
|
|
120
157
|
want = begin
|
|
@@ -136,7 +173,7 @@ module Hecks
|
|
|
136
173
|
end
|
|
137
174
|
end
|
|
138
175
|
unless position
|
|
139
|
-
raise NotFound, RefusalWording.
|
|
176
|
+
raise NotFound, RefusalWording.render_site(
|
|
140
177
|
"NotFound", "entity_element_missing",
|
|
141
178
|
entity: entity_name, identity: Identity.reading(entity),
|
|
142
179
|
wants: wants&.map { |_h, path, _want, raw| Identity.scalar(path, raw) }&.join(", "),
|
|
@@ -145,17 +182,17 @@ module Hecks
|
|
|
145
182
|
)
|
|
146
183
|
end
|
|
147
184
|
|
|
148
|
-
#
|
|
185
|
+
# One level deeper than Instance#dup, for the same reason: a list
|
|
149
186
|
# attribute holds Hashes, and `apply_to_element` mutates the found
|
|
150
|
-
# one
|
|
187
|
+
# one in place — the update mechanism for an entity, not a bug. But
|
|
151
188
|
# in place means aliased with the adapter's own record until this
|
|
152
189
|
# copies the array and the target element before handing either
|
|
153
190
|
# back, and writes the fresh array into `container` so the copy is
|
|
154
|
-
# what persists on success and
|
|
191
|
+
# what persists on success and nothing aliased survives a refusal.
|
|
155
192
|
# `container[list_attr.name] = copied` reaches `instance` itself
|
|
156
|
-
# when this is the
|
|
157
|
-
#
|
|
158
|
-
#
|
|
193
|
+
# when this is the first hop, and reaches the (already copied)
|
|
194
|
+
# parent element when it is a later one — either way it is the
|
|
195
|
+
# same already-fresh object `locate_chain` is about to hand back
|
|
159
196
|
# as `container` for the next hop, so nothing further has to
|
|
160
197
|
# propagate a write back up the chain by hand.
|
|
161
198
|
copied = original.dup
|
|
@@ -165,14 +202,20 @@ module Hecks
|
|
|
165
202
|
element
|
|
166
203
|
end
|
|
167
204
|
|
|
168
|
-
#
|
|
169
|
-
# twin of `Identity.of`, reading off the
|
|
170
|
-
# than a dispatch payload. An id is a
|
|
205
|
+
# The element's own identity, joined from its parts — the entity-level
|
|
206
|
+
# twin of `Identity.of`, reading off the stored element (a Hash) rather
|
|
207
|
+
# than a dispatch payload. An id is a scalar, and the path is how it is
|
|
171
208
|
# reached — never by opening a value object and taking whatever single
|
|
172
209
|
# field is inside. That unwrapping is gone from the language: a piece
|
|
173
210
|
# that does not name its fields is refused when the bluebook loads ("an
|
|
174
211
|
# entity says what it is known by", "an identity part names something"),
|
|
175
212
|
# so by the time a dispatch arrives here there is always a path to dig.
|
|
213
|
+
#
|
|
214
|
+
# @param entity [Bluebook::Entity] the entity type whose identity paths
|
|
215
|
+
# are read
|
|
216
|
+
# @param element [Hash{Symbol => Object}] the stored element to read an
|
|
217
|
+
# identity off
|
|
218
|
+
# @return [String] the element's identity, joined from its declared parts
|
|
176
219
|
def element_identity(entity, element)
|
|
177
220
|
parts = entity.identity_paths.map do |path|
|
|
178
221
|
head = path.to_s.split(".").first.to_sym
|
|
@@ -191,10 +234,10 @@ module Hecks
|
|
|
191
234
|
# refuses what it cannot check applying nothing instead. `Member`/
|
|
192
235
|
# `Dispatch` (S17) are the first real callers: both need to
|
|
193
236
|
# append a value-object-typed element (`Pair`/`Binding`) onto a
|
|
194
|
-
# list attribute
|
|
237
|
+
# list attribute they own, once they become entities of
|
|
195
238
|
# `ValueObject`/`ProcessManager` rather than separate aggregates.
|
|
196
239
|
#
|
|
197
|
-
# `:increment`/`:decrement`/`:multiply`
|
|
240
|
+
# `:increment`/`:decrement`/`:multiply` also fixed here, found
|
|
198
241
|
# while proving this method against a real fixture: they wrapped
|
|
199
242
|
# `amount` unconditionally whenever `attribute` existed, the same
|
|
200
243
|
# asymmetric-wrapping shape `MutationApplier#rewrap_arithmetic_
|
|
@@ -215,8 +258,32 @@ module Hecks
|
|
|
215
258
|
# intentional mirroring, which is what lets the two be diffed
|
|
216
259
|
# against each other when one gets a fix the other needs too.
|
|
217
260
|
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
261
|
+
# Applies one declared mutation to an entity element, in place.
|
|
262
|
+
#
|
|
218
263
|
# `pre` — the element as it was before this command (C4.2): every
|
|
219
264
|
# read below goes through it, every write lands on `element`.
|
|
265
|
+
#
|
|
266
|
+
# @param rules [Runtime::CommandRules] the shared rules engine `resolve_source`/
|
|
267
|
+
# `arithmetic`/`multiply`/`clamp`/`sign_of` are read through
|
|
268
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
269
|
+
# coercion
|
|
270
|
+
# @param entity [Bluebook::Entity] the entity type `element` is an instance of
|
|
271
|
+
# @param element [Hash{Symbol => Object}] the element to mutate; written in place
|
|
272
|
+
# @param mutation [Bluebook::Mutation] the declared mutation to apply
|
|
273
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments,
|
|
274
|
+
# a mutation's source may read from
|
|
275
|
+
# @param pre [Hash{Symbol => Object}] the element as it stood before this
|
|
276
|
+
# command; every read goes through this, every write lands on `element`
|
|
277
|
+
# @return [void]
|
|
278
|
+
# @raise [Runtime::TypeMismatch] if a mutation's own value cannot be coerced,
|
|
279
|
+
# or an arithmetic op's operands are not numeric or compatible value objects
|
|
280
|
+
# @raise [Runtime::InvariantViolation] if a coerced or arithmetic-derived value
|
|
281
|
+
# object breaks one of its own invariants
|
|
282
|
+
# @raise [Runtime::AlreadyExists] if an `:append` mints a nested entity whose
|
|
283
|
+
# identity collides with an existing element
|
|
284
|
+
# @raise [Bluebook::Expression::EvaluationError] if an arithmetic op's product
|
|
285
|
+
# does not fit a signed 64-bit Integer, or is a non-finite Float
|
|
286
|
+
# @raise [Runtime::WiringError] if `mutation.op` names no handled mutation kind
|
|
220
287
|
def apply_to_element(rules, aggregate, entity, element, mutation, args, pre = element)
|
|
221
288
|
case mutation.op
|
|
222
289
|
when :set
|
|
@@ -246,13 +313,13 @@ module Hecks
|
|
|
246
313
|
# `corrects` — BUG#30. `MutationApplier#apply`'s own aggregate-
|
|
247
314
|
# level `:corrects` branch's own comment gives the full reasoning;
|
|
248
315
|
# the same one applies here unchanged: this mutation targets no
|
|
249
|
-
# field on
|
|
250
|
-
# the
|
|
316
|
+
# field on this element at all — its own event name, and whether
|
|
317
|
+
# the owning record has actually emitted it, was already checked
|
|
251
318
|
# once, up front, by `EntityInterpreter#step_enforce_givens`
|
|
252
319
|
# (`CommandRules::Admissibility#enforce_correction_target`, called
|
|
253
|
-
# there against the
|
|
320
|
+
# there against the parent record/root aggregate — see that
|
|
254
321
|
# step's own comment for exactly why). Whatever field a correction
|
|
255
|
-
# actually changes is an
|
|
322
|
+
# actually changes is an ordinary declared `sets`/`increment`/etc.
|
|
256
323
|
# mutation of its own, applied by one of the branches above like
|
|
257
324
|
# any other — `qa/stress_domains/corrections`' own `Entry.Amend`
|
|
258
325
|
# pairs `corrects "EntryRecorded", ...` with a separate `sets
|
|
@@ -269,22 +336,47 @@ module Hecks
|
|
|
269
336
|
end
|
|
270
337
|
end
|
|
271
338
|
|
|
339
|
+
# Rewraps a plain-Numeric arithmetic result into `attribute`'s own declared
|
|
340
|
+
# value-object type, when the arithmetic itself ran unwrapped.
|
|
341
|
+
#
|
|
272
342
|
# `MutationApplier#rewrap_arithmetic_result`'s own entity-scoped
|
|
273
343
|
# twin, byte-for-byte the same fix — see that method's own
|
|
274
344
|
# comment for the full "phantom-field asymmetric wrapping" story.
|
|
275
345
|
# A no-op whenever `current` was already a Value (the arithmetic
|
|
276
346
|
# call already returned one) or the mutation targets no declared
|
|
277
347
|
# attribute at all.
|
|
348
|
+
#
|
|
349
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
350
|
+
# coercion
|
|
351
|
+
# @param attribute [Bluebook::Attribute, nil] the mutated field's own declared
|
|
352
|
+
# attribute; a no-op when nil (targets no declared attribute)
|
|
353
|
+
# @param current [Object] the field's pre-mutation value, as read off `pre`
|
|
354
|
+
# @param result [Object] the arithmetic op's own result
|
|
355
|
+
# @return [Runtime::Value, Object] `result` unchanged when already a `Value`,
|
|
356
|
+
# `current` is a `Value`, or `attribute` is nil; otherwise `result` coerced
|
|
357
|
+
# into `attribute`'s own declared type
|
|
278
358
|
def rewrap_arithmetic_result(aggregate, attribute, current, result)
|
|
279
359
|
return result if current.is_a?(Value) || attribute.nil? || result.is_a?(Value)
|
|
280
360
|
|
|
281
361
|
Value.for_attribute(aggregate, attribute, result)
|
|
282
362
|
end
|
|
283
363
|
|
|
364
|
+
# Resolves an `:append` mutation's own field source against the offered
|
|
365
|
+
# arguments, falling back to the element's own current field.
|
|
366
|
+
#
|
|
284
367
|
# `MutationApplier#resolve_append_source`'s own entity-scoped
|
|
285
|
-
# twin — a caller-supplied
|
|
286
|
-
#
|
|
368
|
+
# twin — a caller-supplied arg first, falling back to the
|
|
369
|
+
# element's own current field (never the parent instance's) when
|
|
287
370
|
# it isn't one.
|
|
371
|
+
#
|
|
372
|
+
# @param source [Symbol, Object] the mutation's own field source: a Symbol
|
|
373
|
+
# names a command argument or, failing that, an element field; anything
|
|
374
|
+
# else is returned as is
|
|
375
|
+
# @param element [Hash{Symbol => Object}] the element to fall back to reading
|
|
376
|
+
# from
|
|
377
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments
|
|
378
|
+
# @return [Object, nil] the named argument's value; failing that, the named
|
|
379
|
+
# element field's value; `source` itself when it is not a Symbol
|
|
288
380
|
def resolve_element_append_source(source, element, args)
|
|
289
381
|
return source unless source.is_a?(Symbol)
|
|
290
382
|
return args[source] if args.key?(source)
|
|
@@ -293,25 +385,41 @@ module Hecks
|
|
|
293
385
|
end
|
|
294
386
|
|
|
295
387
|
# `MutationApplier#appended`'s own entity-scoped twin. Usually a
|
|
296
|
-
#
|
|
388
|
+
# value object element — an entity's own list, appended to by an
|
|
297
389
|
# entity-owned command, holds a value object (`Member.pairs`'
|
|
298
390
|
# own `Pair`, `Dispatch.with_spec`'s own `Binding`) the same way
|
|
299
391
|
# most real corpus appends do — but entity-in-entity nesting (a
|
|
300
|
-
# list of
|
|
392
|
+
# list of another entity, owned by this one) is real now too:
|
|
301
393
|
# `qa/stress_domains/nested_pieces` (`Board.AddCard`, appending a
|
|
302
394
|
# `Card` onto `Board`'s own `cards`) is the first corpus member to
|
|
303
395
|
# do it, the comment this replaces having been written before that
|
|
304
396
|
# domain existed. `element_type` naming an entity rather than a
|
|
305
397
|
# value object falls through to `fields` unchanged, same as
|
|
306
|
-
# before — `MutationApplier#entity_element`'s own identity-minting
|
|
307
|
-
#
|
|
308
|
-
#
|
|
309
|
-
#
|
|
310
|
-
#
|
|
311
|
-
#
|
|
312
|
-
#
|
|
313
|
-
#
|
|
314
|
-
#
|
|
398
|
+
# before — `MutationApplier#entity_element`'s own identity-minting
|
|
399
|
+
# fallback still isn't mirrored here (nothing in this corpus needs
|
|
400
|
+
# auto-minting at this depth — Card supplies its own identity in
|
|
401
|
+
# the append mapping) — but its collision-checking fallback now is
|
|
402
|
+
# (BUG#145, `check_entity_collision`, below — see its own call
|
|
403
|
+
# site's comment for why "collision-checking a nested entity is its
|
|
404
|
+
# own separate, unfixed question," this comment's own prior wording,
|
|
405
|
+
# stopped being true). BUG#12's fix (below) is likewise shared:
|
|
406
|
+
# every declared attribute the append mapping doesn't name gets its
|
|
407
|
+
# own default the same way a fresh aggregate's own attributes
|
|
408
|
+
# already do (`Instance.defaults`), whichever branch built `fields`.
|
|
409
|
+
#
|
|
410
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
411
|
+
# coercion
|
|
412
|
+
# @param entity [Bluebook::Entity] the entity type `element`'s mutated list
|
|
413
|
+
# attribute belongs to
|
|
414
|
+
# @param element [Hash{Symbol => Object}] the element being appended to, read
|
|
415
|
+
# as the source for a field the mutation's own map does not supply directly
|
|
416
|
+
# @param mutation [Bluebook::Mutation] the declared `:append` mutation
|
|
417
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments
|
|
418
|
+
# @return [Array] `element[mutation.target]`'s existing elements, frozen deep,
|
|
419
|
+
# with the newly built element (a `Runtime::Value`, or a Hash for an entity
|
|
420
|
+
# or untyped element) appended last
|
|
421
|
+
# @raise [Runtime::AlreadyExists] if the appended element is a nested entity
|
|
422
|
+
# whose identity collides with an existing one
|
|
315
423
|
def appended_to_element(aggregate, entity, element, mutation, args)
|
|
316
424
|
fields = mutation.source.transform_values { |source| resolve_element_append_source(source, element, args) }
|
|
317
425
|
element_type = entity.attribute(mutation.target)&.type
|
|
@@ -323,22 +431,53 @@ module Hecks
|
|
|
323
431
|
if value_object
|
|
324
432
|
Value.build(value_object, fields, aggregate)
|
|
325
433
|
else
|
|
326
|
-
# `entity.entities`,
|
|
327
|
-
# nested inside a piece is a child of the
|
|
434
|
+
# `entity.entities`, not `aggregate.entities` — a piece
|
|
435
|
+
# nested inside a piece is a child of the owning entity
|
|
328
436
|
# (`Card` is `Board.entities`, never `Workspace.entities`;
|
|
329
437
|
# `Behaviour::Entity#entities` answers direct children only,
|
|
330
438
|
# by design — see its own comment), the same lexical-nesting
|
|
331
439
|
# rule `EntityBuilder#entity_impl` builds the tree with in
|
|
332
440
|
# the first place.
|
|
333
441
|
nested_entity = entity.entities.find { |piece| piece.hecks_name == element_type.to_s }
|
|
334
|
-
|
|
442
|
+
if nested_entity
|
|
443
|
+
# BUG#145 — `MutationApplier#entity_element`'s own
|
|
444
|
+
# `check_entity_collision` call (one hop up, an aggregate's
|
|
445
|
+
# own entity list) never had a twin here: a nested entity's
|
|
446
|
+
# own identity is always caller-supplied at this depth
|
|
447
|
+
# (`Card.sequence` rides the append mapping directly — no
|
|
448
|
+
# nested-entity auto-mint exists anywhere in this corpus,
|
|
449
|
+
# see this method's own header), so this is unconditional,
|
|
450
|
+
# unlike `#entity_element`'s own auto-mint/collision `if`/
|
|
451
|
+
# `else` split — there is no auto-mint branch here to skip.
|
|
452
|
+
# Found live: `qa/stress_domains/nested_pieces`'s own
|
|
453
|
+
# differential sweep, `Board.AddCard` dispatched twice under
|
|
454
|
+
# the same `sequence` — Ruby silently appended a second
|
|
455
|
+
# `Card`, Rust's generated code (`rust/project/mutations.rb`'s
|
|
456
|
+
# `emit_mutation_line_body`, which never splits an
|
|
457
|
+
# aggregate-owned append from an entity-owned one the way
|
|
458
|
+
# this runtime's two separate methods do) already refused
|
|
459
|
+
# `AlreadyExists` for both depths.
|
|
460
|
+
# `entity` (the owner — `Board`), not `aggregate` (the root
|
|
461
|
+
# — `Workspace`), is what the refusal names as "on {…}" —
|
|
462
|
+
# `check_entity_collision`'s first argument is only ever
|
|
463
|
+
# used for that one naming purpose (`owner.hecks_name`,
|
|
464
|
+
# below), matching Rust's own generated wording exactly
|
|
465
|
+
# (`rust/project/mutations.rb`'s own `collision_guard`
|
|
466
|
+
# passes the entity's declaring construct's name the same
|
|
467
|
+
# way — "a Card already exists on Board", never "…on
|
|
468
|
+
# Workspace").
|
|
469
|
+
check_entity_collision(entity, nested_entity, element[mutation.target], fields)
|
|
470
|
+
fill_declared_defaults(aggregate, nested_entity, fields)
|
|
471
|
+
else
|
|
472
|
+
fields
|
|
473
|
+
end
|
|
335
474
|
end
|
|
336
475
|
Freezer.deep(Array(element[mutation.target]) + [appended])
|
|
337
476
|
end
|
|
338
477
|
|
|
339
478
|
# BUG#12 — an entity created via `sets :list, append: {...}` used
|
|
340
|
-
# to leave any of its
|
|
341
|
-
# simply didn't name (an optional field a
|
|
479
|
+
# to leave any of its own declared attributes the append mapping
|
|
480
|
+
# simply didn't name (an optional field a later, separate command
|
|
342
481
|
# sets — `Board.label`, `Card.note`) absent from the stored hash
|
|
343
482
|
# entirely, not even a `nil` placeholder, until that later command
|
|
344
483
|
# actually ran. `rust/project/json_codec.rb#emit_to_json_flat`'s
|
|
@@ -346,16 +485,25 @@ module Hecks
|
|
|
346
485
|
# contract for a persisted record: every declared field present,
|
|
347
486
|
# `null` when unset, "because Ruby's own `JSON.generate(state)`
|
|
348
487
|
# round-trip this mirrors does the same" — true for a freshly
|
|
349
|
-
# created
|
|
488
|
+
# created aggregate (`Instance.defaults` already fills one key per
|
|
350
489
|
# declared attribute, `default_for` per attribute), never true for
|
|
351
490
|
# an entity minted by an append. This closes that gap the same
|
|
352
|
-
# way: `Instance.default_for` is the
|
|
491
|
+
# way: `Instance.default_for` is the same per-attribute default
|
|
353
492
|
# rule (nil with no declared `default:`, a fully-defaulted value
|
|
354
|
-
# object when every one of
|
|
493
|
+
# object when every one of its own fields has one), reused rather
|
|
355
494
|
# than reimplemented so the two creation paths can never drift on
|
|
356
495
|
# what "the default" means. Additive only — a key `fields` already
|
|
357
496
|
# holds (the append mapping, an auto-minted identity, a lifecycle
|
|
358
497
|
# default) is never overwritten.
|
|
498
|
+
#
|
|
499
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
500
|
+
# coercion
|
|
501
|
+
# @param entity [Bluebook::Entity] the entity type whose declared attributes
|
|
502
|
+
# are defaulted
|
|
503
|
+
# @param fields [Hash{Symbol => Object}] the element fields already resolved;
|
|
504
|
+
# written in place
|
|
505
|
+
# @return [Hash{Symbol => Object}] `fields`, with every declared attribute it
|
|
506
|
+
# did not already hold filled with its own default
|
|
359
507
|
def fill_declared_defaults(aggregate, entity, fields)
|
|
360
508
|
entity.attributes.each do |attribute|
|
|
361
509
|
next if fields.key?(attribute.name)
|
|
@@ -366,11 +514,23 @@ module Hecks
|
|
|
366
514
|
end
|
|
367
515
|
|
|
368
516
|
# `MutationApplier#removed`'s own entity-scoped twin — matches by
|
|
369
|
-
#
|
|
370
|
-
# never be lost" reasoning that method's own comment gives,
|
|
517
|
+
# value equality, element-wise, the same "so a concurrent Add can
|
|
518
|
+
# never be lost" reasoning that method's own comment gives, unless
|
|
371
519
|
# the list this targets is itself entity-typed — see
|
|
372
520
|
# `list_element_match?`, below, which both this and
|
|
373
521
|
# `MutationApplier#removed` now share.
|
|
522
|
+
#
|
|
523
|
+
# @param rules [Runtime::CommandRules] the shared rules engine `resolve_source`
|
|
524
|
+
# is read through
|
|
525
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
526
|
+
# coercion
|
|
527
|
+
# @param entity [Bluebook::Entity] the entity type `element`'s mutated list
|
|
528
|
+
# attribute belongs to
|
|
529
|
+
# @param element [Hash{Symbol => Object}] the element being removed from
|
|
530
|
+
# @param mutation [Bluebook::Mutation] the declared `:remove` mutation
|
|
531
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments
|
|
532
|
+
# @return [Array] `element[mutation.target]`'s existing elements, with any
|
|
533
|
+
# matching the resolved remove target left out
|
|
374
534
|
def removed_from_element(rules, aggregate, entity, element, mutation, args)
|
|
375
535
|
value = rules.resolve_source(mutation.source, args)
|
|
376
536
|
attribute = entity.attribute(mutation.target)
|
|
@@ -378,23 +538,23 @@ module Hecks
|
|
|
378
538
|
Array(element[mutation.target]).reject { |candidate| list_element_match?(aggregate, attribute, candidate, value) }
|
|
379
539
|
end
|
|
380
540
|
|
|
381
|
-
# BUG#32 (QualityControl ledger) —
|
|
382
|
-
#
|
|
383
|
-
#
|
|
541
|
+
# BUG#32 (QualityControl ledger) — the match rule `remove:` uses
|
|
542
|
+
# against one stored list element. Value equality for a
|
|
543
|
+
# value-object-typed list stays exactly what it always was — an
|
|
384
544
|
# element and `value` are both real `Value`s there, so `==` already
|
|
385
545
|
# compares every field, the "concurrent Add can never be lost"
|
|
386
546
|
# shape `removed`/`removed_from_element`'s own headers describe.
|
|
387
|
-
# An
|
|
547
|
+
# An entity-typed list is different in kind, not just in type: a
|
|
388
548
|
# stored element is a plain Hash, never a `Value` (`Entity`'s own
|
|
389
549
|
# header — "an entity must never answer .value_object"), so there
|
|
390
550
|
# is no whole-value shape to compare against at all — only the
|
|
391
|
-
# entity's own
|
|
551
|
+
# entity's own identity field, the same field a caller already has
|
|
392
552
|
# to name to address that element any other way
|
|
393
553
|
# (`element_of`'s own `wants`, above). `value` arrives here already
|
|
394
554
|
# coerced against that identity field's declared type
|
|
395
555
|
# (`Coercion#hydrate_entity_identity`, run underneath
|
|
396
556
|
# `Value.for_attribute` before either caller above ever sees it),
|
|
397
|
-
# so this only has to know
|
|
557
|
+
# so this only has to know which field to read off the stored
|
|
398
558
|
# element — `entity.identity_heads`'s own single head, when there
|
|
399
559
|
# is exactly one. A composite identity (more than one head, or
|
|
400
560
|
# none) has no single field a bare `remove:` target could mean —
|
|
@@ -405,11 +565,22 @@ module Hecks
|
|
|
405
565
|
# entity identity elsewhere in this runtime.
|
|
406
566
|
#
|
|
407
567
|
# Shared by `MutationApplier#removed` (an aggregate's own list) and
|
|
408
|
-
# `#removed_from_element` (a list an
|
|
568
|
+
# `#removed_from_element` (a list an entity owns), so the two
|
|
409
569
|
# `remove:` call sites can never quietly disagree on what
|
|
410
570
|
# "matches" means — the same reasoning this file's own header
|
|
411
571
|
# gives for centralizing `locate_chain`/`element_of` once rather
|
|
412
572
|
# than twice.
|
|
573
|
+
#
|
|
574
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, to resolve
|
|
575
|
+
# whether `attribute`'s own type is an entity
|
|
576
|
+
# @param attribute [Bluebook::Attribute, nil] the list attribute `element`
|
|
577
|
+
# belongs to
|
|
578
|
+
# @param element [Object] the stored list element to check: a Hash for an
|
|
579
|
+
# entity-typed list, a `Runtime::Value` otherwise
|
|
580
|
+
# @param value [Object] the `remove:` target to match `element` against
|
|
581
|
+
# @return [Boolean] whole-value equality for a non-entity-typed list;
|
|
582
|
+
# identity-field equality for an entity-typed one with a single identity
|
|
583
|
+
# head; false for a composite or absent identity
|
|
413
584
|
def list_element_match?(aggregate, attribute, element, value)
|
|
414
585
|
entity = attribute&.list? ? Value.find_entity(aggregate, attribute.type.to_s) : nil
|
|
415
586
|
return element == value unless entity
|
|
@@ -419,6 +590,72 @@ module Hecks
|
|
|
419
590
|
|
|
420
591
|
element.is_a?(Hash) && element[head] == value
|
|
421
592
|
end
|
|
593
|
+
|
|
594
|
+
# Refuses a caller-supplied or composite identity that already names an
|
|
595
|
+
# element on `current`.
|
|
596
|
+
#
|
|
597
|
+
# BUG#13 — the same check #hydrate gives every creating
|
|
598
|
+
# aggregate command (`repository.find(id)`,
|
|
599
|
+
# `command_interpreter.rb`), one level down. Shared with
|
|
600
|
+
# `MutationApplier` (mutation_applier.rb), called from
|
|
601
|
+
# `#entity_element` — an aggregate's own entity list (`Workspace.
|
|
602
|
+
# boards`, `Ledger.entries`) — and, moved here (BUG#145), from `#appended_
|
|
603
|
+
# to_element`, above — an entity's own nested entity list one hop
|
|
604
|
+
# further in (`Board.cards`) — so neither call site reimplements
|
|
605
|
+
# it a second time, the same way `#list_element_
|
|
606
|
+
# match?` already avoids that split for `remove:`.
|
|
607
|
+
#
|
|
608
|
+
# Reached, at the aggregate-owned call site, only on the two
|
|
609
|
+
# branches that do not auto-mint: a caller-supplied identity (the
|
|
610
|
+
# field is already in the append's own field map) or a composite
|
|
611
|
+
# one (`entity.identified_by` is nil for those — Runtime::
|
|
612
|
+
# Identified#derive_identity). Without this, neither checks the sibling
|
|
613
|
+
# list at all: a second LogVisit with the same date+sequence, or a
|
|
614
|
+
# second IssueKey with the same serial, would append a silent
|
|
615
|
+
# duplicate — worse than an ordinary duplicate row, because
|
|
616
|
+
# `EntityElement#element_of`'s own `find_index` always matches the
|
|
617
|
+
# first match, so the second becomes permanently unaddressable by
|
|
618
|
+
# any later command. At the entity-owned call site (`#appended_to_
|
|
619
|
+
# element`), there is no auto-mint branch at all — every caller
|
|
620
|
+
# reaches this unconditionally, since a nested entity's own
|
|
621
|
+
# identity is always caller-supplied in this corpus.
|
|
622
|
+
#
|
|
623
|
+
# Auto-minted (aggregate-owned) entities never reach here —
|
|
624
|
+
# `identity_heads` for them is still checked at mint time by
|
|
625
|
+
# construction (`current.size + 1` can only repeat if something
|
|
626
|
+
# `remove:`s from the list between mints, which no real domain does
|
|
627
|
+
# today), so they can't be flagged by mistake.
|
|
628
|
+
# `owner` — the declaring construct named in the "…already exists on
|
|
629
|
+
# {owner}" wording: the root aggregate at the aggregate-owned call
|
|
630
|
+
# site (`Workspace`, `Ledger`), the immediately-enclosing entity at
|
|
631
|
+
# the entity-owned one (`Board` — never the root `Workspace` two
|
|
632
|
+
# hops up). Used for that naming purpose only (`owner.hecks_name`) —
|
|
633
|
+
# never for `Value`/namespace resolution, which is why an `Entity`
|
|
634
|
+
# (not just an `Aggregate`) is a valid thing to pass here.
|
|
635
|
+
#
|
|
636
|
+
# @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct named
|
|
637
|
+
# in a refusal as what the duplicate "already exists on"
|
|
638
|
+
# @param entity [Bluebook::Entity] the entity type being checked for a
|
|
639
|
+
# colliding identity
|
|
640
|
+
# @param current [Array<Hash>, Object] the entity's own existing elements;
|
|
641
|
+
# coerced through `Array()`, so a single element or nil is also accepted
|
|
642
|
+
# @param fields [Hash{Symbol => Object}] the new element's own fields, whose
|
|
643
|
+
# identity heads are checked against every element in `current`
|
|
644
|
+
# @return [void]
|
|
645
|
+
# @raise [Runtime::AlreadyExists] if `current` already holds an element
|
|
646
|
+
# whose identity heads match `fields`'s own
|
|
647
|
+
def check_entity_collision(owner, entity, current, fields)
|
|
648
|
+
heads = entity.identity_heads
|
|
649
|
+
return if heads.empty?
|
|
650
|
+
|
|
651
|
+
collision = Array(current).find { |element| heads.all? { |head| element[head] == fields[head] } }
|
|
652
|
+
return unless collision
|
|
653
|
+
|
|
654
|
+
raise(AlreadyExists, RefusalWording.render_site("AlreadyExists", "entity_duplicate",
|
|
655
|
+
entity: entity.hecks_name, aggregate: owner.hecks_name,
|
|
656
|
+
identity: Identity.reading(entity),
|
|
657
|
+
offered: heads.map { |head| Rendering.describe(fields[head]) }))
|
|
658
|
+
end
|
|
422
659
|
end
|
|
423
660
|
end
|
|
424
661
|
end
|