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
|
@@ -1,134 +1,161 @@
|
|
|
1
|
+
require_relative "../vocabulary"
|
|
2
|
+
|
|
1
3
|
module Hecks
|
|
2
4
|
module Runtime
|
|
3
5
|
# Every DomainRefusal wording that is not already data — `given`/
|
|
4
6
|
# `ensures`/a declared `invariant` already carry their own description,
|
|
5
7
|
# read at dispatch time off the command or value object that declared
|
|
6
|
-
# them. These are different in kind:
|
|
8
|
+
# them. These are different in kind: language-level refusals, the same
|
|
7
9
|
# wording for every domain, not authored per-bluebook.
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
# (language/bluebook/vocabulary.bluebook)
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
11
|
+
# Read off the generated table, not typed a second time. The rows are
|
|
12
|
+
# Vocabulary::RefusalTemplate (language/bluebook/vocabulary.bluebook),
|
|
13
|
+
# projected into lib/hecks/vocabulary.rb by bin/project_vocabulary and
|
|
14
|
+
# into rust/src/kernel/vocab/refusal_template.rs by
|
|
15
|
+
# bin/project_rust_vocabulary; both regenerations are diffed in CI, so
|
|
16
|
+
# there is no hand copy left here to drift. Declared order is kept.
|
|
17
|
+
#
|
|
18
|
+
# **The arguments are data too**. Vocabulary::RefusalSiteArgument names the
|
|
19
|
+
# values each site takes and how each is written (a list's separator,
|
|
20
|
+
# its sort, its empty reading, its quoting). Call sites use
|
|
21
|
+
# `render_site` and hand over raw values; the Rust kernel's typed
|
|
22
|
+
# `render_args` applies the same rows, and the projection that writes it
|
|
23
|
+
# pins every site's output against `format_argument`/`substitute` here.
|
|
15
24
|
module RefusalWording
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# rubocop:disable-next Layout/HashAlignment
|
|
23
|
-
TEMPLATES = {
|
|
24
|
-
%w[NotFound creating_no_identity] =>
|
|
25
|
-
"{command} creates a {aggregate} — pass {identity}:",
|
|
26
|
-
%w[AlreadyExists creating_duplicate] =>
|
|
27
|
-
"{command} creates a {aggregate} that already exists — {identity} {offered}",
|
|
28
|
-
%w[AlreadyExists entity_duplicate] =>
|
|
29
|
-
"a {entity} already exists on {aggregate} — {identity} {offered}",
|
|
30
|
-
%w[NotFound acting_no_identity] =>
|
|
31
|
-
"{command} acts on an existing {aggregate} — pass {identity}:",
|
|
32
|
-
%w[NotFound record_missing] =>
|
|
33
|
-
"no {aggregate} with {identity} {offered}",
|
|
34
|
-
%w[NotFound entity_parent_no_identity] =>
|
|
35
|
-
"{command} acts on a {aggregate}'s {entity} — pass {identity}:",
|
|
36
|
-
%w[UnknownVerb entity_unknown] =>
|
|
37
|
-
"{aggregate} has no entity {entity}",
|
|
38
|
-
%w[NotFound entity_element_no_identity] =>
|
|
39
|
-
"{command} acts on one {entity} — pass {identity}:",
|
|
40
|
-
%w[NotFound entity_element_missing] =>
|
|
41
|
-
"no {entity} with {identity} {wants} on {aggregate} {parent_id}",
|
|
42
|
-
%w[NotFound reference_target_missing] =>
|
|
43
|
-
"no {target} with {heads} {key}",
|
|
44
|
-
%w[NotFound read_model_reference_missing] =>
|
|
45
|
-
"no {aggregate} with reference {offered}",
|
|
46
|
-
%w[TypeMismatch read_model_object_reference] =>
|
|
47
|
-
"{query} refused — a reference is an id, and {field} arrived as an object",
|
|
48
|
-
%w[UnknownVerb no_query] =>
|
|
49
|
-
"{aggregate} has no query {query}",
|
|
50
|
-
%w[UnknownVerb entity_query_missing] =>
|
|
51
|
-
"{entity} has no query {query}",
|
|
52
|
-
%w[UnknownVerb entity_holds_no_list] =>
|
|
53
|
-
"{aggregate} holds no list of {entity}",
|
|
54
|
-
%w[UnknownVerb entity_no_command] =>
|
|
55
|
-
"{entity} has no command {command}",
|
|
56
|
-
%w[UnknownVerb aggregate_no_command] =>
|
|
57
|
-
"{aggregate} has no command {command}",
|
|
58
|
-
%w[UnknownVerb port_no_operation] =>
|
|
59
|
-
"{port} has no operation {operation}",
|
|
60
|
-
%w[UnknownVerb no_domain] =>
|
|
61
|
-
"no domain {domain} loaded (verb {verb})",
|
|
62
|
-
%w[UnknownVerb no_read_model] =>
|
|
63
|
-
"{domain} has no read model {query}",
|
|
64
|
-
%w[UnknownVerb not_fully_qualified] =>
|
|
65
|
-
"{verb} is not a fully-qualified verb (Domain::Aggregate.Command)",
|
|
66
|
-
%w[UnknownVerb no_aggregate] =>
|
|
67
|
-
"{domain} has no aggregate {aggregate}",
|
|
68
|
-
%w[LifecycleRefused transition_blocked] =>
|
|
69
|
-
"{command} refused — {field} is {current}, and {command} moves it only from {allowed}",
|
|
70
|
-
%w[TypeMismatch value_object_shape] =>
|
|
71
|
-
"{name} is a {type} — pass its fields as an object, not {offered}",
|
|
72
|
-
%w[TypeMismatch reference_wrong_shape] =>
|
|
73
|
-
"{command} refused — a reference is an id, and {attribute} arrived as {offered}{known_by}",
|
|
74
|
-
%w[TypeMismatch multi_field_scalar] =>
|
|
75
|
-
"{type} has multiple fields and cannot stand in for a scalar",
|
|
76
|
-
%w[TypeMismatch composite_identity] =>
|
|
77
|
-
"{type} is a composite identity — an identity must have exactly one field",
|
|
78
|
-
%w[TypeMismatch numeric_field] =>
|
|
79
|
-
"{type}.{field} expects {expected}, got {offered}",
|
|
80
|
-
%w[TypeMismatch non_finite_field] =>
|
|
81
|
-
"{type}.{field} must be a finite number, got {offered}",
|
|
82
|
-
%w[TypeMismatch integer_range] =>
|
|
83
|
-
"{type}.{field} must fit in a 64-bit integer, got {offered}",
|
|
84
|
-
%w[TypeMismatch pattern_mismatch] =>
|
|
85
|
-
"{type}.{field} must match {pattern}, got {offered}",
|
|
86
|
-
%w[TypeMismatch arithmetic_amount] =>
|
|
87
|
-
"{op} of {target} needs an Integer, got {offered}",
|
|
88
|
-
%w[TypeMismatch arithmetic_current] =>
|
|
89
|
-
"{op} of {target} needs an Integer {target}, got {offered}",
|
|
90
|
-
%w[TypeMismatch arithmetic_shared_field] =>
|
|
91
|
-
"{op} of {target} needs a value object with one shared Integer field",
|
|
92
|
-
%w[UnknownArgument unknown_args] =>
|
|
93
|
-
"{command} does not declare {unknown} — it takes {declared}",
|
|
94
|
-
%w[AbsentArgument absent_args] =>
|
|
95
|
-
"{command} was not given {absent} — it takes {declared}",
|
|
96
|
-
%w[InvariantViolation closed_set_member] =>
|
|
97
|
-
"{type} admits {admitted} — got {offered}",
|
|
98
|
-
%w[InvariantViolation value_object_invariant] =>
|
|
99
|
-
"{name} invariant violated — {description} (given {offered})",
|
|
100
|
-
%w[InvariantViolation admits_declared_set] =>
|
|
101
|
-
"{name} admits {admits} — {admitted} — got {offered}",
|
|
102
|
-
%w[InvariantViolation undeclared_set] =>
|
|
103
|
-
"{name} admits {admits}, which this chapter does not declare — a closed set is named Aggregate::SetName, and it must " \
|
|
104
|
-
"be one the bluebook actually holds",
|
|
105
|
-
%w[Unauthorized tenant_required] =>
|
|
106
|
-
"{query} declares authorize with tenant: {field} — pass {field}: to name which {field} this ask is scoped to",
|
|
107
|
-
%w[Unauthorized role_mismatch] =>
|
|
108
|
-
"{command} refused — role: {role}, and the caller stated {caller_role}",
|
|
109
|
-
%w[Unauthorized cross_tenant_reference] =>
|
|
110
|
-
"{aggregate} {field} is {tenant}, but {attribute} names a {target} whose own {target_field} " \
|
|
111
|
-
"is {other} — a cross-tenant reference",
|
|
112
|
-
%w[AttributeAbsent absent_read] =>
|
|
113
|
-
"{aggregate} {field} is absent on this record — declared, not optional, and added since it was written. Backfill it " \
|
|
114
|
-
"in a translation (backfill :{field}, default: ...), or declare it optional: true",
|
|
115
|
-
%w[ProjectionAbsent absent_read] =>
|
|
116
|
-
"{aggregate} {field} is not yet projected on this record — declared via projects :{field}, but no rebuild sweep has " \
|
|
117
|
-
"populated it. Run the sweep, or read {reference}.{remote_field} directly if this rule cannot wait"
|
|
118
|
-
}.freeze
|
|
25
|
+
TEMPLATES = Hecks::Vocabulary.rows("RefusalTemplate")
|
|
26
|
+
.to_h { |row| [[row["refusal"], row["site"]].freeze, row["template"]] }
|
|
27
|
+
.freeze
|
|
28
|
+
|
|
29
|
+
SHAPES = %w[scalar list].freeze
|
|
30
|
+
QUOTINGS = %w[none inspect].freeze
|
|
119
31
|
|
|
120
32
|
module_function
|
|
121
33
|
|
|
34
|
+
# Renders `refusal`/`site`'s template, substituting `values` verbatim.
|
|
35
|
+
#
|
|
122
36
|
# Plain text substitution, never expression syntax — a template is
|
|
123
|
-
# read, not evaluated.
|
|
124
|
-
#
|
|
125
|
-
#
|
|
37
|
+
# read, not evaluated. Values arrive already formatted; prefer
|
|
38
|
+
# `render_site`, which formats them off the declared rows.
|
|
39
|
+
#
|
|
40
|
+
# @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
|
|
41
|
+
# @param site [String] the template site within `refusal`, such as `"unknown_args"`
|
|
42
|
+
# @param values [Hash{Symbol => #to_s}] each `{name}` placeholder's already-formatted
|
|
43
|
+
# replacement text
|
|
44
|
+
# @return [String] the rendered refusal message
|
|
45
|
+
# @raise [KeyError] if no `RefusalTemplate` row declares `refusal`/`site`
|
|
126
46
|
def render(refusal, site, **values)
|
|
127
|
-
template
|
|
47
|
+
substitute(template(refusal, site), values)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The one door call sites use: exactly the arguments the site
|
|
51
|
+
# declares (a missing or undeclared one raises ArgumentError before
|
|
52
|
+
# any wording exists), each formatted by its RefusalSiteArgument row,
|
|
53
|
+
# substituted in declared order.
|
|
54
|
+
#
|
|
55
|
+
# RefusalWording.render_site("UnknownArgument", "unknown_args",
|
|
56
|
+
# command: "Close", unknown: [:parcel], declared: [])
|
|
57
|
+
# # => "Close does not declare parcel — it takes none"
|
|
58
|
+
#
|
|
59
|
+
# @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
|
|
60
|
+
# @param site [String] the template site within `refusal`, such as `"unknown_args"`
|
|
61
|
+
# @param arguments [Hash{Symbol => Object}] raw values, one per `RefusalSiteArgument`
|
|
62
|
+
# row declared for `refusal`/`site`; formatted per row before substitution
|
|
63
|
+
# @return [String] the rendered refusal message
|
|
64
|
+
# @raise [ArgumentError] if `arguments` is missing a declared argument or offers one
|
|
65
|
+
# `refusal`/`site` does not declare
|
|
66
|
+
# @raise [KeyError] if no `RefusalTemplate`/`RefusalSiteArgument` rows declare
|
|
67
|
+
# `refusal`/`site`
|
|
68
|
+
def render_site(refusal, site, **arguments)
|
|
69
|
+
specs = argument_rows(refusal, site)
|
|
70
|
+
declared = specs.map { |spec| spec["argument"].to_sym }
|
|
71
|
+
missing = declared - arguments.keys
|
|
72
|
+
extra = arguments.keys - declared
|
|
73
|
+
if missing.any? || extra.any?
|
|
74
|
+
raise ArgumentError, "#{refusal}/#{site} takes #{declared.join(', ')} — " \
|
|
75
|
+
"missing: #{missing.join(', ')}; undeclared: #{extra.join(', ')}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
render_with(template(refusal, site), specs, arguments)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# `render_site` without the registry lookups: a template, its
|
|
82
|
+
# argument rows, and raw values. The Rust projection calls this with
|
|
83
|
+
# the chapter's own rows to compute the expected wording it pins.
|
|
84
|
+
#
|
|
85
|
+
# @param template [String] the raw template, `{name}` placeholders unsubstituted
|
|
86
|
+
# @param specs [Array<Hash>] the `RefusalSiteArgument` rows to format `arguments`
|
|
87
|
+
# against, one per declared argument name
|
|
88
|
+
# @param arguments [Hash{Symbol => Object}] raw values, one per entry in `specs`
|
|
89
|
+
# @return [String] the rendered refusal message
|
|
90
|
+
def render_with(template, specs, arguments)
|
|
91
|
+
values = specs.to_h do |spec|
|
|
92
|
+
name = spec["argument"].to_sym
|
|
93
|
+
[name, format_argument(spec, arguments.fetch(name))]
|
|
94
|
+
end
|
|
95
|
+
substitute(template, values)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# One argument, written the way its row says. A list is sorted first
|
|
99
|
+
# (before quoting), then each item quoted, then joined; an empty list
|
|
100
|
+
# reads `when_empty`. A scalar is quoted or taken as its own text.
|
|
101
|
+
#
|
|
102
|
+
# @param spec [Hash] the argument's `RefusalSiteArgument` row (`"shape"`, `"quoting"`,
|
|
103
|
+
# and, for a list, `"sorted"`, `"separator"`, `"when_empty"`)
|
|
104
|
+
# @param value [Object, Array] the raw value to format; an Array (or anything
|
|
105
|
+
# `Array()`-coercible) for a `"list"`-shaped spec, a scalar otherwise
|
|
106
|
+
# @return [String] the formatted text
|
|
107
|
+
def format_argument(spec, value)
|
|
108
|
+
inspect = spec.fetch("quoting") == "inspect"
|
|
109
|
+
return inspect ? value.inspect : value.to_s unless spec.fetch("shape") == "list"
|
|
110
|
+
|
|
111
|
+
items = Array(value)
|
|
112
|
+
items = items.sort if spec.fetch("sorted") == "true"
|
|
113
|
+
items = items.map(&:inspect) if inspect
|
|
114
|
+
items.empty? ? spec.fetch("when_empty") : items.join(spec.fetch("separator"))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Replaces each `{name}` placeholder in `template` with its value's text.
|
|
118
|
+
#
|
|
119
|
+
# @param template [String] the raw template, `{name}` placeholders unsubstituted
|
|
120
|
+
# @param values [Hash{Symbol => #to_s}] each placeholder name mapped to its
|
|
121
|
+
# replacement text
|
|
122
|
+
# @return [String] `template` with every `{name}` placeholder substituted
|
|
123
|
+
def substitute(template, values)
|
|
124
|
+
values.reduce(template) { |text, (key, value)| text.gsub("{#{key}}", value.to_s) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Looks up the raw template text declared for one refusal site.
|
|
128
|
+
#
|
|
129
|
+
# @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
|
|
130
|
+
# @param site [String] the template site within `refusal`, such as `"unknown_args"`
|
|
131
|
+
# @return [String] the raw `RefusalTemplate` text, `{name}` placeholders unsubstituted
|
|
132
|
+
# @raise [KeyError] if no `RefusalTemplate` row declares `refusal`/`site`
|
|
133
|
+
def template(refusal, site)
|
|
134
|
+
TEMPLATES.fetch([refusal, site]) do
|
|
128
135
|
raise KeyError, "no refusal template for #{refusal}/#{site} — declare it in " \
|
|
129
136
|
"Vocabulary::RefusalTemplate first"
|
|
130
137
|
end
|
|
131
|
-
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The declared arguments for one refusal site.
|
|
141
|
+
#
|
|
142
|
+
# Read lazily, not into a constant: bin/project_vocabulary boots
|
|
143
|
+
# `hecks` (and so this file) before it writes the table a newly
|
|
144
|
+
# declared site's rows live in.
|
|
145
|
+
#
|
|
146
|
+
# @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
|
|
147
|
+
# @param site [String] the template site within `refusal`, such as `"unknown_args"`
|
|
148
|
+
# @return [Array<Hash>] the `RefusalSiteArgument` rows declared for `refusal`/`site`
|
|
149
|
+
# @raise [KeyError] if no `RefusalSiteArgument` rows declare `refusal`/`site`
|
|
150
|
+
def argument_rows(refusal, site)
|
|
151
|
+
@argument_rows ||= Hecks::Vocabulary.rows("RefusalSiteArgument")
|
|
152
|
+
.group_by { |row| [row["refusal"], row["site"]] }
|
|
153
|
+
.transform_values(&:freeze)
|
|
154
|
+
.freeze
|
|
155
|
+
@argument_rows.fetch([refusal, site]) do
|
|
156
|
+
raise KeyError, "no refusal arguments for #{refusal}/#{site} — declare them in " \
|
|
157
|
+
"Vocabulary::RefusalSiteArgument first"
|
|
158
|
+
end
|
|
132
159
|
end
|
|
133
160
|
end
|
|
134
161
|
end
|
|
@@ -3,43 +3,52 @@ require_relative "../saga_pending_dispatch"
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Runtime
|
|
5
5
|
class Registry
|
|
6
|
-
#
|
|
6
|
+
# **Saga persistence** — no new DSL verb, no new world setting.
|
|
7
7
|
# Whatever adapter a domain's own aggregates already use for
|
|
8
8
|
# durability, its sagas use the same one, automatically: three
|
|
9
|
-
#
|
|
9
|
+
# optional adapter methods (`save_saga`/`delete_saga`/`each_saga`),
|
|
10
10
|
# `respond_to?`-checked the same way `Ports::Persistence::AppendOnly`
|
|
11
11
|
# already treats an adapter's own optional methods
|
|
12
12
|
# (`find`/`all`/`count`/`reset!`/`events`/`record_event`).
|
|
13
13
|
module SagaPersistence
|
|
14
|
-
# Resolved and memoized per domain — the
|
|
14
|
+
# Resolved and memoized per domain — the first real aggregate
|
|
15
15
|
# declared in the domain's own bluebook is the resolution
|
|
16
16
|
# anchor. `Ports::Persistence::BindingPolicy.resolve` for that
|
|
17
17
|
# aggregate already falls back to a domain-level default bind
|
|
18
18
|
# (§0's `Hecksagon#bind_for`) before falling back to Memory, so
|
|
19
19
|
# this needs no separate "was a default declared" branch of its
|
|
20
|
-
# own — whatever adapter that anchor resolves to already
|
|
20
|
+
# own — whatever adapter that anchor resolves to already is the
|
|
21
21
|
# domain's own default in the normal case (every aggregate
|
|
22
22
|
# shares one adapter), and is the honest, documented fallback
|
|
23
23
|
# for the rarer domain genuinely split across more than one
|
|
24
24
|
# local adapter with no default declared.
|
|
25
25
|
#
|
|
26
|
-
#
|
|
26
|
+
# Genuinely lazy, genuinely POST-boot — unlike `rehydrate_sagas!`
|
|
27
27
|
# below (boot-only), this is called from live dispatch
|
|
28
28
|
# (`SagaInterpreter#checkpoint`/`#end_saga`, on every saga
|
|
29
|
-
# transition), so the
|
|
29
|
+
# transition), so the first call for a given domain can come from
|
|
30
30
|
# any dispatching thread, not just the boot thread. `@saga_persistence`
|
|
31
31
|
# itself is a plain Hash stood up once in `Registry#initialize` (no
|
|
32
32
|
# race on the container), but `resolve_saga_persistence` is real work
|
|
33
33
|
# (a `BindingPolicy.resolve` plus a lazy `repository` build) whose
|
|
34
|
-
#
|
|
35
|
-
# through — must be the
|
|
34
|
+
# result — the actual adapter instance a domain's sagas persist
|
|
35
|
+
# through — must be the same object for every caller: two threads
|
|
36
36
|
# racing the first lookup and each building their own adapter would
|
|
37
37
|
# silently split one domain's saga writes across two adapter
|
|
38
38
|
# instances (worse than `Dispatcher#reaction_depth`'s M20 — that bug
|
|
39
|
-
# corrupted a counter; this one can corrupt
|
|
40
|
-
# state lands in). Double-checked locking against a
|
|
39
|
+
# corrupted a counter; this one can corrupt which store a saga's
|
|
40
|
+
# state lands in). Double-checked locking against a dedicated mutex
|
|
41
41
|
# — never `@saga_mutex` — see `Registry#initialize`'s own comment for
|
|
42
42
|
# why reusing that one would deadlock.
|
|
43
|
+
# Resolves and memoizes the adapter a domain's sagas persist through.
|
|
44
|
+
#
|
|
45
|
+
# @param domain [String, Symbol] the domain to resolve saga persistence for
|
|
46
|
+
# @return [Adapters::Heki, Adapters::Postgres, Adapters::Sqlite, Adapters::D1,
|
|
47
|
+
# Ports::Persistence::Plugins::Era::PostgresEra, Ports::Persistence::NullSagaStore]
|
|
48
|
+
# the same adapter instance the domain's anchor aggregate persists through, when it
|
|
49
|
+
# implements `save_saga`; `NULL_SAGA_STORE` for a domain with no anchor aggregate, an
|
|
50
|
+
# adapter that does not implement the capability, a `RemoteRuntime`-shaped adapter,
|
|
51
|
+
# or a `Runtime::WiringError` resolving the anchor's own bind
|
|
43
52
|
def saga_persistence(domain)
|
|
44
53
|
key = domain.to_s
|
|
45
54
|
@saga_persistence[key] || @saga_persistence_mutex.synchronize do
|
|
@@ -47,26 +56,28 @@ module Hecks
|
|
|
47
56
|
end
|
|
48
57
|
end
|
|
49
58
|
|
|
50
|
-
#
|
|
59
|
+
# Walks every loaded domain, repopulating `saga_instances` from
|
|
51
60
|
# whatever `saga_persistence(domain)` resolves to — a real store
|
|
52
61
|
# for a domain whose adapter answers the capability, `each_saga`
|
|
53
62
|
# yielding real rows; `NULL_SAGA_STORE`'s own `each_saga` for
|
|
54
63
|
# everything else, which never yields at all, so this needs no
|
|
55
|
-
# `respond_to?` branch of its own — the
|
|
64
|
+
# `respond_to?` branch of its own — the same reason every other
|
|
56
65
|
# call site in this capability never needs one. Called once at
|
|
57
66
|
# boot (`Loader.boot`, between `verify!` and dispatcher
|
|
58
67
|
# construction) — a process that's been running has no reason to
|
|
59
68
|
# re-walk its own already-current `saga_instances`.
|
|
60
69
|
#
|
|
61
|
-
#
|
|
70
|
+
# Boot-time-only (or its test-runner equivalent) — this method's
|
|
62
71
|
# only callers are `Loader.run_boot_gates!` (single-threaded, before
|
|
63
72
|
# `dispatcher_for` ever exists) and `Registry#reset_runtime_state!`
|
|
64
73
|
# (single-threaded test runner — see that method's own comment).
|
|
65
74
|
# Never called from live dispatch, so `@saga_instances` mutation
|
|
66
|
-
# here has no concurrent caller to race, unlike its
|
|
75
|
+
# here has no concurrent caller to race, unlike its other two write
|
|
67
76
|
# points inside `@saga_mutex.synchronize` blocks (`saga_interpreter.
|
|
68
77
|
# rb`), which genuinely do and are guarded accordingly.
|
|
69
78
|
# rubocop:disable-next Hecks/ThreadSharedIvarMutation
|
|
79
|
+
#
|
|
80
|
+
# @return [Hecks::Runtime::Registry] self
|
|
70
81
|
def rehydrate_sagas!
|
|
71
82
|
@hecksagons.each_key do |domain|
|
|
72
83
|
saga_persistence(domain).each_saga do |process_manager, correlation, state, memory, completed_compensations = []|
|
|
@@ -81,28 +92,28 @@ module Hecks
|
|
|
81
92
|
|
|
82
93
|
private
|
|
83
94
|
|
|
84
|
-
#
|
|
95
|
+
# The other half of the outbox-shaped fix (see saga_pending_
|
|
85
96
|
# dispatch.rb) — `SAGA_PENDING_DISPATCH_KEY`, if the crashed
|
|
86
97
|
# process left it standing, means the row's own `state` was
|
|
87
98
|
# checkpointed but the dispatch cascade that justifies it may
|
|
88
99
|
# never have run. Stripped out of `memory` before it becomes
|
|
89
|
-
# this instance's
|
|
100
|
+
# this instance's live `:memory` (so nothing downstream — a
|
|
90
101
|
# `given`, a `with:` mapping, the fuzzer — ever sees it), and
|
|
91
|
-
# surfaced loudly instead: a
|
|
102
|
+
# surfaced loudly instead: a warning plus a `saga_log` entry,
|
|
92
103
|
# never an automatic redrive (see saga_pending_dispatch.rb for
|
|
93
104
|
# why redriving without idempotent delivery would be worse than
|
|
94
|
-
# the stall). This is real, durable crash-recovery
|
|
105
|
+
# the stall). This is real, durable crash-recovery visibility —
|
|
95
106
|
# still not the reconciliation itself, which stays a human's
|
|
96
107
|
# call until hecks has idempotent redelivery to make it safe.
|
|
97
108
|
# rubocop:disable-next Hecks/ThreadSharedIvarMutation -- same
|
|
98
109
|
# justification as `rehydrate_sagas!`'s own disable comment
|
|
99
|
-
# above: this method's
|
|
110
|
+
# above: this method's only caller is that boot-time-only walk,
|
|
100
111
|
# never live dispatch, so `@saga_log` has no concurrent writer
|
|
101
112
|
# to race here.
|
|
102
113
|
def warn_stalled_saga(domain, process_manager, correlation, state, pending)
|
|
103
114
|
# `.transform_keys(&:to_sym)` — Heki's own `each_saga` only
|
|
104
|
-
# symbolizes `memory`'s
|
|
105
|
-
# own `transform_keys`, one level deep); a value
|
|
115
|
+
# symbolizes `memory`'s top-level keys (`SagaStore#each_saga`'s
|
|
116
|
+
# own `transform_keys`, one level deep); a value nested under
|
|
106
117
|
# one of those keys, like this marker, comes back with plain
|
|
107
118
|
# string keys from Heki specifically, symbol keys already from
|
|
108
119
|
# Postgres/SQLite/D1's own `symbolize_names: true` parse. Normalizing
|