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
|
@@ -11,7 +11,7 @@ module Hecks
|
|
|
11
11
|
module Ports
|
|
12
12
|
module Persistence
|
|
13
13
|
module Plugins
|
|
14
|
-
# ADR 0033 — requiring
|
|
14
|
+
# ADR 0033 — requiring this file is installing the plugin. Nothing in
|
|
15
15
|
# Hecks core requires it; an app that binds `PostgresEra`, or wants
|
|
16
16
|
# schema-translation support at all, requires it explicitly — the
|
|
17
17
|
# same shape every adapter-specific spec fixture already uses to
|
|
@@ -19,6 +19,8 @@ module Hecks
|
|
|
19
19
|
module Era
|
|
20
20
|
module_function
|
|
21
21
|
|
|
22
|
+
# Registers this plugin's era gates on one boot's gate list.
|
|
23
|
+
#
|
|
22
24
|
# `Runtime::Loader.run_boot_gates!` asks every loaded persistence
|
|
23
25
|
# plugin to contribute here, generically — it never mentions
|
|
24
26
|
# `EraCheck` or "era" by name. Two gates, both `:pre_verify`:
|
|
@@ -29,11 +31,19 @@ module Hecks
|
|
|
29
31
|
# unchanged) — this is the rich, adapter-aware version of that
|
|
30
32
|
# check; `Runtime::Loader`'s own structural backstop (plain
|
|
31
33
|
# `Bluebook::Translation` data, no plugin-specific class) only
|
|
32
|
-
# ever fires when
|
|
34
|
+
# ever fires when no persistence plugin is loaded at all.
|
|
33
35
|
#
|
|
34
36
|
# `:era_check` — conditional, exactly ADR 0031's own gate,
|
|
35
37
|
# unchanged: registered only when this registry has an aggregate
|
|
36
38
|
# actually bound to a lineage-capable adapter.
|
|
39
|
+
#
|
|
40
|
+
# @param registry [Runtime::Registry] the registry being booted, asked whether any
|
|
41
|
+
# bluebook's first aggregate is bound to a lineage-capable adapter
|
|
42
|
+
# @param gates [Runtime::BootGates] this boot's gate list, registered onto in place
|
|
43
|
+
# @return [Runtime::BootGates, nil] `gates` when `:era_check` was registered; nil when
|
|
44
|
+
# the registry binds nothing lineage-capable, so only `:era_compute_rules` was added
|
|
45
|
+
# @raise [Runtime::WiringError] if an aggregate's persistence binding is missing,
|
|
46
|
+
# ambiguous, or carries an unsupported role
|
|
37
47
|
def contribute_boot_gates(registry, gates)
|
|
38
48
|
gates.register(:era_compute_rules, lambda { |reg, _dir|
|
|
39
49
|
Runtime::EraCheck.check_compute_rules_for_registry!(reg)
|
|
@@ -3,12 +3,12 @@ require_relative "../../runtime/registry"
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Ports
|
|
5
5
|
module Persistence
|
|
6
|
-
#
|
|
6
|
+
# The other shape an adapter can be. `AppendOnly` names one shape
|
|
7
7
|
# already — something that stores bytes, locally (Postgres, SQLite,
|
|
8
8
|
# Heki) or over the network (D1), doesn't matter, the point is it
|
|
9
9
|
# does real local interpretation and has real entries to replay.
|
|
10
10
|
# This names the second, different shape: not "storage reached
|
|
11
|
-
# remotely," but the real
|
|
11
|
+
# remotely," but the real interpreter itself living behind a call
|
|
12
12
|
# boundary — `append`/`project` don't do partial local work and
|
|
13
13
|
# then fail, they raise unconditionally, because there is no local
|
|
14
14
|
# write-ahead log to have; `entries` is always `[]` for the same
|
|
@@ -28,6 +28,10 @@ module Hecks
|
|
|
28
28
|
# `Runtime::RemoteDispatcher`'s own use) instead of comparing
|
|
29
29
|
# adapter names by string.
|
|
30
30
|
module RemoteRuntime
|
|
31
|
+
# Refuses every local write; `project` is an alias and refuses the same way.
|
|
32
|
+
#
|
|
33
|
+
# @return [void] never returns
|
|
34
|
+
# @raise [Runtime::WiringError] always, pointing the caller at `Runtime::RemoteDispatcher`
|
|
31
35
|
def append(*)
|
|
32
36
|
raise Runtime::WiringError,
|
|
33
37
|
"#{self.class.name} is a remote-runtime delegate — dispatch through " \
|
|
@@ -35,6 +39,9 @@ module Hecks
|
|
|
35
39
|
end
|
|
36
40
|
alias project append
|
|
37
41
|
|
|
42
|
+
# Reports an empty journal, since a remote-runtime delegate keeps no local log.
|
|
43
|
+
#
|
|
44
|
+
# @return [Array] always `[]`
|
|
38
45
|
def entries = []
|
|
39
46
|
end
|
|
40
47
|
end
|
|
@@ -1,27 +1,50 @@
|
|
|
1
1
|
require_relative "append_only"
|
|
2
|
+
require_relative "codec_boundary"
|
|
2
3
|
|
|
3
4
|
module Hecks
|
|
4
5
|
module Ports
|
|
5
6
|
module Persistence
|
|
6
7
|
# Turns a declared adapter binding plus its world configuration into a
|
|
7
8
|
# concrete repository. Selection policy stays out of adapter creation.
|
|
9
|
+
#
|
|
10
|
+
# Every adapter it builds is guarded (`CodecBoundary.guard!`) before
|
|
11
|
+
# anything else touches it — `recover!` included — so no adapter a
|
|
12
|
+
# runtime reaches can build an `Instance` from undecoded state.
|
|
8
13
|
module RepositoryFactory
|
|
9
14
|
module_function
|
|
10
15
|
|
|
16
|
+
# Instantiates the adapter a bind names, guards it, and wraps it as a repository.
|
|
17
|
+
#
|
|
18
|
+
# @param registry [Runtime::Registry] the registry supplying the adapter class, the
|
|
19
|
+
# domain's world settings, its resolved eras and the root path
|
|
20
|
+
# @param domain [String, Symbol] name of the domain the aggregate belongs to
|
|
21
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate to persist
|
|
22
|
+
# @param bind [Bluebook::Bind] the bind naming the adapter, as `BindingPolicy.resolve`
|
|
23
|
+
# or the projection port chose it
|
|
24
|
+
# @param recover [Boolean] true to replay the journal through `project` before returning
|
|
25
|
+
# @param settings_verb [String] the verb whose world settings configure the adapter;
|
|
26
|
+
# `"persisted_by"` by default, `Ports::Projection::VERB` for a projection
|
|
27
|
+
# @return [Persistence::AppendOnly] the repository over the guarded adapter
|
|
28
|
+
# @raise [Runtime::WiringError] if the bind's verb or settings fail the registry's
|
|
29
|
+
# checks, no `Hecks::Adapters` constant matches the adapter name, or the adapter
|
|
30
|
+
# lacks `append`, `project` or `entries`
|
|
11
31
|
def build(registry, domain, aggregate, bind, recover: true, settings_verb: VERB)
|
|
12
32
|
registry.check_verb(bind)
|
|
13
33
|
settings = (registry.world(domain)&.for_binding(settings_verb, bind.adapter) || {})
|
|
14
34
|
.reject { |key, _| key.to_sym == :role }
|
|
15
35
|
registry.check_settings(bind, settings)
|
|
16
|
-
# The domain
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
36
|
+
# The domain, the resolved era, and (for an old checkout) the era
|
|
37
|
+
# that superseded it ride along after the declared-settings
|
|
38
|
+
# check: a lineage adapter journals per domain, writes into its
|
|
39
|
+
# own era's partition, and refuses to write at all once that era
|
|
40
|
+
# is superseded (`PostgresEra#append`, BUG#24) — none of which a
|
|
41
|
+
# world's settings carry.
|
|
20
42
|
adapter = registry.adapter_class(bind.adapter)
|
|
21
43
|
.new(aggregate: aggregate,
|
|
22
|
-
settings: settings.merge(domain: domain.to_s, era: registry.resolved_eras[domain.to_s]
|
|
44
|
+
settings: settings.merge(domain: domain.to_s, era: registry.resolved_eras[domain.to_s],
|
|
45
|
+
superseded_by: registry.superseded_eras[domain.to_s]),
|
|
23
46
|
root: registry.root)
|
|
24
|
-
repository = AppendOnly.new(adapter)
|
|
47
|
+
repository = AppendOnly.new(CodecBoundary.guard!(adapter))
|
|
25
48
|
recover ? repository.recover! : repository
|
|
26
49
|
end
|
|
27
50
|
end
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "../../runtime/value"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Ports
|
|
6
|
+
module Persistence
|
|
7
|
+
# One spelling of an aggregate's state across the store boundary
|
|
8
|
+
# (Phase 2, Track A, PR A2). This codec is the single, IR-driven
|
|
9
|
+
# answer every adapter converges on, because adapters left to decode
|
|
10
|
+
# their own way disagree: symbolizing the top level only (a Heki head,
|
|
11
|
+
# a journal reader), symbolizing deep (a SQL or Lambda head), or never
|
|
12
|
+
# serializing at all (Memory's shallow `state.dup`).
|
|
13
|
+
# spec/ports/persistence_legacy_decode_spec.rb pins that bytes written
|
|
14
|
+
# without the codec still decode to the one canonical shape. Every
|
|
15
|
+
# adapter writes through `encode` and reads through `decode`
|
|
16
|
+
# (Memory through `copy`; PR A3), and `CodecBoundary` — installed on every
|
|
17
|
+
# adapter `RepositoryFactory.build` makes — refuses an `Instance`
|
|
18
|
+
# built inside an adapter call from state `decoded?` rejects.
|
|
19
|
+
#
|
|
20
|
+
# ## The three operations
|
|
21
|
+
#
|
|
22
|
+
# - `encode` — canonical and JSON-ready: string keys at every depth,
|
|
23
|
+
# `Runtime::Value`s materialized, only JSON scalars at the leaves.
|
|
24
|
+
# Needs no IR (JSON has one spelling), but takes it for symmetry.
|
|
25
|
+
# - `decode` — walks the aggregate's IR: attributes, value objects
|
|
26
|
+
# (their fields, recursively), `list_of` value objects and entities
|
|
27
|
+
# (entity fields, nested entities, the entity's own lifecycle),
|
|
28
|
+
# references, the lifecycle field, and projected fields. A declared
|
|
29
|
+
# key becomes a symbol at every depth, whichever spelling arrived.
|
|
30
|
+
# - `copy` — `decode(encode(state))`: what a durable adapter would hand
|
|
31
|
+
# back, for Memory, with no JSON text in between.
|
|
32
|
+
#
|
|
33
|
+
# ## What decode never does
|
|
34
|
+
#
|
|
35
|
+
# It never invents a key. A declared field absent from the stored
|
|
36
|
+
# state stays absent — not a present nil — because the runtime reads
|
|
37
|
+
# absence as "this record predates the field": `Instance.
|
|
38
|
+
# hydrate_with_defaults` fills a declared `default:` only when the key
|
|
39
|
+
# is missing (spec/runtime/hydrate_defaults_spec.rb), Era translation
|
|
40
|
+
# backfills only `unless state.key?` (era/lineage.rb#translate), and a
|
|
41
|
+
# required declared-but-absent field reads as a named refusal rather
|
|
42
|
+
# than nil (spec/runtime/attribute_absence_spec.rb). A present nil
|
|
43
|
+
# would silently suppress all three. For the same reason it never
|
|
44
|
+
# drops a key, nil or not: a stored nil stays a stored nil.
|
|
45
|
+
#
|
|
46
|
+
# It never touches an undeclared key's value — a retired field, or a
|
|
47
|
+
# member a value object no longer declares, is exactly what an Era
|
|
48
|
+
# translation (rename/move/drop) still has to read. Its key keeps its
|
|
49
|
+
# spelling below the top level; at the top level every key is a
|
|
50
|
+
# symbol, declared or not, because every adapter has always
|
|
51
|
+
# symbolized the top level and `Lineage#translate` reads retired
|
|
52
|
+
# top-level names as symbols.
|
|
53
|
+
#
|
|
54
|
+
# ## When both spellings arrive
|
|
55
|
+
#
|
|
56
|
+
# When a hash carries both spellings of one declared key, the symbol
|
|
57
|
+
# spelling wins: it can only have been written by Ruby after the
|
|
58
|
+
# string one was read.
|
|
59
|
+
module StateCodec
|
|
60
|
+
JSON_SCALARS = [String, Integer, Float, TrueClass, FalseClass, NilClass].freeze
|
|
61
|
+
|
|
62
|
+
module_function
|
|
63
|
+
|
|
64
|
+
# Converts state into its canonical JSON-ready form for a durable adapter to write.
|
|
65
|
+
#
|
|
66
|
+
# @param _aggregate [Bluebook::Aggregate, Bluebook::Entity] unused; taken so `encode`
|
|
67
|
+
# and `decode` have one signature
|
|
68
|
+
# @param state [Hash, Runtime::Value, nil] the state to store; nil for a delete entry
|
|
69
|
+
# @return [Hash{String => Object}, nil] a new Hash with String keys at every depth and
|
|
70
|
+
# only JSON scalars, Arrays and Hashes below; nil when `state` is nil
|
|
71
|
+
def encode(_aggregate, state) = encode_value(state)
|
|
72
|
+
|
|
73
|
+
# Respells stored state into the declared shape by walking the aggregate's IR.
|
|
74
|
+
#
|
|
75
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
76
|
+
# declarations name the keys to symbolize
|
|
77
|
+
# @param raw [Hash, Object, nil] parsed stored state, with keys in either spelling
|
|
78
|
+
# @return [Hash{Symbol => Object}, Object, nil] a new Hash with every top-level key and
|
|
79
|
+
# every declared nested key a Symbol; anything that is not a Hash is returned as given
|
|
80
|
+
def decode(aggregate, raw)
|
|
81
|
+
return raw unless raw.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
fields = declared_top_level(aggregate)
|
|
84
|
+
decode_hash(aggregate, fields, raw, symbolize_undeclared: true)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Deep-copies state into the shape a durable adapter would read back, without JSON text.
|
|
88
|
+
#
|
|
89
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
90
|
+
# declarations name the keys to symbolize
|
|
91
|
+
# @param state [Hash, Runtime::Value, nil] live state, such as `Instance#state`
|
|
92
|
+
# @return [Hash{Symbol => Object}, nil] `decode(encode(state))`, sharing no Hash or
|
|
93
|
+
# Array with `state`; nil when `state` is nil
|
|
94
|
+
def copy(aggregate, state) = decode(aggregate, encode(aggregate, state))
|
|
95
|
+
|
|
96
|
+
# Maps every declared top-level field name to its attribute.
|
|
97
|
+
#
|
|
98
|
+
# The field walk Sqlite::Codec#persisted_fields does for columns,
|
|
99
|
+
# as name => Attribute (nil for the lifecycle field and projected
|
|
100
|
+
# fields: bare scalars with no attribute of their own). The same
|
|
101
|
+
# three sources, the same precedence — an attribute that happens to
|
|
102
|
+
# share the lifecycle's or a projected field's name keeps its type.
|
|
103
|
+
# An entity (no `projected_fields` of its own) gets its entity
|
|
104
|
+
# field set — `decoded?` is asked about any `Instance`, and an
|
|
105
|
+
# entity is "structurally interchangeable with an aggregate".
|
|
106
|
+
#
|
|
107
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct to walk
|
|
108
|
+
# @return [Hash{Symbol => Bluebook::Attribute, nil}] a new Hash of field name to
|
|
109
|
+
# attribute; nil marks the lifecycle field or a projected field with no attribute
|
|
110
|
+
def declared_top_level(aggregate)
|
|
111
|
+
fields = entity_fields(aggregate)
|
|
112
|
+
return fields unless aggregate.respond_to?(:projected_fields)
|
|
113
|
+
|
|
114
|
+
aggregate.projected_fields.each { |field| fields[field.name.to_sym] = nil unless fields.key?(field.name.to_sym) }
|
|
115
|
+
fields
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Checks whether state already has the shape `decode` produces.
|
|
119
|
+
#
|
|
120
|
+
# Whether `decode` would hand `state` back unchanged — every
|
|
121
|
+
# top-level key a Symbol, every declared key below it a Symbol, no
|
|
122
|
+
# hash carrying both spellings of a declared key. Allocates
|
|
123
|
+
# nothing; `CodecBoundary` asks it of every `Instance` an adapter
|
|
124
|
+
# builds. A `Runtime::Value` (hydrated state, a save's own entry)
|
|
125
|
+
# already is the declared shape, so it answers true.
|
|
126
|
+
#
|
|
127
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the state
|
|
128
|
+
# belongs to
|
|
129
|
+
# @param state [Hash, Runtime::Value, nil] the state to inspect
|
|
130
|
+
# @return [Boolean] true when `state` is decoded, and for anything that is not a Hash
|
|
131
|
+
def decoded?(aggregate, state)
|
|
132
|
+
return true unless state.is_a?(Hash)
|
|
133
|
+
|
|
134
|
+
hash_decoded?(aggregate, declared_top_level(aggregate), state, top: true)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# ── encode ──────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
# Encodes one value, recursing through Hashes, Arrays and `Runtime::Value`s.
|
|
140
|
+
#
|
|
141
|
+
# @param value [Object] any state value; a `Runtime::Value` is encoded as its `to_h`
|
|
142
|
+
# @return [Hash{String => Object}, Array, String, Integer, Float, Boolean, nil] the
|
|
143
|
+
# JSON-ready form; a leaf outside `JSON_SCALARS` (a Symbol, a Time) becomes whatever
|
|
144
|
+
# a `JSON.generate` then `JSON.parse` round trip makes of it
|
|
145
|
+
def encode_value(value)
|
|
146
|
+
case value
|
|
147
|
+
when Runtime::Value then encode_value(value.to_h)
|
|
148
|
+
when Hash then value.each_with_object({}) { |(key, inner), out| out[key.to_s] = encode_value(inner) }
|
|
149
|
+
when Array then value.map { |inner| encode_value(inner) }
|
|
150
|
+
when *JSON_SCALARS then value
|
|
151
|
+
# A Symbol, a Time, anything else: exactly what `JSON.generate`
|
|
152
|
+
# then `JSON.parse` would make of it, so Memory's copy and a
|
|
153
|
+
# durable adapter's row agree on the leaf too.
|
|
154
|
+
else JSON.parse(JSON.generate([value])).first
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# ── decode ──────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
# Decodes one Hash level, symbolizing its declared keys and recursing into their values.
|
|
161
|
+
#
|
|
162
|
+
# `fields` is name => Attribute-or-nil for this level. Key order is
|
|
163
|
+
# kept; a string key is skipped when the same hash also holds its
|
|
164
|
+
# symbol spelling, so the symbol wins wherever either one sits.
|
|
165
|
+
#
|
|
166
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the root construct, through
|
|
167
|
+
# which nested value objects and entities resolve
|
|
168
|
+
# @param fields [Hash{Symbol => Bluebook::Attribute, nil}] the fields declared at
|
|
169
|
+
# this level
|
|
170
|
+
# @param raw [Hash] the stored Hash for this level
|
|
171
|
+
# @param symbolize_undeclared [Boolean] true to symbolize undeclared keys too, as the
|
|
172
|
+
# top level does; false to leave their spelling alone
|
|
173
|
+
# @return [Hash] a new Hash in `raw`'s key order
|
|
174
|
+
def decode_hash(aggregate, fields, raw, symbolize_undeclared: false)
|
|
175
|
+
raw.each_with_object({}) do |(key, value), out|
|
|
176
|
+
name = key.to_s.to_sym
|
|
177
|
+
next if key.is_a?(String) && raw.key?(name)
|
|
178
|
+
|
|
179
|
+
if fields.key?(name)
|
|
180
|
+
out[name] = decode_field(aggregate, fields[name], value)
|
|
181
|
+
else
|
|
182
|
+
out[symbolize_undeclared ? name : key] = value
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Decodes the stored value of one declared field.
|
|
188
|
+
#
|
|
189
|
+
# One declared field's value, by the four shapes `Value::Coercion`
|
|
190
|
+
# names (scalar / list / optional / composite). nil, a reference (a
|
|
191
|
+
# bare id or a list of them), and a scalar pass through untouched; a
|
|
192
|
+
# value object or entity recurses only when the stored value really
|
|
193
|
+
# is the Hash/Array its declaration says — anything else (a legacy
|
|
194
|
+
# bare scalar for a one-field value object) is left for hydration.
|
|
195
|
+
#
|
|
196
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the root construct, through
|
|
197
|
+
# which the field's type resolves
|
|
198
|
+
# @param attribute [Bluebook::Attribute, nil] the field's declaration; nil for a
|
|
199
|
+
# lifecycle or projected field
|
|
200
|
+
# @param value [Object, nil] the stored value
|
|
201
|
+
# @return [Object, nil] `value` itself when nothing needs respelling, otherwise a new
|
|
202
|
+
# Hash or Array of decoded composites
|
|
203
|
+
def decode_field(aggregate, attribute, value)
|
|
204
|
+
return value if attribute.nil? || value.nil? || attribute.reference?
|
|
205
|
+
|
|
206
|
+
if attribute.list?
|
|
207
|
+
return value unless value.is_a?(Array)
|
|
208
|
+
|
|
209
|
+
value.map { |element| decode_composite(aggregate, attribute.type.to_s, element) }
|
|
210
|
+
else
|
|
211
|
+
decode_composite(aggregate, attribute.type.to_s, value)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Decodes one stored value object or entity, looked up by type name.
|
|
216
|
+
#
|
|
217
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the root construct that
|
|
218
|
+
# declares, or whose chapter declares, the type
|
|
219
|
+
# @param type [String] the declared type name, such as `"Address"`
|
|
220
|
+
# @param value [Object] the stored value
|
|
221
|
+
# @return [Hash, Object] a new decoded Hash; `value` unchanged when it is not a Hash or
|
|
222
|
+
# `type` names neither an entity nor an unambiguous value object
|
|
223
|
+
def decode_composite(aggregate, type, value)
|
|
224
|
+
return value unless value.is_a?(Hash)
|
|
225
|
+
|
|
226
|
+
entity = Runtime::Value.find_entity(aggregate, type)
|
|
227
|
+
return decode_hash(aggregate, entity_fields(entity), value) if entity
|
|
228
|
+
|
|
229
|
+
value_object = Runtime::Value.value_object_for(aggregate, type)
|
|
230
|
+
return value unless value_object
|
|
231
|
+
|
|
232
|
+
decode_hash(aggregate, value_object.attributes.to_h { |field| [field.name, field] }, value)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Maps an entity's (or aggregate's) attribute names, plus its lifecycle field, to
|
|
236
|
+
# their attributes.
|
|
237
|
+
#
|
|
238
|
+
# An entity is "structurally interchangeable with an aggregate"
|
|
239
|
+
# (behaviour/entity.rb): its attributes plus its own lifecycle field.
|
|
240
|
+
# Its value objects and nested entities resolve through the root
|
|
241
|
+
# aggregate, the same way `EntityListCoercion#hydrate_entity_list`
|
|
242
|
+
# resolves them.
|
|
243
|
+
#
|
|
244
|
+
# @param entity [Bluebook::Entity, Bluebook::Aggregate] the construct to walk
|
|
245
|
+
# @return [Hash{Symbol => Bluebook::Attribute, nil}] a new Hash of field name to
|
|
246
|
+
# attribute; nil marks a lifecycle field that no attribute declares
|
|
247
|
+
def entity_fields(entity)
|
|
248
|
+
fields = entity.attributes.to_h { |attribute| [attribute.name, attribute] }
|
|
249
|
+
lifecycle = entity.lifecycle
|
|
250
|
+
fields[lifecycle.field.to_sym] = nil if lifecycle && !fields.key?(lifecycle.field.to_sym)
|
|
251
|
+
fields
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# ── decoded? ────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
# Checks one Hash level for keys `decode` would respell.
|
|
257
|
+
#
|
|
258
|
+
# The mirror of `decode_hash`: a key `decode` would respell (any
|
|
259
|
+
# non-Symbol at the top, a non-Symbol declared key below it) means
|
|
260
|
+
# "not decoded"; an undeclared nested key keeps whatever spelling
|
|
261
|
+
# it has, exactly as `decode` keeps it.
|
|
262
|
+
#
|
|
263
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the root construct, through
|
|
264
|
+
# which nested types resolve
|
|
265
|
+
# @param fields [Hash{Symbol => Bluebook::Attribute, nil}] the fields declared at
|
|
266
|
+
# this level
|
|
267
|
+
# @param hash [Hash] the Hash to inspect
|
|
268
|
+
# @param top [Boolean] true for the top level, where every key must be a Symbol
|
|
269
|
+
# @return [Boolean] true when this level and every declared composite below it is
|
|
270
|
+
# decoded
|
|
271
|
+
def hash_decoded?(aggregate, fields, hash, top: false)
|
|
272
|
+
hash.all? do |key, value|
|
|
273
|
+
if key.is_a?(Symbol)
|
|
274
|
+
!fields.key?(key) || field_decoded?(aggregate, fields[key], value)
|
|
275
|
+
else
|
|
276
|
+
!top && !fields.key?(key.to_s.to_sym)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Checks the stored value of one declared field, the mirror of `decode_field`.
|
|
282
|
+
#
|
|
283
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the root construct, through
|
|
284
|
+
# which the field's type resolves
|
|
285
|
+
# @param attribute [Bluebook::Attribute, nil] the field's declaration; nil for a
|
|
286
|
+
# lifecycle or projected field
|
|
287
|
+
# @param value [Object, nil] the value to inspect
|
|
288
|
+
# @return [Boolean] true when `decode_field` would leave `value` as it is; always true
|
|
289
|
+
# for nil, a reference, an undeclared attribute, or a list that is not an Array
|
|
290
|
+
def field_decoded?(aggregate, attribute, value)
|
|
291
|
+
return true if attribute.nil? || value.nil? || attribute.reference?
|
|
292
|
+
return composite_decoded?(aggregate, attribute.type.to_s, value) unless attribute.list?
|
|
293
|
+
|
|
294
|
+
!value.is_a?(Array) || value.all? { |element| composite_decoded?(aggregate, attribute.type.to_s, element) }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Checks one stored value object or entity, the mirror of `decode_composite`.
|
|
298
|
+
#
|
|
299
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the root construct that
|
|
300
|
+
# declares, or whose chapter declares, the type
|
|
301
|
+
# @param type [String] the declared type name
|
|
302
|
+
# @param value [Object] the value to inspect
|
|
303
|
+
# @return [Boolean] true when `value` is not a Hash, `type` names neither an entity nor
|
|
304
|
+
# an unambiguous value object, or every declared key in it is a Symbol
|
|
305
|
+
def composite_decoded?(aggregate, type, value)
|
|
306
|
+
return true unless value.is_a?(Hash)
|
|
307
|
+
|
|
308
|
+
entity = Runtime::Value.find_entity(aggregate, type)
|
|
309
|
+
return hash_decoded?(aggregate, entity_fields(entity), value) if entity
|
|
310
|
+
|
|
311
|
+
value_object = Runtime::Value.value_object_for(aggregate, type)
|
|
312
|
+
return true unless value_object
|
|
313
|
+
|
|
314
|
+
hash_decoded?(aggregate, value_object.attributes.to_h { |field| [field.name, field] }, value)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
@@ -15,6 +15,7 @@ require_relative "persistence/append_only"
|
|
|
15
15
|
require_relative "persistence/execution"
|
|
16
16
|
require_relative "persistence/remote_runtime"
|
|
17
17
|
require_relative "persistence/null_saga_store"
|
|
18
|
+
require_relative "persistence/state_codec"
|
|
18
19
|
|
|
19
20
|
module Hecks
|
|
20
21
|
module Ports
|
|
@@ -25,16 +26,50 @@ module Hecks
|
|
|
25
26
|
module Persistence
|
|
26
27
|
module_function
|
|
27
28
|
|
|
28
|
-
#
|
|
29
|
+
# Builds the repository that reads and writes one aggregate's authoritative store.
|
|
30
|
+
#
|
|
31
|
+
# The public persistence port owns only authoritative aggregate heads. The repository
|
|
32
|
+
# comes back already recovered: every journal entry has been re-projected.
|
|
33
|
+
#
|
|
34
|
+
# @param registry [Runtime::Registry] the booted registry holding the domain's
|
|
35
|
+
# hecksagon, world and adapters
|
|
36
|
+
# @param domain [String, Symbol] name of the domain the aggregate belongs to
|
|
37
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate to persist
|
|
38
|
+
# @return [Persistence::AppendOnly] repository over the aggregate's authoritative
|
|
39
|
+
# adapter, or over a `Memory` adapter when the domain declares no hecksagon
|
|
40
|
+
# @raise [Runtime::WiringError] if the aggregate has no authoritative bind, more than
|
|
41
|
+
# one, or a bind with a role this port does not support; or if the bound adapter is
|
|
42
|
+
# unknown, answers a different verb, is given a setting it does not declare, has no
|
|
43
|
+
# Ruby implementation, or lacks a method its port's `answers` list or the
|
|
44
|
+
# append-only contract (`append`, `project`, `entries`) requires
|
|
29
45
|
def repository(registry, domain, aggregate)
|
|
30
46
|
authoritative = BindingPolicy.resolve(registry, domain, aggregate)
|
|
31
47
|
RepositoryFactory.build(registry, domain, aggregate, authoritative)
|
|
32
48
|
end
|
|
33
49
|
|
|
50
|
+
# Resolves an aggregate's authoritative bind, paired with an always-empty Array.
|
|
51
|
+
#
|
|
52
|
+
# @param registry [Runtime::Registry] the booted registry holding the domain's hecksagon
|
|
53
|
+
# @param domain [String, Symbol] name of the domain the aggregate belongs to
|
|
54
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose binding is wanted
|
|
55
|
+
# @return [Array(Bluebook::Bind, Array)] the authoritative `persisted_by` bind (the
|
|
56
|
+
# default `Memory` bind when the domain declares no hecksagon), then an Array that
|
|
57
|
+
# is always `[]`
|
|
58
|
+
# @raise [Runtime::WiringError] if the aggregate has no authoritative bind, more than
|
|
59
|
+
# one, or a bind with a role this port does not support
|
|
34
60
|
def binds_for(registry, domain, aggregate)
|
|
35
61
|
[BindingPolicy.resolve(registry, domain, aggregate), []]
|
|
36
62
|
end
|
|
37
63
|
|
|
64
|
+
# Resolves the one bind naming an aggregate's authoritative store.
|
|
65
|
+
#
|
|
66
|
+
# @param registry [Runtime::Registry] the booted registry holding the domain's hecksagon
|
|
67
|
+
# @param domain [String, Symbol] name of the domain the aggregate belongs to
|
|
68
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose binding is wanted
|
|
69
|
+
# @return [Bluebook::Bind] the authoritative `persisted_by` bind, or the default
|
|
70
|
+
# `Memory` bind when the domain declares no hecksagon
|
|
71
|
+
# @raise [Runtime::WiringError] if the aggregate has no authoritative bind, more than
|
|
72
|
+
# one, or a bind with a role this port does not support
|
|
38
73
|
def bind_for(registry, domain, aggregate)
|
|
39
74
|
binds_for(registry, domain, aggregate).first
|
|
40
75
|
end
|
|
@@ -12,10 +12,32 @@ module Hecks
|
|
|
12
12
|
|
|
13
13
|
module_function
|
|
14
14
|
|
|
15
|
+
# Lists the `projected_by` binds a domain's hecksagon declares for one aggregate.
|
|
16
|
+
#
|
|
17
|
+
# @param registry [Runtime::Registry] the booted registry to search
|
|
18
|
+
# @param domain [String, Symbol] name of the domain the aggregate belongs to
|
|
19
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate to find projection binds for
|
|
20
|
+
# @return [Array<Bluebook::Bind>] the binds naming the aggregate, or failing that the
|
|
21
|
+
# hecksagon's aggregate-less `projected_by` binds; `[]` if there are none or the
|
|
22
|
+
# domain declares no hecksagon
|
|
15
23
|
def binds_for(registry, domain, aggregate)
|
|
16
24
|
registry.hecksagon(domain)&.binds_for(aggregate.hecks_name, VERB) || []
|
|
17
25
|
end
|
|
18
26
|
|
|
27
|
+
# Builds a worker that catches one projection's store up to its authoritative journal.
|
|
28
|
+
#
|
|
29
|
+
# @param registry [Runtime::Registry] the booted registry to resolve binds against
|
|
30
|
+
# @param domain [String, Symbol] name of the domain the aggregate belongs to
|
|
31
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate being projected
|
|
32
|
+
# @param policy [Symbol, String] `:refresh` (reset the store and rebuild it) or
|
|
33
|
+
# `:strict` (refuse on divergent history) — see `Worker::VALID_POLICIES`
|
|
34
|
+
# @return [Ports::Projection::Worker, nil] a worker for the aggregate's first declared
|
|
35
|
+
# bind, or nil if it has no projection bind
|
|
36
|
+
# @raise [Runtime::WiringError] if the aggregate's authoritative persistence bind does
|
|
37
|
+
# not resolve (see `Persistence.repository`), or the projection's adapter is unknown,
|
|
38
|
+
# answers a different verb, is given a setting it does not declare, has no Ruby
|
|
39
|
+
# implementation, or lacks a method the append-only contract requires
|
|
40
|
+
# @raise [ArgumentError] if `policy` is not one of `Worker::VALID_POLICIES`
|
|
19
41
|
def worker(registry, domain, aggregate, policy: :refresh)
|
|
20
42
|
bind = binds_for(registry, domain, aggregate).first
|
|
21
43
|
return unless bind
|
|
@@ -32,23 +54,32 @@ module Hecks
|
|
|
32
54
|
# separate process or scheduler, never from the command-side write
|
|
33
55
|
# path.
|
|
34
56
|
class Worker
|
|
57
|
+
# @return [Persistence::AppendOnly] the projection store this worker catches up
|
|
35
58
|
attr_reader :projection
|
|
36
59
|
|
|
37
60
|
# The only two policies anything in this codebase ever passes
|
|
38
61
|
# (`bin/project`, every spec) — there is no third, legitimate
|
|
39
|
-
# "lenient append" policy on record anywhere.
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
62
|
+
# "lenient append" policy on record anywhere. This list is the
|
|
63
|
+
# contract for what a valid policy is: `catch_up!` enforces
|
|
64
|
+
# history agreement only for the exact symbol `:strict`, so any
|
|
65
|
+
# other unrecognised value — a typo like `:strikt` — would fall
|
|
66
|
+
# through the `consistent?` check below and append onto
|
|
67
|
+
# divergent history without a word. A caller-supplied String
|
|
68
|
+
# `"strict"` is accepted on purpose, normalised by `policy.to_sym`
|
|
69
|
+
# in `initialize` before it is checked against this list, not by
|
|
70
|
+
# the accident of duck typing.
|
|
46
71
|
# Refusing loudly at construction, once, for anything outside
|
|
47
72
|
# this list turns a silent no-op into an immediate, named error
|
|
48
73
|
# — the "refuse rather than silently skip" reading of L1, since
|
|
49
74
|
# `:strict` really is meant to be the only enforcing contract.
|
|
50
75
|
VALID_POLICIES = %i[refresh strict].freeze
|
|
51
76
|
|
|
77
|
+
# @param authoritative [Persistence::AppendOnly] the authoritative repository whose
|
|
78
|
+
# journal the projection is caught up to
|
|
79
|
+
# @param projection [Persistence::AppendOnly] the projection store being caught up
|
|
80
|
+
# @param policy [Symbol, String] one of `VALID_POLICIES`, as a Symbol or its String
|
|
81
|
+
# spelling
|
|
82
|
+
# @raise [ArgumentError] if `policy` is not one of `VALID_POLICIES`
|
|
52
83
|
def initialize(authoritative, projection, policy: :refresh)
|
|
53
84
|
@authoritative = authoritative
|
|
54
85
|
@projection = projection
|
|
@@ -60,8 +91,20 @@ module Hecks
|
|
|
60
91
|
end
|
|
61
92
|
end
|
|
62
93
|
|
|
94
|
+
# Appends and projects every authoritative entry the projection store lacks.
|
|
95
|
+
#
|
|
63
96
|
# Invoke from a separate process or scheduler. The command-side write
|
|
64
97
|
# path never calls this method.
|
|
98
|
+
#
|
|
99
|
+
# Under `:refresh` the store is reset first and rebuilt from the whole journal. Under
|
|
100
|
+
# `:strict` the store's own entries must be a prefix of the authoritative journal
|
|
101
|
+
# (same operation, id and state, in order); only the entries after that prefix are
|
|
102
|
+
# replayed.
|
|
103
|
+
#
|
|
104
|
+
# @return [Persistence::AppendOnly] the projection store, now caught up
|
|
105
|
+
# @raise [Runtime::WiringError] under `:strict`, if the projection's history is not a
|
|
106
|
+
# prefix of the authoritative journal; under `:refresh`, if the projection's
|
|
107
|
+
# adapter cannot `reset!`
|
|
65
108
|
def catch_up!
|
|
66
109
|
entries = Queue.new(@authoritative).entries
|
|
67
110
|
present = @projection.entries
|
|
@@ -79,6 +122,10 @@ module Hecks
|
|
|
79
122
|
@projection
|
|
80
123
|
end
|
|
81
124
|
|
|
125
|
+
# Counts the entries the projection store holds, which is how far it has caught up.
|
|
126
|
+
#
|
|
127
|
+
# @return [Integer] number of journal entries already appended to the projection
|
|
128
|
+
# store; 0 for a store that has never caught up
|
|
82
129
|
def checkpoint = @projection.entries.length
|
|
83
130
|
|
|
84
131
|
private
|
|
@@ -96,7 +143,14 @@ module Hecks
|
|
|
96
143
|
# It is committed before a worker sees it; projection entries are the
|
|
97
144
|
# worker's durable checkpoint, so delivery is at-least-once and safe to replay.
|
|
98
145
|
class Queue
|
|
146
|
+
# @param authoritative [Persistence::AppendOnly] the authoritative repository to read
|
|
147
|
+
# entries from
|
|
99
148
|
def initialize(authoritative) = @authoritative = authoritative
|
|
149
|
+
|
|
150
|
+
# Reads the whole authoritative journal, the work a projection worker replays from.
|
|
151
|
+
#
|
|
152
|
+
# @return [Array<Persistence::Entry>] every committed entry, oldest first; `[]` for
|
|
153
|
+
# an empty journal
|
|
100
154
|
def entries = @authoritative.entries
|
|
101
155
|
end
|
|
102
156
|
end
|
|
@@ -27,7 +27,7 @@ module Hecks
|
|
|
27
27
|
field = declared.order_by&.field
|
|
28
28
|
matched = Ordering.apply(matched, declared.order_by, declared.null_semantics,
|
|
29
29
|
identity: ->(record) { record.id.to_s }) { |record| comparable(FieldPath.dig(record, field)) }
|
|
30
|
-
#
|
|
30
|
+
# **Offset first, then limit** — the order SQL means by `LIMIT n
|
|
31
31
|
# OFFSET m`, which is what `SqlQueryBuilder` emits and therefore
|
|
32
32
|
# what every SQL-backed aggregate already answers. Written the
|
|
33
33
|
# other way round here, and the two engines disagreed on the
|
|
@@ -38,7 +38,7 @@ module Hecks
|
|
|
38
38
|
#
|
|
39
39
|
# It gets worse the further you page, which is the case nobody
|
|
40
40
|
# writing the first page ever sees: at `limit 10, offset 10`,
|
|
41
|
-
# taking ten and then dropping ten leaves
|
|
41
|
+
# taking ten and then dropping ten leaves nothing, so page two
|
|
42
42
|
# of a memory-backed query came back empty however many rows
|
|
43
43
|
# were really there.
|
|
44
44
|
matched = matched.drop(resolve(declared.offset.value, args).to_i) if declared.offset
|
|
@@ -50,7 +50,7 @@ module Hecks
|
|
|
50
50
|
# QuerySpecification::Common::Comparison — this module and
|
|
51
51
|
# Runtime::QueryInterpreter used to carry a copy each, and the two
|
|
52
52
|
# drifted (see that file's own comment for what it cost). What
|
|
53
|
-
# stays here is how a value is
|
|
53
|
+
# stays here is how a value is reached for this path: a registry
|
|
54
54
|
# arrives as an argument rather than as instance state, and the
|
|
55
55
|
# field is dug through FieldPath before it arrives.
|
|
56
56
|
def holds?(clause, held, args, registry: nil)
|