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
|
@@ -28,14 +28,29 @@ module Hecks
|
|
|
28
28
|
|
|
29
29
|
attr_reader :registry
|
|
30
30
|
|
|
31
|
+
# @param registry [Runtime::Registry] the booted registry whose declared
|
|
32
|
+
# process managers and saga persistence this interpreter runs against
|
|
33
|
+
# @param door [Runtime::Dispatcher] the dispatcher a saga leg's own dispatch
|
|
34
|
+
# re-enters through
|
|
31
35
|
def initialize(registry, door:)
|
|
32
36
|
@registry = registry
|
|
33
37
|
@door = door
|
|
34
38
|
end
|
|
35
39
|
|
|
40
|
+
# Runs `domain`'s declared process managers against `event`: begins,
|
|
41
|
+
# advances or ends each matching saga instance, checkpointing durably
|
|
42
|
+
# before its own dispatches run.
|
|
43
|
+
#
|
|
36
44
|
# `only:` — one process manager, the outbox relay's way of running
|
|
37
45
|
# exactly the consumer a row names (`Runtime::Outbox::Relay#
|
|
38
46
|
# run_consumer`); nil advances every manager the domain declares.
|
|
47
|
+
#
|
|
48
|
+
# @param event [Runtime::Event] the just-emitted event to react to
|
|
49
|
+
# @param domain [String, Symbol] the domain whose declared process managers
|
|
50
|
+
# are checked
|
|
51
|
+
# @param only [Bluebook::ProcessManager, nil] one process manager to run
|
|
52
|
+
# exactly, instead of every manager `domain` declares
|
|
53
|
+
# @return [void]
|
|
39
54
|
def advance(event, domain, only: nil)
|
|
40
55
|
bluebook = @registry.bluebook(domain)
|
|
41
56
|
return unless bluebook
|
|
@@ -49,26 +64,25 @@ module Hecks
|
|
|
49
64
|
|
|
50
65
|
private
|
|
51
66
|
|
|
52
|
-
#
|
|
53
|
-
# holds `saga_mutex` across
|
|
67
|
+
# The checkpoint write, shared by every mutation site below —
|
|
68
|
+
# holds `saga_mutex` across both the in-memory Hash mutation and
|
|
54
69
|
# the persistence write (§7), not just the Hash mutation alone:
|
|
55
|
-
# two threads racing the
|
|
70
|
+
# two threads racing the same (process_manager, correlation) key
|
|
56
71
|
# could otherwise interleave their writes out of order, silently
|
|
57
72
|
# reordering a saga's own transition history — worse for the
|
|
58
73
|
# adapters with no locking of their own (Heki) than for Postgres.
|
|
59
|
-
# `deep_copy` guards against
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
# itself calls `JSON.generate` on it.
|
|
74
|
+
# `deep_copy` guards against a real bug shape (over-freezing a live,
|
|
75
|
+
# still-mutated Hash) — never hand a persistence adapter the same
|
|
76
|
+
# object `advance_saga`/`unwind` go on to mutate in place;
|
|
77
|
+
# round-tripping through JSON is also what guarantees the value is
|
|
78
|
+
# safe for every adapter that itself calls `JSON.generate` on it.
|
|
65
79
|
# `pending:` — see saga_pending_dispatch.rb. Injected into the
|
|
66
|
-
#
|
|
80
|
+
# written copy of memory only, never into `instance[:memory]`
|
|
67
81
|
# itself: every other reader of a live instance's memory
|
|
68
82
|
# (`dispatch_args`'s "opening event memory" scope, the fuzzer's
|
|
69
83
|
# own round-trip/shape checks, `saga_spec.rb`'s exact-equality
|
|
70
84
|
# assertion against a fresh instance's seeded memory) sees exactly
|
|
71
|
-
# what it always did. The marker exists
|
|
85
|
+
# what it always did. The marker exists only in the persisted
|
|
72
86
|
# blob, and only for as long as a dispatch cascade is genuinely
|
|
73
87
|
# in flight for this instance.
|
|
74
88
|
def checkpoint(process_manager, correlation, instance, domain, pending: nil)
|
|
@@ -82,8 +96,8 @@ module Hecks
|
|
|
82
96
|
end
|
|
83
97
|
|
|
84
98
|
# `deep_copy` is `JSON.parse(JSON.generate(hash), ...)`, which
|
|
85
|
-
# only accepts an
|
|
86
|
-
# is an
|
|
99
|
+
# only accepts an object at the top level — `completed_compensations`
|
|
100
|
+
# is an array, so it gets its own wrap-and-unwrap rather than a
|
|
87
101
|
# second, parallel `deep_copy_array` reimplementing the same
|
|
88
102
|
# round-trip. `|| []` — an instance from before this field existed
|
|
89
103
|
# (or one that has never completed a compensable leg) rehydrates
|
|
@@ -105,15 +119,15 @@ module Hecks
|
|
|
105
119
|
created = @registry.saga_mutex.synchronize do
|
|
106
120
|
next false if @registry.saga_instances[process_manager.name].key?(correlation)
|
|
107
121
|
|
|
108
|
-
# `.dup`,
|
|
109
|
-
# as a
|
|
122
|
+
# `.dup`, not the same object — a fresh saga's own memory starts
|
|
123
|
+
# as a copy of the starting event's own payload, never the
|
|
110
124
|
# payload itself. A saga's own memory is meant to be written
|
|
111
125
|
# into over its lifetime (remember-style, growing beyond what
|
|
112
126
|
# the starting event carried) ; the payload it was seeded from
|
|
113
|
-
# is a fact about something that
|
|
127
|
+
# is a fact about something that already happened, logged and
|
|
114
128
|
# emitted before the saga ever saw it. Sharing the one Hash
|
|
115
129
|
# object between them means a write into the saga's own memory
|
|
116
|
-
# is silently
|
|
130
|
+
# is silently also a write into an already-emitted event's own
|
|
117
131
|
# payload — retroactively adding a field nothing announced.
|
|
118
132
|
# `event.payload` is deep-frozen by `Event#emit!` by the time
|
|
119
133
|
# this runs, so a naive in-place write here would raise
|
|
@@ -132,11 +146,11 @@ module Hecks
|
|
|
132
146
|
instance: correlation, born: true, state: process_manager.states.first }
|
|
133
147
|
end
|
|
134
148
|
|
|
135
|
-
#
|
|
136
|
-
#
|
|
149
|
+
# The mutex covers only the state-check-and-mutate-and-checkpoint
|
|
150
|
+
# step, never the dispatch cascade that follows — `deliver_saga_
|
|
137
151
|
# dispatch` calls `@door.reenter`, which can recursively re-enter
|
|
138
|
-
#
|
|
139
|
-
# or itself again) on the
|
|
152
|
+
# this same interpreter (a saga's own leg triggering another saga,
|
|
153
|
+
# or itself again) on the same thread, and `Mutex` is not
|
|
140
154
|
# reentrant: holding it across that call would deadlock the
|
|
141
155
|
# thread against itself the moment any real chain did that.
|
|
142
156
|
def advance_saga(process_manager, event, domain)
|
|
@@ -156,7 +170,7 @@ module Hecks
|
|
|
156
170
|
@registry.saga_log << record.merge(advanced: false, reason: "no conversation remembers #{correlation.inspect}")
|
|
157
171
|
next false
|
|
158
172
|
end
|
|
159
|
-
#
|
|
173
|
+
# The leg is chosen by (event, current state) — C10.3. Read
|
|
160
174
|
# under the mutex, against the state this instance holds right
|
|
161
175
|
# now, so two legs on the same event from different states each
|
|
162
176
|
# answer exactly when their own state is current.
|
|
@@ -189,7 +203,7 @@ module Hecks
|
|
|
189
203
|
{ on: event.name, from: from_state, to: to_state, dispatches: handler.dispatches.map(&:command_name) }
|
|
190
204
|
end
|
|
191
205
|
|
|
192
|
-
#
|
|
206
|
+
# The shared tail of `advance_saga` and `unwind` — both are "guard,
|
|
193
207
|
# mutate, checkpoint-with-pending" under the mutex (kept separate
|
|
194
208
|
# per caller: `advance_saga`'s own guard also has to handle "no
|
|
195
209
|
# instance at all", `unwind`'s doesn't), then this: log the real
|
|
@@ -197,11 +211,11 @@ module Hecks
|
|
|
197
211
|
# pending marker once that cascade — however it ended — is done.
|
|
198
212
|
def settle_transition(process_manager, event, handler, instance, correlation, domain, record, pre_state,
|
|
199
213
|
drain_compensations: false)
|
|
200
|
-
# `from:`/`to:` are the
|
|
214
|
+
# `from:`/`to:` are the instance's own real pre/post state — read
|
|
201
215
|
# back from `instance` itself, never re-derived from `handler.
|
|
202
216
|
# from_state`/`handler.to_state` a second time. `Properties.saga_
|
|
203
217
|
# advances_follow_declared_handlers` (fuzzing/properties.rb) builds
|
|
204
|
-
# its
|
|
218
|
+
# its own "declared edges" list from this same handler object (via
|
|
205
219
|
# `process_manager.handlers`), so a log entry that just echoed `handler.
|
|
206
220
|
# from_state`/`handler.to_state` back could never disagree with
|
|
207
221
|
# that list no matter what the runtime actually did — the entry
|
|
@@ -213,10 +227,10 @@ module Hecks
|
|
|
213
227
|
# real mismatch instead of vanishing into a tautology.
|
|
214
228
|
@registry.saga_log << record.merge(advanced: true, from: pre_state, to: instance[:state])
|
|
215
229
|
|
|
216
|
-
#
|
|
217
|
-
# own call (`drain_compensations: true`): every leg
|
|
230
|
+
# **Derived compensation first, newest-first** — only for `unwind`'s
|
|
231
|
+
# own call (`drain_compensations: true`): every leg this instance
|
|
218
232
|
# actually completed that declared its own `compensates`, popped
|
|
219
|
-
# and dispatched in reverse completion order,
|
|
233
|
+
# and dispatched in reverse completion order, before any
|
|
220
234
|
# hand-written `on :refused` dispatches below — coexistence, not
|
|
221
235
|
# replacement. Drained (not just read) as it fires: a saga's own
|
|
222
236
|
# `on :refused` handler is guarded against re-entry by `unwind`'s
|
|
@@ -234,19 +248,19 @@ module Hecks
|
|
|
234
248
|
deliver_saga_dispatch(process_manager, spec, event, instance, correlation, domain)
|
|
235
249
|
end
|
|
236
250
|
|
|
237
|
-
#
|
|
251
|
+
# **The clear** — guarded by the same identity check `end_saga`'s own
|
|
238
252
|
# `.delete` return value implies: `deliver_saga_dispatch`'s
|
|
239
|
-
# `@door.reenter` can synchronously trigger this
|
|
253
|
+
# `@door.reenter` can synchronously trigger this same correlation's
|
|
240
254
|
# `ends_on` event as a nested reaction (a leg's own dispatch is
|
|
241
255
|
# what makes the saga's terminal event fire), which deletes this
|
|
242
256
|
# row from the store before this line ever runs. Writing the
|
|
243
|
-
# clear unconditionally would
|
|
257
|
+
# clear unconditionally would resurrect a legitimately-ended saga
|
|
244
258
|
# — this diff's own first attempt did exactly that, caught by
|
|
245
259
|
# `saga_durability_spec.rb`'s "deletes the checkpoint once a saga
|
|
246
260
|
# genuinely ends" — so this only re-checkpoints when `instance`
|
|
247
|
-
# is still
|
|
261
|
+
# is still the same object `@saga_instances` holds for this
|
|
248
262
|
# correlation (`.equal?`, not `==`: a fresh saga reborn under the
|
|
249
|
-
# same correlation between then and now is a
|
|
263
|
+
# same correlation between then and now is a different instance,
|
|
250
264
|
# and writing this stale one's state onto that one's row would be
|
|
251
265
|
# its own corruption). Under the mutex — dispatching is over by
|
|
252
266
|
# now, so this is not the reentrancy hazard `advance_saga`'s own
|
|
@@ -264,9 +278,9 @@ module Hecks
|
|
|
264
278
|
args = dispatch_args(process_manager, spec, event, instance, correlation)
|
|
265
279
|
record = { process_manager: process_manager.name, instance: correlation, dispatch: spec.command_name }
|
|
266
280
|
|
|
267
|
-
#
|
|
281
|
+
# The raw inputs `args` was resolved from, captured alongside the
|
|
268
282
|
# result — never re-derived from history[:saga_instances] later
|
|
269
|
-
# (that only ever holds the
|
|
283
|
+
# (that only ever holds the final memory, after every step has
|
|
270
284
|
# run; this dispatch's own memory, at the moment it actually
|
|
271
285
|
# fired, is a different fact for a saga whose memory keeps
|
|
272
286
|
# changing). `spec.with_spec.empty?` skipped: nothing declared
|
|
@@ -282,7 +296,7 @@ module Hecks
|
|
|
282
296
|
end
|
|
283
297
|
|
|
284
298
|
if @door.reaction_depth_reached?
|
|
285
|
-
#
|
|
299
|
+
# The ceiling is not a domain decision either — same reasoning as a
|
|
286
300
|
# crash, below — but unlike a crash there is nothing ambiguous
|
|
287
301
|
# about it: the leg unambiguously did not run, so it unwinds
|
|
288
302
|
# exactly like a refusal instead of stranding the instance for a
|
|
@@ -301,18 +315,18 @@ module Hecks
|
|
|
301
315
|
attempt = 0
|
|
302
316
|
compensation_recorded = false
|
|
303
317
|
begin
|
|
304
|
-
#
|
|
305
|
-
# returns — `@door.reenter` can recursively
|
|
306
|
-
# saga interpreter (the event
|
|
307
|
-
#
|
|
308
|
-
#
|
|
309
|
-
# "after reenter succeeds" would be too late for a
|
|
318
|
+
# Recorded before dispatching, not after `@door.reenter`
|
|
319
|
+
# returns — `@door.reenter` can recursively re-enter this same
|
|
320
|
+
# saga interpreter (the event this dispatch emits triggers a
|
|
321
|
+
# later handler, which can itself refuse and unwind) entirely
|
|
322
|
+
# within this one call, before it ever returns here. Recording
|
|
323
|
+
# "after reenter succeeds" would be too late for a nested
|
|
310
324
|
# refusal to ever see this leg's own compensation — found
|
|
311
325
|
# live: Settlement's own AccountDebited handler refuses
|
|
312
|
-
# Account.Credit and unwinds from
|
|
326
|
+
# Account.Credit and unwinds from inside Account.Debit's own
|
|
313
327
|
# `reenter` call, so "delivered: true, then record" left the
|
|
314
328
|
# ledger empty at the exact moment it was needed. Popped back
|
|
315
|
-
# off in the rescues below if
|
|
329
|
+
# off in the rescues below if this leg's own attempt is the
|
|
316
330
|
# one that failed — never left recorded for a refusal that
|
|
317
331
|
# was never this leg's own to compensate.
|
|
318
332
|
if spec.compensates && !compensation_recorded
|
|
@@ -336,7 +350,7 @@ module Hecks
|
|
|
336
350
|
rescue *DOMAIN_REFUSALS => e
|
|
337
351
|
unrecord_compensation(instance, correlation, domain, process_manager) if compensation_recorded
|
|
338
352
|
# Same rule as the policy interpreter : a refusal by the target is
|
|
339
|
-
# a recorded outcome, and the leg that raised it
|
|
353
|
+
# a recorded outcome, and the leg that raised it unwinds — see
|
|
340
354
|
# `unwind`'s own comment for why the procedure runs its
|
|
341
355
|
# compensation here rather than leaving the money (or whatever
|
|
342
356
|
# else a leg moved) sitting out.
|
|
@@ -345,13 +359,13 @@ module Hecks
|
|
|
345
359
|
rescue StandardError => e
|
|
346
360
|
unrecord_compensation(instance, correlation, domain, process_manager) if compensation_recorded
|
|
347
361
|
compensation_recorded = false
|
|
348
|
-
# A
|
|
362
|
+
# A defect, not a refusal — see PolicyInterpreter#deliver's own
|
|
349
363
|
# comment for the full reasoning: the same DOMAIN_REFUSALS split,
|
|
350
364
|
# and the same "the triggering command already succeeded and
|
|
351
365
|
# persisted by the time this runs" fact that makes catching it
|
|
352
366
|
# here safe rather than reckless.
|
|
353
367
|
#
|
|
354
|
-
#
|
|
368
|
+
# Unlike a refusal, a crash is not a decision the domain made, so
|
|
355
369
|
# it does not unwind on the first failure — MAX_DEFECT_RETRIES
|
|
356
370
|
# gives a transient failure (a DB timeout, a race, a cold start)
|
|
357
371
|
# a chance to clear on its own, retrying the identical dispatch,
|
|
@@ -381,36 +395,36 @@ module Hecks
|
|
|
381
395
|
end
|
|
382
396
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
383
397
|
|
|
384
|
-
#
|
|
398
|
+
# The rollback half of `deliver_saga_dispatch`'s own speculative
|
|
385
399
|
# pre-record (that method's own comment for why it has to be
|
|
386
|
-
# speculative) —
|
|
400
|
+
# speculative) — this leg's own attempt is the one that failed,
|
|
387
401
|
# so whatever was just pushed for it was never actually earned.
|
|
388
402
|
# `.pop`, not a search-and-delete: nothing else can have pushed
|
|
389
|
-
#
|
|
403
|
+
# after this leg's own entry without this leg's own `@door.
|
|
390
404
|
# reenter` call having already returned (the recursive re-entry
|
|
391
|
-
# this whole mechanism exists for only ever runs
|
|
405
|
+
# this whole mechanism exists for only ever runs between this
|
|
392
406
|
# push and this leg's own return, and a nested refusal that
|
|
393
407
|
# consumed it already popped it itself — this rollback only ever
|
|
394
|
-
# runs for
|
|
408
|
+
# runs for this leg's own, still-present entry).
|
|
395
409
|
def unrecord_compensation(instance, correlation, domain, process_manager)
|
|
396
410
|
instance[:completed_compensations].pop
|
|
397
411
|
checkpoint(process_manager, correlation, instance, domain)
|
|
398
412
|
end
|
|
399
413
|
|
|
400
|
-
# A refused leg
|
|
414
|
+
# A refused leg unwinds — the procedure runs the leg declared `on :refused`,
|
|
401
415
|
# which is where the compensation lives. So does a leg that hit the
|
|
402
416
|
# reaction-depth ceiling, and so does a leg that crashed and stayed
|
|
403
417
|
# crashing through MAX_DEFECT_RETRIES — see `deliver_saga_dispatch`'s own
|
|
404
418
|
# comments for why each of those is safe to route here.
|
|
405
419
|
#
|
|
406
|
-
# Until this existed a refusal was
|
|
420
|
+
# Until this existed a refusal was recorded and nothing else happened. The
|
|
407
421
|
# wire's thousand was taken from the source, refused by the destination, and
|
|
408
422
|
# sat nowhere until a human dispatched the reversal by hand ; banking's
|
|
409
423
|
# settlement left a debit standing with no credit and no compensation at all.
|
|
410
424
|
# Both bluebooks had written the compensating leg. Nothing armed it.
|
|
411
425
|
#
|
|
412
|
-
# A compensation that is itself refused does
|
|
413
|
-
# flag to stop it: the state moves to the compensating leg's to_state
|
|
426
|
+
# A compensation that is itself refused does not unwind again, and needs no
|
|
427
|
+
# flag to stop it: the state moves to the compensating leg's to_state before
|
|
414
428
|
# its dispatches run, so a second refusal finds the instance no longer in
|
|
415
429
|
# from_state and records that instead. The check is the guard.
|
|
416
430
|
def unwind(process_manager, event, instance, correlation, domain)
|
|
@@ -423,7 +437,7 @@ module Hecks
|
|
|
423
437
|
# Same non-reentrancy reasoning as `advance_saga`'s own comment —
|
|
424
438
|
# the mutex covers only the check-and-mutate-and-checkpoint step.
|
|
425
439
|
advanced = @registry.saga_mutex.synchronize do
|
|
426
|
-
# The compensating leg is selected by (REFUSED
|
|
440
|
+
# The compensating leg is selected by (`REFUSED`, current state)
|
|
427
441
|
# too — C10.3, one rule for every leg.
|
|
428
442
|
handler = process_manager.handler_for(REFUSED, instance[:state])
|
|
429
443
|
unless handler
|
|
@@ -442,7 +456,7 @@ module Hecks
|
|
|
442
456
|
|
|
443
457
|
# See `settle_transition`'s own comment on `pre_state`/
|
|
444
458
|
# `instance[:state]` — the real observed transition, not a
|
|
445
|
-
# second read of the
|
|
459
|
+
# second read of the same handler object `Properties.saga_
|
|
446
460
|
# advances_follow_declared_handlers` checks this log against.
|
|
447
461
|
# `drain_compensations: true` — only `unwind`'s own call site
|
|
448
462
|
# fires derived compensation; `advance_saga`'s own call never
|
|
@@ -451,16 +465,16 @@ module Hecks
|
|
|
451
465
|
drain_compensations: true)
|
|
452
466
|
end
|
|
453
467
|
|
|
454
|
-
# A
|
|
468
|
+
# A derived compensation — `entry[:args]` is already resolved
|
|
455
469
|
# (`record_completed_compensation`'s own comment for why), so this
|
|
456
470
|
# skips `dispatch_args` entirely and goes straight to delivery,
|
|
457
|
-
# through the
|
|
471
|
+
# through the same retry-on-defect path an ordinary forward leg
|
|
458
472
|
# uses. Never re-enters `unwind` on its own failure — a
|
|
459
473
|
# compensation that itself refuses is a real, pre-existing gap
|
|
460
474
|
# this feature makes visible rather than closes (see this file's
|
|
461
475
|
# own class-level notes); `compensation_failed: true` tags it
|
|
462
476
|
# distinctly in the log instead of recording it identically to an
|
|
463
|
-
# ordinary failed delivery, and every
|
|
477
|
+
# ordinary failed delivery, and every other completed compensation
|
|
464
478
|
# still queued still gets its own attempt.
|
|
465
479
|
def deliver_derived_compensation(process_manager, entry, correlation, domain)
|
|
466
480
|
record = { process_manager: process_manager.name, instance: correlation, dispatch: entry[:command_name] }
|
|
@@ -506,40 +520,40 @@ module Hecks
|
|
|
506
520
|
)
|
|
507
521
|
end
|
|
508
522
|
|
|
509
|
-
# BUG#6 —
|
|
510
|
-
# from `command_name`'s own shape.
|
|
511
|
-
# `::` after `Naming.command_ref`'s own rewrite
|
|
512
|
-
# domain-qualified" and left alone
|
|
523
|
+
# BUG#6 — unconditionally the saga's own home domain, never inferred
|
|
524
|
+
# from `command_name`'s own shape. Guessing from a leftover
|
|
525
|
+
# `::` after `Naming.command_ref`'s own rewrite (read as "already
|
|
526
|
+
# domain-qualified" and left alone) cannot actually
|
|
513
527
|
# tell a genuinely cross-domain reference (`Banking::Account::
|
|
514
|
-
# Debit` -> one `::` survives) apart from a
|
|
528
|
+
# Debit` -> one `::` survives) apart from a same-domain entity
|
|
515
529
|
# command reference (`Manifest::Slot::Fill` -> one `::` survives
|
|
516
530
|
# too, for an unrelated reason — entity nesting, not a domain
|
|
517
531
|
# qualifier) — both collapse to the identical "one `::` left" shape,
|
|
518
532
|
# and the string alone carries no further signal to split them
|
|
519
533
|
# (confirmed against `Naming.command_ref`'s own rewrite: it only
|
|
520
|
-
# ever strips the
|
|
534
|
+
# ever strips the last `::`, so the count of what remains is blind
|
|
521
535
|
# to why it's there). Picking the cross-domain reading unconditionally
|
|
522
|
-
#
|
|
523
|
-
# `Manifest::Slot::Fill` — an entity command in its
|
|
524
|
-
# unprefixed, so `Naming.split_verb` read "Manifest" as a domain
|
|
536
|
+
# would leave `qa/stress_domains/waybill`'s own `Packing` saga dispatching
|
|
537
|
+
# `Manifest::Slot::Fill` — an entity command in its own domain —
|
|
538
|
+
# unprefixed, so `Naming.split_verb` would read "Manifest" as a domain
|
|
525
539
|
# name instead of this chapter's own aggregate, and the dispatch
|
|
526
|
-
#
|
|
540
|
+
# would fail with `UnknownVerb`, silently recorded as an ordinary
|
|
527
541
|
# domain refusal rather than surfacing as the real bug it is.
|
|
528
542
|
#
|
|
529
|
-
#
|
|
543
|
+
# The fix mirrors `PolicyInterpreter#deliver`'s own mechanism,
|
|
530
544
|
# which never had this bug: a policy's cross-domain target is a
|
|
531
|
-
#
|
|
545
|
+
# separate, explicit field (`Policy#target_domain`, set only by the
|
|
532
546
|
# `across` keyword) — `deliver` unconditionally builds
|
|
533
547
|
# `"#{policy.target_domain || domain}::#{policy.trigger_command}"`,
|
|
534
|
-
# never asking whether `trigger_command`
|
|
548
|
+
# never asking whether `trigger_command` looks already-qualified.
|
|
535
549
|
# A saga's own `dispatch`/`compensates` has no such explicit field
|
|
536
|
-
# and no keyword to set one — and, confirmed against the
|
|
550
|
+
# and no keyword to set one — and, confirmed against the entire
|
|
537
551
|
# corpus (banking's Onboarding/Settlement/ExternalSettlement,
|
|
538
552
|
# quality_control's BugCiWatch, and this domain's own Packing),
|
|
539
553
|
# no saga anywhere ever dispatches genuinely cross-domain: "every
|
|
540
554
|
# command a saga fires lands inside its own bluebook chapter"
|
|
541
555
|
# (`Projections::Diagrams#saga_diagram`'s own comment, written
|
|
542
|
-
# independently of this fix and still true). So the home domain
|
|
556
|
+
# independently of this fix and still true). So the home domain is
|
|
543
557
|
# the only explicit context a saga dispatch ever has — this applies
|
|
544
558
|
# it the same way `deliver` applies its own default (no `across`)
|
|
545
559
|
# case, without inventing a keyword nothing in the corpus needs.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Runtime
|
|
3
|
-
#
|
|
3
|
+
# The one shared constant between `SagaInterpreter` (the writer) and
|
|
4
4
|
# `Registry::SagaPersistence#rehydrate_sagas!` (the reader) for a
|
|
5
5
|
# scoped, minimal answer to the saga-durability review's item 8 (a
|
|
6
6
|
# durable outbox): a marker that survives exactly the window a crash
|
|
7
7
|
# in `advance_saga`/`unwind` can otherwise hide.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# **The problem this closes** — `checkpoint` persists a saga's new state
|
|
10
|
+
# before the leg that justifies it (`handler.dispatches`) runs, and
|
|
11
11
|
# deliberately so: the mutex it holds is not reentrant, and a
|
|
12
12
|
# dispatch can re-enter this same interpreter. If the process dies
|
|
13
13
|
# in that window, the store says the transition happened and there
|
|
@@ -15,33 +15,33 @@ module Hecks
|
|
|
15
15
|
# domain never got asked), not a defect (nothing raised), just
|
|
16
16
|
# silence indistinguishable from a leg that finished cleanly.
|
|
17
17
|
#
|
|
18
|
-
#
|
|
18
|
+
# **The fix** — `checkpoint` now writes this key into the same already-
|
|
19
19
|
# durable `memory` blob (no new column, no adapter/schema change:
|
|
20
20
|
# `memory` is already an opaque, adapter-agnostic JSON blob every
|
|
21
21
|
# `save_saga` implementation round-trips verbatim) whenever it
|
|
22
|
-
# checkpoints a state a dispatch cascade hasn't run for
|
|
22
|
+
# checkpoints a state a dispatch cascade hasn't run for yet, and
|
|
23
23
|
# clears it (a second checkpoint, `pending: nil`) once that cascade
|
|
24
24
|
# — success, refusal-compensated, defect-compensated, or ceiling-
|
|
25
25
|
# compensated — has actually run. A crash between those two writes
|
|
26
26
|
# leaves the marker standing; `rehydrate_sagas!` strips it back out
|
|
27
|
-
# of the
|
|
27
|
+
# of the live instance's own `:memory` (so no dispatch/`given`/
|
|
28
28
|
# fuzzer/doc consumer of a saga's memory ever sees this key — it
|
|
29
29
|
# exists only in the persisted blob) and surfaces it loudly instead.
|
|
30
30
|
#
|
|
31
|
-
#
|
|
31
|
+
# **What this deliberately does not do** — auto-redrive the pending leg.
|
|
32
32
|
# Redelivering a dispatch whose outcome is genuinely unknown is only
|
|
33
33
|
# safe with idempotent delivery (the downstream command recognizing
|
|
34
34
|
# and no-op'ing a duplicate), which hecks's command/event pipeline
|
|
35
35
|
# has no mechanism for today. Blindly re-dispatching without that is
|
|
36
|
-
# how a stalled transfer becomes a
|
|
36
|
+
# how a stalled transfer becomes a double-credited one — a strictly
|
|
37
37
|
# worse defect than the stall it would replace. So this is real,
|
|
38
|
-
# durable, crash-surviving
|
|
38
|
+
# durable, crash-surviving visibility into exactly what a stalled
|
|
39
39
|
# saga was doing when the process died — the missing half of "no
|
|
40
40
|
# reconciliation pass exists". The full pending → claimed →
|
|
41
41
|
# delivered outbox now exists too (`Runtime::Outbox`, ADR 0053) and
|
|
42
|
-
# is
|
|
43
|
-
#
|
|
44
|
-
# claim it); this marker names the saga's own
|
|
42
|
+
# is complementary, not a replacement: the outbox row names the
|
|
43
|
+
# event owed to this saga (and redrives it if the saga never got to
|
|
44
|
+
# claim it); this marker names the saga's own leg mid-flight after
|
|
45
45
|
# the event was delivered. A crash can leave either standing.
|
|
46
46
|
SAGA_PENDING_DISPATCH_KEY = :__hecks_saga_pending_dispatch__
|
|
47
47
|
end
|
|
@@ -2,29 +2,31 @@ require_relative "../ports/persistence/binding_policy"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Runtime
|
|
5
|
-
# The capability idiom for
|
|
5
|
+
# The capability idiom for multi-tenant hosting — mirrors EraCheck's
|
|
6
6
|
# own `lineage_capable?`, one level over. An adapter answers
|
|
7
|
-
# `tenant_capable?` with true when its
|
|
7
|
+
# `tenant_capable?` with true when its own instances genuinely
|
|
8
8
|
# isolate one boot's data from another's, given each tenant is its
|
|
9
9
|
# own separate `Runtime.boot` call (its own Registry, its own
|
|
10
10
|
# Dispatcher, its own adapter instances) rather than one shared
|
|
11
11
|
# process switching connections mid-dispatch.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
13
|
+
# ## Why no ambient "current tenant"
|
|
14
|
+
#
|
|
15
|
+
# The project register (Bluebook::ProjectRegister) already resolves an
|
|
16
|
+
# address's realm to a dispatcher at registration time — Router#resolve
|
|
17
|
+
# looks the FQN up in one flat table keyed by realm::domain::aggregate.verb,
|
|
18
|
+
# and each entry already carries its own dispatcher from its own
|
|
19
|
+
# boot. So "which tenant" is decided once, at boot/registration time
|
|
19
20
|
# (which of possibly many boots of the same directory a request's
|
|
20
21
|
# realm resolves to), never per-dispatch inside a shared registry.
|
|
21
22
|
# No ambient thread-local "current tenant" is needed, and no
|
|
22
23
|
# connection cache is needed beyond what booting-once-per-tenant
|
|
23
24
|
# already gives for free — each tenant's own PostgresEra instance
|
|
24
|
-
#
|
|
25
|
+
# is its own connection, held for the life of that boot.
|
|
26
|
+
#
|
|
27
|
+
# ## What `tenant_capable?` really asks
|
|
25
28
|
#
|
|
26
|
-
#
|
|
27
|
-
# not "can this adapter switch tenants," but "does booting this
|
|
29
|
+
# Not "can this adapter switch tenants," but "does booting this
|
|
28
30
|
# adapter twice, with different settings, for the same directory,
|
|
29
31
|
# actually keep the two boots' data apart." Memory answers true
|
|
30
32
|
# trivially — a `@records` Hash is a plain instance variable, and
|
|
@@ -41,13 +43,22 @@ module Hecks
|
|
|
41
43
|
module TenantCheck
|
|
42
44
|
module_function
|
|
43
45
|
|
|
44
|
-
#
|
|
46
|
+
# Refuses to let `domain` boot for more than one tenant unless every
|
|
47
|
+
# aggregate's resolved persistence adapter is `tenant_capable?`.
|
|
48
|
+
#
|
|
49
|
+
# A domain is safe to boot for more than one tenant only if every
|
|
45
50
|
# aggregate's resolved persistence adapter is tenant_capable? — one
|
|
46
51
|
# ungoverned adapter sharing state across two tenant boots is a
|
|
47
52
|
# real data leak, not a theoretical one, so this is checked before
|
|
48
53
|
# a second tenant boot of the same directory is trusted, the same
|
|
49
54
|
# severity EraCheck/refuse_ungoverned_roles! already hold their
|
|
50
55
|
# own gates to.
|
|
56
|
+
#
|
|
57
|
+
# @param registry [Runtime::Registry] the booted registry to check
|
|
58
|
+
# @param domain [String, Symbol] the domain name to check every aggregate of
|
|
59
|
+
# @return [void]
|
|
60
|
+
# @raise [Runtime::WiringError] if any aggregate in `domain` is bound to an adapter
|
|
61
|
+
# that is not `tenant_capable?`
|
|
51
62
|
def refuse_unless_tenant_capable!(registry, domain)
|
|
52
63
|
bluebook = registry.bluebook(domain)
|
|
53
64
|
return unless bluebook
|
|
@@ -67,12 +78,21 @@ module Hecks
|
|
|
67
78
|
"or keep #{domain} single-tenant."
|
|
68
79
|
end
|
|
69
80
|
|
|
70
|
-
#
|
|
81
|
+
# Answers whether `adapter_name`'s Ruby implementation keeps two
|
|
82
|
+
# tenants' boots of the same directory from sharing data.
|
|
83
|
+
#
|
|
84
|
+
# The capability idiom itself — an adapter class that answers
|
|
71
85
|
# tenant_capable? with true keeps two boots' data apart by
|
|
72
86
|
# construction (Memory) or by an explicit per-boot isolation
|
|
73
87
|
# setting (PostgresEra's schema:). Same defensive shape
|
|
74
88
|
# EraCheck#lineage_capable? already uses: a class that doesn't
|
|
75
89
|
# respond at all is false, not an error.
|
|
90
|
+
#
|
|
91
|
+
# @param registry [Runtime::Registry] the booted registry the adapter is wired into
|
|
92
|
+
# @param adapter_name [String] the adapter's declared name, such as `"PostgresEra"`
|
|
93
|
+
# @return [Boolean] true when the adapter is registered, has a Ruby implementation,
|
|
94
|
+
# and answers `tenant_capable?` true; false for any other case, including a
|
|
95
|
+
# missing adapter or one whose lookup raises
|
|
76
96
|
def tenant_capable?(registry, adapter_name)
|
|
77
97
|
adapter_class = registry.adapters[adapter_name] && registry.adapter_class(adapter_name)
|
|
78
98
|
adapter_class.respond_to?(:tenant_capable?) && adapter_class.tenant_capable?
|
|
@@ -7,9 +7,9 @@ module Hecks
|
|
|
7
7
|
module Runtime
|
|
8
8
|
# `authorize policy, tenant: :field` declared a tenant boundary that
|
|
9
9
|
# nothing enforced — the policy name and the field were stored and read
|
|
10
|
-
# by nothing at dispatch time. This is the half that
|
|
10
|
+
# by nothing at dispatch time. This is the half that can be enforced
|
|
11
11
|
# without a caller-identity/session system: the boundary itself, made
|
|
12
|
-
# mandatory. Whether
|
|
12
|
+
# mandatory. Whether this caller actually holds `policy` for this
|
|
13
13
|
# tenant needs real identity infrastructure this runtime does not have
|
|
14
14
|
# — that stays a named, open gap, not something this quietly pretends
|
|
15
15
|
# to answer.
|
|
@@ -22,21 +22,31 @@ module Hecks
|
|
|
22
22
|
# free, with no per-engine code and no way for one engine to forget it.
|
|
23
23
|
# `Scoped` is handed only to those engines as their `declared`/
|
|
24
24
|
# `specification` argument — never returned to a caller that might call
|
|
25
|
-
# an IR-level method (`filtered_head_name`, `to_h`, …) whose
|
|
25
|
+
# an IR-level method (`filtered_head_name`, `to_h`, …) whose own
|
|
26
26
|
# internal `wheres` call would resolve against the original object, not
|
|
27
27
|
# this override, since `SimpleDelegator` only intercepts calls made
|
|
28
28
|
# directly on the wrapper.
|
|
29
29
|
module TenantScope
|
|
30
30
|
module_function
|
|
31
31
|
|
|
32
|
+
# Wraps a declared query/read-model spec with its tenant boundary clause, if it has one.
|
|
33
|
+
#
|
|
34
|
+
# @param declared [Bluebook::Query, Bluebook::ReadModel] the declared specification to
|
|
35
|
+
# scope
|
|
36
|
+
# @param args [Hash] the query's arguments, checked for the declared tenant field
|
|
37
|
+
# @return [Bluebook::Query, Bluebook::ReadModel, Runtime::TenantScope::Scoped]
|
|
38
|
+
# `declared` unchanged when it declares no `authorize policy, tenant:`; otherwise a
|
|
39
|
+
# `Scoped` wrapper whose `#wheres` adds the tenant `eq` clause
|
|
40
|
+
# @raise [Runtime::Unauthorized] if `declared` declares a tenant boundary and `args`
|
|
41
|
+
# omits that field
|
|
32
42
|
def apply(declared, args)
|
|
33
43
|
tenant = declared.authorization&.tenant
|
|
34
44
|
return declared unless tenant
|
|
35
45
|
|
|
36
46
|
tenant = tenant.to_sym
|
|
37
47
|
unless args.key?(tenant)
|
|
38
|
-
raise Unauthorized, RefusalWording.
|
|
39
|
-
|
|
48
|
+
raise Unauthorized, RefusalWording.render_site("Unauthorized", "tenant_required",
|
|
49
|
+
query: declared.name, field: tenant)
|
|
40
50
|
end
|
|
41
51
|
|
|
42
52
|
Scoped.new(declared, QuerySpecification::Common::WhereClause.new(field: tenant, op: "eq", value: tenant))
|
|
@@ -50,11 +60,19 @@ module Hecks
|
|
|
50
60
|
# own internal `.wheres` read would bypass this override (see this
|
|
51
61
|
# module's own header for why).
|
|
52
62
|
class Scoped < SimpleDelegator
|
|
63
|
+
# @param declared [Bluebook::Query, Bluebook::ReadModel] the specification to wrap,
|
|
64
|
+
# delegated to for everything but `#wheres`
|
|
65
|
+
# @param clause [QuerySpecification::Common::WhereClause] the synthetic tenant `eq`
|
|
66
|
+
# clause to append
|
|
53
67
|
def initialize(declared, clause)
|
|
54
68
|
super(declared)
|
|
55
69
|
@clause = clause
|
|
56
70
|
end
|
|
57
71
|
|
|
72
|
+
# Reads the wrapped specification's where-clauses, with the tenant clause appended.
|
|
73
|
+
#
|
|
74
|
+
# @return [Array<QuerySpecification::Common::WhereClause>] `declared.wheres` with the
|
|
75
|
+
# tenant clause appended
|
|
58
76
|
def wheres = __getobj__.wheres + [@clause]
|
|
59
77
|
end
|
|
60
78
|
end
|