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
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,14 +31,40 @@ 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
|
|
|
38
38
|
module_function
|
|
39
39
|
|
|
40
|
+
# The ambient caller bound by the innermost enclosing `as` block on this thread.
|
|
41
|
+
#
|
|
42
|
+
# @return [Runtime::Caller::Current, nil] the bound caller, or nil when no `as`
|
|
43
|
+
# block is on the stack for this thread
|
|
40
44
|
def current = Thread.current[:hecks_caller]
|
|
41
45
|
|
|
46
|
+
# Binds the ambient caller for the duration of the block, restoring whatever
|
|
47
|
+
# caller (if any) was bound before, even if the block raises.
|
|
48
|
+
#
|
|
49
|
+
# `actor_id`, `as_of` and `scope` are each optional, self-asserted facts about
|
|
50
|
+
# the caller rather than derived from the command — see `Current`'s own header
|
|
51
|
+
# above for why each stays here instead of becoming a command-level DSL
|
|
52
|
+
# construct.
|
|
53
|
+
#
|
|
54
|
+
# @param role [String] the role the caller states it holds, checked by string
|
|
55
|
+
# equality against the command's declared `role`
|
|
56
|
+
# @param actor_id [String, nil] who the caller is; when given, lets
|
|
57
|
+
# `CommandRules::Authorization` check a real Governance `RoleAssignment`
|
|
58
|
+
# instead of the bare role-string comparison
|
|
59
|
+
# @param as_of [Integer, nil] Unix epoch seconds the caller asserts as "now",
|
|
60
|
+
# filled at the door from `Ports::Clock.now`; nil leaves a `RoleAssignment`'s
|
|
61
|
+
# `starts_at` unchecked
|
|
62
|
+
# @param scope [String, nil] the scope the caller states it is acting in,
|
|
63
|
+
# checked against the matching `RoleAssignment`'s own `scope`; nil skips
|
|
64
|
+
# that check
|
|
65
|
+
# @yield the code that should see `role`/`actor_id`/`as_of`/`scope` as the
|
|
66
|
+
# ambient caller
|
|
67
|
+
# @return [Object] the block's result
|
|
42
68
|
def as(role:, actor_id: nil, as_of: nil, scope: nil)
|
|
43
69
|
previous = Thread.current[:hecks_caller]
|
|
44
70
|
Thread.current[:hecks_caller] = Current.new(
|
|
@@ -49,11 +75,17 @@ module Hecks
|
|
|
49
75
|
Thread.current[:hecks_caller] = previous
|
|
50
76
|
end
|
|
51
77
|
|
|
52
|
-
#
|
|
78
|
+
# Clears the ambient caller for the duration of the block, restoring
|
|
79
|
+
# whatever caller (if any) was bound before, even if the block raises.
|
|
80
|
+
#
|
|
81
|
+
# A reaction is the system acting, not the caller who happened to be
|
|
53
82
|
# on the stack when the triggering command ran — `Dispatcher#reenter`
|
|
54
83
|
# clears the ambient caller around a reaction's own dispatch so a
|
|
55
84
|
# triggering caller's role can neither satisfy nor block a reaction
|
|
56
85
|
# command it has nothing to do with.
|
|
86
|
+
#
|
|
87
|
+
# @yield the code that should see no ambient caller bound
|
|
88
|
+
# @return [Object] the block's result
|
|
57
89
|
def without
|
|
58
90
|
previous = Thread.current[:hecks_caller]
|
|
59
91
|
Thread.current[:hecks_caller] = nil
|
|
@@ -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
|
|
@@ -14,6 +14,8 @@ module Hecks
|
|
|
14
14
|
# adapter names the port it implements, and neither can point at a third
|
|
15
15
|
# port. There is no edge for a cycle to be made of.
|
|
16
16
|
class CapabilityGraph
|
|
17
|
+
# @param registry [Runtime::Registry] the booted registry to read ports and adapters from
|
|
18
|
+
# @return [void]
|
|
17
19
|
def initialize(registry)
|
|
18
20
|
@registry = registry
|
|
19
21
|
end
|
|
@@ -21,6 +23,9 @@ module Hecks
|
|
|
21
23
|
# { port name => [adapter name, ...] }, for every port the registry
|
|
22
24
|
# declares — including the ports nothing implements, so a caller can
|
|
23
25
|
# tell "declared, unfulfilled" from "never declared at all".
|
|
26
|
+
#
|
|
27
|
+
# @return [Hash{String => Array<String>}] each declared port's name mapped to the names
|
|
28
|
+
# of every adapter bound to it (empty when none are)
|
|
24
29
|
def fulfillments
|
|
25
30
|
@fulfillments ||= @registry.ports.each_key.to_h { |name| [name, adapters_for(name)] }
|
|
26
31
|
end
|
|
@@ -28,10 +33,16 @@ module Hecks
|
|
|
28
33
|
# The ports with zero adapters bound — the gap `Runtime::WiringError`
|
|
29
34
|
# would otherwise only surface at the moment something tries to dispatch
|
|
30
35
|
# through one.
|
|
36
|
+
#
|
|
37
|
+
# @return [Array<String>] the names of every declared port with no adapter bound to it
|
|
31
38
|
def unfulfilled
|
|
32
39
|
fulfillments.select { |_, adapters| adapters.empty? }.keys
|
|
33
40
|
end
|
|
34
41
|
|
|
42
|
+
# Reports the port-dependency cycles this registry has — always none; see the module
|
|
43
|
+
# header for why no port model here can express one.
|
|
44
|
+
#
|
|
45
|
+
# @return [Array] always empty
|
|
35
46
|
def cycles = []
|
|
36
47
|
|
|
37
48
|
private
|
|
@@ -10,19 +10,19 @@ module Hecks
|
|
|
10
10
|
module ArgumentGate
|
|
11
11
|
private
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
# normalize_args walks the
|
|
15
|
-
# never had
|
|
16
|
-
# nothing, in silence.
|
|
13
|
+
# Without this check, anything else would ride along in the payload
|
|
14
|
+
# untouched — normalize_args walks only the declared attributes, so
|
|
15
|
+
# a name the command never had is simply never looked at, and a
|
|
16
|
+
# misspelled argument would do 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,66 +33,68 @@ 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
|
-
# `Customer.Register`
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
52
|
+
# Refusing `Customer.Register` for a missing `name` needs to be
|
|
53
|
+
# deliberate, not an accident with a lie for a message. Without this
|
|
54
|
+
# check, `then_set :name, to: :name` finds nothing to resolve, passes
|
|
55
|
+
# the literal symbol on, and coercion reports `name is a PersonName —
|
|
56
|
+
# pass its fields as an object`, which describes a mistake the caller
|
|
57
|
+
# did not make. The real mistake — an argument simply missing — is
|
|
58
|
+
# never the one named unless this refuses it on purpose.
|
|
58
59
|
#
|
|
59
60
|
# No command attribute anywhere in the corpus carries a default — checked,
|
|
60
61
|
# all eight chapters, zero — so there is no optional argument for this to
|
|
61
62
|
# step on. Every declared attribute is a fact the command needs.
|
|
62
63
|
#
|
|
63
|
-
# `aggregate:` —
|
|
64
|
+
# `aggregate:` — present only for a port operation. An aggregate
|
|
64
65
|
# command's own self-address never reaches this list at all
|
|
65
66
|
# (`CommandBuilder#reference_to`'s bare self-reference mints no
|
|
66
67
|
# attribute to be absent) — but `PortOperationBuilder#reference_to`
|
|
67
|
-
#
|
|
68
|
-
# creates?/acts_on distinction to protect"), because an operation
|
|
69
|
-
#
|
|
70
|
-
# `to:` is that
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
# operation with an identity attribute
|
|
75
|
-
# calls, dispatched exactly the way `to:`
|
|
76
|
-
# the same terms an aggregate command's
|
|
77
|
-
# an address, not a fact the operation
|
|
68
|
+
# always mints one (this file's own header on `PortOperation`: "no
|
|
69
|
+
# creates?/acts_on distinction to protect"), because an operation had
|
|
70
|
+
# no other way to say which record it addressed before `to:` existed.
|
|
71
|
+
# `to:` is that way now (`Dispatcher#port_invocation` promotes the
|
|
72
|
+
# identity attribute's own value out of the payload and into
|
|
73
|
+
# routing) — which leaves the attribute still declared, still
|
|
74
|
+
# non-optional, and never present in `args` at all: without this
|
|
75
|
+
# exemption, every operation with an identity attribute would refuse
|
|
76
|
+
# its own well-formed calls, dispatched exactly the way `to:`
|
|
77
|
+
# intends. Exempted here on the same terms an aggregate command's
|
|
78
|
+
# self-address always was — an address, not a fact the operation
|
|
79
|
+
# still needs handed back.
|
|
78
80
|
def refuse_absent_arguments(command, args, aggregate: nil)
|
|
79
81
|
given = args.keys.map(&:to_sym)
|
|
80
82
|
exempt = aggregate && command.respond_to?(:identity_attribute) &&
|
|
81
83
|
command.identity_attribute(aggregate.hecks_name)&.name
|
|
82
84
|
required = command.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym } - [exempt]
|
|
83
|
-
#
|
|
85
|
+
# Sorted, for the same reason the unknown list is : refusal wording is
|
|
84
86
|
# contract, and a pinned wording cannot depend on the order a set
|
|
85
87
|
# difference happens to be computed in.
|
|
86
88
|
absent = (required - given).sort
|
|
87
89
|
return if absent.empty?
|
|
88
90
|
|
|
89
91
|
raise AbsentArgument,
|
|
90
|
-
RefusalWording.
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
RefusalWording.render_site("AbsentArgument", "absent_args",
|
|
93
|
+
command: command.hecks_name, absent: absent,
|
|
94
|
+
declared: declared_names(command))
|
|
93
95
|
end
|
|
94
96
|
|
|
95
|
-
# A
|
|
97
|
+
# A command that declares nothing still has to say so. `Account
|
|
96
98
|
# .Freeze` is `reference_to Account` and no attributes at all, so
|
|
97
99
|
# `{declared}` rendered empty and the sentence trailed off mid-
|
|
98
100
|
# clause : "Freeze does not declare standing — it takes ". Read
|
|
@@ -104,17 +106,14 @@ module Hecks
|
|
|
104
106
|
# one attribute renders exactly as it did before, because refusal
|
|
105
107
|
# wording is contract and pinned byte-for-byte by the corpus (see
|
|
106
108
|
# `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
|
|
109
|
+
def declared_names(command) = command.attributes.map(&:name)
|
|
111
110
|
|
|
112
|
-
# What a process manager correlates by is
|
|
111
|
+
# What a process manager correlates by is routing, not description. A saga
|
|
113
112
|
# threads its correlation key through every leg it dispatches so the event
|
|
114
113
|
# each leg emits carries it and the next step can be correlated — so the key
|
|
115
114
|
# arrives on commands that never declare it, and legitimately.
|
|
116
115
|
#
|
|
117
|
-
# This is the weakest part of the gate. Correlation is the
|
|
116
|
+
# This is the weakest part of the gate. Correlation is the saga's business,
|
|
118
117
|
# and the better shape is for the saga to stamp its own key onto the event
|
|
119
118
|
# it caused rather than smuggle it through the command's payload. Until it
|
|
120
119
|
# does, refusing the key here would break every saga in the corpus.
|
|
@@ -19,19 +19,19 @@ module Hecks
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
# A
|
|
22
|
+
# A case statement over a closed, declared set — every mutation op
|
|
23
23
|
# the grammar can emit gets its own branch, including the `else`
|
|
24
24
|
# backstop for the day a new op reaches this method undeclared (see
|
|
25
25
|
# its own comment). Splitting each branch into its own method would
|
|
26
26
|
# not reduce what a reader has to hold at once (each op's own
|
|
27
|
-
# comment already explains why
|
|
27
|
+
# comment already explains why it is shaped the way it is) and
|
|
28
28
|
# would obscure that the set is closed and exhaustive.
|
|
29
29
|
# rubocop:disable Lint/DuplicateBranch -- :delegate and :corrects
|
|
30
30
|
# both no-op here, for two unrelated documented reasons (see each
|
|
31
31
|
# branch's own comment below); merging would blur that distinction.
|
|
32
32
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
33
33
|
#
|
|
34
|
-
# `pre` —
|
|
34
|
+
# `pre` — the pre-dispatch state (C4.2): every source below reads
|
|
35
35
|
# it, every target is written to `instance`. A command's effects
|
|
36
36
|
# are one update set; declaration order carries no meaning, and
|
|
37
37
|
# build refuses a field written twice (`CommandBuilder#
|
|
@@ -54,7 +54,7 @@ module Hecks
|
|
|
54
54
|
current = pre[mutation.target]
|
|
55
55
|
# Vendored fix, not (yet) upstream hecks (migration plan
|
|
56
56
|
# task 9): see #rewrap_arithmetic_result's own comment below
|
|
57
|
-
# -- `amount` is wrapped
|
|
57
|
+
# -- `amount` is wrapped only when `current` already is, not
|
|
58
58
|
# merely because the target attribute exists.
|
|
59
59
|
amount = Value.for_attribute(aggregate, attribute, amount) if attribute && current.is_a?(Value)
|
|
60
60
|
result = @rules.arithmetic(current, amount, mutation.target, @rules.sign_of(mutation.op))
|
|
@@ -86,22 +86,22 @@ module Hecks
|
|
|
86
86
|
when :clamp
|
|
87
87
|
instance[mutation.target] = @rules.clamp(pre[mutation.target], mutation.source, mutation.target)
|
|
88
88
|
# `delegates_to` — CommandBuilder#delegates_to's own comment gives
|
|
89
|
-
# the full reasoning for storing it as a mutation at all. A
|
|
90
|
-
# no-op here, not a gap: it targets no field on
|
|
91
|
-
# `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
|
|
92
92
|
# the same dispatch, is what actually applies it, against the
|
|
93
93
|
# target entity element `EntityElement.apply_to_element` reaches,
|
|
94
94
|
# never through this method.
|
|
95
95
|
when :delegate
|
|
96
96
|
nil
|
|
97
97
|
# `corrects` — CommandBuilder#corrects_impl's own comment gives
|
|
98
|
-
# the full reasoning for storing it as a mutation at all. A
|
|
99
|
-
# no-op here too: it targets no field on
|
|
100
|
-
# 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
|
|
101
101
|
# emitted it, is checked once, up front, by
|
|
102
102
|
# CommandRules::Admissibility#enforce_correction_target, not
|
|
103
103
|
# here. Whatever field this correction actually changes is an
|
|
104
|
-
#
|
|
104
|
+
# ordinary declared (or, for `reverses: true`, derived — see
|
|
105
105
|
# AggregateBuilder#seal_correction_targets) mutation of its own,
|
|
106
106
|
# applied by one of the branches above like any other.
|
|
107
107
|
when :corrects
|
|
@@ -120,9 +120,9 @@ module Hecks
|
|
|
120
120
|
# rubocop:enable Lint/DuplicateBranch
|
|
121
121
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
122
122
|
|
|
123
|
-
# A
|
|
124
|
-
# name something the
|
|
125
|
-
# 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
|
|
126
126
|
# caller appending at the end without computing or supplying a
|
|
127
127
|
# position (`position: :next_position`, a field the command never
|
|
128
128
|
# declares as an argument at all). `args.key?`, not a truthiness
|
|
@@ -149,9 +149,9 @@ module Hecks
|
|
|
149
149
|
value_object = aggregate.value_object(element_type)
|
|
150
150
|
value_object&.attributes&.each do |attribute|
|
|
151
151
|
held = fields[attribute.name]
|
|
152
|
-
# A
|
|
152
|
+
# A single-field value unwraps to its scalar so it bridges
|
|
153
153
|
# into the element's own (differently named) wrapper; a
|
|
154
|
-
#
|
|
154
|
+
# multi-field one (a `state(:en_passant_square)` Square
|
|
155
155
|
# copied off the record) has no scalar to stand in for it
|
|
156
156
|
# and is handed across whole — `Value.for_attribute` keeps a
|
|
157
157
|
# value of the element field's own type as it is.
|
|
@@ -165,24 +165,24 @@ module Hecks
|
|
|
165
165
|
fields)
|
|
166
166
|
end
|
|
167
167
|
|
|
168
|
-
#
|
|
169
|
-
# appended list
|
|
170
|
-
# push straight into an aggregate's own state after the
|
|
168
|
+
# Frozen, like every other value the domain hands back — without
|
|
169
|
+
# this, an appended list would come back mutable, letting a
|
|
170
|
+
# caller push straight into an aggregate's own state after the
|
|
171
171
|
# dispatch had finished.
|
|
172
172
|
Freezer.deep(Array(instance[mutation.target]) + [element])
|
|
173
173
|
end
|
|
174
174
|
|
|
175
175
|
# Vendored addition, not (yet) upstream hecks (migration plan
|
|
176
176
|
# task 4): the removal counterpart to #appended -- matches by
|
|
177
|
-
#
|
|
177
|
+
# value equality, element-wise, no read-modify-write (plan.
|
|
178
178
|
# bluebook's own words: "so a concurrent Add can never be lost")
|
|
179
|
-
# --
|
|
179
|
+
# -- unless the target list is itself entity-typed (BUG#32,
|
|
180
180
|
# QualityControl ledger), in which case `EntityElement.
|
|
181
|
-
# list_element_match?` matches by the entity's own
|
|
181
|
+
# list_element_match?` matches by the entity's own identity
|
|
182
182
|
# field instead -- see that method's own comment for the full
|
|
183
183
|
# "why identity, not whole-value equality" reasoning. `mutation.
|
|
184
184
|
# source` is a single field reference (`:dependency`), unlike
|
|
185
|
-
# append's field-map -- resolved and Value-coerced the
|
|
185
|
+
# append's field-map -- resolved and Value-coerced the same way
|
|
186
186
|
# increment/decrement already coerce their own amount, so the
|
|
187
187
|
# comparison is against a like-shaped Value, not a raw scalar
|
|
188
188
|
# against a wrapped one.
|
|
@@ -197,26 +197,26 @@ module Hecks
|
|
|
197
197
|
|
|
198
198
|
# Vendored fix, not (yet) upstream hecks (migration plan
|
|
199
199
|
# task 9): #apply's `:increment`/`:decrement`/`:multiply`
|
|
200
|
-
# branches
|
|
200
|
+
# branches wrapping `amount` into a `Value` unconditionally
|
|
201
201
|
# whenever the target attribute existed, never checking whether
|
|
202
|
-
# `current` (the field's
|
|
203
|
-
# `instance[mutation.target]`) was
|
|
204
|
-
# of the same arithmetic call
|
|
205
|
-
#
|
|
202
|
+
# `current` (the field's own existing value, read straight off
|
|
203
|
+
# `instance[mutation.target]`) was also wrapped, would let the two
|
|
204
|
+
# sides of the same arithmetic call disagree on Value-ness. On a
|
|
205
|
+
# phantom-created field this is the common case, not an edge
|
|
206
206
|
# one: `Instance.defaults`/`#default_for` leaves a VO-typed
|
|
207
207
|
# attribute with no declared `default:` genuinely absent (nil),
|
|
208
208
|
# and `CommandRules::Arithmetic#arithmetic`/`#multiply`'s own
|
|
209
|
-
# `current ||= 0` then turns that nil into a
|
|
209
|
+
# `current ||= 0` then turns that nil into a raw, unwrapped
|
|
210
210
|
# Integer `0` -- so `current.is_a?(Value) && amount.is_a?(Value)`
|
|
211
211
|
# read false even though `amount` (correctly wrapped by the old
|
|
212
212
|
# unconditional line) genuinely held a valid, correctly-typed
|
|
213
213
|
# number, and the primitive path's `unless amount.is_a?(Numeric)`
|
|
214
|
-
# guard refused it as a
|
|
214
|
+
# guard refused it as a type mismatch the caller never made --
|
|
215
215
|
# an artifact of this method's own asymmetric wrapping, not bad
|
|
216
216
|
# input.
|
|
217
217
|
#
|
|
218
|
-
# Fixed at the call site (above) by wrapping `amount`
|
|
219
|
-
# `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
|
|
220
220
|
# field (a multi-field Money balance, say, already hydrated from
|
|
221
221
|
# a prior save) keeps going through
|
|
222
222
|
# `Arithmetic#arithmetic_value_object` exactly as before (the
|
|
@@ -225,7 +225,7 @@ module Hecks
|
|
|
225
225
|
# `current` and raw `amount` both take the plain-Numeric path
|
|
226
226
|
# together. This method closes the other half: the plain-Numeric
|
|
227
227
|
# path returns a bare Ruby number, and if the attribute is
|
|
228
|
-
# itself VO-typed (the norm), that raw result needs the
|
|
228
|
+
# itself VO-typed (the norm), that raw result needs the same
|
|
229
229
|
# wrap `:set` already gives its own resolved value (`Value.for`)
|
|
230
230
|
# before it's stored, so a field's stored shape doesn't depend
|
|
231
231
|
# on which dispatch happened to mutate it first. A no-op
|
|
@@ -251,10 +251,10 @@ module Hecks
|
|
|
251
251
|
attribute = entity.attribute(entity.identified_by)
|
|
252
252
|
fields[entity.identified_by] = Value.from_identifier(aggregate, attribute, next_identity(current, entity))
|
|
253
253
|
else
|
|
254
|
-
check_entity_collision(aggregate, entity, current, fields)
|
|
254
|
+
EntityElement.check_entity_collision(aggregate, entity, current, fields)
|
|
255
255
|
end
|
|
256
256
|
fields[entity.lifecycle.field] ||= entity.lifecycle.default if entity.lifecycle
|
|
257
|
-
# BUG#12 — every one of
|
|
257
|
+
# BUG#12 — every one of this entity's own declared attributes
|
|
258
258
|
# the append mapping (and the identity/lifecycle filling just
|
|
259
259
|
# above) didn't already touch gets its own default, the same
|
|
260
260
|
# way a fresh aggregate's own attributes already do
|
|
@@ -267,7 +267,7 @@ module Hecks
|
|
|
267
267
|
EntityElement.fill_declared_defaults(aggregate, entity, fields)
|
|
268
268
|
end
|
|
269
269
|
|
|
270
|
-
#
|
|
270
|
+
# The minted identity is one past the highest held (C4.5) — not
|
|
271
271
|
# `size + 1`, which repeats an identity the moment the list has
|
|
272
272
|
# ever shrunk. One strategy for the language, so the IR declares
|
|
273
273
|
# none; the Rust generators mint by the same rule.
|
|
@@ -276,37 +276,15 @@ module Hecks
|
|
|
276
276
|
held.max.to_i + 1
|
|
277
277
|
end
|
|
278
278
|
|
|
279
|
-
#
|
|
280
|
-
# (
|
|
281
|
-
#
|
|
282
|
-
#
|
|
283
|
-
#
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
#
|
|
287
|
-
#
|
|
288
|
-
# IssueKey with the same serial, appended a silent duplicate — worse
|
|
289
|
-
# than an ordinary duplicate row, because EntityInterpreter#element_of's
|
|
290
|
-
# `find_index` always matches the FIRST match, so the second becomes
|
|
291
|
-
# permanently unaddressable by any later command.
|
|
292
|
-
#
|
|
293
|
-
# Auto-minted entities never reach here — `identity_heads` for them
|
|
294
|
-
# is still checked at mint time by construction (`current.size + 1`
|
|
295
|
-
# can only repeat if something `remove:`s from the list between
|
|
296
|
-
# mints, which no real domain does today), so they can't be flagged
|
|
297
|
-
# by mistake.
|
|
298
|
-
def check_entity_collision(aggregate, entity, current, fields)
|
|
299
|
-
heads = entity.identity_heads
|
|
300
|
-
return if heads.empty?
|
|
301
|
-
|
|
302
|
-
collision = Array(current).find { |element| heads.all? { |head| element[head] == fields[head] } }
|
|
303
|
-
return unless collision
|
|
304
|
-
|
|
305
|
-
raise(AlreadyExists, RefusalWording.render("AlreadyExists", "entity_duplicate",
|
|
306
|
-
entity: entity.hecks_name, aggregate: aggregate.hecks_name,
|
|
307
|
-
identity: Identity.reading(entity),
|
|
308
|
-
offered: heads.map { |head| Rendering.describe(fields[head]) }.join(", ")))
|
|
309
|
-
end
|
|
279
|
+
# The entity-collision check lives in `EntityElement.check_entity_collision`
|
|
280
|
+
# (entity_element.rb) — BUG#145 — shared by both `#entity_element` above
|
|
281
|
+
# (an aggregate's own entity list, e.g. `Workspace.boards`) and
|
|
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
|
+
# which needs the exact same guard against a caller-supplied nested
|
|
285
|
+
# identity silently duplicating. See that method's own call site and
|
|
286
|
+
# comment; the check's own doc comment (heads/composite/auto-mint
|
|
287
|
+
# reasoning) lives with the code, in entity_element.rb.
|
|
310
288
|
end
|
|
311
289
|
end
|
|
312
290
|
end
|