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
|
@@ -7,7 +7,7 @@ require_relative "../../ports/query/in_memory"
|
|
|
7
7
|
module Hecks
|
|
8
8
|
module Runtime
|
|
9
9
|
class CommandRules
|
|
10
|
-
# A reference must point at something that
|
|
10
|
+
# A reference must point at something that exists.
|
|
11
11
|
#
|
|
12
12
|
# `reference_to Customer` is the one guarantee an aggregate reference is
|
|
13
13
|
# for, and it was declared 14 times across banking and enforced nowhere :
|
|
@@ -16,13 +16,22 @@ module Hecks
|
|
|
16
16
|
# no corpus step ever passed a dangling reference.
|
|
17
17
|
module References
|
|
18
18
|
# Resolved here rather than in coercion because coercion is pure — it
|
|
19
|
-
# holds no repository. A reference
|
|
19
|
+
# holds no repository. A reference into another domain is left alone : a
|
|
20
20
|
# cross-domain target may legitimately not be loaded, which is the same
|
|
21
21
|
# reading `across` policies already get.
|
|
22
22
|
#
|
|
23
23
|
# Shared by CommandInterpreter and EntityInterpreter — an entity command
|
|
24
24
|
# can declare a reference-typed attribute the same way an aggregate
|
|
25
25
|
# command can, even though nothing in the real corpus does yet.
|
|
26
|
+
#
|
|
27
|
+
# @param domain [String, Symbol] the domain the referenced aggregate is
|
|
28
|
+
# resolved in
|
|
29
|
+
# @param command [Class] the command or port-operation class (`Bluebook::Command`
|
|
30
|
+
# or `PortOperation` subclass) whose reference-typed attributes are checked
|
|
31
|
+
# @param args [Hash{Symbol => Object}] the offered, already-coerced argument values
|
|
32
|
+
# @return [void]
|
|
33
|
+
# @raise [Runtime::NotFound] if a reference-typed argument names an identity that
|
|
34
|
+
# does not exist in its target aggregate's repository
|
|
26
35
|
def resolve_references(domain, command, args)
|
|
27
36
|
command.attributes.each do |attribute|
|
|
28
37
|
next unless attribute.reference?
|
|
@@ -41,6 +50,20 @@ module Hecks
|
|
|
41
50
|
# Structural references are checked again against the settled state.
|
|
42
51
|
# This is what makes a `has_many` declared on an aggregate honest even
|
|
43
52
|
# when a command supplies its list through an ordinary typed argument.
|
|
53
|
+
#
|
|
54
|
+
# @param domain [String, Symbol] the domain the referenced aggregate is
|
|
55
|
+
# resolved in
|
|
56
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
57
|
+
# whose reference-typed attributes are checked against `state`
|
|
58
|
+
# @param state [Hash{Symbol => Object}] the settled, post-mutation state to
|
|
59
|
+
# check every reference and relationship cardinality against
|
|
60
|
+
# @return [void]
|
|
61
|
+
# @raise [Runtime::TypeMismatch] if a required `has_one`/`belongs_to`
|
|
62
|
+
# relationship holds nil
|
|
63
|
+
# @raise [Runtime::NotFound] if a reference-typed field names an identity
|
|
64
|
+
# that does not exist in its target aggregate's repository
|
|
65
|
+
# @raise [Runtime::Unauthorized] if a referenced record belongs to a
|
|
66
|
+
# different tenant than `construct`'s own record
|
|
44
67
|
def resolve_state_references(domain, construct, state)
|
|
45
68
|
own_tenant_field = tenant_field_for(construct)
|
|
46
69
|
|
|
@@ -73,6 +96,14 @@ module Hecks
|
|
|
73
96
|
# checking only command arguments would let a required relationship be
|
|
74
97
|
# persisted as nil. `has_many` admits zero members, so its empty list is
|
|
75
98
|
# already a valid cardinality and needs no presence refusal.
|
|
99
|
+
#
|
|
100
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
101
|
+
# `attribute` is declared on, named in a refusal
|
|
102
|
+
# @param attribute [Bluebook::Attribute] the reference-typed attribute to check
|
|
103
|
+
# @param held [Object, nil] the field's settled value
|
|
104
|
+
# @return [void]
|
|
105
|
+
# @raise [Runtime::TypeMismatch] if `attribute` declares a required (non-optional,
|
|
106
|
+
# non-list) `has_one`/`belongs_to` relationship and `held` is nil
|
|
76
107
|
def validate_relationship_cardinality(construct, attribute, held)
|
|
77
108
|
return if attribute.relationship.nil? || attribute.list?
|
|
78
109
|
return unless held.nil? && !attribute.optional?
|
|
@@ -83,16 +114,32 @@ module Hecks
|
|
|
83
114
|
"#{attribute.type.target_name} identity, got nil"
|
|
84
115
|
end
|
|
85
116
|
|
|
86
|
-
# The reference
|
|
87
|
-
# bluebook's declared heads are the index
|
|
88
|
-
# name out of "Reference<Customer>" and
|
|
89
|
-
# `registry.bluebook(domain).aggregates` for it
|
|
90
|
-
# target through Ruby's constant tree, a class thrown away for its
|
|
91
|
-
# the moment it was found.
|
|
117
|
+
# The reference resolves itself — through the chapter's own IR, so the
|
|
118
|
+
# bluebook's declared heads are the index, rather than regexing the
|
|
119
|
+
# target's name out of "Reference<Customer>" and searching
|
|
120
|
+
# `registry.bluebook(domain).aggregates` for it, or reaching the
|
|
121
|
+
# target through Ruby's constant tree, a class thrown away for its
|
|
122
|
+
# `.ir` the moment it was found.
|
|
123
|
+
#
|
|
124
|
+
# @param attribute [Bluebook::Attribute] the reference-typed attribute to resolve
|
|
125
|
+
# @return [Bluebook::Aggregate, nil] the target aggregate the reference names,
|
|
126
|
+
# or nil if it cannot be resolved
|
|
127
|
+
# @raise [Bluebook::DSL::Malformed] if the reference does not know which
|
|
128
|
+
# aggregate declares it
|
|
92
129
|
def referenced_aggregate(attribute)
|
|
93
130
|
attribute.type.resolve
|
|
94
131
|
end
|
|
95
132
|
|
|
133
|
+
# Refuses a reference-typed value whose target identity does not exist.
|
|
134
|
+
#
|
|
135
|
+
# @param domain [String, Symbol] the domain `target` is resolved in
|
|
136
|
+
# @param target [Bluebook::Aggregate] the referenced aggregate
|
|
137
|
+
# @param held [Object, Array<Object>] the offered value: a single reference,
|
|
138
|
+
# or, when `list:` is true, an Array of them
|
|
139
|
+
# @param list [Boolean] whether `held` is list-shaped
|
|
140
|
+
# @return [void]
|
|
141
|
+
# @raise [Runtime::NotFound] if any referenced identity does not exist in
|
|
142
|
+
# `target`'s repository
|
|
96
143
|
def validate_reference_values(domain, target, held, list:)
|
|
97
144
|
values = list ? Array(held) : [held]
|
|
98
145
|
values.each do |value|
|
|
@@ -101,37 +148,53 @@ module Hecks
|
|
|
101
148
|
next if @registry.repository(domain, target).find(key)
|
|
102
149
|
|
|
103
150
|
raise NotFound,
|
|
104
|
-
RefusalWording.
|
|
105
|
-
|
|
106
|
-
|
|
151
|
+
RefusalWording.render_site("NotFound", "reference_target_missing",
|
|
152
|
+
target: target.name, heads: target.identity_heads.join(", "),
|
|
153
|
+
key: key)
|
|
107
154
|
end
|
|
108
155
|
end
|
|
109
156
|
|
|
110
|
-
#
|
|
111
|
-
# tenant_scope.rb) — the
|
|
157
|
+
# Angle-8's own write-side half of `TenantScope.apply` (runtime/
|
|
158
|
+
# tenant_scope.rb) — the query-side mechanism this mirrors. That
|
|
112
159
|
# module turns a declared `authorize policy, tenant: :field` into a
|
|
113
|
-
# synthetic where-clause checked against the
|
|
160
|
+
# synthetic where-clause checked against the caller's own supplied
|
|
114
161
|
# tenant argument; there is no caller-identity/session system this
|
|
115
|
-
# runtime has to check a
|
|
162
|
+
# runtime has to check a write's caller against (TenantScope's own
|
|
116
163
|
# header names that as a separate, still-open gap), so this checks
|
|
117
|
-
# the one thing that
|
|
164
|
+
# the one thing that is available without one: whether the record
|
|
118
165
|
# being written and the record it references agree about which
|
|
119
166
|
# tenant they belong to. `lib/hecks/fuzzing/properties/guards.rb`'s
|
|
120
167
|
# `commands_respect_tenant_scope` states the identical claim,
|
|
121
168
|
# read off `history[:instances]` after the fact — this is what
|
|
122
|
-
# makes that claim hold
|
|
169
|
+
# makes that claim hold by construction (a refused write is never
|
|
123
170
|
# stored) rather than merely checked for regression.
|
|
124
171
|
#
|
|
125
172
|
# Hooked into `resolve_state_references` rather than a new
|
|
126
173
|
# DISPATCH_ORDER step deliberately: that method already walks
|
|
127
|
-
# every `reference_to`-typed attribute against the
|
|
174
|
+
# every `reference_to`-typed attribute against the settled,
|
|
128
175
|
# post-mutation state (the same moment `commands_respect_tenant_
|
|
129
176
|
# scope` itself inspects), already resolves the referenced record
|
|
130
|
-
# through the repository right above, and already runs from
|
|
177
|
+
# through the repository right above, and already runs from both
|
|
131
178
|
# `CommandInterpreter#step_save` and `EntityInterpreter#step_save`
|
|
132
179
|
# — one change, both interpreters covered, no new vocabulary step
|
|
133
180
|
# to keep in sync with `Vocabulary::AggregateDispatchOrder`/
|
|
134
181
|
# `EntityDispatchOrder`.
|
|
182
|
+
#
|
|
183
|
+
# @param domain [String, Symbol] the domain `target` is resolved in
|
|
184
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
185
|
+
# `attribute` is declared on, named in a refusal
|
|
186
|
+
# @param attribute [Bluebook::Attribute] the reference-typed attribute being
|
|
187
|
+
# checked
|
|
188
|
+
# @param target [Bluebook::Aggregate] the referenced aggregate
|
|
189
|
+
# @param held [Object, Array<Object>] the offered value: a single reference,
|
|
190
|
+
# or, when `attribute.list?` is true, an Array of them
|
|
191
|
+
# @param state [Hash{Symbol => Object}] `construct`'s own settled state, read
|
|
192
|
+
# for its own tenant field's value
|
|
193
|
+
# @param own_tenant_field [Symbol, nil] `construct`'s own declared tenant field;
|
|
194
|
+
# a no-op if nil or absent from `state`
|
|
195
|
+
# @return [void]
|
|
196
|
+
# @raise [Runtime::Unauthorized] if a referenced record's own tenant field
|
|
197
|
+
# disagrees with `construct`'s
|
|
135
198
|
def enforce_tenant_boundary(domain, construct, attribute, target, held, state, own_tenant_field)
|
|
136
199
|
return unless own_tenant_field && state.key?(own_tenant_field)
|
|
137
200
|
|
|
@@ -152,50 +215,63 @@ module Hecks
|
|
|
152
215
|
next if target_tenant == own_tenant
|
|
153
216
|
|
|
154
217
|
raise Unauthorized,
|
|
155
|
-
RefusalWording.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
218
|
+
RefusalWording.render_site("Unauthorized", "cross_tenant_reference",
|
|
219
|
+
aggregate: construct.hecks_name, field: own_tenant_field,
|
|
220
|
+
tenant: Rendering.describe(state[own_tenant_field]),
|
|
221
|
+
attribute: attribute.name, target: target.name,
|
|
222
|
+
target_field: target_tenant_field,
|
|
223
|
+
other: Rendering.describe(record.state[target_tenant_field]))
|
|
161
224
|
end
|
|
162
225
|
end
|
|
163
226
|
|
|
164
|
-
#
|
|
227
|
+
# The field an aggregate's own query names as tenant-scoping — the
|
|
165
228
|
# exact same lookup `Fuzzing::Properties::Guards#tenant_field_for`
|
|
166
229
|
# already established for the property that found this gap, reused
|
|
167
230
|
# here rather than reinvented: an aggregate's own declared tenant
|
|
168
|
-
# field is whichever field
|
|
231
|
+
# field is whichever field one of its own queries names in
|
|
169
232
|
# `authorize policy, tenant: :field`. `nil` for a construct that
|
|
170
233
|
# declares no such query — not every aggregate is tenant-scoped,
|
|
171
234
|
# and an entity never declares a query of its own at all today
|
|
172
235
|
# (`Entity.queries` is always empty in the real corpus), so this
|
|
173
236
|
# answers `nil` for every entity without needing to special-case
|
|
174
237
|
# one.
|
|
238
|
+
#
|
|
239
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
240
|
+
# whose declared queries are searched
|
|
241
|
+
# @return [Symbol, nil] the tenant-scoping field name, or nil if `construct`
|
|
242
|
+
# declares no query with `authorize policy, tenant: :field`
|
|
175
243
|
def tenant_field_for(construct)
|
|
176
244
|
authorization = construct.queries.filter_map(&:authorization).find(&:tenant)
|
|
177
245
|
authorization&.tenant&.to_sym
|
|
178
246
|
end
|
|
179
247
|
|
|
180
|
-
#
|
|
248
|
+
# Renders a reference-typed value as the plain string key its target
|
|
249
|
+
# record is looked up by.
|
|
250
|
+
#
|
|
251
|
+
# `value` is the referenced record's own id, exactly as `Identity.of`
|
|
181
252
|
# would build it for that record — a bare scalar for a single-field
|
|
182
253
|
# identity (the overwhelming common case; Banking's own plain
|
|
183
254
|
# `reference_to Customer` holds one already, so `Value.
|
|
184
255
|
# materialize_unwrapped` is a no-op passthrough here), or a
|
|
185
256
|
# `Naming.identity`-joined string for a compound one
|
|
186
257
|
# (`belongs_to Translation, as: :translation_ref` — Translation's
|
|
187
|
-
# own `identified_by :domain, :from, :to`,
|
|
188
|
-
#
|
|
189
|
-
# Value hit Ruby's default `Object#to_s` (a raw,
|
|
190
|
-
# memory address) instead of joining the record's
|
|
191
|
-
# live via bin/fuzz on the self-hosted "translation"
|
|
192
|
-
# (replay_is_deterministic), the
|
|
258
|
+
# own `identified_by :domain, :from, :to`, three fields). Without
|
|
259
|
+
# `materialize_unwrapped`, plain `value.to_s` on that compound case's
|
|
260
|
+
# own coerced Value would hit Ruby's default `Object#to_s` (a raw,
|
|
261
|
+
# run-to-run-random memory address) instead of joining the record's
|
|
262
|
+
# real id — found live via bin/fuzz on the self-hosted "translation"
|
|
263
|
+
# domain (replay_is_deterministic), the same class of gap `Identity.from`
|
|
193
264
|
# already had for a compound `identified_by`'s own bare (undotted)
|
|
194
265
|
# attribute paths. `materialize_unwrapped` recurses a multi-
|
|
195
266
|
# attribute value object to a plain Hash keyed by attribute name,
|
|
196
267
|
# in declaration order — `Naming.identity` on `.values` reproduces
|
|
197
268
|
# the identical join `Identity.of` itself would produce for the
|
|
198
|
-
#
|
|
269
|
+
# same fields.
|
|
270
|
+
#
|
|
271
|
+
# @param value [Object] the offered reference value: a `Runtime::Value`, a
|
|
272
|
+
# scalar, or anything `Value.materialize_unwrapped` can open
|
|
273
|
+
# @return [String] the key to look the referenced record up by; empty when
|
|
274
|
+
# `value` names no id
|
|
199
275
|
def reference_key(value)
|
|
200
276
|
unwrapped = Value.materialize_unwrapped(value)
|
|
201
277
|
return Naming.identity(unwrapped.values).to_s if unwrapped.is_a?(Hash)
|
|
@@ -203,35 +279,33 @@ module Hecks
|
|
|
203
279
|
unwrapped.to_s
|
|
204
280
|
end
|
|
205
281
|
|
|
206
|
-
#
|
|
207
|
-
# `given`/`ensures` — `disputed_by.status`, say, `CardPayment
|
|
282
|
+
# Hydrates a command argument's own related record, reachable by name
|
|
283
|
+
# from `given`/`ensures` — `disputed_by.status`, say, `CardPayment
|
|
208
284
|
# .Dispute`'s own fresh `Reference<Customer>` argument — without
|
|
209
285
|
# teaching the pure expression evaluator anything about
|
|
210
|
-
# repositories. The lookup happens
|
|
286
|
+
# repositories. The lookup happens here, once, before evaluation;
|
|
211
287
|
# `Resolver#lookup` just digs into a plain Hash exactly as it
|
|
212
288
|
# always has.
|
|
213
289
|
#
|
|
214
|
-
# `owner`
|
|
290
|
+
# `owner` narrowed to `command` only (S12, ADR 0025 — "rules
|
|
215
291
|
# confined to their own aggregate boundary"): dereferencing the
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
# `
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
# against an ALREADY-PERSISTED reference. `enforce_givens`/
|
|
292
|
+
# declaring aggregate/entity's own stored `reference_to` is not part
|
|
293
|
+
# of this method's job — a cross-aggregate fact a rule needs has to
|
|
294
|
+
# be a `projects`-maintained local field (`AggregateBuilder#
|
|
295
|
+
# projects_impl`'s own comment), already present in `subject`'s own
|
|
296
|
+
# state, no hydration needed, rather than a live query against
|
|
297
|
+
# another aggregate's own repository on every `given`/`ensures`/
|
|
298
|
+
# `invariant` run. A reference-typed command argument stays in
|
|
299
|
+
# bounds, though — the ADR's own boundary list names "its command
|
|
300
|
+
# arguments" as readable, and nothing is stored yet for a fresh
|
|
301
|
+
# argument to project from; resolving it once here, synchronous
|
|
302
|
+
# with this command's own admission, is a different shape from a
|
|
303
|
+
# live query against an already-persisted reference. `enforce_givens`/
|
|
229
304
|
# `enforce_ensures` are this method's only two remaining callers,
|
|
230
305
|
# both passing `command`/`args`, never a `subject`'s own
|
|
231
|
-
# aggregate — verified
|
|
232
|
-
# assumed.
|
|
306
|
+
# aggregate — verified by grep, not assumed.
|
|
233
307
|
#
|
|
234
|
-
#
|
|
308
|
+
# Recurses into what it finds, so a chain deeper than one hop
|
|
235
309
|
# still resolves in one pass. Depth-bounded rather than cycle-
|
|
236
310
|
# detected — nothing in this corpus dots more than two hops on a
|
|
237
311
|
# fresh argument, and a bound is simpler than tracking visited
|
|
@@ -239,6 +313,22 @@ module Hecks
|
|
|
239
313
|
DEREFERENCE_DEPTH = 4
|
|
240
314
|
private_constant :DEREFERENCE_DEPTH
|
|
241
315
|
|
|
316
|
+
# Hydrates `owner`'s own reference-typed attributes into the records they name.
|
|
317
|
+
#
|
|
318
|
+
# @param domain [String, Symbol] the domain a referenced aggregate is
|
|
319
|
+
# resolved in
|
|
320
|
+
# @param owner [Class, Bluebook::Aggregate, Bluebook::Entity, nil] the
|
|
321
|
+
# construct whose reference-typed attributes are dereferenced; nil (or a
|
|
322
|
+
# depth of zero) short-circuits to an empty Hash
|
|
323
|
+
# @param source [Hash{Symbol => Object}] the offered payload to read each
|
|
324
|
+
# reference-typed attribute's raw id from
|
|
325
|
+
# @param depth [Integer] how many more hops to recurse into a resolved
|
|
326
|
+
# reference's own reference-typed fields
|
|
327
|
+
# @return [Hash{Symbol => Object}] one entry per resolved reference-typed
|
|
328
|
+
# attribute, keyed by the attribute name with a trailing `_id` stripped,
|
|
329
|
+
# valued at the referenced record's state merged with its own dereferenced
|
|
330
|
+
# references; an attribute with no id, an unresolvable target, or no
|
|
331
|
+
# matching record contributes nothing
|
|
242
332
|
def dereference(domain, owner, source, depth: DEREFERENCE_DEPTH)
|
|
243
333
|
return {} if depth <= 0 || owner.nil?
|
|
244
334
|
|
|
@@ -20,6 +20,9 @@ module Hecks
|
|
|
20
20
|
|
|
21
21
|
attr_reader :registry
|
|
22
22
|
|
|
23
|
+
# @param registry [Runtime::Registry, nil] the booted registry the rules read bluebooks,
|
|
24
|
+
# repositories and the event log from; nil serves only the rules that read no
|
|
25
|
+
# registry, such as `sign_of`
|
|
23
26
|
def initialize(registry)
|
|
24
27
|
@registry = registry
|
|
25
28
|
end
|
|
@@ -17,12 +17,28 @@ module Hecks
|
|
|
17
17
|
:unresolved_dependencies,
|
|
18
18
|
keyword_init: true
|
|
19
19
|
) do
|
|
20
|
+
# Reports whether every owner field this command could touch is a known,
|
|
21
|
+
# deterministic write.
|
|
22
|
+
#
|
|
23
|
+
# @return [Boolean] `complete_state`
|
|
20
24
|
def complete_state? = complete_state
|
|
25
|
+
|
|
26
|
+
# Reports whether the command needs no prior state at all (implies `complete_state?`).
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] `state_independent`
|
|
21
29
|
def state_independent? = state_independent
|
|
22
30
|
|
|
31
|
+
# Chooses the dispatch strategy this plan's proof and the adapter's own
|
|
32
|
+
# capabilities together allow.
|
|
33
|
+
#
|
|
23
34
|
# Capability negotiation is correctness-first: an optimization is
|
|
24
35
|
# selected only when both the semantic proof and adapter capability
|
|
25
36
|
# are present. This is planning data only; no runtime path calls it yet.
|
|
37
|
+
#
|
|
38
|
+
# @param capabilities [Array<String, Symbol>] the repository's declared capabilities
|
|
39
|
+
# @return [Symbol] `DependencyPlanning::ATOMIC_PUT` when the plan is complete,
|
|
40
|
+
# state-independent, and the adapter declares that capability;
|
|
41
|
+
# `DependencyPlanning::TRANSACTIONAL_FALLBACK` otherwise
|
|
26
42
|
def strategy_for(capabilities: [])
|
|
27
43
|
return TRANSACTIONAL_FALLBACK unless complete_state? && state_independent?
|
|
28
44
|
return TRANSACTIONAL_FALLBACK unless capabilities.map(&:to_sym).include?(ATOMIC_PUT)
|
|
@@ -38,13 +54,27 @@ module Hecks
|
|
|
38
54
|
module ExpressionReads
|
|
39
55
|
module_function
|
|
40
56
|
|
|
57
|
+
# Finds every dotted path a canonical expression reads, without evaluating it.
|
|
58
|
+
#
|
|
41
59
|
# Read the same parsed canonical-expression nodes the evaluator uses.
|
|
42
60
|
# A generic Struct walk keeps this additive when the expression grammar
|
|
43
61
|
# gains a composed node; only Lookup nodes carry domain dependencies.
|
|
62
|
+
#
|
|
63
|
+
# @param canonical [String] the canonical expression text, such as a rule's `canonical`
|
|
64
|
+
# @return [Array<String>] the dotted paths the expression reads
|
|
44
65
|
def paths(canonical)
|
|
45
66
|
collect(Bluebook::Expression::Evaluator.parse(canonical), Set.new)
|
|
46
67
|
end
|
|
47
68
|
|
|
69
|
+
# Walks one parsed expression node, collecting the dotted paths it reads.
|
|
70
|
+
#
|
|
71
|
+
# @param node [Object] a node from `Bluebook::Expression::Evaluator.parse` — a
|
|
72
|
+
# `Bluebook::Expression::Resolver::Lookup`, a `Bluebook::Expression::Resolver::
|
|
73
|
+
# BlockPredicate`, a `Struct` composed of further nodes, an `Array` of them, or any
|
|
74
|
+
# other value (a literal), which contributes no paths
|
|
75
|
+
# @param bound_names [Set<String>] names locally bound by an enclosing block
|
|
76
|
+
# predicate's own parameter, excluded from the result rather than reported as reads
|
|
77
|
+
# @return [Array<String>] the dotted paths read under `node`, excluding `bound_names`
|
|
48
78
|
def collect(node, bound_names)
|
|
49
79
|
case node
|
|
50
80
|
when Bluebook::Expression::Resolver::Lookup
|
|
@@ -74,12 +104,12 @@ module Hecks
|
|
|
74
104
|
STATEFUL_MUTATIONS = %i[append increment decrement multiply clamp remove].freeze
|
|
75
105
|
|
|
76
106
|
# `root_aggregate:` — Wave 8's own audit surfaced a real bug here,
|
|
77
|
-
# not merely a missing feature: for an
|
|
107
|
+
# not merely a missing feature: for an entity-owned command,
|
|
78
108
|
# `EntityInterpreter` calls this with `aggregate:` set to the
|
|
79
|
-
#
|
|
109
|
+
# entity itself (`element_interpreter.rb`'s own `Analyzer.call
|
|
80
110
|
# (aggregate: entity, command:)`), so `owner_fields` was always
|
|
81
111
|
# the entity's own attribute set. A `given`/`ensures` reading
|
|
82
|
-
# `parent.X` legitimately means the
|
|
112
|
+
# `parent.X` legitimately means the root aggregate's own field —
|
|
83
113
|
# a genuinely different owner — but `classify_path`'s `:parent`
|
|
84
114
|
# branch checked that read against `owner_fields` (the entity's),
|
|
85
115
|
# which can never contain a root-level field, so every entity
|
|
@@ -87,8 +117,20 @@ module Hecks
|
|
|
87
117
|
# unresolved regardless of correctness. Defaults to `aggregate`
|
|
88
118
|
# (a no-op) for the plain-aggregate case — `CommandInterpreter`'s
|
|
89
119
|
# own call site never needed to change.
|
|
120
|
+
#
|
|
121
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the command
|
|
122
|
+
# is dispatched against; an entity for an entity-owned command
|
|
123
|
+
# @param command [Bluebook::Command] the command to analyze
|
|
124
|
+
# @param root_aggregate [Bluebook::Aggregate] the owning aggregate a `parent.*` read
|
|
125
|
+
# resolves against; defaults to `aggregate` for a plain-aggregate command
|
|
126
|
+
# @return [Hecks::Runtime::DependencyPlanning::Plan] the derived, frozen plan
|
|
90
127
|
def self.call(aggregate:, command:, root_aggregate: aggregate) = new(aggregate, command, root_aggregate).call
|
|
91
128
|
|
|
129
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the command
|
|
130
|
+
# is dispatched against
|
|
131
|
+
# @param command [Bluebook::Command] the command to analyze
|
|
132
|
+
# @param root_aggregate [Bluebook::Aggregate] the owning aggregate `parent.*` reads
|
|
133
|
+
# resolve against; defaults to `aggregate`
|
|
92
134
|
def initialize(aggregate, command, root_aggregate = aggregate)
|
|
93
135
|
@aggregate = aggregate
|
|
94
136
|
@command = command
|
|
@@ -96,10 +138,10 @@ module Hecks
|
|
|
96
138
|
@owner_fields << aggregate.lifecycle.field.to_sym if aggregate.lifecycle
|
|
97
139
|
@root_owner_fields = root_aggregate.attributes.to_set(&:name)
|
|
98
140
|
@root_owner_fields << root_aggregate.lifecycle.field.to_sym if root_aggregate.lifecycle
|
|
99
|
-
# `projects`
|
|
141
|
+
# `projects` fields (S12, ADR 0025) are owner state too — a
|
|
100
142
|
# `given`/`ensures` reading one (e.g. `customer_status ==
|
|
101
143
|
# "active"`) is reading this record's own stored field, same
|
|
102
|
-
# as any attribute, even though nothing here
|
|
144
|
+
# as any attribute, even though nothing here writes it via a
|
|
103
145
|
# declared mutation (`CommandInterpreter#seed_projected_fields`
|
|
104
146
|
# populates it outside this analysis entirely). Left out of
|
|
105
147
|
# `known_writes` deliberately: `add_preservation_reads` then
|
|
@@ -107,7 +149,7 @@ module Hecks
|
|
|
107
149
|
# a partial mutation, which is exactly right — a projected
|
|
108
150
|
# field's freshness comes from the interpreter reseeding it on
|
|
109
151
|
# save, not from anything a caller-supplied write set carries.
|
|
110
|
-
# Applies to
|
|
152
|
+
# Applies to both `owner_fields` and `root_owner_fields` — an
|
|
111
153
|
# entity's own `parent.*` read can name the root aggregate's
|
|
112
154
|
# projected field just as easily as one of its real attributes
|
|
113
155
|
# (`Banking::Withdrawal.Dispute`'s own `parent.account_customer_
|
|
@@ -126,6 +168,9 @@ module Hecks
|
|
|
126
168
|
@unresolved = Set.new
|
|
127
169
|
end
|
|
128
170
|
|
|
171
|
+
# Runs the analysis and derives the command's dependency plan.
|
|
172
|
+
#
|
|
173
|
+
# @return [Hecks::Runtime::DependencyPlanning::Plan] the derived, frozen plan
|
|
129
174
|
def call
|
|
130
175
|
analyze_initial_state
|
|
131
176
|
analyze_mutations
|
|
@@ -237,7 +282,7 @@ module Hecks
|
|
|
237
282
|
end
|
|
238
283
|
end
|
|
239
284
|
|
|
240
|
-
#
|
|
285
|
+
# Known, harmless gap: `corrects ..., as: :name`'s bound name
|
|
241
286
|
# (admissibility.rb's `enforce_correction_target`/`enforce_givens`/
|
|
242
287
|
# `enforce_ensures`) isn't special-cased here the way `:old`/
|
|
243
288
|
# `:parent` are — a given/ensures referencing it falls through to
|
|
@@ -247,7 +292,7 @@ module Hecks
|
|
|
247
292
|
# `hydrate_existing` path instead of the `ATOMIC_PUT` fast path.
|
|
248
293
|
# Not a correctness bug — `as:`'s runtime binding (a plain `attrs`
|
|
249
294
|
# merge, exactly like `old:`'s) resolves and evaluates correctly
|
|
250
|
-
# regardless of what this
|
|
295
|
+
# regardless of what this static analysis concludes — just a real,
|
|
251
296
|
# deliberately-left optimization gap: closing it would mean
|
|
252
297
|
# threading "which names this command declares as correction
|
|
253
298
|
# bindings" into the Analyzer, which doesn't have that per-command
|
|
@@ -258,9 +303,9 @@ module Hecks
|
|
|
258
303
|
name = head.to_sym
|
|
259
304
|
|
|
260
305
|
if name == :parent
|
|
261
|
-
# `root_owner_fields` —
|
|
262
|
-
# command `owner_fields` is the
|
|
263
|
-
# `parent.X` always means the
|
|
306
|
+
# `root_owner_fields` — not `owner_fields`. For an entity-owned
|
|
307
|
+
# command `owner_fields` is the entity's own attribute set;
|
|
308
|
+
# `parent.X` always means the root aggregate's own field, a
|
|
264
309
|
# genuinely different owner (`root_aggregate:`'s own header,
|
|
265
310
|
# above, has the full bug this fixes). Identical for a plain
|
|
266
311
|
# aggregate command, where root_aggregate defaults to aggregate
|