hecks 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +51 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +301 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -20
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +124 -0
- data/lib/hecks/fuzzing/form_census.rb +199 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
- data/lib/hecks/fuzzing/properties/guards.rb +129 -26
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
- data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +84 -39
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +224 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +676 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +39 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
- data/lib/hecks/fuzzing/value_generator.rb +66 -14
- data/lib/hecks/fuzzing.rb +11 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +89 -21
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +250 -0
- data/lib/hecks/projections/glossary/markdown.rb +105 -0
- data/lib/hecks/projections/glossary/mermaid.rb +110 -0
- data/lib/hecks/projections/glossary/page.css +271 -0
- data/lib/hecks/projections/glossary/page.js +72 -0
- data/lib/hecks/projections/glossary/sections.rb +17 -0
- data/lib/hecks/projections/glossary/sentences.rb +205 -0
- data/lib/hecks/projections/glossary.rb +214 -286
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +15 -22
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +132 -24
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +94 -19
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +282 -46
- data/lib/hecks/runtime/entity_interpreter.rb +160 -69
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +111 -56
- data/lib/hecks/runtime/reaction_invocation.rb +76 -9
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -112
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +56 -27
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -88
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +93 -56
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +379 -226
- data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
- data/lib/hecks/runtime/value.rb +28 -23
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +207 -5
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +43 -2
|
@@ -26,20 +26,11 @@ module Hecks
|
|
|
26
26
|
queries << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{query.name}",
|
|
27
27
|
query: query, aggregate: aggregate }
|
|
28
28
|
end
|
|
29
|
-
aggregate
|
|
30
|
-
entity.commands.each do |command|
|
|
31
|
-
entity_commands << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{command.hecks_name}",
|
|
32
|
-
command: command, aggregate: aggregate, entity: entity }
|
|
33
|
-
end
|
|
34
|
-
entity.queries.each do |query|
|
|
35
|
-
entity_queries << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{query.name}",
|
|
36
|
-
query: query, aggregate: aggregate, entity: entity }
|
|
37
|
-
end
|
|
38
|
-
end
|
|
29
|
+
catalog_entities(domain_name, aggregate, entity_commands, entity_queries)
|
|
39
30
|
end
|
|
40
31
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
32
|
+
# **The bare domain form** — "Domain.report_name", no "::" — the
|
|
33
|
+
# same shape `Dispatcher#query` itself branches on to route a
|
|
43
34
|
# read-model ask apart from an aggregate query. A report was
|
|
44
35
|
# never in this catalog at all before: `aggregation_matches_
|
|
45
36
|
# recompute` (count/median) has only ever been exercised by
|
|
@@ -56,41 +47,121 @@ module Hecks
|
|
|
56
47
|
{ creating: creating, instance: instance, entity_commands: entity_commands,
|
|
57
48
|
queries: queries, entity_queries: entity_queries, read_models: read_models,
|
|
58
49
|
populators: populators(runtime),
|
|
50
|
+
# Every `role "..."` any command in the boot declares — the
|
|
51
|
+
# "wrong hat" pool the caller draw's `mismatched` shape picks
|
|
52
|
+
# from (adversary.rb `other_role`).
|
|
53
|
+
roles: (creating + instance + entity_commands).filter_map { |e| e[:command].role }
|
|
54
|
+
.map(&:to_s).reject(&:empty?).uniq.sort,
|
|
55
|
+
# The grant verb every loaded authorization provider declares
|
|
56
|
+
# (`provides "authorization", grant: ...`) — what the caller
|
|
57
|
+
# draw steers at a declared role and records as a real grant.
|
|
58
|
+
# Read off the declaration, never the literal Governance name.
|
|
59
|
+
grant_verbs: runtime.registry.authorization_providers
|
|
60
|
+
.filter_map { |chapter| chapter.provided_verb(Bluebook::Capabilities::AUTHORIZATION, :grant) }
|
|
61
|
+
.sort,
|
|
59
62
|
# Which aggregates this corpus can actually make one of — the ones
|
|
60
63
|
# `satisfiable?` is entitled to wait for.
|
|
61
64
|
creatable: creating.to_set { |entry| entry[:aggregate].hecks_name } }
|
|
62
65
|
end
|
|
63
66
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
+
# Every entity, at every depth — `Card` nested inside `Board`
|
|
68
|
+
# inside `Workspace` (qa/stress_domains/nested_pieces) walks in
|
|
69
|
+
# as `chain: [Board, Card]`, the exact hop list
|
|
70
|
+
# `EntityInterpreter::Resolution.of` resolves the dotted verb
|
|
71
|
+
# back into. Before this walk existed the catalog only ever read
|
|
72
|
+
# `aggregate.entities` one level down, so a two-hop entity
|
|
73
|
+
# command (BUG#11's whole class) could never be generated at all
|
|
74
|
+
# — the one shape the differential harness most needed to reach
|
|
75
|
+
# was structurally absent from every sequence it ever produced.
|
|
76
|
+
# `entity:` stays the last hop (what every existing reader means
|
|
77
|
+
# by "the entity"); `chain:` is the whole path.
|
|
78
|
+
#
|
|
79
|
+
# Entity queries stay one hop deep, exactly as before — a nested
|
|
80
|
+
# entity's query has no established wire spelling this generator
|
|
81
|
+
# can vouch for, and nothing in the corpus declares one.
|
|
82
|
+
def catalog_entities(domain_name, aggregate, entity_commands, entity_queries)
|
|
83
|
+
each_entity_chain(aggregate) do |chain|
|
|
84
|
+
entity = chain.last
|
|
85
|
+
path = chain.map(&:hecks_name).join(".")
|
|
86
|
+
entity.commands.each do |command|
|
|
87
|
+
entity_commands << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{path}.#{command.hecks_name}",
|
|
88
|
+
command: command, aggregate: aggregate, entity: entity, chain: chain }
|
|
89
|
+
end
|
|
90
|
+
next unless chain.size == 1
|
|
91
|
+
|
|
92
|
+
entity.queries.each do |query|
|
|
93
|
+
entity_queries << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{path}.#{query.name}",
|
|
94
|
+
query: query, aggregate: aggregate, entity: entity }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Depth-first, parents before children, in declaration order — so
|
|
100
|
+
# the depth-1 entries land in `entity_commands` in exactly the
|
|
101
|
+
# order they always did (a pinned seed's picker pool is the same
|
|
102
|
+
# pool it was), and a nested entity's own entries follow its
|
|
103
|
+
# parent's.
|
|
104
|
+
def each_entity_chain(owner, chain = [], &block)
|
|
105
|
+
owner.entities.each do |entity|
|
|
106
|
+
path = chain + [entity]
|
|
107
|
+
yield path
|
|
108
|
+
each_entity_chain(entity, path, &block)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Which command, on which owner, appends to which entity list — so a
|
|
113
|
+
# successful dispatch can predict the identity the element it just
|
|
114
|
+
# added landed on. Entity#identified_by is filled by `Array(current).size + 1`
|
|
67
115
|
# (CommandInterpreter#entity_element) when the append's own field
|
|
68
116
|
# mapping doesn't already assign it — the common case, predicted here.
|
|
69
117
|
# A domain whose append explicitly assigns identity through a mapped
|
|
70
|
-
# argument is covered too, without guessing: whatever value
|
|
71
|
-
# generator supplied for that argument at dispatch time
|
|
118
|
+
# argument is covered too, without guessing: whatever value this
|
|
119
|
+
# generator supplied for that argument at dispatch time is the
|
|
72
120
|
# identity, and gets recorded directly (see `record_outcome`).
|
|
121
|
+
#
|
|
122
|
+
# `owner_chain:` — `[]` for an aggregate-level append (`Board.AddList`,
|
|
123
|
+
# `Folder.AddSlip`), the entity path for an entity-level one
|
|
124
|
+
# (`Board.AddCard` appending into `Board.cards`, owner_chain
|
|
125
|
+
# `[Board]`) — so `record_outcome` can key the appended element
|
|
126
|
+
# under the exact parent-plus-hops it landed beneath.
|
|
127
|
+
# `identity_arguments:` — every identity head the mapping sources
|
|
128
|
+
# from a command argument (a composite entity identity has several),
|
|
129
|
+
# what the adversarial duplicate-identity mutation replays;
|
|
130
|
+
# `identity_argument:` stays the single-head reading the existing
|
|
131
|
+
# auto-mint prediction already keys on.
|
|
73
132
|
def populators(runtime)
|
|
74
133
|
runtime.registry.bluebooks.each_value.flat_map do |bluebook|
|
|
75
134
|
bluebook.aggregates.flat_map do |aggregate|
|
|
76
|
-
aggregate
|
|
77
|
-
|
|
78
|
-
next unless append
|
|
79
|
-
|
|
80
|
-
list_attribute = aggregate.attribute(append.target)
|
|
81
|
-
next unless list_attribute&.list?
|
|
82
|
-
|
|
83
|
-
entity = aggregate.entities.find { |candidate| candidate.hecks_name == list_attribute.type.to_s }
|
|
84
|
-
next unless entity
|
|
135
|
+
owners = [[aggregate, []]]
|
|
136
|
+
each_entity_chain(aggregate) { |chain| owners << [chain.last, chain] }
|
|
85
137
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
{ command: command, aggregate: aggregate, entity: entity,
|
|
89
|
-
identity_field: identity_field, identity_argument: mapped.is_a?(Symbol) ? mapped : nil }
|
|
138
|
+
owners.flat_map do |owner, chain|
|
|
139
|
+
owner.commands.filter_map { |command| populator_for(aggregate, owner, chain, command) }
|
|
90
140
|
end
|
|
91
141
|
end
|
|
92
142
|
end
|
|
93
143
|
end
|
|
144
|
+
|
|
145
|
+
def populator_for(aggregate, owner, chain, command)
|
|
146
|
+
append = command.mutations.find { |mutation| mutation.op == :append }
|
|
147
|
+
return unless append
|
|
148
|
+
|
|
149
|
+
list_attribute = owner.attribute(append.target)
|
|
150
|
+
return unless list_attribute&.list?
|
|
151
|
+
|
|
152
|
+
entity = owner.entities.find { |candidate| candidate.hecks_name == list_attribute.type.to_s }
|
|
153
|
+
return unless entity
|
|
154
|
+
|
|
155
|
+
identity_field = entity.identified_by
|
|
156
|
+
mapped = identity_field && append.source[identity_field]
|
|
157
|
+
identity_arguments = entity.identity_heads.filter_map do |head|
|
|
158
|
+
source = append.source[head]
|
|
159
|
+
source if source.is_a?(Symbol)
|
|
160
|
+
end
|
|
161
|
+
{ command: command, aggregate: aggregate, owner: owner, owner_chain: chain, entity: entity,
|
|
162
|
+
identity_field: identity_field, identity_argument: mapped.is_a?(Symbol) ? mapped : nil,
|
|
163
|
+
identity_arguments: identity_arguments }
|
|
164
|
+
end
|
|
94
165
|
end
|
|
95
166
|
end
|
|
96
167
|
end
|
|
@@ -15,11 +15,12 @@ module Hecks
|
|
|
15
15
|
parent_scalar = identity_scalar_of(aggregate, args)
|
|
16
16
|
|
|
17
17
|
@known_ids[aggregate.hecks_name] << parent_scalar if entry[:entity].nil? && entry[:command].creates?
|
|
18
|
+
record_grant(args) if catalog[:grant_verbs].include?(entry[:verb])
|
|
18
19
|
|
|
19
|
-
populator = catalog
|
|
20
|
+
populator = populator_for_entry(catalog, entry)
|
|
20
21
|
return unless populator
|
|
21
22
|
|
|
22
|
-
key =
|
|
23
|
+
key = append_pool_key(populator, args)
|
|
23
24
|
|
|
24
25
|
# The auto-minted case (CommandInterpreter#entity_element): the
|
|
25
26
|
# element just landed at count-so-far + 1. The explicit case: this
|
|
@@ -33,20 +34,66 @@ module Hecks
|
|
|
33
34
|
(@entity_known_ids[key].size + 1).to_s
|
|
34
35
|
end
|
|
35
36
|
@entity_known_ids[key] << new_id
|
|
37
|
+
|
|
38
|
+
# The caller-supplied identity tuple, kept whole — every mapped
|
|
39
|
+
# identity argument and the exact value this step offered for it
|
|
40
|
+
# — so the adversarial duplicate-identity mutation (BUG#13's own
|
|
41
|
+
# shape, `Folder.AddSlip` twice under the same `reference`) can
|
|
42
|
+
# offer it again later against the same parent, composite
|
|
43
|
+
# identities included (BUG#13's fix explicitly did not cover
|
|
44
|
+
# those; this is how a sequence gets to ask).
|
|
45
|
+
return if populator[:identity_arguments].empty?
|
|
46
|
+
|
|
47
|
+
@appended_identities[key] << populator[:identity_arguments].to_h { |name| [name.to_s, args[name.to_s]] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A grant this sequence made for real — the authorization provider's
|
|
51
|
+
# declared `grant:` verb (`Governance::RoleAssignment.Assign` in
|
|
52
|
+
# every boot today) succeeded with these exact args, so `actor_id` now holds
|
|
53
|
+
# `role_name` in this boot's own store, on both replay sides. The
|
|
54
|
+
# `actor_known` caller shape (adversary.rb) replays that identity
|
|
55
|
+
# against a command gated on the same role: the one way a
|
|
56
|
+
# generated step reaches `holds_role?`'s authorized branch rather
|
|
57
|
+
# than only its refusing one.
|
|
58
|
+
def record_grant(args)
|
|
59
|
+
role = ValueGenerator.scalar_of(args["role_name"]).to_s
|
|
60
|
+
actor = ValueGenerator.scalar_of(args["actor_id"]).to_s
|
|
61
|
+
@granted[role] << actor unless role.empty? || actor.empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The pool an appended element lands in — the aggregate's own
|
|
65
|
+
# identity, then one scalar per owning hop (an aggregate-level
|
|
66
|
+
# append has none; `Board.AddCard` has `Board`'s own `number`,
|
|
67
|
+
# read straight back off the args this step addressed it by).
|
|
68
|
+
def append_pool_key(populator, args)
|
|
69
|
+
parent_scalar = identity_scalar_of(populator[:aggregate], args)
|
|
70
|
+
owner_scalars = populator[:owner_chain].map do |piece|
|
|
71
|
+
ValueGenerator.scalar_of(args[(piece.identified_by || :id).to_s])
|
|
72
|
+
end
|
|
73
|
+
entity_pool_key(populator[:aggregate].hecks_name,
|
|
74
|
+
populator[:owner_chain].map(&:hecks_name) + [populator[:entity].hecks_name],
|
|
75
|
+
[parent_scalar] + owner_scalars)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# `"Agg.Board#w1"` for a depth-1 pool — byte-identical to the key
|
|
79
|
+
# this always used, so nothing a pinned seed draws from moves —
|
|
80
|
+
# and `"Agg.Board.Card#w1/1"` one hop deeper.
|
|
81
|
+
def entity_pool_key(aggregate_name, chain_names, scalars)
|
|
82
|
+
"#{aggregate_name}.#{chain_names.join('.')}##{scalars.join('/')}"
|
|
36
83
|
end
|
|
37
84
|
|
|
38
|
-
#
|
|
85
|
+
# The scalar this step's own aggregate identity resolves to, from the
|
|
39
86
|
# args a creating (or entity-populating) command actually
|
|
40
|
-
# dispatched — a
|
|
87
|
+
# dispatched — a single declared head (or the untyped default
|
|
41
88
|
# `:id`) reads straight off its own top-level arg the way this
|
|
42
89
|
# always has (`ValueGenerator.scalar_of`, opening the identity value
|
|
43
|
-
# object). A genuinely
|
|
90
|
+
# object). A genuinely composite identity (`composite_identity?`,
|
|
44
91
|
# shared with StepBuilder#add_identity! — see its own comment) has
|
|
45
92
|
# no such top-level arg at all: `add_identity!` deliberately leaves
|
|
46
93
|
# a composite creating command's own individually-declared fields
|
|
47
94
|
# alone rather than forcing a synthetic `id` neither the bluebook
|
|
48
95
|
# nor the command ever declared, so this joins those fields itself,
|
|
49
|
-
# in the
|
|
96
|
+
# in the same declaration order `Runtime::Identity.of` joins them
|
|
50
97
|
# at dispatch (`Naming.identity`, `Naming::IDENTITY_JOIN`) — which
|
|
51
98
|
# is exactly what makes the result usable later as the bare `id:`
|
|
52
99
|
# a composite Revoke/act-again command expects (`Identity.from`'s
|
|
@@ -68,8 +115,8 @@ module Hecks
|
|
|
68
115
|
pool.sample(random: @random)
|
|
69
116
|
end
|
|
70
117
|
|
|
71
|
-
def pick_entity_known(
|
|
72
|
-
pool = @entity_known_ids[
|
|
118
|
+
def pick_entity_known(key)
|
|
119
|
+
pool = @entity_known_ids[key]
|
|
73
120
|
return ValueGenerator.random_id(@random) if pool.empty? || @random.rand < ValueGenerator::INVALID_REFERENCE_PROBABILITY
|
|
74
121
|
|
|
75
122
|
pool.sample(random: @random)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Fuzzing
|
|
3
3
|
class SequenceGenerator
|
|
4
|
-
# Which step to try next: eligibility (what is
|
|
5
|
-
# state so far) and weighting (what is
|
|
4
|
+
# Which step to try next: eligibility (what is possible from the
|
|
5
|
+
# state so far) and weighting (what is likely to reach somewhere
|
|
6
6
|
# new).
|
|
7
7
|
module Picker
|
|
8
8
|
private
|
|
@@ -16,15 +16,26 @@ module Hecks
|
|
|
16
16
|
|
|
17
17
|
makers = catalog[:creating].select { |entry| satisfiable?(catalog, entry) }
|
|
18
18
|
pool = rest + makers.flat_map { |entry| [entry] * creating_weight(rest.size) }
|
|
19
|
+
pool += deep_entity_bias(rest) if adversarial?
|
|
19
20
|
|
|
20
21
|
steer(pool).sample(random: @random)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
#
|
|
24
|
+
# BUG#11's own preference (adversary.rb) — an entity command two
|
|
25
|
+
# or more hops deep is weighted up the same way an unexercised
|
|
26
|
+
# verb is, only in adversarial mode: the eligibility rules above
|
|
27
|
+
# still decide what is possible, and a default-mode pool is
|
|
28
|
+
# exactly the pool it always was.
|
|
29
|
+
def deep_entity_bias(rest)
|
|
30
|
+
deep = rest.select { |entry| (entry[:chain] || []).size >= Adversary::DEEP_ENTITY_DEPTH }
|
|
31
|
+
deep * Adversary::DEEP_ENTITY_WEIGHT
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# While there is nothing to find, making something is the only useful move.
|
|
24
35
|
#
|
|
25
36
|
# A flat weight is right once the domain has records in it, and badly
|
|
26
37
|
# wrong before: banking declares ten queries, and from an empty store
|
|
27
|
-
# exactly
|
|
38
|
+
# exactly one creating command is satisfiable — Customer.Register, the
|
|
28
39
|
# verb the whole cascade waits on. Two entries against ten left runs
|
|
29
40
|
# spending their entire budget querying a store nothing had been written
|
|
30
41
|
# to ; one seed answered ten empty queries in a row and emitted nothing
|
|
@@ -43,9 +54,9 @@ module Hecks
|
|
|
43
54
|
@known_ids[entry[:aggregate].hecks_name].any? && satisfiable?(catalog, entry)
|
|
44
55
|
end
|
|
45
56
|
|
|
46
|
-
# A
|
|
57
|
+
# A rootless report (no `reference_to` at all) has nothing to wait
|
|
47
58
|
# for — it reads whole tables, the same "always eligible" position
|
|
48
|
-
# `catalog[:queries]` itself takes. A
|
|
59
|
+
# `catalog[:queries]` itself takes. A rooted one needs a real
|
|
49
60
|
# instance of its own `reference_target` to ask about first, same
|
|
50
61
|
# rule `actionable?` already gives an instance command — asking
|
|
51
62
|
# `Banking.disputed_payment_count` before any Account exists would
|
|
@@ -55,7 +66,7 @@ module Hecks
|
|
|
55
66
|
entry[:model].reference_target.nil? || @known_ids[entry[:model].reference_target].any?
|
|
56
67
|
end
|
|
57
68
|
|
|
58
|
-
# A
|
|
69
|
+
# A command that references nothing that exists cannot succeed, so it is
|
|
59
70
|
# not offered until something does.
|
|
60
71
|
#
|
|
61
72
|
# `ValueGenerator.reference_value` has no real id to hand over when its
|
|
@@ -67,7 +78,7 @@ module Hecks
|
|
|
67
78
|
# spent its whole budget being refused.
|
|
68
79
|
#
|
|
69
80
|
# This is the rule instance commands already follow — `known_ids.any?` —
|
|
70
|
-
# applied to what a command
|
|
81
|
+
# applied to what a command references rather than to what it acts on.
|
|
71
82
|
# A target no creating command in this corpus can make (a cross-domain
|
|
72
83
|
# reference, which `CommandRules#resolve_references` skips anyway) is
|
|
73
84
|
# exempt, or the whole domain would starve waiting for it.
|
|
@@ -81,14 +92,23 @@ module Hecks
|
|
|
81
92
|
# Coverage-guided rather than uniformly random : a verb this sequence has
|
|
82
93
|
# not dispatched yet is weighted up, so a run spends its budget on the
|
|
83
94
|
# commands it has not reached instead of re-rolling the ones it has. The
|
|
84
|
-
# eligibility rules above still decide what is
|
|
95
|
+
# eligibility rules above still decide what is possible — this only decides
|
|
85
96
|
# what is likely, so a verb gated behind state it does not have yet stays
|
|
86
97
|
# out of the pool entirely rather than being preferred forever.
|
|
87
98
|
def steer(pool)
|
|
88
99
|
fresh = pool.reject { |entry| @exercised.include?(entry[:verb]) }
|
|
89
|
-
|
|
100
|
+
steered = fresh.empty? ? pool : pool + (fresh * UNEXERCISED_WEIGHT)
|
|
101
|
+
favor(steered)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# `favor:` (CoverageCampaign's rare verbs) — weighted up when
|
|
105
|
+
# eligible, never made eligible. An empty favor list returns the
|
|
106
|
+
# pool untouched, so an unguided seed draws exactly what it did.
|
|
107
|
+
def favor(pool)
|
|
108
|
+
return pool if @favor.empty?
|
|
90
109
|
|
|
91
|
-
pool
|
|
110
|
+
favored = pool.uniq.select { |entry| @favor.include?(entry[:verb]) }
|
|
111
|
+
pool + (favored * SequenceGenerator::FAVOR_WEIGHT)
|
|
92
112
|
end
|
|
93
113
|
end
|
|
94
114
|
end
|
|
@@ -20,17 +20,17 @@ module Hecks
|
|
|
20
20
|
{ "query" => entry[:verb], "args" => args }
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# A
|
|
23
|
+
# A report ask — the bare domain form `entry[:verb]` already carries
|
|
24
24
|
# ("Domain.report_name", no "::"), so `Dispatcher#query` routes it
|
|
25
25
|
# to the read model rather than an aggregate query. A `ReadModel`
|
|
26
26
|
# has no declared `.attributes` the way a `Query` does — its own
|
|
27
|
-
# argument surface is exactly
|
|
27
|
+
# argument surface is exactly one key, `reference_name`, and only
|
|
28
28
|
# for a rooted model (`read_model_actionable?` already gated a
|
|
29
29
|
# rootless one straight into eligibility with nothing to supply).
|
|
30
|
-
# A
|
|
31
|
-
# refuse_object_reference` explicitly
|
|
30
|
+
# A bare scalar, not `identity_shaped` — `ReadModelInterpreter#
|
|
31
|
+
# refuse_object_reference` explicitly rejects a Hash/Value offered
|
|
32
32
|
# here (this is the one place in the whole generator where the
|
|
33
|
-
# subject's own identity must
|
|
33
|
+
# subject's own identity must not be wrapped the way a command
|
|
34
34
|
# argument's would be).
|
|
35
35
|
def build_read_model_step(runtime, entry)
|
|
36
36
|
model = entry[:model]
|
|
@@ -40,21 +40,72 @@ module Hecks
|
|
|
40
40
|
{ "query" => entry[:verb], "args" => args }
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# The adversarial layer sits here, and only here — after the step's
|
|
44
|
+
# arguments and identity are fully built, before the one real
|
|
45
|
+
# dispatch this generator makes to learn what the step did, and
|
|
46
|
+
# before the step is returned as corpus data. That ordering is
|
|
47
|
+
# the whole contract: the mutated `args` are what this generator's
|
|
48
|
+
# own inline dispatch sees (so `known_ids` tracking reflects what
|
|
49
|
+
# actually happened), and they are the bytes `Fuzzing::Replay`
|
|
50
|
+
# later hands Ruby's runtime and the bytes `JSON.generate({steps:
|
|
51
|
+
# ...})` hands the compiled Rust binary — one step, one payload,
|
|
52
|
+
# both engines. Nothing downstream of this method can tell a
|
|
53
|
+
# mutated step from an ordinary one except by reading the
|
|
54
|
+
# `"adversarial"` metadata it carries (see adversary.rb).
|
|
55
|
+
#
|
|
56
|
+
# Two more draws sit right after the mutation, in a fixed order —
|
|
57
|
+
# a caller (`caller_draw!`, adversary.rb: `role:`/`actor_id:` on
|
|
58
|
+
# the step, bound around this one dispatch exactly the way
|
|
59
|
+
# `Fuzzing::Replay` and `kernel/cli.rs` will later bind it) and
|
|
60
|
+
# then the dry-run coin (`{"dry_run": verb}` instead of
|
|
61
|
+
# `{"verb": verb}` — `Dispatcher#dry_run?` here, the same door on
|
|
62
|
+
# both replay sides). Order matters for the seed contract: every
|
|
63
|
+
# draw comes from the one `Random.new(seed)`, so the sequence of
|
|
64
|
+
# draws per step is what makes a seed reproducible. Both are
|
|
65
|
+
# off by default and draw nothing when off.
|
|
43
66
|
def build_command_step(runtime, catalog, entry)
|
|
44
67
|
args = args_for(entry[:command].attributes, entry[:aggregate])
|
|
45
68
|
add_identity!(args, entry)
|
|
69
|
+
steer_grant!(args, entry, catalog)
|
|
70
|
+
mutations = adversarial_mutations!(args, entry, catalog)
|
|
71
|
+
caller, caller_note = caller_draw!(entry, catalog)
|
|
72
|
+
mutations << caller_note if caller_note
|
|
73
|
+
@state_before = state_before(runtime, entry, args)
|
|
46
74
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
step =
|
|
76
|
+
if dry_run_draw?
|
|
77
|
+
safe_call { as_caller(caller) { runtime.dry_run?(entry[:verb], **symbolize(args)) } }
|
|
78
|
+
{ "dry_run" => entry[:verb], "args" => args }
|
|
79
|
+
else
|
|
80
|
+
outcome = safe_call { as_caller(caller) { runtime.dispatch_flat(entry[:verb], symbolize(args)) } }
|
|
81
|
+
if outcome
|
|
82
|
+
record_outcome(catalog, entry, args)
|
|
83
|
+
@event_count += outcome.events.length
|
|
84
|
+
end
|
|
85
|
+
{ "verb" => entry[:verb], "args" => args }
|
|
86
|
+
end
|
|
87
|
+
step.merge!(caller) if caller
|
|
88
|
+
step["adversarial"] = mutations unless mutations.empty?
|
|
89
|
+
step
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# No RNG draw at all when the fraction is zero — the same "off is
|
|
93
|
+
# byte-identical" contract `adversarial?` keeps.
|
|
94
|
+
def dry_run_draw? = @dry_run.positive? && @random.rand < @dry_run
|
|
95
|
+
|
|
96
|
+
# `Hecks.as_caller` for exactly this block, or a bare yield — the
|
|
97
|
+
# same binding `Fuzzing::Replay` makes from the step's own keys
|
|
98
|
+
# later, so the generator's inline dispatch and both replays see
|
|
99
|
+
# one caller.
|
|
100
|
+
def as_caller(caller, &)
|
|
101
|
+
return yield unless caller
|
|
102
|
+
|
|
103
|
+
Hecks.as_caller(role: caller["role"], actor_id: caller["actor_id"], &)
|
|
53
104
|
end
|
|
54
105
|
|
|
55
106
|
def args_for(attributes, aggregate)
|
|
56
107
|
args = attributes.each_with_object({}) do |attribute, built|
|
|
57
|
-
#
|
|
108
|
+
# An optional argument is sometimes not given, and that is an
|
|
58
109
|
# ordinary payload rather than a damaged one — see
|
|
59
110
|
# OPTIONAL_OMITTED_PROBABILITY for why this cannot live in
|
|
60
111
|
# `malform` below and what it was costing while it did not
|
|
@@ -63,13 +114,13 @@ module Hecks
|
|
|
63
114
|
|
|
64
115
|
if attribute.list?
|
|
65
116
|
value = list_value_for(attribute, aggregate)
|
|
66
|
-
# A list-of-
|
|
117
|
+
# A list-of-entity command attribute has no real example
|
|
67
118
|
# anywhere in this repo's domains — every entity-owned list is
|
|
68
119
|
# populated via a per-element append command instead, never a
|
|
69
120
|
# whole-list command argument — so `list_value_for` (below)
|
|
70
121
|
# answers `nil` for one rather than guessing at an entity's own
|
|
71
122
|
# shape, and this step still skips it exactly as it always
|
|
72
|
-
# has. A list-of-
|
|
123
|
+
# has. A list-of-value-object attribute (`ConsoleSettings::
|
|
73
124
|
# Collection.ReplaceColumns`' own `columns`, `list_of(Column)`)
|
|
74
125
|
# is the real, previously-unfuzzable case this now covers —
|
|
75
126
|
# `sets :columns` imports the owner aggregate's own declared
|
|
@@ -87,9 +138,9 @@ module Hecks
|
|
|
87
138
|
malform(args, attributes, aggregate)
|
|
88
139
|
end
|
|
89
140
|
|
|
90
|
-
# A `list_of`
|
|
141
|
+
# A `list_of` attribute's own value — an array of independently
|
|
91
142
|
# generated elements, each shaped exactly the way a bare (non-list)
|
|
92
|
-
# attribute of the
|
|
143
|
+
# attribute of the same declared element type already is
|
|
93
144
|
# (`ValueGenerator.value_for`), since `list_of(X)`'s own element
|
|
94
145
|
# coercion is `X`'s ordinary shape repeated, not a different one
|
|
95
146
|
# (`Attribute#type` is already unwrapped from `list_of(...)` at
|
|
@@ -107,12 +158,12 @@ module Hecks
|
|
|
107
158
|
Array.new(@random.rand(0..3)) { ValueGenerator.value_for(attribute, aggregate, random: @random, known_ids: @known_ids) }
|
|
108
159
|
end
|
|
109
160
|
|
|
110
|
-
#
|
|
161
|
+
# One malformation at a time, and usually none. A step whose payload is
|
|
111
162
|
# wrong in three ways only ever proves which check runs first ; wrong in
|
|
112
163
|
# exactly one way names the check that fired. And the rate stays low on
|
|
113
164
|
# purpose — a corrupted step is almost always refused, a sequence of
|
|
114
165
|
# refusals reaches no state at all, and bin/fuzz already counts those as
|
|
115
|
-
#
|
|
166
|
+
# silent rather than scoring them.
|
|
116
167
|
def malform(args, attributes, aggregate)
|
|
117
168
|
return args if args.empty? || @random.rand >= MALFORMED_ARGUMENT_PROBABILITY
|
|
118
169
|
|
|
@@ -123,12 +174,12 @@ module Hecks
|
|
|
123
174
|
end
|
|
124
175
|
end
|
|
125
176
|
|
|
126
|
-
#
|
|
177
|
+
# Never the identity. A creating command with no id auto-mints one, and
|
|
127
178
|
# a minted id is deliberately unreproducible — a random hex, never a
|
|
128
179
|
# guessable counter — so dropping it manufactures a step whose outcome
|
|
129
180
|
# cannot be replayed and says nothing about the runtime's behaviour.
|
|
130
181
|
# Every step in the hand-written corpus
|
|
131
|
-
# supplies an id for the same reason. Whether an auto-minted id
|
|
182
|
+
# supplies an id for the same reason. Whether an auto-minted id ought to
|
|
132
183
|
# be reproducible is a real question, but it is not one a payload fuzzer
|
|
133
184
|
# can ask.
|
|
134
185
|
def drop_one(args, aggregate)
|
|
@@ -154,23 +205,37 @@ module Hecks
|
|
|
154
205
|
if entry[:entity]
|
|
155
206
|
parent_scalar = pick_known(aggregate.hecks_name)
|
|
156
207
|
args[parent_key] = identity_shaped(aggregate, aggregate.identified_by, parent_scalar, aggregate)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
208
|
+
# One identity per hop, each drawn from the pool its own
|
|
209
|
+
# parent-plus-hops landed elements in (`entity_pool_key`) —
|
|
210
|
+
# a depth-1 chain draws exactly what it always did; a
|
|
211
|
+
# `Board.Card` chain draws a Board under this Workspace, then
|
|
212
|
+
# a Card under that Board. Flat args, one head per hop, is
|
|
213
|
+
# the legacy addressing `EntityElement#locate_chain` reads
|
|
214
|
+
# (`args[head]` per identity path); the routed `to: {
|
|
215
|
+
# aggregate:, entities: [...] }` spelling is an adversarial
|
|
216
|
+
# shape layered on top (adversary.rb), never the default.
|
|
217
|
+
scalars = [parent_scalar]
|
|
218
|
+
names = []
|
|
219
|
+
entry[:chain].each do |piece|
|
|
220
|
+
names << piece.hecks_name
|
|
221
|
+
scalar = pick_entity_known(entity_pool_key(aggregate.hecks_name, names, scalars))
|
|
222
|
+
args[(piece.identified_by || :id).to_s] = identity_shaped(piece, piece.identified_by, scalar, aggregate)
|
|
223
|
+
scalars << scalar
|
|
224
|
+
end
|
|
160
225
|
elsif entry[:command].creates?
|
|
161
|
-
# A
|
|
162
|
-
#
|
|
226
|
+
# A composite identity (`identified_by` answering nil with more
|
|
227
|
+
# than one declared path — Behaviour::Identified's own "a
|
|
163
228
|
# composite has no single head" comment) supplies every one of
|
|
164
229
|
# its parts as its own ordinary, individually-declared command
|
|
165
230
|
# attribute already — `RoleAssignment::Assign` takes actor_id/
|
|
166
231
|
# role_name/starts_at directly, `args_for` (above) already
|
|
167
232
|
# generated all three. Forcing a synthetic top-level `id` here
|
|
168
|
-
# too — this codebase's own fallback for the
|
|
233
|
+
# too — this codebase's own fallback for the single-key and the
|
|
169
234
|
# genuinely untyped (`identity_paths.empty?`, no `identified_by`
|
|
170
235
|
# declared at all) cases — hands a composite creating command
|
|
171
236
|
# an argument it never declared at all, refused every time as
|
|
172
237
|
# unknown before this check existed (a creating `Assign`/`Grant`
|
|
173
|
-
# step was never anything
|
|
238
|
+
# step was never anything but refused). `identity_paths.empty?`
|
|
174
239
|
# is the untyped default (falls all the way back to a minted
|
|
175
240
|
# `:id` the runtime itself never declared as an attribute
|
|
176
241
|
# either), which still needs exactly the old minting behavior.
|
|
@@ -184,9 +249,9 @@ module Hecks
|
|
|
184
249
|
end
|
|
185
250
|
end
|
|
186
251
|
|
|
187
|
-
#
|
|
252
|
+
# True only for a genuine multi-field identity — `identified_by`
|
|
188
253
|
# returns nil both for a real composite (`identity_paths.size > 1`)
|
|
189
|
-
# and for the untyped default with
|
|
254
|
+
# and for the untyped default with no identity declared at all
|
|
190
255
|
# (`identity_paths.size == 0`, Behaviour::Identified's own
|
|
191
256
|
# `Array(@identified_by)` fallback) ; only the first of those two
|
|
192
257
|
# has its own parts already sitting in `args` as real, individually-
|
|
@@ -230,7 +295,7 @@ module Hecks
|
|
|
230
295
|
|
|
231
296
|
# A step the runtime declines is not a generator failure — it simply did
|
|
232
297
|
# not take effect, so nothing is recorded and the sequence carries on. The
|
|
233
|
-
# step still goes into the corpus, because a
|
|
298
|
+
# step still goes into the corpus, because a refusal is an answer, and
|
|
234
299
|
# its wording is pinned by the corpus.
|
|
235
300
|
#
|
|
236
301
|
# EvaluationError sits alongside the declared refusals deliberately : a
|
|
@@ -241,10 +306,36 @@ module Hecks
|
|
|
241
306
|
# generator built a step that breaks the interpreter for reasons that have
|
|
242
307
|
# nothing to do with the domain declining a payload.
|
|
243
308
|
def safe_call
|
|
244
|
-
yield
|
|
245
|
-
|
|
309
|
+
result = yield
|
|
310
|
+
@last_outcome = "ok"
|
|
311
|
+
result
|
|
312
|
+
rescue *Hecks::Runtime::DOMAIN_REFUSALS, Hecks::Bluebook::Expression::EvaluationError => e
|
|
313
|
+
@last_outcome = e.class.name.split("::").last
|
|
246
314
|
nil
|
|
247
315
|
end
|
|
316
|
+
|
|
317
|
+
# Where the addressed aggregate stood just before this dispatch — its
|
|
318
|
+
# lifecycle value, or `exists`/`absent` when it declares none. Read
|
|
319
|
+
# straight off the repository, never through dispatch, and draws
|
|
320
|
+
# nothing from the RNG, so it changes no generated byte. An identity
|
|
321
|
+
# an adversarial mutation mangled past resolving is `?`.
|
|
322
|
+
def state_before(runtime, entry, args)
|
|
323
|
+
aggregate = entry[:aggregate]
|
|
324
|
+
symbolic = symbolize(args)
|
|
325
|
+
id = Runtime::Identity.of(aggregate, symbolic) || Runtime::Identity.from(aggregate, symbolic, :id)
|
|
326
|
+
return "absent" unless id
|
|
327
|
+
|
|
328
|
+
record = runtime.registry.repository(entry[:verb].split("::").first, aggregate).find(id)
|
|
329
|
+
return "absent" unless record
|
|
330
|
+
|
|
331
|
+
lifecycle = aggregate.lifecycle
|
|
332
|
+
return "exists" unless lifecycle
|
|
333
|
+
|
|
334
|
+
key = record.state.key?(lifecycle.field) ? lifecycle.field : lifecycle.field.to_s
|
|
335
|
+
record.state[key].to_s
|
|
336
|
+
rescue StandardError
|
|
337
|
+
"?"
|
|
338
|
+
end
|
|
248
339
|
end
|
|
249
340
|
end
|
|
250
341
|
end
|