hecks 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "open3"
|
|
3
|
+
require_relative "form_census"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Fuzzing
|
|
7
|
+
# An agent reads the adversarial corpus and writes the next meeting.
|
|
8
|
+
#
|
|
9
|
+
# `DomainGenerator` forces two `FormCensus::FORMS` onto one aggregate
|
|
10
|
+
# from a seed — cheap, mechanical, and blind to why a combination
|
|
11
|
+
# should break. The bugs the ledger actually logged came from someone
|
|
12
|
+
# reading a stress domain, a bug title, and a runtime file side by
|
|
13
|
+
# side and guessing where the next divergence lives (`corrections`
|
|
14
|
+
# from BUG#30-33, `case_escalation` from ADR 0037 Finding 5 meeting
|
|
15
|
+
# `corrects`). This is that guess, delegated: the mechanical half
|
|
16
|
+
# (what the corpus already puts together, which pairs nothing meets,
|
|
17
|
+
# what the recent bugs were) is computed here; the judgment half is a
|
|
18
|
+
# prompt (`qa/combination_miner/prompt.md`) an agent answers by
|
|
19
|
+
# writing candidate bluebooks, each with a hypothesis. Checking them
|
|
20
|
+
# is not this module's job — `bin/qa_mine_combinations` hands every
|
|
21
|
+
# valid candidate to `bin/qa_generated_domains --source`, the same
|
|
22
|
+
# differential, self-consistency, Rust build and shrinking path a
|
|
23
|
+
# generated domain takes.
|
|
24
|
+
#
|
|
25
|
+
# **Opt-in, never the rotation**. An agent call costs money and minutes
|
|
26
|
+
# and answers differently every time; `bin/qa_tick` never runs it and
|
|
27
|
+
# no `QualityControlDials` entry turns it on. A person runs
|
|
28
|
+
# `bin/qa_mine_combinations` when they want new shapes.
|
|
29
|
+
module CombinationMiner
|
|
30
|
+
PROMPT_TEMPLATE = "qa/combination_miner/prompt.md".freeze
|
|
31
|
+
HYPOTHESIS_FILE = "HYPOTHESIS.md".freeze
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# Every adversarial domain plus every example — the corpus the
|
|
36
|
+
# census is measured over. Promoted generated domains are included:
|
|
37
|
+
# their shapes are already swept too.
|
|
38
|
+
#
|
|
39
|
+
# @param root [String] repository root to search under
|
|
40
|
+
# @return [Array<String>] paths of every stress/example corpus member that
|
|
41
|
+
# declares at least one bluebook file
|
|
42
|
+
def corpus_paths(root)
|
|
43
|
+
Hecks::Corpus.members(:stress, :example, root: root).map(&:path)
|
|
44
|
+
.select { |path| Hecks::Corpus.bluebook_files(path) }
|
|
45
|
+
.sort
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The last `limit` bug commit subjects on this branch — cheap, needs
|
|
49
|
+
# no ledger or Postgres, and names the mechanism in the title by
|
|
50
|
+
# convention (`BUG#n: …`).
|
|
51
|
+
#
|
|
52
|
+
# @param root [String] repository root to run `git log` in
|
|
53
|
+
# @param limit [Integer] maximum number of commit subjects to return
|
|
54
|
+
# @return [Array<String>] matching commit subjects, newest first; empty if
|
|
55
|
+
# `git log` fails
|
|
56
|
+
def recent_bug_titles(root, limit: 60)
|
|
57
|
+
out, status = Open3.capture2("git", "log", "--format=%s", "--grep=BUG#", "-n", limit.to_s, chdir: root)
|
|
58
|
+
status.success? ? out.lines.map(&:strip).reject(&:empty?) : []
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The mechanical half: which form pairs the corpus meets (and by
|
|
62
|
+
# whom), which it never meets, and what could not be measured.
|
|
63
|
+
#
|
|
64
|
+
# @param paths [Array<String>] corpus member paths to census, as returned by
|
|
65
|
+
# `#corpus_paths`
|
|
66
|
+
# @param root [String] repository root, to render `paths` relative to it
|
|
67
|
+
# @param bug_titles [Array<String>] recent bug commit subjects, as returned
|
|
68
|
+
# by `#recent_bug_titles`
|
|
69
|
+
# @return [Hash] `"forms"` (`Array<String>`, every declared form name),
|
|
70
|
+
# `"corpus"` (`Array<String>`, `paths` relative to `root`),
|
|
71
|
+
# `"unmet_pairs"` (`Array<String>`, form pairs no corpus member meets),
|
|
72
|
+
# `"single_carrier_pairs"` (`Array<String>`, pairs only one form name
|
|
73
|
+
# meets), `"skipped"` (`Array<String>`, paths that could not be
|
|
74
|
+
# censused, with the error), `"recent_bugs"` (`bug_titles`), and
|
|
75
|
+
# `"covered"` (`Hash{String => Array<String>}`, pair to every form name
|
|
76
|
+
# that meets it)
|
|
77
|
+
def brief(paths, root:, bug_titles:)
|
|
78
|
+
covered = Hash.new { |hash, key| hash[key] = [] }
|
|
79
|
+
skipped = []
|
|
80
|
+
paths.each do |path|
|
|
81
|
+
FormCensus.covered_pairs(FormCensus.census(path)).each { |pair, names| covered[pair].concat(names) }
|
|
82
|
+
rescue StandardError, ScriptError => e
|
|
83
|
+
skipped << "#{relative(path, root)} (#{e.class}: #{e.message.lines.first&.strip})"
|
|
84
|
+
end
|
|
85
|
+
{ "forms" => FormCensus::FORMS.keys, "corpus" => paths.map { |path| relative(path, root) },
|
|
86
|
+
"unmet_pairs" => (FormCensus.pairs - covered.keys).sort,
|
|
87
|
+
"single_carrier_pairs" => covered.select { |_, names| names.uniq.size == 1 }
|
|
88
|
+
.map { |pair, names| "#{pair} (only #{names.first})" }.sort,
|
|
89
|
+
"skipped" => skipped, "recent_bugs" => bug_titles, "covered" => covered }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Fills `PROMPT_TEMPLATE`'s `{{placeholders}}` with `brief`'s own data, for
|
|
93
|
+
# the agent to answer by writing candidate bluebooks.
|
|
94
|
+
#
|
|
95
|
+
# @param root [String] repository root the prompt template is read from
|
|
96
|
+
# @param brief [Hash] the mechanical brief, as returned by `#brief`
|
|
97
|
+
# @param count [Integer] how many candidates the prompt asks the agent for
|
|
98
|
+
# @param out_dir [String] where the prompt asks the agent to write candidates
|
|
99
|
+
# @return [String] the filled-in prompt text
|
|
100
|
+
# @raise [KeyError] if the template names a placeholder `substitutions` does
|
|
101
|
+
# not provide
|
|
102
|
+
def prompt(root, brief, count:, out_dir:)
|
|
103
|
+
substitutions = {
|
|
104
|
+
"count" => count.to_s, "out_dir" => out_dir, "forms" => brief["forms"].join(", "),
|
|
105
|
+
"corpus" => bulleted(brief["corpus"]), "unmet_pairs" => bulleted(brief["unmet_pairs"]),
|
|
106
|
+
"single_carrier_pairs" => bulleted(brief["single_carrier_pairs"]),
|
|
107
|
+
"recent_bugs" => bulleted(brief["recent_bugs"]), "skipped" => bulleted(brief["skipped"])
|
|
108
|
+
}
|
|
109
|
+
File.read(File.join(root, PROMPT_TEMPLATE)).gsub(/\{\{(\w+)\}\}/) { substitutions.fetch(Regexp.last_match(1)) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The second, narrower ask: these candidates did not boot, here is
|
|
113
|
+
# why, fix them in place. Nothing else in the directory changes.
|
|
114
|
+
#
|
|
115
|
+
# @param failures [Array<Array(Hash, String)>] `[candidate, error]` pairs,
|
|
116
|
+
# `candidate` as returned by `#candidates`, `error` the boot failure message
|
|
117
|
+
# @param out_dir [String] the directory the candidates were written under
|
|
118
|
+
# @return [String] the repair round's own prompt text
|
|
119
|
+
def repair_prompt(failures, out_dir:)
|
|
120
|
+
listed = failures.map { |candidate, error| "- #{candidate[:bluebook]}\n boot error: #{error}" }.join("\n")
|
|
121
|
+
<<~PROMPT
|
|
122
|
+
REPAIR ROUND. You wrote candidate Hecks bluebooks under #{out_dir} for the QA combination miner.
|
|
123
|
+
These did not boot. Edit each file IN PLACE so it boots, keeping the construct combination its
|
|
124
|
+
#{HYPOTHESIS_FILE} names (update the hypothesis if the fix changes the shape). Do not create new
|
|
125
|
+
candidates and do not touch anything outside #{out_dir}.
|
|
126
|
+
|
|
127
|
+
#{listed}
|
|
128
|
+
PROMPT
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# `<out_dir>/<slug>/<anything>.bluebook` plus an optional
|
|
132
|
+
# HYPOTHESIS.md beside it. One bluebook per candidate: the first,
|
|
133
|
+
# alphabetically, if an agent wrote more.
|
|
134
|
+
#
|
|
135
|
+
# @param out_dir [String] the directory candidates were written under
|
|
136
|
+
# @return [Array<Hash>] one `{slug:, dir:, bluebook:, hypothesis:}` entry per
|
|
137
|
+
# candidate subdirectory that declares a `.bluebook` file; `hypothesis` is
|
|
138
|
+
# `nil` when no `HYPOTHESIS.md` sits beside it
|
|
139
|
+
def candidates(out_dir)
|
|
140
|
+
Dir[File.join(out_dir, "*")].select { |dir| File.directory?(dir) }.sort.filter_map do |dir|
|
|
141
|
+
bluebook = Dir[File.join(dir, "*.bluebook")].min
|
|
142
|
+
next unless bluebook
|
|
143
|
+
|
|
144
|
+
hypothesis = File.join(dir, HYPOTHESIS_FILE)
|
|
145
|
+
{ slug: File.basename(dir), dir: dir, bluebook: bluebook,
|
|
146
|
+
hypothesis: File.exist?(hypothesis) ? File.read(hypothesis).strip : nil }
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Pairs the candidate meets on one aggregate that no corpus domain
|
|
151
|
+
# does — informational, never a gate: an agent may be aiming at a
|
|
152
|
+
# shape the census does not name yet.
|
|
153
|
+
#
|
|
154
|
+
# @param candidate_dir [String] path to the candidate's own directory
|
|
155
|
+
# @param covered [Hash{String => Array<String>}] the corpus's own covered
|
|
156
|
+
# pairs, as returned in `#brief`'s `"covered"` entry
|
|
157
|
+
# @return [Array<String>] form pairs the candidate meets that `covered` does
|
|
158
|
+
# not already name, sorted
|
|
159
|
+
def new_pairs(candidate_dir, covered)
|
|
160
|
+
FormCensus.covered_pairs(FormCensus.census(candidate_dir)).keys.reject { |pair| covered.key?(pair) }.sort
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Renders a path relative to the repository root.
|
|
164
|
+
#
|
|
165
|
+
# @param path [String] an absolute (or `root`-prefixed) path
|
|
166
|
+
# @param root [String] the prefix to strip
|
|
167
|
+
# @return [String] `path` with `root`'s own prefix removed
|
|
168
|
+
def relative(path, root) = path.delete_prefix("#{root}/")
|
|
169
|
+
|
|
170
|
+
# Renders a list of strings as a Markdown bullet list.
|
|
171
|
+
#
|
|
172
|
+
# @param items [Array<String>] lines to render as a Markdown bullet list
|
|
173
|
+
# @return [String] one `"- item"` line per entry, newline-joined; `"(none)"`
|
|
174
|
+
# when `items` is empty
|
|
175
|
+
def bulleted(items) = items.empty? ? "(none)" : items.map { |item| "- #{item}" }.join("\n")
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -3,56 +3,65 @@ require "tmpdir"
|
|
|
3
3
|
require "tempfile"
|
|
4
4
|
require "json"
|
|
5
5
|
require_relative "isolated_boot"
|
|
6
|
+
require_relative "../naming"
|
|
6
7
|
|
|
7
8
|
module Hecks
|
|
8
9
|
module Fuzzing
|
|
9
|
-
# A
|
|
10
|
+
# **A generated sequence, raced for real** — `spec/adapters/driven/
|
|
10
11
|
# postgres_era_concurrent_dispatch_spec.rb` proves ADR 0036's own fix
|
|
11
12
|
# (a real `pg_advisory_xact_lock` serializes a PostgresEra-bound
|
|
12
|
-
# dispatch across separate OS processes) against
|
|
13
|
-
# fixture and
|
|
14
|
-
# against a `$10,000` account). This module asks the
|
|
13
|
+
# dispatch across separate OS processes) against one hand-authored
|
|
14
|
+
# fixture and one hand-picked conflicting pair (two `$6,000` Debits
|
|
15
|
+
# against a `$10,000` account). This module asks the same question —
|
|
15
16
|
# does the cross-process write lock actually serialize concurrent
|
|
16
|
-
# writers? — of an
|
|
17
|
+
# writers? — of an arbitrary generated sequence against a real target
|
|
17
18
|
# domain, so the practice's own adversarial generator gets to pick the
|
|
18
19
|
# conflict instead of a human picking it once and never again.
|
|
19
20
|
#
|
|
20
|
-
#
|
|
21
|
+
# ## The mechanism
|
|
22
|
+
#
|
|
23
|
+
# Generalized from that spec rather than rederived: a
|
|
21
24
|
# command step partway through a generated sequence is chosen as the
|
|
22
|
-
#
|
|
25
|
+
# race step; every step before it is setup (replayed once, sequentially,
|
|
23
26
|
# to bring a fresh disposable schema to the state the race step expects
|
|
24
|
-
# to act against); the race step is then dispatched
|
|
27
|
+
# to act against); the race step is then dispatched twice — once from
|
|
25
28
|
# each of two real, separate, forked OS processes racing against that
|
|
26
|
-
#
|
|
29
|
+
# same schema, no artificial gating, whichever the scheduler favors.
|
|
30
|
+
#
|
|
31
|
+
# ## The oracle
|
|
27
32
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
33
|
+
# The same pair, dispatched sequentially, not a
|
|
34
|
+
# hardcoded expectation — unlike the hand-authored spec (which can
|
|
30
35
|
# assert `%w[refused succeeded]` because it knows its own fixture's
|
|
31
36
|
# business rule), this module has no idea whether an arbitrary
|
|
32
37
|
# generated race step conflicts with itself at all. So it asks the
|
|
33
|
-
#
|
|
34
|
-
# dispatch the
|
|
38
|
+
# domain: replay the identical setup on a separate fresh schema, then
|
|
39
|
+
# dispatch the same race step twice, one after the other, in one
|
|
35
40
|
# process, with no contention at all — genuinely correct by
|
|
36
41
|
# construction, since nothing else can touch that schema while it
|
|
37
42
|
# runs. Two conflicting writes settle as {"succeeded", "refused"};
|
|
38
43
|
# two independent ones settle as {"succeeded", "succeeded"}; either
|
|
39
|
-
# way,
|
|
44
|
+
# way, that is the multiset the concurrent pair must also produce if
|
|
40
45
|
# the write lock actually serializes them — order does not matter
|
|
41
|
-
# (which real racer wins a genuine race is never controlled), the
|
|
46
|
+
# (which real racer wins a genuine race is never controlled), the set
|
|
42
47
|
# of outcomes does.
|
|
43
48
|
#
|
|
44
|
-
#
|
|
49
|
+
# ## What a broken lock looks like here
|
|
50
|
+
#
|
|
51
|
+
# The concurrent pair settling as
|
|
45
52
|
# {"succeeded", "succeeded"} where the sequential oracle says
|
|
46
53
|
# {"succeeded", "refused"} — two processes each hydrated the
|
|
47
54
|
# pre-write state, neither saw the other's write, and the second
|
|
48
55
|
# commit landed as a silent lost update instead of failing its own
|
|
49
|
-
# `given`. That is the
|
|
56
|
+
# `given`. That is the exact corruption class ADR 0036 fixed for
|
|
50
57
|
# PostgresEra and `postgres_concurrent_dispatch_spec.rb` still
|
|
51
58
|
# documents, unfixed, for plain Postgres.
|
|
52
59
|
#
|
|
53
|
-
#
|
|
60
|
+
# ## Not a replacement for the hand-authored spec
|
|
61
|
+
#
|
|
62
|
+
# That spec proves the
|
|
54
63
|
# mechanism once, precisely, with controlled gating so the assertion
|
|
55
|
-
# is deterministic; this module proves the
|
|
64
|
+
# is deterministic; this module proves the same mechanism holds for
|
|
56
65
|
# whatever a real domain's own generated sequences throw at it, with
|
|
57
66
|
# no gating (a genuine, ungated race), on every sweep this mode runs.
|
|
58
67
|
module ConcurrentDispatch
|
|
@@ -60,8 +69,8 @@ module Hecks
|
|
|
60
69
|
|
|
61
70
|
COMMAND_STEP = ->(step) { step["verb"] && !step["query"] && !step["dry_run"] }
|
|
62
71
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
72
|
+
# One divergence list, the same shape every other mode in this
|
|
73
|
+
# practice produces — `[]` when nothing was found (including the
|
|
65
74
|
# legitimate "this seed's generated sequence has no command step to
|
|
66
75
|
# race at all" case: a sequence of pure queries/dry-runs has nothing
|
|
67
76
|
# to concurrently dispatch, and that is not a finding).
|
|
@@ -70,14 +79,35 @@ module Hecks
|
|
|
70
79
|
# (`bin/qa_sweep`'s own `persistence_parity_database`, reused here
|
|
71
80
|
# for the identical reason: a container, not the thing that's
|
|
72
81
|
# unique per run). `race_schema:`/`reference_schema:` are two
|
|
73
|
-
#
|
|
82
|
+
# different disposable schema names this one call owns for its own
|
|
74
83
|
# duration — the caller creates neither ahead of time (both are
|
|
75
84
|
# wiped fresh by the boots below) and drops both afterward, the
|
|
76
85
|
# same lifecycle `persistence_parity_schema` already has.
|
|
86
|
+
#
|
|
87
|
+
# @param domain_path [String] filesystem path to the target domain's directory
|
|
88
|
+
# @param steps [Array<Hash>] a generated sequence's own steps, any-keyed (normalized
|
|
89
|
+
# to String keys internally)
|
|
90
|
+
# @param database [String] the shared, never-dropped scratch database to connect to
|
|
91
|
+
# @param race_schema [String] a disposable schema name for the two racing processes
|
|
92
|
+
# @param reference_schema [String] a disposable schema name for the sequential oracle
|
|
93
|
+
# @return [Array<Hash>] `[]` if nothing raceable was found or no divergence occurred;
|
|
94
|
+
# otherwise one or more `field:`-keyed divergence findings
|
|
77
95
|
def check(domain_path, steps, database:, race_schema:, reference_schema:)
|
|
78
96
|
normalized = steps.map { |step| step.transform_keys(&:to_s) }
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
lockable, probe_errors = lockable_verbs(domain_path, normalized, database: database, schema: reference_schema)
|
|
98
|
+
race_index = pick_race_index(normalized, lockable)
|
|
99
|
+
unless race_index
|
|
100
|
+
# `[]` here means "nothing to race", and must only ever mean that.
|
|
101
|
+
# A sequence of pure queries/dry-runs is a legitimate clean
|
|
102
|
+
# result; a probe that raised for every verb is not — see
|
|
103
|
+
# `lockable_verbs`, whose own `probe_errors` half exists so this
|
|
104
|
+
# branch can tell the two apart rather than answering `[]` for both.
|
|
105
|
+
return [] if probe_errors.empty?
|
|
106
|
+
|
|
107
|
+
return [{ field: "concurrency_unraceable",
|
|
108
|
+
detail: "no command step could be raced because the cross-process-lock probe failed: " \
|
|
109
|
+
"#{probe_errors.uniq.join('; ')}" }]
|
|
110
|
+
end
|
|
81
111
|
|
|
82
112
|
setup_steps = normalized[0...race_index]
|
|
83
113
|
race_step = normalized[race_index]
|
|
@@ -90,7 +120,7 @@ module Hecks
|
|
|
90
120
|
[{ field: "process", detail: "#{e.class}: #{e.message}" }]
|
|
91
121
|
end
|
|
92
122
|
|
|
93
|
-
#
|
|
123
|
+
# The command step closest to the middle of the sequence — not the
|
|
94
124
|
# first (racing a bare identity-creation with no setup at all is a
|
|
95
125
|
# legitimate, useful case, so index 0 is not excluded) and not
|
|
96
126
|
# chosen for any domain-specific reason: a mid-sequence step has, on
|
|
@@ -98,13 +128,129 @@ module Hecks
|
|
|
98
128
|
# actually conflicting with itself. `nil` when the generated
|
|
99
129
|
# sequence has no command step at all (every step a query or a dry
|
|
100
130
|
# run) — nothing here for this seed to race.
|
|
101
|
-
|
|
131
|
+
#
|
|
132
|
+
# `lockable_verbs` — optional, but `check` always passes one: the
|
|
133
|
+
# set of verbs whose own aggregate is actually bound to an adapter
|
|
134
|
+
# that declares `:cross_process_lock` in this domain (see
|
|
135
|
+
# `lockable_verbs` below for why this can't be assumed just because
|
|
136
|
+
# the domain binds some of its own aggregates to PostgresEra).
|
|
137
|
+
# Racing anything outside that set is not a legitimate race at
|
|
138
|
+
# all — nil when nothing eligible is left, same as the "no command
|
|
139
|
+
# step" case, never a finding of its own.
|
|
140
|
+
#
|
|
141
|
+
# @param steps [Array<Hash>] a normalized (String-keyed) generated sequence
|
|
142
|
+
# @param lockable_verbs [Array<String>, nil] the verbs eligible to race, as
|
|
143
|
+
# `lockable_verbs` returns; nil to consider every command step eligible
|
|
144
|
+
# @return [Integer, nil] the index of the chosen race step, or nil if no eligible
|
|
145
|
+
# command step exists
|
|
146
|
+
def pick_race_index(steps, lockable_verbs = nil)
|
|
102
147
|
command_indices = steps.each_index.select { |i| COMMAND_STEP.call(steps[i]) }
|
|
148
|
+
command_indices = command_indices.select { |i| lockable_verbs.include?(steps[i]["verb"]) } if lockable_verbs
|
|
103
149
|
return nil if command_indices.empty?
|
|
104
150
|
|
|
105
151
|
command_indices[command_indices.size / 2]
|
|
106
152
|
end
|
|
107
153
|
|
|
154
|
+
# Which of this sequence's own command verbs are even candidates to
|
|
155
|
+
# race — BUG#142 (SW-quality_control-1789768606's own first real
|
|
156
|
+
# concurrency run, seed 3): a domain that `persisted_by("PostgresEra")`
|
|
157
|
+
# binds its own aggregates is not thereby binding a framework
|
|
158
|
+
# member it merely `uses_framework`s — `hecksagon_builder.rb`'s own
|
|
159
|
+
# `uses_framework` loads only that member's shape, never its
|
|
160
|
+
# persistence (see `examples/banking/bluebook/banking.hecksagon`'s
|
|
161
|
+
# own comment: a framework member's aggregates need a sibling
|
|
162
|
+
# hecksagon, registered under that member's own name, or
|
|
163
|
+
# `Ports::Persistence::BindingPolicy.default_binding` silently
|
|
164
|
+
# gives them "Memory" — no hecksagon registered under that name at
|
|
165
|
+
# all, so `resolve`'s own `missing_binding` refusal (which only
|
|
166
|
+
# fires when a hecksagon exists for that domain and simply omits
|
|
167
|
+
# this aggregate) never gets a chance to say so).
|
|
168
|
+
# `qa/bluebook/quality_control.hecksagon` attaches `Governance` via
|
|
169
|
+
# `uses_framework` with no such sibling — `Governance::
|
|
170
|
+
# RoleAssignment`/`RoleTransition` are Memory-backed, process-
|
|
171
|
+
# local, in the real ledger, `concurrency` mode included. Racing a
|
|
172
|
+
# Memory-backed aggregate across two real OS processes can never
|
|
173
|
+
# agree with the single-process sequential oracle — each racer's
|
|
174
|
+
# own boot gets its own independent, empty store — no matter how
|
|
175
|
+
# correct any write lock is; that is a guaranteed false positive,
|
|
176
|
+
# not evidence of a broken lock.
|
|
177
|
+
#
|
|
178
|
+
# Boots the same PostgresEra-rebound copy the race itself boots
|
|
179
|
+
# (`boot_preserving_schema` — structural inspection only, nothing
|
|
180
|
+
# dispatched, so the schema it's given is left exactly as it found
|
|
181
|
+
# it) and asks each distinct command verb's own resolved repository
|
|
182
|
+
# whether it actually declares `:cross_process_lock` — the same
|
|
183
|
+
# capability `Interpreting#run_dispatch_order_with_isolation`
|
|
184
|
+
# itself keys off of to decide whether a real advisory lock is
|
|
185
|
+
# even in play for that aggregate. A verb this boot can't resolve
|
|
186
|
+
# at all (a malformed adversarial verb, say) is conservatively
|
|
187
|
+
# excluded, not raced on a guess.
|
|
188
|
+
# Answers `[lockable, probe_errors]`. The errors half exists because
|
|
189
|
+
# the probe below rescues to `false`: a verb that cannot be resolved
|
|
190
|
+
# is indistinguishable, from the outside, from one that resolves fine
|
|
191
|
+
# and simply declares no `:cross_process_lock`. If resolution broke
|
|
192
|
+
# for every verb (a renamed capability symbol, a wiring change),
|
|
193
|
+
# `lockable` came back empty, `check` returned `[]`, and the sweep
|
|
194
|
+
# logged a clean concurrency Check for a race that never happened.
|
|
195
|
+
# `check` reports that case now instead of holding it.
|
|
196
|
+
#
|
|
197
|
+
# @param domain_path [String] filesystem path to the target domain's directory
|
|
198
|
+
# @param steps [Array<Hash>] a normalized (String-keyed) generated sequence
|
|
199
|
+
# @param database [String] the shared, never-dropped scratch database to connect to
|
|
200
|
+
# @param schema [String] a disposable schema name for the structural-inspection boot
|
|
201
|
+
# @return [Array(Array<String>, Array<String>)] `[lockable, probe_errors]` — the
|
|
202
|
+
# command verbs bound to a `:cross_process_lock`-declaring adapter, and one
|
|
203
|
+
# `"verb: Class: message"` entry per verb whose resolution raised
|
|
204
|
+
def lockable_verbs(domain_path, steps, database:, schema:)
|
|
205
|
+
verbs = steps.select { |step| COMMAND_STEP.call(step) }.map { |step| step["verb"] }.uniq
|
|
206
|
+
lockable = []
|
|
207
|
+
probe_errors = []
|
|
208
|
+
boot_preserving_schema(domain_path, database: database, schema: schema) do |copy|
|
|
209
|
+
runtime = Hecks.boot(copy)
|
|
210
|
+
verbs.each do |verb|
|
|
211
|
+
lockable << verb if verb_cross_process_lockable?(runtime, verb, probe_errors)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
[lockable, probe_errors]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Answers whether `verb`'s own resolved repository declares a cross-process lock.
|
|
218
|
+
#
|
|
219
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted,
|
|
220
|
+
# structural-inspection-only runtime to resolve `verb` against
|
|
221
|
+
# @param verb [String] the command verb to check
|
|
222
|
+
# @param probe_errors [Array<String>] mutated in place: appended with
|
|
223
|
+
# `"verb: Class: message"` if resolving `verb` raises
|
|
224
|
+
# @return [Boolean] whether `verb`'s aggregate resolves and declares
|
|
225
|
+
# `:cross_process_lock`; false (with an appended probe error) if resolution raises
|
|
226
|
+
def verb_cross_process_lockable?(runtime, verb, probe_errors = [])
|
|
227
|
+
domain, aggregate_name, = Naming.split_verb(verb)
|
|
228
|
+
return false unless domain && aggregate_name
|
|
229
|
+
|
|
230
|
+
aggregate = runtime.registry.bluebook(domain)&.aggregate(aggregate_name)
|
|
231
|
+
return false unless aggregate
|
|
232
|
+
|
|
233
|
+
repository = runtime.registry.repository(domain, aggregate)
|
|
234
|
+
repository.capabilities.include?(:cross_process_lock)
|
|
235
|
+
rescue StandardError => e
|
|
236
|
+
# Still `false` — a verb this boot cannot resolve is not raced on a
|
|
237
|
+
# guess — but no longer silent: `check` needs to tell "nothing here
|
|
238
|
+
# declares a cross-process lock" from "asking broke".
|
|
239
|
+
probe_errors << "#{verb}: #{e.class}: #{e.message}"
|
|
240
|
+
false
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Compares the sequential oracle's outcomes against the two racing
|
|
244
|
+
# processes' own outcomes, and reports any crash or disagreement.
|
|
245
|
+
#
|
|
246
|
+
# @param race_step [Hash] the race step, String-keyed
|
|
247
|
+
# @param reference [Array<String>] the sequential oracle's two outcomes,
|
|
248
|
+
# `"succeeded"`, `"refused"`, or `"crashed:..."`
|
|
249
|
+
# @param concurrent [Array<String>] the two racing processes' own outcomes,
|
|
250
|
+
# the same shape as `reference`
|
|
251
|
+
# @return [Array<Hash>] `[]` if the two multisets agree and neither side crashed;
|
|
252
|
+
# otherwise one or more `field:`-keyed divergence findings (`"concurrency_crash"`
|
|
253
|
+
# or `"concurrency_race"`)
|
|
108
254
|
def divergences_for(race_step, reference, concurrent)
|
|
109
255
|
crashes = (reference + concurrent).select { |outcome| outcome.start_with?("crashed:") }.uniq
|
|
110
256
|
return crashes.map { |c| { field: "concurrency_crash", verb: race_step["verb"], detail: c } } if crashes.any?
|
|
@@ -117,10 +263,18 @@ module Hecks
|
|
|
117
263
|
"#{reference.sort} — the cross-process write lock did not correctly serialize this write" }]
|
|
118
264
|
end
|
|
119
265
|
|
|
120
|
-
#
|
|
121
|
-
#
|
|
266
|
+
# The oracle — one boot, one process, the setup then the race step
|
|
267
|
+
# twice in immediate succession. Nothing else ever touches this
|
|
122
268
|
# schema while this runs, so whatever the domain itself settles on
|
|
123
269
|
# is correct by construction, not asserted.
|
|
270
|
+
#
|
|
271
|
+
# @param domain_path [String] filesystem path to the target domain's directory
|
|
272
|
+
# @param setup_steps [Array<Hash>] the normalized steps replayed once, sequentially,
|
|
273
|
+
# before the race step
|
|
274
|
+
# @param race_step [Hash] the normalized step dispatched twice in immediate succession
|
|
275
|
+
# @param database [String] the shared, never-dropped scratch database to connect to
|
|
276
|
+
# @param schema [String] this oracle's own disposable schema name
|
|
277
|
+
# @return [Array<String>] the two outcomes `dispatch_one` recorded, in dispatch order
|
|
124
278
|
def reference_outcomes(domain_path, setup_steps, race_step, database:, schema:)
|
|
125
279
|
outcomes = []
|
|
126
280
|
IsolatedBoot.call(domain_path, adapter: :postgres_era, database: database, schema: schema) do |copy|
|
|
@@ -132,35 +286,44 @@ module Hecks
|
|
|
132
286
|
outcomes
|
|
133
287
|
end
|
|
134
288
|
|
|
135
|
-
#
|
|
289
|
+
# The race itself — setup runs once, sequentially, in this process
|
|
136
290
|
# (the same `IsolatedBoot.call` wipe-then-boot every other mode
|
|
137
|
-
# here already uses), and only
|
|
138
|
-
# Each racer boots its
|
|
139
|
-
#
|
|
291
|
+
# here already uses), and only then do the two real racers run.
|
|
292
|
+
# Each racer boots its own fresh copy of the domain against the
|
|
293
|
+
# same now-populated schema — `boot_preserving_schema`, below,
|
|
140
294
|
# deliberately skips the wipe `IsolatedBoot.call` always does, or
|
|
141
295
|
# the setup this line just wrote would be gone before either racer
|
|
142
296
|
# ever dispatched anything.
|
|
143
297
|
#
|
|
144
|
-
#
|
|
298
|
+
# Real, separate OS processes, not `Thread.new` — `postgres_era_
|
|
145
299
|
# concurrent_dispatch_spec.rb`'s own header explains why:
|
|
146
300
|
# `Runtime::AggregateLock`'s in-process registry would fully (and
|
|
147
|
-
# misleadingly) serialize two
|
|
301
|
+
# misleadingly) serialize two threads sharing one process even with
|
|
148
302
|
# the cross-process lock fix reverted. Only two genuinely separate
|
|
149
303
|
# OS processes exercise the gap this check exists to catch.
|
|
150
304
|
#
|
|
151
|
-
# `Process.spawn`,
|
|
305
|
+
# `Process.spawn`, not `Process.fork` — `bin/qa_sweep`'s own
|
|
152
306
|
# top-of-file comment on `--all` names the identical hazard this
|
|
153
|
-
# sidesteps: by the time a `concurrency` seed runs,
|
|
307
|
+
# sidesteps: by the time a `concurrency` seed runs, this process
|
|
154
308
|
# already holds the QualityControl ledger's own live PostgresEra
|
|
155
309
|
# connection (this module's own caller, `bin/qa_sweep`, booted it
|
|
156
310
|
# long before any seed ran). `Process.fork` duplicates every open
|
|
157
311
|
# file descriptor, SSL session state included — confirmed live
|
|
158
312
|
# while wiring this mode up: forking directly from here corrupted
|
|
159
|
-
# the
|
|
160
|
-
# surfacing on the
|
|
313
|
+
# the ledger's own connection the moment either racer child exited,
|
|
314
|
+
# surfacing on the next unrelated ledger write, nowhere near this
|
|
161
315
|
# method's own code. `bin/qa_concurrency_racer` is this method's own
|
|
162
316
|
# worker, one real `ruby` process per racer — read that script's own
|
|
163
317
|
# header for the rest of this reasoning.
|
|
318
|
+
#
|
|
319
|
+
# @param domain_path [String] filesystem path to the target domain's directory
|
|
320
|
+
# @param setup_steps [Array<Hash>] the normalized steps replayed once, sequentially,
|
|
321
|
+
# before the two racers spawn
|
|
322
|
+
# @param race_step [Hash] the normalized step both racer processes dispatch
|
|
323
|
+
# @param database [String] the shared, never-dropped scratch database to connect to
|
|
324
|
+
# @param schema [String] this race's own disposable schema name, shared by both racers
|
|
325
|
+
# @return [Array<String>] the two racers' own outcomes, in `Process.wait` order;
|
|
326
|
+
# `"crashed:no output from bin/qa_concurrency_racer"` for a racer that printed nothing
|
|
164
327
|
def concurrent_outcomes(domain_path, setup_steps, race_step, database:, schema:)
|
|
165
328
|
IsolatedBoot.call(domain_path, adapter: :postgres_era, database: database, schema: schema) do |copy|
|
|
166
329
|
dispatch_all!(Hecks.boot(copy), setup_steps)
|
|
@@ -186,14 +349,19 @@ module Hecks
|
|
|
186
349
|
end
|
|
187
350
|
end
|
|
188
351
|
|
|
189
|
-
#
|
|
352
|
+
# One step, one outcome — never raises: a declared domain refusal is
|
|
190
353
|
# "refused" (the expected, ordinary answer a `given`/invariant can
|
|
191
354
|
# give), anything else escaping is "crashed:<class>: <message>", a
|
|
192
355
|
# genuine finding this module's own caller surfaces rather than lets
|
|
193
356
|
# kill a forked racer silently.
|
|
357
|
+
#
|
|
358
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
359
|
+
# to dispatch against
|
|
360
|
+
# @param step [Hash] the normalized (String-keyed) step to dispatch
|
|
361
|
+
# @return [String] `"succeeded"`, `"refused"`, or `"crashed:<class>: <message>"`
|
|
194
362
|
def dispatch_one(runtime, step)
|
|
195
363
|
args = (step["args"] || {}).transform_keys(&:to_sym)
|
|
196
|
-
runtime.
|
|
364
|
+
runtime.dispatch_flat(step["verb"], args)
|
|
197
365
|
"succeeded"
|
|
198
366
|
rescue *Hecks::Runtime::DOMAIN_REFUSALS, Hecks::Bluebook::Expression::EvaluationError
|
|
199
367
|
"refused"
|
|
@@ -201,12 +369,21 @@ module Hecks
|
|
|
201
369
|
"crashed:#{e.class}: #{e.message}"
|
|
202
370
|
end
|
|
203
371
|
|
|
204
|
-
#
|
|
372
|
+
# Dispatches every step in order, raising the first time one crashes.
|
|
373
|
+
#
|
|
374
|
+
# Setup tolerates an ordinary refusal (a generated sequence's own
|
|
205
375
|
# earlier step can legitimately refuse — every other mode in this
|
|
206
376
|
# practice already replays a prefix that way) but never a crash: an
|
|
207
377
|
# unexpected exception during setup means the schema this race is
|
|
208
378
|
# about to run against is in an unknown state, which is itself
|
|
209
379
|
# worth surfacing, not silently racing anyway.
|
|
380
|
+
#
|
|
381
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
382
|
+
# to dispatch against
|
|
383
|
+
# @param steps [Array<Hash>] the normalized steps to dispatch, in order
|
|
384
|
+
# @return [void]
|
|
385
|
+
# @raise [RuntimeError] if any step's own `dispatch_one` outcome is a crash, naming
|
|
386
|
+
# the step's verb and outcome
|
|
210
387
|
def dispatch_all!(runtime, steps)
|
|
211
388
|
steps.each do |step|
|
|
212
389
|
outcome = dispatch_one(runtime, step)
|
|
@@ -214,19 +391,30 @@ module Hecks
|
|
|
214
391
|
end
|
|
215
392
|
end
|
|
216
393
|
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
394
|
+
# Boots a `PostgresEra`-rebound copy of the domain against an
|
|
395
|
+
# existing schema, without wiping it first.
|
|
396
|
+
#
|
|
397
|
+
# The same copy-and-rebind `IsolatedBoot.call(..., adapter:
|
|
398
|
+
# :postgres_era, ...)` does, minus the schema wipe — deliberately
|
|
399
|
+
# not reusing `IsolatedBoot.rebind_to_postgres_era!` itself, which
|
|
220
400
|
# bundles `ensure_postgres_era_schema!`'s own `DROP SCHEMA` into the
|
|
221
401
|
# same call with no way to opt out (see that method's own header:
|
|
222
402
|
# the wipe is the "zero-history guarantee every other adapter mode
|
|
223
|
-
# already gives," exactly the guarantee
|
|
403
|
+
# already gives," exactly the guarantee this caller must not have —
|
|
224
404
|
# the whole point of a race is booting against what setup already
|
|
225
405
|
# wrote). `copy_dereferencing`/`rewrite_bindings!` are the same two
|
|
226
406
|
# public steps that method itself calls first; only the `.world`
|
|
227
407
|
# this writes is duplicated from it, not re-derived, because the
|
|
228
408
|
# shape a `PostgresEra`-bound copy's `.world` needs is exactly that
|
|
229
409
|
# method's own, one step short.
|
|
410
|
+
#
|
|
411
|
+
# @param domain_path [String] filesystem path to the target domain's directory
|
|
412
|
+
# @param database [String] the PostgresEra database name to bind the copy to
|
|
413
|
+
# @param schema [String] the existing PostgresEra schema name to bind the copy to,
|
|
414
|
+
# left exactly as found
|
|
415
|
+
# @yield [copy] runs against the rebound copy
|
|
416
|
+
# @yieldparam copy [String] filesystem path to the rebound domain copy
|
|
417
|
+
# @return [Object] the block's own result
|
|
230
418
|
def boot_preserving_schema(domain_path, database:, schema:)
|
|
231
419
|
Dir.mktmpdir("hecks-concurrency") do |tmp|
|
|
232
420
|
copy = File.join(tmp, File.basename(domain_path))
|
|
@@ -238,6 +426,14 @@ module Hecks
|
|
|
238
426
|
end
|
|
239
427
|
end
|
|
240
428
|
|
|
429
|
+
# Writes a `.world` file binding every hecksagon-named member in
|
|
430
|
+
# `copy` to `database`/`schema`, replacing any other `.world` file
|
|
431
|
+
# the copy already carries.
|
|
432
|
+
#
|
|
433
|
+
# @param copy [String] filesystem path to the domain copy to rewrite
|
|
434
|
+
# @param database [String] the PostgresEra database name to write into the `.world` file
|
|
435
|
+
# @param schema [String] the PostgresEra schema name to write into the `.world` file
|
|
436
|
+
# @return [void]
|
|
241
437
|
def write_postgres_era_world!(copy, database:, schema:)
|
|
242
438
|
Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |hecksagon_path|
|
|
243
439
|
names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten.uniq
|