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
|
@@ -3,9 +3,9 @@ require_relative "value"
|
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Runtime
|
|
6
|
-
#
|
|
6
|
+
# **The scalar an identity path names**.
|
|
7
7
|
#
|
|
8
|
-
# An identity is
|
|
8
|
+
# An identity is declared as a path — `identified_by :number` — and
|
|
9
9
|
# this is the one place that reads one. It follows the path and nothing else.
|
|
10
10
|
#
|
|
11
11
|
# What it replaced was `Value.identifier`, which opened a one-field value
|
|
@@ -23,19 +23,38 @@ module Hecks
|
|
|
23
23
|
module Identity
|
|
24
24
|
module_function
|
|
25
25
|
|
|
26
|
+
# Reads one key off `hash` by whichever of its Symbol/String spelling
|
|
27
|
+
# is actually present, favoring the Symbol spelling.
|
|
28
|
+
#
|
|
26
29
|
# A hash read that decides which spelling of a key answers by
|
|
27
|
-
#
|
|
30
|
+
# presence, never by `||` — a bare `||` treats a genuinely-held
|
|
28
31
|
# `false` the same as an absent key and falls through to the other
|
|
29
32
|
# spelling, landing on `nil` instead of the real, stored answer.
|
|
33
|
+
#
|
|
34
|
+
# @param hash [Hash] the hash to read, potentially keyed by either spelling
|
|
35
|
+
# @param key [String, Symbol] the key to look up
|
|
36
|
+
# @return [Object, nil] the value under `key.to_sym` if that key is present,
|
|
37
|
+
# otherwise the value under `key` as given; nil if neither is a key of `hash`
|
|
30
38
|
def hash_lookup(hash, key)
|
|
31
39
|
sym = key.to_sym
|
|
32
40
|
hash.key?(sym) ? hash[sym] : hash[key]
|
|
33
41
|
end
|
|
34
42
|
|
|
35
|
-
#
|
|
43
|
+
# Digs `path`'s fields out of `held`, past the head a caller already
|
|
44
|
+
# consumed to look `held` up.
|
|
45
|
+
#
|
|
46
|
+
# The head names the attribute and is consumed by whoever looked the value
|
|
36
47
|
# up; what is left is the walk down into it. A path with no fields to walk
|
|
37
48
|
# — an aggregate that declares no identity and falls back to `id` — hands
|
|
38
49
|
# back what it was given, because there is nothing declared to dig for.
|
|
50
|
+
#
|
|
51
|
+
# @param path [String, Symbol] the dotted identity path (`"number.value"`) or a
|
|
52
|
+
# bare head (`:id`); only the segments after the head are walked
|
|
53
|
+
# @param held [Object] the value already looked up for the path's head — a
|
|
54
|
+
# `Runtime::Value`, a Hash, or a plain scalar
|
|
55
|
+
# @return [Object, nil] `held` unchanged when `path` has no fields past its head;
|
|
56
|
+
# otherwise the value found by walking those fields, or nil when a segment
|
|
57
|
+
# is missing or the value being dug into is not a Hash
|
|
39
58
|
def scalar(path, held)
|
|
40
59
|
_head, *fields = path.to_s.split(".")
|
|
41
60
|
return held if fields.empty?
|
|
@@ -45,37 +64,66 @@ module Hecks
|
|
|
45
64
|
end
|
|
46
65
|
end
|
|
47
66
|
|
|
48
|
-
#
|
|
67
|
+
# Derives `construct`'s identity string from `args`, joining every
|
|
68
|
+
# declared identity part in declaration order.
|
|
69
|
+
#
|
|
70
|
+
# The identity is the join of its parts, in declaration order. Shared by
|
|
49
71
|
# `CommandInterpreter` (an aggregate acting on itself) and
|
|
50
72
|
# `EntityInterpreter` (a piece addressed through its aggregate) — a piece
|
|
51
73
|
# declares an identity the same shape a head does, so it derives one the
|
|
52
74
|
# same way. `construct` answers `identity_paths` / `identity_heads` /
|
|
53
75
|
# `attribute` (an Aggregate or an Entity, either one) ; `value_owner`
|
|
54
76
|
# answers for coercion (`Value.for_attribute`'s first argument), which for
|
|
55
|
-
# an entity is its
|
|
77
|
+
# an entity is its owning aggregate — an entity's value objects resolve
|
|
56
78
|
# through the aggregate's namespace, not its own.
|
|
57
79
|
#
|
|
58
|
-
# A part the payload does not carry makes the
|
|
80
|
+
# A part the payload does not carry makes the whole identity unresolvable,
|
|
59
81
|
# rather than half of one. Half an identity names nothing, and joining what
|
|
60
82
|
# did arrive would silently name a different record on every dispatch — the
|
|
61
83
|
# precise failure that minting an id caused, arrived at by another road.
|
|
84
|
+
#
|
|
85
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
86
|
+
# `identity_paths` are resolved
|
|
87
|
+
# @param args [Hash{Symbol => Object}] the offered payload to resolve each
|
|
88
|
+
# identity path against
|
|
89
|
+
# @param value_owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
90
|
+
# namespace a value-object identity part is coerced against; `construct` itself
|
|
91
|
+
# unless the caller passes the owning aggregate for an entity
|
|
92
|
+
# @return [String, nil] the joined identity string, or nil when `construct`
|
|
93
|
+
# declares no identity path or any resolved part is nil or blank
|
|
62
94
|
def of(construct, args, value_owner: construct)
|
|
63
95
|
paths = construct.identity_paths
|
|
64
96
|
return nil if paths.empty?
|
|
65
97
|
|
|
66
98
|
parts = paths.map { |path| from(construct, args, path, value_owner: value_owner) }
|
|
67
|
-
# A
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
# source did not survive extraction)
|
|
99
|
+
# A blank part names nothing, the same as an absent one — an ID is a
|
|
100
|
+
# scalar, and "" is not a fact about anything. Checking only `nil?`
|
|
101
|
+
# would let a canonical text extracted as "" (an expression whose
|
|
102
|
+
# source did not survive extraction) resolve to a real, empty-string
|
|
71
103
|
# identity — a record addressable by an id no caller could have meant.
|
|
72
104
|
return nil if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
|
|
73
105
|
|
|
74
106
|
Naming.identity(parts)
|
|
75
107
|
end
|
|
76
108
|
|
|
109
|
+
# Resolves one identity path (or a bare head such as `:id`) against `args`.
|
|
110
|
+
#
|
|
77
111
|
# A path digs into the value object that carries the identity, so what is
|
|
78
|
-
# stored is the
|
|
112
|
+
# stored is the scalar inside it rather than the object serialised whole.
|
|
113
|
+
#
|
|
114
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct `key` is
|
|
115
|
+
# checked against when it is a bare head
|
|
116
|
+
# @param args [Hash{Symbol => Object}] the offered payload to resolve `key` against
|
|
117
|
+
# @param key [String, Symbol, nil] the identity path to resolve, dotted
|
|
118
|
+
# (`"number.value"`) or bare (`:id`); nil resolves to nil
|
|
119
|
+
# @param value_owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
120
|
+
# namespace a value-object head is coerced against; `construct` itself unless
|
|
121
|
+
# the caller passes the owning aggregate for an entity
|
|
122
|
+
# @return [String, Object, nil] the resolved identity text — a String once dug
|
|
123
|
+
# through a dotted path or coerced through a declared value-object attribute;
|
|
124
|
+
# the raw `args[key]` value, unconverted, when the bare head names no declared
|
|
125
|
+
# identity attribute of `construct`; nil when `key` is nil, absent from `args`,
|
|
126
|
+
# or a dotted walk finds nothing
|
|
79
127
|
def from(construct, args, key, value_owner: construct)
|
|
80
128
|
return nil unless key
|
|
81
129
|
|
|
@@ -86,7 +134,7 @@ module Hecks
|
|
|
86
134
|
unless rest.empty?
|
|
87
135
|
held = args[head]
|
|
88
136
|
held = held.to_h if held.respond_to?(:to_h)
|
|
89
|
-
#
|
|
137
|
+
# **An ID is always a scalar**. The path says which field carries it, so a
|
|
90
138
|
# caller may hand that field's value straight over — a string or a
|
|
91
139
|
# number, never a serialised object. Only a value object that actually
|
|
92
140
|
# arrived whole has to be opened.
|
|
@@ -95,18 +143,18 @@ module Hecks
|
|
|
95
143
|
return rest.reduce(held) { |h, f| h.is_a?(Hash) ? hash_lookup(h, f) : nil }&.to_s
|
|
96
144
|
end
|
|
97
145
|
|
|
98
|
-
# Coerced against the identity
|
|
146
|
+
# Coerced against the identity attribute only when the caller actually
|
|
99
147
|
# named it. A saga addresses an aggregate by its correlation key, and
|
|
100
|
-
# that key carries the id
|
|
148
|
+
# that key carries the id already resolved — coercing "w1" against a
|
|
101
149
|
# WireReference asked the caller to pass fields for a value object they
|
|
102
150
|
# never mentioned.
|
|
103
151
|
attribute = construct.identity_heads.include?(head) ? construct.attribute(head) : nil
|
|
104
152
|
raw = args[head]
|
|
105
153
|
return raw unless attribute
|
|
106
154
|
|
|
107
|
-
#
|
|
108
|
-
# already keeps, just reached a different way here: a
|
|
109
|
-
# (undotted) identity path names one of
|
|
155
|
+
# **An ID is always a scalar** — same contract the dotted branch above
|
|
156
|
+
# already keeps, just reached a different way here: a bare
|
|
157
|
+
# (undotted) identity path names one of this construct's own
|
|
110
158
|
# declared attributes directly, and when that attribute's type is
|
|
111
159
|
# a value object (Translation's own compound `identified_by
|
|
112
160
|
# :domain, :from, :to`, each typed `TranslationDomainName`/
|
|
@@ -118,23 +166,32 @@ module Hecks
|
|
|
118
166
|
# memory address (`#<Hecks::Runtime::Value:0x...>`) into
|
|
119
167
|
# every refusal quoting this identity — found live via bin/fuzz on
|
|
120
168
|
# the self-hosted "translation" domain (replay_is_deterministic:
|
|
121
|
-
# the
|
|
169
|
+
# the same address never repeats, so two replays of the
|
|
122
170
|
# identical steps produced different histories the moment a
|
|
123
171
|
# Translation went missing). `materialize_unwrapped` is the
|
|
124
|
-
#
|
|
172
|
+
# same single-field-VO-recurses-to-its-bare-scalar helper
|
|
125
173
|
# `read_model_interpreter.rb` already uses for exactly this
|
|
126
174
|
# unwrap; passthrough for anything that isn't a Value at all.
|
|
127
175
|
Value.materialize_unwrapped(Value.for_attribute(value_owner, attribute, raw)).to_s
|
|
128
176
|
end
|
|
129
177
|
|
|
130
|
-
#
|
|
178
|
+
# Renders `construct`'s identity paths for a refusal message.
|
|
179
|
+
#
|
|
180
|
+
# How an identity reads when the runtime has to name it in a refusal — the
|
|
131
181
|
# paths as they were declared, so the message quotes the bluebook back.
|
|
182
|
+
#
|
|
183
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
184
|
+
# declared identity paths are rendered
|
|
185
|
+
# @return [String] `construct`'s identity paths, comma-separated, exactly as declared
|
|
132
186
|
def reading(construct)
|
|
133
187
|
construct.identity_paths.join(", ")
|
|
134
188
|
end
|
|
135
189
|
|
|
136
|
-
#
|
|
137
|
-
#
|
|
190
|
+
# Resolves the best identity string available for `construct` from `args`,
|
|
191
|
+
# without raising, for use as a lock key only.
|
|
192
|
+
#
|
|
193
|
+
# Best-effort, for a lock key only — `Runtime::AggregateLock`'s own
|
|
194
|
+
# per-record striping needs some id to key on before dispatch has run
|
|
138
195
|
# far enough to hydrate for real, so this walks the identical chain
|
|
139
196
|
# `CommandInterpreter#hydrate_existing`/`#hydrate_prior_or_initial`
|
|
140
197
|
# and `EntityInterpreter#parent` already use to locate the real
|
|
@@ -143,6 +200,17 @@ module Hecks
|
|
|
143
200
|
# from the raw, pre-normalized payload this runs against" — the
|
|
144
201
|
# caller locks by aggregate type alone in that case (coarser, still
|
|
145
202
|
# correct, just less concurrent).
|
|
203
|
+
#
|
|
204
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct being
|
|
205
|
+
# located for the lock key
|
|
206
|
+
# @param args [Hash{Symbol => Object}] the raw, pre-normalized payload to resolve
|
|
207
|
+
# an identity from
|
|
208
|
+
# @param route [Runtime::Routing::Envelope, nil] the call's resolved routing
|
|
209
|
+
# envelope, if any; its own `aggregate` identity is tried first
|
|
210
|
+
# @param reference_key [Symbol, nil] the command's reference-key fallback,
|
|
211
|
+
# tried last if given
|
|
212
|
+
# @return [String, Object, nil] the best-effort identity to lock by, or nil
|
|
213
|
+
# when nothing resolves (the caller then locks by aggregate type alone)
|
|
146
214
|
def best_effort(construct, args, route = nil, reference_key: nil)
|
|
147
215
|
route&.aggregate ||
|
|
148
216
|
of(construct, args) ||
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require_relative "value"
|
|
2
2
|
require_relative "identity"
|
|
3
|
+
require_relative "../ports/persistence/codec_boundary"
|
|
3
4
|
|
|
4
5
|
module Hecks
|
|
5
6
|
module Runtime
|
|
@@ -12,7 +13,7 @@ module Hecks
|
|
|
12
13
|
class Instance
|
|
13
14
|
attr_reader :aggregate, :id
|
|
14
15
|
attr_accessor :state
|
|
15
|
-
#
|
|
16
|
+
# **Out-of-band adapter bookkeeping, not domain state** — the optimistic-
|
|
16
17
|
# concurrency version a CAS-capable adapter (Postgres today) stamps
|
|
17
18
|
# on a record it reads/writes, so a later `save` can assert "commit
|
|
18
19
|
# only if nobody has written since". Deliberately absent from
|
|
@@ -24,16 +25,54 @@ module Hecks
|
|
|
24
25
|
# ADR) for the full mechanism.
|
|
25
26
|
attr_accessor :version
|
|
26
27
|
|
|
27
|
-
# `args:` —
|
|
28
|
+
# `args:` — the original command payload, offered only by a fresh
|
|
28
29
|
# creation (`CommandInterpreter#hydrate_legacy_creation`/
|
|
29
30
|
# `#hydrate_complete_state`/`#hydrate_prior_or_initial`, each already
|
|
30
31
|
# holding it when they mint a brand-new record). See
|
|
31
32
|
# `materialize_identity!` for why a composite identity needs it.
|
|
32
|
-
|
|
33
|
+
#
|
|
34
|
+
# `hydrate:` — on by default, and every existing caller keeps getting
|
|
35
|
+
# exactly what it always got: `state` re-walked through
|
|
36
|
+
# `hydrate_with_defaults` (declared defaults filled, every attribute
|
|
37
|
+
# re-coerced through `Value.for_attribute`, an entity list's every
|
|
38
|
+
# element rebuilt and re-validated). `false` is for exactly one
|
|
39
|
+
# caller (`Adapters::Memory#build_instance`, judge-bootstrapping
|
|
40
|
+
# only — see its own header) that already knows `state` needs none
|
|
41
|
+
# of that: it is a shallow dup of an already-hydrated, already-
|
|
42
|
+
# validated live `Instance`'s own state, not a raw value pulled off
|
|
43
|
+
# a wire. Skipping the re-walk is what turns a `list_of` entity's Nth
|
|
44
|
+
# save from O(N) (re-hydrating every element saved so far, for every
|
|
45
|
+
# save) into O(1) — the quadratic cost `Adapters::Memory`'s own
|
|
46
|
+
# header traces start to finish. `CodecBoundary.check_state!` still
|
|
47
|
+
# runs either way ; only the re-hydration is skipped.
|
|
48
|
+
#
|
|
49
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct this
|
|
50
|
+
# record's state is declared by
|
|
51
|
+
# @param id [String, nil] this record's identity; nil for a record whose identity
|
|
52
|
+
# is not yet resolvable (e.g. an entity element hydrated before its own view exists)
|
|
53
|
+
# @param state [Hash{Symbol => Object}, nil] the record's stored (or partial)
|
|
54
|
+
# attribute values; nil for a brand-new record, hydrated entirely from declared
|
|
55
|
+
# defaults
|
|
56
|
+
# @param args [Hash{Symbol => Object}, nil] the original command payload, offered
|
|
57
|
+
# only by a fresh creation; materializes a composite identity's own head
|
|
58
|
+
# attributes
|
|
59
|
+
# @param hydrate [Boolean] whether `state` is re-walked through
|
|
60
|
+
# `hydrate_with_defaults`; false only for a caller that already knows `state` is
|
|
61
|
+
# already hydrated and validated
|
|
62
|
+
def initialize(aggregate:, id:, state: nil, args: nil, hydrate: true)
|
|
33
63
|
@aggregate = aggregate
|
|
34
64
|
@id = id
|
|
35
|
-
|
|
36
|
-
|
|
65
|
+
# Inside a persistence adapter call this refuses undecoded stored
|
|
66
|
+
# state (Ports::Persistence::CodecBoundary); everywhere else, no-op.
|
|
67
|
+
Ports::Persistence::CodecBoundary.check_state!(aggregate, state) if state
|
|
68
|
+
@state = if !hydrate
|
|
69
|
+
state || self.class.defaults(aggregate)
|
|
70
|
+
elsif state
|
|
71
|
+
self.class.hydrate_with_defaults(aggregate, state)
|
|
72
|
+
else
|
|
73
|
+
self.class.defaults(aggregate)
|
|
74
|
+
end
|
|
75
|
+
@version = nil
|
|
37
76
|
materialize_identity!(args)
|
|
38
77
|
end
|
|
39
78
|
|
|
@@ -43,6 +82,12 @@ module Hecks
|
|
|
43
82
|
# written — arrives filled instead of nil. Only declared defaults
|
|
44
83
|
# fill in; an attribute with no default stays absent, exactly as
|
|
45
84
|
# stored.
|
|
85
|
+
#
|
|
86
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
87
|
+
# declared attributes and defaults `state` is hydrated against
|
|
88
|
+
# @param state [Hash{Symbol => Object}] the raw stored (or partial) attribute values
|
|
89
|
+
# @return [Hash{Symbol => Object}] `state` coerced through every declared attribute,
|
|
90
|
+
# with any missing declared-default attribute filled in
|
|
46
91
|
def self.hydrate_with_defaults(aggregate, state)
|
|
47
92
|
hydrated = Value.hydrate(aggregate, state)
|
|
48
93
|
defaults(aggregate).each do |name, value|
|
|
@@ -51,9 +96,16 @@ module Hecks
|
|
|
51
96
|
hydrated
|
|
52
97
|
end
|
|
53
98
|
|
|
99
|
+
# Builds a fresh record's starting state: every declared attribute's
|
|
100
|
+
# default value, an empty frozen Array for a `list_of` attribute, and
|
|
101
|
+
# the lifecycle field's declared starting value when `aggregate` has one.
|
|
102
|
+
#
|
|
103
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
104
|
+
# declared attributes and lifecycle are read
|
|
105
|
+
# @return [Hash{Symbol => Object}] the default state, keyed by attribute name
|
|
54
106
|
def self.defaults(aggregate)
|
|
55
107
|
state = aggregate.attributes.to_h do |attr|
|
|
56
|
-
#
|
|
108
|
+
# Frozen, like a list that has had something appended to it.
|
|
57
109
|
# An untouched list is the easiest one to miss and the easiest
|
|
58
110
|
# to mutate: nothing has replaced it yet, so a caller pushing
|
|
59
111
|
# into it writes straight into the aggregate's own state.
|
|
@@ -63,6 +115,16 @@ module Hecks
|
|
|
63
115
|
state
|
|
64
116
|
end
|
|
65
117
|
|
|
118
|
+
# Resolves one attribute's default value: its own declared `default:`
|
|
119
|
+
# if it has one, otherwise a value object built entirely from its own
|
|
120
|
+
# members' defaults, when every member declares one.
|
|
121
|
+
#
|
|
122
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
123
|
+
# `attribute` belongs to
|
|
124
|
+
# @param attribute [Bluebook::Attribute] the attribute to resolve a default for
|
|
125
|
+
# @return [Runtime::Value, Object, nil] the coerced default value; nil when
|
|
126
|
+
# `attribute` declares no default and either names no value object or names
|
|
127
|
+
# one with a member that itself declares no default
|
|
66
128
|
def self.default_for(aggregate, attribute)
|
|
67
129
|
return Value.for_attribute(aggregate, attribute, attribute.default) unless attribute.default.nil?
|
|
68
130
|
# An entity's members hydrate through the same path but an entity
|
|
@@ -75,10 +137,24 @@ module Hecks
|
|
|
75
137
|
Value.build(value_object, {}, aggregate)
|
|
76
138
|
end
|
|
77
139
|
|
|
140
|
+
# Reads one state field by name.
|
|
141
|
+
#
|
|
142
|
+
# @param name [String, Symbol] the declared attribute name to read
|
|
143
|
+
# @return [Object, nil] the field's current value; nil if `name` is not a key
|
|
144
|
+
# of `state`
|
|
78
145
|
def [](name) = @state[name.to_sym]
|
|
79
146
|
|
|
147
|
+
# Reports whether `state` holds a value for `name`.
|
|
148
|
+
#
|
|
149
|
+
# @param name [String, Symbol] the declared attribute name to check
|
|
150
|
+
# @return [Boolean] true if `state` has a key for `name`
|
|
80
151
|
def key?(name) = @state.key?(name.to_sym)
|
|
81
152
|
|
|
153
|
+
# Writes one state field by name.
|
|
154
|
+
#
|
|
155
|
+
# @param name [String, Symbol] the declared attribute name to write
|
|
156
|
+
# @param value [Object] the value to store
|
|
157
|
+
# @return [Object] `value`, unchanged
|
|
82
158
|
def []=(name, value)
|
|
83
159
|
@state[name.to_sym] = value
|
|
84
160
|
end
|
|
@@ -93,27 +169,36 @@ module Hecks
|
|
|
93
169
|
@state.key?(name) || super
|
|
94
170
|
end
|
|
95
171
|
|
|
96
|
-
#
|
|
172
|
+
# Renders this record's wire/storage shape, state plus its identity.
|
|
173
|
+
#
|
|
174
|
+
# `id: @id` last, not first — see Facade::Handle#to_h's own comment
|
|
97
175
|
# for the full story (the same fix, landed there first): an
|
|
98
176
|
# aggregate free to declare its own attribute literally named `id`
|
|
99
177
|
# (BurningManPrep's `Item`, `attribute :id, ItemId`) has that
|
|
100
178
|
# attribute's own wrapped value object sitting in `@state[:id]` —
|
|
101
179
|
# merging `@state` on top of `{ id: @id }` let it silently clobber
|
|
102
180
|
# the correct bare identity. `@id` merged last always wins.
|
|
181
|
+
#
|
|
182
|
+
# @return [Hash{Symbol => Object}] `state`, with `id` merged in last
|
|
103
183
|
def to_h = @state.merge(id: @id)
|
|
104
184
|
|
|
105
|
-
#
|
|
185
|
+
# Copies this record, deep enough that a mutation on the copy cannot
|
|
186
|
+
# reach the original's own state.
|
|
187
|
+
#
|
|
188
|
+
# A copy a mutation may touch. Every adapter but Memory hands `find`
|
|
106
189
|
# a freshly-decoded Instance already; Memory's holds the record it
|
|
107
|
-
# eventually saves — the
|
|
190
|
+
# eventually saves — the same state Hash, aliased. Before `ensures`
|
|
108
191
|
# existed, nothing could refuse between apply_mutations and save, so
|
|
109
192
|
# that aliasing was invisible: a dispatch either ran to completion or
|
|
110
193
|
# raised before touching state at all. `ensures` is the first refusal
|
|
111
|
-
# to sit
|
|
194
|
+
# to sit after mutation, and it found the bug the moment it did — an
|
|
112
195
|
# in-memory record left half-mutated by a dispatch that then refused.
|
|
113
|
-
# `command_interpreter`/`entity_interpreter` hydrate an
|
|
196
|
+
# `command_interpreter`/`entity_interpreter` hydrate an existing
|
|
114
197
|
# record through this, never through the adapter's own return value
|
|
115
198
|
# directly, so a refused ensures leaves the stored record untouched
|
|
116
199
|
# regardless of which adapter is holding it.
|
|
200
|
+
#
|
|
201
|
+
# @return [Runtime::Instance] a copy of this record, with its own state Hash
|
|
117
202
|
def dup
|
|
118
203
|
copy = super
|
|
119
204
|
copy.state = @state.dup
|
|
@@ -127,22 +212,22 @@ module Hecks
|
|
|
127
212
|
|
|
128
213
|
private
|
|
129
214
|
|
|
130
|
-
# M17 — a
|
|
215
|
+
# M17 — a composite identity (`identity_heads.size > 1`, e.g.
|
|
131
216
|
# `identified_by :branch_code, :box_number`) has no single
|
|
132
217
|
# `identified_by` to fall back to `:id` for — `@aggregate.identified_by`
|
|
133
218
|
# is nil the moment there is more than one head (`Behaviour::Identified
|
|
134
219
|
# #derive_identity`), so the single-head branch below never runs for
|
|
135
|
-
# it at all.
|
|
136
|
-
# attributes but doesn't
|
|
137
|
-
# the command's own args already named)
|
|
138
|
-
# nil — the id correctly
|
|
139
|
-
# attributes
|
|
220
|
+
# it at all. Without this, a creating command that declares those heads
|
|
221
|
+
# as ordinary attributes but doesn't also `sets` them (redundant with
|
|
222
|
+
# the identity the command's own args already named) would persist
|
|
223
|
+
# every head as nil — the id correctly naming the record, but the
|
|
224
|
+
# record's own attributes forgetting what named it.
|
|
140
225
|
#
|
|
141
226
|
# Filled from `args`, never from splitting `@id` back apart — the
|
|
142
227
|
# same reason the single-head branch below won't guess a multi-path
|
|
143
228
|
# identifier from its joined string: `@id` is a display key, not a
|
|
144
229
|
# reversible serialization, and a composite's own separator can
|
|
145
|
-
# collide with a part's own text. `args` is only offered by a
|
|
230
|
+
# collide with a part's own text. `args` is only offered by a fresh
|
|
146
231
|
# creation (`Instance.new`'s own `args:` comment); an existing record
|
|
147
232
|
# read back from storage has no args to lean on, and doesn't need
|
|
148
233
|
# one since a correctly-persisted record already carries its own
|
|
@@ -8,18 +8,26 @@ module Hecks
|
|
|
8
8
|
# in CommandInterpreter and EntityInterpreter, where they could only
|
|
9
9
|
# ever drift.
|
|
10
10
|
module Interpreting
|
|
11
|
+
# Ruby's module-inclusion hook. Gives `interpreter` its own `trace`
|
|
12
|
+
# accessor, since a class including this module needs its own copy
|
|
13
|
+
# rather than one shared across every interpreter.
|
|
14
|
+
#
|
|
11
15
|
# Each including interpreter gets its own `trace` — set by a spec to
|
|
12
16
|
# observe dispatch order (Vocabulary::AggregateDispatchOrder and
|
|
13
17
|
# Vocabulary::EntityDispatchOrder in language/bluebook/vocabulary.bluebook);
|
|
14
18
|
# nil in production, always — one array push and a nil check per step
|
|
15
19
|
# is the entire cost of leaving this in.
|
|
20
|
+
#
|
|
21
|
+
# @param interpreter [Class] the class (`CommandInterpreter`, `EntityInterpreter`)
|
|
22
|
+
# including this module
|
|
23
|
+
# @return [void]
|
|
16
24
|
def self.included(interpreter)
|
|
17
25
|
interpreter.singleton_class.attr_accessor :trace
|
|
18
26
|
end
|
|
19
27
|
|
|
20
28
|
private
|
|
21
29
|
|
|
22
|
-
# Logged
|
|
30
|
+
# Logged after the step's own work, so a step that wraps sub-steps logs
|
|
23
31
|
# itself once everything inside it has already logged — trace order is
|
|
24
32
|
# completion order, which is dispatch order.
|
|
25
33
|
def step(name)
|
|
@@ -28,21 +36,21 @@ module Hecks
|
|
|
28
36
|
result
|
|
29
37
|
end
|
|
30
38
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
39
|
+
# Drives `DISPATCH_ORDER` (CommandInterpreter/EntityInterpreter, each
|
|
40
|
+
# read off its own generated Vocabulary::*DispatchOrder table —
|
|
41
|
+
# vocabulary.bluebook, via lib/hecks/vocabulary.rb) by `send`ing
|
|
34
42
|
# each declared step name against the including interpreter's own
|
|
35
|
-
# `step_<name>` handler, in declared order.
|
|
36
|
-
#
|
|
37
|
-
# tracing a real dispatch and comparing it to the declaration is
|
|
43
|
+
# `step_<name>` handler, in declared order. Data drives that sequence,
|
|
44
|
+
# rather than `call` spelling it out as a literal sequence of method
|
|
45
|
+
# calls — tracing a real dispatch and comparing it to the declaration is
|
|
38
46
|
# tautological once `call` mechanically follows the declaration; a
|
|
39
47
|
# conditional step (assign_creation_attributes, advance_lifecycle) still
|
|
40
|
-
# has to guard
|
|
48
|
+
# has to guard itself at the top of its own handler and skip tracing
|
|
41
49
|
# when it does not fire, rather than the caller branching around it —
|
|
42
50
|
# see CommandInterpreter#step_assign_creation_attributes.
|
|
43
|
-
#
|
|
51
|
+
# The commit boundary. Every step up to `save` runs as before;
|
|
44
52
|
# `save`, `emit`, and the outbox enqueue that follows them run
|
|
45
|
-
# inside
|
|
53
|
+
# inside one `repository.transaction` — so the aggregate row, its
|
|
46
54
|
# journal entry, the recorded event, and the outbox rows naming
|
|
47
55
|
# who is owed a reaction commit together or not at all. No new
|
|
48
56
|
# step is added to the vocabulary's dispatch order (the step list
|
|
@@ -71,20 +79,20 @@ module Hecks
|
|
|
71
79
|
ctx.outbox_rows = @registry.outbox.enqueue(ctx.repository, Array(ctx.result), ctx.domain)
|
|
72
80
|
end
|
|
73
81
|
|
|
74
|
-
#
|
|
82
|
+
# The concurrency-control split — see docs/decisions/ (concurrency
|
|
75
83
|
# control ADR) for the full mechanism. A repository that declares
|
|
76
84
|
# `:optimistic_concurrency` (Postgres today) already closes the
|
|
77
85
|
# lost-update gap itself, via `step_save`'s CAS + `#call`'s own
|
|
78
86
|
# `StaleWrite` retry loop — an extra in-process lock here would be
|
|
79
87
|
# pointless overhead, not incorrect, so it's skipped for clarity.
|
|
80
88
|
# A repository that declares `:cross_process_lock` (PostgresEra —
|
|
81
|
-
# ADR 0036) holds a
|
|
89
|
+
# ADR 0036) holds a real Postgres advisory lock for the whole
|
|
82
90
|
# dispatch order instead: unlike Heki/Memory (confirmed
|
|
83
91
|
# process-local, never a second process writing the same store),
|
|
84
92
|
# PostgresEra's own tables can be dispatched against concurrently
|
|
85
93
|
# by `rust/host` from a separate OS process, and an in-process
|
|
86
|
-
# `Mutex` is invisible to that. Every
|
|
87
|
-
# striped `Mutex` below, held for the
|
|
94
|
+
# `Mutex` is invisible to that. Every other repository gets the
|
|
95
|
+
# striped `Mutex` below, held for the whole dispatch-order run, so
|
|
88
96
|
# a second thread's own hydrate can't start until the first
|
|
89
97
|
# thread's save has landed. `lock_key_id` is best-effort
|
|
90
98
|
# (`Identity.best_effort`) — `nil` still locks correctly, just
|
|
@@ -116,8 +124,8 @@ module Hecks
|
|
|
116
124
|
# refuse_absent_arguments are separate DISPATCH_ORDER steps now (the
|
|
117
125
|
# declared vocabulary lists all three as flat, sequential members, not
|
|
118
126
|
# one nesting the other two), and EntityInterpreter never had them here
|
|
119
|
-
# at all (an entity inherits its aggregate's own gate). One
|
|
120
|
-
#
|
|
127
|
+
# at all (an entity inherits its aggregate's own gate). One shared
|
|
128
|
+
# copy, rather than two identical ones that can only ever drift apart.
|
|
121
129
|
def normalize_args(aggregate, command, args)
|
|
122
130
|
coerce_declared_arguments(aggregate, command, args)
|
|
123
131
|
end
|