hecks 0.2.0 → 1.0.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 +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +427 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +179 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +145 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +438 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +306 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +54 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +125 -0
- data/lib/hecks/behaviors/expectations.rb +340 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
- data/lib/hecks/bluebook/assembly/marks.rb +228 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +91 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +78 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
- data/lib/hecks/bluebook/expression/projection.json +218 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +781 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +542 -0
- data/lib/hecks/bluebook/model_check.rb +445 -0
- data/lib/hecks/bluebook/pattern_subset.rb +184 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +128 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +66 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +410 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +119 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +166 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +341 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +177 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +84 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +161 -0
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +119 -0
- data/lib/hecks/forms/record_table.rb +68 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
- data/lib/hecks/fuzzing/properties.rb +1192 -0
- data/lib/hecks/fuzzing/replay.rb +668 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
- data/lib/hecks/fuzzing/value_generator.rb +200 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +324 -0
- data/lib/hecks/grammar/expression.bluebook +420 -0
- data/lib/hecks/grammar/expression_operators.json +1648 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +471 -0
- data/lib/hecks/language/bluebook/entity.bluebook +392 -0
- data/lib/hecks/language/bluebook/policy.bluebook +189 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +333 -0
- data/lib/hecks/language/port.bluebook +120 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +174 -0
- data/lib/hecks/ports/access_control.port +9 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +8 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +6 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +7 -0
- data/lib/hecks/ports/authorization.rb +62 -0
- data/lib/hecks/ports/clock.port +5 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +5 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +5 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +5 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +5 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +138 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +23 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +96 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +642 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +44 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +446 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +158 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +411 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +92 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +107 -0
- data/lib/hecks/query_specification/hop_path.rb +132 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +66 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
- data/lib/hecks/runtime/command_interpreter.rb +497 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +189 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +245 -0
- data/lib/hecks/runtime/dispatcher.rb +301 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +299 -0
- data/lib/hecks/runtime/errors.rb +122 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +156 -0
- data/lib/hecks/runtime/instance.rb +175 -0
- data/lib/hecks/runtime/interpreting.rb +90 -0
- data/lib/hecks/runtime/loader.rb +184 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +286 -0
- data/lib/hecks/runtime/reaction_invocation.rb +253 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +126 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
- data/lib/hecks/runtime/registry/verification.rb +246 -0
- data/lib/hecks/runtime/registry.rb +279 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +503 -0
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +130 -0
- data/lib/hecks/runtime/value/coercion.rb +565 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +107 -0
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +218 -0
- data/lib/hecks.rb +123 -6
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +412 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "saga_interpreter/correlation"
|
|
3
|
+
require_relative "../bluebook/process_manager"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "reaction_invocation"
|
|
6
|
+
require_relative "value"
|
|
7
|
+
require_relative "saga_pending_dispatch"
|
|
8
|
+
|
|
9
|
+
module Hecks
|
|
10
|
+
module Runtime
|
|
11
|
+
class SagaInterpreter
|
|
12
|
+
include Correlation
|
|
13
|
+
|
|
14
|
+
# The trigger lives on the declaration it triggers, not on the runtime that
|
|
15
|
+
# notices it — see ProcessManager::REFUSED.
|
|
16
|
+
REFUSED = Bluebook::ProcessManager::REFUSED
|
|
17
|
+
|
|
18
|
+
# A crash gets this many extra attempts before the procedure gives up
|
|
19
|
+
# and treats it as something to compensate for — see
|
|
20
|
+
# `deliver_saga_dispatch`'s own comment for why a crash isn't unwound
|
|
21
|
+
# on the first failure the way a domain refusal is.
|
|
22
|
+
MAX_DEFECT_RETRIES = 3
|
|
23
|
+
|
|
24
|
+
attr_reader :registry
|
|
25
|
+
|
|
26
|
+
def initialize(registry, door:)
|
|
27
|
+
@registry = registry
|
|
28
|
+
@door = door
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def advance(event, domain)
|
|
32
|
+
bluebook = @registry.bluebook(domain)
|
|
33
|
+
return unless bluebook
|
|
34
|
+
|
|
35
|
+
bluebook.process_managers.each do |pm|
|
|
36
|
+
begin_saga(pm, event, domain)
|
|
37
|
+
advance_saga(pm, event, domain)
|
|
38
|
+
end_saga(pm, event, domain)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# THE CHECKPOINT WRITE, shared by every mutation site below —
|
|
45
|
+
# holds `saga_mutex` across BOTH the in-memory Hash mutation and
|
|
46
|
+
# the persistence write (§7), not just the Hash mutation alone:
|
|
47
|
+
# two threads racing the SAME (process_manager, correlation) key
|
|
48
|
+
# could otherwise interleave their writes out of order, silently
|
|
49
|
+
# reordering a saga's own transition history — worse for the
|
|
50
|
+
# adapters with no locking of their own (Heki) than for Postgres.
|
|
51
|
+
# `deep_copy` guards against the exact shape of bug PR #175 itself
|
|
52
|
+
# already found once (over-freezing a live, still-mutated Hash) —
|
|
53
|
+
# never hand a persistence adapter the SAME object `advance_saga`/
|
|
54
|
+
# `unwind` go on to mutate in place; round-tripping through JSON
|
|
55
|
+
# is also what guarantees the value is safe for every adapter that
|
|
56
|
+
# itself calls `JSON.generate` on it.
|
|
57
|
+
# `pending:` — see saga_pending_dispatch.rb. Injected into the
|
|
58
|
+
# WRITTEN copy of memory only, never into `instance[:memory]`
|
|
59
|
+
# itself: every other reader of a live instance's memory
|
|
60
|
+
# (`dispatch_args`'s "opening event memory" scope, the fuzzer's
|
|
61
|
+
# own round-trip/shape checks, `saga_spec.rb`'s exact-equality
|
|
62
|
+
# assertion against a fresh instance's seeded memory) sees exactly
|
|
63
|
+
# what it always did. The marker exists ONLY in the persisted
|
|
64
|
+
# blob, and only for as long as a dispatch cascade is genuinely
|
|
65
|
+
# in flight for this instance.
|
|
66
|
+
def checkpoint(pm, correlation, instance, domain, pending: nil)
|
|
67
|
+
memory = deep_copy(instance[:memory])
|
|
68
|
+
memory[SAGA_PENDING_DISPATCH_KEY] = pending if pending
|
|
69
|
+
@registry.saga_persistence(domain).save_saga(
|
|
70
|
+
process_manager: pm.name, correlation: correlation,
|
|
71
|
+
state: instance[:state], memory: memory,
|
|
72
|
+
completed_compensations: deep_copy_array(instance[:completed_compensations])
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# `deep_copy` is `JSON.parse(JSON.generate(hash), ...)`, which
|
|
77
|
+
# only accepts an OBJECT at the top level — `completed_compensations`
|
|
78
|
+
# is an ARRAY, so it gets its own wrap-and-unwrap rather than a
|
|
79
|
+
# second, parallel `deep_copy_array` reimplementing the same
|
|
80
|
+
# round-trip. `|| []` — an instance from before this field existed
|
|
81
|
+
# (or one that has never completed a compensable leg) rehydrates
|
|
82
|
+
# to an empty ledger, never nil.
|
|
83
|
+
def deep_copy_array(array) = deep_copy(list: array || [])[:list]
|
|
84
|
+
|
|
85
|
+
def deep_copy(hash) = JSON.parse(JSON.generate(hash), symbolize_names: true)
|
|
86
|
+
|
|
87
|
+
def begin_saga(pm, event, domain)
|
|
88
|
+
return unless event.name == pm.starts_on
|
|
89
|
+
|
|
90
|
+
correlation = saga_correlation(pm, event)
|
|
91
|
+
if correlation.to_s.empty?
|
|
92
|
+
@registry.saga_log << { process_manager: pm.name, on: event.name,
|
|
93
|
+
born: false, reason: "no #{pm.correlates_by} in the payload" }
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
created = @registry.saga_mutex.synchronize do
|
|
98
|
+
next false if @registry.saga_instances[pm.name].key?(correlation)
|
|
99
|
+
|
|
100
|
+
# `.dup`, NOT THE SAME OBJECT — a fresh saga's own memory starts
|
|
101
|
+
# as a COPY of the starting event's own payload, never the
|
|
102
|
+
# payload itself. A saga's own memory is meant to be written
|
|
103
|
+
# into over its lifetime (remember-style, growing beyond what
|
|
104
|
+
# the starting event carried) ; the payload it was seeded from
|
|
105
|
+
# is a fact about something that ALREADY happened, logged and
|
|
106
|
+
# emitted before the saga ever saw it. Sharing the one Hash
|
|
107
|
+
# object between them means a write into the saga's own memory
|
|
108
|
+
# is silently ALSO a write into an already-emitted event's own
|
|
109
|
+
# payload — retroactively adding a field nothing announced.
|
|
110
|
+
# `event.payload` is deep-frozen by `Event#emit!` by the time
|
|
111
|
+
# this runs, so a naive in-place write here would raise
|
|
112
|
+
# FrozenError rather than corrupt silently — but the `.dup`
|
|
113
|
+
# still matters: it is what makes the saga's own memory a
|
|
114
|
+
# normal, writable Hash of its own, rather than one write away
|
|
115
|
+
# from crashing every future in-place `remember`.
|
|
116
|
+
instance = { state: pm.states.first, memory: event.payload.dup, completed_compensations: [] }
|
|
117
|
+
@registry.saga_instances[pm.name][correlation] = instance
|
|
118
|
+
checkpoint(pm, correlation, instance, domain)
|
|
119
|
+
true
|
|
120
|
+
end
|
|
121
|
+
return unless created
|
|
122
|
+
|
|
123
|
+
@registry.saga_log << { process_manager: pm.name, on: event.name,
|
|
124
|
+
instance: correlation, born: true, state: pm.states.first }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# THE MUTEX COVERS ONLY THE STATE-CHECK-AND-MUTATE-AND-CHECKPOINT
|
|
128
|
+
# STEP, never the dispatch cascade that follows — `deliver_saga_
|
|
129
|
+
# dispatch` calls `@door.reenter`, which can recursively re-enter
|
|
130
|
+
# THIS SAME interpreter (a saga's own leg triggering another saga,
|
|
131
|
+
# or itself again) on the SAME thread, and `Mutex` is not
|
|
132
|
+
# reentrant: holding it across that call would deadlock the
|
|
133
|
+
# thread against itself the moment any real chain did that.
|
|
134
|
+
def advance_saga(pm, event, domain)
|
|
135
|
+
handler = pm.handler_for(event.name)
|
|
136
|
+
return unless handler
|
|
137
|
+
|
|
138
|
+
correlation = saga_correlation(pm, event)
|
|
139
|
+
return if correlation.to_s.empty?
|
|
140
|
+
|
|
141
|
+
record = { process_manager: pm.name, on: event.name, instance: correlation }
|
|
142
|
+
instance = nil
|
|
143
|
+
pre_state = nil
|
|
144
|
+
|
|
145
|
+
advanced = @registry.saga_mutex.synchronize do
|
|
146
|
+
instance = @registry.saga_instances[pm.name][correlation]
|
|
147
|
+
unless instance
|
|
148
|
+
@registry.saga_log << record.merge(advanced: false, reason: "no conversation remembers #{correlation.inspect}")
|
|
149
|
+
next false
|
|
150
|
+
end
|
|
151
|
+
unless instance[:state] == handler.from_state
|
|
152
|
+
@registry.saga_log << record.merge(advanced: false,
|
|
153
|
+
reason: "in #{instance[:state].inspect}, not #{handler.from_state.inspect}")
|
|
154
|
+
next false
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
pre_state = instance[:state]
|
|
158
|
+
instance[:state] = handler.to_state
|
|
159
|
+
checkpoint(pm, correlation, instance, domain,
|
|
160
|
+
pending: pending_marker(event, handler, pre_state, instance[:state]))
|
|
161
|
+
true
|
|
162
|
+
end
|
|
163
|
+
return unless advanced
|
|
164
|
+
|
|
165
|
+
settle_transition(pm, event, handler, instance, correlation, domain, record, pre_state)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def pending_marker(event, handler, from_state, to_state)
|
|
169
|
+
{ on: event.name, from: from_state, to: to_state, dispatches: handler.dispatches.map(&:command_name) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# THE SHARED TAIL of `advance_saga` and `unwind` — both are "guard,
|
|
173
|
+
# mutate, checkpoint-with-pending" under the mutex (kept separate
|
|
174
|
+
# per caller: `advance_saga`'s own guard also has to handle "no
|
|
175
|
+
# instance at all", `unwind`'s doesn't), then this: log the real
|
|
176
|
+
# observed transition, run the leg's dispatches, and clear the
|
|
177
|
+
# pending marker once that cascade — however it ended — is done.
|
|
178
|
+
def settle_transition(pm, event, handler, instance, correlation, domain, record, pre_state, drain_compensations: false)
|
|
179
|
+
# `from:`/`to:` are the INSTANCE'S OWN real pre/post state — read
|
|
180
|
+
# back from `instance` itself, never re-derived from `handler.
|
|
181
|
+
# from_state`/`handler.to_state` a second time. `Properties.saga_
|
|
182
|
+
# advances_follow_declared_handlers` (fuzzing/properties.rb) builds
|
|
183
|
+
# its OWN "declared edges" list from this SAME handler object (via
|
|
184
|
+
# `pm.handlers`), so a log entry that just echoed `handler.
|
|
185
|
+
# from_state`/`handler.to_state` back could never disagree with
|
|
186
|
+
# that list no matter what the runtime actually did — the entry
|
|
187
|
+
# and the thing it's checked against would be the identical fact,
|
|
188
|
+
# read twice. Logging the instance's own observed state instead
|
|
189
|
+
# means a future defect that moves an instance somewhere its own
|
|
190
|
+
# declared handler didn't say (a stale handler reference, the
|
|
191
|
+
# wrong handler picked, a second racing mutation) shows up as a
|
|
192
|
+
# real mismatch instead of vanishing into a tautology.
|
|
193
|
+
@registry.saga_log << record.merge(advanced: true, from: pre_state, to: instance[:state])
|
|
194
|
+
|
|
195
|
+
# DERIVED COMPENSATION FIRST, NEWEST-FIRST — only for `unwind`'s
|
|
196
|
+
# own call (`drain_compensations: true`): every leg THIS INSTANCE
|
|
197
|
+
# actually completed that declared its own `compensates`, popped
|
|
198
|
+
# and dispatched in reverse completion order, BEFORE any
|
|
199
|
+
# hand-written `on :refused` dispatches below — coexistence, not
|
|
200
|
+
# replacement. Drained (not just read) as it fires: a saga's own
|
|
201
|
+
# `on :refused` handler is guarded against re-entry by `unwind`'s
|
|
202
|
+
# own `instance[:state] == handler.from_state` check, so this can
|
|
203
|
+
# only ever run once per refusal — but draining rather than
|
|
204
|
+
# leaving the ledger populated is what makes that true by
|
|
205
|
+
# construction too, not only by the state guard.
|
|
206
|
+
if drain_compensations
|
|
207
|
+
compensations = instance[:completed_compensations] || []
|
|
208
|
+
deliver_derived_compensation(pm, compensations.pop, correlation, domain) until compensations.empty?
|
|
209
|
+
checkpoint(pm, correlation, instance, domain)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
handler.dispatches.each do |spec|
|
|
213
|
+
deliver_saga_dispatch(pm, spec, event, instance, correlation, domain)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# THE CLEAR — guarded by the SAME identity check `end_saga`'s own
|
|
217
|
+
# `.delete` return value implies: `deliver_saga_dispatch`'s
|
|
218
|
+
# `@door.reenter` can synchronously trigger this SAME correlation's
|
|
219
|
+
# `ends_on` event as a nested reaction (a leg's own dispatch is
|
|
220
|
+
# what makes the saga's terminal event fire), which deletes this
|
|
221
|
+
# row from the store before this line ever runs. Writing the
|
|
222
|
+
# clear unconditionally would RESURRECT a legitimately-ended saga
|
|
223
|
+
# — this diff's own first attempt did exactly that, caught by
|
|
224
|
+
# `saga_durability_spec.rb`'s "deletes the checkpoint once a saga
|
|
225
|
+
# genuinely ends" — so this only re-checkpoints when `instance`
|
|
226
|
+
# is still THE SAME object `@saga_instances` holds for this
|
|
227
|
+
# correlation (`.equal?`, not `==`: a fresh saga reborn under the
|
|
228
|
+
# same correlation between then and now is a DIFFERENT instance,
|
|
229
|
+
# and writing this stale one's state onto that one's row would be
|
|
230
|
+
# its own corruption). Under the mutex — dispatching is over by
|
|
231
|
+
# now, so this is not the reentrancy hazard `advance_saga`'s own
|
|
232
|
+
# comment warns about.
|
|
233
|
+
@registry.saga_mutex.synchronize do
|
|
234
|
+
next unless @registry.saga_instances[pm.name][correlation].equal?(instance)
|
|
235
|
+
|
|
236
|
+
checkpoint(pm, correlation, instance, domain, pending: nil)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def deliver_saga_dispatch(pm, spec, event, instance, correlation, domain)
|
|
241
|
+
args = dispatch_args(pm, spec, event, instance, correlation)
|
|
242
|
+
record = { process_manager: pm.name, instance: correlation, dispatch: spec.command_name }
|
|
243
|
+
|
|
244
|
+
# THE RAW INPUTS `args` WAS RESOLVED FROM, captured alongside the
|
|
245
|
+
# result — never re-derived from history[:saga_instances] later
|
|
246
|
+
# (that only ever holds the FINAL memory, after every step has
|
|
247
|
+
# run; this dispatch's own memory, at the moment it actually
|
|
248
|
+
# fired, is a different fact for a saga whose memory keeps
|
|
249
|
+
# changing). `spec.with_spec.empty?` skipped: nothing declared
|
|
250
|
+
# to check, a tautological pass, the same reason a command with
|
|
251
|
+
# no givens/from is skipped by lifecycle_guard_and_given_
|
|
252
|
+
# violations_are_refused.
|
|
253
|
+
unless spec.with_spec.to_a.empty?
|
|
254
|
+
@registry.saga_dispatch_log << { process_manager: pm.name, instance: correlation, dispatch: spec.command_name,
|
|
255
|
+
on: event.name, correlation_head: pm.correlation_head,
|
|
256
|
+
event_payload: event.payload, memory: Value.materialize(instance[:memory]),
|
|
257
|
+
with_spec: spec.with_spec, args: args }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
if @door.reaction_depth_reached?
|
|
261
|
+
# THE CEILING IS NOT A DOMAIN DECISION EITHER — same reasoning as a
|
|
262
|
+
# crash, below — but unlike a crash there is nothing ambiguous
|
|
263
|
+
# about it: the leg unambiguously did not run, so it unwinds
|
|
264
|
+
# exactly like a refusal instead of stranding the instance for a
|
|
265
|
+
# human to notice. `unwind`'s own state guard (it moves to its
|
|
266
|
+
# `to_state` before its dispatches run) is what keeps this from
|
|
267
|
+
# looping if the ceiling is still in effect when the compensating
|
|
268
|
+
# leg tries to dispatch — that leg's own attempt hits this same
|
|
269
|
+
# branch, calls `unwind` again, and finds the instance already
|
|
270
|
+
# past `from_state`.
|
|
271
|
+
@registry.saga_log << record.merge(delivered: false,
|
|
272
|
+
reason: "reaction depth #{@door.max_reaction_depth} reached")
|
|
273
|
+
unwind(pm, event, instance, correlation, domain)
|
|
274
|
+
return
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
attempt = 0
|
|
278
|
+
compensation_recorded = false
|
|
279
|
+
begin
|
|
280
|
+
# RECORDED BEFORE DISPATCHING, not after `@door.reenter`
|
|
281
|
+
# returns — `@door.reenter` can recursively RE-ENTER THIS SAME
|
|
282
|
+
# saga interpreter (the event THIS dispatch emits triggers a
|
|
283
|
+
# LATER handler, which can itself refuse and unwind) entirely
|
|
284
|
+
# WITHIN this one call, before it ever returns here. Recording
|
|
285
|
+
# "after reenter succeeds" would be too late for a NESTED
|
|
286
|
+
# refusal to ever see this leg's own compensation — found
|
|
287
|
+
# live: Settlement's own AccountDebited handler refuses
|
|
288
|
+
# Account.Credit and unwinds from INSIDE Account.Debit's own
|
|
289
|
+
# `reenter` call, so "delivered: true, then record" left the
|
|
290
|
+
# ledger empty at the exact moment it was needed. Popped back
|
|
291
|
+
# off in the rescues below if THIS leg's own attempt is the
|
|
292
|
+
# one that failed — never left recorded for a refusal that
|
|
293
|
+
# was never this leg's own to compensate.
|
|
294
|
+
if spec.compensates && !compensation_recorded
|
|
295
|
+
resolved = dispatch_args(pm, spec.compensates, event, instance, correlation)
|
|
296
|
+
instance[:completed_compensations] << { command_name: spec.compensates.command_name, args: resolved }
|
|
297
|
+
checkpoint(pm, correlation, instance, domain)
|
|
298
|
+
compensation_recorded = true
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
invocation = ReactionInvocation.build(
|
|
302
|
+
registry: @registry,
|
|
303
|
+
verb: qualified(spec.command_name, domain),
|
|
304
|
+
projected: args,
|
|
305
|
+
explicit: ReactionInvocation.projection_declared?(spec),
|
|
306
|
+
passthrough: [pm.correlation_head],
|
|
307
|
+
source_receiver: { aggregate: event.aggregate, identity: event.id }
|
|
308
|
+
)
|
|
309
|
+
@door.reenter(qualified(spec.command_name, domain),
|
|
310
|
+
saga_correlation: { pm.correlation_head.to_s => correlation }, **invocation)
|
|
311
|
+
@registry.saga_log << record.merge(delivered: true)
|
|
312
|
+
rescue *DOMAIN_REFUSALS => error
|
|
313
|
+
unrecord_compensation(instance, correlation, domain, pm) if compensation_recorded
|
|
314
|
+
# Same rule as the policy interpreter : a refusal by the target is
|
|
315
|
+
# a recorded outcome, and the leg that raised it UNWINDS — see
|
|
316
|
+
# `unwind`'s own comment for why the procedure runs its
|
|
317
|
+
# compensation here rather than leaving the money (or whatever
|
|
318
|
+
# else a leg moved) sitting out.
|
|
319
|
+
@registry.saga_log << record.merge(delivered: false, reason: error.message)
|
|
320
|
+
unwind(pm, event, instance, correlation, domain)
|
|
321
|
+
rescue StandardError => error
|
|
322
|
+
unrecord_compensation(instance, correlation, domain, pm) if compensation_recorded
|
|
323
|
+
compensation_recorded = false
|
|
324
|
+
# A DEFECT, not a refusal — see PolicyInterpreter#deliver's own
|
|
325
|
+
# comment for the full reasoning: the same DOMAIN_REFUSALS split,
|
|
326
|
+
# and the same "the triggering command already succeeded and
|
|
327
|
+
# persisted by the time this runs" fact that makes catching it
|
|
328
|
+
# here safe rather than reckless.
|
|
329
|
+
#
|
|
330
|
+
# UNLIKE a refusal, a crash is not a decision the domain made, so
|
|
331
|
+
# it does not unwind on the first failure — MAX_DEFECT_RETRIES
|
|
332
|
+
# gives a transient failure (a DB timeout, a race, a cold start)
|
|
333
|
+
# a chance to clear on its own, retrying the identical dispatch,
|
|
334
|
+
# before this is treated as something to compensate for. Every
|
|
335
|
+
# attempt is recorded distinguishably (`defect: true`, the
|
|
336
|
+
# error's own class); only once retries are exhausted is it
|
|
337
|
+
# warned to STDERR and unwound — tagged `defect_compensated:
|
|
338
|
+
# true` rather than folded into an ordinary refusal's shape, so
|
|
339
|
+
# the log never misrepresents a crash as a decision the domain
|
|
340
|
+
# made. Compensating a genuinely stuck leg beats leaving it for a
|
|
341
|
+
# human to find; misrepresenting *why* it compensated is what the
|
|
342
|
+
# tag is for.
|
|
343
|
+
attempt += 1
|
|
344
|
+
if attempt <= MAX_DEFECT_RETRIES
|
|
345
|
+
@registry.saga_log << record.merge(delivered: false, reason: error.message,
|
|
346
|
+
defect: true, error_class: error.class.name,
|
|
347
|
+
attempt: attempt, retrying: true)
|
|
348
|
+
retry
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
warn "[hecks] defect in saga #{pm.name} — instance #{correlation.inspect} " \
|
|
352
|
+
"dispatching #{spec.command_name} after #{attempt} attempts: #{error.class}: #{error.message}"
|
|
353
|
+
@registry.saga_log << record.merge(delivered: false, reason: error.message, defect: true,
|
|
354
|
+
error_class: error.class.name, defect_compensated: true)
|
|
355
|
+
unwind(pm, event, instance, correlation, domain)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# THE ROLLBACK HALF of `deliver_saga_dispatch`'s own speculative
|
|
360
|
+
# pre-record (that method's own comment for why it has to be
|
|
361
|
+
# speculative) — THIS leg's own attempt is the one that failed,
|
|
362
|
+
# so whatever was just pushed for it was never actually earned.
|
|
363
|
+
# `.pop`, not a search-and-delete: nothing else can have pushed
|
|
364
|
+
# AFTER this leg's own entry without this leg's own `@door.
|
|
365
|
+
# reenter` call having already returned (the recursive re-entry
|
|
366
|
+
# this whole mechanism exists for only ever runs BETWEEN this
|
|
367
|
+
# push and this leg's own return, and a nested refusal that
|
|
368
|
+
# consumed it already popped it itself — this rollback only ever
|
|
369
|
+
# runs for THIS leg's own, still-present entry).
|
|
370
|
+
def unrecord_compensation(instance, correlation, domain, pm)
|
|
371
|
+
instance[:completed_compensations].pop
|
|
372
|
+
checkpoint(pm, correlation, instance, domain)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# A refused leg UNWINDS — the procedure runs the leg declared `on :refused`,
|
|
376
|
+
# which is where the compensation lives. So does a leg that hit the
|
|
377
|
+
# reaction-depth ceiling, and so does a leg that crashed and stayed
|
|
378
|
+
# crashing through MAX_DEFECT_RETRIES — see `deliver_saga_dispatch`'s own
|
|
379
|
+
# comments for why each of those is safe to route here.
|
|
380
|
+
#
|
|
381
|
+
# Until this existed a refusal was RECORDED and nothing else happened. The
|
|
382
|
+
# wire's thousand was taken from the source, refused by the destination, and
|
|
383
|
+
# sat nowhere until a human dispatched the reversal by hand ; banking's
|
|
384
|
+
# settlement left a debit standing with no credit and no compensation at all.
|
|
385
|
+
# Both bluebooks had written the compensating leg. Nothing armed it.
|
|
386
|
+
#
|
|
387
|
+
# A compensation that is itself refused does NOT unwind again, and needs no
|
|
388
|
+
# flag to stop it: the state moves to the compensating leg's to_state BEFORE
|
|
389
|
+
# its dispatches run, so a second refusal finds the instance no longer in
|
|
390
|
+
# from_state and records that instead. The check is the guard.
|
|
391
|
+
def unwind(pm, event, instance, correlation, domain)
|
|
392
|
+
handler = pm.handler_for(REFUSED)
|
|
393
|
+
return unless handler && instance
|
|
394
|
+
|
|
395
|
+
record = { process_manager: pm.name, on: REFUSED, instance: correlation }
|
|
396
|
+
pre_state = nil
|
|
397
|
+
|
|
398
|
+
# Same non-reentrancy reasoning as `advance_saga`'s own comment —
|
|
399
|
+
# the mutex covers only the check-and-mutate-and-checkpoint step.
|
|
400
|
+
advanced = @registry.saga_mutex.synchronize do
|
|
401
|
+
unless instance[:state] == handler.from_state
|
|
402
|
+
@registry.saga_log << record.merge(advanced: false,
|
|
403
|
+
reason: "in #{instance[:state].inspect}, not #{handler.from_state.inspect}")
|
|
404
|
+
next false
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
pre_state = instance[:state]
|
|
408
|
+
instance[:state] = handler.to_state
|
|
409
|
+
checkpoint(pm, correlation, instance, domain,
|
|
410
|
+
pending: pending_marker(event, handler, pre_state, instance[:state]))
|
|
411
|
+
true
|
|
412
|
+
end
|
|
413
|
+
return unless advanced
|
|
414
|
+
|
|
415
|
+
# See `settle_transition`'s own comment on `pre_state`/
|
|
416
|
+
# `instance[:state]` — the real observed transition, not a
|
|
417
|
+
# second read of the SAME handler object `Properties.saga_
|
|
418
|
+
# advances_follow_declared_handlers` checks this log against.
|
|
419
|
+
# `drain_compensations: true` — only `unwind`'s own call site
|
|
420
|
+
# fires derived compensation; `advance_saga`'s own call never
|
|
421
|
+
# does.
|
|
422
|
+
settle_transition(pm, event, handler, instance, correlation, domain, record, pre_state,
|
|
423
|
+
drain_compensations: true)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# A DERIVED COMPENSATION — `entry[:args]` is already resolved
|
|
427
|
+
# (`record_completed_compensation`'s own comment for why), so this
|
|
428
|
+
# skips `dispatch_args` entirely and goes straight to delivery,
|
|
429
|
+
# through the SAME retry-on-defect path an ordinary forward leg
|
|
430
|
+
# uses. Never re-enters `unwind` on its own failure — a
|
|
431
|
+
# compensation that itself refuses is a real, pre-existing gap
|
|
432
|
+
# this feature makes visible rather than closes (see this file's
|
|
433
|
+
# own class-level notes); `compensation_failed: true` tags it
|
|
434
|
+
# distinctly in the log instead of recording it identically to an
|
|
435
|
+
# ordinary failed delivery, and every OTHER completed compensation
|
|
436
|
+
# still queued still gets its own attempt.
|
|
437
|
+
def deliver_derived_compensation(pm, entry, correlation, domain)
|
|
438
|
+
record = { process_manager: pm.name, instance: correlation, dispatch: entry[:command_name] }
|
|
439
|
+
|
|
440
|
+
attempt = 0
|
|
441
|
+
begin
|
|
442
|
+
invocation = ReactionInvocation.build(
|
|
443
|
+
registry: @registry,
|
|
444
|
+
verb: qualified(entry[:command_name], domain),
|
|
445
|
+
projected: entry[:args],
|
|
446
|
+
explicit: true,
|
|
447
|
+
passthrough: [pm.correlation_head],
|
|
448
|
+
source_receiver: nil
|
|
449
|
+
)
|
|
450
|
+
@door.reenter(qualified(entry[:command_name], domain),
|
|
451
|
+
saga_correlation: { pm.correlation_head.to_s => correlation }, **invocation)
|
|
452
|
+
@registry.saga_log << record.merge(delivered: true, compensation: true)
|
|
453
|
+
rescue *DOMAIN_REFUSALS => error
|
|
454
|
+
@registry.saga_log << record.merge(delivered: false, reason: error.message, compensation: true, compensation_failed: true)
|
|
455
|
+
rescue StandardError => error
|
|
456
|
+
attempt += 1
|
|
457
|
+
if attempt <= MAX_DEFECT_RETRIES
|
|
458
|
+
@registry.saga_log << record.merge(delivered: false, reason: error.message, compensation: true,
|
|
459
|
+
defect: true, error_class: error.class.name,
|
|
460
|
+
attempt: attempt, retrying: true)
|
|
461
|
+
retry
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
warn "[hecks] defect compensating saga #{pm.name} — instance #{correlation.inspect} " \
|
|
465
|
+
"dispatching #{entry[:command_name]} after #{attempt} attempts: #{error.class}: #{error.message}"
|
|
466
|
+
@registry.saga_log << record.merge(delivered: false, reason: error.message, compensation: true,
|
|
467
|
+
defect: true, error_class: error.class.name, compensation_failed: true)
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def dispatch_args(pm, spec, event, instance, correlation)
|
|
472
|
+
ReactionInvocation.resolve_mapping(
|
|
473
|
+
with_spec: spec.with_spec,
|
|
474
|
+
scopes: [["current event payload", event.payload], ["opening event memory", instance[:memory]]],
|
|
475
|
+
bindings: { pm.correlation_head => correlation },
|
|
476
|
+
label: "#{pm.name}'s dispatch #{spec.command_name}"
|
|
477
|
+
)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def qualified(command_name, domain)
|
|
481
|
+
command_name.include?("::") ? command_name : "#{domain}::#{command_name}"
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def end_saga(pm, event, domain)
|
|
485
|
+
return unless event.name == pm.ends_on
|
|
486
|
+
|
|
487
|
+
correlation = saga_correlation(pm, event)
|
|
488
|
+
return if correlation.to_s.empty?
|
|
489
|
+
|
|
490
|
+
ended = @registry.saga_mutex.synchronize do
|
|
491
|
+
next false unless @registry.saga_instances[pm.name].delete(correlation)
|
|
492
|
+
|
|
493
|
+
@registry.saga_persistence(domain).delete_saga(process_manager: pm.name, correlation: correlation)
|
|
494
|
+
true
|
|
495
|
+
end
|
|
496
|
+
return unless ended
|
|
497
|
+
|
|
498
|
+
@registry.saga_log << { process_manager: pm.name, on: event.name,
|
|
499
|
+
instance: correlation, ended: true }
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Runtime
|
|
3
|
+
# THE ONE SHARED CONSTANT between `SagaInterpreter` (the writer) and
|
|
4
|
+
# `Registry::SagaPersistence#rehydrate_sagas!` (the reader) for a
|
|
5
|
+
# scoped, minimal answer to the saga-durability review's item 8 (a
|
|
6
|
+
# durable outbox): a marker that survives exactly the window a crash
|
|
7
|
+
# in `advance_saga`/`unwind` can otherwise hide.
|
|
8
|
+
#
|
|
9
|
+
# THE PROBLEM THIS CLOSES — `checkpoint` persists a saga's new state
|
|
10
|
+
# BEFORE the leg that justifies it (`handler.dispatches`) runs, and
|
|
11
|
+
# deliberately so: the mutex it holds is not reentrant, and a
|
|
12
|
+
# dispatch can re-enter this same interpreter. If the process dies
|
|
13
|
+
# in that window, the store says the transition happened and there
|
|
14
|
+
# is no record that its dispatch(es) never ran — not a refusal (the
|
|
15
|
+
# domain never got asked), not a defect (nothing raised), just
|
|
16
|
+
# silence indistinguishable from a leg that finished cleanly.
|
|
17
|
+
#
|
|
18
|
+
# THE FIX — `checkpoint` now writes this key into the SAME already-
|
|
19
|
+
# durable `memory` blob (no new column, no adapter/schema change:
|
|
20
|
+
# `memory` is already an opaque, adapter-agnostic JSON blob every
|
|
21
|
+
# `save_saga` implementation round-trips verbatim) whenever it
|
|
22
|
+
# checkpoints a state a dispatch cascade hasn't run for YET, and
|
|
23
|
+
# clears it (a second checkpoint, `pending: nil`) once that cascade
|
|
24
|
+
# — success, refusal-compensated, defect-compensated, or ceiling-
|
|
25
|
+
# compensated — has actually run. A crash between those two writes
|
|
26
|
+
# leaves the marker standing; `rehydrate_sagas!` strips it back out
|
|
27
|
+
# of the LIVE instance's own `:memory` (so no dispatch/`given`/
|
|
28
|
+
# fuzzer/doc consumer of a saga's memory ever sees this key — it
|
|
29
|
+
# exists only in the persisted blob) and surfaces it loudly instead.
|
|
30
|
+
#
|
|
31
|
+
# WHAT THIS DELIBERATELY DOES NOT DO — auto-redrive the pending leg.
|
|
32
|
+
# Redelivering a dispatch whose outcome is genuinely unknown is only
|
|
33
|
+
# safe with idempotent delivery (the downstream command recognizing
|
|
34
|
+
# and no-op'ing a duplicate), which hecks's command/event pipeline
|
|
35
|
+
# has no mechanism for today. Blindly re-dispatching without that is
|
|
36
|
+
# how a stalled transfer becomes a DOUBLE-CREDITED one — a strictly
|
|
37
|
+
# worse defect than the stall it would replace. So this is real,
|
|
38
|
+
# durable, crash-surviving VISIBILITY into exactly what a stalled
|
|
39
|
+
# saga was doing when the process died — the missing half of "no
|
|
40
|
+
# reconciliation pass exists" — not the full pending → claimed →
|
|
41
|
+
# delivered outbox `future-features.md` still lists as unbuilt, and
|
|
42
|
+
# not a substitute for it.
|
|
43
|
+
SAGA_PENDING_DISPATCH_KEY = :__hecks_saga_pending_dispatch__
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require_relative "../ports/persistence/binding_policy"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Runtime
|
|
5
|
+
# The capability idiom for MULTI-TENANT hosting — mirrors EraCheck's
|
|
6
|
+
# own `lineage_capable?`, one level over. An adapter answers
|
|
7
|
+
# `tenant_capable?` with true when its OWN instances genuinely
|
|
8
|
+
# isolate one boot's data from another's, given each tenant is its
|
|
9
|
+
# own separate `Runtime.boot` call (its own Registry, its own
|
|
10
|
+
# Dispatcher, its own adapter instances) rather than one shared
|
|
11
|
+
# process switching connections mid-dispatch.
|
|
12
|
+
#
|
|
13
|
+
# THAT LAST PART IS THE FINDING THIS MODULE ENCODES. The project
|
|
14
|
+
# register (Bluebook::ProjectRegister) already resolves an address's
|
|
15
|
+
# REALM to a DISPATCHER at registration time — Router#resolve looks
|
|
16
|
+
# the FQN up in one flat table keyed by realm::domain::aggregate.verb,
|
|
17
|
+
# and each entry already carries its OWN dispatcher from its OWN
|
|
18
|
+
# boot. So "which tenant" is decided ONCE, at boot/registration time
|
|
19
|
+
# (which of possibly many boots of the same directory a request's
|
|
20
|
+
# realm resolves to), never per-dispatch inside a shared registry.
|
|
21
|
+
# No ambient thread-local "current tenant" is needed, and no
|
|
22
|
+
# connection cache is needed beyond what booting-once-per-tenant
|
|
23
|
+
# already gives for free — each tenant's own PostgresEra instance
|
|
24
|
+
# IS its own connection, held for the life of that boot.
|
|
25
|
+
#
|
|
26
|
+
# So `tenant_capable?` asks a narrower question than it might sound:
|
|
27
|
+
# not "can this adapter switch tenants," but "does booting this
|
|
28
|
+
# adapter twice, with different settings, for the same directory,
|
|
29
|
+
# actually keep the two boots' data apart." Memory answers true
|
|
30
|
+
# trivially — a `@records` Hash is a plain instance variable, and
|
|
31
|
+
# two `Runtime.boot` calls build two entirely separate Registry
|
|
32
|
+
# objects, so two Memory adapter instances never share state by
|
|
33
|
+
# construction. PostgresEra answers true because its own `schema:`
|
|
34
|
+
# setting (already built, already the Storehouse mechanism) puts
|
|
35
|
+
# each boot's tables in their own Postgres schema via `SET
|
|
36
|
+
# search_path` — proven for real, not assumed, by
|
|
37
|
+
# tenant_isolation_spec.rb. Plain Postgres (no schema story) and D1
|
|
38
|
+
# (no schema-equivalent at all — see world.bluebook's own comment on
|
|
39
|
+
# the lifeadelics D1 tradeoff) answer false, or don't answer at all,
|
|
40
|
+
# which this module treats identically to false.
|
|
41
|
+
module TenantCheck
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# A domain is safe to boot for MORE THAN ONE TENANT only if every
|
|
45
|
+
# aggregate's resolved persistence adapter is tenant_capable? — one
|
|
46
|
+
# ungoverned adapter sharing state across two tenant boots is a
|
|
47
|
+
# real data leak, not a theoretical one, so this is checked before
|
|
48
|
+
# a second tenant boot of the same directory is trusted, the same
|
|
49
|
+
# severity EraCheck/refuse_ungoverned_roles! already hold their
|
|
50
|
+
# own gates to.
|
|
51
|
+
def refuse_unless_tenant_capable!(registry, domain)
|
|
52
|
+
bluebook = registry.bluebook(domain)
|
|
53
|
+
return unless bluebook
|
|
54
|
+
|
|
55
|
+
offender = bluebook.aggregates.find do |aggregate|
|
|
56
|
+
adapter_name = Ports::Persistence::BindingPolicy.resolve(registry, domain, aggregate).adapter
|
|
57
|
+
!tenant_capable?(registry, adapter_name)
|
|
58
|
+
end
|
|
59
|
+
return unless offender
|
|
60
|
+
|
|
61
|
+
adapter_name = Ports::Persistence::BindingPolicy.resolve(registry, domain, offender).adapter
|
|
62
|
+
raise WiringError,
|
|
63
|
+
"#{domain}::#{offender.hecks_name} is bound to #{adapter_name}, which is not " \
|
|
64
|
+
"tenant_capable? — booting #{domain} for more than one tenant would share " \
|
|
65
|
+
"#{adapter_name}'s own storage across tenants that must never see each other's data. " \
|
|
66
|
+
"Bind a tenant-capable adapter (Memory, PostgresEra with its own schema: per tenant), " \
|
|
67
|
+
"or keep #{domain} single-tenant."
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The capability idiom itself — an adapter CLASS that answers
|
|
71
|
+
# tenant_capable? with true keeps two boots' data apart by
|
|
72
|
+
# construction (Memory) or by an explicit per-boot isolation
|
|
73
|
+
# setting (PostgresEra's schema:). Same defensive shape
|
|
74
|
+
# EraCheck#lineage_capable? already uses: a class that doesn't
|
|
75
|
+
# respond at all is false, not an error.
|
|
76
|
+
def tenant_capable?(registry, adapter_name)
|
|
77
|
+
adapter_class = registry.adapters[adapter_name] && registry.adapter_class(adapter_name)
|
|
78
|
+
adapter_class.respond_to?(:tenant_capable?) && adapter_class.tenant_capable?
|
|
79
|
+
rescue StandardError
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|