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
|
@@ -11,7 +11,7 @@ module Hecks
|
|
|
11
11
|
# Whether a command may run at all: its declared givens, and the
|
|
12
12
|
# lifecycle transition it asks for.
|
|
13
13
|
module Admissibility
|
|
14
|
-
#
|
|
14
|
+
# Wraps `subject` so a guard can read a declared-but-storage-absent
|
|
15
15
|
# optional attribute as nil, not "cannot resolve." `Instance
|
|
16
16
|
# #hydrate_with_defaults` deliberately leaves one absent rather
|
|
17
17
|
# than nil-filled — "an attribute with no default stays absent,
|
|
@@ -25,10 +25,10 @@ module Hecks
|
|
|
25
25
|
# exact behavior, zero risk to a path this bug was never measured
|
|
26
26
|
# against.
|
|
27
27
|
#
|
|
28
|
-
# NIL
|
|
29
|
-
# covered an
|
|
28
|
+
# NIL is not the only outcome, though. The Item.Promote fix above
|
|
29
|
+
# covered an optional field — the honest case, where absence is
|
|
30
30
|
# exactly what optional means. It also, as a side effect, let a
|
|
31
|
-
#
|
|
31
|
+
# non-optional field predating a record read nil the same way,
|
|
32
32
|
# which is the `ne:`/array-`in:` bug class applied to rule
|
|
33
33
|
# evaluation: a predicate silently answers against a value nobody
|
|
34
34
|
# ever wrote (ADR 0025, "Added attributes and absence"). `[]`
|
|
@@ -37,17 +37,19 @@ module Hecks
|
|
|
37
37
|
# identifying the field, so a rule that reads it fails loud
|
|
38
38
|
# instead of quietly wrong.
|
|
39
39
|
class GuardState
|
|
40
|
+
# @param instance [Runtime::Instance] the record a `given`/`ensures`/
|
|
41
|
+
# `invariant` rule reads state from
|
|
40
42
|
def initialize(instance)
|
|
41
43
|
@instance = instance
|
|
42
44
|
@declared = instance.respond_to?(:aggregate) ? instance.aggregate.attributes.to_h { |a| [a.name, a] } : {}
|
|
43
|
-
# S12, ADR 0025 — a
|
|
45
|
+
# S12, ADR 0025 — a separate index, the same reason
|
|
44
46
|
# `Aggregate#projected_fields` is a separate IR collection
|
|
45
47
|
# rather than folded into `attributes` (see that field's
|
|
46
48
|
# own comment): a projected field's absence means
|
|
47
49
|
# something different from an ordinary attribute's, so it
|
|
48
50
|
# needs its own refusal below, not `AttributeAbsent`'s.
|
|
49
|
-
# `projects` is
|
|
50
|
-
# `instance.aggregate` answers the
|
|
51
|
+
# `projects` is aggregate-scoped only — an entity's own
|
|
52
|
+
# `instance.aggregate` answers the entity construct here
|
|
51
53
|
# (EntityInterpreter's own subject), which declares no
|
|
52
54
|
# `projected_fields` of its own, hence the extra guard
|
|
53
55
|
# `@declared` above does not need.
|
|
@@ -55,8 +57,23 @@ module Hecks
|
|
|
55
57
|
@projected = owner.respond_to?(:projected_fields) ? owner.projected_fields.to_h { |f| [f.name, f] } : {}
|
|
56
58
|
end
|
|
57
59
|
|
|
60
|
+
# Reports whether `name` is a declared attribute, a projected field, or
|
|
61
|
+
# a key `instance`'s own state actually holds.
|
|
62
|
+
#
|
|
63
|
+
# @param name [String, Symbol] the field name to check
|
|
64
|
+
# @return [Boolean] true if `name` is readable one of those three ways
|
|
58
65
|
def key?(name) = @declared.key?(name.to_sym) || @projected.key?(name.to_sym) || @instance.key?(name)
|
|
59
66
|
|
|
67
|
+
# Reads one field, the way a `given`/`ensures`/`invariant` rule expects
|
|
68
|
+
# absence to read.
|
|
69
|
+
#
|
|
70
|
+
# @param name [String, Symbol] the field name to read
|
|
71
|
+
# @return [Object, nil] the stored value; nil for a declared, optional
|
|
72
|
+
# attribute the record predates
|
|
73
|
+
# @raise [Runtime::AttributeAbsent] if `name` is a declared, non-optional
|
|
74
|
+
# attribute the record's own state does not hold
|
|
75
|
+
# @raise [Runtime::ProjectionAbsent] if `name` is a `projects` field the
|
|
76
|
+
# record's own state does not hold
|
|
60
77
|
def [](name)
|
|
61
78
|
return @instance[name] if @instance.key?(name)
|
|
62
79
|
|
|
@@ -67,17 +84,17 @@ module Hecks
|
|
|
67
84
|
return nil if attribute.nil? || attribute.optional?
|
|
68
85
|
|
|
69
86
|
raise AttributeAbsent,
|
|
70
|
-
RefusalWording.
|
|
71
|
-
|
|
87
|
+
RefusalWording.render_site("AttributeAbsent", "absent_read",
|
|
88
|
+
aggregate: @instance.aggregate.hecks_name, field: name)
|
|
72
89
|
end
|
|
73
90
|
|
|
74
91
|
private
|
|
75
92
|
|
|
76
93
|
def raise_projection_absent(projected)
|
|
77
94
|
raise ProjectionAbsent,
|
|
78
|
-
RefusalWording.
|
|
79
|
-
|
|
80
|
-
|
|
95
|
+
RefusalWording.render_site("ProjectionAbsent", "absent_read",
|
|
96
|
+
aggregate: @instance.aggregate.hecks_name, field: projected.name,
|
|
97
|
+
reference: projected.reference, remote_field: projected.remote_field)
|
|
81
98
|
end
|
|
82
99
|
end
|
|
83
100
|
private_constant :GuardState
|
|
@@ -89,54 +106,74 @@ module Hecks
|
|
|
89
106
|
# `Instance`); a `subject` with no `.aggregate` just hydrates
|
|
90
107
|
# nothing from state, same as GuardState degrades above.
|
|
91
108
|
#
|
|
92
|
-
#
|
|
109
|
+
# Merge order matters, and it is not "args always win": an
|
|
93
110
|
# unaliased command-level reference dereferences under a
|
|
94
111
|
# different name than the argument holds (`account_id` the arg,
|
|
95
112
|
# `account` the hydrated key — no collision, order is moot). An
|
|
96
|
-
#
|
|
97
|
-
# under the
|
|
113
|
+
# aliased one (`reference_to Customer, as: :customer`) hydrates
|
|
114
|
+
# under the same name the argument itself holds — `customer` is
|
|
98
115
|
# both the raw id an arg puts there and the key `customer.status`
|
|
99
116
|
# expects to dig into. If `args` merged last, the raw id (a
|
|
100
117
|
# String) would win and `.status` on a String is where a fuzzer
|
|
101
118
|
# found this — TypeError, not a refusal. Command-level
|
|
102
|
-
# dereferencing is the one thing that is
|
|
119
|
+
# dereferencing is the one thing that is supposed to override
|
|
103
120
|
# its own source argument for exactly this reason; args still
|
|
104
|
-
# wins over stored
|
|
105
|
-
# `parent:` is an entity command's
|
|
106
|
-
# (EntityInterpreter's `ctx.instance` — "the
|
|
121
|
+
# wins over stored owner state.
|
|
122
|
+
# `parent:` is an entity command's own parent aggregate record
|
|
123
|
+
# (EntityInterpreter's `ctx.instance` — "the parent aggregate
|
|
107
124
|
# record", its own doc comment) — the entity's containment, not a
|
|
108
125
|
# declared reference attribute, so it doesn't come from
|
|
109
126
|
# `dereference`'s attribute scan the way `owner`'s do. Hydrated
|
|
110
|
-
# the same shape regardless: the parent's own state,
|
|
127
|
+
# the same shape regardless: the parent's own state, merged so
|
|
111
128
|
# the dereferenced hash wins over the raw reference it replaces
|
|
112
|
-
# (ADR 0025
|
|
129
|
+
# (ADR 0025 drops the `_id` suffix that once kept the two
|
|
113
130
|
# apart by name, so `parent.state.merge(dereference(...))` is
|
|
114
|
-
#
|
|
131
|
+
# load-bearing, not redundant) — plus its own references
|
|
115
132
|
# dereferenced one level in, so `parent.customer.status` (a
|
|
116
|
-
# parent aggregate reaching
|
|
133
|
+
# parent aggregate reaching its own customer) resolves the same
|
|
117
134
|
# way `account.customer.status` does for a command-level reference.
|
|
118
135
|
# nil for an aggregate command — CommandInterpreter never passes it.
|
|
119
136
|
# `correction:` — the `{as_name => payload}` bindings
|
|
120
137
|
# `enforce_correction_target` (above) already located, merged in
|
|
121
|
-
#
|
|
138
|
+
# last so an `as:` name wins the same way `old:` always wins in
|
|
122
139
|
# `enforce_ensures`, below — it is a fresh local binding a
|
|
123
140
|
# `corrects` command introduces, not a real argument/state field
|
|
124
141
|
# a caller could collide with by accident.
|
|
142
|
+
#
|
|
143
|
+
# @param subject [Runtime::Instance] the pre-mutation record a `given` is
|
|
144
|
+
# checked against
|
|
145
|
+
# @param command [Class] the command class (`Bluebook::Command` subclass)
|
|
146
|
+
# whose declared `givens` are checked
|
|
147
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments,
|
|
148
|
+
# readable by a given
|
|
149
|
+
# @param domain [String, Symbol] the domain a reference-typed argument is
|
|
150
|
+
# dereferenced in
|
|
151
|
+
# @param declaring [Bluebook::Aggregate, Bluebook::Entity, nil] the construct
|
|
152
|
+
# `command` is declared on; also checks `enforce_lifecycle_guard` when given
|
|
153
|
+
# @param parent [Runtime::Instance, nil] an entity command's own parent
|
|
154
|
+
# aggregate record, readable as `parent.*`; nil for an aggregate command
|
|
155
|
+
# @param correction [Hash{Symbol => Object}] the `{as_name => payload}`
|
|
156
|
+
# bindings a `corrects` command's located old event offers
|
|
157
|
+
# @return [void]
|
|
158
|
+
# @raise [Runtime::GivenNotMet] if a declared `given` does not hold
|
|
159
|
+
# @raise [Runtime::LifecycleRefused] if `declaring` is given and the command's
|
|
160
|
+
# own `from:` guard refuses the record's current lifecycle state
|
|
161
|
+
# @raise [Bluebook::Expression::EvaluationError] if a given's own rule cannot
|
|
162
|
+
# be evaluated (an unresolvable field, a bad comparison)
|
|
125
163
|
def enforce_givens(subject, command, args, domain:, declaring: nil, parent: nil, correction: {})
|
|
126
164
|
state = GuardState.new(subject)
|
|
127
|
-
# A
|
|
128
|
-
# ADR 0025) — `subject`'s own
|
|
129
|
-
# dereferenced here at all
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
# field is a REGULAR stored attribute, already present in
|
|
165
|
+
# A rule may only read within its own aggregate boundary (S12,
|
|
166
|
+
# ADR 0025) — `subject`'s own stored references are no longer
|
|
167
|
+
# dereferenced here at all: a `projects :customer_status, from:
|
|
168
|
+
# :"customer.status"` field is just `subject`'s own
|
|
169
|
+
# field, a regular stored attribute, already present in
|
|
133
170
|
# `subject`/`state` with no hydration step needed. `dereference`
|
|
134
171
|
# is still called on `command`/`args`, below — that is a
|
|
135
|
-
#
|
|
136
|
-
# arguments"): a reference-typed
|
|
172
|
+
# different case the ADR explicitly keeps in bounds ("its command
|
|
173
|
+
# arguments"): a reference-typed argument this dispatch was just
|
|
137
174
|
# handed (`Dispute`'s own `disputed_by`, say) has nothing stored
|
|
138
175
|
# to project yet, so resolving it here, once, synchronously with
|
|
139
|
-
#
|
|
176
|
+
# this command's own admission, is not the live-query-against-
|
|
140
177
|
# another-aggregate's-stored-state pattern the boundary rule
|
|
141
178
|
# forbids.
|
|
142
179
|
attrs = args.merge(dereference(domain, command, args))
|
|
@@ -154,42 +191,90 @@ module Hecks
|
|
|
154
191
|
enforce_lifecycle_guard(declaring, command, subject) if declaring
|
|
155
192
|
end
|
|
156
193
|
|
|
157
|
-
# `corrects` — CommandBuilder#corrects_impl's own comment.
|
|
194
|
+
# `corrects` — CommandBuilder#corrects_impl's own comment. Not
|
|
158
195
|
# expressible as an ordinary `given`: "has this exact record
|
|
159
196
|
# already emitted this exact event" is not a predicate over the
|
|
160
|
-
# record's
|
|
197
|
+
# record's own fields, it is a fact about the event log, so it is
|
|
161
198
|
# raised structurally here, the same way NotFound/AlreadyExists
|
|
162
199
|
# are, rather than through the expression evaluator. The build-
|
|
163
|
-
# time half — does
|
|
200
|
+
# time half — does anything in this aggregate ever emit the named
|
|
164
201
|
# event at all — is `AggregateBuilder#seal_correction_targets`;
|
|
165
|
-
# this is the dispatch-time half — has
|
|
202
|
+
# this is the dispatch-time half — has this record actually done
|
|
166
203
|
# so yet.
|
|
167
204
|
#
|
|
168
|
-
#
|
|
205
|
+
# Also locates the matched event now, not just its existence, and
|
|
169
206
|
# returns a `{as_name => payload}` bindings hash — one entry per
|
|
170
207
|
# `:corrects` mutation that named an `as:` — so `given`/`ensures`
|
|
171
|
-
# on a corrects-bearing command can reference the located
|
|
208
|
+
# on a corrects-bearing command can reference the located old
|
|
172
209
|
# event by that name, the same shape `enforce_ensures`'s own
|
|
173
210
|
# `old:` binding already has (CommandBuilder#corrects_impl's own
|
|
174
211
|
# comment: `as:` was stored, from the start, specifically to be
|
|
175
212
|
# wired into the evaluator once a real runtime consumer existed —
|
|
176
|
-
# this is that consumer). `.reverse.find` — the
|
|
213
|
+
# this is that consumer). `.reverse.find` — the most recent
|
|
177
214
|
# matching event, if this record has somehow emitted the same
|
|
178
215
|
# correction target more than once; the prior existence-only
|
|
179
216
|
# check never had to make this choice, so it's a genuinely new
|
|
180
217
|
# one, made deliberately: `as:` reads as "the instance being
|
|
181
218
|
# corrected," which is naturally the latest fact on record, not
|
|
182
219
|
# an arbitrary one.
|
|
220
|
+
# Reads the durable event history a `corrects` mutation is judged against.
|
|
221
|
+
#
|
|
183
222
|
# C9.2 (docs/semantics/bluebook-semantics.md) — a correction target
|
|
184
|
-
# is judged against the record's
|
|
223
|
+
# is judged against the record's durable history: the events the
|
|
185
224
|
# aggregate's own store recorded (`AppendOnly#events`), which
|
|
186
225
|
# survive a restart the way the Rust kernel's persisted
|
|
187
226
|
# `emitted_<event>` flag does. The in-process log is the fallback
|
|
188
227
|
# only for an adapter that records no readable history.
|
|
228
|
+
#
|
|
229
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to
|
|
230
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose repository is read
|
|
231
|
+
# @return [Array<Runtime::Event>] the aggregate's own durably recorded events
|
|
232
|
+
# if its repository keeps a readable log, otherwise the registry's in-process
|
|
233
|
+
# event log
|
|
189
234
|
def correction_history(domain, aggregate)
|
|
190
235
|
@registry.repository(domain, aggregate).events || @registry.event_log
|
|
191
236
|
end
|
|
192
237
|
|
|
238
|
+
# Locates each `:corrects` mutation's own already-emitted target event, and
|
|
239
|
+
# binds every `as:`-named one for `given`/`ensures` to reference.
|
|
240
|
+
#
|
|
241
|
+
# `corrects` — CommandBuilder#corrects_impl's own comment. Not
|
|
242
|
+
# expressible as an ordinary `given`: "has this exact record
|
|
243
|
+
# already emitted this exact event" is not a predicate over the
|
|
244
|
+
# record's own fields, it is a fact about the event log, so it is
|
|
245
|
+
# raised structurally here, the same way NotFound/AlreadyExists
|
|
246
|
+
# are, rather than through the expression evaluator. The build-
|
|
247
|
+
# time half — does anything in this aggregate ever emit the named
|
|
248
|
+
# event at all — is `AggregateBuilder#seal_correction_targets`;
|
|
249
|
+
# this is the dispatch-time half — has this record actually done
|
|
250
|
+
# so yet.
|
|
251
|
+
#
|
|
252
|
+
# Also locates the matched event now, not just its existence, and
|
|
253
|
+
# returns a `{as_name => payload}` bindings hash — one entry per
|
|
254
|
+
# `:corrects` mutation that named an `as:` — so `given`/`ensures`
|
|
255
|
+
# on a corrects-bearing command can reference the located old
|
|
256
|
+
# event by that name, the same shape `enforce_ensures`'s own
|
|
257
|
+
# `old:` binding already has (CommandBuilder#corrects_impl's own
|
|
258
|
+
# comment: `as:` was stored, from the start, specifically to be
|
|
259
|
+
# wired into the evaluator once a real runtime consumer existed —
|
|
260
|
+
# this is that consumer). `.reverse.find` — the most recent
|
|
261
|
+
# matching event, if this record has somehow emitted the same
|
|
262
|
+
# correction target more than once; the prior existence-only
|
|
263
|
+
# check never had to make this choice, so it's a genuinely new
|
|
264
|
+
# one, made deliberately: `as:` reads as "the instance being
|
|
265
|
+
# corrected," which is naturally the latest fact on record, not
|
|
266
|
+
# an arbitrary one.
|
|
267
|
+
#
|
|
268
|
+
# @param instance [Runtime::Instance] the record being corrected
|
|
269
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `instance` belongs to
|
|
270
|
+
# @param command [Class] the command class (`Bluebook::Command` subclass)
|
|
271
|
+
# whose `:corrects` mutations are located
|
|
272
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to
|
|
273
|
+
# @return [Hash{Symbol => Object}] the located event's payload, keyed by
|
|
274
|
+
# each `:corrects` mutation's own `as:` name; empty for a mutation with
|
|
275
|
+
# no `as:`
|
|
276
|
+
# @raise [Runtime::NothingToCorrect] if a `:corrects` mutation names an
|
|
277
|
+
# event `instance` has never emitted
|
|
193
278
|
def enforce_correction_target(instance, aggregate, command, domain:)
|
|
194
279
|
bindings = {}
|
|
195
280
|
command.mutations.each do |mutation|
|
|
@@ -213,15 +298,25 @@ module Hecks
|
|
|
213
298
|
bindings
|
|
214
299
|
end
|
|
215
300
|
|
|
216
|
-
#
|
|
217
|
-
# "Debit", from: "open"` checked here, folded into the
|
|
301
|
+
# Lifecycle state as a command guard (S10, ADR 0025) — `command
|
|
302
|
+
# "Debit", from: "open"` checked here, folded into the same
|
|
218
303
|
# dispatch step `given` already runs at (both are preconditions,
|
|
219
304
|
# evaluated before any mutation) rather than earning its own
|
|
220
|
-
# DISPATCH_ORDER entry. A
|
|
305
|
+
# DISPATCH_ORDER entry. A guard, never a transition: it names no
|
|
221
306
|
# target state and `step_advance_lifecycle` never sees it — see
|
|
222
307
|
# `admissible_transition`, right below, for the transition this
|
|
223
|
-
# is deliberately
|
|
308
|
+
# is deliberately not reusing (its own `StateTransition#target`
|
|
224
309
|
# is required, and a guard-only command has none to give it).
|
|
310
|
+
#
|
|
311
|
+
# @param declaring [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
312
|
+
# whose lifecycle field is checked
|
|
313
|
+
# @param command [Class] the command class (`Bluebook::Command` subclass)
|
|
314
|
+
# whose `from:` guard is checked
|
|
315
|
+
# @param subject [Runtime::Instance] the pre-mutation record to read the
|
|
316
|
+
# current lifecycle state off
|
|
317
|
+
# @return [void]
|
|
318
|
+
# @raise [Runtime::LifecycleRefused] if `command` declares `from:` and the
|
|
319
|
+
# record's current lifecycle state is not one of them
|
|
225
320
|
def enforce_lifecycle_guard(declaring, command, subject)
|
|
226
321
|
return unless command.from
|
|
227
322
|
|
|
@@ -229,49 +324,68 @@ module Hecks
|
|
|
229
324
|
current = Value.scalar(subject[lifecycle.field]).to_s
|
|
230
325
|
return if Array(command.from).include?(current)
|
|
231
326
|
|
|
232
|
-
#
|
|
233
|
-
#
|
|
327
|
+
# Routed through RefusalWording's own "transition_blocked"
|
|
328
|
+
# template — the same one #admissible_transition, right below,
|
|
234
329
|
# already raises LifecycleRefused through for the same
|
|
235
|
-
# refusal class
|
|
330
|
+
# refusal class, rather than hand-rolling its own wording
|
|
236
331
|
# inline ("...only runs from..." vs. the template's "...moves
|
|
237
|
-
# it only from...")
|
|
332
|
+
# it only from..."), two shapes for one refusal kind, so
|
|
238
333
|
# anything string-matching a LifecycleRefused message (a
|
|
239
|
-
# property, a spec, a caller)
|
|
240
|
-
# than one.
|
|
334
|
+
# property, a spec, a caller) would have to know both existed
|
|
335
|
+
# rather than one.
|
|
241
336
|
raise LifecycleRefused,
|
|
242
|
-
RefusalWording.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
337
|
+
RefusalWording.render_site("LifecycleRefused", "transition_blocked",
|
|
338
|
+
command: command.hecks_name, field: lifecycle.field,
|
|
339
|
+
current: Rendering.describe(current),
|
|
340
|
+
allowed: Array(command.from))
|
|
246
341
|
end
|
|
247
342
|
|
|
248
|
-
# The far side of the contract: evaluated against the
|
|
343
|
+
# The far side of the contract: evaluated against the settled record
|
|
249
344
|
# — after mutations and the lifecycle move, before anything persists
|
|
250
345
|
# — with `old` carrying the state as the givens saw it. Injected into
|
|
251
346
|
# the attrs at evaluation time only; the payload gate never sees it.
|
|
252
347
|
#
|
|
253
|
-
# `old` — and every dispatch
|
|
348
|
+
# `old` — and every dispatch argument — wins over a same-named state
|
|
254
349
|
# field in expression scope (Resolver#fetch checks attrs first). An
|
|
255
350
|
# ensures naming a field the command also takes as an argument (or,
|
|
256
351
|
# on an entity, a field that doubles as the addressing argument
|
|
257
|
-
# element_of reads) will read the
|
|
352
|
+
# element_of reads) will read the argument, not the settled value.
|
|
258
353
|
# Not new to ensures — `given` lives under the same rule — but an
|
|
259
354
|
# ensures is more likely to collide, since it typically re-reads a
|
|
260
355
|
# field the command just took in to mutate it.
|
|
356
|
+
#
|
|
357
|
+
# @param subject [Runtime::Instance] the settled, post-mutation record an
|
|
358
|
+
# `ensures` is checked against
|
|
359
|
+
# @param command [Class] the command class (`Bluebook::Command` subclass)
|
|
360
|
+
# whose declared `ensures` are checked
|
|
361
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments,
|
|
362
|
+
# readable by an ensures unless `subject` shares the same field name
|
|
363
|
+
# @param old [Hash{Symbol => Object}, nil] the pre-mutation state, readable
|
|
364
|
+
# as `old.*`; nil when `command` declares no ensures (dup skipped upstream)
|
|
365
|
+
# @param domain [String, Symbol] the domain a reference-typed argument is
|
|
366
|
+
# dereferenced in
|
|
367
|
+
# @param parent [Runtime::Instance, nil] an entity command's own parent
|
|
368
|
+
# aggregate record, readable as `parent.*`; nil for an aggregate command
|
|
369
|
+
# @param correction [Hash{Symbol => Object}] the `{as_name => payload}`
|
|
370
|
+
# bindings a `corrects` command's located old event offers
|
|
371
|
+
# @return [void]
|
|
372
|
+
# @raise [Runtime::EnsuresNotMet] if a declared `ensures` does not hold
|
|
373
|
+
# @raise [Bluebook::Expression::EvaluationError] if an ensures's own rule
|
|
374
|
+
# cannot be evaluated (an unresolvable field, a bad comparison)
|
|
261
375
|
def enforce_ensures(subject, command, args, old:, domain:, parent: nil, correction: {})
|
|
262
376
|
state = GuardState.new(subject)
|
|
263
377
|
# S12, ADR 0025 — same boundary reasoning as enforce_givens
|
|
264
378
|
# above: `subject`'s own stored references are no longer
|
|
265
379
|
# dereferenced here; a `projects`-maintained field is already
|
|
266
380
|
# part of `state`. `command`/`args` still dereferences — a
|
|
267
|
-
# fresh reference-typed
|
|
381
|
+
# fresh reference-typed argument stays in bounds.
|
|
268
382
|
# `old` still wins over everything, unchanged. `correction`
|
|
269
383
|
# (an `as:`-bound corrected event, if this command declares
|
|
270
384
|
# one) wins right alongside it — a settled-record ensures can
|
|
271
385
|
# reference the correction target exactly as freely as a
|
|
272
386
|
# pre-mutation given already can.
|
|
273
387
|
# C2.3 (docs/semantics/bluebook-semantics.md) — an ensures reads
|
|
274
|
-
# the
|
|
388
|
+
# the settled state first: an argument that shares a field's
|
|
275
389
|
# name does not shadow the candidate here (it does in a given,
|
|
276
390
|
# C2.2), so `sets :note` + `ensures { note == ... }` judges what
|
|
277
391
|
# landed, and `old.<field>` remains the pre-state.
|
|
@@ -286,25 +400,37 @@ module Hecks
|
|
|
286
400
|
end
|
|
287
401
|
end
|
|
288
402
|
|
|
289
|
-
#
|
|
403
|
+
# The aggregate boundary, checked after every command, before
|
|
290
404
|
# save (S10, ADR 0025 — "Rules") — the same point `enforce_
|
|
291
405
|
# ensures` already checks at, and for the same reason: an
|
|
292
|
-
# invariant is a claim about the
|
|
406
|
+
# invariant is a claim about the settled record, not the
|
|
293
407
|
# command that produced it, so it reads no `args`/`old` at all,
|
|
294
408
|
# only the record's own state. `subject` here is
|
|
295
|
-
# always the
|
|
409
|
+
# always the aggregate's own instance — `CommandInterpreter`
|
|
296
410
|
# passes its own `ctx.instance`, and `EntityInterpreter` passes
|
|
297
|
-
# the
|
|
298
|
-
# entity mutation changes data inside the
|
|
411
|
+
# the parent record (`ctx.instance`, not the element), since an
|
|
412
|
+
# entity mutation changes data inside the same aggregate
|
|
299
413
|
# boundary the invariant guards; there is no separate "entity
|
|
300
414
|
# invariant" to check the piece's own view against.
|
|
301
415
|
#
|
|
302
|
-
#
|
|
416
|
+
# No `dereference` (S12, ADR 0025) — an invariant may only read
|
|
303
417
|
# `subject`'s own boundary, same rule `enforce_givens`/
|
|
304
418
|
# `enforce_ensures` now hold to. No invariant in the corpus has
|
|
305
|
-
# ever read across a `reference_to` (verified
|
|
306
|
-
#
|
|
307
|
-
#
|
|
419
|
+
# ever read across a `reference_to` (verified by grep), so this
|
|
420
|
+
# is not a migration, just closing the same capability off here
|
|
421
|
+
# that was already unused.
|
|
422
|
+
#
|
|
423
|
+
# @param subject [Runtime::Instance] the settled aggregate record an
|
|
424
|
+
# invariant is checked against
|
|
425
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose declared
|
|
426
|
+
# `invariants` are checked
|
|
427
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to, passed
|
|
428
|
+
# through to `check_entity_invariants`
|
|
429
|
+
# @return [void]
|
|
430
|
+
# @raise [Runtime::InvariantViolation] if a declared invariant does not hold,
|
|
431
|
+
# on `aggregate` itself or any of its entities
|
|
432
|
+
# @raise [Bluebook::Expression::EvaluationError] if an invariant's own rule
|
|
433
|
+
# cannot be evaluated (an unresolvable field, a bad comparison)
|
|
308
434
|
def enforce_invariants(subject, aggregate, domain:)
|
|
309
435
|
state = GuardState.new(subject)
|
|
310
436
|
attrs = {}
|
|
@@ -317,20 +443,20 @@ module Hecks
|
|
|
317
443
|
check_entity_invariants(aggregate, subject, domain: domain)
|
|
318
444
|
end
|
|
319
445
|
|
|
320
|
-
# A
|
|
446
|
+
# A piece's own shape rule, checked against every instance the
|
|
321
447
|
# aggregate holds — not a separate boundary from the aggregate's
|
|
322
448
|
# own invariants just above (same two checkpoints: after every
|
|
323
|
-
# mutation, before save), just a
|
|
449
|
+
# mutation, before save), just a wider one: the aggregate's own
|
|
324
450
|
# consistency includes each of its pieces individually looking
|
|
325
451
|
# right, the same way `ValueObject#invariants` already checks
|
|
326
|
-
# each of
|
|
452
|
+
# each of its own instances one construct up. Recurses into
|
|
327
453
|
# nested pieces (S17, ADR 0026 — Dispatch inside Handler) the
|
|
328
454
|
# same way `check_entity_invariants`'s own caller recurses
|
|
329
455
|
# nowhere else needs to, since a piece's `entities` are already
|
|
330
456
|
# exactly as reachable as an aggregate's.
|
|
331
457
|
#
|
|
332
|
-
# `list_attr` reuses the
|
|
333
|
-
# element_of` already makes to locate a
|
|
458
|
+
# `list_attr` reuses the exact lookup `EntityInterpreter#
|
|
459
|
+
# element_of` already makes to locate a single addressed
|
|
334
460
|
# element by identity — this reads every element instead, but
|
|
335
461
|
# the "which field on the owner holds this piece's own
|
|
336
462
|
# instances" question is the identical one. A piece declaring
|
|
@@ -338,6 +464,17 @@ module Hecks
|
|
|
338
464
|
# matching list attribute) is a static-analysis gap for a
|
|
339
465
|
# future gate, not a runtime concern here — `next` past it
|
|
340
466
|
# rather than raising mid-enforcement for an unrelated command.
|
|
467
|
+
#
|
|
468
|
+
# @param owner_construct [Bluebook::Aggregate, Bluebook::Entity] the
|
|
469
|
+
# construct whose nested entities are checked
|
|
470
|
+
# @param owner_instance [Runtime::Instance] the settled record holding
|
|
471
|
+
# `owner_construct`'s own entity lists
|
|
472
|
+
# @param domain [String, Symbol] the domain `owner_construct` belongs to
|
|
473
|
+
# @return [void]
|
|
474
|
+
# @raise [Runtime::InvariantViolation] if a declared entity invariant does
|
|
475
|
+
# not hold, on any element or its own nested entities
|
|
476
|
+
# @raise [Bluebook::Expression::EvaluationError] if an invariant's own rule
|
|
477
|
+
# cannot be evaluated (an unresolvable field, a bad comparison)
|
|
341
478
|
def check_entity_invariants(owner_construct, owner_instance, domain:)
|
|
342
479
|
owner_construct.entities.each do |entity|
|
|
343
480
|
next if entity.invariants.empty?
|
|
@@ -348,7 +485,7 @@ module Hecks
|
|
|
348
485
|
Array(owner_instance[list_attr.name]).each do |element|
|
|
349
486
|
wrapped = Instance.new(aggregate: entity, id: nil, state: element)
|
|
350
487
|
element_state = GuardState.new(wrapped)
|
|
351
|
-
#
|
|
488
|
+
# No `dereference` (S12, ADR 0025) — same boundary rule as
|
|
352
489
|
# enforce_invariants above; `parent` (the owner's own
|
|
353
490
|
# state, projected fields included) stays readable.
|
|
354
491
|
attrs = { parent: owner_instance.state }
|
|
@@ -364,6 +501,20 @@ module Hecks
|
|
|
364
501
|
end
|
|
365
502
|
end
|
|
366
503
|
|
|
504
|
+
# Finds the lifecycle transition `command` admits from `subject`'s current
|
|
505
|
+
# state, if `declaring` declares a lifecycle and `command` moves it.
|
|
506
|
+
#
|
|
507
|
+
# @param declaring [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
508
|
+
# whose lifecycle is checked
|
|
509
|
+
# @param command [Class] the command class (`Bluebook::Command` subclass)
|
|
510
|
+
# to find a declared transition for
|
|
511
|
+
# @param subject [Runtime::Instance] the pre-mutation record to read the
|
|
512
|
+
# current lifecycle state off
|
|
513
|
+
# @return [Bluebook::StateTransition, nil] the admitted transition, or nil
|
|
514
|
+
# when `declaring` has no lifecycle or `command` declares no transition
|
|
515
|
+
# @raise [Runtime::LifecycleRefused] if `command` declares one or more
|
|
516
|
+
# transitions, each constrained by its own `from:`, and none admits the
|
|
517
|
+
# record's current lifecycle state
|
|
367
518
|
def admissible_transition(declaring, command, subject)
|
|
368
519
|
lifecycle = declaring.lifecycle
|
|
369
520
|
return nil unless lifecycle
|
|
@@ -379,7 +530,7 @@ module Hecks
|
|
|
379
530
|
# default `Object#to_s` instead of unwrapping the inner
|
|
380
531
|
# scalar first -- `current` came back as a raw object-pointer
|
|
381
532
|
# string (`"#<Hecks::Runtime::Value:0x...>"`) that could
|
|
382
|
-
# never match any declared `from` state, so
|
|
533
|
+
# never match any declared `from` state, so every transition
|
|
383
534
|
# on a VO-typed lifecycle field refused unconditionally, and
|
|
384
535
|
# when it refused the message leaked the pointer too.
|
|
385
536
|
# Confirmed live via `Plan::Task.Complete` (status defaults to
|
|
@@ -399,10 +550,10 @@ module Hecks
|
|
|
399
550
|
|
|
400
551
|
allowed = candidates.flat_map { |t| Array(t.from) }.uniq
|
|
401
552
|
raise LifecycleRefused,
|
|
402
|
-
RefusalWording.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
553
|
+
RefusalWording.render_site("LifecycleRefused", "transition_blocked",
|
|
554
|
+
command: command.hecks_name, field: lifecycle.field,
|
|
555
|
+
current: Rendering.describe(current),
|
|
556
|
+
allowed: allowed)
|
|
406
557
|
end
|
|
407
558
|
end
|
|
408
559
|
end
|