hecks 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +51 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +301 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -20
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +124 -0
- data/lib/hecks/fuzzing/form_census.rb +199 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
- data/lib/hecks/fuzzing/properties/guards.rb +129 -26
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
- data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +84 -39
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +224 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +676 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +39 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
- data/lib/hecks/fuzzing/value_generator.rb +66 -14
- data/lib/hecks/fuzzing.rb +11 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +89 -21
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +250 -0
- data/lib/hecks/projections/glossary/markdown.rb +105 -0
- data/lib/hecks/projections/glossary/mermaid.rb +110 -0
- data/lib/hecks/projections/glossary/page.css +271 -0
- data/lib/hecks/projections/glossary/page.js +72 -0
- data/lib/hecks/projections/glossary/sections.rb +17 -0
- data/lib/hecks/projections/glossary/sentences.rb +205 -0
- data/lib/hecks/projections/glossary.rb +214 -286
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +15 -22
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +132 -24
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +94 -19
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +282 -46
- data/lib/hecks/runtime/entity_interpreter.rb +160 -69
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +111 -56
- data/lib/hecks/runtime/reaction_invocation.rb +76 -9
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -112
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +56 -27
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -88
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +93 -56
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +379 -226
- data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
- data/lib/hecks/runtime/value.rb +28 -23
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +207 -5
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +43 -2
|
@@ -3,19 +3,19 @@ require_relative "../runtime/registry"
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Ports
|
|
5
5
|
# A creating command with no natural key needs a value from
|
|
6
|
-
#
|
|
6
|
+
# somewhere to be its identity — this is that somewhere. Resolved
|
|
7
7
|
# the same way `Ports::Extraction` resolves its own adapter (one
|
|
8
8
|
# adapter registry-wide implements this port, not a per-aggregate
|
|
9
9
|
# binding the way persistence needs — different aggregates have no
|
|
10
10
|
# real reason to want different id-generation strategies within
|
|
11
11
|
# one running app).
|
|
12
12
|
#
|
|
13
|
-
#
|
|
13
|
+
# Replay needs no suppression here, on purpose. A UUID minted for a
|
|
14
14
|
# creating command's identity gets baked into that step's own args
|
|
15
15
|
# — an ordinary string value — the moment it's generated, by
|
|
16
16
|
# whoever issues the first, live dispatch. A recorded corpus
|
|
17
17
|
# script or a captured fuzz-replay step already holds that
|
|
18
|
-
# concrete value; replaying it calls `dispatch` with the
|
|
18
|
+
# concrete value; replaying it calls `dispatch` with the same
|
|
19
19
|
# args, and this module is never consulted again, for the same
|
|
20
20
|
# reason `SecureRandom.uuid` never runs twice for one
|
|
21
21
|
# already-recorded step today. `Event#occurred_at`
|
|
@@ -28,8 +28,22 @@ module Hecks
|
|
|
28
28
|
|
|
29
29
|
module_function
|
|
30
30
|
|
|
31
|
+
# Mints a fresh identity value from the bound adapter.
|
|
32
|
+
#
|
|
33
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
34
|
+
# @return [String] a newly minted identity: a random UUID from `SecureRandomIdentity`,
|
|
35
|
+
# or a decimal counter (`"1"`, `"2"`, …) from the deterministic `SequentialIdentity`
|
|
36
|
+
# spec double — unique per adapter, not guaranteed UUID-shaped
|
|
37
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
38
|
+
# (see `adapter`)
|
|
31
39
|
def uuid(registry) = adapter(registry).uuid
|
|
32
40
|
|
|
41
|
+
# Finds the single adapter bound to this port, refusing an ambiguous wiring.
|
|
42
|
+
#
|
|
43
|
+
# @param registry [Runtime::Registry] the booted registry to search
|
|
44
|
+
# @return [Module] the adapter module or class implementing this port
|
|
45
|
+
# @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
|
|
46
|
+
# or the one that does has no Ruby implementation under `Hecks::Adapters`
|
|
33
47
|
def adapter(registry)
|
|
34
48
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
35
49
|
|
|
@@ -2,7 +2,7 @@ require_relative "../runtime/registry"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Ports
|
|
5
|
-
#
|
|
5
|
+
# An authenticated (issuer, subject) pair, resolved to the id of a stable
|
|
6
6
|
# `Identity` — the other half of the same symmetry `Authorization`
|
|
7
7
|
# already has for Governance: one adapter registry-wide answers this
|
|
8
8
|
# port, resolved the same zero/one/many way, so an application never
|
|
@@ -17,10 +17,27 @@ module Hecks
|
|
|
17
17
|
|
|
18
18
|
module_function
|
|
19
19
|
|
|
20
|
+
# Looks up the id of the identity an authenticated (issuer, subject) pair is linked to.
|
|
21
|
+
#
|
|
22
|
+
# @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
|
|
23
|
+
# and handed on to it
|
|
24
|
+
# @param issuer [String] the OIDC issuer that authenticated the caller
|
|
25
|
+
# @param subject [String] the OIDC subject the issuer vouches for
|
|
26
|
+
# @return [String, nil] the linked identity's id, usable as an `actor_id` for
|
|
27
|
+
# `Ports::Authorization.holds_role?` — not an `Identity` record; nil if nothing has
|
|
28
|
+
# linked this pair
|
|
29
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
30
|
+
# (see `adapter`)
|
|
20
31
|
def resolve(registry, issuer:, subject:)
|
|
21
32
|
adapter(registry).resolve(registry, issuer: issuer, subject: subject)
|
|
22
33
|
end
|
|
23
34
|
|
|
35
|
+
# Finds the single adapter bound to this port, refusing an ambiguous wiring.
|
|
36
|
+
#
|
|
37
|
+
# @param registry [Runtime::Registry] the booted registry to search
|
|
38
|
+
# @return [Module] the adapter module or class implementing this port
|
|
39
|
+
# @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
|
|
40
|
+
# or the one that does has no Ruby implementation under `Hecks::Adapters`
|
|
24
41
|
def adapter(registry)
|
|
25
42
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
26
43
|
|
data/lib/hecks/ports/loading.rb
CHANGED
|
@@ -10,6 +10,10 @@ module Hecks
|
|
|
10
10
|
|
|
11
11
|
module_function
|
|
12
12
|
|
|
13
|
+
# Builds the loader a boot starts from, before any registry exists to resolve one.
|
|
14
|
+
#
|
|
15
|
+
# @return [Adapters::Folder] a new `Folder` adapter with no settings and no root,
|
|
16
|
+
# constructed directly rather than resolved through a registry
|
|
13
17
|
def bootstrap = Adapters::Folder.new
|
|
14
18
|
end
|
|
15
19
|
end
|
|
@@ -7,7 +7,14 @@ module Hecks
|
|
|
7
7
|
# `mirrors` is durable replication intent. It is part of the same
|
|
8
8
|
# append as the authoritative state, never a second outbox store.
|
|
9
9
|
Entry = Struct.new(:operation, :id, :state, :mirrors, keyword_init: true) do
|
|
10
|
+
# Tells a projecting adapter that this entry writes a record.
|
|
11
|
+
#
|
|
12
|
+
# @return [Boolean] true when `operation` is the String `"save"`
|
|
10
13
|
def save? = operation == "save"
|
|
14
|
+
|
|
15
|
+
# Tells a projecting adapter that this entry removes a record.
|
|
16
|
+
#
|
|
17
|
+
# @return [Boolean] true when `operation` is the String `"delete"`
|
|
11
18
|
def delete? = operation == "delete"
|
|
12
19
|
end
|
|
13
20
|
|
|
@@ -17,8 +24,14 @@ module Hecks
|
|
|
17
24
|
class AppendOnly
|
|
18
25
|
attr_reader :adapter
|
|
19
26
|
|
|
27
|
+
# Names the aggregate this repository stores, as the wrapped adapter holds it.
|
|
28
|
+
#
|
|
29
|
+
# @return [Bluebook::Aggregate] the aggregate the adapter was built for
|
|
20
30
|
def aggregate = @adapter.aggregate
|
|
21
31
|
|
|
32
|
+
# @param adapter [Object] a driven persistence adapter; it must respond to `append`,
|
|
33
|
+
# `project` and `entries`
|
|
34
|
+
# @raise [Runtime::WiringError] if the adapter lacks any of those three methods
|
|
22
35
|
def initialize(adapter)
|
|
23
36
|
@adapter = adapter
|
|
24
37
|
required = %i[append project entries]
|
|
@@ -29,58 +42,130 @@ module Hecks
|
|
|
29
42
|
end
|
|
30
43
|
end
|
|
31
44
|
|
|
45
|
+
# Reads one record's current projected state from the adapter.
|
|
46
|
+
#
|
|
47
|
+
# @param id [String, Object] the record's identity; adapters compare it as `id.to_s`
|
|
48
|
+
# @return [Runtime::Instance, nil] the stored record, or nil when no record has that id
|
|
32
49
|
def find(id) = @adapter.find(id)
|
|
50
|
+
|
|
51
|
+
# Lists every record the adapter currently projects, forwarding the keywords untouched.
|
|
52
|
+
#
|
|
53
|
+
# The shipped adapters accept `order_by:` (an attribute name, default nil for id or
|
|
54
|
+
# insertion order) and `direction:` (`:asc` or `:desc`).
|
|
55
|
+
#
|
|
56
|
+
# @return [Array<Runtime::Instance>] the stored records; `[]` when there are none
|
|
57
|
+
# @raise [Runtime::WiringError] from a SQL adapter when `order_by:` names no attribute
|
|
58
|
+
# of the aggregate
|
|
33
59
|
def all(**) = @adapter.all(**)
|
|
60
|
+
|
|
61
|
+
# Counts the records the adapter currently projects.
|
|
62
|
+
#
|
|
63
|
+
# @return [Integer] number of stored records, not of journal entries
|
|
34
64
|
def count = @adapter.count
|
|
65
|
+
|
|
66
|
+
# Reads the adapter's whole journal, oldest entry first.
|
|
67
|
+
#
|
|
68
|
+
# @return [Array<Persistence::Entry>] every appended entry with decoded state; `[]` for
|
|
69
|
+
# an empty journal or a `RemoteRuntime` adapter
|
|
70
|
+
# @raise [Runtime::WiringError] if a guarded adapter answers an entry whose state is
|
|
71
|
+
# undecoded (`CodecBoundary.check_entries!`)
|
|
35
72
|
def entries = @adapter.entries
|
|
36
73
|
|
|
74
|
+
# Lists the optional persistence behaviours the adapter advertises.
|
|
75
|
+
#
|
|
76
|
+
# @return [Array<Symbol>] frozen capability names such as `:atomic_put`,
|
|
77
|
+
# `:optimistic_concurrency` or `:cross_process_lock`; `[]` when the adapter declares
|
|
78
|
+
# no `persistence_capabilities`
|
|
37
79
|
def capabilities
|
|
38
80
|
return [] unless @adapter.respond_to?(:persistence_capabilities)
|
|
39
81
|
|
|
40
82
|
Array(@adapter.persistence_capabilities).map(&:to_sym).freeze
|
|
41
83
|
end
|
|
42
84
|
|
|
85
|
+
# Clears the adapter's stored records and journal so a kept runtime starts clean.
|
|
86
|
+
#
|
|
87
|
+
# @return [Object] whatever the adapter's `reset!` returns; every shipped adapter
|
|
88
|
+
# returns itself
|
|
89
|
+
# @raise [Runtime::WiringError] if the adapter has no `reset!`, or (PostgresEra) if row
|
|
90
|
+
# level security silently matched none of the journal rows
|
|
43
91
|
def reset!
|
|
44
92
|
raise Runtime::WiringError, "append-only adapter cannot reset" unless @adapter.respond_to?(:reset!)
|
|
45
93
|
|
|
46
94
|
@adapter.reset!
|
|
47
95
|
end
|
|
48
96
|
|
|
49
|
-
#
|
|
50
|
-
#
|
|
97
|
+
# Reads the events the adapter has durably recorded.
|
|
98
|
+
#
|
|
99
|
+
# Not an endless `def events = ... if ...` — that modifier binds to
|
|
100
|
+
# the whole `def`, not just its body, so it evaluates against
|
|
51
101
|
# `@adapter` while `@adapter` is still nil (class-body time,
|
|
52
102
|
# before `initialize` ever runs) and silently skips defining the
|
|
53
103
|
# method at all. Found live: nothing in this codebase called
|
|
54
104
|
# `AppendOnly#events` before Memory got a `reset!` test that did.
|
|
105
|
+
#
|
|
106
|
+
# @return [Array<Runtime::Event>, nil] recorded events, oldest first; nil when the
|
|
107
|
+
# adapter keeps no event log
|
|
55
108
|
def events
|
|
56
109
|
@adapter.events if @adapter.respond_to?(:events)
|
|
57
110
|
end
|
|
58
111
|
|
|
112
|
+
# Replays the whole journal through `project` to rebuild the projected records.
|
|
113
|
+
#
|
|
59
114
|
# An append is durable before a projection is attempted. Replaying the
|
|
60
115
|
# log restores a snapshot/table after a crash in that small window.
|
|
116
|
+
#
|
|
117
|
+
# @return [Persistence::AppendOnly] self, so a factory can build and recover in one
|
|
118
|
+
# expression
|
|
61
119
|
def recover!
|
|
62
120
|
entries.each { |entry| project(entry) }
|
|
63
121
|
self
|
|
64
122
|
end
|
|
65
123
|
|
|
124
|
+
# Writes one entry to the adapter's durable journal, before any projection of it.
|
|
125
|
+
#
|
|
126
|
+
# @param entry [Persistence::Entry] the save or delete to journal
|
|
127
|
+
# @return [Persistence::Entry] the entry as the adapter returns it; every shipped
|
|
128
|
+
# adapter returns the entry it was given
|
|
129
|
+
# @raise [Runtime::WiringError] from a `RemoteRuntime` adapter, which has no local
|
|
130
|
+
# journal, and from PostgresEra when its era is superseded
|
|
66
131
|
def append(entry) = @adapter.append(entry)
|
|
132
|
+
|
|
133
|
+
# Applies one journaled entry to the adapter's current-state store.
|
|
134
|
+
#
|
|
135
|
+
# @param entry [Persistence::Entry] the save or delete to materialize
|
|
136
|
+
# @return [Object, nil] adapter-defined: Memory, Sqlite, Postgres and PostgresEra answer
|
|
137
|
+
# the saved `Runtime::Instance`; Heki and `SqliteProjection` answer the entry;
|
|
138
|
+
# a delete answers a driver result, the removed record, or nil
|
|
139
|
+
# @raise [Runtime::WiringError] from a `RemoteRuntime` adapter, which projects nothing
|
|
140
|
+
# locally
|
|
67
141
|
def project(entry) = @adapter.project(entry)
|
|
68
142
|
|
|
143
|
+
# Journals an instance's state, projects it, and reports how the write landed.
|
|
144
|
+
#
|
|
69
145
|
# Returns an `Outcome`, not a bare `Instance` — every call site
|
|
70
146
|
# (`CommandInterpreter`/`EntityInterpreter`'s own `step_save`,
|
|
71
147
|
# `RebuildSweep#refresh`) reads it that way.
|
|
72
148
|
#
|
|
73
149
|
# `expected_version:` requests optimistic-concurrency CAS — commit
|
|
74
|
-
# only if the stored record's version still matches what
|
|
150
|
+
# only if the stored record's version still matches what this
|
|
75
151
|
# instance was read at. It is `nil` both when a caller explicitly
|
|
76
152
|
# doesn't want CAS (`RebuildSweep#refresh`'s own projection-field
|
|
77
153
|
# touch-up, which has no `given` to protect) and when the instance
|
|
78
154
|
# is brand new (never read from storage, so `instance.version` is
|
|
79
155
|
# nil) — both cases fall through to the plain, unconditional
|
|
80
156
|
# `project(entry)` below, byte-for-byte today's behavior. Only an
|
|
81
|
-
# adapter that both receives a non-nil `expected_version`
|
|
157
|
+
# adapter that both receives a non-nil `expected_version` and
|
|
82
158
|
# declares `:optimistic_concurrency` gets CAS treatment; every
|
|
83
159
|
# other adapter/call site is unaffected.
|
|
160
|
+
#
|
|
161
|
+
# @param instance [Runtime::Instance] the record to persist; its `state` is shallow
|
|
162
|
+
# copied into the entry
|
|
163
|
+
# @param expected_version [Integer, nil] the version the instance was read at, or nil
|
|
164
|
+
# for an unconditional write
|
|
165
|
+
# @return [Persistence::Outcome] status `:saved` or `:stale`; on `:saved` its `instance`
|
|
166
|
+
# is what the adapter's `project` answered, else the instance passed in; on `:stale`
|
|
167
|
+
# it is the instance passed in. The entry is journaled even when the result is `:stale`
|
|
168
|
+
# @raise [Runtime::WiringError] when the adapter's `append` or `project` refuses
|
|
84
169
|
def save(instance, expected_version: nil)
|
|
85
170
|
entry = Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
|
|
86
171
|
append(entry)
|
|
@@ -94,6 +179,17 @@ module Hecks
|
|
|
94
179
|
Outcome.new(status: :saved, instance: saved || instance)
|
|
95
180
|
end
|
|
96
181
|
|
|
182
|
+
# Writes an instance through the adapter's own existence-check-and-write critical
|
|
183
|
+
# section, so a create cannot race another writer.
|
|
184
|
+
#
|
|
185
|
+
# @param instance [Runtime::Instance] the record to persist
|
|
186
|
+
# @param insert_only [Boolean] true to refuse replacing a record that already exists
|
|
187
|
+
# @return [Persistence::Outcome] status `:inserted`, `:replaced`, or `:conflicted` when
|
|
188
|
+
# `insert_only` met an existing record and nothing was written; `instance` is always
|
|
189
|
+
# the instance passed in
|
|
190
|
+
# @raise [Runtime::WiringError] if the adapter does not both advertise `:atomic_put`
|
|
191
|
+
# and implement it
|
|
192
|
+
# @raise [ArgumentError] if the adapter answers a status outside `Outcome::STATUSES`
|
|
97
193
|
def atomic_put(instance, insert_only: false)
|
|
98
194
|
unless capabilities.include?(:atomic_put) && @adapter.respond_to?(:atomic_put)
|
|
99
195
|
raise Runtime::WiringError,
|
|
@@ -105,6 +201,12 @@ module Hecks
|
|
|
105
201
|
Outcome.new(status: status, instance: instance)
|
|
106
202
|
end
|
|
107
203
|
|
|
204
|
+
# Journals and projects the removal of one record, if it exists.
|
|
205
|
+
#
|
|
206
|
+
# @param id [String, Object] the record's identity; journaled as `id.to_s`
|
|
207
|
+
# @return [Boolean] true when a record was found and deleted, false when there was
|
|
208
|
+
# none and nothing was journaled
|
|
209
|
+
# @raise [Runtime::WiringError] when the adapter's `append` or `project` refuses
|
|
108
210
|
def delete(id)
|
|
109
211
|
return false unless find(id)
|
|
110
212
|
|
|
@@ -114,7 +216,9 @@ module Hecks
|
|
|
114
216
|
true
|
|
115
217
|
end
|
|
116
218
|
|
|
117
|
-
#
|
|
219
|
+
# Records one emitted event durably, when the adapter keeps an event log.
|
|
220
|
+
#
|
|
221
|
+
# Not an endless `def record_event = ... if ...` — same gotcha as
|
|
118
222
|
# `events` above, and it bit for real here: this guard evaluated
|
|
119
223
|
# against `@adapter` at class-body time (nil, always false), so
|
|
120
224
|
# `record_event` was never defined at all. `emission.rb`'s own
|
|
@@ -126,11 +230,17 @@ module Hecks
|
|
|
126
230
|
# found nothing to tail. `sqlite_spec.rb`/`postgres_spec.rb`/
|
|
127
231
|
# `postgres_era_spec.rb` all call `adapter.record_event` directly,
|
|
128
232
|
# bypassing this wrapper — which is exactly why no spec noticed.
|
|
233
|
+
#
|
|
234
|
+
# @param event [Runtime::Event] the event to record
|
|
235
|
+
# @return [Object, nil] adapter-defined write result (an Array for Memory, a driver
|
|
236
|
+
# result for the SQL adapters); nil when the adapter has no `record_event`
|
|
129
237
|
def record_event(event)
|
|
130
238
|
@adapter.record_event(event) if @adapter.respond_to?(:record_event)
|
|
131
239
|
end
|
|
132
240
|
|
|
133
|
-
#
|
|
241
|
+
# Runs the block inside the adapter's transaction, or plainly when it has none.
|
|
242
|
+
#
|
|
243
|
+
# One COMMIT boundary for save + emit + outbox — `Interpreting#
|
|
134
244
|
# run_dispatch_order` runs the `save` and `emit` steps inside
|
|
135
245
|
# this block, so an adapter that owns a real transaction
|
|
136
246
|
# (Sqlite, Postgres) commits the aggregate row, its journal
|
|
@@ -141,39 +251,93 @@ module Hecks
|
|
|
141
251
|
# re-entrant (both SQL adapters check for an open transaction
|
|
142
252
|
# first) — a reaction dispatched from inside a drain never nests
|
|
143
253
|
# here anyway, because draining happens after this block returns.
|
|
254
|
+
#
|
|
255
|
+
# @yield the writes to commit together; an exception raised inside rolls back an
|
|
256
|
+
# adapter-owned transaction and propagates
|
|
257
|
+
# @return [Object] adapter-defined; the block's own value for every adapter without a
|
|
258
|
+
# `transaction` and for Memory
|
|
144
259
|
def transaction(&)
|
|
145
260
|
return @adapter.transaction(&) if @adapter.respond_to?(:transaction)
|
|
146
261
|
|
|
147
262
|
yield
|
|
148
263
|
end
|
|
149
264
|
|
|
150
|
-
#
|
|
265
|
+
# Runs the block while holding the adapter's cross-process write lock.
|
|
266
|
+
#
|
|
267
|
+
# Only an adapter advertising `:cross_process_lock` (PostgresEra —
|
|
151
268
|
# see ADR 0036) implements this; `run_dispatch_order_with_isolation`
|
|
152
269
|
# (runtime/interpreting.rb) checks `capabilities` before ever
|
|
153
270
|
# calling it, so the plain `yield` fallback here only guards
|
|
154
271
|
# against a stray direct call, not the real dispatch path.
|
|
272
|
+
#
|
|
273
|
+
# @yield the dispatch to run with writers serialised
|
|
274
|
+
# @return [Object] adapter-defined; the block's own value when the adapter has no
|
|
275
|
+
# `with_write_lock`
|
|
155
276
|
def with_write_lock(&)
|
|
156
277
|
return @adapter.with_write_lock(&) if @adapter.respond_to?(:with_write_lock)
|
|
157
278
|
|
|
158
279
|
yield
|
|
159
280
|
end
|
|
160
281
|
|
|
161
|
-
#
|
|
282
|
+
# **The outbox contract** — four optional adapter methods, probed
|
|
162
283
|
# together the way `save_saga`/`delete_saga`/`each_saga` are
|
|
163
284
|
# (`Registry::SagaPersistence`): an adapter either has an outbox
|
|
164
285
|
# or it doesn't, never half of one. See `Runtime::Outbox`.
|
|
165
286
|
OUTBOX_METHODS = %i[outbox_enqueue outbox_claim outbox_settle outbox_rows].freeze
|
|
166
287
|
|
|
288
|
+
# Reports whether the adapter implements the whole outbox contract; the answer is
|
|
289
|
+
# memoized per repository.
|
|
290
|
+
#
|
|
291
|
+
# @return [Boolean] true only when the adapter responds to all four `OUTBOX_METHODS`
|
|
167
292
|
def outbox?
|
|
168
293
|
@outbox = OUTBOX_METHODS.all? { |method| @adapter.respond_to?(method) } if @outbox.nil?
|
|
169
294
|
@outbox
|
|
170
295
|
end
|
|
171
296
|
|
|
297
|
+
# Stores pending outbox rows, skipping any whose `delivery_id` is already held.
|
|
298
|
+
#
|
|
299
|
+
# Call only after `outbox?` answers true; the other three outbox methods share that
|
|
300
|
+
# precondition.
|
|
301
|
+
#
|
|
302
|
+
# @param rows [Array<Runtime::Outbox::Row>] the rows to enqueue, one per event and consumer
|
|
303
|
+
# @return [Array<Runtime::Outbox::Row>] the rows actually stored, with `id` assigned;
|
|
304
|
+
# duplicates are dropped
|
|
172
305
|
def outbox_enqueue(rows) = @adapter.outbox_enqueue(rows)
|
|
306
|
+
|
|
307
|
+
# Moves one pending outbox row to `claimed` and counts the attempt.
|
|
308
|
+
#
|
|
309
|
+
# @param id [Integer] the row id assigned by `outbox_enqueue`
|
|
310
|
+
# @return [Boolean] true when this call claimed the row; false when it is missing or
|
|
311
|
+
# no longer pending
|
|
173
312
|
def outbox_claim(id) = @adapter.outbox_claim(id)
|
|
313
|
+
|
|
314
|
+
# Records the final status of a claimed outbox row.
|
|
315
|
+
#
|
|
316
|
+
# @param id [Integer] the row id assigned by `outbox_enqueue`
|
|
317
|
+
# @param status [String, Symbol] the settled status; `Runtime::Outbox` passes
|
|
318
|
+
# `"delivered"` or `"failed"`
|
|
319
|
+
# @param error [String, nil] failure text, `"ErrorClass: message"`; nil on success
|
|
320
|
+
# @return [Boolean] true when a row with that id was updated
|
|
174
321
|
def outbox_settle(id, status:, error: nil) = @adapter.outbox_settle(id, status: status, error: error)
|
|
322
|
+
|
|
323
|
+
# Lists this aggregate's outbox rows, oldest first.
|
|
324
|
+
#
|
|
325
|
+
# @param status [String, Symbol, nil] keep only rows with this status; nil for all rows
|
|
326
|
+
# @return [Array<Runtime::Outbox::Row>] copies of the stored rows; `[]` when none match
|
|
175
327
|
def outbox_rows(status: nil) = @adapter.outbox_rows(status: status)
|
|
176
328
|
|
|
329
|
+
# Answers a read model from the adapter's own projected tables, when it can.
|
|
330
|
+
#
|
|
331
|
+
# @param domain [String, Symbol] name of the domain declaring the read model
|
|
332
|
+
# @param model [Bluebook::ReadModel] the read model to answer
|
|
333
|
+
# @param args [Hash{Symbol => Object}] the read model's arguments, including its
|
|
334
|
+
# reference argument
|
|
335
|
+
# @param bluebook [Bluebook::Chapter, nil] the domain's bluebook, which the SQLite
|
|
336
|
+
# projection needs to find the included aggregates
|
|
337
|
+
# @return [Array<Hash>, nil] a one-element Array holding the report Hash keyed by head
|
|
338
|
+
# name; nil when the adapter has no `query_read_model`
|
|
339
|
+
# @raise [ArgumentError] if the adapter answers natively and `bluebook` is nil
|
|
340
|
+
# @raise [Runtime::NotFound] if the referenced root record is not in the projection
|
|
177
341
|
def query_read_model(domain, model, args, bluebook = nil)
|
|
178
342
|
return unless @adapter.respond_to?(:query_read_model)
|
|
179
343
|
|
|
@@ -10,6 +10,18 @@ module Hecks
|
|
|
10
10
|
module BindingPolicy
|
|
11
11
|
module_function
|
|
12
12
|
|
|
13
|
+
# Picks the one bind that names an aggregate's authoritative store.
|
|
14
|
+
#
|
|
15
|
+
# A domain with no hecksagon gets the default in-memory bind. A domain that has one
|
|
16
|
+
# must bind the aggregate exactly once without a role.
|
|
17
|
+
#
|
|
18
|
+
# @param registry [Runtime::Registry] the registry holding the domain's hecksagon
|
|
19
|
+
# @param domain [String, Symbol] name of the domain the aggregate belongs to
|
|
20
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose binding is wanted
|
|
21
|
+
# @return [Bluebook::Bind] the authoritative `persisted_by` bind, or the default
|
|
22
|
+
# `Memory` bind when the domain declares no hecksagon
|
|
23
|
+
# @raise [Runtime::WiringError] if the hecksagon has no `persisted_by` bind for the
|
|
24
|
+
# aggregate, more or fewer than one bind without a role, or any bind with a role
|
|
13
25
|
def resolve(registry, domain, aggregate)
|
|
14
26
|
hexagon = registry.hecksagon(domain)
|
|
15
27
|
return default_binding(aggregate) unless hexagon
|
|
@@ -24,10 +36,19 @@ module Hecks
|
|
|
24
36
|
authoritative.first
|
|
25
37
|
end
|
|
26
38
|
|
|
39
|
+
# Builds the bind an aggregate gets when its domain declares no hecksagon.
|
|
40
|
+
#
|
|
41
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate to bind
|
|
42
|
+
# @return [Bluebook::Bind] a roleless `persisted_by` bind to `DEFAULT_ADAPTER`
|
|
27
43
|
def default_binding(aggregate)
|
|
28
44
|
Bluebook::Bind.new(aggregate: aggregate.hecks_name, verb: VERB, adapter: DEFAULT_ADAPTER)
|
|
29
45
|
end
|
|
30
46
|
|
|
47
|
+
# Builds, without raising, the error for an aggregate a hecksagon leaves unbound.
|
|
48
|
+
#
|
|
49
|
+
# @param domain [String, Symbol] name of the domain, used in the message
|
|
50
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate with no bind
|
|
51
|
+
# @return [Runtime::WiringError] an error whose message says how to bind the aggregate
|
|
31
52
|
def missing_binding(domain, aggregate)
|
|
32
53
|
Runtime::WiringError.new(
|
|
33
54
|
"#{domain}::#{aggregate.hecks_name} has no #{VERB} bind. #{domain} declares a " \
|
|
@@ -37,6 +58,13 @@ module Hecks
|
|
|
37
58
|
)
|
|
38
59
|
end
|
|
39
60
|
|
|
61
|
+
# Builds, without raising, the error for an aggregate whose count of roleless binds
|
|
62
|
+
# is not one.
|
|
63
|
+
#
|
|
64
|
+
# @param domain [String, Symbol] name of the domain, used in the message
|
|
65
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate with the wrong bind count
|
|
66
|
+
# @param authoritative [Array<Bluebook::Bind>] the roleless binds found; may be empty
|
|
67
|
+
# @return [Runtime::WiringError] an error whose message reports the count
|
|
40
68
|
def ambiguous_binding(domain, aggregate, authoritative)
|
|
41
69
|
Runtime::WiringError.new(
|
|
42
70
|
"#{domain}::#{aggregate.hecks_name} has #{authoritative.size} authoritative #{VERB} bindings. " \
|
|
@@ -44,6 +72,12 @@ module Hecks
|
|
|
44
72
|
)
|
|
45
73
|
end
|
|
46
74
|
|
|
75
|
+
# Builds, without raising, the error for binds that carry a role this port ignores.
|
|
76
|
+
#
|
|
77
|
+
# @param domain [String, Symbol] name of the domain, used in the message
|
|
78
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate carrying the binds
|
|
79
|
+
# @param bindings [Array<Bluebook::Bind>] the binds that declare a role
|
|
80
|
+
# @return [Runtime::WiringError] an error whose message lists each role
|
|
47
81
|
def unsupported_roles(domain, aggregate, bindings)
|
|
48
82
|
Runtime::WiringError.new(
|
|
49
83
|
"#{domain}::#{aggregate.hecks_name} uses persistence role#{'s' unless bindings.size == 1} " \
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
require_relative "state_codec"
|
|
2
|
+
require_relative "../../runtime/errors"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Ports
|
|
6
|
+
module Persistence
|
|
7
|
+
# No adapter can build an `Instance` from undecoded state (Phase 2,
|
|
8
|
+
# Track A, PR A3). Routing every adapter through `StateCodec` is a
|
|
9
|
+
# convention until something refuses the bypass; this is that
|
|
10
|
+
# something, and it is installed by `RepositoryFactory.build` — the
|
|
11
|
+
# one place every runtime repository is made — so no adapter can opt
|
|
12
|
+
# out of it.
|
|
13
|
+
#
|
|
14
|
+
# ## The mechanism, two halves
|
|
15
|
+
#
|
|
16
|
+
# 1. `guard!(adapter)` extends the adapter object (not its class,
|
|
17
|
+
# not a proxy — `is_a?`, `class`, and `===` stay the adapter's
|
|
18
|
+
# own) with a module that wraps every public method the adapter's
|
|
19
|
+
# class defines. Each call runs inside the boundary: a
|
|
20
|
+
# thread-local flag, re-entrant, restored on the way out. A block
|
|
21
|
+
# the caller passes (`transaction`, `with_write_lock`,
|
|
22
|
+
# `each_saga`) runs outside it — that block is the dispatch
|
|
23
|
+
# itself (hydrate, entity views, mutation), not adapter code.
|
|
24
|
+
# Reaching the adapter through `repository.adapter` (the query
|
|
25
|
+
# port, saga persistence, `bin/heki_compact`) is guarded all the
|
|
26
|
+
# same: the wrapper is on the object.
|
|
27
|
+
#
|
|
28
|
+
# 2. `Runtime::Instance#initialize` asks `check_state!` whenever it
|
|
29
|
+
# is handed `state:`. Outside the boundary that is a no-op; inside
|
|
30
|
+
# it, state `StateCodec.decoded?` rejects refuses by name. An
|
|
31
|
+
# `entries` answer is checked the same way on its way out, since a
|
|
32
|
+
# journal `Entry` is not an `Instance`.
|
|
33
|
+
#
|
|
34
|
+
# ## Checked, not silently decoded
|
|
35
|
+
#
|
|
36
|
+
# An adapter that forgets the codec is a bug in that adapter, and
|
|
37
|
+
# decoding for it here would hide the forgetting. Hydration does not
|
|
38
|
+
# respell keys either (A4) — `Value.hydrate` refuses a non-Symbol
|
|
39
|
+
# top-level key everywhere — so this boundary's deep check and
|
|
40
|
+
# hydration's shallow one agree.
|
|
41
|
+
module CodecBoundary
|
|
42
|
+
KEY = :hecks_persistence_codec_boundary
|
|
43
|
+
|
|
44
|
+
module_function
|
|
45
|
+
|
|
46
|
+
# Extends an adapter object so every public method its class defines runs inside the
|
|
47
|
+
# boundary; guarding an adapter twice wraps it once.
|
|
48
|
+
#
|
|
49
|
+
# @param adapter [Object] a driven persistence adapter instance
|
|
50
|
+
# @return [Object] the same adapter object, now including `Guarded`
|
|
51
|
+
def guard!(adapter)
|
|
52
|
+
adapter.extend(wrapper_for(adapter.class)) unless adapter.singleton_class.include?(Guarded)
|
|
53
|
+
adapter
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Reports whether the current thread is executing inside a guarded adapter call.
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean] true between entering a guarded adapter method and yielding to the
|
|
59
|
+
# caller's block or returning
|
|
60
|
+
def active? = Thread.current[KEY] == true
|
|
61
|
+
|
|
62
|
+
# Runs the block with the boundary switched on for this thread, restoring the earlier
|
|
63
|
+
# setting afterwards.
|
|
64
|
+
#
|
|
65
|
+
# @yield adapter code whose `Instance` construction is to be checked
|
|
66
|
+
# @return [Object] the block's value
|
|
67
|
+
def within(&) = with_flag(true, &)
|
|
68
|
+
|
|
69
|
+
# Runs the block with the boundary switched off for this thread, restoring the earlier
|
|
70
|
+
# setting afterwards.
|
|
71
|
+
#
|
|
72
|
+
# @yield caller code, such as a dispatch running inside the adapter's transaction
|
|
73
|
+
# @return [Object] the block's value
|
|
74
|
+
def outside(&) = with_flag(false, &)
|
|
75
|
+
|
|
76
|
+
# Refuses state an adapter is about to build an `Instance` from unless it is decoded;
|
|
77
|
+
# outside the boundary it checks nothing.
|
|
78
|
+
#
|
|
79
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the state
|
|
80
|
+
# belongs to
|
|
81
|
+
# @param state [Hash, Runtime::Value] the state handed to `Runtime::Instance.new`
|
|
82
|
+
# @return [nil] when the boundary is inactive or the state is decoded
|
|
83
|
+
# @raise [Runtime::WiringError] if the boundary is active and
|
|
84
|
+
# `StateCodec.decoded?` rejects the state
|
|
85
|
+
def check_state!(aggregate, state)
|
|
86
|
+
return unless active?
|
|
87
|
+
return if StateCodec.decoded?(aggregate, state)
|
|
88
|
+
|
|
89
|
+
raise Runtime::WiringError,
|
|
90
|
+
"a persistence adapter built a #{aggregate.name} record from undecoded state " \
|
|
91
|
+
"(#{state.keys.inspect}) — decode stored state through " \
|
|
92
|
+
"Hecks::Ports::Persistence::StateCodec.decode before handing it to Runtime::Instance"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Refuses a guarded adapter's `entries` answer if any entry's state is undecoded.
|
|
96
|
+
#
|
|
97
|
+
# @param adapter [Object] the guarded adapter, asked for its `aggregate` and class name
|
|
98
|
+
# @param entries [Array<Persistence::Entry>, Object] what the adapter's `entries`
|
|
99
|
+
# returned; anything that is not an Array passes through unchecked
|
|
100
|
+
# @return [Array<Persistence::Entry>, Object] `entries` itself
|
|
101
|
+
# @raise [Runtime::WiringError] if an `Entry` in the Array carries state
|
|
102
|
+
# `StateCodec.decoded?` rejects
|
|
103
|
+
def check_entries!(adapter, entries)
|
|
104
|
+
return entries unless entries.is_a?(Array)
|
|
105
|
+
|
|
106
|
+
entries.each do |entry|
|
|
107
|
+
next unless entry.is_a?(Entry) && !StateCodec.decoded?(adapter.aggregate, entry.state)
|
|
108
|
+
|
|
109
|
+
raise Runtime::WiringError,
|
|
110
|
+
"#{adapter.class} answered a #{adapter.aggregate.name} journal entry #{entry.id.inspect} with " \
|
|
111
|
+
"undecoded state — decode it through Hecks::Ports::Persistence::StateCodec.decode"
|
|
112
|
+
end
|
|
113
|
+
entries
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Sets the thread-local boundary flag for the length of the block, which is what makes
|
|
117
|
+
# the boundary re-entrant.
|
|
118
|
+
#
|
|
119
|
+
# @param value [Boolean] true to switch the boundary on, false to switch it off
|
|
120
|
+
# @yield the code to run under that setting
|
|
121
|
+
# @return [Object] the block's value; the earlier flag is restored even when the
|
|
122
|
+
# block raises
|
|
123
|
+
def with_flag(value)
|
|
124
|
+
previous = Thread.current[KEY]
|
|
125
|
+
Thread.current[KEY] = value
|
|
126
|
+
yield
|
|
127
|
+
ensure
|
|
128
|
+
Thread.current[KEY] = previous
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Marks a guarded adapter so a second `guard!` (a projection and an
|
|
132
|
+
# authoritative repository built over one object) never wraps twice.
|
|
133
|
+
module Guarded; end
|
|
134
|
+
|
|
135
|
+
# Filled lazily under `WRAPPERS_LOCK`, one entry per adapter class —
|
|
136
|
+
# a cache, deliberately mutable.
|
|
137
|
+
WRAPPERS = {} # rubocop:disable Style/MutableConstant
|
|
138
|
+
WRAPPERS_LOCK = Mutex.new
|
|
139
|
+
|
|
140
|
+
# Builds, or fetches from the cache, the module that guards one adapter class.
|
|
141
|
+
#
|
|
142
|
+
# One wrapper module per adapter class, built once: every public
|
|
143
|
+
# instance method the class (and its ancestors below Object)
|
|
144
|
+
# defines, each forwarding to `super` inside the boundary.
|
|
145
|
+
#
|
|
146
|
+
# @param klass [Class] the adapter's class
|
|
147
|
+
# @return [Module] an anonymous module including `Guarded`, meant to be `extend`ed onto
|
|
148
|
+
# instances of `klass`
|
|
149
|
+
def wrapper_for(klass)
|
|
150
|
+
WRAPPERS_LOCK.synchronize do
|
|
151
|
+
WRAPPERS[klass] ||= Module.new do
|
|
152
|
+
include Guarded
|
|
153
|
+
|
|
154
|
+
(klass.public_instance_methods - Object.public_instance_methods).each do |name|
|
|
155
|
+
define_method(name) do |*args, **kwargs, &block|
|
|
156
|
+
result = CodecBoundary.within { super(*args, **kwargs, &CodecBoundary.outside_block(block)) }
|
|
157
|
+
name == :entries ? CodecBoundary.check_entries!(self, result) : result
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Wraps the caller's own block so it runs outside the boundary
|
|
165
|
+
# whenever the adapter yields to it.
|
|
166
|
+
#
|
|
167
|
+
# @param block [Proc, nil] the block the caller passed to the adapter method
|
|
168
|
+
# @return [Proc, nil] a proc forwarding its arguments to `block` under `outside`; nil
|
|
169
|
+
# when no block was given
|
|
170
|
+
def outside_block(block)
|
|
171
|
+
return nil unless block
|
|
172
|
+
|
|
173
|
+
proc { |*yielded, **yielded_kwargs, &inner| outside { block.call(*yielded, **yielded_kwargs, &inner) } }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -10,6 +10,10 @@ module Hecks
|
|
|
10
10
|
# `Runtime::StaleWrite` (runtime/errors.rb) and `AppendOnly#save`.
|
|
11
11
|
STATUSES = %i[inserted replaced updated conflicted missing saved stale].freeze
|
|
12
12
|
|
|
13
|
+
# @param status [Symbol, String] one of `STATUSES`; stored as a Symbol
|
|
14
|
+
# @param instance [Runtime::Instance, Object, nil] the record the write concerned, as
|
|
15
|
+
# the repository reports it; nil when there is none
|
|
16
|
+
# @raise [ArgumentError] if `status` is not one of `STATUSES`
|
|
13
17
|
def initialize(status:, instance: nil)
|
|
14
18
|
normalized = status.to_sym
|
|
15
19
|
raise ArgumentError, "unknown persistence outcome #{status.inspect}" unless STATUSES.include?(normalized)
|