hecks 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +51 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +301 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -20
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +124 -0
- data/lib/hecks/fuzzing/form_census.rb +199 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
- data/lib/hecks/fuzzing/properties/guards.rb +129 -26
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
- data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +84 -39
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +224 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +676 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +39 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
- data/lib/hecks/fuzzing/value_generator.rb +66 -14
- data/lib/hecks/fuzzing.rb +11 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +89 -21
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +250 -0
- data/lib/hecks/projections/glossary/markdown.rb +105 -0
- data/lib/hecks/projections/glossary/mermaid.rb +110 -0
- data/lib/hecks/projections/glossary/page.css +271 -0
- data/lib/hecks/projections/glossary/page.js +72 -0
- data/lib/hecks/projections/glossary/sections.rb +17 -0
- data/lib/hecks/projections/glossary/sentences.rb +205 -0
- data/lib/hecks/projections/glossary.rb +214 -286
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +15 -22
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +132 -24
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +94 -19
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +282 -46
- data/lib/hecks/runtime/entity_interpreter.rb +160 -69
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +111 -56
- data/lib/hecks/runtime/reaction_invocation.rb +76 -9
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -112
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +56 -27
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -88
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +93 -56
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +379 -226
- data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
- data/lib/hecks/runtime/value.rb +28 -23
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +207 -5
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +43 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
require_relative "../../bluebook/expression/evaluator"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Fuzzing
|
|
5
|
+
module Properties
|
|
6
|
+
# The transactional outbox's own contract (`Runtime::Outbox`, that
|
|
7
|
+
# file's own header), held to the history a replay actually produced
|
|
8
|
+
# rather than trusted. Two facts, each independently checkable from
|
|
9
|
+
# `history[:outbox_traces]` (`Replay#call`'s own before/after
|
|
10
|
+
# capture, one entry per step whose dispatch enqueued at least one
|
|
11
|
+
# row):
|
|
12
|
+
#
|
|
13
|
+
# 1. "delivery is inline by default" — a row this replay's own
|
|
14
|
+
# dispatch enqueued must not still be `pending`/`claimed` once
|
|
15
|
+
# that same call returns (nothing here ever simulates a
|
|
16
|
+
# crash), and must not be `failed` either — `deliver_row`'s
|
|
17
|
+
# own rescue only reaches `failed` for a genuine defect in the
|
|
18
|
+
# relay's own consumer resolution (a row naming a policy/
|
|
19
|
+
# process_manager `run_consumer`'s own independent registry
|
|
20
|
+
# lookup cannot find — `WiringError`), never an ordinary
|
|
21
|
+
# domain refusal (`PolicyInterpreter#deliver`/`SagaInterpreter#
|
|
22
|
+
# advance` both rescue those themselves, recording `delivered:
|
|
23
|
+
# false` on the reaction/saga log and letting `run_consumer`
|
|
24
|
+
# return normally). Both checked for every row, `saga:` and
|
|
25
|
+
# `policy:` alike.
|
|
26
|
+
#
|
|
27
|
+
# 2. A `policy:` row specifically — `PolicyInterpreter#deliver`
|
|
28
|
+
# returns `nil` (no `reaction_log` entry appended at all)
|
|
29
|
+
# exactly when its own `where` (or, for a fan-out policy, the
|
|
30
|
+
# same `where`, gating the whole `for_each`) does not hold;
|
|
31
|
+
# every other outcome (delivered, refused, a defect,
|
|
32
|
+
# reaction-depth-reached) is still a non-nil record `#react`
|
|
33
|
+
# appends. So a `delivered` policy row with no matching
|
|
34
|
+
# `reaction_log` entry is legitimate only when that policy's
|
|
35
|
+
# own `where`, independently re-evaluated here against the
|
|
36
|
+
# row's own recorded event, genuinely does not hold. A
|
|
37
|
+
# `for_each` policy's own fan-out correctness (how many rows
|
|
38
|
+
# it should have dispatched to) is `fanout_dispatches_once_
|
|
39
|
+
# per_matching_row`'s job, not this one's.
|
|
40
|
+
#
|
|
41
|
+
# A `saga:` row has no equivalent second check, deliberately — this
|
|
42
|
+
# was the first shape this property shipped with, and it was wrong,
|
|
43
|
+
# caught live against `examples/banking` before this comment
|
|
44
|
+
# existed: `Fanout.sagas`' own `listens?` (starts_on/ends_on/
|
|
45
|
+
# handler_for matching the event name alone) says nothing about
|
|
46
|
+
# whether a correlation resolves or a live instance exists, and
|
|
47
|
+
# `begin_saga`/`end_saga` (saga_interpreter.rb) both have silent,
|
|
48
|
+
# perfectly ordinary no-op paths that append nothing to `saga_log`
|
|
49
|
+
# — `begin_saga` when an instance under that correlation already
|
|
50
|
+
# exists, `end_saga` when no live instance exists to end (an
|
|
51
|
+
# `AccountOpened` fired by opening an account directly, bypassing
|
|
52
|
+
# the onboarding flow whose `ends_on` names that same event,
|
|
53
|
+
# reproduces this exactly: `Fanout.listens?` enqueues the row
|
|
54
|
+
# because the event name matches `ends_on`, `end_saga` finds
|
|
55
|
+
# nothing under that correlation to delete, and neither logs a
|
|
56
|
+
# word). A `saga:` row draining to `delivered` with zero matching
|
|
57
|
+
# `saga_log` entries is therefore not a finding — only check 1
|
|
58
|
+
# applies to it.
|
|
59
|
+
#
|
|
60
|
+
# **Not a grammar construct** — `FEATURE_COVERAGE`'s own `dry_runs_
|
|
61
|
+
# leave_no_trace` precedent: the outbox is a runtime door
|
|
62
|
+
# (`Runtime::Outbox`), not a word a bluebook declares, so there is
|
|
63
|
+
# no feature string here to claim.
|
|
64
|
+
module Outbox
|
|
65
|
+
def outbox_rows_match_reactions(history)
|
|
66
|
+
bluebooks = history.fetch(:bluebooks, {})
|
|
67
|
+
|
|
68
|
+
offenders = Array(history[:outbox_traces]).flat_map do |trace|
|
|
69
|
+
trace[:rows].flat_map { |row| outbox_row_offenders(row, trace, bluebooks) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
offenders.empty? || offenders.join("; ")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def outbox_row_offenders(row, trace, bluebooks)
|
|
76
|
+
on = row.dig(:event, :name)
|
|
77
|
+
|
|
78
|
+
case row[:status]
|
|
79
|
+
when "pending", "claimed"
|
|
80
|
+
["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) never drained inline — status stayed " \
|
|
81
|
+
"#{row[:status].inspect} though delivery is inline by contract (Runtime::Outbox's own header)"]
|
|
82
|
+
when "failed"
|
|
83
|
+
["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) failed to deliver: #{row[:error]} — " \
|
|
84
|
+
"a domain refusal never reaches this far; a failed row names a defect in the relay's own consumer " \
|
|
85
|
+
"resolution"]
|
|
86
|
+
when "delivered"
|
|
87
|
+
outbox_delivered_policy_offenders(row, on, trace, bluebooks)
|
|
88
|
+
else
|
|
89
|
+
[]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# See this file's own header for why a `saga:` row is exempt: its
|
|
94
|
+
# own `listens?` gives no such guarantee, unlike a policy's single,
|
|
95
|
+
# deterministic `where` gate.
|
|
96
|
+
def outbox_delivered_policy_offenders(row, on, trace, bluebooks)
|
|
97
|
+
kind, fqn = row[:consumer].to_s.split(":", 2)
|
|
98
|
+
return [] unless kind == "policy"
|
|
99
|
+
|
|
100
|
+
home, name = fqn.to_s.split("::", 2)
|
|
101
|
+
return [] if trace[:reactions].any? { |entry| entry[:policy] == name && entry[:on] == on }
|
|
102
|
+
|
|
103
|
+
policy = bluebooks[home]&.policies&.find { |candidate| candidate.name == name }
|
|
104
|
+
return [] unless policy # nothing declared under this name — inconclusive, not a claimed mismatch
|
|
105
|
+
return [] if policy.fans_out? # fan-out row count is fanout_dispatches_once_per_matching_row's job
|
|
106
|
+
|
|
107
|
+
held = independently_re_evaluate_policy_where(policy, row[:event])
|
|
108
|
+
return [] if held != true # false, or inconclusive (the where itself raised) — never a claimed mismatch
|
|
109
|
+
|
|
110
|
+
["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) drained as delivered, but no matching " \
|
|
111
|
+
"reaction_log entry exists and the policy's own where clause independently re-evaluates true — " \
|
|
112
|
+
"PolicyInterpreter#deliver only ever returns nil (no reaction_log entry) when where does not hold"]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# `PolicyInterpreter#where_holds?`'s own two branches, reproduced —
|
|
116
|
+
# never calling that method again, which would only ever agree
|
|
117
|
+
# with itself (the same rule `resolve_dispatch_binding`'s own
|
|
118
|
+
# comment states). `Evaluator.call` (the raw-string entry, parsed
|
|
119
|
+
# and cached — never `call_rule`, which needs the policy's own
|
|
120
|
+
# build-time `where_rule` AST, an object this history has no
|
|
121
|
+
# reason to carry) is the exact same call `Replay#fan_out_finding`
|
|
122
|
+
# already makes for the identical fact one property over
|
|
123
|
+
# (`policy.where.to_s.empty? || Evaluator.call(policy.where, {},
|
|
124
|
+
# payload)`), reused rather than re-derived a second, slightly
|
|
125
|
+
# different way. `rescue`d to `nil`, not `false`: a where clause
|
|
126
|
+
# that cannot be re-evaluated from the row's own recorded payload
|
|
127
|
+
# alone is inconclusive, not proof either way — the same "never a
|
|
128
|
+
# claimed pass or a claimed mismatch from a resolution this replay
|
|
129
|
+
# cannot actually reproduce" discipline `build_guard_check`'s own
|
|
130
|
+
# rescue clause already follows.
|
|
131
|
+
def independently_re_evaluate_policy_where(policy, event)
|
|
132
|
+
return true if policy.where.to_s.empty?
|
|
133
|
+
|
|
134
|
+
payload = (event[:payload] || {}).transform_keys(&:to_sym)
|
|
135
|
+
Bluebook::Expression::Evaluator.call(policy.where, {}, payload)
|
|
136
|
+
rescue StandardError
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -10,7 +10,7 @@ module Hecks
|
|
|
10
10
|
# (#query_for_verb, #query_eligible_rows, #resolve_hop_clause) other
|
|
11
11
|
# property modules in this directory also call.
|
|
12
12
|
module Querying
|
|
13
|
-
#
|
|
13
|
+
# The query oracle — differential testing within the one runtime,
|
|
14
14
|
# the shape the retired cross-runtime harness should always have
|
|
15
15
|
# been. Every generated ask was answered twice at the same instant
|
|
16
16
|
# (Replay records both): once through whatever the aggregate is
|
|
@@ -18,17 +18,17 @@ module Hecks
|
|
|
18
18
|
# a SQL binding would compile it), once through the reference
|
|
19
19
|
# interpreter's own evaluation. The two are separate, live
|
|
20
20
|
# implementations of the same comparator vocabulary, and they have
|
|
21
|
-
# drifted before — an adapter that
|
|
21
|
+
# drifted before — an adapter that accepts what the reference says
|
|
22
22
|
# matches nothing, or orders what it refuses to order, shows up
|
|
23
23
|
# here as a finding no self-referential adapter spec could see.
|
|
24
24
|
# M23 — `Replay` now runs the native and reference engines
|
|
25
|
-
#
|
|
25
|
+
# independently (each in its own begin/rescue — see that file's own
|
|
26
26
|
# comment at the capture site), so this property can tell apart what
|
|
27
27
|
# used to be indistinguishable: "both engines refused" (fine — the
|
|
28
28
|
# ask was genuinely bad, nothing to compare) from "one refused and
|
|
29
29
|
# the other did not" (a real divergence — the two engines disagree
|
|
30
|
-
# about whether the ask was even
|
|
31
|
-
# `native_refused`/`reference_refused` are read by
|
|
30
|
+
# about whether the ask was even valid, never mind what it answers).
|
|
31
|
+
# `native_refused`/`reference_refused` are read by key presence, not
|
|
32
32
|
# truthiness — `Replay` only ever adds `:error`/`:reference_error`
|
|
33
33
|
# to an entry when that side actually raised, so an absent key is an
|
|
34
34
|
# unambiguous "this side answered." A read-model ask (no reference
|
|
@@ -59,16 +59,16 @@ module Hecks
|
|
|
59
59
|
offenders.empty? || offenders.join("; ")
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
#
|
|
62
|
+
# The same "two engines, compared" shape query_answers_match_reference
|
|
63
63
|
# already uses, aimed squarely at Query#options' offset/limit pair —
|
|
64
|
-
# but recomputed from history[:instances] directly, a
|
|
64
|
+
# but recomputed from history[:instances] directly, a third,
|
|
65
65
|
# independent computation, rather than comparing QueryInterpreter's
|
|
66
66
|
# own native and reference paths against each other (which could
|
|
67
67
|
# share the identical bug neither implementation happened to hit —
|
|
68
|
-
# see #4's own fix, which touched
|
|
68
|
+
# see #4's own fix, which touched both #interpret and
|
|
69
69
|
# #reference_interpret at once). `order_by` declared alongside
|
|
70
70
|
# `offset` or `limit` names a genuinely paged query. Ports::Query::
|
|
71
|
-
# Ordering.apply is the
|
|
71
|
+
# Ordering.apply is the same engine QueryInterpreter#ordered calls,
|
|
72
72
|
# reused here rather than re-derived, so this oracle cannot drift
|
|
73
73
|
# from what "in order" means without the interpreter drifting the
|
|
74
74
|
# identical way — only the offset-then-limit .drop/.first slice
|
|
@@ -112,7 +112,7 @@ module Hecks
|
|
|
112
112
|
offenders.empty? || offenders.join("; ")
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
#
|
|
115
|
+
# The declared Query itself, resolved from a replayed verb — the
|
|
116
116
|
# same shape #command_for_verb resolves a command by, one
|
|
117
117
|
# construct over. Entity-level queries (a dotted query_path) are
|
|
118
118
|
# out of scope here — paging on an entity's own list has no real
|
|
@@ -128,24 +128,24 @@ module Hecks
|
|
|
128
128
|
aggregate&.query(query_path)
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
# A
|
|
131
|
+
# A query's own rows — unlike #eligible_rows (a ReadModel's
|
|
132
132
|
# reduced/grouped many-side head, possibly FK-joined against a
|
|
133
|
-
# root), a Query always asks about its
|
|
133
|
+
# root), a Query always asks about its own owning aggregate
|
|
134
134
|
# directly ; no join, no reference_target. `id:` merged in the
|
|
135
135
|
# same way #eligible_rows' own rows are, since a stable sort
|
|
136
136
|
# (Ordering.apply's own `identity:`) and the real answer's own
|
|
137
137
|
# `record.state.merge(id: record.id)` both need it.
|
|
138
|
-
# `bluebooks:` — needed
|
|
138
|
+
# `bluebooks:` — needed only to recognise and resolve a `/` hop
|
|
139
139
|
# clause (`engagement/client/status`, hop_chain.bluebook's own
|
|
140
|
-
# PricedAboveViaEngagement): a hop's head names one of the
|
|
140
|
+
# PricedAboveViaEngagement): a hop's head names one of the owning
|
|
141
141
|
# aggregate's declared references, and only the declaration graph
|
|
142
142
|
# can say which attribute that is and which aggregate it targets.
|
|
143
143
|
# A local clause never consults it. Latent gap this closed, found
|
|
144
144
|
# by the fuzzer itself the first time a generated sequence ever
|
|
145
|
-
# built a full hop chain
|
|
145
|
+
# built a full hop chain and had its paged query answer a row
|
|
146
146
|
# (seed 1, the moment scalar_value_objects.bluebook joined the
|
|
147
147
|
# fixtures corpus and shifted every seeded draw): the recompute
|
|
148
|
-
# dug `engagement/client/status` as a
|
|
148
|
+
# dug `engagement/client/status` as a local dotted path, found
|
|
149
149
|
# nil, and declared every genuinely-eligible row ineligible — a
|
|
150
150
|
# false property violation against a correct runtime answer,
|
|
151
151
|
# reproducible on an untouched main with this same 4-step script.
|
|
@@ -173,7 +173,7 @@ module Hecks
|
|
|
173
173
|
# would be checking the runtime against itself). One hop peels
|
|
174
174
|
# off the head (`HopPath.next_hop`, the identical one-step
|
|
175
175
|
# primitive the live fold uses), the inner clause recurses
|
|
176
|
-
# through `query_eligible_rows` against the
|
|
176
|
+
# through `query_eligible_rows` against the target's own
|
|
177
177
|
# snapshot rows (so a multi-hop tail resolves hop by hop, exactly
|
|
178
178
|
# as the live path's own recursion does), and the ids that
|
|
179
179
|
# answered fold back as the same local `in` membership clause the
|
|
@@ -9,35 +9,37 @@ require_relative "properties/querying"
|
|
|
9
9
|
require_relative "properties/guards"
|
|
10
10
|
require_relative "properties/dispatch_and_mutations"
|
|
11
11
|
require_relative "properties/invariants_and_aggregation"
|
|
12
|
+
require_relative "properties/corrections"
|
|
13
|
+
require_relative "properties/outbox"
|
|
12
14
|
|
|
13
15
|
module Hecks
|
|
14
16
|
module Fuzzing
|
|
15
|
-
# Declared properties, checked over a
|
|
17
|
+
# Declared properties, checked over a replayed history — the other
|
|
16
18
|
# half of property-based testing the fuzzer was missing: it already
|
|
17
19
|
# generates and (with bin/fuzz's shrinker) minimizes, but checked
|
|
18
20
|
# nothing beyond "did the interpreter crash" and "did the replay
|
|
19
21
|
# match the claim." A property here is a fact that should hold of
|
|
20
|
-
#
|
|
22
|
+
# any history a valid domain produces, independent of which seed
|
|
21
23
|
# produced it.
|
|
22
24
|
#
|
|
23
25
|
# Each property is `name => ->(history) { true/false, or a message
|
|
24
26
|
# string naming what broke }` — a truthy return (including `true`)
|
|
25
|
-
# is a pass; a String return is a failure, and the string
|
|
27
|
+
# is a pass; a String return is a failure, and the string is the
|
|
26
28
|
# finding. `history` is Replay's return shape.
|
|
27
29
|
#
|
|
28
|
-
#
|
|
30
|
+
# Every property declares the language feature it covers, in
|
|
29
31
|
# `FEATURE_COVERAGE` below — a "Construct#attribute" pair spelled
|
|
30
32
|
# exactly as `Bluebook::MetaValidator.grammar_registry` names it,
|
|
31
|
-
# the
|
|
33
|
+
# the same meta-domain that judges every real bluebook (see that
|
|
32
34
|
# module's own header: "the language IS the source"). That is the
|
|
33
35
|
# link this file exists to make real: a construct the language
|
|
34
36
|
# declares is a fact `spec/meta_domain_coverage_spec.rb` can
|
|
35
37
|
# enumerate on its own, without anyone re-typing the list here —
|
|
36
38
|
# so a new attribute added to `language/bluebook/*.bluebook` shows
|
|
37
|
-
# up in that spec as
|
|
39
|
+
# up in that spec as unclaimed the moment it lands, not whenever
|
|
38
40
|
# someone remembers to go looking. Claiming a feature here is a
|
|
39
41
|
# deliberate act (a real property, checked at least once failing
|
|
40
|
-
#
|
|
42
|
+
# and once passing — `spec/fuzzing/properties_spec.rb`'s own
|
|
41
43
|
# discipline) or an explicit, reasoned exemption in that same
|
|
42
44
|
# spec — never silence.
|
|
43
45
|
module Properties
|
|
@@ -55,15 +57,18 @@ module Hecks
|
|
|
55
57
|
extend Querying
|
|
56
58
|
extend Guards
|
|
57
59
|
extend DispatchAndMutations
|
|
60
|
+
extend DryRuns
|
|
58
61
|
extend InvariantsAndAggregation
|
|
62
|
+
extend Corrections
|
|
63
|
+
extend Outbox
|
|
59
64
|
|
|
60
65
|
module_function
|
|
61
66
|
|
|
62
|
-
#
|
|
67
|
+
# Which language feature each property is answerable for. Not
|
|
63
68
|
# exhaustive of everything a property's body happens to touch —
|
|
64
69
|
# `Command#attributes`, say, is exercised by nearly every property
|
|
65
|
-
# here without being what any of them was
|
|
66
|
-
# exhaustive of the feature that would go
|
|
70
|
+
# here without being what any of them was written to guard — but
|
|
71
|
+
# exhaustive of the feature that would go unchecked if this
|
|
67
72
|
# property did not exist. That is the question the coverage gate
|
|
68
73
|
# actually asks.
|
|
69
74
|
FEATURE_COVERAGE = {
|
|
@@ -75,17 +80,17 @@ module Hecks
|
|
|
75
80
|
authorize_scopes_or_refuses: %w[Query#options],
|
|
76
81
|
guard_refusals_are_declared: %w[Command#givens Command#ensures],
|
|
77
82
|
lifecycle_guard_and_given_violations_are_refused: %w[Command#from Aggregate#preconditions Entity#preconditions],
|
|
78
|
-
# Dispatch#command_name/Dispatch#with_spec are
|
|
79
|
-
# feature names — META_DOMAIN_ALL_FEATURES only walks
|
|
83
|
+
# Dispatch#command_name/Dispatch#with_spec are not claimable
|
|
84
|
+
# feature names — META_DOMAIN_ALL_FEATURES only walks one level
|
|
80
85
|
# of entity nesting (`agg.entities.flat_map`, meta_domain_
|
|
81
|
-
# coverage_spec.rb), and Dispatch sits
|
|
86
|
+
# coverage_spec.rb), and Dispatch sits two deep (ProcessManager
|
|
82
87
|
# -> Handler -> Dispatch), so those strings never exist there
|
|
83
88
|
# to claim — a pre-existing meta-domain coverage-generation gap,
|
|
84
89
|
# found here (their old META_DOMAIN_KNOWN_GAPS entries were
|
|
85
90
|
# themselves already-orphaned strings no completeness check ever
|
|
86
91
|
# verified, since KNOWN_GAPS has no "never lets a gap rot" check
|
|
87
92
|
# the way FEATURE_COVERAGE/GUARANTEED_BY_CONSTRUCTION both do).
|
|
88
|
-
# This property still closes the
|
|
93
|
+
# This property still closes the real behavior both would have
|
|
89
94
|
# named — a Dispatch's own command_name/with_spec are exactly
|
|
90
95
|
# what dispatch_args resolves and this property checks — the
|
|
91
96
|
# grammar just has no feature string for either one.
|
|
@@ -96,39 +101,70 @@ module Hecks
|
|
|
96
101
|
fanout_dispatches_once_per_matching_row: %w[Policy#for_each Policy#where],
|
|
97
102
|
aggregation_matches_recompute: %w[ReadModel#count ReadModel#median_field],
|
|
98
103
|
stored_records_satisfy_declared_invariants: %w[Aggregate#invariants Entity#invariants],
|
|
99
|
-
group_by_matches_recompute: %w[ReadModel#group_by]
|
|
104
|
+
group_by_matches_recompute: %w[ReadModel#group_by],
|
|
105
|
+
# A runtime door, not a grammar construct — `Dispatcher#dry_run?`
|
|
106
|
+
# is something an application asks of a booted domain, not a
|
|
107
|
+
# word a bluebook can declare, so there is no feature string
|
|
108
|
+
# for it to claim. Listed (empty) rather than omitted so the
|
|
109
|
+
# discipline this table states — every property names what it
|
|
110
|
+
# is answerable for — has no silent exception.
|
|
111
|
+
dry_runs_leave_no_trace: [],
|
|
112
|
+
# Another runtime door, not a grammar construct — same reasoning
|
|
113
|
+
# as dry_runs_leave_no_trace right above: `Runtime::Outbox` is
|
|
114
|
+
# something a persistence adapter provides underneath a booted
|
|
115
|
+
# domain, never a word a bluebook declares.
|
|
116
|
+
outbox_rows_match_reactions: [],
|
|
117
|
+
# **The `corrects` mutation's own target** — this property reads
|
|
118
|
+
# `command.mutations.select { op == :corrects }` and asks whether
|
|
119
|
+
# the event each one names was ever actually emitted, so the
|
|
120
|
+
# feature it answers for is the mutation list, the same one
|
|
121
|
+
# `mutations_match_recompute` reads for a different question.
|
|
122
|
+
# (Not `Command#references`: that field is the dangling-reference
|
|
123
|
+
# question no property asks yet, and it stays a named gap.)
|
|
124
|
+
corrections_reference_an_emitted_event: %w[Command#mutations],
|
|
125
|
+
# No feature string exists for what this one reads. It depends on
|
|
126
|
+
# an argument's own `relationship` (which reference-typed argument
|
|
127
|
+
# points at which aggregate) — but `Argument` is a value object,
|
|
128
|
+
# and the meta-domain walk enumerates aggregate and entity fields
|
|
129
|
+
# only, so no `Argument#…` name is claimable. The declaration side
|
|
130
|
+
# it shares with queries, `authorize …, tenant:`, is
|
|
131
|
+
# `Query#options`, already claimed by `authorize_scopes_or_refuses`;
|
|
132
|
+
# claiming it twice would say this property covers a query
|
|
133
|
+
# question it never asks. Listed (empty) rather than omitted, the
|
|
134
|
+
# same discipline the two runtime doors above keep.
|
|
135
|
+
commands_respect_tenant_scope: []
|
|
100
136
|
}.freeze
|
|
101
137
|
|
|
102
|
-
#
|
|
103
|
-
#
|
|
138
|
+
# Features a replay property could never catch violated, because the
|
|
139
|
+
# runtime's own construction makes the violation impossible to
|
|
104
140
|
# produce in the first place — not "untested," but unfalsifiable by
|
|
105
141
|
# a history, the same class of guarantee this codebase already
|
|
106
|
-
# states for identity ("
|
|
107
|
-
# own header) and now generalises. Each entry names the
|
|
142
|
+
# states for identity ("nothing is minted" — command_interpreter.rb's
|
|
143
|
+
# own header) and now generalises. Each entry names the one place in
|
|
108
144
|
# the runtime that makes it true, universally, for every domain and
|
|
109
145
|
# every adapter — never per-domain logic a future domain could
|
|
110
146
|
# accidentally route around.
|
|
111
147
|
#
|
|
112
|
-
#
|
|
113
|
-
# declares
|
|
148
|
+
# The bluebook/hecksagon boundary is why this works: a bluebook
|
|
149
|
+
# declares shape (attribute types, patterns, closed sets, VO
|
|
114
150
|
# invariants — see docs/decisions/0009), and shape is enforced by
|
|
115
|
-
#
|
|
151
|
+
# one coercion door every domain's every attribute passes through
|
|
116
152
|
# (`Runtime::Value.build`, via value/coercion.rb + value/admission.rb)
|
|
117
153
|
# regardless of which hecksagon later binds the aggregate to Memory,
|
|
118
154
|
# Postgres, or anything else. A value that violated its own declared
|
|
119
|
-
# pattern, invariant, or closed set could never be
|
|
120
|
-
# could never be
|
|
155
|
+
# pattern, invariant, or closed set could never be coerced, so it
|
|
156
|
+
# could never be stored, so it could never appear in a replay's own
|
|
121
157
|
# `:instances` to be caught violating it. Checking for it after the
|
|
122
158
|
# fact would be watching for something the construction path already
|
|
123
159
|
# made impossible.
|
|
124
160
|
#
|
|
125
|
-
#
|
|
126
|
-
# the
|
|
161
|
+
# Not a place to hide a real gap — a feature belongs here only once
|
|
162
|
+
# the specific enforcing code path has been read and confirmed, the
|
|
127
163
|
# same discipline `spec/fuzzing/meta_domain_coverage_spec.rb` demands
|
|
128
164
|
# of `KNOWN_GAPS` in the other direction. `Entity#identified_by` was
|
|
129
|
-
# checked
|
|
165
|
+
# checked for this category once before and found not to qualify —
|
|
130
166
|
# `command_interpreter.rb`'s `AlreadyExists` refusal was given to
|
|
131
|
-
# every
|
|
167
|
+
# every creating aggregate command uniformly, and MutationApplier
|
|
132
168
|
# (command_interpreter/mutation_applier.rb) had no matching check on
|
|
133
169
|
# an entity's own append. It does now: #check_entity_collision runs
|
|
134
170
|
# unconditionally on both branches an entity identity can arrive by
|
|
@@ -154,13 +190,18 @@ module Hecks
|
|
|
154
190
|
"(command_interpreter.rb, command.creates?) for every creating command uniformly, " \
|
|
155
191
|
"before a duplicate id can ever be stored — collision is refused at the door, not " \
|
|
156
192
|
"produced and later caught",
|
|
157
|
-
"Entity#identified_by" => "
|
|
158
|
-
"checks Array(current) against every part
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"
|
|
163
|
-
"
|
|
193
|
+
"Entity#identified_by" => "EntityElement.check_entity_collision (runtime/entity_element.rb, moved there " \
|
|
194
|
+
"BUG#145 so both call sites share it) checks Array(current) against every part " \
|
|
195
|
+
"of the entity's own identity before an append can land — MutationApplier#" \
|
|
196
|
+
"entity_element's aggregate-owned call (Workspace.boards, on both branches " \
|
|
197
|
+
"identity arrives by: caller-supplied, or composite) AND EntityElement#" \
|
|
198
|
+
"appended_to_element's entity-owned, nested-one-hop-further call (Board.cards — " \
|
|
199
|
+
"unconditional, no auto-mint branch exists at that depth) — the same " \
|
|
200
|
+
"AlreadyExists refusal Aggregate#identified_by gets above, one or two levels " \
|
|
201
|
+
"down. Auto-minted (aggregate-owned) entities never reach the check " \
|
|
202
|
+
"(current.size + 1 can't repeat unless something remove:s from the list between " \
|
|
203
|
+
"mints, which no real domain does today — see the comment on #entity_element " \
|
|
204
|
+
"itself)",
|
|
164
205
|
"Command#attributes" => "command arguments are coerced through the SAME Value.build door as any other " \
|
|
165
206
|
"attribute — an accepted dispatch's own args already passed pattern/admits/invariant checks",
|
|
166
207
|
"Command#emits" => "CommandRules::Emission#emit iterates command.emits ITSELF to construct every " \
|
|
@@ -181,7 +222,7 @@ module Hecks
|
|
|
181
222
|
# ValueObject), so this reads "Member#pairs", not "Member#shape" —
|
|
182
223
|
# the free-text, un-parsed spelling a standalone root once needed
|
|
183
224
|
# no longer exists at all, an entity's own element is never
|
|
184
|
-
# serialized as text. "ValueObject#members" is the
|
|
225
|
+
# serialized as text. "ValueObject#members" is the same fact
|
|
185
226
|
# "ValueObject#rows" already counts, seen from the other side — a
|
|
186
227
|
# value object cannot declare admitted rows without a members list
|
|
187
228
|
# to hold them, and vice versa.
|
|
@@ -189,8 +230,8 @@ module Hecks
|
|
|
189
230
|
"Member#pairs" => "one level into ValueObject#rows — same door"
|
|
190
231
|
}.freeze
|
|
191
232
|
|
|
192
|
-
#
|
|
193
|
-
# except determinism, which needs to replay
|
|
233
|
+
# The standard battery, run over one replayed history — everything
|
|
234
|
+
# except determinism, which needs to replay twice itself and so
|
|
194
235
|
# takes the steps directly rather than a single history.
|
|
195
236
|
def check(history)
|
|
196
237
|
{ lifecycle_values_are_declared: lifecycle_values_are_declared(history),
|
|
@@ -205,8 +246,12 @@ module Hecks
|
|
|
205
246
|
paging_offset_partitions_correctly: paging_offset_partitions_correctly(history),
|
|
206
247
|
lifecycle_guard_and_given_violations_are_refused: lifecycle_guard_and_given_violations_are_refused(history),
|
|
207
248
|
authorize_scopes_or_refuses: authorize_scopes_or_refuses(history),
|
|
249
|
+
commands_respect_tenant_scope: commands_respect_tenant_scope(history),
|
|
208
250
|
dispatch_binding_fidelity: dispatch_binding_fidelity(history),
|
|
209
|
-
mutations_match_recompute: mutations_match_recompute(history)
|
|
251
|
+
mutations_match_recompute: mutations_match_recompute(history),
|
|
252
|
+
dry_runs_leave_no_trace: dry_runs_leave_no_trace(history),
|
|
253
|
+
corrections_reference_an_emitted_event: corrections_reference_an_emitted_event(history),
|
|
254
|
+
outbox_rows_match_reactions: outbox_rows_match_reactions(history) }
|
|
210
255
|
end
|
|
211
256
|
end
|
|
212
257
|
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Fuzzing
|
|
5
|
+
# Reads `qa/settings.yml` — the hecks_qa practice's own dials, now
|
|
6
|
+
# data in a file rather than Ruby constant literals. The why of each
|
|
7
|
+
# dial (what it does, who reads it) stays exactly one place: the
|
|
8
|
+
# comments on `QualityControlDials` in `qa/bluebook/quality_control.
|
|
9
|
+
# bluebook`, which now sources every value from an instance of this
|
|
10
|
+
# class instead of writing it inline. This class is only the loading
|
|
11
|
+
# and the validation — no dial policy lives here.
|
|
12
|
+
#
|
|
13
|
+
# **Fails loud, not quiet** — the whole practice's own opening line
|
|
14
|
+
# ("the enemy is the quiet divergence") applies to its own settings
|
|
15
|
+
# file too: a missing key, an extra key nothing recognises, or a
|
|
16
|
+
# value of the wrong shape all raise immediately, at load time
|
|
17
|
+
# (which is bluebook-load time, i.e. `Hecks.boot`), naming exactly
|
|
18
|
+
# what's wrong — never a `nil` dial silently reaching a script that
|
|
19
|
+
# assumes a number.
|
|
20
|
+
#
|
|
21
|
+
# **Plain data in, frozen data out**. `.load` parses the YAML with
|
|
22
|
+
# `Psych.safe_load_file` (no custom tags, no arbitrary Ruby objects)
|
|
23
|
+
# and hands back an instance whose accessors are the exact values a
|
|
24
|
+
# human wrote in the file — a `Hash`/`Array` for the nested dials,
|
|
25
|
+
# never a second, richer wrapper type nothing else in this practice
|
|
26
|
+
# expects.
|
|
27
|
+
class QaSettings
|
|
28
|
+
# One entry per dial this class knows about: the accessor name
|
|
29
|
+
# (matching `qa/settings.yml`'s own key, and `QualityControlDials`'
|
|
30
|
+
# constant name snake_cased) mapped to the class (or classes) a
|
|
31
|
+
# valid value must be an instance of. `TrueClass`/`FalseClass`
|
|
32
|
+
# both name a boolean dial — Ruby has no single class both `true`
|
|
33
|
+
# and `false` share. `Numeric` admits both an Integer and a Float
|
|
34
|
+
# for a fraction dial (`0` and `0.0` are both a human plausibly
|
|
35
|
+
# types for "off").
|
|
36
|
+
EXPECTED_TYPES = {
|
|
37
|
+
cadence_seconds: Integer,
|
|
38
|
+
pr_cap_per_day: Integer,
|
|
39
|
+
widening_tiers: Array,
|
|
40
|
+
sweep_max_parallel: Integer,
|
|
41
|
+
liveness_fallback_seconds: Integer,
|
|
42
|
+
draft_only: [TrueClass, FalseClass],
|
|
43
|
+
auto_merge: [TrueClass, FalseClass],
|
|
44
|
+
branch_prefix: String,
|
|
45
|
+
adversarial_fraction: Numeric,
|
|
46
|
+
guided_generation: [TrueClass, FalseClass],
|
|
47
|
+
corpus_splice_probability: Numeric,
|
|
48
|
+
favor_rare_verbs: Integer,
|
|
49
|
+
self_consistency_checks: [TrueClass, FalseClass],
|
|
50
|
+
shrink_budget: Integer,
|
|
51
|
+
yield_weight_seconds: Integer,
|
|
52
|
+
yield_decay_percent: Integer,
|
|
53
|
+
rotation_stale_floor_seconds: Integer,
|
|
54
|
+
persistence_parity_seed_cap: Integer,
|
|
55
|
+
concurrency_seed_cap: Integer,
|
|
56
|
+
adapter_parity_pairs: Hash,
|
|
57
|
+
modes: Hash,
|
|
58
|
+
role_draw_probability: Numeric,
|
|
59
|
+
dry_run_fraction: Numeric,
|
|
60
|
+
generated_domains_per_tick: Integer,
|
|
61
|
+
generated_domains_rust: [TrueClass, FalseClass],
|
|
62
|
+
generated_domain_seeds: Integer,
|
|
63
|
+
structural_refusal_boundary: Array
|
|
64
|
+
}.freeze
|
|
65
|
+
|
|
66
|
+
attr_reader(*EXPECTED_TYPES.keys)
|
|
67
|
+
|
|
68
|
+
# **The real file, always** — resolved off this file's own `__dir__`
|
|
69
|
+
# (lib/hecks/fuzzing/), never off the caller's. `QualityControlDials`
|
|
70
|
+
# is defined inside `qa/bluebook/quality_control.bluebook`, and that
|
|
71
|
+
# exact directory gets copied to a tmpdir for every isolated/replayed
|
|
72
|
+
# boot (`Hecks::Fuzzing::IsolatedBoot#copy_dereferencing` copies only
|
|
73
|
+
# `qa/bluebook`'s own contents, never its parent `qa/`) — a path
|
|
74
|
+
# resolved from the bluebook's own `__dir__` would silently point at
|
|
75
|
+
# a copy with no `settings.yml` beside it at all. `qa/settings.yml`
|
|
76
|
+
# is read-only, human-edited data with no lifecycle (see this class's
|
|
77
|
+
# own header) — there is no isolation reason to ever read a copy of
|
|
78
|
+
# it, real boot or fuzzed one, so every caller gets the one real file
|
|
79
|
+
# by default. `qa_settings_spec.rb` passes its own fixture paths
|
|
80
|
+
# explicitly instead, the same way every other test in this practice
|
|
81
|
+
# that needs a non-default dial passes one in rather than mutating
|
|
82
|
+
# global state.
|
|
83
|
+
DEFAULT_PATH = File.expand_path("../../../qa/settings.yml", __dir__)
|
|
84
|
+
|
|
85
|
+
class << self
|
|
86
|
+
def load(path = DEFAULT_PATH)
|
|
87
|
+
raise ArgumentError, "qa settings file not found: #{path}" unless File.file?(path)
|
|
88
|
+
|
|
89
|
+
raw = begin
|
|
90
|
+
YAML.safe_load_file(path, symbolize_names: true)
|
|
91
|
+
rescue Psych::SyntaxError => e
|
|
92
|
+
raise ArgumentError, "#{path} is not valid YAML: #{e.message}"
|
|
93
|
+
end
|
|
94
|
+
raise ArgumentError, "#{path} must be a YAML mapping at the top level, got #{raw.class}" unless raw.is_a?(Hash)
|
|
95
|
+
|
|
96
|
+
new(raw, path)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def initialize(raw, path)
|
|
101
|
+
missing = EXPECTED_TYPES.keys - raw.keys
|
|
102
|
+
raise ArgumentError, "#{path} is missing #{missing.sort.join(', ')}" if missing.any?
|
|
103
|
+
|
|
104
|
+
extra = raw.keys - EXPECTED_TYPES.keys
|
|
105
|
+
if extra.any?
|
|
106
|
+
raise ArgumentError,
|
|
107
|
+
"#{path} declares unknown key(s) #{extra.sort.join(', ')} — " \
|
|
108
|
+
"Hecks::Fuzzing::QaSettings::EXPECTED_TYPES doesn't recognise them"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
EXPECTED_TYPES.each do |key, expected|
|
|
112
|
+
value = raw.fetch(key)
|
|
113
|
+
expected_classes = Array(expected)
|
|
114
|
+
unless expected_classes.any? { |klass| value.is_a?(klass) }
|
|
115
|
+
raise ArgumentError,
|
|
116
|
+
"#{path}: #{key} must be a #{expected_classes.map(&:name).join(' or ')}, " \
|
|
117
|
+
"got #{value.class} (#{value.inspect})"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
instance_variable_set(:"@#{key}", value)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
symbolize_adapter_parity_pairs!(path)
|
|
124
|
+
freeze_values!
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
# `left:`/`right:` name adapters `IsolatedBoot` case-matches by
|
|
130
|
+
# symbol (`case adapter when :memory ...`), and YAML has no way to
|
|
131
|
+
# spell a bare Ruby Symbol as a mapping value — only
|
|
132
|
+
# `symbolize_names:` turns a key into one. So `adapter_parity_
|
|
133
|
+
# pairs` is the one dial that needs a coercion step after the
|
|
134
|
+
# type check above, rather than every dial growing one.
|
|
135
|
+
def symbolize_adapter_parity_pairs!(path)
|
|
136
|
+
@adapter_parity_pairs = @adapter_parity_pairs.to_h do |mode, pair|
|
|
137
|
+
unless pair.is_a?(Hash) && pair.key?(:left) && pair.key?(:right)
|
|
138
|
+
raise ArgumentError,
|
|
139
|
+
"#{path}: adapter_parity_pairs.#{mode} must have both left and right, got #{pair.inspect}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
[mode, { left: pair[:left].to_sym, right: pair[:right].to_sym }]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def freeze_values!
|
|
147
|
+
EXPECTED_TYPES.each_key { |key| instance_variable_get(:"@#{key}").freeze }
|
|
148
|
+
freeze
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|