hecks 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
|
@@ -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
|
|
@@ -18,6 +18,19 @@ module Hecks
|
|
|
18
18
|
|
|
19
19
|
module_function
|
|
20
20
|
|
|
21
|
+
# Filters, orders and pages `records` against a declared query specification.
|
|
22
|
+
#
|
|
23
|
+
# @param records [Array<Runtime::Instance, Hash>] the candidate records to filter —
|
|
24
|
+
# an Instance, a Value, or a plain row hash per record (see `FieldPath.dig`)
|
|
25
|
+
# @param declared [QuerySpecification::Common::Options,
|
|
26
|
+
# Bluebook::Behaviour::ReadModel::FilteredOptions] the specification providing
|
|
27
|
+
# `wheres`, `order_by`, `offset` and `limit`
|
|
28
|
+
# @param args [Hash{Symbol => Object}] bound values for any Symbol placeholder in a
|
|
29
|
+
# where clause, `offset`, or `limit`
|
|
30
|
+
# @param registry [Runtime::Registry, nil] the booted registry, passed through to a
|
|
31
|
+
# registry-aware comparison; nil when none is available
|
|
32
|
+
# @return [Array<Runtime::Instance, Hash>] the matching records, ordered and paged;
|
|
33
|
+
# the same element type as `records`
|
|
21
34
|
def execute(records, declared, args = {}, registry: nil)
|
|
22
35
|
matched = records.select do |record|
|
|
23
36
|
declared.wheres.all? do |clause|
|
|
@@ -27,7 +40,7 @@ module Hecks
|
|
|
27
40
|
field = declared.order_by&.field
|
|
28
41
|
matched = Ordering.apply(matched, declared.order_by, declared.null_semantics,
|
|
29
42
|
identity: ->(record) { record.id.to_s }) { |record| comparable(FieldPath.dig(record, field)) }
|
|
30
|
-
#
|
|
43
|
+
# **Offset first, then limit** — the order SQL means by `LIMIT n
|
|
31
44
|
# OFFSET m`, which is what `SqlQueryBuilder` emits and therefore
|
|
32
45
|
# what every SQL-backed aggregate already answers. Written the
|
|
33
46
|
# other way round here, and the two engines disagreed on the
|
|
@@ -38,7 +51,7 @@ module Hecks
|
|
|
38
51
|
#
|
|
39
52
|
# It gets worse the further you page, which is the case nobody
|
|
40
53
|
# writing the first page ever sees: at `limit 10, offset 10`,
|
|
41
|
-
# taking ten and then dropping ten leaves
|
|
54
|
+
# taking ten and then dropping ten leaves nothing, so page two
|
|
42
55
|
# of a memory-backed query came back empty however many rows
|
|
43
56
|
# were really there.
|
|
44
57
|
matched = matched.drop(resolve(declared.offset.value, args).to_i) if declared.offset
|
|
@@ -47,18 +60,38 @@ module Hecks
|
|
|
47
60
|
end
|
|
48
61
|
|
|
49
62
|
# The comparator table itself lives in
|
|
50
|
-
# QuerySpecification::Common::Comparison
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
# stays here is how a value is
|
|
54
|
-
# arrives as an argument rather than
|
|
55
|
-
# field is dug through FieldPath
|
|
63
|
+
# QuerySpecification::Common::Comparison, not duplicated per
|
|
64
|
+
# caller — this module and Runtime::QueryInterpreter drifted
|
|
65
|
+
# when each carried its own copy (see that file's own comment
|
|
66
|
+
# for what it cost). What stays here is how a value is reached
|
|
67
|
+
# for this path: a registry arrives as an argument rather than
|
|
68
|
+
# as instance state, and the field is dug through FieldPath
|
|
69
|
+
# before it arrives.
|
|
70
|
+
# @param clause [QuerySpecification::Common::WhereClause] the declared comparison
|
|
71
|
+
# @param held [Object, nil] the record's own value for `clause.field`
|
|
72
|
+
# @param args [Hash{Symbol => Object}] bound values for a Symbol-named `clause.value`
|
|
73
|
+
# @param registry [Runtime::Registry, nil] used only by `none_in_state`; see
|
|
74
|
+
# `Comparison.holds?`
|
|
75
|
+
# @return [Boolean] whether the comparison holds
|
|
76
|
+
# @raise [Runtime::WiringError] if `clause.op` names no comparator in
|
|
77
|
+
# `Comparison`'s table, or `none_in_state`'s target aggregate has no wired repository
|
|
56
78
|
def holds?(clause, held, args, registry: nil)
|
|
57
79
|
Comparison.holds?(clause.op, held, comparable(resolve(clause.value, args)), registry: registry)
|
|
58
80
|
end
|
|
59
81
|
|
|
82
|
+
# Resolves a where-clause value, looking up a Symbol placeholder in `args`.
|
|
83
|
+
#
|
|
84
|
+
# @param value [Object] a literal value, or a Symbol naming a key in `args`
|
|
85
|
+
# @param args [Hash{Symbol => Object}] bound argument values
|
|
86
|
+
# @return [Object] `args[value]` when `value` is a Symbol, else `value` unchanged
|
|
60
87
|
def resolve(value, args) = value.is_a?(Symbol) ? args[value] : value
|
|
61
88
|
|
|
89
|
+
# Normalises a value into the shape `Comparison` compares against.
|
|
90
|
+
#
|
|
91
|
+
# @param value [Runtime::Value, Hash, Object, nil] a held or wanted value; a
|
|
92
|
+
# `Runtime::Value` is read through its `to_h`
|
|
93
|
+
# @return [Object, nil] the sole numeric member, or the sole member, of a Hash-shaped
|
|
94
|
+
# value; otherwise `value` unchanged
|
|
62
95
|
def comparable(value) = Comparison.comparable(value)
|
|
63
96
|
end
|
|
64
97
|
end
|
|
@@ -3,30 +3,45 @@ require_relative "../../query_specification/common/null_policy"
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Ports
|
|
5
5
|
module Query
|
|
6
|
-
# What order an ask
|
|
6
|
+
# What order an ask answers in — the meaning of the ask, not a property
|
|
7
7
|
# of the store that happens to hold it. Declared here once so an adapter
|
|
8
8
|
# may satisfy it natively but never redefine it : SQLite pushes both
|
|
9
9
|
# tiers into SQL (NullPolicy.sql_order renders `field DIR, id DIR`),
|
|
10
10
|
# while Heki and Memory have no query engine and delegate straight back
|
|
11
11
|
# to InMemory.
|
|
12
12
|
#
|
|
13
|
-
# Two tiers, in this order : the
|
|
14
|
-
# then
|
|
13
|
+
# Two tiers, in this order : the declared order_by when there is one,
|
|
14
|
+
# then identity, always. The identity tier is what makes an ask total.
|
|
15
15
|
# Without it, an ask with no order_by — or a declared order with tied
|
|
16
16
|
# keys — hands back whatever order the store happened to hold, and
|
|
17
17
|
# store order was quietly standing in for a rule while every
|
|
18
18
|
# hand-written query in the corpus stayed green : not one of them
|
|
19
19
|
# had a tie for store order to decide.
|
|
20
20
|
#
|
|
21
|
-
# An adapter that pushes ordering down
|
|
21
|
+
# An adapter that pushes ordering down must push limit down with it.
|
|
22
22
|
# Re-ordering a page the store already cut would be a top-N of the
|
|
23
23
|
# wrong N — the one way this can be got quietly, expensively wrong.
|
|
24
24
|
module Ordering
|
|
25
25
|
module_function
|
|
26
26
|
|
|
27
|
+
# Orders an Array of rows by the declared order, with identity as the total-order
|
|
28
|
+
# tiebreaker.
|
|
29
|
+
#
|
|
30
|
+
# @param rows [Array<Object>] the rows to order; any shape `identity` and the block
|
|
31
|
+
# can read (a `Runtime::Instance`, a plain Hash row, …)
|
|
32
|
+
# @param order_by [QuerySpecification::Common::OrderBy, nil] the declared order;
|
|
33
|
+
# nil orders by identity alone
|
|
34
|
+
# @param null_semantics [QuerySpecification::Common::NullSemantics, nil] where a
|
|
35
|
+
# nil-valued row sorts, passed to `NullPolicy.order`; nil uses its native default
|
|
36
|
+
# @param identity [Proc] yields a row, returns its comparable identity value, used
|
|
37
|
+
# as the stable tiebreaker
|
|
38
|
+
# @yieldparam row [Object] one row being ordered
|
|
39
|
+
# @yieldreturn [Object, nil] the row's comparable value for `order_by`'s field
|
|
40
|
+
# @return [Array<Object>] `rows`, ordered by `order_by` then identity (identity
|
|
41
|
+
# alone when `order_by` is nil)
|
|
27
42
|
def apply(rows, order_by, null_semantics = nil, identity:, &value_of)
|
|
28
|
-
#
|
|
29
|
-
# otherwise swap arbitrarily, and a tier meant to
|
|
43
|
+
# Stable, because sort_by is not : two rows whose identity ties would
|
|
44
|
+
# otherwise swap arbitrarily, and a tier meant to remove store-dependence
|
|
30
45
|
# would be adding a coin flip of its own.
|
|
31
46
|
rows = rows.each_with_index.sort_by { |row, index| [identity.call(row), index] }.map(&:first)
|
|
32
47
|
return rows unless order_by
|