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
|
@@ -12,18 +12,18 @@ module Hecks
|
|
|
12
12
|
# verb-to-declaration lookups (#command_for_verb,
|
|
13
13
|
# #effective_guard_descriptions) each resolves a refusal against.
|
|
14
14
|
module Guards
|
|
15
|
-
# `Query#options`'
|
|
15
|
+
# `Query#options`' other half — TenantScope.apply's own contract
|
|
16
16
|
# (tenant_scope.rb), independently restated as a property rather
|
|
17
17
|
# than exercised only through whatever the generator happens to
|
|
18
|
-
# try.
|
|
18
|
+
# try. Not closed by the generator here on purpose: SafeDepositBox.
|
|
19
19
|
# Rented — the only real corpus query declaring `authorize` at
|
|
20
|
-
# all — declares
|
|
20
|
+
# all — declares zero attributes of its own, so StepBuilder#args_for
|
|
21
21
|
# always hands it `{}` and TenantScope.apply refuses every
|
|
22
22
|
# generated attempt, unconditionally (confirmed: no successful ask
|
|
23
23
|
# against an authorize-bearing query reaches this property via the
|
|
24
24
|
# standard battery today). Extending the generator to invent a
|
|
25
25
|
# `tenant:` value ran into a separate, real finding along the way —
|
|
26
|
-
# SafeDepositBox is
|
|
26
|
+
# SafeDepositBox is composite-identified (`identified_by` is nil
|
|
27
27
|
# for it — Runtime::Identified#derive_identity), so the generator's
|
|
28
28
|
# existing `known_ids` pool (keyed by `aggregate.identified_by ||
|
|
29
29
|
# "id"`) tracks a stray, never-real scalar for it rather than its
|
|
@@ -37,10 +37,10 @@ module Hecks
|
|
|
37
37
|
# where a `tenant:` arg came from.
|
|
38
38
|
#
|
|
39
39
|
# Two claims, matching TenantScope.apply's own two branches: every
|
|
40
|
-
#
|
|
40
|
+
# successful answer's own tenant field agrees with the tenant arg
|
|
41
41
|
# given (the WhereClause TenantScope injects is a Symbol reference
|
|
42
|
-
# into args, resolved dynamically — this checks the
|
|
43
|
-
# re-deriving that resolution) ; every ask
|
|
42
|
+
# into args, resolved dynamically — this checks the outcome, not
|
|
43
|
+
# re-deriving that resolution) ; every ask missing a required
|
|
44
44
|
# tenant: refuses with the declared wording, never succeeds. A
|
|
45
45
|
# refusal for an unrelated reason with the tenant arg present is
|
|
46
46
|
# not this property's claim either way — skipped, not graded.
|
|
@@ -54,6 +54,11 @@ module Hecks
|
|
|
54
54
|
# branches shares logic with the others beyond that shared setup.
|
|
55
55
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
56
56
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
57
|
+
#
|
|
58
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
59
|
+
# @return [true, String] true if every tenant-scoped query answer, and every
|
|
60
|
+
# refusal of one, agrees with `TenantScope.apply`'s own contract; otherwise a
|
|
61
|
+
# message naming the offending query, its args, and what it should have done
|
|
57
62
|
def authorize_scopes_or_refuses(history)
|
|
58
63
|
bluebooks = history.fetch(:bluebooks)
|
|
59
64
|
|
|
@@ -92,14 +97,14 @@ module Hecks
|
|
|
92
97
|
offenders.empty? || offenders.join("; ")
|
|
93
98
|
end
|
|
94
99
|
|
|
95
|
-
#
|
|
96
|
-
#
|
|
100
|
+
# Every given/ensures refusal a run actually raised names a rule
|
|
101
|
+
# the command actually declares. `GivenNotMet`/`EnsuresNotMet` both
|
|
97
102
|
# quote their guard's own `description` verbatim
|
|
98
103
|
# (command_rules/admissibility.rb: `"#{command.hecks_name} refused
|
|
99
|
-
# — #{given.description}"`) — the
|
|
104
|
+
# — #{given.description}"`) — the same text `behavior.bluebook`'s
|
|
100
105
|
# own `Rule`/Command.Ensure hold as `Rule#description`, so a
|
|
101
106
|
# refusal whose quoted text is not among the refusing command's
|
|
102
|
-
#
|
|
107
|
+
# own `guard_descriptions` (Behaviour::Command, both givens and
|
|
103
108
|
# ensures) is either a stale message surviving a renamed rule, a
|
|
104
109
|
# rule firing against the wrong command's own guard set, or the
|
|
105
110
|
# wording drifting out from under the declaration it is supposed
|
|
@@ -107,7 +112,7 @@ module Hecks
|
|
|
107
112
|
# guards, some through a cross-aggregate dereference) are exactly
|
|
108
113
|
# the surface this exists to hold to its word.
|
|
109
114
|
#
|
|
110
|
-
# `kind:` is what tells a guard refusal apart from the
|
|
115
|
+
# `kind:` is what tells a guard refusal apart from the four other
|
|
111
116
|
# `RefusalWording` templates sharing the identical "X refused — Y"
|
|
112
117
|
# shape (LifecycleRefused/transition_blocked, both TypeMismatch
|
|
113
118
|
# object-reference templates, Unauthorized/role_mismatch) — see
|
|
@@ -116,6 +121,14 @@ module Hecks
|
|
|
116
121
|
# those; the raised class does not.
|
|
117
122
|
GUARD_REFUSAL_KINDS = %w[Hecks::Runtime::GivenNotMet Hecks::Runtime::EnsuresNotMet].freeze
|
|
118
123
|
|
|
124
|
+
# Checks that every given/ensures refusal a run actually raised names a rule
|
|
125
|
+
# the refusing command actually declares.
|
|
126
|
+
#
|
|
127
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
128
|
+
# @return [true, String] true if every guard refusal's quoted description matches
|
|
129
|
+
# a `given`/`ensures` the refusing command (or its delegation target) declares;
|
|
130
|
+
# otherwise a message naming the refusal and the declared descriptions it does
|
|
131
|
+
# not match
|
|
119
132
|
def guard_refusals_are_declared(history)
|
|
120
133
|
bluebooks = history.fetch(:bluebooks)
|
|
121
134
|
|
|
@@ -138,27 +151,27 @@ module Hecks
|
|
|
138
151
|
offenders.empty? || offenders.join("; ")
|
|
139
152
|
end
|
|
140
153
|
|
|
141
|
-
#
|
|
154
|
+
# Angle-8's own write-side half. `authorize_scopes_or_refuses`
|
|
142
155
|
# (above) enforces `TenantScope.apply`'s boundary, and that
|
|
143
|
-
# boundary exists
|
|
156
|
+
# boundary exists only for queries/read models — `authorize
|
|
144
157
|
# policy, tenant: field` is a word `QuerySpecification::Common::
|
|
145
158
|
# DSL#authorize_impl` grants to `QueryBuilder`/`ReadModelBuilder`
|
|
146
159
|
# alone; `CommandBuilder` never includes that module, so no
|
|
147
160
|
# bluebook can declare it on a command at all (confirmed by
|
|
148
|
-
# reading the grammar directly, not inferred). A
|
|
161
|
+
# reading the grammar directly, not inferred). A write that
|
|
149
162
|
# carries a `reference_to` from one tenant-scoped record into
|
|
150
|
-
# another's is checked by
|
|
163
|
+
# another's is checked by nothing at dispatch time: `TenantScope`
|
|
151
164
|
# never runs for a command, and no runtime `given`/`ensures`
|
|
152
165
|
# anywhere in this corpus reads a cross-aggregate tenant field
|
|
153
166
|
# either. `qa/stress_domains/tenant_ledger` exists to give this
|
|
154
167
|
# property a real place to fire.
|
|
155
168
|
#
|
|
156
|
-
#
|
|
157
|
-
# field one of
|
|
169
|
+
# The rule: an aggregate's own declared tenant field is whichever
|
|
170
|
+
# field one of its own queries names in `authorize policy, tenant:
|
|
158
171
|
# :field` — the exact same declaration `authorize_scopes_or_
|
|
159
172
|
# refuses` reads off a query above, reused here to name a field
|
|
160
|
-
# on the
|
|
161
|
-
# For every
|
|
173
|
+
# on the aggregate itself that stores the tenant it belongs to.
|
|
174
|
+
# For every stored record (`history[:instances]` — a refused
|
|
162
175
|
# dispatch never writes one, so "a refusal is correct behaviour,
|
|
163
176
|
# not a finding" holds by construction, the same way `history
|
|
164
177
|
# [:instances]` already guarantees this for `stored_records_
|
|
@@ -166,30 +179,36 @@ module Hecks
|
|
|
166
179
|
# tenant field, walk every `reference_to`-typed attribute it
|
|
167
180
|
# carries (`Bluebook::Reference` — "a reference IS the id",
|
|
168
181
|
# value/coercion.rb's own header, so the stored value is always a
|
|
169
|
-
# plain id, never a nested payload) pointing at
|
|
170
|
-
# that
|
|
182
|
+
# plain id, never a nested payload) pointing at another aggregate
|
|
183
|
+
# that also declares a tenant field: if the referenced record's
|
|
171
184
|
# own tenant value disagrees with the referencing record's own
|
|
172
185
|
# tenant value, the write crossed a tenant boundary and nothing
|
|
173
186
|
# refused it — a finding.
|
|
174
187
|
#
|
|
175
|
-
# A
|
|
188
|
+
# A dangling/unresolvable reference is skipped — a different,
|
|
176
189
|
# existence-shaped property's claim, not this one's (the same
|
|
177
190
|
# "inconclusive, not a claimed pass" restraint `lifecycle_guard_
|
|
178
191
|
# and_given_violations_are_refused` already documents for a
|
|
179
192
|
# differently-shaped case). Comparison goes through `Ports::
|
|
180
|
-
# Query::InMemory.comparable` (the
|
|
193
|
+
# Query::InMemory.comparable` (the same normalization `authorize_
|
|
181
194
|
# scopes_or_refuses` already applies to a query row's own tenant
|
|
182
195
|
# field, just above) rather than `Runtime::Value#==` directly —
|
|
183
|
-
# two single-attribute value objects with the
|
|
184
|
-
#
|
|
196
|
+
# two single-attribute value objects with the same scalar but
|
|
197
|
+
# different declared names (`LedgerRegion`/`TransferRegion`, this
|
|
185
198
|
# domain's own pair — a value object is always declared inside
|
|
186
199
|
# the aggregate that owns it, so two independently tenant-scoped
|
|
187
|
-
# aggregates can never share one) compare
|
|
200
|
+
# aggregates can never share one) compare unequal under `Value#==`
|
|
188
201
|
# (`type_name` is part of that equality) despite meaning the
|
|
189
202
|
# identical tenant, which would make every same-tenant write a
|
|
190
203
|
# false positive.
|
|
191
204
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
192
205
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
206
|
+
#
|
|
207
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
208
|
+
# @return [true, String] true if every stored record's `reference_to` attribute
|
|
209
|
+
# points at a record sharing the same tenant value (or has no resolvable
|
|
210
|
+
# target, or neither side declares a tenant field); otherwise a message naming
|
|
211
|
+
# the referencing record, the reference, and the tenant mismatch
|
|
193
212
|
def commands_respect_tenant_scope(history)
|
|
194
213
|
bluebooks = history.fetch(:bluebooks)
|
|
195
214
|
instances = history.fetch(:instances)
|
|
@@ -230,27 +249,31 @@ module Hecks
|
|
|
230
249
|
offenders.empty? || offenders.join("; ")
|
|
231
250
|
end
|
|
232
251
|
|
|
233
|
-
#
|
|
252
|
+
# The field an aggregate's own query names as tenant-scoping —
|
|
234
253
|
# shared by `commands_respect_tenant_scope` above for both sides
|
|
235
254
|
# of a `reference_to`. `nil` for an aggregate with no `authorize
|
|
236
255
|
# ..., tenant:` on any of its own queries — not every aggregate
|
|
237
256
|
# is tenant-scoped, and one that isn't has nothing for this
|
|
238
257
|
# property to check either side of.
|
|
258
|
+
#
|
|
259
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate to look up
|
|
260
|
+
# @return [Symbol, nil] the tenant field name, or nil if no query on the
|
|
261
|
+
# aggregate declares `authorize ..., tenant:`
|
|
239
262
|
def tenant_field_for(aggregate)
|
|
240
263
|
authorization = aggregate.queries.filter_map(&:authorization).find(&:tenant)
|
|
241
264
|
authorization&.tenant&.to_sym
|
|
242
265
|
end
|
|
243
266
|
|
|
244
|
-
# A
|
|
267
|
+
# A declared process manager's own command — `command.hecks_name`,
|
|
245
268
|
# or an entity's own if the verb's second component is itself
|
|
246
269
|
# dotted (`Aggregate.Entity.Command`, the same two shapes
|
|
247
270
|
# `Dispatcher#dispatch` itself branches on). Shared by the guard
|
|
248
271
|
# property above and available for anything else that needs to go
|
|
249
272
|
# from a replayed verb back to its declaration.
|
|
250
273
|
#
|
|
251
|
-
#
|
|
274
|
+
# Resolved against `bluebooks` (the full map, `history[:bluebooks]`
|
|
252
275
|
# — every loaded domain, keyed by name), never a single assumed
|
|
253
|
-
# bluebook: a verb names its
|
|
276
|
+
# bluebook: a verb names its own domain (`Naming.split_verb`'s
|
|
254
277
|
# first element), and that domain is not always the one Replay
|
|
255
278
|
# happens to expose as `history[:bluebook]`. A fuzz run against
|
|
256
279
|
# `lib/hecks/grammar` (Expression + Translation, in load
|
|
@@ -258,15 +281,23 @@ module Hecks
|
|
|
258
281
|
# refusal read as "no declared command resolves that verb" purely
|
|
259
282
|
# because `history[:bluebook]` was Expression, not Translation; the
|
|
260
283
|
# refusal was real, this property's own domain resolution was not.
|
|
261
|
-
# A
|
|
284
|
+
# A delegating door refuses with its target's own words. `delegates_to`
|
|
262
285
|
# (CommandBuilder#delegates_to_impl) hands the whole dispatch to one
|
|
263
286
|
# entity command, and that command's given is what refuses — raised
|
|
264
287
|
# back through the door, in the door's name (chess: `Game.MoveKnight
|
|
265
288
|
# refused — "it is that color's turn"`, a given Knight.Move declares
|
|
266
289
|
# and MoveKnight, a pure passthrough, never could). Read the door's
|
|
267
290
|
# own guards first, then every delegation target's; an offence is
|
|
268
|
-
# only a description
|
|
291
|
+
# only a description neither declares. Found live mining chess's
|
|
269
292
|
# history: every refused move through a door read as undeclared.
|
|
293
|
+
#
|
|
294
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
295
|
+
# keyed by domain name
|
|
296
|
+
# @param verb [String] the dispatched verb, `"Domain::Aggregate.Command"` or
|
|
297
|
+
# `"Domain::Aggregate.Entity.Command"`
|
|
298
|
+
# @param command [Bluebook::Command] the command `verb` resolves to
|
|
299
|
+
# @return [Array<String>] `command`'s own declared given/ensures descriptions,
|
|
300
|
+
# plus every `delegate`d command's, in that order
|
|
270
301
|
def effective_guard_descriptions(bluebooks, verb, command)
|
|
271
302
|
own = command.guard_descriptions
|
|
272
303
|
delegated = command.mutations.select { |m| m.op == :delegate }.flat_map do |delegation|
|
|
@@ -277,6 +308,14 @@ module Hecks
|
|
|
277
308
|
own + delegated
|
|
278
309
|
end
|
|
279
310
|
|
|
311
|
+
# Resolves a dispatched verb back to the declared command it names.
|
|
312
|
+
#
|
|
313
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
314
|
+
# keyed by domain name
|
|
315
|
+
# @param verb [String] the dispatched verb, `"Domain::Aggregate.Command"` or
|
|
316
|
+
# `"Domain::Aggregate.Entity.Command"`
|
|
317
|
+
# @return [Bluebook::Command, nil] the declared command, or nil if `verb`'s
|
|
318
|
+
# domain, aggregate, entity, or command is not found among `bluebooks`
|
|
280
319
|
def command_for_verb(bluebooks, verb)
|
|
281
320
|
domain, aggregate_name, command_path = Naming.split_verb(verb)
|
|
282
321
|
return nil unless command_path
|
|
@@ -296,9 +335,9 @@ module Hecks
|
|
|
296
335
|
end
|
|
297
336
|
end
|
|
298
337
|
|
|
299
|
-
# `guard_refusals_are_declared`'s
|
|
338
|
+
# `guard_refusals_are_declared`'s own opposite direction. That
|
|
300
339
|
# property is passive and one-directional — for a refusal that
|
|
301
|
-
#
|
|
340
|
+
# already happened, is the quoted text real declared text? It says
|
|
302
341
|
# nothing about a guard that should have refused and silently did
|
|
303
342
|
# not — a call site that stopped calling enforce_givens/enforce_
|
|
304
343
|
# lifecycle_guard would never appear in history[:refusals] at all,
|
|
@@ -306,7 +345,7 @@ module Hecks
|
|
|
306
345
|
#
|
|
307
346
|
# This one calls Admissibility#enforce_givens (which itself folds
|
|
308
347
|
# in #enforce_lifecycle_guard whenever `declaring:` is passed)
|
|
309
|
-
#
|
|
348
|
+
# directly, against Replay's own pre-dispatch snapshot
|
|
310
349
|
# (history[:guard_checks], one bounded, additive extension — see
|
|
311
350
|
# that file's own comment at the capture site) — an independent
|
|
312
351
|
# recomputation, not grading production against itself, the same
|
|
@@ -316,27 +355,32 @@ module Hecks
|
|
|
316
355
|
# could mutate anything a cross-aggregate given dereferences) is
|
|
317
356
|
# compared against `actual_refused` (GivenNotMet/LifecycleRefused
|
|
318
357
|
# specifically — Replay's own comment on GUARD_REFUSAL_CLASSES
|
|
319
|
-
# explains why
|
|
358
|
+
# explains why any other refusal class, or an outright success,
|
|
320
359
|
# both count as "the guard did not fire," since enforce_givens
|
|
321
|
-
# runs
|
|
360
|
+
# runs first in DISPATCH_ORDER).
|
|
322
361
|
#
|
|
323
362
|
# Aggregate#preconditions closes for free alongside this — a
|
|
324
363
|
# no-block `given` reference (CommandBuilder#given) pushes the
|
|
325
|
-
#
|
|
364
|
+
# same Given struct object `enforce_givens` already iterates
|
|
326
365
|
# command.givens for, so there is no separate runtime path a
|
|
327
366
|
# property could exercise beyond what this already reaches.
|
|
328
367
|
# Entity#preconditions closes the identical way, one level down
|
|
329
368
|
# (ADR 0028) — a piece's own bare `given` reference pushes the
|
|
330
|
-
#
|
|
369
|
+
# same Given struct onto its own referencing command's givens,
|
|
331
370
|
# so LedgerEntry's own Amend/Reverse (banking) already exercise
|
|
332
371
|
# this through the exact mechanism above, no separate path.
|
|
333
372
|
#
|
|
334
373
|
# Real targets: Account.Debit/CloseAccount (`from:` guards),
|
|
335
374
|
# Credit/Debit (the named-once `given("customer is active")`
|
|
336
375
|
# precondition) — FreezeAccount deliberately references the
|
|
337
|
-
#
|
|
376
|
+
# different named precondition `"customer is not closed"` instead
|
|
338
377
|
# (a suspended customer must still be freezable), so it is not a
|
|
339
|
-
# `"customer is active"` example, just the same
|
|
378
|
+
# `"customer is active"` example, just the same mechanism.
|
|
379
|
+
#
|
|
380
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
381
|
+
# @return [true, String] true if every recomputed `enforce_givens`/
|
|
382
|
+
# `enforce_lifecycle_guard` check agrees with what the real dispatch did;
|
|
383
|
+
# otherwise a message naming the verb and the disagreement
|
|
340
384
|
def lifecycle_guard_and_given_violations_are_refused(history)
|
|
341
385
|
offenders = history.fetch(:guard_checks).filter_map do |check|
|
|
342
386
|
next if check[:recomputed_refused] == check[:actual_refused]
|
|
@@ -14,11 +14,11 @@ module Hecks
|
|
|
14
14
|
# (#check_piece_invariants, #eligible_rows, #nest_rows,
|
|
15
15
|
# #recompute_median) each leans on.
|
|
16
16
|
module InvariantsAndAggregation
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# admissibility.rb) checks these
|
|
20
|
-
#
|
|
21
|
-
# now re-checked a record
|
|
17
|
+
# Every stored record still satisfies its own aggregate's declared
|
|
18
|
+
# invariants — Admissibility#enforce_invariants (command_rules/
|
|
19
|
+
# admissibility.rb) checks these after every command's mutations,
|
|
20
|
+
# before save, the same point `ensures` is checked. Nothing until
|
|
21
|
+
# now re-checked a record after a whole replay finished, independent
|
|
22
22
|
# of whichever call site was supposed to have refused a violation
|
|
23
23
|
# in the first place — a record failing its own declared invariant
|
|
24
24
|
# here is proof a violating write landed anyway: the call site
|
|
@@ -27,9 +27,9 @@ module Hecks
|
|
|
27
27
|
#
|
|
28
28
|
# `history[:instances]` entries are already plain, symbol-keyed
|
|
29
29
|
# state Hashes (Replay.call's own `record.state`) — called against
|
|
30
|
-
# Evaluator.call the
|
|
30
|
+
# Evaluator.call the same way ValueObject::Builder#build already
|
|
31
31
|
# does for a VO's own invariants (value/coercion.rb), no GuardState
|
|
32
|
-
# wrapper needed the way enforce_invariants' own
|
|
32
|
+
# wrapper needed the way enforce_invariants' own live call uses one
|
|
33
33
|
# (GuardState exists for `parent.`/projected-field dereferencing
|
|
34
34
|
# mid-dispatch; a stored record's own scalar fields need none of
|
|
35
35
|
# that to re-check a same-aggregate invariant against itself).
|
|
@@ -39,8 +39,8 @@ module Hecks
|
|
|
39
39
|
#
|
|
40
40
|
# Entity#invariants (round 7) closes here too, not for free —
|
|
41
41
|
# `stored_records_satisfy_declared_invariants` only ever checked
|
|
42
|
-
# the
|
|
43
|
-
# checked against every
|
|
42
|
+
# the aggregate's own flat state; a piece's own invariant is
|
|
43
|
+
# checked against every element of a `list_of` field, a genuinely
|
|
44
44
|
# different walk `check_piece_invariants` below makes,
|
|
45
45
|
# independently of `Admissibility#check_entity_invariants` (the
|
|
46
46
|
# live enforcement path this property exists to catch drifting
|
|
@@ -49,6 +49,11 @@ module Hecks
|
|
|
49
49
|
#
|
|
50
50
|
# Real target: SafeDepositBox's own Visit — `invariant("a written
|
|
51
51
|
# note is not blank") { !note || !note.text.to_s.empty? }`.
|
|
52
|
+
#
|
|
53
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
54
|
+
# @return [true, String] true if every stored record satisfies its own
|
|
55
|
+
# (and its own entities') declared invariants; otherwise a message
|
|
56
|
+
# naming the record and the invariant it violates
|
|
52
57
|
def stored_records_satisfy_declared_invariants(history)
|
|
53
58
|
bluebooks = history.fetch(:bluebooks)
|
|
54
59
|
|
|
@@ -70,12 +75,18 @@ module Hecks
|
|
|
70
75
|
offenders.empty? || offenders.join("; ")
|
|
71
76
|
end
|
|
72
77
|
|
|
73
|
-
# A
|
|
74
|
-
# `list_of` field holds — the
|
|
78
|
+
# A piece's own invariant, checked against every element a
|
|
79
|
+
# `list_of` field holds — the same lookup `Admissibility#
|
|
75
80
|
# check_entity_invariants` makes (`owner.attributes.find { |a|
|
|
76
81
|
# a.list? && a.type.to_s == entity.hecks_name }`), independently
|
|
77
|
-
# reapplied here against a
|
|
82
|
+
# reapplied here against a stored record's own plain Hash state
|
|
78
83
|
# rather than a live `Instance`.
|
|
84
|
+
# @param owner_construct [Bluebook::Aggregate, Bluebook::Entity] the
|
|
85
|
+
# construct whose own `list_of` entities to check
|
|
86
|
+
# @param owner_state [Hash] `owner_construct`'s own stored state
|
|
87
|
+
# @param key [String] the top-level record key, for the message
|
|
88
|
+
# @return [String, nil] a message naming the first violating piece found,
|
|
89
|
+
# at any nesting depth; `nil` if every piece satisfies its invariants
|
|
79
90
|
def check_piece_invariants(owner_construct, owner_state, key)
|
|
80
91
|
owner_construct.entities.each do |entity|
|
|
81
92
|
next if entity.invariants.empty?
|
|
@@ -99,29 +110,33 @@ module Hecks
|
|
|
99
110
|
nil
|
|
100
111
|
end
|
|
101
112
|
|
|
102
|
-
# A
|
|
103
|
-
#
|
|
113
|
+
# A saga instance's own checkpoint survives being written and read
|
|
114
|
+
# back — the durability contract `SagaInterpreter#checkpoint` makes
|
|
104
115
|
# (`state:` plus a `deep_copy`d `memory:`, handed to whatever
|
|
105
116
|
# adapter answers `save_saga`) and `Registry#rehydrate_sagas!`
|
|
106
117
|
# promises to restore on the next boot (`each_saga` yielding
|
|
107
118
|
# `[pm, correlation, state, memory]` back into `saga_instances`).
|
|
108
|
-
# `Replay` captures the
|
|
119
|
+
# `Replay` captures the live store already materialised the same
|
|
109
120
|
# way `checkpoint` itself does (`Value.materialize`, not raw
|
|
110
121
|
# `Runtime::Value`s — see its own comment); this property pushes
|
|
111
|
-
# that captured memory through the
|
|
122
|
+
# that captured memory through the same `JSON.generate` then
|
|
112
123
|
# `JSON.parse(symbolize_names: true)` round-trip `checkpoint`'s own
|
|
113
124
|
# `deep_copy` performs (mirrored here rather than called — a
|
|
114
125
|
# private instance method with no registry to hand it) and checks
|
|
115
126
|
# it comes back byte-identical. A memory holding anything that
|
|
116
127
|
# round-trip cannot carry faithfully — a bare Symbol leaf, a
|
|
117
128
|
# non-JSON type a future field introduces — is corruption the
|
|
118
|
-
# durable path would introduce on a
|
|
129
|
+
# durable path would introduce on a real restart, caught here
|
|
119
130
|
# without needing one.
|
|
120
131
|
#
|
|
121
132
|
# `declares_state?` (Behaviour::ProcessManager) is the other half:
|
|
122
133
|
# a live or rehydrated instance sitting in a state the procedure
|
|
123
134
|
# never declares is the saga-durability twin of
|
|
124
135
|
# `lifecycle_values_are_declared` above.
|
|
136
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
137
|
+
# @return [true, String] true if every saga instance holds a declared
|
|
138
|
+
# state and its own memory survives a checkpoint round-trip; otherwise a
|
|
139
|
+
# message naming the process manager, correlation, and problem
|
|
125
140
|
def sagas_rehydrate_cleanly(history)
|
|
126
141
|
bluebook = history.fetch(:bluebook)
|
|
127
142
|
process_managers = bluebook.process_managers.to_h { |pm| [pm.name, pm] }
|
|
@@ -150,11 +165,11 @@ module Hecks
|
|
|
150
165
|
offenders.empty? || offenders.join("; ")
|
|
151
166
|
end
|
|
152
167
|
|
|
153
|
-
# A `for_each`
|
|
154
|
-
#
|
|
168
|
+
# A `for_each` policy dispatches exactly once per row its declared
|
|
169
|
+
# query answers — never once for the triggering event regardless of
|
|
155
170
|
# row count, never skipping a matched row, never firing on a row a
|
|
156
|
-
# concurrent mutation only made match
|
|
157
|
-
# computes the expected row-id set
|
|
171
|
+
# concurrent mutation only made match after the fact. `Replay`
|
|
172
|
+
# computes the expected row-id set independently, at the same
|
|
158
173
|
# instant the real dispatch runs (`Replay.expected_fan_out_rows`,
|
|
159
174
|
# the query oracle's own shape aimed at fan-out: two engines
|
|
160
175
|
# compared, never one graded against itself), and records it
|
|
@@ -163,6 +178,10 @@ module Hecks
|
|
|
163
178
|
# dispatch is the claim then, not "dispatched to zero rows," and a
|
|
164
179
|
# policy that dispatched anyway despite a failing guard is as real
|
|
165
180
|
# a finding as a row it skipped.
|
|
181
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
182
|
+
# @return [true, String] true if every fan-out finding's actual dispatches
|
|
183
|
+
# match its independently computed expected row set; otherwise a message
|
|
184
|
+
# naming the policy, event, and disagreement
|
|
166
185
|
def fanout_dispatches_once_per_matching_row(history)
|
|
167
186
|
offenders = history.fetch(:fan_outs).filter_map do |finding|
|
|
168
187
|
expected = finding[:expected_row_ids]
|
|
@@ -183,21 +202,21 @@ module Hecks
|
|
|
183
202
|
offenders.empty? || offenders.join("; ")
|
|
184
203
|
end
|
|
185
204
|
|
|
186
|
-
# A `count`/`median`
|
|
187
|
-
#
|
|
205
|
+
# A `count`/`median` report's reduced scalar matches the same
|
|
206
|
+
# reduction done independently, over the same eligible rows —
|
|
188
207
|
# `ReadModelInterpreter#project`'s own FK-join (root first, then
|
|
189
208
|
# each many-side head matched against it) and `#median` (odd →
|
|
190
209
|
# the true middle, even → the average of the two middles as a
|
|
191
210
|
# Float, empty → `nil`; `count` is the filtered length, empty →
|
|
192
211
|
# `0`), reproduced here in plain Ruby against `history[:instances]`
|
|
193
212
|
# rather than a live registry — `FieldPath.dig` +
|
|
194
|
-
# `Ports::Query::InMemory.comparable`/`.holds?` are the
|
|
213
|
+
# `Ports::Query::InMemory.comparable`/`.holds?` are the same two
|
|
195
214
|
# calls the interpreter itself makes to read a field and judge a
|
|
196
215
|
# `where`, called here rather than re-derived, so this oracle
|
|
197
216
|
# cannot drift from what "read a field" or "a clause holds" mean
|
|
198
217
|
# without the interpreter drifting the identical way.
|
|
199
218
|
#
|
|
200
|
-
# Only a report whose `:query` is answered by the
|
|
219
|
+
# Only a report whose `:query` is answered by the same bluebook
|
|
201
220
|
# `history[:bluebook]` carries (the bare `Domain.report_name`
|
|
202
221
|
# form, `domain == bluebook.name`) is checked — the same "only
|
|
203
222
|
# what we have the grammar for" scope `lifecycle_values_are_declared`
|
|
@@ -206,11 +225,15 @@ module Hecks
|
|
|
206
225
|
# ("only a report answered by this bluebook, only a count/median
|
|
207
226
|
# report, only one with a reduced many-side head") each gating the
|
|
208
227
|
# next, ending in one independent recomputation compared against
|
|
209
|
-
# the live answer. The guards are what make this oracle
|
|
228
|
+
# the live answer. The guards are what make this oracle scoped
|
|
210
229
|
# correctly, not incidental complexity — narrower than "every
|
|
211
230
|
# branch reads as its own precondition."
|
|
212
231
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
213
232
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
233
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
234
|
+
# @return [true, String] true if every eligible count/median report answer
|
|
235
|
+
# matches an independent recomputation; otherwise a message naming the
|
|
236
|
+
# query and the disagreement
|
|
214
237
|
def aggregation_matches_recompute(history)
|
|
215
238
|
bluebook = history.fetch(:bluebook)
|
|
216
239
|
|
|
@@ -239,14 +262,14 @@ module Hecks
|
|
|
239
262
|
end
|
|
240
263
|
|
|
241
264
|
# `aggregation_matches_recompute`'s own shape, extended from
|
|
242
|
-
# reducing a many-side head to a scalar (count/median) to
|
|
265
|
+
# reducing a many-side head to a scalar (count/median) to nesting
|
|
243
266
|
# it — `ReadModelInterpreter#group_by_target`/`#nest`, reproduced
|
|
244
267
|
# here in plain Ruby against `history[:instances]` the same way
|
|
245
268
|
# `eligible_rows` already reproduces the FK-join and `where`
|
|
246
269
|
# narrowing count/median share. `Value.materialize_unwrapped` is
|
|
247
|
-
# the
|
|
270
|
+
# the same call `#project` makes before nesting (a single-field
|
|
248
271
|
# value object recurses to its bare scalar — a real grouping key
|
|
249
|
-
# has to
|
|
272
|
+
# has to be one) — called here rather than re-derived, so this
|
|
250
273
|
# oracle cannot drift from what "the group key" means without the
|
|
251
274
|
# interpreter drifting the identical way.
|
|
252
275
|
#
|
|
@@ -257,6 +280,11 @@ module Hecks
|
|
|
257
280
|
# of count/median, nest_rows in place of recompute_median.
|
|
258
281
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
259
282
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
283
|
+
#
|
|
284
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
285
|
+
# @return [true, String] true if every eligible group_by report answer matches
|
|
286
|
+
# an independent recompute; otherwise a semicolon-joined message naming each
|
|
287
|
+
# offending query
|
|
260
288
|
def group_by_matches_recompute(history)
|
|
261
289
|
bluebook = history.fetch(:bluebook)
|
|
262
290
|
|
|
@@ -290,6 +318,11 @@ module Hecks
|
|
|
290
318
|
# per `group_by` field in declared order, leaf is the row with
|
|
291
319
|
# every grouped field stripped (already spent, as the keys that
|
|
292
320
|
# reached it).
|
|
321
|
+
# @param rows [Array<Hash>] materialized, symbol-keyed rows to nest
|
|
322
|
+
# @param fields [Array<Symbol>] the `group_by` fields, in declared order
|
|
323
|
+
# @return [Hash] one level of nesting per field, in order; the leaf under
|
|
324
|
+
# each key path is the first row in that group with every grouped field
|
|
325
|
+
# stripped
|
|
293
326
|
def nest_rows(rows, fields)
|
|
294
327
|
field, *rest = fields
|
|
295
328
|
rows.group_by { |row| row[field] }.transform_values do |group|
|
|
@@ -298,17 +331,27 @@ module Hecks
|
|
|
298
331
|
end
|
|
299
332
|
end
|
|
300
333
|
|
|
301
|
-
#
|
|
334
|
+
# The eligible rows a `count`/`median` head reduces — every
|
|
302
335
|
# instance of the reduced head's own aggregate, FK-matched against
|
|
303
336
|
# the report's root reference (if it has one; a rootless report has
|
|
304
337
|
# none to match) exactly the way `ReadModelInterpreter#reference_fields`
|
|
305
338
|
# finds the matching attribute, then narrowed by the report's own
|
|
306
|
-
# `where` clauses via the
|
|
339
|
+
# `where` clauses via the same `InMemory.holds?` the interpreter's
|
|
307
340
|
# `execute` calls.
|
|
341
|
+
# @param bluebook [Bluebook::Chapter] the bluebook the report belongs to
|
|
342
|
+
# @param instances [Hash] the snapshot to read rows from (`history[:instances]`
|
|
343
|
+
# shape, or the query's own `instances_at`)
|
|
344
|
+
# @param domain [String] the domain name the reduced aggregate belongs to
|
|
345
|
+
# @param model [Bluebook::ReadModel] the report being recomputed
|
|
346
|
+
# @param reduced_head [Hash] the many-side aggregate head being reduced, from
|
|
347
|
+
# `model.aggregate_heads`
|
|
348
|
+
# @param args [Hash] the query's own arguments, for FK matching and `where`
|
|
349
|
+
# evaluation
|
|
350
|
+
# @return [Array<Hash>] every eligible row's own state, `id:` merged in
|
|
308
351
|
def eligible_rows(bluebook, instances, domain, model, reduced_head, args)
|
|
309
352
|
aggregate = bluebook.aggregate(reduced_head[:aggregate])
|
|
310
353
|
prefix = "#{domain}::#{reduced_head[:aggregate]}#"
|
|
311
|
-
# `id:`
|
|
354
|
+
# `id:` merged in, the same `record.to_h` (`@state.merge(id:
|
|
312
355
|
# @id)`) every live head row carries — count/median never read
|
|
313
356
|
# it, but group_by_matches_recompute's own independent nesting
|
|
314
357
|
# does, the same way ReadModelInterpreter#row(record) = record.
|
|
@@ -337,6 +380,11 @@ module Hecks
|
|
|
337
380
|
# → the average of the two middle values, as a Float; empty → nil,
|
|
338
381
|
# never zero, so a caller cannot mistake "nothing to average" for
|
|
339
382
|
# "averaged to zero."
|
|
383
|
+
# @param rows [Array<Hash>] eligible rows, as returned by `#eligible_rows`
|
|
384
|
+
# @param field [Symbol] the field to average
|
|
385
|
+
# @return [Object, Float, nil] the true middle value for an odd count, the
|
|
386
|
+
# Float average of the two middle values for an even count, or `nil` for
|
|
387
|
+
# an empty `rows`
|
|
340
388
|
def recompute_median(rows, field)
|
|
341
389
|
values = rows.map { |state| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, field)) }
|
|
342
390
|
.compact.sort
|