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,309 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
# A failing step list, made small enough to read.
|
|
4
|
+
#
|
|
5
|
+
# `bin/fuzz` always shrank its findings; `bin/qa_sweep` — the loop that
|
|
6
|
+
# actually finds the bugs — never did, so the ledger filled with
|
|
7
|
+
# demonstrations like "seed 25, 25 steps, step 16" and a human cut
|
|
8
|
+
# BUG#40 down to its four real steps by hand. This is `bin/fuzz`'s
|
|
9
|
+
# shrinker lifted out of that script so both callers share it, and
|
|
10
|
+
# made cheaper at the same time: a sweep's candidate check can cost a
|
|
11
|
+
# Rust subprocess (or a real Postgres round trip), where `bin/fuzz`'s
|
|
12
|
+
# only ever cost an in-process replay.
|
|
13
|
+
#
|
|
14
|
+
# ## Ownership of "same finding"
|
|
15
|
+
#
|
|
16
|
+
# The caller owns it. `call` is handed a block that
|
|
17
|
+
# answers true when a candidate step list still reproduces what the
|
|
18
|
+
# original did — `bin/fuzz` compares its own verdict signature,
|
|
19
|
+
# `bin/qa_sweep` compares `Shrinker.signature` of one mode's
|
|
20
|
+
# divergences. This module never replays anything itself, so it has
|
|
21
|
+
# no idea which engine, adapter or comparison it is minimizing for.
|
|
22
|
+
#
|
|
23
|
+
# ## Two passes, in order
|
|
24
|
+
#
|
|
25
|
+
# 1. Steps, chunks first. Removing one step at a time (what
|
|
26
|
+
# `bin/fuzz` did) costs O(n²) candidate checks on a sequence
|
|
27
|
+
# where most steps are irrelevant. Delta-debugging style: try
|
|
28
|
+
# removing halves, then quarters, … then single steps, keeping
|
|
29
|
+
# any removal that still reproduces; a single-step pass repeats
|
|
30
|
+
# until it changes nothing, so the result is 1-minimal (no one
|
|
31
|
+
# remaining step can be dropped).
|
|
32
|
+
# 2. Arguments, inside each surviving step. Unchanged from `bin/fuzz`
|
|
33
|
+
# (spec/bin_fuzz_spec.rb pins its accumulation contract): drop
|
|
34
|
+
# one key at a time from the step's current args, keep it dropped
|
|
35
|
+
# only while the finding still reproduces.
|
|
36
|
+
#
|
|
37
|
+
# ## Budget
|
|
38
|
+
#
|
|
39
|
+
# A budget, because a sweep has other targets waiting. `budget:` caps
|
|
40
|
+
# how many candidate checks one call may spend (nil = unbounded, the
|
|
41
|
+
# `bin/fuzz` behaviour). When it runs out the best candidate found so
|
|
42
|
+
# far is returned — every accepted candidate reproduced, so a partial
|
|
43
|
+
# shrink is still a correct, just less small, demonstration.
|
|
44
|
+
module Shrinker
|
|
45
|
+
Result = Struct.new(:steps, :attempts, :exhausted, keyword_init: true)
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
# Shrinks `steps` to the smallest step list the block still accepts as
|
|
50
|
+
# reproducing the original finding — pass 1 (steps), then pass 2
|
|
51
|
+
# (arguments), as this module's own header describes.
|
|
52
|
+
#
|
|
53
|
+
# @param steps [Array<Hash>] the step list to shrink, each step a command,
|
|
54
|
+
# query, or read-model hash as `SequenceGenerator` produces
|
|
55
|
+
# @param budget [Integer, nil] maximum candidate checks to spend; nil for
|
|
56
|
+
# unbounded
|
|
57
|
+
# @yield [candidate] tests whether a candidate step list still reproduces
|
|
58
|
+
# the finding being shrunk
|
|
59
|
+
# @yieldparam candidate [Array<Hash>] a step-dropped or argument-trimmed
|
|
60
|
+
# copy of `steps`
|
|
61
|
+
# @yieldreturn [Boolean] true if `candidate` still reproduces the finding
|
|
62
|
+
# @return [Fuzzing::Shrinker::Result] `steps:` the shrunk step list,
|
|
63
|
+
# `attempts:` how many candidate checks were spent, `exhausted:` whether
|
|
64
|
+
# the budget ran out before shrinking finished
|
|
65
|
+
# @raise [ArgumentError] if no block is given
|
|
66
|
+
def call(steps, budget: nil, &reproduces)
|
|
67
|
+
raise ArgumentError, "Shrinker.call needs a block answering whether a candidate reproduces" unless reproduces
|
|
68
|
+
|
|
69
|
+
meter = Meter.new(budget)
|
|
70
|
+
current = drop_steps(steps.dup, meter, &reproduces)
|
|
71
|
+
current = drop_arguments(current, meter, &reproduces)
|
|
72
|
+
Result.new(steps: current, attempts: meter.used, exhausted: meter.exhausted?)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Runs pass 1 of the shrink: delta-debugging removal of step chunks,
|
|
76
|
+
# halving the chunk size each full pass until single steps are tried
|
|
77
|
+
# and nothing more can be dropped.
|
|
78
|
+
#
|
|
79
|
+
# @param steps [Array<Hash>] the step list to shrink
|
|
80
|
+
# @param meter [Fuzzing::Shrinker::Meter] the shared budget meter
|
|
81
|
+
# @yield [candidate] tests whether a candidate step list still reproduces
|
|
82
|
+
# the finding being shrunk
|
|
83
|
+
# @yieldparam candidate [Array<Hash>] `steps` with one chunk removed
|
|
84
|
+
# @yieldreturn [Boolean] true if `candidate` still reproduces the finding
|
|
85
|
+
# @return [Array<Hash>] the smallest step list pass 1 could reach, or the
|
|
86
|
+
# best found so far if the budget ran out first
|
|
87
|
+
def drop_steps(steps, meter, &reproduces)
|
|
88
|
+
current = steps
|
|
89
|
+
chunk = [current.length / 2, 1].max
|
|
90
|
+
loop do
|
|
91
|
+
changed = false
|
|
92
|
+
index = 0
|
|
93
|
+
while index < current.length
|
|
94
|
+
return current if meter.exhausted?
|
|
95
|
+
|
|
96
|
+
candidate = current[0...index] + (current[(index + chunk)..] || [])
|
|
97
|
+
if candidate.empty?
|
|
98
|
+
index += chunk
|
|
99
|
+
next
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if meter.try { reproduces.call(candidate) }
|
|
103
|
+
current = candidate
|
|
104
|
+
changed = true
|
|
105
|
+
else
|
|
106
|
+
index += chunk
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if chunk > 1
|
|
111
|
+
chunk = [chunk / 2, 1].max
|
|
112
|
+
elsif !changed
|
|
113
|
+
break
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
current
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Runs pass 2 of the shrink: drops one argument key at a time from
|
|
120
|
+
# each surviving step's args, keeping the drop only while the
|
|
121
|
+
# finding still reproduces.
|
|
122
|
+
#
|
|
123
|
+
# See the module header's pass 2 — `args` is read by whichever
|
|
124
|
+
# spelling the step actually carries (`key?` first, never `||`,
|
|
125
|
+
# which cannot tell a stored `false` from an absent key).
|
|
126
|
+
#
|
|
127
|
+
# @param steps [Array<Hash>] the step list, already step-shrunk by `drop_steps`
|
|
128
|
+
# @param meter [Fuzzing::Shrinker::Meter] the shared budget meter
|
|
129
|
+
# @yield [candidate] tests whether a candidate step list still reproduces
|
|
130
|
+
# the finding being shrunk
|
|
131
|
+
# @yieldparam candidate [Array<Hash>] `steps` with one step's one argument
|
|
132
|
+
# key dropped
|
|
133
|
+
# @yieldreturn [Boolean] true if `candidate` still reproduces the finding
|
|
134
|
+
# @return [Array<Hash>] `steps` with every argument key `drop_arguments`
|
|
135
|
+
# accepted dropped
|
|
136
|
+
def drop_arguments(steps, meter, &reproduces)
|
|
137
|
+
steps.each_index do |position|
|
|
138
|
+
original = args_of(steps[position])
|
|
139
|
+
next unless original.is_a?(Hash)
|
|
140
|
+
|
|
141
|
+
original.each_key do |key|
|
|
142
|
+
return steps if meter.exhausted?
|
|
143
|
+
|
|
144
|
+
step = steps[position]
|
|
145
|
+
trimmed = args_of(step).reject { |name, _| name == key }
|
|
146
|
+
candidate = steps.map(&:dup)
|
|
147
|
+
candidate[position] = step.merge("args" => trimmed)
|
|
148
|
+
steps = candidate if meter.try { reproduces.call(candidate) }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
steps
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Reads a step's argument hash, whichever key spelling it uses.
|
|
155
|
+
#
|
|
156
|
+
# @param step [Hash] one step, string- or symbol-keyed
|
|
157
|
+
# @return [Hash, nil] the step's `"args"` value if the step is string-keyed,
|
|
158
|
+
# else its `:args` value (nil if neither key is present)
|
|
159
|
+
def args_of(step)
|
|
160
|
+
step.key?("args") ? step["args"] : step[:args]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Which finding this is, as a set of stable strings — the identity a
|
|
164
|
+
# shrink candidate has to keep. The `field` alone is too loose for
|
|
165
|
+
# the comparisons that carry lists: "refusals differ" on a 3-step
|
|
166
|
+
# candidate could be a different refusal split than the one the
|
|
167
|
+
# original seed found, and a shrinker that accepted it would hand
|
|
168
|
+
# back a demonstration of the wrong bug. So:
|
|
169
|
+
#
|
|
170
|
+
# refusals/queries/dry_runs/reactions — the field plus every verb
|
|
171
|
+
# (or query) named in the symmetric difference of the two sides;
|
|
172
|
+
# instances — the field plus the aggregate of every top-level key
|
|
173
|
+
# whose two sides disagree (the `#id` suffix dropped);
|
|
174
|
+
# a crash/process finding — the field plus the exception class
|
|
175
|
+
# leading its detail;
|
|
176
|
+
# anything else (a property name, a self-consistency axis) — the
|
|
177
|
+
# field, which already names the finding.
|
|
178
|
+
#
|
|
179
|
+
# `reproduces?` holds when a candidate's signature contains the
|
|
180
|
+
# original's: removing steps may add a second divergence, but it
|
|
181
|
+
# must never lose the one being demonstrated.
|
|
182
|
+
LIST_FIELDS = %w[refusals queries dry_runs reactions].freeze
|
|
183
|
+
|
|
184
|
+
CRASH_FIELDS = %w[crash process generator_crash].freeze
|
|
185
|
+
|
|
186
|
+
# Computes which finding a set of divergences is — the stable identity
|
|
187
|
+
# a shrink candidate has to keep. See this method's own preceding
|
|
188
|
+
# comment for what each field kind contributes.
|
|
189
|
+
#
|
|
190
|
+
# @param divergences [Array<Hash>] divergence entries, each at least
|
|
191
|
+
# `{field:, detail:}` plus whichever mode-specific keys carry the two
|
|
192
|
+
# compared sides
|
|
193
|
+
# @return [Set<String>] stable identity strings: each divergence's `field`,
|
|
194
|
+
# plus `detail_keys`' own per-field detail strings
|
|
195
|
+
def signature(divergences)
|
|
196
|
+
divergences.each_with_object(Set.new) do |divergence, keys|
|
|
197
|
+
field = divergence[:field].to_s
|
|
198
|
+
keys << field
|
|
199
|
+
keys.merge(detail_keys(field, divergence))
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Extracts the finer-grained identity strings one divergence's field
|
|
204
|
+
# contributes beyond the field name itself.
|
|
205
|
+
#
|
|
206
|
+
# @param field [String] the divergence's `field`, stringified
|
|
207
|
+
# @param divergence [Hash] the divergence entry `field` came from
|
|
208
|
+
# @return [Array<String>] extra identity strings for `field`'s own kind
|
|
209
|
+
# (list, `instances`, or crash/process); empty for any other field
|
|
210
|
+
def detail_keys(field, divergence)
|
|
211
|
+
left, right = divergence.except(:field, :detail).values.select { |v| v.is_a?(Array) || v.is_a?(Hash) }
|
|
212
|
+
if LIST_FIELDS.include?(field) then list_keys(field, left, right)
|
|
213
|
+
elsif field == "instances" then instance_keys(left, right)
|
|
214
|
+
elsif CRASH_FIELDS.include?(field) && divergence[:detail]
|
|
215
|
+
["#{field}:#{divergence[:detail].to_s[/\A\w+(?:::\w+)*/]}"]
|
|
216
|
+
else []
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Names the rows present on one side of a list-field divergence and not
|
|
221
|
+
# the other.
|
|
222
|
+
#
|
|
223
|
+
# @param field [String] the divergence's field name, prefixed onto each key
|
|
224
|
+
# @param left [Array, nil] one side's list; a non-Array yields no keys
|
|
225
|
+
# @param right [Array, nil] the other side's list; a non-Array yields no keys
|
|
226
|
+
# @return [Array<String>] `"field:name"` for every row in the symmetric
|
|
227
|
+
# difference of `left` and `right`; empty unless both are arrays
|
|
228
|
+
def list_keys(field, left, right)
|
|
229
|
+
return [] unless left.is_a?(Array) && right.is_a?(Array)
|
|
230
|
+
|
|
231
|
+
((left - right) + (right - left)).map { |row| "#{field}:#{named(row)}" }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Names the top-level keys the two sides of an `instances` divergence
|
|
235
|
+
# disagree on.
|
|
236
|
+
#
|
|
237
|
+
# Instance keys are `Aggregate#id` on the wire — the id is whatever
|
|
238
|
+
# the generator minted, so it names the record, not the finding;
|
|
239
|
+
# keeping it would pin every creating step.
|
|
240
|
+
#
|
|
241
|
+
# @param left [Hash, nil] one side's instance snapshot; a non-Hash yields no keys
|
|
242
|
+
# @param right [Hash, nil] the other side's instance snapshot; a non-Hash
|
|
243
|
+
# yields no keys
|
|
244
|
+
# @return [Array<String>] `"instances:Aggregate"` for every top-level key
|
|
245
|
+
# the two sides disagree on, its `#id` suffix dropped; empty unless both
|
|
246
|
+
# are hashes
|
|
247
|
+
def instance_keys(left, right)
|
|
248
|
+
return [] unless left.is_a?(Hash) && right.is_a?(Hash)
|
|
249
|
+
|
|
250
|
+
(left.keys | right.keys).reject { |key| left[key] == right[key] }
|
|
251
|
+
.map { |key| "instances:#{key.to_s.split('#').first}" }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Checks that a candidate's own divergences still demonstrate the
|
|
255
|
+
# finding being shrunk.
|
|
256
|
+
#
|
|
257
|
+
# @param original_signature [Set<String>] the finding's own signature, as
|
|
258
|
+
# returned by `signature`
|
|
259
|
+
# @param divergences [Array<Hash>] the candidate's own divergence entries,
|
|
260
|
+
# in `signature`'s own shape
|
|
261
|
+
# @return [Boolean] true if `divergences` is non-empty and its signature is
|
|
262
|
+
# a superset of `original_signature`
|
|
263
|
+
def reproduces?(original_signature, divergences)
|
|
264
|
+
!divergences.empty? && original_signature.subset?(signature(divergences))
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Names one row from a list-field divergence, for `list_keys`.
|
|
268
|
+
#
|
|
269
|
+
# @param row [Object] one item from a list-field divergence's array;
|
|
270
|
+
# typically a Hash carrying a `verb`, `query`, or `policy` key (string
|
|
271
|
+
# or symbol)
|
|
272
|
+
# @return [String] `row`'s `verb`/`query`/`policy` value, stringified, if
|
|
273
|
+
# `row` is a Hash carrying one of those keys; otherwise `row.to_s`
|
|
274
|
+
def named(row)
|
|
275
|
+
return row.to_s unless row.is_a?(Hash)
|
|
276
|
+
|
|
277
|
+
key = [%w[verb query policy], %i[verb query policy]].flatten.find { |name| row.key?(name) }
|
|
278
|
+
(key ? row[key] : row).to_s
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Counts candidate checks against the budget.
|
|
282
|
+
class Meter
|
|
283
|
+
attr_reader :used
|
|
284
|
+
|
|
285
|
+
# @param budget [Integer, nil] maximum candidate checks to allow; nil for unbounded
|
|
286
|
+
def initialize(budget)
|
|
287
|
+
@budget = budget
|
|
288
|
+
@used = 0
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Reports whether this meter's budget has been fully spent.
|
|
292
|
+
#
|
|
293
|
+
# @return [Boolean] true if a budget is set and every allotted check has
|
|
294
|
+
# been used
|
|
295
|
+
def exhausted? = !@budget.nil? && @used >= @budget
|
|
296
|
+
|
|
297
|
+
# Spends one candidate check against the budget and runs the block.
|
|
298
|
+
#
|
|
299
|
+
# @yield runs the candidate check being counted
|
|
300
|
+
# @yieldreturn [Object] the block's own result
|
|
301
|
+
# @return [Object] whatever the block returns
|
|
302
|
+
def try
|
|
303
|
+
@used += 1
|
|
304
|
+
yield
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
@@ -1,146 +1,53 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Fuzzing
|
|
3
|
-
#
|
|
3
|
+
# What each query verb the differential comparison skipped was hiding.
|
|
4
4
|
#
|
|
5
|
-
# `
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# much of the domain was never compared at all. Worse, the drop keys
|
|
14
|
-
# on Rust's wording alone, so a codegen regression that started
|
|
15
|
-
# refusing a plain wheres-only query the same way would ALSO vanish
|
|
16
|
-
# into "agreed" — the exact quiet divergence the practice hunts.
|
|
5
|
+
# `Differential.manifest_partition` drops a named query or read model
|
|
6
|
+
# from both sides of the Ruby/Rust comparison only when the binary's own
|
|
7
|
+
# manifest.json declares it `generated: false`. The drop is correct, but
|
|
8
|
+
# a sweep that dropped every `offset`/`cursor` ask would still read as
|
|
9
|
+
# "agreed across all steps". `bin/qa_sweep` therefore logs one Check per
|
|
10
|
+
# sweep naming every skipped verb and the construct family that caused
|
|
11
|
+
# it, and marks it Surprised when a family is outside
|
|
12
|
+
# `QualityControlDials::STRUCTURAL_REFUSAL_BOUNDARY`.
|
|
17
13
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# WHY — and mark it Surprised whenever a skipped verb declares nothing
|
|
23
|
-
# the dial `QualityControlDials::STRUCTURAL_REFUSAL_BOUNDARY` admits as
|
|
24
|
-
# a reason to skip (a wheres-only query on plain fields, a rooted
|
|
25
|
-
# read model with nothing but heads, a verb Ruby doesn't even declare).
|
|
26
|
-
#
|
|
27
|
-
# COARSER THAN CODEGEN'S OWN PREDICATE, ON PURPOSE. `query_where_skip_
|
|
28
|
-
# reason` decides per where clause from the field's resolved KIND
|
|
29
|
-
# (number/string/multi-member value object) — reproducing that here
|
|
30
|
-
# would be a second copy of codegen's typing rules, drifting on its
|
|
31
|
-
# own. Instead a literal-valued where is reported as `where_literal`,
|
|
32
|
-
# a family the dial admits, and only shapes with NO admitted family at
|
|
33
|
-
# all surprise. That trades some sensitivity for zero false surprises
|
|
34
|
-
# on today's corpus; the Check's observation still names every skipped
|
|
35
|
-
# verb with its constructs, so a human reading the sweep sees what was
|
|
36
|
-
# accepted and can narrow the dial when codegen grows.
|
|
14
|
+
# The construct comes straight from the manifest entry, written by the
|
|
15
|
+
# generator branch that made the skip (`Projector::SkipReason`), rather
|
|
16
|
+
# than guessed from the Ruby declaration's `to_h` keys, which is coarser
|
|
17
|
+
# than codegen's own decision and can drift from it.
|
|
37
18
|
module StructuralSkips
|
|
38
19
|
module_function
|
|
39
20
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
21
|
+
# Attributes each skipped verb to the construct family its manifest
|
|
22
|
+
# entry names.
|
|
23
|
+
#
|
|
24
|
+
# One entry per skipped verb: `{ verb:, constructs: [...] }`, sorted by
|
|
25
|
+
# verb so the printed observation is stable across seeds and runs. A
|
|
26
|
+
# verb the manifest doesn't declare answers `unknown`, which no
|
|
27
|
+
# boundary admits.
|
|
28
|
+
#
|
|
29
|
+
# @param gaps [Fuzzing::RustGapManifest] the binary's not-generated manifest entries
|
|
30
|
+
# @param verbs [Array<String>] the skipped query/read-model verbs to attribute
|
|
31
|
+
# @return [Array<Hash{Symbol => Object}>] one `{ verb: String, constructs: Array<String> }`
|
|
32
|
+
# per verb, in sorted-verb order
|
|
33
|
+
def attribute(gaps, verbs)
|
|
34
|
+
verbs.sort.map do |verb|
|
|
35
|
+
entry = gaps.not_generated(verb)
|
|
36
|
+
{ verb: verb, constructs: entry ? [entry.fetch("construct")] : %w[unknown] }
|
|
37
|
+
end
|
|
53
38
|
end
|
|
54
39
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
40
|
+
# Selects the attributed entries whose constructs are not all inside `boundary`.
|
|
41
|
+
#
|
|
42
|
+
# @param attributed [Array<Hash{Symbol => Object}>] entries as returned by `attribute`
|
|
43
|
+
# @param boundary [Array<String, Symbol>] the construct families the documented codegen
|
|
44
|
+
# boundary admits
|
|
45
|
+
# @return [Array<Hash{Symbol => Object}>] the subset of `attributed` naming an
|
|
46
|
+
# unadmitted construct, or no construct at all
|
|
58
47
|
def outside_boundary(attributed, boundary)
|
|
59
48
|
admitted = boundary.map(&:to_s)
|
|
60
49
|
attributed.select { |entry| entry[:constructs].empty? || (entry[:constructs] - admitted).any? }
|
|
61
50
|
end
|
|
62
|
-
|
|
63
|
-
def constructs_of(bluebooks, verb)
|
|
64
|
-
if verb.include?("::")
|
|
65
|
-
query = find_query(bluebooks, verb)
|
|
66
|
-
return %w[unknown] unless query
|
|
67
|
-
|
|
68
|
-
query_constructs(query)
|
|
69
|
-
else
|
|
70
|
-
model = find_read_model(bluebooks, verb)
|
|
71
|
-
return %w[unknown] unless model
|
|
72
|
-
|
|
73
|
-
read_model_constructs(model)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# "Domain::Aggregate.Query" or "Domain::Aggregate.Entity.Query" — the
|
|
78
|
-
# two named-query spellings `Fuzzing::SequenceGenerator`'s catalog
|
|
79
|
-
# generates (entity queries stay one hop deep there).
|
|
80
|
-
def find_query(bluebooks, verb)
|
|
81
|
-
domain, rest = verb.split("::", 2)
|
|
82
|
-
aggregate_name, *path = rest.to_s.split(".")
|
|
83
|
-
aggregate = bluebooks[domain]&.aggregate(aggregate_name)
|
|
84
|
-
return nil unless aggregate && path.any?
|
|
85
|
-
|
|
86
|
-
owner = aggregate
|
|
87
|
-
path[0...-1].each do |entity_name|
|
|
88
|
-
owner = owner.entities.find { |entity| entity.hecks_name == entity_name }
|
|
89
|
-
return nil unless owner
|
|
90
|
-
end
|
|
91
|
-
owner.query(path.last)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# "Domain.report_name" — the bare read-model form `Dispatcher#query`
|
|
95
|
-
# routes by the absence of "::".
|
|
96
|
-
def find_read_model(bluebooks, verb)
|
|
97
|
-
domain, name = verb.split(".", 2)
|
|
98
|
-
bluebooks[domain]&.read_models&.find { |model| model.query_name.to_s == name.to_s }
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def query_constructs(query)
|
|
102
|
-
spec = query.to_h
|
|
103
|
-
constructs = QUERY_OPTION_KEYS.select { |key| present?(spec[key]) }.map(&:to_s)
|
|
104
|
-
constructs << "null_semantics" if spec[:null_semantics] && spec[:null_semantics] != { mode: "native" }
|
|
105
|
-
constructs << "no_wheres" if Array(spec[:wheres]).empty?
|
|
106
|
-
constructs.concat(where_constructs(Array(spec[:wheres])))
|
|
107
|
-
constructs.uniq.sort
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def read_model_constructs(model)
|
|
111
|
-
spec = model.to_h
|
|
112
|
-
constructs = READ_MODEL_OPTION_KEYS.select { |key| present?(spec[key]) }.map(&:to_s)
|
|
113
|
-
constructs << "null_semantics" if spec[:null_semantics] && spec[:null_semantics] != { mode: "native" }
|
|
114
|
-
constructs << "rootless" if model.reference_target.nil?
|
|
115
|
-
constructs.concat(where_constructs(Array(spec[:wheres])))
|
|
116
|
-
constructs.uniq.sort
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# The where-clause families `query_where_skip_reason` refuses by:
|
|
120
|
-
# a `/` hop through a reference, a `.` walk into a nested field
|
|
121
|
-
# (an entity-scoped or value-object path codegen may not resolve),
|
|
122
|
-
# `none_in_state` (admitted by the vocabulary, deliberately not
|
|
123
|
-
# generated), and any literal (non-Symbol) value, whose true wire
|
|
124
|
-
# type codegen may not recover from the IR.
|
|
125
|
-
def where_constructs(wheres)
|
|
126
|
-
wheres.flat_map do |where|
|
|
127
|
-
field = where[:field].to_s
|
|
128
|
-
value = where[:value].to_s
|
|
129
|
-
families = []
|
|
130
|
-
families << "reference_hop_where" if field.include?("/")
|
|
131
|
-
families << "where_nested_field" if field.include?(".")
|
|
132
|
-
families << "where_none_in_state" if where[:op].to_s == "none_in_state"
|
|
133
|
-
families << "where_literal" unless value.start_with?(":")
|
|
134
|
-
families
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def present?(value)
|
|
139
|
-
return false if value.nil? || value == false
|
|
140
|
-
return value.any? if value.respond_to?(:empty?)
|
|
141
|
-
|
|
142
|
-
true
|
|
143
|
-
end
|
|
144
51
|
end
|
|
145
52
|
end
|
|
146
53
|
end
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Fuzzing
|
|
3
|
-
#
|
|
3
|
+
# How hard one sweep fuzzes, as a function of `Target.clean_streak`
|
|
4
4
|
# (qa/bluebook/quality_control.bluebook — that attribute's own comment
|
|
5
5
|
# says what the streak means). The sibling of `RotationPriority`: the
|
|
6
6
|
# ledger holds the number, `QualityControlDials::WIDENING_TIERS` holds
|
|
7
7
|
# the policy, and this module is where the two meet — a pure function
|
|
8
8
|
# of both, nothing else read.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# Why this is not in `bin/qa_sweep` any more. It was — `WIDENING_TIERS`
|
|
11
11
|
# and `widen_for_streak` lived at the top of that script, which meant
|
|
12
12
|
# policy data lived in a script (unlike every other dial, which lives
|
|
13
13
|
# in the bluebook a human edits and reviews) and duplicated itself as
|
|
@@ -15,7 +15,7 @@ module Hecks
|
|
|
15
15
|
# and `bin/qa_sweep` calls it the way it already calls
|
|
16
16
|
# `RotationPriority.pick`.
|
|
17
17
|
#
|
|
18
|
-
#
|
|
18
|
+
# **Pure, deliberately** — the same discipline `RotationPriority` keeps:
|
|
19
19
|
# same streak in, same `[seeds, steps]` out, every time, which is what
|
|
20
20
|
# lets a human predict what a given sweep is about to do before it
|
|
21
21
|
# runs one. `tiers:` defaults to the dial but is a plain argument, so
|
|
@@ -26,7 +26,7 @@ module Hecks
|
|
|
26
26
|
module SweepDepth
|
|
27
27
|
module_function
|
|
28
28
|
|
|
29
|
-
#
|
|
29
|
+
# The fallback table, for a boot with no `QualityControlDials` at all
|
|
30
30
|
# — the same shape and the same three rows the dial ships with, so a
|
|
31
31
|
# dial-less ledger fuzzes exactly as a dialled one does by default.
|
|
32
32
|
# Never read when the dial exists; `bin/qa_sweep` passes the dial in.
|
|
@@ -40,6 +40,14 @@ module Hecks
|
|
|
40
40
|
# streak does not exceed wins — rows are read in order, so the table
|
|
41
41
|
# must be ascending, and the last row's `Float::INFINITY` is what
|
|
42
42
|
# makes it the ceiling rather than a gap.
|
|
43
|
+
#
|
|
44
|
+
# @param streak [Integer] `Target.clean_streak`, the number of consecutive clean sweeps
|
|
45
|
+
# @param tiers [Array<Hash{Symbol => Numeric}>] ascending `{ upto:, seeds:, steps: }` rows;
|
|
46
|
+
# defaults to `DEFAULT_TIERS`
|
|
47
|
+
# @return [Array(Integer, Integer)] `[seeds, steps]` for the first row `streak` fits under
|
|
48
|
+
# @raise [ArgumentError] if `streak` is negative
|
|
49
|
+
# @raise [ArgumentError] if no row in `tiers` covers `streak` (the last row must declare
|
|
50
|
+
# `upto: Float::INFINITY`)
|
|
43
51
|
def for_streak(streak, tiers: DEFAULT_TIERS)
|
|
44
52
|
raise ArgumentError, "streak must not be negative" if streak.negative?
|
|
45
53
|
|