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
|
@@ -6,7 +6,7 @@ require_relative "identity"
|
|
|
6
6
|
require_relative "instance"
|
|
7
7
|
require_relative "value"
|
|
8
8
|
require_relative "refusal_wording"
|
|
9
|
-
require_relative "
|
|
9
|
+
require_relative "invocation"
|
|
10
10
|
require_relative "dependency_planning"
|
|
11
11
|
require_relative "../ports/persistence/execution"
|
|
12
12
|
require_relative "entity_element"
|
|
@@ -23,9 +23,9 @@ module Hecks
|
|
|
23
23
|
# entity-owned commands.
|
|
24
24
|
class EntityInterpreter
|
|
25
25
|
include Interpreting
|
|
26
|
-
#
|
|
27
|
-
# run — bug audit H1 (docs/audits/2026-08-10-main-bug-audit.md):
|
|
28
|
-
#
|
|
26
|
+
# The same payload gate aggregate commands and port operations already
|
|
27
|
+
# run — bug audit H1 (docs/audits/2026-08-10-main-bug-audit.md): without
|
|
28
|
+
# this, this class runs neither refuse_unknown_arguments nor
|
|
29
29
|
# refuse_absent_arguments, on a comment claiming "an entity inherits
|
|
30
30
|
# its aggregate's own gate." Nothing on the entity dispatch path ever
|
|
31
31
|
# ran one — confirmed live, `LedgerEntry.Reverse` accepted an
|
|
@@ -39,11 +39,9 @@ module Hecks
|
|
|
39
39
|
|
|
40
40
|
attr_reader :registry
|
|
41
41
|
|
|
42
|
-
#
|
|
43
|
-
# (language/bluebook/vocabulary.bluebook
|
|
44
|
-
#
|
|
45
|
-
# own DISPATCH_ORDER is; see that constant's doc comment for why this is
|
|
46
|
-
# hand-typed rather than read live off the meta-domain at every dispatch.
|
|
42
|
+
# **The declared order** — Vocabulary::EntityDispatchOrder
|
|
43
|
+
# (language/bluebook/vocabulary.bluebook), read off the generated table
|
|
44
|
+
# the same way CommandInterpreter's own DISPATCH_ORDER is.
|
|
47
45
|
# `refuse_unknown_arguments`/`refuse_absent_arguments` now lead it, same
|
|
48
46
|
# position `AggregateDispatchOrder` holds them at (H1, above) — the only
|
|
49
47
|
# remaining difference from the aggregate order is no
|
|
@@ -55,7 +53,7 @@ module Hecks
|
|
|
55
53
|
# see that constant's own comment.
|
|
56
54
|
MAX_STALE_WRITE_RETRIES = 5
|
|
57
55
|
|
|
58
|
-
# `instance` is the
|
|
56
|
+
# `instance` is the parent aggregate record (what gets saved and
|
|
59
57
|
# returned) ; `element`/`view` are the entity piece itself — `view`
|
|
60
58
|
# wraps `element` as it stood at `locate_element`, pre-mutation, and
|
|
61
59
|
# `enforce_ensures` builds its own settled wrapper off `element` as it
|
|
@@ -63,7 +61,7 @@ module Hecks
|
|
|
63
61
|
#
|
|
64
62
|
# `chain` — S17, ADR 0026 — every entity the dotted verb passes
|
|
65
63
|
# through, root-first (`[Handler, Dispatch]` for `Handler.Dispatch.
|
|
66
|
-
# Bind`) ; `entity`/`entity_name` stay the
|
|
64
|
+
# Bind`) ; `entity`/`entity_name` stay the chain's own last entry,
|
|
67
65
|
# the one a command actually belongs to and a mutation actually
|
|
68
66
|
# targets, so every step written before this ADR (enforce_givens,
|
|
69
67
|
# apply_mutations, advance_lifecycle, element_identity, ...) reads
|
|
@@ -76,8 +74,66 @@ module Hecks
|
|
|
76
74
|
Context = Struct.new(:domain, :aggregate, :entity, :entity_name, :command, :command_name,
|
|
77
75
|
:args, :repository, :instance, :chain, :element, :view, :transition,
|
|
78
76
|
:old_element, :result, :route, :plan, :persistence_outcome, :dry_run, :outbox_rows,
|
|
79
|
-
:correction_bindings)
|
|
77
|
+
:correction_bindings, :invocation)
|
|
80
78
|
|
|
79
|
+
# A dotted entity verb resolved against its aggregate. `Resolution.of`
|
|
80
|
+
# lives here rather than as a second public verb on the interpreter
|
|
81
|
+
# (spec/runtime/command_rules_spec.rb holds each interpreter to one),
|
|
82
|
+
# so `Dispatcher` can resolve it at the point `Invocation.from_call`
|
|
83
|
+
# reads the declaring command — after `to:` is parsed, before the
|
|
84
|
+
# facts are.
|
|
85
|
+
Resolution = Data.define(:entity_names, :chain, :command_name, :command) do
|
|
86
|
+
# Resolves a dotted entity verb against `aggregate` into the entity
|
|
87
|
+
# chain it walks and the command it names.
|
|
88
|
+
#
|
|
89
|
+
# Refuses UnknownVerb for an unknown entity or command.
|
|
90
|
+
#
|
|
91
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate the dotted verb
|
|
92
|
+
# is resolved against
|
|
93
|
+
# @param dotted [String, Symbol] the entity verb, with the leading
|
|
94
|
+
# "Domain::Aggregate." already stripped (e.g. `"Handler.Dispatch.Bind"`)
|
|
95
|
+
# @return [EntityInterpreter::Resolution] the resolved entity names, chain,
|
|
96
|
+
# command name and command class
|
|
97
|
+
# @raise [Runtime::UnknownVerb] if `dotted` names no entity, or the located
|
|
98
|
+
# entity declares no such command
|
|
99
|
+
def self.of(aggregate, dotted)
|
|
100
|
+
*entity_names, command_name = dotted.to_s.split(".")
|
|
101
|
+
if entity_names.empty?
|
|
102
|
+
raise UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_unknown",
|
|
103
|
+
aggregate: aggregate.hecks_name, entity: dotted.to_s)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
chain = walk(aggregate, entity_names)
|
|
107
|
+
command = chain.last.command(command_name) ||
|
|
108
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_no_command",
|
|
109
|
+
entity: chain.last.hecks_name, command: command_name))
|
|
110
|
+
new(entity_names: entity_names, chain: chain, command_name: command_name, command: command)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# **One hop per dotted segment** — `ProcessManager.Handler.Dispatch.Bind`
|
|
114
|
+
# (once the dispatcher has already stripped "Domain::Aggregate.")
|
|
115
|
+
# walks Handler off the aggregate, then Dispatch off Handler, each
|
|
116
|
+
# step reading `.entities` exactly the way the single-level case
|
|
117
|
+
# always did — a nested entity is "structurally interchangeable
|
|
118
|
+
# with an aggregate" (Entity's own header) for precisely this
|
|
119
|
+
# reason. Two levels is what Handler/Dispatch need today ; nothing
|
|
120
|
+
# here assumes it stops at two.
|
|
121
|
+
def self.walk(aggregate, entity_names)
|
|
122
|
+
owner = aggregate
|
|
123
|
+
entity_names.map do |name|
|
|
124
|
+
found = owner.entities.find { |piece| piece.hecks_name == name } ||
|
|
125
|
+
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_unknown",
|
|
126
|
+
aggregate: owner.hecks_name, entity: name))
|
|
127
|
+
owner = found
|
|
128
|
+
found
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
private_class_method :walk
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @param registry [Runtime::Registry] the booted registry this interpreter reads
|
|
135
|
+
# @param rules [Runtime::CommandRules] the shared rules engine (admissibility,
|
|
136
|
+
# references, arithmetic, authorization, emission) dispatch runs through
|
|
81
137
|
def initialize(registry, rules:)
|
|
82
138
|
@registry = registry
|
|
83
139
|
@rules = rules
|
|
@@ -87,39 +143,57 @@ module Hecks
|
|
|
87
143
|
# comment for the shared reasoning (Dispatcher#dry_run?'s own entry
|
|
88
144
|
# point). `step_save`/`step_emit` are the only two steps here that
|
|
89
145
|
# read it either.
|
|
90
|
-
#
|
|
146
|
+
# Retries the whole method body on `StaleWrite` — same reasoning as
|
|
91
147
|
# `CommandInterpreter#call`'s own retry: a fresh `ctx`, a fresh
|
|
92
148
|
# `step_hydrate_parent`/`step_locate_element` re-reading current
|
|
93
149
|
# state.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
150
|
+
#
|
|
151
|
+
# `resolution` is #resolve's answer; `invocation` the
|
|
152
|
+
# `Runtime::Invocation` `Dispatcher` built — `ctx.args` is its
|
|
153
|
+
# `to_args`, `ctx.route` its `target`.
|
|
154
|
+
#
|
|
155
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to
|
|
156
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate owning the entity
|
|
157
|
+
# chain the command targets
|
|
158
|
+
# @param resolution [EntityInterpreter::Resolution] the resolved entity chain
|
|
159
|
+
# and command, as `Resolution.of` builds it
|
|
160
|
+
# @param invocation [Runtime::Invocation] the invocation `Dispatcher` built for
|
|
161
|
+
# this call
|
|
162
|
+
# @param dry_run [Boolean] whether to run every step through validation without
|
|
163
|
+
# saving, emitting or enqueueing
|
|
164
|
+
# @return [Array(Runtime::Instance, Array<Runtime::Event>,
|
|
165
|
+
# Runtime::DependencyPlanning::Plan, Ports::Persistence::Execution,
|
|
166
|
+
# Array<Runtime::Outbox::Row>)] the settled parent aggregate instance, emitted
|
|
167
|
+
# events, execution plan, persistence outcome and outbox rows — the last
|
|
168
|
+
# three nil on a dry run, which skips save/emit/outbox
|
|
169
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
|
|
170
|
+
# given/ensures/invariant/authorization/admissibility rule refuses
|
|
171
|
+
# @raise [Runtime::StaleWrite] if concurrent writers beat this one through every
|
|
172
|
+
# retry (`MAX_STALE_WRITE_RETRIES`)
|
|
173
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
174
|
+
def call(domain, aggregate, resolution, invocation, dry_run: false)
|
|
175
|
+
chain = resolution.chain
|
|
176
|
+
entity = chain.last
|
|
177
|
+
command = resolution.command
|
|
178
|
+
command_name = resolution.command_name
|
|
179
|
+
route = invocation.target
|
|
180
|
+
args = invocation.to_args
|
|
108
181
|
attempt = 0
|
|
109
182
|
begin
|
|
110
|
-
ctx = Context.new(domain, aggregate, entity, entity_names.join("."), command, command_name, args)
|
|
183
|
+
ctx = Context.new(domain, aggregate, entity, resolution.entity_names.join("."), command, command_name, args)
|
|
184
|
+
ctx.invocation = invocation
|
|
111
185
|
ctx.chain = chain
|
|
112
186
|
ctx.route = route
|
|
113
187
|
ctx.dry_run = dry_run
|
|
114
188
|
# `root_aggregate:` — `entity` is the immediate owner (what
|
|
115
189
|
# `owner_fields` inside the Analyzer means), but a `parent.X`
|
|
116
|
-
# read inside this command's own given/ensures means the
|
|
117
|
-
# aggregate's own field, not the entity's — `aggregate` here
|
|
190
|
+
# read inside this command's own given/ensures means the root
|
|
191
|
+
# aggregate's own field, not the entity's — `aggregate` here is
|
|
118
192
|
# that root (this method's own first parameter, never the
|
|
119
193
|
# entity). See DependencyPlanning::Analyzer.call's own header
|
|
120
194
|
# for the bug this closes.
|
|
121
195
|
ctx.plan = DependencyPlanning::Analyzer.call(aggregate: entity, command: command, root_aggregate: aggregate)
|
|
122
|
-
#
|
|
196
|
+
# **Resolved here, once** — see CommandInterpreter#call's own comment;
|
|
123
197
|
# `step_hydrate_parent` reads `ctx.repository` without re-fetching.
|
|
124
198
|
ctx.repository = @registry.repository(domain, aggregate)
|
|
125
199
|
lock_id = Identity.best_effort(aggregate, args, route)
|
|
@@ -134,28 +208,13 @@ module Hecks
|
|
|
134
208
|
|
|
135
209
|
private
|
|
136
210
|
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
# step reading `.entities` exactly the way the single-level case
|
|
141
|
-
# always did — a nested entity is "structurally interchangeable
|
|
142
|
-
# with an aggregate" (Entity's own header) for precisely this
|
|
143
|
-
# reason. Two levels is what Handler/Dispatch need today ; nothing
|
|
144
|
-
# here assumes it stops at two.
|
|
145
|
-
def walk_entity_chain(aggregate, entity_names)
|
|
146
|
-
owner = aggregate
|
|
147
|
-
entity_names.map do |name|
|
|
148
|
-
found = owner.entities.find { |piece| piece.hecks_name == name } ||
|
|
149
|
-
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_unknown",
|
|
150
|
-
aggregate: owner.hecks_name, entity: name.inspect))
|
|
151
|
-
owner = found
|
|
152
|
-
found
|
|
153
|
-
end
|
|
154
|
-
end
|
|
211
|
+
# **A no-op, and untraced** — Vocabulary::EntityDispatchOrder's
|
|
212
|
+
# decode_arguments. See CommandInterpreter#step_decode_arguments.
|
|
213
|
+
def step_decode_arguments(_ctx); end
|
|
155
214
|
|
|
156
215
|
# `extra_identity_heads:` — every entity `ctx.chain` walks through, not
|
|
157
216
|
# just the root aggregate `ArgumentGate` already knows about. A
|
|
158
|
-
# two-hop dispatch (`Handler.Dispatch.Bind`) is addressed by
|
|
217
|
+
# two-hop dispatch (`Handler.Dispatch.Bind`) is addressed by both
|
|
159
218
|
# hops' own identity, each read straight out of `args` by
|
|
160
219
|
# `EntityElement#element_of` — refusing those as unknown would refuse
|
|
161
220
|
# every legitimate nested-entity dispatch there is, the same reasoning
|
|
@@ -202,30 +261,30 @@ module Hecks
|
|
|
202
261
|
ctx.element = step(:locate_element) do
|
|
203
262
|
EntityElement.locate_chain(ctx.aggregate, ctx.chain, ctx.instance, ctx.args, ctx.command_name, ctx.route)
|
|
204
263
|
end
|
|
205
|
-
# `view` was hydrated
|
|
264
|
+
# `view` was hydrated once, here, into its own state hash
|
|
206
265
|
# (Value.hydrate builds a fresh Hash — never aliased with `element`)
|
|
207
266
|
# — exactly right for enforce_givens, which must read pre-mutation.
|
|
208
267
|
ctx.view = Instance.new(aggregate: ctx.entity, id: EntityElement.element_identity(ctx.entity, ctx.element).to_s,
|
|
209
268
|
state: ctx.element)
|
|
210
269
|
end
|
|
211
270
|
|
|
212
|
-
# BUG#30 —
|
|
271
|
+
# BUG#30 — the entity-level half of `CommandInterpreter#step_enforce_
|
|
213
272
|
# givens`'s own structural-before-declared ordering (see that
|
|
214
273
|
# method's comment for the shared reasoning): "does the fact this
|
|
215
274
|
# command's `corrects` names even exist" is checked here too, once,
|
|
216
275
|
# before the entity's own `given`s.
|
|
217
276
|
#
|
|
218
|
-
#
|
|
219
|
-
# deliberately `ctx.instance`/`ctx.aggregate` (the
|
|
220
|
-
# record and the
|
|
277
|
+
# Admissibility is checked against the parent/root, not the entity —
|
|
278
|
+
# deliberately `ctx.instance`/`ctx.aggregate` (the parent aggregate
|
|
279
|
+
# record and the root aggregate construct), never `ctx.view`/
|
|
221
280
|
# `ctx.entity` (the entity's own pre-mutation view/construct). This
|
|
222
|
-
# is not a simplification; it is the
|
|
281
|
+
# is not a simplification; it is the only choice that lines up with
|
|
223
282
|
# how the event being corrected was actually recorded: an entity has
|
|
224
283
|
# no event stream of its own — `CommandRules::Emission#emit` (called
|
|
225
|
-
# from
|
|
284
|
+
# from this class's own `step_emit`, and from `CommandInterpreter`'s
|
|
226
285
|
# `step_emit` for an aggregate-level command alike) always stamps an
|
|
227
|
-
# emitted event with the
|
|
228
|
-
# (`"#{domain}::#{aggregate.hecks_name}"`) and the
|
|
286
|
+
# emitted event with the root aggregate's own qualified name
|
|
287
|
+
# (`"#{domain}::#{aggregate.hecks_name}"`) and the parent record's
|
|
229
288
|
# own id (`ctx.instance.id`), regardless of which level dispatched
|
|
230
289
|
# it. `enforce_correction_target` (CommandRules::Admissibility)
|
|
231
290
|
# looks a correction target up by exactly those two fields plus the
|
|
@@ -234,31 +293,31 @@ module Hecks
|
|
|
234
293
|
# carry, and every entity-level correction would refuse
|
|
235
294
|
# (NothingToCorrect) even against a real, already-emitted event.
|
|
236
295
|
# `qa/stress_domains/corrections`' own `Entry.Amend` (corrects
|
|
237
|
-
# "EntryRecorded", which `Ledger.Record` — an
|
|
296
|
+
# "EntryRecorded", which `Ledger.Record` — an aggregate-level
|
|
238
297
|
# command — actually emits) is exactly this shape: the corrected
|
|
239
|
-
# event's `aggregate`/`id` are the
|
|
298
|
+
# event's `aggregate`/`id` are the ledger's, never the Entry's own
|
|
240
299
|
# (an Entry has no id an event could be filed under in the first
|
|
241
300
|
# place). `Fuzzing::Properties::Corrections#corrections_reference_
|
|
242
301
|
# an_emitted_event` independently encodes the identical rule
|
|
243
|
-
# (`aggregate_key` built off the
|
|
302
|
+
# (`aggregate_key` built off the outer aggregate for both the
|
|
244
303
|
# `corrects` target and the `emits` produced event, regardless of
|
|
245
304
|
# entity nesting depth) — this is that property's dispatch-time
|
|
246
305
|
# enforcement counterpart, not a new invention.
|
|
247
306
|
#
|
|
248
307
|
# One structural consequence, worth being explicit about for a
|
|
249
|
-
# Rust port: because the lookup is scoped to the
|
|
308
|
+
# Rust port: because the lookup is scoped to the parent record
|
|
250
309
|
# (not to any one entity element within it), an entity-level
|
|
251
310
|
# `corrects` only proves "this parent record has emitted the named
|
|
252
|
-
# event at some point" — it does
|
|
311
|
+
# event at some point" — it does not, and cannot, further narrow
|
|
253
312
|
# to "...specifically for THIS entity element" (a Ledger with three
|
|
254
313
|
# Entries all satisfy the same `EntryRecorded`-was-emitted check).
|
|
255
|
-
# That is not a gap this fix introduces: it is the
|
|
314
|
+
# That is not a gap this fix introduces: it is the same granularity
|
|
256
315
|
# the aggregate-level check already has (one record, one event
|
|
257
316
|
# history), just observed from one level down. A command wanting a
|
|
258
317
|
# tighter, element-specific correlation has to encode it itself, in
|
|
259
318
|
# its own `given`s, off `correction`-bound payload fields.
|
|
260
319
|
#
|
|
261
|
-
# `correction:` bindings computed here are threaded through to
|
|
320
|
+
# `correction:` bindings computed here are threaded through to both
|
|
262
321
|
# halves of the same command's admissibility, same as the
|
|
263
322
|
# aggregate-level path: `ctx.correction_bindings` is read again by
|
|
264
323
|
# `step_enforce_ensures`, below, so an `as:`-named binding is
|
|
@@ -292,7 +351,7 @@ module Hecks
|
|
|
292
351
|
step(:advance_lifecycle) { ctx.element[ctx.entity.lifecycle.field] = ctx.transition.target }
|
|
293
352
|
end
|
|
294
353
|
|
|
295
|
-
# An ensures reads the
|
|
354
|
+
# An ensures reads the settled record, so it needs a view hydrated from
|
|
296
355
|
# `element` as it stands now, mutations included — unlike `view` above,
|
|
297
356
|
# built once and read pre-mutation by enforce_givens.
|
|
298
357
|
def step_enforce_ensures(ctx)
|
|
@@ -309,9 +368,9 @@ module Hecks
|
|
|
309
368
|
end
|
|
310
369
|
end
|
|
311
370
|
|
|
312
|
-
#
|
|
371
|
+
# The parent aggregate's own invariants — `ctx.instance` is the
|
|
313
372
|
# parent record an entity mutation writes into (this file's own
|
|
314
|
-
# `Context` comment), the
|
|
373
|
+
# `Context` comment), the same boundary an aggregate-level
|
|
315
374
|
# invariant guards regardless of which interpreter changed it. No
|
|
316
375
|
# separate "entity invariant" exists (S10, ADR 0025 scopes
|
|
317
376
|
# `invariant` to the aggregate only) — see `Admissibility#
|
|
@@ -320,21 +379,23 @@ module Hecks
|
|
|
320
379
|
step(:enforce_invariants) { @rules.enforce_invariants(ctx.instance, ctx.aggregate, domain: ctx.domain) }
|
|
321
380
|
end
|
|
322
381
|
|
|
323
|
-
# `dry_run:` skips this — see
|
|
324
|
-
#
|
|
325
|
-
#
|
|
382
|
+
# `dry_run:` skips the write half of this step — see
|
|
383
|
+
# CommandInterpreter#step_save's own comment (BUG#127): the
|
|
384
|
+
# reference-existence check stays unconditional, only the actual
|
|
385
|
+
# persist is behind the early return.
|
|
326
386
|
def step_save(ctx)
|
|
387
|
+
step(:save) { @rules.resolve_state_references(ctx.domain, ctx.aggregate, ctx.instance.state) }
|
|
388
|
+
|
|
327
389
|
return if ctx.dry_run
|
|
328
390
|
|
|
329
391
|
step(:save) do
|
|
330
|
-
@rules.resolve_state_references(ctx.domain, ctx.aggregate, ctx.instance.state)
|
|
331
392
|
# `expected_version:` — see CommandInterpreter#step_save's own
|
|
332
393
|
# comment: nil for a repository that isn't CAS-capable, or an
|
|
333
394
|
# instance never read from storage, either of which falls
|
|
334
395
|
# through to a plain save inside `AppendOnly#save`.
|
|
335
396
|
ctx.persistence_outcome = ctx.repository.save(ctx.instance, expected_version: ctx.instance.version)
|
|
336
397
|
if ctx.persistence_outcome.status == :stale
|
|
337
|
-
#
|
|
398
|
+
# Not a `RefusalWording.render` call — see
|
|
338
399
|
# `CommandInterpreter#step_save`'s identical branch and
|
|
339
400
|
# `Runtime::StaleWrite`'s own comment.
|
|
340
401
|
raise(StaleWrite,
|
|
@@ -353,7 +414,7 @@ module Hecks
|
|
|
353
414
|
ctx.result = step(:emit) { @rules.emit(ctx.command, ctx.domain, ctx.aggregate, ctx.instance, ctx.args, ctx.repository) }
|
|
354
415
|
end
|
|
355
416
|
|
|
356
|
-
#
|
|
417
|
+
# The parent aggregate, addressed exactly as `CommandInterpreter#hydrate`
|
|
357
418
|
# addresses one acting on itself — derive from the declared identity first
|
|
358
419
|
# (`Identity.of`), and let a bare `id:` name an already-derived record when
|
|
359
420
|
# the identity itself is not what the caller is holding.
|
|
@@ -361,24 +422,24 @@ module Hecks
|
|
|
361
422
|
parent_id = route&.aggregate ||
|
|
362
423
|
Identity.of(aggregate, args) ||
|
|
363
424
|
Identity.from(aggregate, args, :id) ||
|
|
364
|
-
raise(NotFound, RefusalWording.
|
|
365
|
-
|
|
366
|
-
|
|
425
|
+
raise(NotFound, RefusalWording.render_site("NotFound", "entity_parent_no_identity",
|
|
426
|
+
command: command_name, aggregate: aggregate.hecks_name,
|
|
427
|
+
entity: entity_name, identity: Identity.reading(aggregate)))
|
|
367
428
|
found = repository.find(parent_id) ||
|
|
368
|
-
raise(NotFound, RefusalWording.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
429
|
+
raise(NotFound, RefusalWording.render_site("NotFound", "record_missing",
|
|
430
|
+
aggregate: aggregate.hecks_name,
|
|
431
|
+
identity: Identity.reading(aggregate),
|
|
432
|
+
offered: Rendering.describe(parent_id)))
|
|
372
433
|
found.dup
|
|
373
434
|
end
|
|
374
435
|
|
|
375
436
|
# `locate_chain`/`element_of`/`element_identity`/`apply_to_element` and
|
|
376
|
-
# their own helpers
|
|
377
|
-
# (see that file's own header)
|
|
378
|
-
# `delegate_to_entity` step can locate and
|
|
379
|
-
# same way, against an aggregate record
|
|
380
|
-
# above, and every `step_*` method
|
|
381
|
-
#
|
|
437
|
+
# their own helpers live in `Runtime::EntityElement`
|
|
438
|
+
# (see that file's own header), shared rather than kept here, so
|
|
439
|
+
# `CommandInterpreter`'s own `delegate_to_entity` step can locate and
|
|
440
|
+
# mutate the same element the same way, against an aggregate record
|
|
441
|
+
# already held in memory. `call`, above, and every `step_*` method
|
|
442
|
+
# reach them through that module.
|
|
382
443
|
end
|
|
383
444
|
end
|
|
384
445
|
end
|
data/lib/hecks/runtime/errors.rb
CHANGED
|
@@ -2,17 +2,21 @@ require_relative "value/invariant_violation"
|
|
|
2
2
|
require_relative "../vocabulary"
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
|
+
# What runs a booted domain: dispatch, the command/entity/query/policy/saga
|
|
6
|
+
# interpreters, the registry a boot assembles, and the errors below —
|
|
7
|
+
# everything downstream of a `.bluebook`/`.hecksagon`/`.world` declaration.
|
|
8
|
+
# See `lib/hecks/runtime.rb` for the module's own facade and boot entry points.
|
|
5
9
|
module Runtime
|
|
6
10
|
class UnknownVerb < StandardError; end
|
|
7
11
|
class EnsuresNotMet < StandardError; end
|
|
8
12
|
|
|
9
13
|
# `detail` — the failing comparison's own resolved operands, "left: X,
|
|
10
|
-
# right: Y" — set only when the given's
|
|
14
|
+
# right: Y" — set only when the given's top-level shape is a bare
|
|
11
15
|
# comparison (`Evaluator.comparison_detail`'s own comment has the full
|
|
12
|
-
# scoping); nil otherwise. Deliberately
|
|
16
|
+
# scoping); nil otherwise. Deliberately not folded into `#message`:
|
|
13
17
|
# that string is pinned byte-for-byte across this corpus's own specs
|
|
14
18
|
# (`raise_error(GivenNotMet, "...")`, command_rules_spec.rb and every
|
|
15
|
-
# domain that vendors this gem) as
|
|
19
|
+
# domain that vendors this gem) as contract, so changing its shape by
|
|
16
20
|
# default would be a breaking change for every one of them. Riding on
|
|
17
21
|
# `#detailed_message` instead (Ruby 3.2+, what irb/a Rails console's
|
|
18
22
|
# own unhandled-exception banner already calls to show more than
|
|
@@ -21,11 +25,21 @@ module Hecks
|
|
|
21
25
|
class GivenNotMet < StandardError
|
|
22
26
|
attr_reader :detail
|
|
23
27
|
|
|
28
|
+
# @param message [String, nil] the refusal text, pinned byte-for-byte across specs
|
|
29
|
+
# @param detail [String, nil] the failing comparison's resolved operands ("left: X,
|
|
30
|
+
# right: Y"), or nil when the given's top-level shape is not a bare comparison
|
|
24
31
|
def initialize(message = nil, detail: nil)
|
|
25
32
|
super(message)
|
|
26
33
|
@detail = detail
|
|
27
34
|
end
|
|
28
35
|
|
|
36
|
+
# Renders `message` with `detail` appended, for a human reading an unhandled refusal.
|
|
37
|
+
#
|
|
38
|
+
# @param highlight [Boolean] unused; accepted for compatibility with `Exception#
|
|
39
|
+
# detailed_message`'s own signature
|
|
40
|
+
# @param opts [Hash] unused; accepted for compatibility with `Exception#
|
|
41
|
+
# detailed_message`'s own signature
|
|
42
|
+
# @return [String] `message`, with `" (#{detail})"` appended when `detail` is present
|
|
29
43
|
def detailed_message(highlight: false, **opts)
|
|
30
44
|
base = super
|
|
31
45
|
detail ? "#{base} (#{detail})" : base
|
|
@@ -39,7 +53,7 @@ module Hecks
|
|
|
39
53
|
# is the right name carrying the wrong thing, this one is a name the command
|
|
40
54
|
# never had. Both are the payload gate refusing before any rule runs.
|
|
41
55
|
class UnknownArgument < StandardError; end
|
|
42
|
-
# The third of the trio, and the one that was missing : a name the command
|
|
56
|
+
# The third of the trio, and the one that was missing : a name the command does
|
|
43
57
|
# declare, absent. TypeMismatch is the right name carrying the wrong thing,
|
|
44
58
|
# UnknownArgument a name that was never declared, AbsentArgument a declared name
|
|
45
59
|
# that never arrived. Between them they say a command takes exactly the
|
|
@@ -56,18 +70,18 @@ module Hecks
|
|
|
56
70
|
# `given`/`ensures`/`invariant` that reads the field would otherwise
|
|
57
71
|
# evaluate against a value nobody wrote, which is the same silent-
|
|
58
72
|
# wrong-answer class as an unpopulated projection reading "not
|
|
59
|
-
# active" (ADR 0025, "Added attributes and absence"). An
|
|
73
|
+
# active" (ADR 0025, "Added attributes and absence"). An optional
|
|
60
74
|
# attribute in the same spot reads nil instead — that is what
|
|
61
75
|
# optional means, and this refusal is deliberately narrower than the
|
|
62
76
|
# nil-read it sits beside, not a replacement for it.
|
|
63
77
|
class AttributeAbsent < StandardError; end
|
|
64
|
-
#
|
|
78
|
+
# The same silent-wrong-answer class as above, one line up — a
|
|
65
79
|
# `projects` field (S12, ADR 0025) this record predates, or that no
|
|
66
80
|
# rebuild sweep has populated yet, read by a `given`/`ensures`/
|
|
67
81
|
# `invariant` as though it carried a real value. `GuardState` is
|
|
68
82
|
# the one place this is raised, the same way AttributeAbsent is —
|
|
69
|
-
# a
|
|
70
|
-
# from that one only in
|
|
83
|
+
# a declared field the record does not yet carry, distinguished
|
|
84
|
+
# from that one only in why: an ordinary attribute is absent
|
|
71
85
|
# because nobody backfilled it, a projected field is absent
|
|
72
86
|
# because nobody has swept it yet.
|
|
73
87
|
class ProjectionAbsent < StandardError; end
|
|
@@ -79,14 +93,14 @@ module Hecks
|
|
|
79
93
|
class Unauthorized < StandardError; end
|
|
80
94
|
# `corrects` names a past event this record must have already emitted
|
|
81
95
|
# (CommandBuilder#corrects_impl's own comment) — a fact the expression
|
|
82
|
-
# evaluator cannot check (it is not a predicate over the record's
|
|
96
|
+
# evaluator cannot check (it is not a predicate over the record's own
|
|
83
97
|
# fields, it is "did this exact record ever announce this"), so it is
|
|
84
98
|
# raised structurally, the same way AlreadyExists/NotFound are, rather
|
|
85
99
|
# than being expressible as an ordinary `given`. Raised by
|
|
86
100
|
# `CommandRules::Admissibility#enforce_correction_target`.
|
|
87
101
|
class NothingToCorrect < StandardError; end
|
|
88
102
|
|
|
89
|
-
# A
|
|
103
|
+
# A runtime fault, not a domain refusal — deliberately absent from
|
|
90
104
|
# `DOMAIN_REFUSALS` below and from `vocabulary.bluebook`'s own
|
|
91
105
|
# `DomainRefusal` list. Raised when an optimistic-concurrency CAS write
|
|
92
106
|
# (`AppendOnly#save`'s `expected_version:`) finds the stored version has
|
|
@@ -102,29 +116,29 @@ module Hecks
|
|
|
102
116
|
|
|
103
117
|
# A Lambda-routed domain's own refusal (rust/host, `Runtime::
|
|
104
118
|
# RemoteDispatcher`), carrying Rust's own refusal text verbatim —
|
|
105
|
-
#
|
|
119
|
+
# not yet mapped back to the specific matching class above
|
|
106
120
|
# (GivenNotMet vs. EnsuresNotMet vs. ...), a real, known,
|
|
107
121
|
# documented gap: the WASM projector's own event/refusal-wording
|
|
108
|
-
# parity work (ADR 0021) makes the
|
|
122
|
+
# parity work (ADR 0021) makes the text match Ruby's, but nothing
|
|
109
123
|
# yet parses that text back into a typed Ruby exception the way a
|
|
110
124
|
# local dispatch already raises one directly. Callers that only
|
|
111
125
|
# need "the domain said no" (not which specific rule) are
|
|
112
|
-
# unaffected; callers pattern-matching a
|
|
126
|
+
# unaffected; callers pattern-matching a specific refusal class
|
|
113
127
|
# against a Lambda-routed domain are the ones this gap would bite.
|
|
114
128
|
class RemoteRefusal < StandardError; end
|
|
115
129
|
|
|
116
|
-
# The domain saying
|
|
130
|
+
# The domain saying no — the errors a reaction may legitimately meet and
|
|
117
131
|
# record as an undelivered outcome. A policy whose target refuses is a fact
|
|
118
132
|
# about the domain ; the originating command still stands.
|
|
119
133
|
#
|
|
120
|
-
# Everything
|
|
134
|
+
# Everything else is a defect : a NoMethodError in an interpreter, a
|
|
121
135
|
# NameError from a missing constant, a TypeError from a bad assumption. A
|
|
122
|
-
# blanket `rescue StandardError`
|
|
123
|
-
# `delivered: false, reason: "..."` —
|
|
124
|
-
# indistinguishable from a rule doing its job,
|
|
125
|
-
# in the log.
|
|
136
|
+
# blanket `rescue StandardError` would fold both into one line —
|
|
137
|
+
# `delivered: false, reason: "..."` — making a crash in the runtime
|
|
138
|
+
# indistinguishable from a rule doing its job, reading as normal
|
|
139
|
+
# operation in the log.
|
|
126
140
|
#
|
|
127
|
-
# UnknownVerb
|
|
141
|
+
# UnknownVerb is one of these, and deliberately : a cross-domain policy
|
|
128
142
|
# (`across "Notifications"`) fires in deployments where that domain is not
|
|
129
143
|
# loaded, and recording the undelivered reaction rather than raising is the
|
|
130
144
|
# design — spec/policy_spec states it in so many words, "records a reaction
|
|
@@ -133,12 +147,12 @@ module Hecks
|
|
|
133
147
|
# its own rule is the domain saying no as plainly as a given is — but the
|
|
134
148
|
# class is declared over in value.rb and never made the list, so the policy
|
|
135
149
|
# and saga interpreters, which rescue exactly these, would let it propagate
|
|
136
|
-
# as though the
|
|
150
|
+
# as though the runtime had broken. A reaction whose target violates an
|
|
137
151
|
# invariant is declined, not crashed. Found by spec/domain_refusal_spec on
|
|
138
152
|
# its first run : every corpus refusal must be a class named here, and 23
|
|
139
153
|
# of banking's were InvariantViolation.
|
|
140
|
-
#
|
|
141
|
-
# `DomainRefusal` declares
|
|
154
|
+
# The names come from the language, the classes from this module.
|
|
155
|
+
# `DomainRefusal` declares which refusals are the domain's own —
|
|
142
156
|
# a rule the caller broke — as against a runtime fault. Resolving
|
|
143
157
|
# each name here means a refusal declared but never defined fails
|
|
144
158
|
# at load with a NameError, rather than being quietly absent from
|
data/lib/hecks/runtime/event.rb
CHANGED
|
@@ -2,7 +2,7 @@ require "time"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Runtime
|
|
5
|
-
# `correlation` is
|
|
5
|
+
# `correlation` is not on the wire — `to_h` below deliberately omits it,
|
|
6
6
|
# the same as `bin/run`'s own event projection does. It is runtime
|
|
7
7
|
# bookkeeping stamped by `Dispatcher#dispatch` when a saga leg's own
|
|
8
8
|
# dispatch causes this event (see `SagaInterpreter#deliver_saga_dispatch`
|
|
@@ -11,20 +11,25 @@ module Hecks
|
|
|
11
11
|
# one correlating on a different field. Absent for any event no saga
|
|
12
12
|
# dispatch caused, which is most of them.
|
|
13
13
|
Event = Struct.new(:name, :aggregate, :id, :payload, :occurred_at, :correlation, keyword_init: true) do
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# Freezes the event deep, so nothing about it can change after it happens.
|
|
15
|
+
#
|
|
16
|
+
# An emitted event is a record of something that happened, and a
|
|
17
|
+
# mutable audit trail is not one. The payload — the domain fact the
|
|
18
|
+
# event carries — is frozen through on emission: freezing the Hash
|
|
17
19
|
# alone would leave every value in it editable in place, which is
|
|
18
20
|
# the shape all four previous freezing bugs had.
|
|
19
21
|
#
|
|
20
|
-
#
|
|
21
|
-
# merged
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
22
|
+
# The whole event, not just its payload. Correlation is set at
|
|
23
|
+
# construction rather than merged in here by `Dispatcher#dispatch`
|
|
24
|
+
# after the event already exists, because it is part of the
|
|
25
|
+
# transaction and known from `dispatch`'s own argument before
|
|
26
|
+
# anything is emitted — that is what keeps an event immutable once
|
|
27
|
+
# it exists.
|
|
25
28
|
#
|
|
26
|
-
# The
|
|
29
|
+
# The log stays appendable: new events are still recorded. It is
|
|
27
30
|
# each event that stops changing once it exists.
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
28
33
|
def emit!
|
|
29
34
|
Freezer.deep(payload)
|
|
30
35
|
Freezer.deep(correlation)
|