hecks 0.2.0 → 0.3.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 +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +403 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +148 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +131 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +391 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
- data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +285 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +36 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +96 -0
- data/lib/hecks/behaviors/expectations.rb +280 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
- data/lib/hecks/bluebook/assembly/marks.rb +227 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +78 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +68 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
- data/lib/hecks/bluebook/expression/projection.json +170 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +630 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +452 -0
- data/lib/hecks/bluebook/model_check.rb +318 -0
- data/lib/hecks/bluebook/pattern_subset.rb +120 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +86 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +58 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +389 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +116 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +154 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +300 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +170 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +53 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +132 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +115 -0
- data/lib/hecks/forms/record_table.rb +63 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
- data/lib/hecks/fuzzing/properties.rb +1146 -0
- data/lib/hecks/fuzzing/replay.rb +635 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
- data/lib/hecks/fuzzing/value_generator.rb +177 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +291 -0
- data/lib/hecks/grammar/expression.bluebook +417 -0
- data/lib/hecks/grammar/expression_operators.json +1152 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +437 -0
- data/lib/hecks/language/bluebook/entity.bluebook +316 -0
- data/lib/hecks/language/bluebook/policy.bluebook +172 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +313 -0
- data/lib/hecks/language/port.bluebook +92 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +121 -0
- data/lib/hecks/ports/access_control.port +4 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +4 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +4 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +4 -0
- data/lib/hecks/ports/authorization.rb +59 -0
- data/lib/hecks/ports/clock.port +4 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +4 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +4 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +4 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +4 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +102 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +18 -0
- data/lib/hecks/ports/persistence/lineage.rb +277 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +76 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +413 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +31 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +437 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +141 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +392 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +80 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +89 -0
- data/lib/hecks/query_specification/hop_path.rb +130 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/caller.rb +50 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
- data/lib/hecks/runtime/command_interpreter.rb +413 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +181 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +231 -0
- data/lib/hecks/runtime/dispatcher.rb +286 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +228 -0
- data/lib/hecks/runtime/era_check.rb +136 -0
- data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/runtime/era_guard.rb +193 -0
- data/lib/hecks/runtime/era_tamper.rb +61 -0
- data/lib/hecks/runtime/errors.rb +100 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +128 -0
- data/lib/hecks/runtime/instance.rb +120 -0
- data/lib/hecks/runtime/interpreting.rb +69 -0
- data/lib/hecks/runtime/loader.rb +143 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +254 -0
- data/lib/hecks/runtime/reaction_invocation.rb +245 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +124 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
- data/lib/hecks/runtime/registry/verification.rb +158 -0
- data/lib/hecks/runtime/registry.rb +218 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +301 -0
- data/lib/hecks/runtime/storage_shape.rb +120 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +112 -0
- data/lib/hecks/runtime/value/coercion.rb +500 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +95 -0
- data/lib/hecks/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/translation/audit/layer_one.rb +37 -0
- data/lib/hecks/translation/audit/layer_two.rb +77 -0
- data/lib/hecks/translation/audit/unfed_report.rb +42 -0
- data/lib/hecks/translation/audit.rb +70 -0
- data/lib/hecks/translation/reattest.rb +72 -0
- data/lib/hecks/translation/rule_compiler.rb +120 -0
- data/lib/hecks/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/translation/scaffold.rb +30 -0
- data/lib/hecks/translation.rb +11 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +213 -0
- data/lib/hecks.rb +118 -6
- metadata +401 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
class SequenceGenerator
|
|
4
|
+
# Everything the booted domain offers, indexed for the picker:
|
|
5
|
+
# creating vs instance commands, entity commands, queries, and the
|
|
6
|
+
# populators that predict where an appended element lands.
|
|
7
|
+
module Catalog
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def build_catalog(runtime)
|
|
11
|
+
creating, instance, entity_commands, queries, entity_queries = [], [], [], [], []
|
|
12
|
+
read_models = []
|
|
13
|
+
|
|
14
|
+
runtime.registry.bluebooks.each do |domain_name, bluebook|
|
|
15
|
+
bluebook.aggregates.each do |aggregate|
|
|
16
|
+
aggregate.commands.each do |command|
|
|
17
|
+
entry = { verb: "#{domain_name}::#{aggregate.hecks_name}.#{command.hecks_name}",
|
|
18
|
+
command: command, aggregate: aggregate }
|
|
19
|
+
(command.creates? ? creating : instance) << entry
|
|
20
|
+
end
|
|
21
|
+
aggregate.queries.each do |query|
|
|
22
|
+
queries << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{query.name}",
|
|
23
|
+
query: query, aggregate: aggregate }
|
|
24
|
+
end
|
|
25
|
+
aggregate.entities.each do |entity|
|
|
26
|
+
entity.commands.each do |command|
|
|
27
|
+
entity_commands << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{command.hecks_name}",
|
|
28
|
+
command: command, aggregate: aggregate, entity: entity }
|
|
29
|
+
end
|
|
30
|
+
entity.queries.each do |query|
|
|
31
|
+
entity_queries << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{query.name}",
|
|
32
|
+
query: query, aggregate: aggregate, entity: entity }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# THE BARE DOMAIN FORM — "Domain.report_name", no "::" — the
|
|
38
|
+
# SAME shape `Dispatcher#query` itself branches on to route a
|
|
39
|
+
# read-model ask apart from an aggregate query. A report was
|
|
40
|
+
# never in this catalog at all before: `aggregation_matches_
|
|
41
|
+
# recompute` (count/median) has only ever been exercised by
|
|
42
|
+
# hand-built specs, never a single real generated sequence,
|
|
43
|
+
# because nothing here ever asked one. `model:`, not
|
|
44
|
+
# `aggregate:` — a rootless report has no aggregate of its
|
|
45
|
+
# own to be eligible against (see picker.rb's own use of
|
|
46
|
+
# `model.reference_target`).
|
|
47
|
+
bluebook.read_models.each do |model|
|
|
48
|
+
read_models << { verb: "#{domain_name}.#{model.query_name}", model: model }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
{ creating: creating, instance: instance, entity_commands: entity_commands,
|
|
53
|
+
queries: queries, entity_queries: entity_queries, read_models: read_models,
|
|
54
|
+
populators: populators(runtime),
|
|
55
|
+
# Which aggregates this corpus can actually make one of — the ones
|
|
56
|
+
# `satisfiable?` is entitled to wait for.
|
|
57
|
+
creatable: creating.map { |entry| entry[:aggregate].hecks_name }.to_set }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Which command, on which aggregate, appends to which entity list — so a
|
|
61
|
+
# successful dispatch can predict the identity the element it just added
|
|
62
|
+
# landed on. Entity#identified_by is filled by `Array(current).size + 1`
|
|
63
|
+
# (CommandInterpreter#entity_element) when the append's own field
|
|
64
|
+
# mapping doesn't already assign it — the common case, predicted here.
|
|
65
|
+
# A domain whose append explicitly assigns identity through a mapped
|
|
66
|
+
# argument is covered too, without guessing: whatever value THIS
|
|
67
|
+
# generator supplied for that argument at dispatch time IS the
|
|
68
|
+
# identity, and gets recorded directly (see `record_outcome`).
|
|
69
|
+
def populators(runtime)
|
|
70
|
+
runtime.registry.bluebooks.each_value.flat_map do |bluebook|
|
|
71
|
+
bluebook.aggregates.flat_map do |aggregate|
|
|
72
|
+
aggregate.commands.filter_map do |command|
|
|
73
|
+
append = command.mutations.find { |mutation| mutation.op == :append }
|
|
74
|
+
next unless append
|
|
75
|
+
|
|
76
|
+
list_attribute = aggregate.attribute(append.target)
|
|
77
|
+
next unless list_attribute&.list?
|
|
78
|
+
|
|
79
|
+
entity = aggregate.entities.find { |candidate| candidate.hecks_name == list_attribute.type.to_s }
|
|
80
|
+
next unless entity
|
|
81
|
+
|
|
82
|
+
identity_field = entity.identified_by
|
|
83
|
+
mapped = identity_field && append.source[identity_field]
|
|
84
|
+
{ command: command, aggregate: aggregate, entity: entity,
|
|
85
|
+
identity_field: identity_field, identity_argument: mapped.is_a?(Symbol) ? mapped : nil }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require_relative "../value_generator"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Fuzzing
|
|
5
|
+
class SequenceGenerator
|
|
6
|
+
# What a successful step created, and how a later step draws on it —
|
|
7
|
+
# the state tracking that makes multi-step sequences reach further
|
|
8
|
+
# than single calls ever did.
|
|
9
|
+
module OutcomeTracker
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def record_outcome(catalog, entry, args)
|
|
13
|
+
aggregate = entry[:aggregate]
|
|
14
|
+
parent_key = (aggregate.identified_by || :id).to_s
|
|
15
|
+
parent_scalar = ValueGenerator.scalar_of(args[parent_key])
|
|
16
|
+
|
|
17
|
+
@known_ids[aggregate.hecks_name] << parent_scalar if entry[:entity].nil? && entry[:command].creates?
|
|
18
|
+
|
|
19
|
+
populator = catalog[:populators].find { |p| p[:command].equal?(entry[:command]) && p[:aggregate].equal?(aggregate) }
|
|
20
|
+
return unless populator
|
|
21
|
+
|
|
22
|
+
key = "#{aggregate.hecks_name}.#{populator[:entity].hecks_name}##{parent_scalar}"
|
|
23
|
+
|
|
24
|
+
# The auto-minted case (CommandInterpreter#entity_element): the
|
|
25
|
+
# element just landed at count-so-far + 1. The explicit case: this
|
|
26
|
+
# generator itself supplied whatever value sits at
|
|
27
|
+
# args[identity_argument] — no guessing needed, it's read straight
|
|
28
|
+
# back.
|
|
29
|
+
new_id =
|
|
30
|
+
if populator[:identity_argument]
|
|
31
|
+
ValueGenerator.scalar_of(args[populator[:identity_argument].to_s])
|
|
32
|
+
else
|
|
33
|
+
(@entity_known_ids[key].size + 1).to_s
|
|
34
|
+
end
|
|
35
|
+
@entity_known_ids[key] << new_id
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def pick_known(name)
|
|
39
|
+
pool = @known_ids[name]
|
|
40
|
+
return ValueGenerator.random_id(@random) if pool.empty? || @random.rand < ValueGenerator::INVALID_REFERENCE_PROBABILITY
|
|
41
|
+
|
|
42
|
+
pool.sample(random: @random)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def pick_entity_known(aggregate_name, entity_name, parent_id)
|
|
46
|
+
pool = @entity_known_ids["#{aggregate_name}.#{entity_name}##{parent_id}"]
|
|
47
|
+
return ValueGenerator.random_id(@random) if pool.empty? || @random.rand < ValueGenerator::INVALID_REFERENCE_PROBABILITY
|
|
48
|
+
|
|
49
|
+
pool.sample(random: @random)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
class SequenceGenerator
|
|
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
|
+
# new).
|
|
7
|
+
module Picker
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def pick(catalog)
|
|
11
|
+
rest = catalog[:queries].dup
|
|
12
|
+
catalog[:instance].each { |entry| rest << entry if actionable?(catalog, entry) }
|
|
13
|
+
catalog[:entity_commands].each { |entry| rest << entry if actionable?(catalog, entry) }
|
|
14
|
+
catalog[:entity_queries].each { |entry| rest << entry if @known_ids[entry[:aggregate].hecks_name].any? }
|
|
15
|
+
catalog[:read_models].each { |entry| rest << entry if read_model_actionable?(entry) }
|
|
16
|
+
|
|
17
|
+
makers = catalog[:creating].select { |entry| satisfiable?(catalog, entry) }
|
|
18
|
+
pool = rest + makers.flat_map { |entry| [entry] * creating_weight(rest.size) }
|
|
19
|
+
|
|
20
|
+
steer(pool).sample(random: @random)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# WHILE THERE IS NOTHING TO FIND, MAKING SOMETHING IS THE ONLY USEFUL MOVE.
|
|
24
|
+
#
|
|
25
|
+
# A flat weight is right once the domain has records in it, and badly
|
|
26
|
+
# wrong before: banking declares ten queries, and from an empty store
|
|
27
|
+
# exactly ONE creating command is satisfiable — Customer.Register, the
|
|
28
|
+
# verb the whole cascade waits on. Two entries against ten left runs
|
|
29
|
+
# spending their entire budget querying a store nothing had been written
|
|
30
|
+
# to ; one seed answered ten empty queries in a row and emitted nothing
|
|
31
|
+
# at all.
|
|
32
|
+
#
|
|
33
|
+
# So while nothing exists, creation matches everything else put together.
|
|
34
|
+
# After that the ordinary weight applies and the run gets on with
|
|
35
|
+
# exercising what it made.
|
|
36
|
+
def creating_weight(rest_size)
|
|
37
|
+
return CREATING_WEIGHT if @known_ids.each_value.any?(&:any?)
|
|
38
|
+
|
|
39
|
+
[rest_size, CREATING_WEIGHT].max
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def actionable?(catalog, entry)
|
|
43
|
+
@known_ids[entry[:aggregate].hecks_name].any? && satisfiable?(catalog, entry)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A ROOTLESS report (no `reference_to` at all) has nothing to wait
|
|
47
|
+
# for — it reads whole tables, the same "always eligible" position
|
|
48
|
+
# `catalog[:queries]` itself takes. A ROOTED one needs a real
|
|
49
|
+
# instance of its own `reference_target` to ask about first, same
|
|
50
|
+
# rule `actionable?` already gives an instance command — asking
|
|
51
|
+
# `Banking.disputed_payment_count` before any Account exists would
|
|
52
|
+
# just be refused (`refuse_object_reference`/`NotFound`) the same
|
|
53
|
+
# way `Account.Credit` would be, for the identical reason.
|
|
54
|
+
def read_model_actionable?(entry)
|
|
55
|
+
entry[:model].reference_target.nil? || @known_ids[entry[:model].reference_target].any?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A COMMAND THAT REFERENCES NOTHING THAT EXISTS CANNOT SUCCEED, so it is
|
|
59
|
+
# not offered until something does.
|
|
60
|
+
#
|
|
61
|
+
# `ValueGenerator.reference_value` has no real id to hand over when its
|
|
62
|
+
# pool is empty, so it mints `missing-…` and the dispatch is refused
|
|
63
|
+
# before it starts — a guaranteed waste of a step, and worse, one that
|
|
64
|
+
# never fills the pool it was waiting on. Banking cascades from it:
|
|
65
|
+
# Account.Open needs a Customer, ATMCard.Issue needs an Account,
|
|
66
|
+
# Transfer.Request needs two, so a run that opened with the wrong verb
|
|
67
|
+
# spent its whole budget being refused.
|
|
68
|
+
#
|
|
69
|
+
# This is the rule instance commands already follow — `known_ids.any?` —
|
|
70
|
+
# applied to what a command REFERENCES rather than to what it acts on.
|
|
71
|
+
# A target no creating command in this corpus can make (a cross-domain
|
|
72
|
+
# reference, which `CommandRules#resolve_references` skips anyway) is
|
|
73
|
+
# exempt, or the whole domain would starve waiting for it.
|
|
74
|
+
def satisfiable?(catalog, entry)
|
|
75
|
+
entry[:command].attributes.select(&:reference?).all? do |attribute|
|
|
76
|
+
target = attribute.type.target_name.to_s
|
|
77
|
+
!catalog[:creatable].include?(target) || @known_ids[target].any?
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Coverage-guided rather than uniformly random : a verb this sequence has
|
|
82
|
+
# not dispatched yet is weighted up, so a run spends its budget on the
|
|
83
|
+
# commands it has not reached instead of re-rolling the ones it has. The
|
|
84
|
+
# eligibility rules above still decide what is POSSIBLE — this only decides
|
|
85
|
+
# what is likely, so a verb gated behind state it does not have yet stays
|
|
86
|
+
# out of the pool entirely rather than being preferred forever.
|
|
87
|
+
def steer(pool)
|
|
88
|
+
fresh = pool.reject { |entry| @exercised.include?(entry[:verb]) }
|
|
89
|
+
return pool if fresh.empty?
|
|
90
|
+
|
|
91
|
+
pool + (fresh * UNEXERCISED_WEIGHT)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
require_relative "../../bluebook/expression/resolver"
|
|
2
|
+
require_relative "../invalid_value_generator"
|
|
3
|
+
require_relative "../value_generator"
|
|
4
|
+
require_relative "../../runtime/errors"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Fuzzing
|
|
8
|
+
class SequenceGenerator
|
|
9
|
+
# Turn a picked entry into a corpus step: generate its arguments,
|
|
10
|
+
# occasionally malform exactly one of them, shape its identity, and
|
|
11
|
+
# dispatch it for real.
|
|
12
|
+
module StepBuilder
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def build_query_step(runtime, entry)
|
|
16
|
+
args = args_for(entry[:query].attributes, entry[:aggregate])
|
|
17
|
+
safe_call { runtime.query(entry[:verb], **symbolize(args)) }
|
|
18
|
+
{ "query" => entry[:verb], "args" => args }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# A REPORT ASK — the bare domain form `entry[:verb]` already carries
|
|
22
|
+
# ("Domain.report_name", no "::"), so `Dispatcher#query` routes it
|
|
23
|
+
# to the read model rather than an aggregate query. A `ReadModel`
|
|
24
|
+
# has no declared `.attributes` the way a `Query` does — its own
|
|
25
|
+
# argument surface is exactly ONE key, `reference_name`, and ONLY
|
|
26
|
+
# for a rooted model (`read_model_actionable?` already gated a
|
|
27
|
+
# rootless one straight into eligibility with nothing to supply).
|
|
28
|
+
# A BARE scalar, not `identity_shaped` — `ReadModelInterpreter#
|
|
29
|
+
# refuse_object_reference` explicitly REJECTS a Hash/Value offered
|
|
30
|
+
# here (this is the one place in the whole generator where the
|
|
31
|
+
# subject's own identity must NOT be wrapped the way a command
|
|
32
|
+
# argument's would be).
|
|
33
|
+
def build_read_model_step(runtime, entry)
|
|
34
|
+
model = entry[:model]
|
|
35
|
+
args = model.reference_target.nil? ? {} : { model.reference_name.to_s => pick_known(model.reference_target) }
|
|
36
|
+
|
|
37
|
+
safe_call { runtime.query(entry[:verb], **symbolize(args)) }
|
|
38
|
+
{ "query" => entry[:verb], "args" => args }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_command_step(runtime, catalog, entry)
|
|
42
|
+
args = args_for(entry[:command].attributes, entry[:aggregate])
|
|
43
|
+
add_identity!(args, entry)
|
|
44
|
+
|
|
45
|
+
outcome = safe_call { runtime.dispatch(entry[:verb], **symbolize(args)) }
|
|
46
|
+
if outcome
|
|
47
|
+
record_outcome(catalog, entry, args)
|
|
48
|
+
@event_count += outcome.events.length
|
|
49
|
+
end
|
|
50
|
+
{ "verb" => entry[:verb], "args" => args }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def args_for(attributes, aggregate)
|
|
54
|
+
args = attributes.each_with_object({}) do |attribute, built|
|
|
55
|
+
# List-typed direct command arguments have no example in this repo's
|
|
56
|
+
# domains today — every list is populated via a per-element append
|
|
57
|
+
# command instead. Skipped rather than guessed at.
|
|
58
|
+
next if attribute.list?
|
|
59
|
+
# AN OPTIONAL ARGUMENT IS SOMETIMES NOT GIVEN, and that is an
|
|
60
|
+
# ordinary payload rather than a damaged one — see
|
|
61
|
+
# OPTIONAL_OMITTED_PROBABILITY for why this cannot live in
|
|
62
|
+
# `malform` below and what it was costing while it did not
|
|
63
|
+
# exist at all.
|
|
64
|
+
next if attribute.optional? && @random.rand < SequenceGenerator::OPTIONAL_OMITTED_PROBABILITY
|
|
65
|
+
|
|
66
|
+
built[attribute.name.to_s] = ValueGenerator.value_for(attribute, aggregate, random: @random, known_ids: @known_ids)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
malform(args, attributes, aggregate)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# ONE MALFORMATION AT A TIME, and usually none. A step whose payload is
|
|
73
|
+
# wrong in three ways only ever proves which check runs first ; wrong in
|
|
74
|
+
# exactly one way names the check that fired. And the rate stays low on
|
|
75
|
+
# purpose — a corrupted step is almost always refused, a sequence of
|
|
76
|
+
# refusals reaches no state at all, and bin/fuzz already counts those as
|
|
77
|
+
# SILENT rather than scoring them.
|
|
78
|
+
def malform(args, attributes, aggregate)
|
|
79
|
+
return args if args.empty? || @random.rand >= MALFORMED_ARGUMENT_PROBABILITY
|
|
80
|
+
|
|
81
|
+
case @random.rand(3)
|
|
82
|
+
when 0 then corrupt_one(args, attributes, aggregate)
|
|
83
|
+
when 1 then drop_one(args, aggregate)
|
|
84
|
+
else args.merge([InvalidValueGenerator.undeclared_argument(random: @random)].to_h)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# NEVER THE IDENTITY. A creating command with no id auto-mints one, and
|
|
89
|
+
# a minted id is deliberately unreproducible — a random hex, never a
|
|
90
|
+
# guessable counter — so dropping it manufactures a step whose outcome
|
|
91
|
+
# cannot be replayed and says nothing about the runtime's behaviour.
|
|
92
|
+
# Every step in the hand-written corpus
|
|
93
|
+
# supplies an id for the same reason. Whether an auto-minted id OUGHT to
|
|
94
|
+
# be reproducible is a real question, but it is not one a payload fuzzer
|
|
95
|
+
# can ask.
|
|
96
|
+
def drop_one(args, aggregate)
|
|
97
|
+
identity = (aggregate.identified_by || :id).to_s
|
|
98
|
+
droppable = args.keys - [identity, "id"]
|
|
99
|
+
return args if droppable.empty?
|
|
100
|
+
|
|
101
|
+
args.reject { |name, _| name == droppable.sample(random: @random) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def corrupt_one(args, attributes, aggregate)
|
|
105
|
+
named = attributes.reject(&:list?).select { |attribute| args.key?(attribute.name.to_s) }
|
|
106
|
+
return args if named.empty?
|
|
107
|
+
|
|
108
|
+
attribute = named.sample(random: @random)
|
|
109
|
+
args.merge(attribute.name.to_s => InvalidValueGenerator.corrupt(attribute, aggregate, random: @random))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def add_identity!(args, entry)
|
|
113
|
+
aggregate = entry[:aggregate]
|
|
114
|
+
parent_key = (aggregate.identified_by || :id).to_s
|
|
115
|
+
|
|
116
|
+
if entry[:entity]
|
|
117
|
+
parent_scalar = pick_known(aggregate.hecks_name)
|
|
118
|
+
args[parent_key] = identity_shaped(aggregate, aggregate.identified_by, parent_scalar, aggregate)
|
|
119
|
+
entity_key = (entry[:entity].identified_by || :id).to_s
|
|
120
|
+
entity_scalar = pick_entity_known(aggregate.hecks_name, entry[:entity].hecks_name, parent_scalar)
|
|
121
|
+
args[entity_key] = identity_shaped(entry[:entity], entry[:entity].identified_by, entity_scalar, aggregate)
|
|
122
|
+
elsif entry[:command].creates?
|
|
123
|
+
args[parent_key] ||= identity_shaped(aggregate, aggregate.identified_by, ValueGenerator.random_id(@random), aggregate)
|
|
124
|
+
else
|
|
125
|
+
scalar = pick_known(aggregate.hecks_name)
|
|
126
|
+
args[parent_key] = identity_shaped(aggregate, aggregate.identified_by, scalar, aggregate)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# A bare scalar id, shaped to match whatever `construct` itself
|
|
131
|
+
# declares that identity field as. `Account::LedgerEntry` is addressed
|
|
132
|
+
# by `sequence`, and `sequence` is declared as a value-object-typed
|
|
133
|
+
# attribute (`LedgerSequence`), not a plain identifier — a fuzz run
|
|
134
|
+
# that skipped this wrapping and dispatched a bare `"1"` had every
|
|
135
|
+
# such step refused at the type gate (a bare scalar against a
|
|
136
|
+
# value-object-typed identity is a TypeMismatch, not a NotFound), so
|
|
137
|
+
# the wrapping is what lets a fuzz step address the record at
|
|
138
|
+
# all. Left as a bare scalar when the construct declares no such
|
|
139
|
+
# attribute at all — the default `:id` case, which really is untyped.
|
|
140
|
+
def identity_shaped(construct, key, scalar, aggregate)
|
|
141
|
+
return scalar unless key
|
|
142
|
+
|
|
143
|
+
attribute = construct.attribute(key)
|
|
144
|
+
return scalar unless attribute
|
|
145
|
+
|
|
146
|
+
value_object = aggregate.value_object(attribute.type.to_s)
|
|
147
|
+
return scalar unless value_object
|
|
148
|
+
|
|
149
|
+
field = value_object.attributes.first
|
|
150
|
+
return scalar unless field
|
|
151
|
+
|
|
152
|
+
{ field.name.to_s => coerce_scalar(field.type.to_s, scalar) }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def coerce_scalar(type_name, scalar)
|
|
156
|
+
case type_name
|
|
157
|
+
when "Integer" then scalar.to_i
|
|
158
|
+
when "Float" then scalar.to_f
|
|
159
|
+
else scalar.to_s
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def symbolize(args) = args.transform_keys(&:to_sym)
|
|
164
|
+
|
|
165
|
+
# A step the runtime declines is not a generator failure — it simply did
|
|
166
|
+
# not take effect, so nothing is recorded and the sequence carries on. The
|
|
167
|
+
# step still goes into the corpus, because a REFUSAL IS AN ANSWER, and
|
|
168
|
+
# its wording is pinned by the corpus.
|
|
169
|
+
#
|
|
170
|
+
# EvaluationError sits alongside the declared refusals deliberately : a
|
|
171
|
+
# payload the interpreter cannot read is the domain declining it, and
|
|
172
|
+
# bin/run already records exactly those in a corpus's refusals —
|
|
173
|
+
# `positive? expects a number, got "lots"` is one of banking's. Anything
|
|
174
|
+
# else still propagates and fails spec/fuzzing, which is what says the
|
|
175
|
+
# generator built a step that breaks the interpreter for reasons that have
|
|
176
|
+
# nothing to do with the domain declining a payload.
|
|
177
|
+
def safe_call
|
|
178
|
+
yield
|
|
179
|
+
rescue *Hecks::Runtime::DOMAIN_REFUSALS, Hecks::Bluebook::Expression::EvaluationError
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "set"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require_relative "isolated_boot"
|
|
5
|
+
require_relative "invalid_value_generator"
|
|
6
|
+
require_relative "value_generator"
|
|
7
|
+
require_relative "sequence_generator/catalog"
|
|
8
|
+
require_relative "sequence_generator/picker"
|
|
9
|
+
require_relative "sequence_generator/step_builder"
|
|
10
|
+
require_relative "sequence_generator/outcome_tracker"
|
|
11
|
+
|
|
12
|
+
module Hecks
|
|
13
|
+
module Fuzzing
|
|
14
|
+
# A random-but-valid sequence of dispatches and queries, in the exact
|
|
15
|
+
# `{name, note, steps}` shape `spec/corpus/*.json` already uses — so a
|
|
16
|
+
# generated sequence replays as a corpus member, completely unchanged.
|
|
17
|
+
# Boots a throwaway copy of
|
|
18
|
+
# the domain and DISPATCHES each candidate step for real as it builds the
|
|
19
|
+
# sequence (not just synthesizing plausible-looking JSON) : the only way to
|
|
20
|
+
# know whether a step actually reached a new state, or which id an
|
|
21
|
+
# auto-minted entity landed on, is to run it and watch what happened.
|
|
22
|
+
#
|
|
23
|
+
# Single-call fuzzing mostly misses the bugs this project has actually
|
|
24
|
+
# found — they needed STATE first (a saga leg acting on a transfer that
|
|
25
|
+
# already exists, a reference pointing at a customer already registered).
|
|
26
|
+
# So this tracks what it has created as it goes, the same way
|
|
27
|
+
# spec/banking_state_machine_spec.rb's hand-written generator does, and
|
|
28
|
+
# weights later steps toward acting on what already exists.
|
|
29
|
+
#
|
|
30
|
+
# One concern per file beside this one: what the domain offers
|
|
31
|
+
# (sequence_generator/catalog.rb), which step to try next (picker.rb),
|
|
32
|
+
# how a step is built and dispatched (step_builder.rb), and what a
|
|
33
|
+
# success taught us (outcome_tracker.rb).
|
|
34
|
+
class SequenceGenerator
|
|
35
|
+
include Catalog
|
|
36
|
+
include Picker
|
|
37
|
+
include StepBuilder
|
|
38
|
+
include OutcomeTracker
|
|
39
|
+
|
|
40
|
+
# Creating commands are always eligible ; weighting them heavier (not
|
|
41
|
+
# exclusively — a domain with only one or two aggregates would starve
|
|
42
|
+
# everything else) makes a sequence reach an actionable state sooner
|
|
43
|
+
# instead of spending its early budget refusing "acts on nothing yet."
|
|
44
|
+
CREATING_WEIGHT = 2
|
|
45
|
+
|
|
46
|
+
# How often a payload is deliberately the wrong shape. Low, because a
|
|
47
|
+
# refused step reaches no new state and a sequence of them is SILENT.
|
|
48
|
+
MALFORMED_ARGUMENT_PROBABILITY = 0.12
|
|
49
|
+
|
|
50
|
+
# HOW OFTEN AN OPTIONAL ARGUMENT IS SIMPLY NOT GIVEN — a fair coin,
|
|
51
|
+
# because that is exactly what `optional:` means: present or absent,
|
|
52
|
+
# both legal, neither the interesting case.
|
|
53
|
+
#
|
|
54
|
+
# This is NOT a malformation and must not be filed as one. `malform`
|
|
55
|
+
# drops an argument too, but a dropped REQUIRED argument is refused,
|
|
56
|
+
# a refusal writes no record, and bin/fuzz counts it SILENT — so the
|
|
57
|
+
# one outcome worth reaching (a stored record carrying a null, then
|
|
58
|
+
# QUERIED) was unreachable from that path by construction. Until
|
|
59
|
+
# this existed no generated history contained a null at all, which
|
|
60
|
+
# meant `query_answers_match_reference` — the differential that
|
|
61
|
+
# diffs every native adapter against the reference interpreter — had
|
|
62
|
+
# never once compared a nullable field. Four query bugs shipped in
|
|
63
|
+
# exactly that blind spot (`ne:` with an empty string, array `in:`,
|
|
64
|
+
# `ne:` against a null, `in` on a numeric field); the adapter
|
|
65
|
+
# agreement gate had the identical hole and found a fourth bug the
|
|
66
|
+
# hour it was closed.
|
|
67
|
+
OPTIONAL_OMITTED_PROBABILITY = 0.5
|
|
68
|
+
|
|
69
|
+
# How strongly an unexercised verb is preferred over one this sequence has
|
|
70
|
+
# already dispatched. Random picking revisits the same handful of verbs and
|
|
71
|
+
# leaves whole commands untouched for a whole run — which is the same
|
|
72
|
+
# "reached no interesting state" problem the SILENT count reports, seen from
|
|
73
|
+
# the generating end rather than the scoring end.
|
|
74
|
+
UNEXERCISED_WEIGHT = 4
|
|
75
|
+
|
|
76
|
+
def self.generate(domain_path, seed:, steps:)
|
|
77
|
+
new(domain_path, seed: seed, steps: steps).call
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# How many EVENTS the generated sequence actually produced — not
|
|
81
|
+
# steps, not successful dispatches, but the sum of every Result#events
|
|
82
|
+
# length across the run. This is the count bin/fuzz declares as the
|
|
83
|
+
# script's own `expectations.events` claim: whatever was achieved
|
|
84
|
+
# DURING generation becomes the claim a fresh replay of the same
|
|
85
|
+
# script is held to. Zero means the sequence never
|
|
86
|
+
# reached an interesting state — a fuzzer-effectiveness fact, not a
|
|
87
|
+
# replay one.
|
|
88
|
+
attr_reader :event_count
|
|
89
|
+
|
|
90
|
+
def initialize(domain_path, seed:, steps:)
|
|
91
|
+
@domain_path = domain_path
|
|
92
|
+
@seed = seed
|
|
93
|
+
@step_count = steps
|
|
94
|
+
@random = Random.new(seed)
|
|
95
|
+
@known_ids = Hash.new { |h, k| h[k] = [] }
|
|
96
|
+
@entity_known_ids = Hash.new { |h, k| h[k] = [] }
|
|
97
|
+
@exercised = Set.new
|
|
98
|
+
@event_count = 0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def call
|
|
102
|
+
# Real leftover data from ordinary use (bin/console, whatever) lives
|
|
103
|
+
# under the example's data/ — a generator that boots against it
|
|
104
|
+
# starts from state its own known_ids tracking doesn't know about.
|
|
105
|
+
# IsolatedBoot resets that AND rebinds persistence to Memory, since
|
|
106
|
+
# a Postgres-bound domain's real store lives outside the copied
|
|
107
|
+
# directory entirely and `rm_rf`ing data/ alone cannot reach it —
|
|
108
|
+
# see isolated_boot.rb's own header.
|
|
109
|
+
IsolatedBoot.call(@domain_path) do |copy|
|
|
110
|
+
runtime = Hecks.boot(copy)
|
|
111
|
+
catalog = build_catalog(runtime)
|
|
112
|
+
Array.new(@step_count) { attempt_step(runtime, catalog) }.compact
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def attempt_step(runtime, catalog)
|
|
119
|
+
entry = pick(catalog)
|
|
120
|
+
return nil unless entry
|
|
121
|
+
|
|
122
|
+
@exercised << entry[:verb]
|
|
123
|
+
if entry[:query] then build_query_step(runtime, entry)
|
|
124
|
+
elsif entry[:model] then build_read_model_step(runtime, entry)
|
|
125
|
+
else build_command_step(runtime, catalog, entry)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|