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
|
@@ -4,30 +4,30 @@ require "securerandom"
|
|
|
4
4
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Fuzzing
|
|
7
|
-
# A
|
|
7
|
+
# A fresh, in-process adapter for every ephemeral boot.
|
|
8
8
|
#
|
|
9
9
|
# Fuzzing/replay copies a domain to a tmpdir and boots from there
|
|
10
10
|
# specifically to get zero-history state — `rm_rf`ing the copy's own
|
|
11
11
|
# `data/` achieves that for a file-based adapter (Memory,
|
|
12
|
-
# SqlitePersistence, Heki), because copying the
|
|
12
|
+
# SqlitePersistence, Heki), because copying the directory copies the
|
|
13
13
|
# store. It achieves nothing for an adapter that lives outside the
|
|
14
14
|
# copied directory entirely — Postgres, named by a fixed connection
|
|
15
15
|
# string in `.world` (examples/pizzas/bluebook/pizzas.world, for
|
|
16
16
|
# instance). Copying the directory does not copy or isolate the
|
|
17
|
-
#
|
|
17
|
+
# database, so a "fresh" boot against a Postgres-bound domain would
|
|
18
18
|
# still see every record any other run, ever, wrote to it.
|
|
19
19
|
#
|
|
20
20
|
# So every `.hecksagon` in the copy gets its persistence binding
|
|
21
21
|
# rewritten to Memory before booting, and any `projected_by` bind
|
|
22
22
|
# dropped outright (optional — `Registry#read_repository` already
|
|
23
23
|
# falls back to the authoritative repository when none exists). The
|
|
24
|
-
# domain's own rules and shape are untouched ; only
|
|
24
|
+
# domain's own rules and shape are untouched ; only which adapter this
|
|
25
25
|
# one ephemeral copy answers through changes. What the domain is
|
|
26
26
|
# bound to for real deployment is never touched — only this tmp copy.
|
|
27
27
|
#
|
|
28
28
|
# `adapter:` (PRD 02) — Memory is the default and the only mode every
|
|
29
29
|
# existing caller still gets with no change. `:sqlite` rebinds to the
|
|
30
|
-
#
|
|
30
|
+
# real SQLite adapter instead of the in-memory one, for exactly the
|
|
31
31
|
# same reason PRD 02 exists: 15 declared properties (properties.rb)
|
|
32
32
|
# and every fuzz/replay run has only ever been checked against
|
|
33
33
|
# Memory's own hand-written repository, never against a real,
|
|
@@ -44,15 +44,25 @@ module Hecks
|
|
|
44
44
|
# `root:`, which `Hecks.boot(copy)` passes as this ephemeral copy's
|
|
45
45
|
# own directory — a fresh, empty `data/` per run, exactly like
|
|
46
46
|
# Memory's own zero-history guarantee, just backed by a real SQLite
|
|
47
|
-
# file instead of a Hash.
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
47
|
+
# file instead of a Hash. Stale as of this paragraph's original
|
|
48
|
+
# writing — both `Postgres` (PRD 02, docs/prds/02-fuzzer-real-
|
|
49
|
+
# adapters.md) and `PostgresEra` (this mode's own header, below,
|
|
50
|
+
# `rebind_to_postgres_era!`) since gained real `adapter:` modes here.
|
|
51
|
+
# Each writes its own fresh `.world` per `.hecksagon` rather than
|
|
52
|
+
# relying on the zero-config default Sqlite/Memory get, and each
|
|
53
|
+
# needs a real, reachable Postgres server — but "no place to source a
|
|
54
|
+
# connection safely" turned out not to be true: `:postgres` sources
|
|
55
|
+
# one shared, permanent scratch database/schema this module itself
|
|
56
|
+
# owns (see `FUZZ_POSTGRES_DATABASE`'s own header); `:postgres_era`
|
|
57
|
+
# instead requires the caller to supply (and own the lifecycle of) its
|
|
58
|
+
# own throwaway `database:`/`schema:`, since its only caller
|
|
59
|
+
# (`bin/qa_sweep --persistence-parity`) already has to manage a
|
|
60
|
+
# disposable database of its own, never a shared one this module could
|
|
61
|
+
# safely default to.
|
|
52
62
|
module IsolatedBoot
|
|
53
63
|
module_function
|
|
54
64
|
|
|
55
|
-
# PRD 02 (docs/future-features.md) — `adapter:` picks
|
|
65
|
+
# PRD 02 (docs/future-features.md) — `adapter:` picks which real
|
|
56
66
|
# persistence this one ephemeral boot answers through, not just
|
|
57
67
|
# Memory. `:memory` is the original, zero-config behavior (every
|
|
58
68
|
# other caller in this codebase that doesn't pass `adapter:` gets
|
|
@@ -65,23 +75,33 @@ module Hecks
|
|
|
65
75
|
# so `bin/fuzz --adapter postgres` is meant to run with smaller
|
|
66
76
|
# seed/step counts than the Memory default, not as a like-for-like
|
|
67
77
|
# swap; see that flag's own comment.
|
|
68
|
-
|
|
78
|
+
# `database:`/`schema:` are only meaningful for `adapter: :postgres_era`
|
|
79
|
+
# — see `rebind_to_postgres_era!`'s own header for why that mode takes
|
|
80
|
+
# caller-supplied connection identity instead of a hardcoded shared
|
|
81
|
+
# constant the way `:postgres` does. Every other adapter ignores both;
|
|
82
|
+
# accepting them unconditionally here (rather than a separate method
|
|
83
|
+
# signature per adapter) keeps `SequenceGenerator`/`Replay`'s own
|
|
84
|
+
# single passthrough (`adapter:`, now joined by these two) uniform
|
|
85
|
+
# across all four modes.
|
|
86
|
+
def call(domain_path, adapter: :memory, database: nil, schema: nil)
|
|
69
87
|
Dir.mktmpdir("hecks-fuzz") do |tmp|
|
|
70
88
|
copy = File.join(tmp, File.basename(domain_path))
|
|
71
89
|
copy_dereferencing(domain_path, copy)
|
|
72
90
|
FileUtils.rm_rf(File.join(copy, "data"))
|
|
73
91
|
case adapter
|
|
74
|
-
when :memory
|
|
75
|
-
when :sqlite
|
|
76
|
-
when :postgres
|
|
77
|
-
|
|
92
|
+
when :memory then rebind_to_memory!(copy)
|
|
93
|
+
when :sqlite then rebind_to_sqlite!(copy)
|
|
94
|
+
when :postgres then rebind_to_postgres!(copy)
|
|
95
|
+
when :postgres_era then rebind_to_postgres_era!(copy, database: database, schema: schema)
|
|
96
|
+
else raise ArgumentError,
|
|
97
|
+
"unknown fuzz adapter #{adapter.inspect} — :memory, :sqlite, :postgres, or :postgres_era"
|
|
78
98
|
end
|
|
79
99
|
yield copy
|
|
80
100
|
end
|
|
81
101
|
end
|
|
82
102
|
|
|
83
|
-
#
|
|
84
|
-
# symlink
|
|
103
|
+
# Symlinks are followed, not copied. `FileUtils.cp_r` reproduces a
|
|
104
|
+
# symlink as a symlink, and a relative one then points at nothing
|
|
85
105
|
# from a tmpdir — `lib/hecks/framework/bluebook/compliance
|
|
86
106
|
# .bluebook` is exactly that, a link to
|
|
87
107
|
# `examples/compliance/bluebook/compliance.bluebook`, so the whole
|
|
@@ -89,7 +109,7 @@ module Hecks
|
|
|
89
109
|
# path under /var/folders that had never existed. Nothing about the
|
|
90
110
|
# domain was wrong; the copy was.
|
|
91
111
|
#
|
|
92
|
-
# `FileUtils.cp` follows a symlink and copies its
|
|
112
|
+
# `FileUtils.cp` follows a symlink and copies its content, which is
|
|
93
113
|
# what an isolated boot wants: the copy has to stand alone, since
|
|
94
114
|
# rebind! rewrites files in it and must not reach back through a
|
|
95
115
|
# link into the real tree.
|
|
@@ -118,9 +138,10 @@ module Hecks
|
|
|
118
138
|
|
|
119
139
|
def rebind_to_memory!(copy)
|
|
120
140
|
rewrite_bindings!(copy, "Memory")
|
|
141
|
+
strip_translations!(copy)
|
|
121
142
|
|
|
122
|
-
#
|
|
123
|
-
# stores a settings block under
|
|
143
|
+
# **The settings, not just the bind** — `WorldBuilder#method_missing`
|
|
144
|
+
# stores a settings block under both "verb:adapter" and the bare
|
|
124
145
|
# "verb" (world_builder.rb:32-33), so a bind rewritten to Memory
|
|
125
146
|
# still falls back to whatever adapter's settings were declared
|
|
126
147
|
# bare — Postgres's `database:`, which Memory does not take and
|
|
@@ -133,7 +154,7 @@ module Hecks
|
|
|
133
154
|
Dir.glob(File.join(copy, "**", "*.world")).each { |path| File.delete(path) }
|
|
134
155
|
end
|
|
135
156
|
|
|
136
|
-
#
|
|
157
|
+
# Same dance as memory, one adapter over — `Adapters::Sqlite#
|
|
137
158
|
# resolve_path` (adapters/driven/sqlite.rb) defaults to
|
|
138
159
|
# `data/<table>.db` under the boot's own root when no `database`
|
|
139
160
|
# setting is declared, which `data/` already being cleared makes a
|
|
@@ -143,27 +164,28 @@ module Hecks
|
|
|
143
164
|
# nothing here creates it up front.
|
|
144
165
|
def rebind_to_sqlite!(copy)
|
|
145
166
|
rewrite_bindings!(copy, "SqlitePersistence")
|
|
167
|
+
strip_translations!(copy)
|
|
146
168
|
Dir.glob(File.join(copy, "**", "*.world")).each { |path| File.delete(path) }
|
|
147
169
|
end
|
|
148
170
|
|
|
149
|
-
#
|
|
171
|
+
# **The expensive one** — Postgres has no zero-config default the way
|
|
150
172
|
# Sqlite/Memory do (`Adapters::Postgres.connect_for` refuses outright
|
|
151
173
|
# with no `database` setting), so dropping `.world` the way the
|
|
152
174
|
# other two do would just move the WiringError from "wrong adapter"
|
|
153
175
|
# to "no adapter." Every domain name this copy declares gets a
|
|
154
|
-
#
|
|
176
|
+
# fresh `.world` written for it instead — not a rewrite of whatever
|
|
155
177
|
# was there, a replacement, same reasoning `rewrite_bindings!`
|
|
156
178
|
# already applies to `.hecksagon`: this ephemeral boot owns every
|
|
157
179
|
# binding decision, nothing about a real deployment's own settings
|
|
158
180
|
# is relevant or safe to half-preserve here.
|
|
159
181
|
#
|
|
160
|
-
#
|
|
182
|
+
# One shared schema, dropped and recreated before every boot — not a
|
|
161
183
|
# fresh randomly-named one per call. `bin/fuzz` drives every
|
|
162
184
|
# ephemeral boot sequentially (one `IsolatedBoot.call` fully exits
|
|
163
185
|
# before the next begins — see that file's own single-threaded
|
|
164
186
|
# `while` loop), so nothing is ever concurrent here ; a fresh name
|
|
165
187
|
# every time would just leak schemas in `FUZZ_POSTGRES_DATABASE`
|
|
166
|
-
# forever with nothing to ever drop them. If a caller ever
|
|
188
|
+
# forever with nothing to ever drop them. If a caller ever does
|
|
167
189
|
# start running fuzz adapters concurrently, this needs to move to a
|
|
168
190
|
# process-unique schema name (`SecureRandom.hex` is already
|
|
169
191
|
# `require`d here for exactly that day) — flagged, not solved,
|
|
@@ -174,13 +196,14 @@ module Hecks
|
|
|
174
196
|
def rebind_to_postgres!(copy)
|
|
175
197
|
require "pg"
|
|
176
198
|
rewrite_bindings!(copy, "Postgres")
|
|
199
|
+
strip_translations!(copy)
|
|
177
200
|
ensure_fuzz_schema!
|
|
178
201
|
|
|
179
|
-
#
|
|
202
|
+
# One `.world` per directory a `.hecksagon` actually lives in, not
|
|
180
203
|
# one at `copy`'s own root — `Folder#load_domain` resolves a
|
|
181
|
-
#
|
|
204
|
+
# single `bluebook_directory` and globs `*.world` there, non-
|
|
182
205
|
# recursively (`Folder#load_each`); a domain can hold several
|
|
183
|
-
# `Hecks.hecksagon "<Name>" do ... end`
|
|
206
|
+
# `Hecks.hecksagon "<Name>" do ... end` sibling blocks in that one
|
|
184
207
|
# file (banking.hecksagon declares "Banking", "Governance", and
|
|
185
208
|
# "Identity" together), so every name found in one `.hecksagon`
|
|
186
209
|
# file gets bundled into one `.world` written beside it, not
|
|
@@ -202,7 +225,7 @@ module Hecks
|
|
|
202
225
|
end.join("\n"))
|
|
203
226
|
end
|
|
204
227
|
|
|
205
|
-
# Any
|
|
228
|
+
# Any pre-existing `.world` this copy shipped with (a real
|
|
206
229
|
# deployment's own connection string) is now redundant with — and
|
|
207
230
|
# would conflict with, `Registry#add_world`'s own header on
|
|
208
231
|
# loading the same domain name twice — the fresh one just
|
|
@@ -213,20 +236,20 @@ module Hecks
|
|
|
213
236
|
end
|
|
214
237
|
end
|
|
215
238
|
|
|
216
|
-
#
|
|
239
|
+
# Admin connection lives outside the tmp copy entirely — same as
|
|
217
240
|
# every other real-Postgres spec in this repo (`support/
|
|
218
241
|
# postgres_probe.rb`'s own header). Database created once per
|
|
219
242
|
# process and remembered (`@fuzz_database_ready` on this module's
|
|
220
243
|
# own singleton, the same memoization shape `PostgresProbe
|
|
221
244
|
# .available?` already uses) ; the schema inside it is dropped and
|
|
222
|
-
# recreated on
|
|
245
|
+
# recreated on every call, which is what actually isolates one
|
|
223
246
|
# ephemeral boot's data from the next.
|
|
224
247
|
def ensure_fuzz_schema!
|
|
225
|
-
# `Adapters::Postgres#initialize` opens
|
|
226
|
-
#
|
|
248
|
+
# `Adapters::Postgres#initialize` opens one real `PG::Connection`
|
|
249
|
+
# per aggregate and never explicitly closes it — fine for a
|
|
227
250
|
# process that boots once and runs, exactly what every other
|
|
228
251
|
# caller of this adapter is. A fuzz run boots dozens to hundreds
|
|
229
|
-
# of
|
|
252
|
+
# of ephemeral times in one process (every seed does at least a
|
|
230
253
|
# generate + a replay, `replay_is_deterministic` doubles that,
|
|
231
254
|
# shrinking multiplies it further), and `PG::Connection` only
|
|
232
255
|
# actually closes its socket when Ruby's GC finalizes the
|
|
@@ -238,7 +261,7 @@ module Hecks
|
|
|
238
261
|
# ephemeral boots, `PG::ConnectionBad: ... "too many clients
|
|
239
262
|
# already"`. A `GC.start` here — right before the next ephemeral
|
|
240
263
|
# boot's connections open, not on some timer — reclaims every
|
|
241
|
-
# connection the
|
|
264
|
+
# connection the previous boot's now-unreferenced adapters held,
|
|
242
265
|
# keeping the live count bounded regardless of run length. This
|
|
243
266
|
# is a real constraint on running Postgres in a loop, not
|
|
244
267
|
# something to route around by connecting less carefully.
|
|
@@ -255,7 +278,7 @@ module Hecks
|
|
|
255
278
|
end
|
|
256
279
|
|
|
257
280
|
db = PG.connect(dbname: FUZZ_POSTGRES_DATABASE)
|
|
258
|
-
#
|
|
281
|
+
# **Quiet on purpose** — same as `Adapters::Postgres.connect_for`'s
|
|
259
282
|
# own `SET client_min_messages`: a `DROP SCHEMA ... CASCADE` that
|
|
260
283
|
# actually has something to drop (every boot after the first)
|
|
261
284
|
# NOTICEs once per dropped object, which is the ordinary case
|
|
@@ -269,7 +292,132 @@ module Hecks
|
|
|
269
292
|
db.close
|
|
270
293
|
end
|
|
271
294
|
|
|
272
|
-
#
|
|
295
|
+
# The adapter `:postgres` never touches — `Postgres` and `PostgresEra`
|
|
296
|
+
# are sibling, not interchangeable, adapters (see postgres_era.rb's
|
|
297
|
+
# own header: "the only one that declares the LINEAGE capability").
|
|
298
|
+
# PRD 02 (docs/prds/02-fuzzer-real-adapters.md) shipped `:postgres`
|
|
299
|
+
# and explicitly scoped `PostgresEra` out: "nothing here touches
|
|
300
|
+
# era/lineage machinery." That gap is real, not cosmetic —
|
|
301
|
+
# `examples/directory` (a `compute`/`rekey` translation edge, the
|
|
302
|
+
# one domain in this corpus that actually exercises PostgresEra-
|
|
303
|
+
# bound SQL compilation) had to be shelved out of `hecks_qa`'s own
|
|
304
|
+
# rotation for exactly this reason: every existing fuzz/replay path,
|
|
305
|
+
# `:memory` included, structurally cannot reach it. This mode closes
|
|
306
|
+
# that — `bin/qa_sweep --persistence-parity` is its first caller.
|
|
307
|
+
#
|
|
308
|
+
# No shared constant database, unlike `:postgres` above — deliberate.
|
|
309
|
+
# `rebind_to_postgres!`'s own `FUZZ_POSTGRES_DATABASE`/`_SCHEMA` are
|
|
310
|
+
# module-level constants because `bin/fuzz --adapter postgres` is a
|
|
311
|
+
# general-purpose, run-it-anytime tool with no caller-tracked
|
|
312
|
+
# lifecycle of its own. This mode's only caller
|
|
313
|
+
# (`bin/qa_sweep --persistence-parity`) is different: it dispatches
|
|
314
|
+
# through `QualityControl::Target.claim!`'s own cross-process lock
|
|
315
|
+
# first (see that script's own header), so at most one sweep is ever
|
|
316
|
+
# touching a given target's own disposable database at a time — but
|
|
317
|
+
# the caller, not this module, is what owns that database's name and
|
|
318
|
+
# lifecycle (created, and genuinely dropped, by the caller itself),
|
|
319
|
+
# exactly the discipline `spec/support/qa_sweep_all_fixture.rb`'s own header
|
|
320
|
+
# describes and this repository's persistence-parity work is
|
|
321
|
+
# required to follow. Accepting `database:`/`schema:` as required
|
|
322
|
+
# keyword arguments (never a fallback constant) is what keeps that
|
|
323
|
+
# ownership from silently drifting back onto this module the way
|
|
324
|
+
# `:postgres`'s own `FUZZ_POSTGRES_DATABASE` already has.
|
|
325
|
+
def rebind_to_postgres_era!(copy, database:, schema:)
|
|
326
|
+
require "pg"
|
|
327
|
+
if database.to_s.empty? || schema.to_s.empty?
|
|
328
|
+
raise ArgumentError,
|
|
329
|
+
"adapter: :postgres_era requires both database: and schema: — a throwaway database/schema " \
|
|
330
|
+
"THIS CALLER creates and drops itself (see rebind_to_postgres_era!'s own header). " \
|
|
331
|
+
"There is no shared default, unlike :postgres, so the caller cannot forget to own the lifecycle."
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
rewrite_bindings!(copy, "PostgresEra")
|
|
335
|
+
ensure_postgres_era_schema!(database: database, schema: schema)
|
|
336
|
+
|
|
337
|
+
# Same one-`.world`-per-`.hecksagon`-directory shape `rebind_to_
|
|
338
|
+
# postgres!` already uses, for the identical reason (`Folder#
|
|
339
|
+
# load_domain` globs `*.world` non-recursively) — see that
|
|
340
|
+
# method's own comment on `world_path` above. `PostgresEra`
|
|
341
|
+
# additionally takes `schema:` (postgres_era.rb's own "shared-
|
|
342
|
+
# instance isolation" comment): the caller-supplied throwaway
|
|
343
|
+
# schema is what actually isolates this one ephemeral boot from
|
|
344
|
+
# the next, the same job `FUZZ_POSTGRES_SCHEMA` does for `:postgres`
|
|
345
|
+
# — `connect_for` itself idempotently `CREATE SCHEMA IF NOT
|
|
346
|
+
# EXISTS`s it, so this method only ever needs to drop it first
|
|
347
|
+
# (in `ensure_postgres_era_schema!`, below) for the zero-history
|
|
348
|
+
# guarantee every other adapter mode already gives.
|
|
349
|
+
#
|
|
350
|
+
# `allow_superuser true` — on the record, on purpose. A bare
|
|
351
|
+
# `database` connects as the ambient Postgres user, and
|
|
352
|
+
# PostgresEra refuses to boot at all when that user is a
|
|
353
|
+
# superuser (its era write-fence is row-level security, which a
|
|
354
|
+
# superuser walks through — `Lineage#check_fence_applies!`,
|
|
355
|
+
# BUG#24). That refusal protects a real ledger from an old
|
|
356
|
+
# checkout's stale writes; nothing here is one. This is an
|
|
357
|
+
# ephemeral boot into a throwaway schema the caller itself
|
|
358
|
+
# creates and drops, whose data no second checkout ever shares,
|
|
359
|
+
# and what it compares is Memory's answers against PostgresEra's
|
|
360
|
+
# own SQL — the era fence is not under test and cannot be
|
|
361
|
+
# crossed. So opt in explicitly rather than make every
|
|
362
|
+
# persistence-parity run first provision a fenced role for a
|
|
363
|
+
# database it is about to throw away; the one-line warning
|
|
364
|
+
# PostgresEra prints per boot under the opt-in is the honest
|
|
365
|
+
# price. Inert on a machine whose ambient user is ordinary.
|
|
366
|
+
Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |hecksagon_path|
|
|
367
|
+
names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten.uniq
|
|
368
|
+
next if names.empty?
|
|
369
|
+
|
|
370
|
+
world_path = File.join(File.dirname(hecksagon_path), "hecks_fuzz_postgres_era.world")
|
|
371
|
+
File.write(world_path, names.map do |name|
|
|
372
|
+
<<~WORLD
|
|
373
|
+
Hecks.world "#{name}" do
|
|
374
|
+
persisted_by("PostgresEra") do
|
|
375
|
+
database "#{database}"
|
|
376
|
+
schema "#{schema}"
|
|
377
|
+
allow_superuser true
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
WORLD
|
|
381
|
+
end.join("\n"))
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
Dir.glob(File.join(copy, "**", "*.world")).each do |path|
|
|
385
|
+
File.delete(path) unless File.basename(path) == "hecks_fuzz_postgres_era.world"
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# The zero-history guarantee for this mode — `DROP SCHEMA ... CASCADE`
|
|
390
|
+
# before every ephemeral boot, mirroring `ensure_fuzz_schema!` above
|
|
391
|
+
# (same `GC.start`-before-connecting fix for the identical
|
|
392
|
+
# `max_connections` exhaustion that method's own comment documents —
|
|
393
|
+
# `PostgresEra` opens real `PG::Connection`s exactly like `Postgres`
|
|
394
|
+
# does, same unclosed-until-GC'd lifetime). The database itself is
|
|
395
|
+
# created here too, idempotently (`CREATE DATABASE IF NOT EXISTS`
|
|
396
|
+
# has no Postgres spelling, hence the existence check) — but never
|
|
397
|
+
# dropped here: this module creates it once per process because
|
|
398
|
+
# `Hecks.boot` needs it to exist before `PostgresEra.connect_for`'s
|
|
399
|
+
# own `PG.connect(dbname: ...)` can succeed at all, but dropping it
|
|
400
|
+
# again is the caller's own job (its name and lifecycle belong to
|
|
401
|
+
# the caller — see `rebind_to_postgres_era!`'s own header), not
|
|
402
|
+
# something this per-ephemeral-boot helper should ever do mid-sweep.
|
|
403
|
+
def ensure_postgres_era_schema!(database:, schema:)
|
|
404
|
+
GC.start
|
|
405
|
+
|
|
406
|
+
admin = PG.connect(dbname: "postgres")
|
|
407
|
+
exists = admin.exec_params(
|
|
408
|
+
"SELECT 1 FROM pg_database WHERE datname = $1", [database]
|
|
409
|
+
).ntuples.positive?
|
|
410
|
+
admin.exec(%(CREATE DATABASE "#{database}")) unless exists
|
|
411
|
+
admin.close
|
|
412
|
+
|
|
413
|
+
db = PG.connect(dbname: database)
|
|
414
|
+
db.exec("SET client_min_messages = warning")
|
|
415
|
+
quoted = db.quote_ident(schema)
|
|
416
|
+
db.exec("DROP SCHEMA IF EXISTS #{quoted} CASCADE")
|
|
417
|
+
db.close
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# The shared rewrite — factored out of `rebind_to_memory!` when
|
|
273
421
|
# Sqlite/Postgres modes needed the identical `.hecksagon` surgery
|
|
274
422
|
# with only the target adapter name differing. `persisted_by`/
|
|
275
423
|
# `projected_by` can be spelled two ways: aggregate-scoped
|
|
@@ -283,6 +431,46 @@ module Hecks
|
|
|
283
431
|
# #read_repository` already falls back to the authoritative
|
|
284
432
|
# repository when none exists, so a read model this ephemeral copy
|
|
285
433
|
# never wires is simply unread, not broken.
|
|
434
|
+
# A `compute`/`rekey` translation edge refuses to boot at all under
|
|
435
|
+
# any non-lineage-capable adapter — found live, wiring this very
|
|
436
|
+
# mode up against `examples/directory`: `Runtime::EraCheck
|
|
437
|
+
# .check_compute_rules!` (era_check.rb) runs unconditionally for
|
|
438
|
+
# every loaded bluebook once the era plugin is loaded at all
|
|
439
|
+
# (`bin/qa_sweep`'s own top-of-file `require "hecks/ports/
|
|
440
|
+
# persistence/plugins/era"`, needed for the ledger's own
|
|
441
|
+
# PostgresEra-bound aggregates), and refuses outright — "compute
|
|
442
|
+
# rules require the Postgres adapter" — for any aggregate whose
|
|
443
|
+
# lineage carries a `compute` rule and whose bound adapter is not
|
|
444
|
+
# lineage-capable. `PostgresEra` is the only adapter that answers
|
|
445
|
+
# `lineage_capable? == true` (postgres_era.rb's own `self.
|
|
446
|
+
# lineage_capable? = true`) — plain `Postgres` does not, so this
|
|
447
|
+
# refusal was already real for `:postgres`/`:sqlite`/`:memory`
|
|
448
|
+
# alike, for any domain with a translation edge, before this
|
|
449
|
+
# mode's own `:postgres_era` ever existed. This is very likely the
|
|
450
|
+
# mechanical reason `examples/directory` had to be shelved out of
|
|
451
|
+
# `hecks_qa`'s own rotation in the first place — not merely "less
|
|
452
|
+
# interesting to fuzz on Memory," but "cannot boot on Memory at
|
|
453
|
+
# all" once the era plugin is loaded, which every real
|
|
454
|
+
# `bin/qa_sweep` invocation already does.
|
|
455
|
+
#
|
|
456
|
+
# The fix is to drop the edge, not to chase the refusal — an
|
|
457
|
+
# ephemeral, zero-history replay boot (every mode `IsolatedBoot`
|
|
458
|
+
# offers) never has a pre-existing era-1 row to translate in the
|
|
459
|
+
# first place, so the translation edge is irrelevant to anything a
|
|
460
|
+
# fuzz/replay run actually exercises (ordinary command dispatch
|
|
461
|
+
# against a fresh boot) — it only ever matters at mint time,
|
|
462
|
+
# against a real, pre-existing database
|
|
463
|
+
# (`PostgresEra::LineageManager.check!`, a wholly separate,
|
|
464
|
+
# human-approved path this harness was never meant to reach).
|
|
465
|
+
# Dropping it here is exactly the same move `rebind_to_memory!`
|
|
466
|
+
# already makes for `.world` (irrelevant/conflicting settings for
|
|
467
|
+
# an ephemeral boot, deleted outright) — never called for
|
|
468
|
+
# `:postgres_era` itself, where the bound adapter genuinely is
|
|
469
|
+
# lineage-capable and the edge causes no refusal to begin with.
|
|
470
|
+
def strip_translations!(copy)
|
|
471
|
+
Dir.glob(File.join(copy, "**", "translations", "*.bluebook")).each { |path| File.delete(path) }
|
|
472
|
+
end
|
|
473
|
+
|
|
286
474
|
def rewrite_bindings!(copy, adapter_name)
|
|
287
475
|
Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |path|
|
|
288
476
|
lines = File.readlines(path).grep_v(/\bprojected_by\s*\(?\s*"/)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
# The one declared set of FIELDS that leave a comparison — partition,
|
|
4
|
+
# not filter. Every comparison in lib/hecks/fuzzing that drops a field
|
|
5
|
+
# before comparing two histories names a group here instead of writing
|
|
6
|
+
# its own literal `except(...)` list, so the reason for dropping a field
|
|
7
|
+
# is written exactly once, beside the field.
|
|
8
|
+
#
|
|
9
|
+
# Two specs hold this set honest (spec/fuzzing/nondeterministic_spec.rb):
|
|
10
|
+
# a literal except-list naming any of these fields anywhere under
|
|
11
|
+
# lib/hecks/fuzzing fails the build (the tolerance must be declared
|
|
12
|
+
# here, not re-derived at a call site), and every declared field must
|
|
13
|
+
# actually be produced where its group says — a field nothing emits any
|
|
14
|
+
# more is a stale tolerance and fails too.
|
|
15
|
+
#
|
|
16
|
+
# Groups are keyed by the shape the field rides on, since the same name
|
|
17
|
+
# can be compared on one surface and dropped on another:
|
|
18
|
+
#
|
|
19
|
+
# - `query_row` — one entry of `Replay.call`'s `:queries`.
|
|
20
|
+
# - `outbox_row` — one row of an `:outbox_traces` entry's `:rows`.
|
|
21
|
+
# - `event` — an event hash in full `Event#to_h` form (an outbox
|
|
22
|
+
# row's `:event`, or a Rust binary's `events` entries,
|
|
23
|
+
# string-keyed there).
|
|
24
|
+
# - `history` — the top-level `Replay.call` history hash itself.
|
|
25
|
+
module Nondeterministic
|
|
26
|
+
FIELDS = {
|
|
27
|
+
query_row: {
|
|
28
|
+
instances_at: "a full state snapshot taken for the query oracle's own use " \
|
|
29
|
+
"(Properties::Querying); the same state is already compared as `instances`, " \
|
|
30
|
+
"so keeping it would re-report one instances divergence under every query step"
|
|
31
|
+
}.freeze,
|
|
32
|
+
outbox_row: {
|
|
33
|
+
event_uid: "`Runtime::Outbox::Fanout#rows_for`'s own SecureRandom.uuid, minted fresh per " \
|
|
34
|
+
"enqueue and kept off `Event#to_h` — relay bookkeeping, never something the " \
|
|
35
|
+
"domain produced, so two otherwise-identical replays carry two different uuids",
|
|
36
|
+
delivery_id: "`\"\#{event_uid}/\#{consumer}\"` — inherits event_uid's per-enqueue uuid"
|
|
37
|
+
}.freeze,
|
|
38
|
+
event: {
|
|
39
|
+
occurred_at: "a wall-clock read; never reproducible byte-for-byte between two independent " \
|
|
40
|
+
"runs (Replay's own projected `:events` leave it off for the same reason)"
|
|
41
|
+
}.freeze,
|
|
42
|
+
history: {
|
|
43
|
+
bluebook: "a live IR object (the first-loaded bluebook) handed to properties — an object " \
|
|
44
|
+
"identity, not a value, so two boots never compare equal on it",
|
|
45
|
+
bluebooks: "the full live IR map, keyed by domain — object identities, same as `bluebook`"
|
|
46
|
+
}.freeze
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
module_function
|
|
50
|
+
|
|
51
|
+
def names(group)
|
|
52
|
+
FIELDS.fetch(group).keys
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Every declared name, across every group.
|
|
56
|
+
def all_names
|
|
57
|
+
FIELDS.values.flat_map(&:keys).uniq
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# `hash` without `group`'s fields — symbol keys, the shape every
|
|
61
|
+
# Ruby-side history carries.
|
|
62
|
+
def strip(hash, group)
|
|
63
|
+
hash.except(*names(group))
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "replay"
|
|
3
|
+
require_relative "nondeterministic"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Fuzzing
|
|
7
|
+
# **The second differential axis** — bin/qa_sweep's own `diff_ruby_vs_rust`
|
|
8
|
+
# compares two different engines (the Ruby interpreter vs the compiled
|
|
9
|
+
# Rust kernel) against the same persistence (Memory, always — see
|
|
10
|
+
# `SequenceGenerator`'s own header: sequence generation stays
|
|
11
|
+
# Memory-only, and `IsolatedBoot`'s own header explains why every
|
|
12
|
+
# existing fuzz/replay path structurally cannot reach a real Postgres-
|
|
13
|
+
# bound domain's own SQL compilation). This module compares the
|
|
14
|
+
# opposite pairing: the same one Ruby engine, against two different
|
|
15
|
+
# persistences — Memory (the reference, exactly as fast and as
|
|
16
|
+
# deterministic as every other fuzz check) and a real, disposable
|
|
17
|
+
# `PostgresEra` database (the actual SQL compute/rekey/query-pushdown
|
|
18
|
+
# path a Postgres-bound domain's users really hit).
|
|
19
|
+
#
|
|
20
|
+
# `examples/directory` is why this exists at all: the one domain in
|
|
21
|
+
# this corpus with a real `compute`/`rekey` translation edge
|
|
22
|
+
# (docs/prds/02-fuzzer-real-adapters.md's own "What shipped" section
|
|
23
|
+
# names it directly), which no existing fuzz/replay/property check has
|
|
24
|
+
# ever been able to exercise against real PostgresEra SQL — every one
|
|
25
|
+
# of them boots through `IsolatedBoot`, and until `adapter:
|
|
26
|
+
# :postgres_era` existed (isolated_boot.rb's own header), that meant
|
|
27
|
+
# Memory, unconditionally, no matter what `directory.world` itself
|
|
28
|
+
# declares.
|
|
29
|
+
#
|
|
30
|
+
# Not a replacement for `bin/qa_sweep`'s own Ruby-vs-Rust differential
|
|
31
|
+
# mode — a genuinely separate axis, opt-in (`--persistence-parity`),
|
|
32
|
+
# because this one pays for a real `PG.connect` and real SQL per
|
|
33
|
+
# dispatch where Memory-vs-Rust pays for neither. See `bin/qa_sweep`'s
|
|
34
|
+
# own `--persistence-parity` handling for the seed-count dial that
|
|
35
|
+
# keeps that cost bounded.
|
|
36
|
+
#
|
|
37
|
+
# Generalized to `left:`/`right:` — originally hardcoded to Memory vs
|
|
38
|
+
# PostgresEra (the only pairing that existed), now any two of
|
|
39
|
+
# `IsolatedBoot`'s own adapter symbols (`:memory`, `:sqlite`,
|
|
40
|
+
# `:postgres`, `:postgres_era`). Defaults preserve the original
|
|
41
|
+
# pairing exactly, so every existing caller (this file's own spec,
|
|
42
|
+
# `bin/qa_sweep`'s `--persistence-parity`) is unchanged. The second
|
|
43
|
+
# pairing this generalization exists for is Memory vs SQLite
|
|
44
|
+
# (`QualityControlDials::ADAPTER_PARITY_PAIRS`, `bin/qa_sweep`'s own
|
|
45
|
+
# `adapter_parity_sqlite` mode) — `:sqlite` is nearly as cheap as
|
|
46
|
+
# Memory itself (`IsolatedBoot#rebind_to_sqlite!`'s own header: an
|
|
47
|
+
# on-disk file, no server, no disposable database/schema lifecycle to
|
|
48
|
+
# own), so that pairing folds straight into the ordinary per-seed
|
|
49
|
+
# loop instead of needing a deferred wave of its own the way
|
|
50
|
+
# PostgresEra does.
|
|
51
|
+
#
|
|
52
|
+
# `database:`/`schema:` — required only when `:postgres_era` is one of
|
|
53
|
+
# the two adapters (the caller — today, only `bin/qa_sweep` — owns the
|
|
54
|
+
# disposable database's whole lifecycle: created before the sweep,
|
|
55
|
+
# dropped after — see that script's own comment, and the discipline
|
|
56
|
+
# `IsolatedBoot#rebind_to_postgres_era!` itself refuses to let a
|
|
57
|
+
# caller skip); every other adapter ignores both, same as `Replay.
|
|
58
|
+
# call`/`IsolatedBoot.call` already do for their own `adapter:`.
|
|
59
|
+
module PersistenceParity
|
|
60
|
+
module_function
|
|
61
|
+
|
|
62
|
+
# The same six fields `bin/qa_sweep`'s own `diff_ruby_vs_rust`
|
|
63
|
+
# compares (its own comment: "instances, events, refusals, queries,
|
|
64
|
+
# sagas, reactions") — deliberately the identical set, so a report
|
|
65
|
+
# this mode produces reads exactly like the sibling mode's own,
|
|
66
|
+
# differing only in which two things were compared, not in what
|
|
67
|
+
# "found something" means.
|
|
68
|
+
#
|
|
69
|
+
# Simpler normalization than `diff_ruby_vs_rust`, on purpose — that
|
|
70
|
+
# method reduces Rust's own JSON-over-stdout output to "wire
|
|
71
|
+
# precision" and filters known Ruby/Rust structural gaps, because
|
|
72
|
+
# it is comparing two genuinely different engines that are allowed
|
|
73
|
+
# to differ in already-catalogued, understood ways. Both sides here
|
|
74
|
+
# are the same Ruby engine (`Replay.call`, called twice, adapter
|
|
75
|
+
# only) — there is no second engine's own known-gap catalogue to
|
|
76
|
+
# filter against, so any real difference is the finding. Both
|
|
77
|
+
# results still round-trip through `JSON.generate`/`JSON.parse`
|
|
78
|
+
# before comparing, matching `diff_ruby_vs_rust`'s own discipline —
|
|
79
|
+
# not because either side needs a wire-format reduction, but so
|
|
80
|
+
# `Hash#==`/`Array#==` compares plain, JSON-shaped data on both
|
|
81
|
+
# sides identically (a `Runtime::Value`, a `Symbol` key, a `Time`
|
|
82
|
+
# nobody asked for — none of that survives an accidental leak into
|
|
83
|
+
# this comparison unnoticed).
|
|
84
|
+
def diff(domain_path, steps, left: :memory, right: :postgres_era, database: nil, schema: nil)
|
|
85
|
+
left_result = Replay.call(domain_path, steps, adapter: left, database: database, schema: schema)
|
|
86
|
+
right_result = Replay.call(domain_path, steps, adapter: right, database: database, schema: schema)
|
|
87
|
+
|
|
88
|
+
divergences = []
|
|
89
|
+
divergences.concat(diff_instances(left_result, right_result, left, right))
|
|
90
|
+
divergences.concat(diff_events(left_result, right_result, left, right))
|
|
91
|
+
divergences.concat(diff_refusals(left_result, right_result, left, right))
|
|
92
|
+
divergences.concat(diff_queries(left_result, right_result, left, right))
|
|
93
|
+
divergences.concat(diff_sagas(left_result, right_result, left, right))
|
|
94
|
+
divergences.concat(diff_reactions(left_result, right_result, left, right))
|
|
95
|
+
divergences
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def as_json(value) = JSON.parse(JSON.generate(value))
|
|
99
|
+
|
|
100
|
+
def diff_instances(left_result, right_result, left, right)
|
|
101
|
+
l = as_json(left_result[:instances])
|
|
102
|
+
r = as_json(right_result[:instances])
|
|
103
|
+
return [] if l == r
|
|
104
|
+
|
|
105
|
+
[{ field: "instances", left => l, right => r }]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def diff_events(left_result, right_result, left, right)
|
|
109
|
+
l = as_json(left_result[:events])
|
|
110
|
+
r = as_json(right_result[:events])
|
|
111
|
+
return [] if l == r
|
|
112
|
+
|
|
113
|
+
[{ field: "events", left => l, right => r }]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# `verb:`/`kind:` normalized to plain strings the same way
|
|
117
|
+
# `diff_ruby_vs_rust`'s own `ruby_refusals` mapping does — both
|
|
118
|
+
# sides here already answer strings (`Replay#refusal_kind` always
|
|
119
|
+
# returns one), so this is belt-and-suspenders consistency with the
|
|
120
|
+
# sibling mode's own shape, not a real coercion.
|
|
121
|
+
def diff_refusals(left_result, right_result, left, right)
|
|
122
|
+
normalize = lambda do |refusals|
|
|
123
|
+
refusals.map { |r| { "verb" => r[:verb].to_s, "kind" => r[:kind].to_s, "error" => r[:error] } }
|
|
124
|
+
end
|
|
125
|
+
l = normalize.call(left_result[:refusals])
|
|
126
|
+
r = normalize.call(right_result[:refusals])
|
|
127
|
+
return [] if l == r
|
|
128
|
+
|
|
129
|
+
[{ field: "refusals", left => l, right => r }]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# `Nondeterministic`'s `query_row` group dropped from every entry —
|
|
133
|
+
# the same group `Differential.diff` drops, for the reason declared
|
|
134
|
+
# there (already covered by `diff_instances` above).
|
|
135
|
+
def diff_queries(left_result, right_result, left, right)
|
|
136
|
+
strip = ->(rows) { rows.map { |row| Nondeterministic.strip(row, :query_row) } }
|
|
137
|
+
l = as_json(strip.call(left_result[:queries]))
|
|
138
|
+
r = as_json(strip.call(right_result[:queries]))
|
|
139
|
+
return [] if l == r
|
|
140
|
+
|
|
141
|
+
[{ field: "queries", left => l, right => r }]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def diff_sagas(left_result, right_result, left, right)
|
|
145
|
+
l = as_json(left_result[:sagas])
|
|
146
|
+
r = as_json(right_result[:sagas])
|
|
147
|
+
return [] if l == r
|
|
148
|
+
|
|
149
|
+
[{ field: "sagas", left => l, right => r }]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def diff_reactions(left_result, right_result, left, right)
|
|
153
|
+
l = as_json(left_result[:reactions])
|
|
154
|
+
r = as_json(right_result[:reactions])
|
|
155
|
+
return [] if l == r
|
|
156
|
+
|
|
157
|
+
[{ field: "reactions", left => l, right => r }]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|