hecks 1.3.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 +5 -4
- 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/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 +18 -18
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +23 -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 +7 -7
- 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 +101 -87
- 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 +170 -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 +180 -19
- 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 +140 -45
- 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 +37 -18
- data/lib/hecks/fuzzing/form_census.rb +35 -20
- 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 +62 -62
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
- data/lib/hecks/fuzzing/properties/guards.rb +42 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +64 -40
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +114 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +141 -112
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
- 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 +58 -31
- data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +23 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
- data/lib/hecks/fuzzing/value_generator.rb +14 -14
- data/lib/hecks/fuzzing.rb +5 -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 +348 -13
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +25 -25
- 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 +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 +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 +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 +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 +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 +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 +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 +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 +3 -3
- data/lib/hecks/projections/glossary/markdown.rb +3 -3
- data/lib/hecks/projections/glossary/mermaid.rb +1 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +5 -5
- data/lib/hecks/projections/glossary.rb +15 -15
- 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 +11 -11
- 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 +109 -27
- 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 +42 -63
- 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 +35 -35
- 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 +151 -67
- data/lib/hecks/runtime/entity_interpreter.rb +108 -82
- 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 +55 -64
- data/lib/hecks/runtime/reaction_invocation.rb +20 -20
- 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 -115
- 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 +45 -28
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -153
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +64 -64
- 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 +318 -127
- data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
- data/lib/hecks/runtime/value.rb +21 -21
- 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 +205 -4
- 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 +22 -2
|
@@ -7,8 +7,8 @@ require_relative "../../runtime/value"
|
|
|
7
7
|
module Hecks
|
|
8
8
|
module Adapters
|
|
9
9
|
# The one SQL query compilation, shared by the SQLite and Postgres
|
|
10
|
-
# adapters —
|
|
11
|
-
#
|
|
10
|
+
# adapters — one walk rather than a near-line-identical copy per
|
|
11
|
+
# adapter, because copies can only drift. Every declared operator
|
|
12
12
|
# compiles fully into SQL or the query refuses loudly; the null-policy
|
|
13
13
|
# predicate, the comma-separated `in` convention, and the identity
|
|
14
14
|
# ORDER BY fallback are spelled once, here.
|
|
@@ -17,7 +17,7 @@ module Hecks
|
|
|
17
17
|
# supplies exactly those hooks:
|
|
18
18
|
#
|
|
19
19
|
# placeholder(binds, value) — "?" or "$N", recording the bind
|
|
20
|
-
# contains_clause(expr, ph) — instr() or position(), for a
|
|
20
|
+
# contains_clause(expr, ph) — instr() or position(), for a scalar field
|
|
21
21
|
# list_contains_clause(name, member, ph) — real element containment,
|
|
22
22
|
# for a `list_of` field (json_each /
|
|
23
23
|
# jsonb_array_elements)
|
|
@@ -33,6 +33,18 @@ module Hecks
|
|
|
33
33
|
"eq" => "=", "ne" => "<>", "gt" => ">", "gte" => ">=", "lt" => "<", "lte" => "<="
|
|
34
34
|
}.freeze
|
|
35
35
|
|
|
36
|
+
# Compiles a declared query into one SQL statement and runs it through the dialect's
|
|
37
|
+
# own `execute_query`, so filtering, ordering and paging all happen in the database.
|
|
38
|
+
#
|
|
39
|
+
# @param declared [QuerySpecification::Common::Options] the declared query: its
|
|
40
|
+
# `wheres`, `order_by`, `limit` and `offset` are compiled; nothing else is read
|
|
41
|
+
# @param args [Hash{Symbol => Object}] values for the specification's symbolic operands
|
|
42
|
+
# @param context [Hash] execution context from `Ports::Query.execute`; accepted for the
|
|
43
|
+
# port's call shape and not read
|
|
44
|
+
# @return [Array<Runtime::Instance>] the matching records in declared order, then by
|
|
45
|
+
# id; `[]` when none match
|
|
46
|
+
# @raise [ArgumentError] if a where clause uses an operator this builder cannot compile,
|
|
47
|
+
# or `contains` targets a list of a value object with more than one field
|
|
36
48
|
def query(declared, args = {}, context: {})
|
|
37
49
|
sql = "SELECT #{select_list} FROM #{from_relation}"
|
|
38
50
|
binds = []
|
|
@@ -91,7 +103,7 @@ module Hecks
|
|
|
91
103
|
members = in_members(value)
|
|
92
104
|
return empty_in_clause if members.empty?
|
|
93
105
|
|
|
94
|
-
#
|
|
106
|
+
# **Both sides as text**. `in` is a textual reading everywhere else
|
|
95
107
|
# — Ports::Query::InMemory#holds? compares `held.to_s` against
|
|
96
108
|
# stringified members, and `in_members` above stringifies its
|
|
97
109
|
# own — so a numeric field was the one shape where the engines
|
|
@@ -104,7 +116,7 @@ module Hecks
|
|
|
104
116
|
# on a String, an Integer, and a value-object member.
|
|
105
117
|
#
|
|
106
118
|
# `eq`/`gt`/`lt` never had this problem because they bind the
|
|
107
|
-
# value's
|
|
119
|
+
# value's own type and route through `comparable_expression`,
|
|
108
120
|
# which Postgres overrides to cast numerics.
|
|
109
121
|
"CAST(#{expression} AS TEXT) IN (#{members.map { |member| placeholder(binds, member) }.join(', ')})"
|
|
110
122
|
else
|
|
@@ -112,16 +124,16 @@ module Hecks
|
|
|
112
124
|
end
|
|
113
125
|
end
|
|
114
126
|
|
|
115
|
-
# A
|
|
127
|
+
# A real array already says where its members end. Splitting one on
|
|
116
128
|
# commas re-reads a boundary it already drew — and an id is a
|
|
117
129
|
# domain value (Naming::IDENTITY_JOIN joins a composite identity's
|
|
118
130
|
# own parts, and the parts it joins are whatever an identity path
|
|
119
131
|
# pulled out of real data), so a name carrying a comma would
|
|
120
132
|
# silently become two members matching the wrong rows, or nothing.
|
|
121
|
-
#
|
|
122
|
-
#
|
|
133
|
+
# Calling `value.to_s` unconditionally would mean only a
|
|
134
|
+
# comma-joined string ever works here, while
|
|
123
135
|
# Ports::Query::InMemory and QueryInterpreter's own `members`
|
|
124
|
-
#
|
|
136
|
+
# handle a real Array — three implementations of `in`,
|
|
125
137
|
# two readings of it. Narrower than those two on one point,
|
|
126
138
|
# deliberately: they unwrap a value-object/Hash element to its
|
|
127
139
|
# own scalar first (`comparable`) before stringifying, a rule
|
|
@@ -140,15 +152,15 @@ module Hecks
|
|
|
140
152
|
# member the bound value (or the declared types) say is numeric,
|
|
141
153
|
# falling back to the one-field convention `value`.
|
|
142
154
|
#
|
|
143
|
-
# A
|
|
155
|
+
# A reference is an ID, stored as a bare scalar (never wrapped —
|
|
144
156
|
# `Runtime::Value.refuse_object_reference` guarantees it), so it
|
|
145
|
-
# takes the
|
|
157
|
+
# takes the same plain-column path as any other non-value-object
|
|
146
158
|
# attribute. Excluding it into the value-object member-picking logic
|
|
147
159
|
# compiles a nested read against a row that has no nested key — a
|
|
148
160
|
# path that can never match. Measured, not assumed: a `where` on a
|
|
149
161
|
# has_one/belongs_to/reference_to field returned zero rows against
|
|
150
162
|
# real data until that exclusion was removed.
|
|
151
|
-
# `member` resolves through two
|
|
163
|
+
# `member` resolves through two ordered fallback tiers — the bound
|
|
152
164
|
# value's own numeric field first, then the declared value object's
|
|
153
165
|
# numeric-or-sole attribute — each documented above as fixing a
|
|
154
166
|
# real, measured bug (a reference field matching nothing, a
|
|
@@ -168,7 +180,7 @@ module Hecks
|
|
|
168
180
|
member = if path.empty? && value
|
|
169
181
|
hash = value.is_a?(Runtime::Value) ? value.to_h : value
|
|
170
182
|
numeric = hash.is_a?(Hash) && hash.find { |_key, item| item.is_a?(Numeric) }
|
|
171
|
-
#
|
|
183
|
+
# Not &.-able: `numeric` can be `false` (hash.is_a?(Hash) came
|
|
172
184
|
# back false) as well as nil (.find came back empty) — `&.`
|
|
173
185
|
# only guards nil, so `false.first` raises. False positive.
|
|
174
186
|
# rubocop:disable-next Style/SafeNavigation
|
|
@@ -176,8 +188,8 @@ module Hecks
|
|
|
176
188
|
end
|
|
177
189
|
member ||= if path.empty? && attribute && value_object?(attribute)
|
|
178
190
|
object = @aggregate.value_object(attribute.type)
|
|
179
|
-
# A
|
|
180
|
-
# numeric — a single-attribute value object
|
|
191
|
+
# A sole attribute is the fallback when no member is
|
|
192
|
+
# numeric — a single-attribute value object is its one
|
|
181
193
|
# field whatever that field is named (`Behaviour::
|
|
182
194
|
# ValueObject#sole_attribute`, the same strict rule
|
|
183
195
|
# `Runtime::Value`'s own `.value` alias enforces), so
|
|
@@ -192,11 +204,11 @@ module Hecks
|
|
|
192
204
|
nested_expression(name, path, member)
|
|
193
205
|
end
|
|
194
206
|
|
|
195
|
-
# A
|
|
207
|
+
# A numeric member wins if the value object has one (Price, Money —
|
|
196
208
|
# what every ordered comparison in the corpus until now compared),
|
|
197
|
-
# otherwise the
|
|
209
|
+
# otherwise the same single-field fallback `query_expression`'s own
|
|
198
210
|
# `nested_expression(name, path, member || "value")` already
|
|
199
|
-
# makes for the
|
|
211
|
+
# makes for the column side — a plain `attribute :value, String`
|
|
200
212
|
# value object (IdentityId, RoleName, ...) has no numeric member at
|
|
201
213
|
# all, and returning nil there silently turned an equality
|
|
202
214
|
# comparison into `IS NULL`, matching nothing. Both sides of one
|
|
@@ -221,16 +233,16 @@ module Hecks
|
|
|
221
233
|
!attr.list? && !@aggregate.value_object(attr.type).nil?
|
|
222
234
|
end
|
|
223
235
|
|
|
224
|
-
# `contains` on a `list_of` field means real
|
|
236
|
+
# `contains` on a `list_of` field means real element membership, not
|
|
225
237
|
# a substring search over the column's raw JSON text — the reading
|
|
226
238
|
# QueryInterpreter#holds? and Ports::Query::InMemory already give a
|
|
227
|
-
# real Array, and
|
|
228
|
-
# match over `[{"value":"not_high_risk"}]` falsely matches
|
|
239
|
+
# real Array, and a substring search would disagree with it (a
|
|
240
|
+
# substring match over `[{"value":"not_high_risk"}]` falsely matches
|
|
229
241
|
# "high_risk"). Returns nil for a non-list field (the caller falls
|
|
230
242
|
# back to `contains_clause`, unchanged), "" for a list of bare
|
|
231
243
|
# scalars (no member to walk into), or the one field name a
|
|
232
244
|
# single-field value-object element carries. A list of a
|
|
233
|
-
#
|
|
245
|
+
# multi-field value object has no one scalar to compare against and
|
|
234
246
|
# is refused rather than guessed.
|
|
235
247
|
def list_member(field)
|
|
236
248
|
return nil if field.to_s.include?(".")
|
|
@@ -16,7 +16,7 @@ module Hecks
|
|
|
16
16
|
fields << { name: lifecycle.field, attribute: nil, sql_type: "TEXT" } if lifecycle && fields.none? do |field|
|
|
17
17
|
field[:name] == lifecycle.field
|
|
18
18
|
end
|
|
19
|
-
# `projects`
|
|
19
|
+
# `projects` fields (S12, ADR 0025) are a local column too —
|
|
20
20
|
# `CommandInterpreter#seed_projected_fields`/`RebuildSweep`
|
|
21
21
|
# both write one straight into `Instance#state` the same as
|
|
22
22
|
# any other field, so a column has to exist to hold it or
|
|
@@ -37,15 +37,15 @@ module Hecks
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def encode(attr, value)
|
|
40
|
-
#
|
|
40
|
+
# **Never set is not empty**. `then_set ... append:` starts a list at `[]`
|
|
41
41
|
# the moment the first element lands, but a list attribute nothing has
|
|
42
|
-
# ever appended to — the shape every
|
|
42
|
+
# ever appended to — the shape every other aggregate in the corpus
|
|
43
43
|
# happened not to carry, until one declared a list-typed head
|
|
44
44
|
# attribute that no creating command populates by default — has to
|
|
45
45
|
# stay NULL to answer the same as Memory does. Forcing it to `[]`
|
|
46
46
|
# here is how a persistence topology invented data no dispatch wrote.
|
|
47
|
-
return (value.nil? ? nil :
|
|
48
|
-
return
|
|
47
|
+
return (value.nil? ? nil : state_json(value)) if attr.list?
|
|
48
|
+
return state_json(value) if value.is_a?(Hash) || value.is_a?(Runtime::Value)
|
|
49
49
|
|
|
50
50
|
value
|
|
51
51
|
end
|
|
@@ -56,25 +56,53 @@ module Hecks
|
|
|
56
56
|
encode(field[:attribute], value)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Every JSON column's text goes through the state codec's `encode`
|
|
60
|
+
# (PR A3) — string keys at every depth, Values materialized.
|
|
61
|
+
def state_json(value) = JSON.generate(Ports::Persistence::StateCodec.encode(@aggregate, value))
|
|
62
|
+
|
|
63
|
+
# The row's columns reassembled into the stored state, then decoded
|
|
64
|
+
# once through the state codec (PR A3) — the same deep, IR-driven
|
|
65
|
+
# key spelling every other adapter's read now produces, instead of
|
|
66
|
+
# this codec's own `symbolize_names:` walk.
|
|
59
67
|
def decode(row)
|
|
60
|
-
persisted_fields.each_with_object({}) do |field,
|
|
68
|
+
state = persisted_fields.each_with_object({}) do |field, raw_state|
|
|
61
69
|
attr = field[:attribute]
|
|
62
70
|
unless attr
|
|
63
|
-
|
|
71
|
+
value = row[field[:name].to_s]
|
|
72
|
+
next if value.nil? && projected_only?(field)
|
|
73
|
+
|
|
74
|
+
raw_state[field[:name]] = value
|
|
64
75
|
next
|
|
65
76
|
end
|
|
66
77
|
raw = row[attr.name.to_s]
|
|
67
|
-
|
|
78
|
+
raw_state[attr.name] =
|
|
68
79
|
if attr.list? || value_object?(attr)
|
|
69
|
-
raw ? JSON.parse(raw
|
|
80
|
+
raw ? JSON.parse(raw) : nil
|
|
70
81
|
else
|
|
71
|
-
# A
|
|
82
|
+
# A reference lands here now, with the ordinary scalars. It holds
|
|
72
83
|
# the id of a head, which is text — `JSON.parse("acct-1")` raises,
|
|
73
84
|
# so reading it as JSON was only ever survivable while the column
|
|
74
85
|
# held an object.
|
|
75
86
|
raw
|
|
76
87
|
end
|
|
77
88
|
end
|
|
89
|
+
Ports::Persistence::StateCodec.decode(@aggregate, state)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A NULL `projects` column is a field never seeded, not a stored
|
|
93
|
+
# NIL — so it reads back absent, the way Heki/PostgresEra (one blob
|
|
94
|
+
# holding only what was written) already answer it. A column holds
|
|
95
|
+
# every persisted field whether or not the record ever had one, so
|
|
96
|
+
# NULL is the only spelling "never set" has here; and nothing ever
|
|
97
|
+
# stores a nil projected value to confuse it with —
|
|
98
|
+
# `CommandInterpreter#seed_projected_fields` and
|
|
99
|
+
# `RebuildSweep#refresh` both skip a nil remote value. Absent is also
|
|
100
|
+
# what `RebuildSweep#refresh`'s `record.key?` and
|
|
101
|
+
# `Registry#projection_current?`'s row comparison expect. The
|
|
102
|
+
# lifecycle field (the other `attribute: nil` column) keeps its NULL.
|
|
103
|
+
def projected_only?(field)
|
|
104
|
+
@aggregate.lifecycle&.field&.to_sym != field[:name].to_sym &&
|
|
105
|
+
@aggregate.projected_fields.any? { |projected| projected.name.to_sym == field[:name].to_sym }
|
|
78
106
|
end
|
|
79
107
|
end
|
|
80
108
|
end
|
|
@@ -6,7 +6,7 @@ require_relative "../../../rendering"
|
|
|
6
6
|
require_relative "../../../ports/query/in_memory"
|
|
7
7
|
|
|
8
8
|
# The subclass needs its parent — and sqlite.rb requires this file at
|
|
9
|
-
# its
|
|
9
|
+
# its bottom, after class Sqlite is defined, so the require cycle this
|
|
10
10
|
# creates resolves correctly from either entry point.
|
|
11
11
|
require_relative "../sqlite"
|
|
12
12
|
|
|
@@ -17,6 +17,13 @@ module Hecks
|
|
|
17
17
|
# bare id it holds — the same shapes the command path writes, so there
|
|
18
18
|
# is no second representation to accept here any more.
|
|
19
19
|
class SqliteProjection < Sqlite
|
|
20
|
+
# Replaces or deletes the read store's row for one journal entry, encoding the entry's
|
|
21
|
+
# state directly rather than through a `Runtime::Instance`.
|
|
22
|
+
#
|
|
23
|
+
# @param entry [Ports::Persistence::Entry] the save or delete to materialize
|
|
24
|
+
# @return [Ports::Persistence::Entry, Array] the same `entry` for a save; for a delete,
|
|
25
|
+
# the `DELETE` statement's empty result rows
|
|
26
|
+
# @raise [SQLite3::Exception] if the statement fails
|
|
20
27
|
def project(entry)
|
|
21
28
|
return @db.execute("DELETE FROM #{quoted_table} WHERE id = ?", [entry.id]) if entry.delete?
|
|
22
29
|
|
|
@@ -34,36 +41,51 @@ module Hecks
|
|
|
34
41
|
# than scanning repositories and matching references in Ruby.
|
|
35
42
|
#
|
|
36
43
|
# M19 (docs/audits/2026-08-10-main-bug-audit.md,
|
|
37
|
-
# docs/audits/2026-08-11-bug-triage.md) — this
|
|
44
|
+
# docs/audits/2026-08-11-bug-triage.md) — this agrees with
|
|
38
45
|
# `Runtime::ReadModelInterpreter#project` (the in-process path) on
|
|
39
|
-
# two counts
|
|
46
|
+
# the two counts where a native path can most easily diverge:
|
|
40
47
|
#
|
|
41
|
-
#
|
|
42
|
-
# `NotFound` when the reference argument names no record
|
|
43
|
-
#
|
|
44
|
-
#
|
|
48
|
+
# **Missing root**: the in-process path's own `fetch` refuses with
|
|
49
|
+
# `NotFound` when the reference argument names no record, and so
|
|
50
|
+
# does this. Answering a silent `{root: nil, ...}` instead would
|
|
51
|
+
# make this the one path where a caller could dispatch a read model
|
|
45
52
|
# against a record that never existed and get back something that
|
|
46
|
-
#
|
|
53
|
+
# looks like an empty report rather than the refusal every other
|
|
47
54
|
# path gives.
|
|
48
55
|
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# references
|
|
52
|
-
# directly
|
|
56
|
+
# **Chained-include join scope**: matching a non-root head against
|
|
57
|
+
# the root's own id alone, regardless of what it actually
|
|
58
|
+
# references, is correct for a head that references the root
|
|
59
|
+
# directly and silently empty for one that references another
|
|
53
60
|
# included head instead (`Leaf` -> `Mid` -> `Root`, `Leaf` itself
|
|
54
|
-
# has no attribute referencing `Root` at all, so `references`
|
|
55
|
-
#
|
|
56
|
-
# (`ReadModelInterpreter#project`'s "
|
|
57
|
-
#
|
|
58
|
-
# only the root — `select_related`
|
|
61
|
+
# has no attribute referencing `Root` at all, so its `references`
|
|
62
|
+
# would always be `[]`). The in-process path
|
|
63
|
+
# (`ReadModelInterpreter#project`'s "root first, always" comment)
|
|
64
|
+
# matches a head against any already-projected source, not
|
|
65
|
+
# only the root — `select_related` does the same: each head is
|
|
59
66
|
# matched against every source resolved so far (root first, then
|
|
60
67
|
# declared order — the same one-level-of-declaration-order
|
|
61
|
-
# dependency the in-process path itself
|
|
68
|
+
# dependency the in-process path itself has, documented
|
|
62
69
|
# there as L2, not a gap introduced here).
|
|
70
|
+
#
|
|
71
|
+
# @param _domain [String, Symbol] name of the domain declaring the read model; not read
|
|
72
|
+
# @param model [Bluebook::ReadModel, Runtime::TenantScope::Scoped] the read model to
|
|
73
|
+
# answer, or the tenant-scoping delegator around one; must have a reference target
|
|
74
|
+
# @param args [Hash{Symbol => Object}] the read model's arguments; the entry under
|
|
75
|
+
# `model.reference_name` is the root record's id
|
|
76
|
+
# @param bluebook [Bluebook::Chapter, nil] the domain's bluebook, which resolves each
|
|
77
|
+
# included aggregate by name; nil is refused
|
|
78
|
+
# @return [Array<Hash>] a one-element Array holding the report Hash, keyed by each
|
|
79
|
+
# head's `as` name: an Array of plain state Hashes (each with `:id`) for a `many`
|
|
80
|
+
# head, otherwise one such Hash, or nil when that head matched no row
|
|
81
|
+
# @raise [ArgumentError] if `bluebook` is nil
|
|
82
|
+
# @raise [KeyError] if `args` has no entry for the model's reference argument
|
|
83
|
+
# @raise [Runtime::NotFound] if no projected row has the referenced root id
|
|
84
|
+
# @raise [SQLite3::Exception] if a statement fails
|
|
63
85
|
def query_read_model(_domain, model, args, bluebook = nil)
|
|
64
86
|
raise ArgumentError, "projection query needs its domain bluebook" unless bluebook
|
|
65
87
|
|
|
66
|
-
# The
|
|
88
|
+
# The reference's own shape, read the same way ReadModelInterpreter
|
|
67
89
|
# reads it — not the identity unwrap, which is gone : an identity is
|
|
68
90
|
# declared as a path and followed.
|
|
69
91
|
reference_id = args.fetch(model.reference_name).to_s
|
|
@@ -72,7 +94,7 @@ module Hecks
|
|
|
72
94
|
# eligible in the same read model now.
|
|
73
95
|
eligible = model.filtered_head_names
|
|
74
96
|
|
|
75
|
-
#
|
|
97
|
+
# **Root first, always** — see this method's own header. Mirrors
|
|
76
98
|
# `ReadModelInterpreter#project`'s identical partition, for the
|
|
77
99
|
# identical reason: a later head's own join has to be able to
|
|
78
100
|
# match against a root (or another head) already resolved.
|
|
@@ -84,9 +106,9 @@ module Hecks
|
|
|
84
106
|
rows = if head[:aggregate] == model.reference_target
|
|
85
107
|
[select_projected(aggregate, reference_id) ||
|
|
86
108
|
raise(Runtime::NotFound,
|
|
87
|
-
Runtime::RefusalWording.
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
Runtime::RefusalWording.render_site("NotFound", "read_model_reference_missing",
|
|
110
|
+
aggregate: head[:aggregate],
|
|
111
|
+
offered: Hecks::Rendering.describe(reference_id)))]
|
|
90
112
|
else
|
|
91
113
|
select_related(aggregate, projected)
|
|
92
114
|
end
|
|
@@ -108,12 +130,12 @@ module Hecks
|
|
|
108
130
|
row && projected_instance(aggregate, row)
|
|
109
131
|
end
|
|
110
132
|
|
|
111
|
-
# Matched against
|
|
133
|
+
# Matched against every source already projected (root first, then
|
|
112
134
|
# declared order — see this class's own `query_read_model` header),
|
|
113
135
|
# not only the root — a head whose own reference points at another
|
|
114
|
-
# included head rather than the root directly
|
|
115
|
-
# at all, since its reference attribute
|
|
116
|
-
# target (the root) it never names.
|
|
136
|
+
# included head rather than the root directly would otherwise match
|
|
137
|
+
# nothing at all, since its reference attribute would be compared
|
|
138
|
+
# against a target (the root) it never names.
|
|
117
139
|
def select_related(aggregate, projected)
|
|
118
140
|
matches = projected.flat_map do |source|
|
|
119
141
|
references = aggregate.attributes.select do |attribute|
|
|
@@ -128,7 +150,7 @@ module Hecks
|
|
|
128
150
|
end
|
|
129
151
|
return [] if matches.empty?
|
|
130
152
|
|
|
131
|
-
# A
|
|
153
|
+
# A reference column holds the ID, so it compares as itself. The
|
|
132
154
|
# `json_extract(col,'$.value') = ? OR col = ?` this replaced was
|
|
133
155
|
# reading both shapes because both existed — one written by the
|
|
134
156
|
# command path, one by older journals. There is one shape now.
|
|
@@ -154,7 +176,7 @@ module Hecks
|
|
|
154
176
|
if (lifecycle = aggregate.lifecycle) && fields.none? { |name, _| name == lifecycle.field }
|
|
155
177
|
fields << [lifecycle.field, nil]
|
|
156
178
|
end
|
|
157
|
-
# `projects`
|
|
179
|
+
# `projects` fields (S12, ADR 0025) need reading back too — `project`
|
|
158
180
|
# (above) already writes one into its own column via `persisted_fields`
|
|
159
181
|
# (`Codec#persisted_fields`, this class's own superclass module), but
|
|
160
182
|
# this method built its own independent field list that never
|
|
@@ -167,24 +189,30 @@ module Hecks
|
|
|
167
189
|
fields
|
|
168
190
|
end
|
|
169
191
|
|
|
192
|
+
# Decoded through the state codec (PR A3) against the row's own
|
|
193
|
+
# aggregate — see Codec#decode, including why a NULL projected-only
|
|
194
|
+
# column reads back absent.
|
|
170
195
|
def decode_fields(fields, aggregate, row)
|
|
171
|
-
fields.each_with_object({}) do |(name, attribute),
|
|
196
|
+
state = fields.each_with_object({}) do |(name, attribute), raw_state|
|
|
172
197
|
raw = row[name.to_s]
|
|
173
198
|
# rubocop:disable Lint/DuplicateBranch -- the nil-attribute and
|
|
174
199
|
# reference-id branches both just answer `raw`, coincidentally, for
|
|
175
200
|
# two unrelated reasons (see each branch's own comment); merging
|
|
176
201
|
# them would blur that distinction.
|
|
177
|
-
|
|
202
|
+
next if attribute.nil? && raw.nil? && aggregate.lifecycle&.field&.to_sym != name.to_sym
|
|
203
|
+
|
|
204
|
+
raw_state[name] =
|
|
178
205
|
if attribute.nil?
|
|
179
206
|
raw
|
|
180
207
|
elsif attribute.list? || !aggregate.value_object(attribute.type).nil?
|
|
181
|
-
raw ? JSON.parse(raw
|
|
208
|
+
raw ? JSON.parse(raw) : nil
|
|
182
209
|
else
|
|
183
210
|
# A reference is a scalar id — see Codec#decode.
|
|
184
211
|
raw
|
|
185
212
|
end
|
|
186
213
|
# rubocop:enable Lint/DuplicateBranch
|
|
187
214
|
end
|
|
215
|
+
Ports::Persistence::StateCodec.decode(aggregate, state)
|
|
188
216
|
end
|
|
189
217
|
end
|
|
190
218
|
end
|
|
@@ -12,11 +12,11 @@ module Hecks
|
|
|
12
12
|
@db.execute(
|
|
13
13
|
"CREATE TABLE IF NOT EXISTS #{quoted_table} (id TEXT PRIMARY KEY#{', ' unless columns.empty?}#{columns.join(', ')})"
|
|
14
14
|
)
|
|
15
|
-
#
|
|
15
|
+
# Right here, not as a separate step in `Sqlite#initialize` —
|
|
16
16
|
# `create_aggregate_table!` is the one piece of DDL `D1`
|
|
17
17
|
# already calls verbatim through this same shared module
|
|
18
18
|
# (`d1.rb`'s own header: "reuses Sqlite::SchemaBuilder and
|
|
19
|
-
# Sqlite::Codec
|
|
19
|
+
# Sqlite::Codec unchanged"). Folding index creation in here,
|
|
20
20
|
# right after the table it indexes exists, is what makes D1's
|
|
21
21
|
# own automatic indexing a real, running thing rather than an
|
|
22
22
|
# aspiration that only Sqlite ever calls — no D1-specific
|
|
@@ -63,7 +63,7 @@ module Hecks
|
|
|
63
63
|
@db.execute("ALTER TABLE #{quoted_entry_table} ADD COLUMN mirrors TEXT")
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
#
|
|
66
|
+
# The optional saga-persistence capability's own table (§2/§4) —
|
|
67
67
|
# shared here so `D1`, which `include`s this module verbatim for
|
|
68
68
|
# its own `events`-table DDL (`d1.rb`), gets this for free too.
|
|
69
69
|
# `domain` stays an explicit column even though SQLite has no
|
|
@@ -131,10 +131,10 @@ module Hecks
|
|
|
131
131
|
# `ensure_head_snapshot!`/`ensure_first_head!` already use) ────
|
|
132
132
|
|
|
133
133
|
# Every field a declared query ever filters or sorts on, across
|
|
134
|
-
# the aggregate's
|
|
134
|
+
# the aggregate's own queries and every entity's own queries —
|
|
135
135
|
# `query_surfaces` in `dsl/aggregate_builder.rb` walks the
|
|
136
|
-
# identical pair.
|
|
137
|
-
#
|
|
136
|
+
# identical pair. An entity's query still runs against this
|
|
137
|
+
# table: it compiles through `query_expression`, which is
|
|
138
138
|
# `@aggregate`-scoped, not entity-scoped, so an entity's
|
|
139
139
|
# declared `where`/`order_by` is indexed here too, same as any
|
|
140
140
|
# other declared query — not skipped as "some other table's
|
|
@@ -158,14 +158,14 @@ module Hecks
|
|
|
158
158
|
# plain-vs-nested decision);
|
|
159
159
|
# - a non-list value-object attribute, referenced bare or
|
|
160
160
|
# through a member path (`field` or `field.member`) — an
|
|
161
|
-
#
|
|
161
|
+
# expression index over `query_expression(field)`'s own
|
|
162
162
|
# `json_extract(...)` text. Reusing the query compiler's own
|
|
163
163
|
# expression, not re-deriving the string a second way, is
|
|
164
164
|
# the whole point: a textual mismatch between the index and
|
|
165
165
|
# what a real query compiles to is invisible to SQLite's
|
|
166
166
|
# planner, and two independent copies of this logic can only
|
|
167
167
|
# drift apart over time;
|
|
168
|
-
# - a list-typed attribute —
|
|
168
|
+
# - a list-typed attribute — no index. SQLite's `contains`
|
|
169
169
|
# compiles to `EXISTS (SELECT 1 FROM json_each(col) WHERE
|
|
170
170
|
# ...)` (`list_contains_clause`, in `sql_query_builder.rb`)
|
|
171
171
|
# — an element-membership scan a plain index on the raw
|
|
@@ -178,8 +178,8 @@ module Hecks
|
|
|
178
178
|
#
|
|
179
179
|
# A field naming neither a real attribute nor the lifecycle
|
|
180
180
|
# field can't happen through the DSL today — `seal_query_field`
|
|
181
|
-
# (`dsl/aggregate_builder.rb`) already refuses it at
|
|
182
|
-
# This runs at adapter
|
|
181
|
+
# (`dsl/aggregate_builder.rb`) already refuses it at parse time.
|
|
182
|
+
# This runs at adapter boot, not parse time, so it skips rather
|
|
183
183
|
# than crashes ugly if that invariant is ever violated.
|
|
184
184
|
def ensure_index_for_field!(field)
|
|
185
185
|
name, * = field.to_s.split(".")
|
|
@@ -195,8 +195,8 @@ module Hecks
|
|
|
195
195
|
)
|
|
196
196
|
end
|
|
197
197
|
|
|
198
|
-
# `idx_<table>_<sanitized field>` —
|
|
199
|
-
# SQLite index names are
|
|
198
|
+
# `idx_<table>_<sanitized field>` — table-prefixed because
|
|
199
|
+
# SQLite index names are global to the database, not scoped per
|
|
200
200
|
# table the way a column name is; two different aggregates each
|
|
201
201
|
# indexing a field called "name" would collide without it. The
|
|
202
202
|
# field itself is sanitized (a dotted path's "." in particular)
|