hecks 0.2.0 → 0.3.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 +403 -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 +62 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +148 -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 +131 -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 +80 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +391 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
- data/lib/hecks/adapters/driven/postgres_era.rb +671 -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 +63 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +285 -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 +36 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +96 -0
- data/lib/hecks/behaviors/expectations.rb +280 -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 +409 -0
- data/lib/hecks/bluebook/assembly/marks.rb +227 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +78 -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 +51 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -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 +68 -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 +906 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +626 -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 +305 -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 +120 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -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 +172 -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 +221 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +77 -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 +55 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
- data/lib/hecks/bluebook/expression/projection.json +170 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +630 -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 +559 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -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 +452 -0
- data/lib/hecks/bluebook/model_check.rb +318 -0
- data/lib/hecks/bluebook/pattern_subset.rb +120 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +86 -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 +58 -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 +389 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +116 -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 +154 -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 +300 -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 +170 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +53 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +132 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +115 -0
- data/lib/hecks/forms/record_table.rb +63 -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 +130 -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/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
- data/lib/hecks/fuzzing/properties.rb +1146 -0
- data/lib/hecks/fuzzing/replay.rb +635 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
- data/lib/hecks/fuzzing/value_generator.rb +177 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +291 -0
- data/lib/hecks/grammar/expression.bluebook +417 -0
- data/lib/hecks/grammar/expression_operators.json +1152 -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 +437 -0
- data/lib/hecks/language/bluebook/entity.bluebook +316 -0
- data/lib/hecks/language/bluebook/policy.bluebook +172 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +264 -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 +447 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -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 +313 -0
- data/lib/hecks/language/port.bluebook +92 -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 +121 -0
- data/lib/hecks/ports/access_control.port +4 -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 +4 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +4 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +4 -0
- data/lib/hecks/ports/authorization.rb +59 -0
- data/lib/hecks/ports/clock.port +4 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +4 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +4 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +4 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +4 -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 +102 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +18 -0
- data/lib/hecks/ports/persistence/lineage.rb +277 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -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 +76 -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 +413 -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 +31 -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 +437 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +141 -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 +392 -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 +80 -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 +89 -0
- data/lib/hecks/query_specification/hop_path.rb +130 -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/caller.rb +50 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
- data/lib/hecks/runtime/command_interpreter.rb +413 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +181 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +231 -0
- data/lib/hecks/runtime/dispatcher.rb +286 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +228 -0
- data/lib/hecks/runtime/era_check.rb +136 -0
- data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/runtime/era_guard.rb +193 -0
- data/lib/hecks/runtime/era_tamper.rb +61 -0
- data/lib/hecks/runtime/errors.rb +100 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +128 -0
- data/lib/hecks/runtime/instance.rb +120 -0
- data/lib/hecks/runtime/interpreting.rb +69 -0
- data/lib/hecks/runtime/loader.rb +143 -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 +254 -0
- data/lib/hecks/runtime/reaction_invocation.rb +245 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +266 -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 +124 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
- data/lib/hecks/runtime/registry/verification.rb +158 -0
- data/lib/hecks/runtime/registry.rb +218 -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 +301 -0
- data/lib/hecks/runtime/storage_shape.rb +120 -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 +112 -0
- data/lib/hecks/runtime/value/coercion.rb +500 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +95 -0
- data/lib/hecks/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/translation/audit/layer_one.rb +37 -0
- data/lib/hecks/translation/audit/layer_two.rb +77 -0
- data/lib/hecks/translation/audit/unfed_report.rb +42 -0
- data/lib/hecks/translation/audit.rb +70 -0
- data/lib/hecks/translation/reattest.rb +72 -0
- data/lib/hecks/translation/rule_compiler.rb +120 -0
- data/lib/hecks/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/translation/scaffold.rb +30 -0
- data/lib/hecks/translation.rb +11 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +213 -0
- data/lib/hecks.rb +118 -6
- metadata +401 -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,143 @@
|
|
|
1
|
+
require_relative "dispatcher"
|
|
2
|
+
require_relative "instance"
|
|
3
|
+
require_relative "event"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "../naming"
|
|
6
|
+
require_relative "../adapters/driven/lambda/client"
|
|
7
|
+
require_relative "../ports/persistence/binding_policy"
|
|
8
|
+
require_relative "../ports/persistence/remote_runtime"
|
|
9
|
+
|
|
10
|
+
module Hecks
|
|
11
|
+
module Runtime
|
|
12
|
+
# THE WRITE-SIDE HALF OF LAMBDA ROUTING — `Runtime::Loader.boot`
|
|
13
|
+
# constructs this INSTEAD OF `Dispatcher` when the booted domain's
|
|
14
|
+
# own `.world` declares Lambda routing (see loader.rb's own
|
|
15
|
+
# `dispatcher_class_for`). Matches `Dispatcher`'s public shape
|
|
16
|
+
# closely enough that everything built ON TOP of a dispatcher —
|
|
17
|
+
# `Handle`, `AggregateDoor`, `Facade::Surface` — needs no changes
|
|
18
|
+
# at all: `Handle#run`'s entire contract is
|
|
19
|
+
# `@dispatcher.dispatch("#{fqn}.#{command}", **identity, **args).instance.state`,
|
|
20
|
+
# and both classes answer that identically.
|
|
21
|
+
#
|
|
22
|
+
# READS DELEGATE, WRITES DON'T. `query`/`reference_query` hand off
|
|
23
|
+
# to a REAL `Dispatcher` built over the SAME registry — since that
|
|
24
|
+
# registry's own repositories are ALREADY Lambda-backed
|
|
25
|
+
# (`Adapters::Lambda`, `persisted_by("Lambda")`, Phase 2's other
|
|
26
|
+
# half), the inherited query machinery (`QueryInterpreter`,
|
|
27
|
+
# `Ports::Query::InMemory.execute`) works completely unchanged, no
|
|
28
|
+
# query logic duplicated here. `dispatch` can't delegate the same
|
|
29
|
+
# way: command validation (givens, constraints) has to actually
|
|
30
|
+
# run in Rust for a Lambda-routed domain, not get pre-checked here
|
|
31
|
+
# against incomplete local state and then merely persisted.
|
|
32
|
+
class RemoteDispatcher
|
|
33
|
+
Result = Struct.new(:verb, :instance, :events, keyword_init: true) do
|
|
34
|
+
def id = instance.id
|
|
35
|
+
def state = instance.to_h
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
attr_reader :registry
|
|
39
|
+
|
|
40
|
+
def initialize(registry, region: "us-east-1")
|
|
41
|
+
@registry = registry
|
|
42
|
+
# `File.basename(registry.root)`, not `bluebooks.keys.first` —
|
|
43
|
+
# matches `Adapters::Lambda`'s own function-name resolution
|
|
44
|
+
# exactly (see its own comment on why: one merged Lambda per
|
|
45
|
+
# deploy, not one per attached chapter, and `root` is the one
|
|
46
|
+
# signal every bluebook in this registry shares regardless of
|
|
47
|
+
# which one attached it) — INCLUDING that same adapter's own
|
|
48
|
+
# `ENV["DOMAIN_NAME"]`-first fix: `root` is always `/var/task`
|
|
49
|
+
# inside a deployed Lambda, giving "task" instead of the real
|
|
50
|
+
# domain name (a real, live AccessDeniedException on
|
|
51
|
+
# "hecks-task" caught this).
|
|
52
|
+
@client = Adapters::Lambda::Client.new(domain: ENV["DOMAIN_NAME"] || File.basename(registry.root), region: region)
|
|
53
|
+
# READ-SIDE DELEGATE ONLY (see class comment) — never dispatched
|
|
54
|
+
# through; a real Dispatcher's own `query`/`reference_query`
|
|
55
|
+
# already resolve generically via `registry.repository(...)`,
|
|
56
|
+
# so building one here reuses that instead of duplicating it.
|
|
57
|
+
@local = Dispatcher.new(registry)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def dispatch(verb, saga_correlation: nil, **args)
|
|
61
|
+
domain, aggregate_name, = Naming.split_verb(verb) ||
|
|
62
|
+
raise(UnknownVerb,
|
|
63
|
+
RefusalWording.render("UnknownVerb", "not_fully_qualified", verb: verb.inspect))
|
|
64
|
+
aggregate = @registry.bluebook(domain)&.aggregate(aggregate_name) ||
|
|
65
|
+
raise(UnknownVerb,
|
|
66
|
+
RefusalWording.render("UnknownVerb", "no_aggregate", domain: domain, aggregate: aggregate_name.inspect))
|
|
67
|
+
|
|
68
|
+
# NOT EVERY AGGREGATE IN A LAMBDA-ROUTED DOMAIN IS ITSELF
|
|
69
|
+
# LAMBDA-BOUND — Member's real name->email rekey carries a
|
|
70
|
+
# `compute` rule (era_check.rb's own `check_compute_rules!`),
|
|
71
|
+
# which can only ever run against Postgres, permanently. Its
|
|
72
|
+
# OWN `.hecksagon` bind stays "Postgres" even when
|
|
73
|
+
# `dispatched_by("Lambda")` is on for everything else — checked
|
|
74
|
+
# here by real CAPABILITY (`Ports::Persistence::RemoteRuntime`,
|
|
75
|
+
# §1), not by comparing the adapter's own name to the string
|
|
76
|
+
# "Lambda" — a bind resolves to whatever adapter CLASS actually
|
|
77
|
+
# backs it, and only a class shaped like "the real interpreter
|
|
78
|
+
# lives behind a call boundary" forwards here; anything else
|
|
79
|
+
# (Postgres, Memory, any future local adapter) falls through to
|
|
80
|
+
# the real local Dispatcher instead of being forwarded to a
|
|
81
|
+
# Lambda that has no way to represent its lineage history at
|
|
82
|
+
# all.
|
|
83
|
+
adapter_name = Ports::Persistence::BindingPolicy.resolve(@registry, domain, aggregate).adapter
|
|
84
|
+
return @local.dispatch(verb, saga_correlation: saga_correlation, **args) unless @registry.adapter_class(adapter_name) <= Ports::Persistence::RemoteRuntime
|
|
85
|
+
|
|
86
|
+
response = @client.dispatch(verb, args)
|
|
87
|
+
|
|
88
|
+
refusal = response.fetch("refusals", []).find { |r| r["verb"] == verb }
|
|
89
|
+
raise RemoteRefusal, "#{verb} refused: #{refusal['error']}" if refusal
|
|
90
|
+
|
|
91
|
+
# THIS STEP'S OWN mutations — `mutations` is one entry per
|
|
92
|
+
# replayed step (rust/host's rehydrate-and-replay design,
|
|
93
|
+
# Phase 1), so `.last` is exactly the step just dispatched.
|
|
94
|
+
# Matched by fully-qualified aggregate name, not just "the
|
|
95
|
+
# first mutation" — a command whose reaction ALSO mutates a
|
|
96
|
+
# different aggregate (a policy, a saga leg) puts more than
|
|
97
|
+
# one mutation in the same step, and the direct effect of
|
|
98
|
+
# THIS verb is the one this dispatch's own caller expects
|
|
99
|
+
# `.instance` to be.
|
|
100
|
+
fqn = "#{domain}::#{aggregate.hecks_name}"
|
|
101
|
+
mutation = response.fetch("mutations", []).last&.find { |m| m["aggregate"] == fqn } ||
|
|
102
|
+
raise(WiringError,
|
|
103
|
+
"#{verb} was accepted but rust/host reported no mutation for #{fqn} — response: #{response.inspect}")
|
|
104
|
+
|
|
105
|
+
instance = Instance.new(aggregate: aggregate, id: mutation["id"],
|
|
106
|
+
state: JSON.parse(JSON.generate(mutation["state"]), symbolize_names: true))
|
|
107
|
+
|
|
108
|
+
Result.new(verb: verb, instance: instance, events: step_events(response))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def query(verb, **args) = @local.query(verb, **args)
|
|
112
|
+
def reference_query(verb, **args) = @local.reference_query(verb, **args)
|
|
113
|
+
|
|
114
|
+
# THE FULL DOMAIN'S EVENT HISTORY, on every call — `{"read":
|
|
115
|
+
# true}` replays the whole journal (Phase 1's `dispatch::read`),
|
|
116
|
+
# so its own `events` array already IS the complete log, the
|
|
117
|
+
# same thing `@registry.event_log` would answer for a local
|
|
118
|
+
# dispatch. Not cached: `AggregateDoor.events`/`Handle#events`
|
|
119
|
+
# are not called in this codebase's own hot paths today: if that
|
|
120
|
+
# changes, caching belongs here, not in every caller.
|
|
121
|
+
def events
|
|
122
|
+
@client.read.fetch("events", []).map { |e| build_event(e) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def step_events(response)
|
|
128
|
+
response.fetch("events", []).map { |e| build_event(e) }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Rust's own event JSON (kernel/cli.rs's `event_to_json`) carries
|
|
132
|
+
# `name`/`aggregate`/`id`/`payload` only — no `occurred_at`
|
|
133
|
+
# (nothing in the kernel tracks wall-clock time; every
|
|
134
|
+
# replay is deterministic and timestamp-free by design). `nil`
|
|
135
|
+
# here, not a synthesized `Time.now` that would silently lie
|
|
136
|
+
# about when something actually happened.
|
|
137
|
+
def build_event(json)
|
|
138
|
+
Event.new(name: json["name"], aggregate: json["aggregate"], id: json["id"],
|
|
139
|
+
payload: JSON.parse(JSON.generate(json["payload"]), symbolize_names: true), occurred_at: nil)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require_relative "errors"
|
|
2
|
+
require_relative "refusal_wording"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Runtime
|
|
6
|
+
# The invocation address is not part of a command's domain payload.
|
|
7
|
+
# Aggregate commands carry one receiver identity; entity commands carry
|
|
8
|
+
# the aggregate receiver followed by one identity for every entity hop.
|
|
9
|
+
module Routing
|
|
10
|
+
Envelope = Struct.new(:aggregate, :entities, keyword_init: true) do
|
|
11
|
+
def initialize(aggregate:, entities: [])
|
|
12
|
+
super(aggregate: aggregate.to_s, entities: Array(entities).map(&:to_s).freeze)
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def envelope(to, entity_depth: 0)
|
|
20
|
+
return nil if to.nil?
|
|
21
|
+
|
|
22
|
+
aggregate, entities = if to.is_a?(Hash)
|
|
23
|
+
hash = to.transform_keys(&:to_sym)
|
|
24
|
+
unknown = hash.keys - %i[aggregate entity entities]
|
|
25
|
+
unless unknown.empty?
|
|
26
|
+
raise TypeMismatch, "to: does not recognize #{unknown.sort.join(', ')}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
[hash[:aggregate], entity_identities(hash)]
|
|
30
|
+
else
|
|
31
|
+
[to, []]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if aggregate.nil? || aggregate.to_s.empty?
|
|
35
|
+
raise TypeMismatch, "to: must name the receiving aggregate identity"
|
|
36
|
+
end
|
|
37
|
+
if entities.size != entity_depth
|
|
38
|
+
raise TypeMismatch,
|
|
39
|
+
"to: for an entity command needs #{entity_depth} entity " \
|
|
40
|
+
"#{entity_depth == 1 ? 'identity' : 'identities'} after the aggregate — got #{entities.size}"
|
|
41
|
+
end
|
|
42
|
+
if entities.any? { |identity| identity.nil? || identity.to_s.empty? }
|
|
43
|
+
raise TypeMismatch, "to: contains a blank entity identity"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
Envelope.new(aggregate: aggregate, entities: entities)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# `with:` is deliberately strict. Compatibility-only calls still pass
|
|
50
|
+
# loose keyword arguments through the old addressing gate, but a caller
|
|
51
|
+
# choosing the explicit envelope cannot smuggle receiver identity back
|
|
52
|
+
# into the payload.
|
|
53
|
+
def payload(command, with:, legacy:)
|
|
54
|
+
if with && !legacy.empty?
|
|
55
|
+
raise TypeMismatch, "dispatch takes command facts in with:, not both with: and loose keyword arguments"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
return legacy unless with
|
|
59
|
+
unless with.is_a?(Hash)
|
|
60
|
+
raise TypeMismatch, "with: must be a hash of command facts"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
offered = with.transform_keys(&:to_sym)
|
|
64
|
+
declared = command.attributes.map { |attribute| attribute.name.to_sym }
|
|
65
|
+
unknown = (offered.keys - declared).sort
|
|
66
|
+
unless unknown.empty?
|
|
67
|
+
reading = declared.empty? ? "none" : declared.join(", ")
|
|
68
|
+
raise UnknownArgument,
|
|
69
|
+
RefusalWording.render("UnknownArgument", "unknown_args",
|
|
70
|
+
command: command.hecks_name, unknown: unknown.join(", "),
|
|
71
|
+
declared: reading)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
absent = command.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym } - offered.keys
|
|
75
|
+
unless absent.empty?
|
|
76
|
+
reading = declared.empty? ? "none" : declared.join(", ")
|
|
77
|
+
raise AbsentArgument,
|
|
78
|
+
RefusalWording.render("AbsentArgument", "absent_args",
|
|
79
|
+
command: command.hecks_name, absent: absent.sort.join(", "),
|
|
80
|
+
declared: reading)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
offered
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def entity_identities(hash)
|
|
87
|
+
if hash.key?(:entities) && hash.key?(:entity)
|
|
88
|
+
raise TypeMismatch, "to: takes entity: or entities:, not both"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
hash.key?(:entities) ? Array(hash[:entities]) : Array(hash[:entity])
|
|
92
|
+
end
|
|
93
|
+
private_class_method :entity_identities
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require_relative "../value"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Runtime
|
|
5
|
+
class SagaInterpreter
|
|
6
|
+
# How a saga decides WHICH CONVERSATION an event belongs to — three
|
|
7
|
+
# tiers, each one a lesson.
|
|
8
|
+
module Correlation
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# A DOTTED PATH NAMES THE SCALAR FIELD, rather than asking a value object
|
|
12
|
+
# to stand in for one. `correlates_by :end_to_end` would key a saga on
|
|
13
|
+
# the whole ExternalTransfer::EndToEndReference — and what a non-scalar
|
|
14
|
+
# correlation key even IS is representation-dependent (the object
|
|
15
|
+
# itself? its serialised text?). `:"end_to_end.value"` reads the one
|
|
16
|
+
# field with a single unambiguous rendering.
|
|
17
|
+
def saga_correlation(pm, event)
|
|
18
|
+
path = pm.correlates_by.to_s.split(".")
|
|
19
|
+
# A LATER EVENT MAY ALREADY HOLD THE SCALAR. `reference.value` digs a
|
|
20
|
+
# value object's field out of a FRESH declaration (TransferRequested's
|
|
21
|
+
# `reference` IS a TransferReference) — but a downstream event this
|
|
22
|
+
# same value was smuggled through as a passthrough argument
|
|
23
|
+
# (AccountDebited's `reference:`, resolved by `dispatch_args` to the
|
|
24
|
+
# bare correlation string) carries it as a scalar already, with
|
|
25
|
+
# nothing left to dig. `"xfer-1".respond_to?(:[])` is true — String
|
|
26
|
+
# has its OWN `[]` (substring indexing) — so checking for keyed
|
|
27
|
+
# lookup explicitly, rather than "responds to `[]` at all", is what
|
|
28
|
+
# stops the second segment from being read as a symbol index into a
|
|
29
|
+
# string that has already arrived.
|
|
30
|
+
value = path.reduce(event.payload) do |held, segment|
|
|
31
|
+
held.is_a?(Hash) || held.is_a?(Value) ? held[segment.to_sym] : held
|
|
32
|
+
end
|
|
33
|
+
return value unless value.to_s.empty?
|
|
34
|
+
|
|
35
|
+
# THE STAMP — `deliver_saga_dispatch` marks its own event before this
|
|
36
|
+
# saga's next step ever asks, for a leg whose command declares
|
|
37
|
+
# NEITHER the correlation field itself nor the emitting aggregate's
|
|
38
|
+
# own reference key (the two tiers above). command_interpreter/
|
|
39
|
+
# argument_gate.rb names the old payload-only lookup "the weakest
|
|
40
|
+
# part of the gate" : a correlation key arriving on a command only
|
|
41
|
+
# because `correlation_keys` widens the undeclared-argument
|
|
42
|
+
# allow-list domain-wide. This is the additive fix — a leg that
|
|
43
|
+
# carries nothing correlation-shaped at all still correlates,
|
|
44
|
+
# because the saga that dispatched it already knows the answer.
|
|
45
|
+
# Keyed by `correlation_head`
|
|
46
|
+
# rather than a bare scalar so an event stamped by one saga cannot be
|
|
47
|
+
# misread by an unrelated one correlating on a different field.
|
|
48
|
+
stamped = event.correlation && event.correlation[pm.correlation_head.to_s]
|
|
49
|
+
return stamped unless stamped.nil? || stamped.to_s.empty?
|
|
50
|
+
|
|
51
|
+
# A SELF-REFERENCING LEG carries the correlation forward under its
|
|
52
|
+
# own emitting record's identity — `event.id`, not a field dug back
|
|
53
|
+
# out of the payload. This used to read `event.payload[own_key]`
|
|
54
|
+
# (`own_key` the aggregate's own reference-key convention, "wire",
|
|
55
|
+
# "transfer"), which only ever held a value because LEGACY dispatch
|
|
56
|
+
# left the self-addressing key riding along in the payload
|
|
57
|
+
# unfiltered. Routing separated from payload (`to:`/`with:`, the
|
|
58
|
+
# facade's own `Handle#run` always uses it) closed exactly that
|
|
59
|
+
# leak — correctly, since an addressing key is not a fact the
|
|
60
|
+
# payload should carry — which left this tier reading an empty
|
|
61
|
+
# Hash for any self-referencing leg with no OTHER declared
|
|
62
|
+
# attributes (`OnboardingCase.Clear`, `.Decline` — no `attribute`
|
|
63
|
+
# lines at all): the saga silently stopped advancing, forever, for
|
|
64
|
+
# exactly the leg this tier exists to correlate.
|
|
65
|
+
#
|
|
66
|
+
# `event.id` says the identical thing this tier always meant —
|
|
67
|
+
# "the record that just emitted this event, by its own identity" —
|
|
68
|
+
# and unlike a payload dig it is populated by the record itself,
|
|
69
|
+
# not by which dispatch convention the caller happened to use.
|
|
70
|
+
#
|
|
71
|
+
# GATED, still — a manually-dispatched command on a wholly
|
|
72
|
+
# UNRELATED aggregate can share an event NAME this pm happens to
|
|
73
|
+
# handle (`Drawer.Take` also emits "Taken", the same name a
|
|
74
|
+
# SAGA-DISPATCHED leg uses) with nothing this saga should read as
|
|
75
|
+
# its own conversation. What makes a leg genuinely
|
|
76
|
+
# self-referencing — the one fact worth trusting `event.id`
|
|
77
|
+
# for — is that `correlates_by`'s own head field IS this event's
|
|
78
|
+
# OWN aggregate's declared identity, not merely a same-shaped
|
|
79
|
+
# name: `OnboardingCase.identity_heads` really does include
|
|
80
|
+
# `:reference`, `correlates_by :"reference.value"`'s own head ;
|
|
81
|
+
# `Drawer.identity_heads` is `[:number]`, nowhere close.
|
|
82
|
+
self_identified?(pm, event) ? event.id : nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self_identified?(pm, event)
|
|
86
|
+
domain, bare_name = event.aggregate.to_s.split("::", 2)
|
|
87
|
+
return false unless bare_name
|
|
88
|
+
|
|
89
|
+
construct = @registry.bluebook(domain)&.aggregate(bare_name)
|
|
90
|
+
return false unless construct
|
|
91
|
+
|
|
92
|
+
construct.identity_heads.map(&:to_s).include?(pm.correlation_head.to_s)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,301 @@
|
|
|
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
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Runtime
|
|
10
|
+
class SagaInterpreter
|
|
11
|
+
include Correlation
|
|
12
|
+
|
|
13
|
+
# The trigger lives on the declaration it triggers, not on the runtime that
|
|
14
|
+
# notices it — see ProcessManager::REFUSED.
|
|
15
|
+
REFUSED = Bluebook::ProcessManager::REFUSED
|
|
16
|
+
|
|
17
|
+
# A crash gets this many extra attempts before the procedure gives up
|
|
18
|
+
# and treats it as something to compensate for — see
|
|
19
|
+
# `deliver_saga_dispatch`'s own comment for why a crash isn't unwound
|
|
20
|
+
# on the first failure the way a domain refusal is.
|
|
21
|
+
MAX_DEFECT_RETRIES = 3
|
|
22
|
+
|
|
23
|
+
attr_reader :registry
|
|
24
|
+
|
|
25
|
+
def initialize(registry, door:)
|
|
26
|
+
@registry = registry
|
|
27
|
+
@door = door
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def advance(event, domain)
|
|
31
|
+
bluebook = @registry.bluebook(domain)
|
|
32
|
+
return unless bluebook
|
|
33
|
+
|
|
34
|
+
bluebook.process_managers.each do |pm|
|
|
35
|
+
begin_saga(pm, event, domain)
|
|
36
|
+
advance_saga(pm, event, domain)
|
|
37
|
+
end_saga(pm, event, domain)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# THE CHECKPOINT WRITE, shared by every mutation site below —
|
|
44
|
+
# holds `saga_mutex` across BOTH the in-memory Hash mutation and
|
|
45
|
+
# the persistence write (§7), not just the Hash mutation alone:
|
|
46
|
+
# two threads racing the SAME (process_manager, correlation) key
|
|
47
|
+
# could otherwise interleave their writes out of order, silently
|
|
48
|
+
# reordering a saga's own transition history — worse for the
|
|
49
|
+
# adapters with no locking of their own (Heki) than for Postgres.
|
|
50
|
+
# `deep_copy` guards against the exact shape of bug PR #175 itself
|
|
51
|
+
# already found once (over-freezing a live, still-mutated Hash) —
|
|
52
|
+
# never hand a persistence adapter the SAME object `advance_saga`/
|
|
53
|
+
# `unwind` go on to mutate in place; round-tripping through JSON
|
|
54
|
+
# is also what guarantees the value is safe for every adapter that
|
|
55
|
+
# itself calls `JSON.generate` on it.
|
|
56
|
+
def checkpoint(pm, correlation, instance, domain)
|
|
57
|
+
@registry.saga_persistence(domain).save_saga(
|
|
58
|
+
process_manager: pm.name, correlation: correlation,
|
|
59
|
+
state: instance[:state], memory: deep_copy(instance[:memory])
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def deep_copy(hash) = JSON.parse(JSON.generate(hash), symbolize_names: true)
|
|
64
|
+
|
|
65
|
+
def begin_saga(pm, event, domain)
|
|
66
|
+
return unless event.name == pm.starts_on
|
|
67
|
+
|
|
68
|
+
correlation = saga_correlation(pm, event)
|
|
69
|
+
if correlation.to_s.empty?
|
|
70
|
+
@registry.saga_log << { process_manager: pm.name, on: event.name,
|
|
71
|
+
born: false, reason: "no #{pm.correlates_by} in the payload" }
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
created = @registry.saga_mutex.synchronize do
|
|
76
|
+
next false if @registry.saga_instances[pm.name].key?(correlation)
|
|
77
|
+
|
|
78
|
+
instance = { state: pm.states.first, memory: event.payload }
|
|
79
|
+
@registry.saga_instances[pm.name][correlation] = instance
|
|
80
|
+
checkpoint(pm, correlation, instance, domain)
|
|
81
|
+
true
|
|
82
|
+
end
|
|
83
|
+
return unless created
|
|
84
|
+
|
|
85
|
+
@registry.saga_log << { process_manager: pm.name, on: event.name,
|
|
86
|
+
instance: correlation, born: true, state: pm.states.first }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# THE MUTEX COVERS ONLY THE STATE-CHECK-AND-MUTATE-AND-CHECKPOINT
|
|
90
|
+
# STEP, never the dispatch cascade that follows — `deliver_saga_
|
|
91
|
+
# dispatch` calls `@door.reenter`, which can recursively re-enter
|
|
92
|
+
# THIS SAME interpreter (a saga's own leg triggering another saga,
|
|
93
|
+
# or itself again) on the SAME thread, and `Mutex` is not
|
|
94
|
+
# reentrant: holding it across that call would deadlock the
|
|
95
|
+
# thread against itself the moment any real chain did that.
|
|
96
|
+
def advance_saga(pm, event, domain)
|
|
97
|
+
handler = pm.handler_for(event.name)
|
|
98
|
+
return unless handler
|
|
99
|
+
|
|
100
|
+
correlation = saga_correlation(pm, event)
|
|
101
|
+
return if correlation.to_s.empty?
|
|
102
|
+
|
|
103
|
+
record = { process_manager: pm.name, on: event.name, instance: correlation }
|
|
104
|
+
instance = nil
|
|
105
|
+
|
|
106
|
+
advanced = @registry.saga_mutex.synchronize do
|
|
107
|
+
instance = @registry.saga_instances[pm.name][correlation]
|
|
108
|
+
unless instance
|
|
109
|
+
@registry.saga_log << record.merge(advanced: false, reason: "no conversation remembers #{correlation.inspect}")
|
|
110
|
+
next false
|
|
111
|
+
end
|
|
112
|
+
unless instance[:state] == handler.from_state
|
|
113
|
+
@registry.saga_log << record.merge(advanced: false,
|
|
114
|
+
reason: "in #{instance[:state].inspect}, not #{handler.from_state.inspect}")
|
|
115
|
+
next false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
instance[:state] = handler.to_state
|
|
119
|
+
checkpoint(pm, correlation, instance, domain)
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
return unless advanced
|
|
123
|
+
|
|
124
|
+
@registry.saga_log << record.merge(advanced: true, from: handler.from_state, to: handler.to_state)
|
|
125
|
+
|
|
126
|
+
handler.dispatches.each do |spec|
|
|
127
|
+
deliver_saga_dispatch(pm, spec, event, instance, correlation, domain)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def deliver_saga_dispatch(pm, spec, event, instance, correlation, domain)
|
|
132
|
+
args = dispatch_args(pm, spec, event, instance, correlation)
|
|
133
|
+
record = { process_manager: pm.name, instance: correlation, dispatch: spec.command_name }
|
|
134
|
+
|
|
135
|
+
# THE RAW INPUTS `args` WAS RESOLVED FROM, captured alongside the
|
|
136
|
+
# result — never re-derived from history[:saga_instances] later
|
|
137
|
+
# (that only ever holds the FINAL memory, after every step has
|
|
138
|
+
# run; this dispatch's own memory, at the moment it actually
|
|
139
|
+
# fired, is a different fact for a saga whose memory keeps
|
|
140
|
+
# changing). `spec.with_spec.empty?` skipped: nothing declared
|
|
141
|
+
# to check, a tautological pass, the same reason a command with
|
|
142
|
+
# no givens/from is skipped by lifecycle_guard_and_given_
|
|
143
|
+
# violations_are_refused.
|
|
144
|
+
unless spec.with_spec.to_a.empty?
|
|
145
|
+
@registry.saga_dispatch_log << { process_manager: pm.name, instance: correlation, dispatch: spec.command_name,
|
|
146
|
+
on: event.name, correlation_head: pm.correlation_head,
|
|
147
|
+
event_payload: event.payload, memory: Value.materialize(instance[:memory]),
|
|
148
|
+
with_spec: spec.with_spec, args: args }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if @door.reaction_depth_reached?
|
|
152
|
+
# THE CEILING IS NOT A DOMAIN DECISION EITHER — same reasoning as a
|
|
153
|
+
# crash, below — but unlike a crash there is nothing ambiguous
|
|
154
|
+
# about it: the leg unambiguously did not run, so it unwinds
|
|
155
|
+
# exactly like a refusal instead of stranding the instance for a
|
|
156
|
+
# human to notice. `unwind`'s own state guard (it moves to its
|
|
157
|
+
# `to_state` before its dispatches run) is what keeps this from
|
|
158
|
+
# looping if the ceiling is still in effect when the compensating
|
|
159
|
+
# leg tries to dispatch — that leg's own attempt hits this same
|
|
160
|
+
# branch, calls `unwind` again, and finds the instance already
|
|
161
|
+
# past `from_state`.
|
|
162
|
+
@registry.saga_log << record.merge(delivered: false,
|
|
163
|
+
reason: "reaction depth #{@door.max_reaction_depth} reached")
|
|
164
|
+
unwind(pm, event, instance, correlation, domain)
|
|
165
|
+
return
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
attempt = 0
|
|
169
|
+
begin
|
|
170
|
+
invocation = ReactionInvocation.build(
|
|
171
|
+
registry: @registry,
|
|
172
|
+
verb: qualified(spec.command_name, domain),
|
|
173
|
+
projected: args,
|
|
174
|
+
explicit: ReactionInvocation.projection_declared?(spec),
|
|
175
|
+
passthrough: [pm.correlation_head],
|
|
176
|
+
source_receiver: { aggregate: event.aggregate, identity: event.id }
|
|
177
|
+
)
|
|
178
|
+
@door.reenter(qualified(spec.command_name, domain),
|
|
179
|
+
saga_correlation: { pm.correlation_head.to_s => correlation }, **invocation)
|
|
180
|
+
@registry.saga_log << record.merge(delivered: true)
|
|
181
|
+
rescue *DOMAIN_REFUSALS => error
|
|
182
|
+
# Same rule as the policy interpreter : a refusal by the target is
|
|
183
|
+
# a recorded outcome, and the leg that raised it UNWINDS — see
|
|
184
|
+
# `unwind`'s own comment for why the procedure runs its
|
|
185
|
+
# compensation here rather than leaving the money (or whatever
|
|
186
|
+
# else a leg moved) sitting out.
|
|
187
|
+
@registry.saga_log << record.merge(delivered: false, reason: error.message)
|
|
188
|
+
unwind(pm, event, instance, correlation, domain)
|
|
189
|
+
rescue StandardError => error
|
|
190
|
+
# A DEFECT, not a refusal — see PolicyInterpreter#deliver's own
|
|
191
|
+
# comment for the full reasoning: the same DOMAIN_REFUSALS split,
|
|
192
|
+
# and the same "the triggering command already succeeded and
|
|
193
|
+
# persisted by the time this runs" fact that makes catching it
|
|
194
|
+
# here safe rather than reckless.
|
|
195
|
+
#
|
|
196
|
+
# UNLIKE a refusal, a crash is not a decision the domain made, so
|
|
197
|
+
# it does not unwind on the first failure — MAX_DEFECT_RETRIES
|
|
198
|
+
# gives a transient failure (a DB timeout, a race, a cold start)
|
|
199
|
+
# a chance to clear on its own, retrying the identical dispatch,
|
|
200
|
+
# before this is treated as something to compensate for. Every
|
|
201
|
+
# attempt is recorded distinguishably (`defect: true`, the
|
|
202
|
+
# error's own class); only once retries are exhausted is it
|
|
203
|
+
# warned to STDERR and unwound — tagged `defect_compensated:
|
|
204
|
+
# true` rather than folded into an ordinary refusal's shape, so
|
|
205
|
+
# the log never misrepresents a crash as a decision the domain
|
|
206
|
+
# made. Compensating a genuinely stuck leg beats leaving it for a
|
|
207
|
+
# human to find; misrepresenting *why* it compensated is what the
|
|
208
|
+
# tag is for.
|
|
209
|
+
attempt += 1
|
|
210
|
+
if attempt <= MAX_DEFECT_RETRIES
|
|
211
|
+
@registry.saga_log << record.merge(delivered: false, reason: error.message,
|
|
212
|
+
defect: true, error_class: error.class.name,
|
|
213
|
+
attempt: attempt, retrying: true)
|
|
214
|
+
retry
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
warn "[hecks] defect in saga #{pm.name} — instance #{correlation.inspect} " \
|
|
218
|
+
"dispatching #{spec.command_name} after #{attempt} attempts: #{error.class}: #{error.message}"
|
|
219
|
+
@registry.saga_log << record.merge(delivered: false, reason: error.message, defect: true,
|
|
220
|
+
error_class: error.class.name, defect_compensated: true)
|
|
221
|
+
unwind(pm, event, instance, correlation, domain)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# A refused leg UNWINDS — the procedure runs the leg declared `on :refused`,
|
|
226
|
+
# which is where the compensation lives. So does a leg that hit the
|
|
227
|
+
# reaction-depth ceiling, and so does a leg that crashed and stayed
|
|
228
|
+
# crashing through MAX_DEFECT_RETRIES — see `deliver_saga_dispatch`'s own
|
|
229
|
+
# comments for why each of those is safe to route here.
|
|
230
|
+
#
|
|
231
|
+
# Until this existed a refusal was RECORDED and nothing else happened. The
|
|
232
|
+
# wire's thousand was taken from the source, refused by the destination, and
|
|
233
|
+
# sat nowhere until a human dispatched the reversal by hand ; banking's
|
|
234
|
+
# settlement left a debit standing with no credit and no reversal at all.
|
|
235
|
+
# Both bluebooks had written the compensating leg. Nothing armed it.
|
|
236
|
+
#
|
|
237
|
+
# A compensation that is itself refused does NOT unwind again, and needs no
|
|
238
|
+
# flag to stop it: the state moves to the compensating leg's to_state BEFORE
|
|
239
|
+
# its dispatches run, so a second refusal finds the instance no longer in
|
|
240
|
+
# from_state and records that instead. The check is the guard.
|
|
241
|
+
def unwind(pm, event, instance, correlation, domain)
|
|
242
|
+
handler = pm.handler_for(REFUSED)
|
|
243
|
+
return unless handler && instance
|
|
244
|
+
|
|
245
|
+
record = { process_manager: pm.name, on: REFUSED, instance: correlation }
|
|
246
|
+
|
|
247
|
+
# Same non-reentrancy reasoning as `advance_saga`'s own comment —
|
|
248
|
+
# the mutex covers only the check-and-mutate-and-checkpoint step.
|
|
249
|
+
advanced = @registry.saga_mutex.synchronize do
|
|
250
|
+
unless instance[:state] == handler.from_state
|
|
251
|
+
@registry.saga_log << record.merge(advanced: false,
|
|
252
|
+
reason: "in #{instance[:state].inspect}, not #{handler.from_state.inspect}")
|
|
253
|
+
next false
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
instance[:state] = handler.to_state
|
|
257
|
+
checkpoint(pm, correlation, instance, domain)
|
|
258
|
+
true
|
|
259
|
+
end
|
|
260
|
+
return unless advanced
|
|
261
|
+
|
|
262
|
+
@registry.saga_log << record.merge(advanced: true, from: handler.from_state, to: handler.to_state)
|
|
263
|
+
|
|
264
|
+
handler.dispatches.each do |spec|
|
|
265
|
+
deliver_saga_dispatch(pm, spec, event, instance, correlation, domain)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def dispatch_args(pm, spec, event, instance, correlation)
|
|
270
|
+
ReactionInvocation.resolve_mapping(
|
|
271
|
+
with_spec: spec.with_spec,
|
|
272
|
+
scopes: [["current event payload", event.payload], ["opening event memory", instance[:memory]]],
|
|
273
|
+
bindings: { pm.correlation_head => correlation },
|
|
274
|
+
label: "#{pm.name}'s dispatch #{spec.command_name}"
|
|
275
|
+
)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def qualified(command_name, domain)
|
|
279
|
+
command_name.include?("::") ? command_name : "#{domain}::#{command_name}"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def end_saga(pm, event, domain)
|
|
283
|
+
return unless event.name == pm.ends_on
|
|
284
|
+
|
|
285
|
+
correlation = saga_correlation(pm, event)
|
|
286
|
+
return if correlation.to_s.empty?
|
|
287
|
+
|
|
288
|
+
ended = @registry.saga_mutex.synchronize do
|
|
289
|
+
next false unless @registry.saga_instances[pm.name].delete(correlation)
|
|
290
|
+
|
|
291
|
+
@registry.saga_persistence(domain).delete_saga(process_manager: pm.name, correlation: correlation)
|
|
292
|
+
true
|
|
293
|
+
end
|
|
294
|
+
return unless ended
|
|
295
|
+
|
|
296
|
+
@registry.saga_log << { process_manager: pm.name, on: event.name,
|
|
297
|
+
instance: correlation, ended: true }
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|