hecks 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
|
@@ -2,20 +2,22 @@ require "hecks/ports/persistence/plugins/era"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Fuzzing
|
|
5
|
-
#
|
|
6
|
-
# this file generates a sequence and compares two answers to the
|
|
5
|
+
# **The fork-loss class, as a check** — not a fuzz. Every other mode in
|
|
6
|
+
# this file generates a sequence and compares two answers to the same
|
|
7
7
|
# question; this one asks a single, unconditional question of a
|
|
8
|
-
# target's own
|
|
8
|
+
# target's own real, already-configured `PostgresEra` ledger:
|
|
9
9
|
# does any ancestor era still hold writes nobody has merged forward?
|
|
10
10
|
#
|
|
11
|
-
#
|
|
11
|
+
# ## The bug this targets, named exactly
|
|
12
|
+
#
|
|
13
|
+
# Minting a new era (an attribute
|
|
12
14
|
# or aggregate addition — `StorageShape.project`, lib/hecks/ports/
|
|
13
15
|
# persistence/plugins/era/storage_shape.rb) advances the readable head
|
|
14
|
-
# to a
|
|
16
|
+
# to a new partition; an old checkout, or a process that boots slower
|
|
15
17
|
# than the mint, can keep writing into the era it still believes is
|
|
16
18
|
# current. Those writes are not lost — `Lineage#diverged_count`
|
|
17
19
|
# (postgres_era/lineage/tail_merge.rb) can always find them — but
|
|
18
|
-
# nothing
|
|
20
|
+
# nothing ever asked it automatically. The practice's own ledger found
|
|
19
21
|
# exactly this live, twice: once as an operational gap this session's
|
|
20
22
|
# Step 0 recovered by hand (`bin/merge_tail`, three conflicting
|
|
21
23
|
# records, three eras deep), and once as BUG#24 (a superuser
|
|
@@ -26,15 +28,17 @@ module Hecks
|
|
|
26
28
|
# line already answers by hand, run as an ordinary sweep Check instead
|
|
27
29
|
# of only when a human remembers to ask.
|
|
28
30
|
#
|
|
29
|
-
#
|
|
31
|
+
# ## Connection and safety
|
|
32
|
+
#
|
|
33
|
+
# The target's real database, read-only, never a disposable one — every
|
|
30
34
|
# other Postgres-touching mode here (`persistence_parity`,
|
|
31
35
|
# `adapter_parity_sqlite`) owns a throwaway schema for the exact
|
|
32
36
|
# reason it must never look at what a real deployment actually holds;
|
|
33
|
-
# this check exists
|
|
37
|
+
# this check exists for what a real deployment actually holds, so it
|
|
34
38
|
# connects the same way `bin/merge_tail` itself does — the target's
|
|
35
39
|
# own `.world` binding — and never writes anything: `eras`/
|
|
36
40
|
# `diverged_count` are both plain `SELECT`s, and `ensure_base!` only
|
|
37
|
-
# ever provisions (`CREATE TABLE IF NOT EXISTS`) when
|
|
41
|
+
# ever provisions (`CREATE TABLE IF NOT EXISTS`) when this connection
|
|
38
42
|
# is the database's own provisioning owner, the identical idempotent
|
|
39
43
|
# call every ordinary boot already makes. A target with no real,
|
|
40
44
|
# reachable database configured (nothing provisioned locally, say) is
|
|
@@ -44,21 +48,39 @@ module Hecks
|
|
|
44
48
|
module EraBoundary
|
|
45
49
|
module_function
|
|
46
50
|
|
|
51
|
+
# Reports whether any ancestor `PostgresEra` era still holds writes the
|
|
52
|
+
# head has never merged forward, for one target's real database.
|
|
53
|
+
#
|
|
47
54
|
# `{ checked: true, diverged_total:, breakdown: [{ordinal:, diverged:}] }`
|
|
48
|
-
# or `{ checked: false, reason: "..." }
|
|
49
|
-
# `false` means (an operational note, not a finding); a `true` with
|
|
55
|
+
# or `{ checked: false, kind:, reason: "..." }`. A `true` with
|
|
50
56
|
# `diverged_total.positive?` is the finding this module exists to
|
|
51
57
|
# surface: real post-cut writes an ancestor era is still holding.
|
|
58
|
+
#
|
|
59
|
+
# `kind:` distinguishes the two different reasons `checked: false` can
|
|
60
|
+
# occur — without it, `bin/qa_sweep` would log a refused connection
|
|
61
|
+
# and a `Lineage` defect exactly like an audit that ran and found
|
|
62
|
+
# nothing, counting either toward `sweep.made` and the target's clean
|
|
63
|
+
# streak. `:not_applicable` is a target with no lineage to audit at
|
|
64
|
+
# all; `:error` is the audit failing to run, which the caller
|
|
65
|
+
# surfaces as a finding rather than holding.
|
|
66
|
+
#
|
|
67
|
+
# @param domain_path [String] filesystem path to the target domain's bluebook
|
|
68
|
+
# directory, or a directory containing one
|
|
69
|
+
# @return [Hash{Symbol => Object}] on success, `{checked: true, era_count: Integer,
|
|
70
|
+
# breakdown: Array<Hash{ordinal: Integer, diverged: Integer}>, diverged_total: Integer}`;
|
|
71
|
+
# otherwise `{checked: false, kind: Symbol, reason: String}`
|
|
52
72
|
def diverged_ancestor_writes(domain_path)
|
|
53
73
|
registry, directory = load_registry(domain_path)
|
|
54
74
|
bluebook = registry.bluebooks.values.first
|
|
55
|
-
return
|
|
75
|
+
return failed("no bluebook in #{directory}") unless bluebook
|
|
56
76
|
|
|
57
77
|
first = bluebook.aggregates.first
|
|
58
|
-
return
|
|
78
|
+
return not_applicable("#{bluebook.name} declares no aggregates") unless first
|
|
59
79
|
|
|
60
80
|
adapter_name = Hecks::Ports::Persistence::BindingPolicy.resolve(registry, bluebook.name, first).adapter
|
|
61
|
-
|
|
81
|
+
unless adapter_name == "PostgresEra"
|
|
82
|
+
return not_applicable("#{bluebook.name} is bound to #{adapter_name}, not PostgresEra")
|
|
83
|
+
end
|
|
62
84
|
|
|
63
85
|
settings = registry.world(bluebook.name)&.for_binding(Hecks::Ports::Persistence::VERB, adapter_name) || {}
|
|
64
86
|
db = Hecks::Adapters::PostgresEra.connect_for(bluebook.name, settings)
|
|
@@ -81,14 +103,41 @@ module Hecks
|
|
|
81
103
|
db.close
|
|
82
104
|
end
|
|
83
105
|
rescue StandardError => e
|
|
84
|
-
|
|
106
|
+
# Could not audit is not the same as nothing to audit — a refused
|
|
107
|
+
# connection, a `Lineage` defect, malformed `.world` settings. Each
|
|
108
|
+
# of these answers `:error`, not the benign `:not_applicable` shape,
|
|
109
|
+
# so `bin/qa_sweep` surfaces it as a finding rather than holding it
|
|
110
|
+
# as a clean audit.
|
|
111
|
+
failed("#{e.class}: #{e.message}")
|
|
85
112
|
end
|
|
86
113
|
|
|
87
|
-
|
|
114
|
+
# Builds the `checked: false` result for a target with no PostgresEra
|
|
115
|
+
# lineage to audit — no ancestor era can carry post-cut writes when
|
|
116
|
+
# there is no lineage at all. Not a finding, and not evidence of
|
|
117
|
+
# anything either: the caller logs no Check at all.
|
|
118
|
+
#
|
|
119
|
+
# @param reason [String] human-readable explanation of why no lineage applies
|
|
120
|
+
# @return [Hash{Symbol => Object}] `{checked: false, kind: :not_applicable, reason: String}`
|
|
121
|
+
def not_applicable(reason) = { checked: false, kind: :not_applicable, reason: reason }
|
|
122
|
+
|
|
123
|
+
# Builds the `checked: false` result for an audit that could not run
|
|
124
|
+
# — reported as a finding, never held.
|
|
125
|
+
#
|
|
126
|
+
# @param reason [String] human-readable explanation of what failed, typically
|
|
127
|
+
# `"#{exception.class}: #{exception.message}"`
|
|
128
|
+
# @return [Hash{Symbol => Object}] `{checked: false, kind: :error, reason: String}`
|
|
129
|
+
def failed(reason) = { checked: false, kind: :error, reason: reason }
|
|
88
130
|
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
131
|
+
# Boots a fresh registry for the domain at `domain_path` — the same
|
|
132
|
+
# load `bin/merge_tail` itself performs (that script's own top half),
|
|
133
|
+
# never the ledger's own registry (this asks about the swept target's
|
|
134
|
+
# lineage, not QualityControl's own).
|
|
135
|
+
#
|
|
136
|
+
# @param domain_path [String] filesystem path to the target domain's bluebook
|
|
137
|
+
# directory, or a directory containing one
|
|
138
|
+
# @return [Array(Runtime::Registry, String)] the booted registry and the resolved
|
|
139
|
+
# bluebook directory path
|
|
140
|
+
# @raise [Errno::ENOENT] if `domain_path` names no existing domain directory
|
|
92
141
|
def load_registry(domain_path)
|
|
93
142
|
loading = Hecks::Ports::Loading.bootstrap
|
|
94
143
|
directory = loading.bluebook_directory(domain_path)
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
require "json"
|
|
2
|
+
require_relative "../corpus"
|
|
2
3
|
|
|
3
4
|
module Hecks
|
|
4
5
|
module Fuzzing
|
|
5
|
-
#
|
|
6
|
+
# What an aggregate can exhibit, and which pairs it puts together.
|
|
6
7
|
#
|
|
7
8
|
# Extracted from `spec/combination_coverage_spec.rb`'s own pairwise
|
|
8
9
|
# table so it has exactly two consumers that can never drift: that
|
|
9
10
|
# spec (the golden corpus, held to every pair) and `bin/qa_domain_
|
|
10
|
-
# novelty` (a
|
|
11
|
+
# novelty` (a candidate stress domain, measured against every path
|
|
11
12
|
# the QA ledger already sweeps — see that script's own header for why
|
|
12
13
|
# a new domain has to name the pair no existing target meets before
|
|
13
14
|
# it earns a place in the rotation).
|
|
14
15
|
#
|
|
15
|
-
#
|
|
16
|
+
# **The unit is one aggregate**. Two forms in the same chapter but
|
|
16
17
|
# different heads never meet at dispatch; two forms on one head do
|
|
17
18
|
# — that spec's own header has the four defects that argument came
|
|
18
19
|
# from. Each entry below is a form the language declares and a
|
|
19
20
|
# runtime has to handle, chosen because it has produced a defect or
|
|
20
21
|
# sits one step from one; adding one here is how a new form joins
|
|
21
|
-
#
|
|
22
|
+
# both gates at once, and it will name its own uncovered pairs on the
|
|
22
23
|
# first run of each.
|
|
23
24
|
#
|
|
24
|
-
#
|
|
25
|
+
# **One flat table, on purpose** — each entry is an independent boolean
|
|
25
26
|
# check against the same string-keyed aggregate IR hash (the shape
|
|
26
27
|
# `spec/golden/ir/*.json` carries and `Projector::Exporter.call`
|
|
27
28
|
# round-trips to through JSON), laid out so every declared form can
|
|
@@ -48,7 +49,7 @@ module Hecks
|
|
|
48
49
|
"closed_set" => ->(a) { (a["value_objects"] || []).any? { |shape| shape["closed_set"] } },
|
|
49
50
|
"has_default" => ->(a) { attributes(a).any? { |held| !held["default"].nil? } },
|
|
50
51
|
"has_optional" => ->(a) { commands(a).any? { |verb| (verb["attributes"] || []).any? { |held| held["optional"] } } },
|
|
51
|
-
#
|
|
52
|
+
# The reference-hop family (angle-2, qa/bluebook ledger) — the
|
|
52
53
|
# forms `qa/stress_domains/referral_chain` exists for, absent
|
|
53
54
|
# from the census until that domain named them. Each is one
|
|
54
55
|
# step from a catalogued gap: `two_hop_given` is `dereference`'s
|
|
@@ -59,6 +60,19 @@ module Hecks
|
|
|
59
60
|
# command redeclaring the aggregate's own reference field under
|
|
60
61
|
# a non-reference type, so only `resolve_state_references` (never
|
|
61
62
|
# ported) can catch a dangling id.
|
|
63
|
+
# A retroactive correction, and a role-gated command — both are
|
|
64
|
+
# declared forms this census could not see, on aggregates it was
|
|
65
|
+
# already measuring: `examples/banking` has carried `corrects`
|
|
66
|
+
# mutations and `role`-bearing commands the whole time, and
|
|
67
|
+
# `qa/stress_domains/corrections` exists for the first. Three
|
|
68
|
+
# stress domains' own NOTES.md record `bin/qa_domain_novelty`
|
|
69
|
+
# answering "no new pair" for a domain whose whole point was a
|
|
70
|
+
# shape this table did not name (case_escalation's is the
|
|
71
|
+
# bluntest: "read that as a gap in the census, not in this
|
|
72
|
+
# domain"). Entity commands count for both — BUG#30-33 and
|
|
73
|
+
# BUG#31 were all entity-level `corrects`.
|
|
74
|
+
"corrects" => ->(a) { every_command(a).any? { |verb| corrects?(verb) } },
|
|
75
|
+
"role_gated" => ->(a) { every_command(a).any? { |verb| !verb["role"].to_s.empty? } },
|
|
62
76
|
"two_hop_given" => ->(a) { two_hop_given?(a) },
|
|
63
77
|
"multi_hop_where" => ->(a) { multi_hop_where?(a) },
|
|
64
78
|
"revalued_reference" => ->(a) { revalued_reference?(a) }
|
|
@@ -66,25 +80,80 @@ module Hecks
|
|
|
66
80
|
|
|
67
81
|
module_function
|
|
68
82
|
|
|
83
|
+
# Reads an aggregate IR hash's own declared entities.
|
|
84
|
+
#
|
|
85
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
86
|
+
# @return [Array<Hash>] the aggregate's own declared entities ("pieces"); `[]`
|
|
87
|
+
# if it declares none
|
|
69
88
|
def entities(aggregate) = aggregate["entities"] || []
|
|
89
|
+
|
|
90
|
+
# Reads an aggregate IR hash's own declared commands.
|
|
91
|
+
#
|
|
92
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
93
|
+
# @return [Array<Hash>] the aggregate's own declared commands; `[]` if it
|
|
94
|
+
# declares none
|
|
70
95
|
def commands(aggregate) = aggregate["commands"] || []
|
|
96
|
+
|
|
97
|
+
# One aggregate's commands, its pieces' included — a form carried by
|
|
98
|
+
# an entity command is carried by the aggregate that owns it, the
|
|
99
|
+
# same way `composite_piece`/`piece_lifecycle` already read pieces.
|
|
100
|
+
#
|
|
101
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
102
|
+
# @return [Array<Hash>] `aggregate`'s own commands, plus every entity's own
|
|
103
|
+
def every_command(aggregate) = commands(aggregate) + entities(aggregate).flat_map { |piece| commands(piece) }
|
|
104
|
+
|
|
105
|
+
# Answers whether a command IR hash declares a `corrects` mutation.
|
|
106
|
+
#
|
|
107
|
+
# @param verb [Hash] a string-keyed command IR hash
|
|
108
|
+
# @return [Boolean] true if `verb` declares a `corrects` mutation
|
|
109
|
+
def corrects?(verb) = (verb["mutations"] || []).any? { |change| change["op"].to_s == "corrects" }
|
|
110
|
+
|
|
111
|
+
# Reads an aggregate IR hash's own declared attributes.
|
|
112
|
+
#
|
|
113
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
114
|
+
# @return [Array<Hash>] the aggregate's own declared attributes; `[]` if it
|
|
115
|
+
# declares none
|
|
71
116
|
def attributes(aggregate) = aggregate["attributes"] || []
|
|
117
|
+
|
|
118
|
+
# Reads an aggregate IR hash's own declared queries.
|
|
119
|
+
#
|
|
120
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
121
|
+
# @return [Array<Hash>] the aggregate's own declared queries; `[]` if it
|
|
122
|
+
# declares none
|
|
72
123
|
def queries(aggregate) = aggregate["queries"] || []
|
|
124
|
+
|
|
125
|
+
# Answers whether an attribute IR hash's own declared type is a reference.
|
|
126
|
+
#
|
|
127
|
+
# @param attribute [Hash] a string-keyed attribute IR hash
|
|
128
|
+
# @return [Boolean] true if `attribute`'s own declared type is a reference
|
|
73
129
|
def reference?(attribute) = attribute["type"].to_s.start_with?("Reference<")
|
|
74
130
|
|
|
131
|
+
# Answers whether any of an aggregate's own commands crosses a two-hop given.
|
|
132
|
+
#
|
|
133
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
134
|
+
# @return [Boolean] true if any command's own given crosses at least two
|
|
135
|
+
# references (`TWO_HOP_GIVEN_PATH_LENGTH`)
|
|
75
136
|
def two_hop_given?(aggregate)
|
|
76
137
|
commands(aggregate).any? { |verb| (verb["givens"] || []).any? { |given| deep_lookup?(given["ast"]) } }
|
|
77
138
|
end
|
|
78
139
|
|
|
79
140
|
# A `where` whose field crosses two `/` — `member/sponsor/standing`.
|
|
141
|
+
#
|
|
142
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
143
|
+
# @return [Boolean] true if any query's own where clause crosses at least
|
|
144
|
+
# two `/` hops
|
|
80
145
|
def multi_hop_where?(aggregate)
|
|
81
146
|
queries(aggregate).any? { |query| (query["wheres"] || []).any? { |where| where["field"].to_s.count("/") >= 2 } }
|
|
82
147
|
end
|
|
83
148
|
|
|
84
149
|
# A command attribute sharing a name with one of the aggregate's
|
|
85
|
-
# own reference-typed attributes while carrying a
|
|
150
|
+
# own reference-typed attributes while carrying a different, non-
|
|
86
151
|
# reference type — `attribute :member, Handle` against
|
|
87
152
|
# `reference_to Member`.
|
|
153
|
+
#
|
|
154
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
155
|
+
# @return [Boolean] true if any command redeclares one of the aggregate's
|
|
156
|
+
# own reference-typed attribute names under a non-reference type
|
|
88
157
|
def revalued_reference?(aggregate)
|
|
89
158
|
references = attributes(aggregate).select { |held| reference?(held) }.to_set { |held| held["name"].to_s }
|
|
90
159
|
commands(aggregate).any? do |verb|
|
|
@@ -94,6 +163,11 @@ module Hecks
|
|
|
94
163
|
|
|
95
164
|
# Walks a given's own exported AST for any `lookup` whose path is
|
|
96
165
|
# long enough to have crossed two references.
|
|
166
|
+
#
|
|
167
|
+
# @param node [Hash, Array, Object] an AST node (or subtree) from an
|
|
168
|
+
# exported given's `"ast"`
|
|
169
|
+
# @return [Boolean] true if any `lookup` node's own path is at least
|
|
170
|
+
# `TWO_HOP_GIVEN_PATH_LENGTH` long
|
|
97
171
|
def deep_lookup?(node)
|
|
98
172
|
case node
|
|
99
173
|
when Hash
|
|
@@ -108,21 +182,39 @@ module Hecks
|
|
|
108
182
|
end
|
|
109
183
|
|
|
110
184
|
# Every form, answered for one aggregate — the table above, applied.
|
|
185
|
+
#
|
|
186
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
187
|
+
# @return [Hash{String => Boolean}] every `FORMS` name mapped to whether
|
|
188
|
+
# `aggregate` exhibits it
|
|
111
189
|
def properties(aggregate)
|
|
112
190
|
FORMS.transform_values { |form| form.call(aggregate) }
|
|
113
191
|
end
|
|
114
192
|
|
|
115
193
|
# Every unordered pair of forms, each rendered "left + right" in
|
|
116
194
|
# alphabetical order — the key both gates' excuse tables use.
|
|
195
|
+
#
|
|
196
|
+
# @return [Array<String>] every unordered pair of `FORMS` names, as
|
|
197
|
+
# `"left + right"`
|
|
117
198
|
def pairs
|
|
118
199
|
FORMS.keys.combination(2).map { |pair| pair_key(*pair) }
|
|
119
200
|
end
|
|
120
201
|
|
|
202
|
+
# Renders two form names as one sorted pair key.
|
|
203
|
+
#
|
|
204
|
+
# @param left [String] a form name
|
|
205
|
+
# @param right [String] a form name
|
|
206
|
+
# @return [String] `left`/`right`, alphabetically ordered, joined as
|
|
207
|
+
# `"left + right"`
|
|
121
208
|
def pair_key(left, right) = [left, right].sort.join(" + ")
|
|
122
209
|
|
|
123
210
|
# `held` is `[[aggregate_name, properties], ...]`. Answers which
|
|
124
|
-
# pairs are met on
|
|
211
|
+
# pairs are met on one aggregate, and by which — a Hash from pair
|
|
125
212
|
# key to the names carrying it, so a caller can say who.
|
|
213
|
+
#
|
|
214
|
+
# @param held [Array<Array(String, Hash)>] `[aggregate_name, properties]`
|
|
215
|
+
# pairs, `properties` as returned by `#properties`
|
|
216
|
+
# @return [Hash{String => Array<String>}] every met pair key mapped to the
|
|
217
|
+
# aggregate names that carry it
|
|
126
218
|
def covered_pairs(held)
|
|
127
219
|
held.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(name, shows), covered|
|
|
128
220
|
shows.select { |_, present| present }.keys.combination(2).each do |left, right|
|
|
@@ -134,33 +226,42 @@ module Hecks
|
|
|
134
226
|
# `[[\"Chapter::Aggregate\", properties], ...]` for every aggregate
|
|
135
227
|
# a string-keyed chapter IR declares — the same walk the golden
|
|
136
228
|
# spec makes over `spec/golden/ir/*.json`.
|
|
229
|
+
#
|
|
230
|
+
# @param chapter_ir [Hash] a string-keyed chapter IR hash, with `"name"` and
|
|
231
|
+
# `"aggregates"`
|
|
232
|
+
# @return [Array<Array(String, Hash)>] `["Chapter::Aggregate", properties]`
|
|
233
|
+
# pairs, one per declared aggregate, `properties` as returned by
|
|
234
|
+
# `#properties`
|
|
137
235
|
def aggregates_in(chapter_ir)
|
|
138
236
|
(chapter_ir["aggregates"] || []).map do |aggregate|
|
|
139
237
|
["#{chapter_ir['name']}::#{aggregate['name']}", properties(aggregate)]
|
|
140
238
|
end
|
|
141
239
|
end
|
|
142
240
|
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
#
|
|
241
|
+
# Where a domain path keeps its bluebooks — see
|
|
242
|
+
# `Hecks::Corpus.bluebook_files`, the one definition every corpus
|
|
243
|
+
# walk shares. `nil` when neither shape holds a bluebook.
|
|
244
|
+
#
|
|
245
|
+
# @param domain_path [String] path to a domain directory
|
|
246
|
+
# @return [Array<String>, nil] the domain's own `.bluebook` file paths, or
|
|
247
|
+
# `nil` if it holds none
|
|
148
248
|
def bluebook_files(domain_path)
|
|
149
|
-
|
|
150
|
-
files = Dir[File.join(dir, "*.bluebook")]
|
|
151
|
-
return files unless files.empty?
|
|
152
|
-
end
|
|
153
|
-
nil
|
|
249
|
+
Hecks::Corpus.bluebook_files(domain_path)
|
|
154
250
|
end
|
|
155
251
|
|
|
156
|
-
#
|
|
252
|
+
# The same census over a domain on disk, booted the lightweight
|
|
157
253
|
# way `bin/model_check` and `Hecks::Codemod.load_bluebook` already
|
|
158
254
|
# do (ports and the two in-process adapters, no `Hecks.boot`, no
|
|
159
|
-
# live database, no `.hecksagon`: the census reads declared
|
|
255
|
+
# live database, no `.hecksagon`: the census reads declared shape,
|
|
160
256
|
# and a framework chapter a `.hecksagon` would attach is not this
|
|
161
257
|
# domain's own). Only the domain's own chapter is measured — the
|
|
162
258
|
# first bluebook loaded, the same "target chapter is always first"
|
|
163
259
|
# fact `bin/project_rust` relies on.
|
|
260
|
+
# @param domain_path [String] path to a domain directory
|
|
261
|
+
# @return [Array<Array(String, Hash)>] `["Chapter::Aggregate", properties]`
|
|
262
|
+
# pairs, one per aggregate declared on `domain_path`'s own first-loaded
|
|
263
|
+
# chapter (see `Hecks::Projector::Exporter.call`)
|
|
264
|
+
# @raise [ArgumentError] if `domain_path` has no bluebook files to measure
|
|
164
265
|
def census(domain_path)
|
|
165
266
|
root = File.expand_path("../../..", __dir__)
|
|
166
267
|
files = bluebook_files(domain_path)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "isolated_boot"
|
|
3
|
+
require_relative "sequence_generator"
|
|
4
|
+
require_relative "replay"
|
|
5
|
+
require_relative "shrinker"
|
|
6
|
+
require_relative "differential"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Fuzzing
|
|
10
|
+
# **One generated domain, checked** — the child-process half of
|
|
11
|
+
# `bin/qa_generated_domains` (one domain per process: every generated
|
|
12
|
+
# domain is named `QaGenerated`, see DomainGenerator's header).
|
|
13
|
+
#
|
|
14
|
+
# The same comparisons `bin/qa_sweep` runs on a rotation target, over
|
|
15
|
+
# sequences generated against a domain nobody wrote: with a compiled
|
|
16
|
+
# binary, `Differential.diff` (Ruby vs Rust, self-consistency, declared
|
|
17
|
+
# properties); without one, a Ruby-only replay (properties,
|
|
18
|
+
# self-consistency, an interpreter crash). A domain that does not even
|
|
19
|
+
# boot is `invalid` — a generator defect, not a finding, reported apart.
|
|
20
|
+
#
|
|
21
|
+
# `match:` is how domain-level shrinking asks "does this smaller domain
|
|
22
|
+
# still show the same finding?": only a divergence in the same mode
|
|
23
|
+
# whose `Shrinker.signature` contains the original's counts. Verb names
|
|
24
|
+
# survive a removal that did not touch them, so the signature does too.
|
|
25
|
+
module GeneratedDomainCheck
|
|
26
|
+
DIFFERENTIAL_MODES = %i[differential self_consistency properties_in_differential].freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# Checks one generated domain over up to `seeds` generated sequences, stopping
|
|
31
|
+
# at the first divergence found.
|
|
32
|
+
#
|
|
33
|
+
# @param domain_path [String] path to the generated domain directory
|
|
34
|
+
# @param seeds [Integer] maximum number of seeds to try before declaring clean
|
|
35
|
+
# @param steps [Integer] steps to generate per seed
|
|
36
|
+
# @param adversarial [Float] fraction of command steps to mutate adversarially
|
|
37
|
+
# @param binary [String, nil] path to a compiled Rust conformance binary to
|
|
38
|
+
# diff against, or `nil` for a Ruby-only check
|
|
39
|
+
# @param differ [Object, nil] duck-typed comparison helper answering
|
|
40
|
+
# `RustConformanceHelpers`' interface (adapter-defined; constructed ad hoc
|
|
41
|
+
# by callers such as bin/qa_generated_domains), or `nil`
|
|
42
|
+
# @param match [Hash, nil] `{"mode" =>, "signature" =>}` from a parent
|
|
43
|
+
# domain's own finding — only a divergence matching it counts, for
|
|
44
|
+
# domain-level shrinking; `nil` accepts the first divergence found
|
|
45
|
+
# @param shrink_budget [Integer] step-level shrink attempts to spend on a
|
|
46
|
+
# found finding; `0` skips shrinking
|
|
47
|
+
# @return [Hash] `{"status" => "invalid", "error" =>}` if the domain does not
|
|
48
|
+
# boot; `{"status" => "clean", "seeds_run" =>}` if no seed diverged;
|
|
49
|
+
# otherwise a finding merged with `{"status" => "found", "seeds_run" =>}`
|
|
50
|
+
# (see `#finding`), plus `"shrunk_steps"` when `shrink_budget` is positive
|
|
51
|
+
def run(domain_path, seeds:, steps:, adversarial:, binary: nil, differ: nil, match: nil, shrink_budget: 0)
|
|
52
|
+
error = boot_error(domain_path)
|
|
53
|
+
return { "status" => "invalid", "error" => error } if error
|
|
54
|
+
|
|
55
|
+
(1..seeds).each do |seed|
|
|
56
|
+
finding = check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
|
|
57
|
+
next unless finding
|
|
58
|
+
|
|
59
|
+
finding["shrunk_steps"] = shrink(domain_path, finding, binary, differ, shrink_budget) if shrink_budget.positive?
|
|
60
|
+
return finding.merge("status" => "found", "seeds_run" => seed)
|
|
61
|
+
end
|
|
62
|
+
{ "status" => "clean", "seeds_run" => seeds }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Boots `domain_path` in an isolated copy, to find out whether it boots at all.
|
|
66
|
+
#
|
|
67
|
+
# @param domain_path [String] path to the domain directory to boot
|
|
68
|
+
# @return [String, nil] `nil` if the boot succeeds; otherwise the raising
|
|
69
|
+
# exception's class and first message line
|
|
70
|
+
def boot_error(domain_path)
|
|
71
|
+
IsolatedBoot.call(domain_path) { |copy| Hecks.boot(copy) }
|
|
72
|
+
nil
|
|
73
|
+
rescue StandardError, ScriptError => e
|
|
74
|
+
"#{e.class}: #{e.message.lines.first&.strip}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Generates one sequence for `seed` and checks it for a divergence.
|
|
78
|
+
#
|
|
79
|
+
# @param domain_path [String] path to the domain directory
|
|
80
|
+
# @param seed [Integer] RNG seed for `SequenceGenerator.generate`
|
|
81
|
+
# @param steps [Integer] steps to generate
|
|
82
|
+
# @param adversarial [Float] fraction of command steps to mutate adversarially
|
|
83
|
+
# @param binary [String, nil] path to a compiled Rust conformance binary, or
|
|
84
|
+
# `nil` for a Ruby-only check
|
|
85
|
+
# @param differ [Object, nil] duck-typed comparison helper answering
|
|
86
|
+
# `RustConformanceHelpers`' interface (adapter-defined), or `nil`
|
|
87
|
+
# @param match [Hash, nil] `{"mode" =>, "signature" =>}` a divergence must
|
|
88
|
+
# match to count, or `nil` to accept the first one found
|
|
89
|
+
# @return [Hash, nil] a finding (see `#finding`) if a matching divergence was
|
|
90
|
+
# found, or if generation itself crashed; `nil` if the sequence was clean
|
|
91
|
+
def check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
|
|
92
|
+
sequence = SequenceGenerator.generate(domain_path, seed: seed, steps: steps, adversarial: adversarial)
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
finding(seed, :generator, [{ field: "generator_crash", detail: "#{e.class}: #{e.message}" }], [], match)
|
|
95
|
+
else
|
|
96
|
+
outcomes(domain_path, sequence, binary, differ).each do |mode, divergences|
|
|
97
|
+
found = finding(seed, mode, divergences, sequence, match)
|
|
98
|
+
return found if found
|
|
99
|
+
end
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Runs the comparison appropriate to whether a compiled binary is available:
|
|
104
|
+
# `Differential.diff` against Rust when `binary` is given, otherwise a
|
|
105
|
+
# Ruby-only replay checked for property and self-consistency divergences.
|
|
106
|
+
#
|
|
107
|
+
# @param domain_path [String] path to the domain directory
|
|
108
|
+
# @param sequence [Array<Hash>] the generated step sequence to replay
|
|
109
|
+
# @param binary [String, nil] path to a compiled Rust conformance binary, or
|
|
110
|
+
# `nil` for a Ruby-only check
|
|
111
|
+
# @param differ [Object, nil] duck-typed comparison helper answering
|
|
112
|
+
# `RustConformanceHelpers`' interface (adapter-defined), or `nil`
|
|
113
|
+
# @return [Hash{Symbol => Array<Hash>}] one divergence list per mode checked;
|
|
114
|
+
# `{ruby_only: [{field: "crash", detail:}]}` if the Ruby-only replay itself
|
|
115
|
+
# raised
|
|
116
|
+
def outcomes(domain_path, sequence, binary, differ)
|
|
117
|
+
return Differential.diff(differ, domain_path, sequence, binary, modes: DIFFERENTIAL_MODES) if binary
|
|
118
|
+
|
|
119
|
+
history = Replay.call(domain_path, sequence, self_consistency: true)
|
|
120
|
+
{ ruby_only: Differential.property_divergences(history),
|
|
121
|
+
self_consistency: Differential.self_consistency_divergences(history) }
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
{ ruby_only: [{ field: "crash", detail: "#{e.class}: #{e.message}" }] }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Builds a reported finding from a mode's divergences, unless `divergences`
|
|
127
|
+
# is empty or `match` names a different signature.
|
|
128
|
+
#
|
|
129
|
+
# @param seed [Integer] the seed that produced `sequence`
|
|
130
|
+
# @param mode [Symbol] the comparison mode the divergences came from
|
|
131
|
+
# @param divergences [Array<Hash>] divergence entries for `mode`
|
|
132
|
+
# @param sequence [Array<Hash>] the generated step sequence
|
|
133
|
+
# @param match [Hash, nil] `{"mode" =>, "signature" =>}` the finding must
|
|
134
|
+
# match, or `nil` to accept any
|
|
135
|
+
# @return [Hash, nil] `{"seed" =>, "mode" =>, "signature" =>, "steps" =>,
|
|
136
|
+
# "divergences" =>}` if `divergences` is non-empty and matches `match`;
|
|
137
|
+
# `nil` otherwise
|
|
138
|
+
def finding(seed, mode, divergences, sequence, match)
|
|
139
|
+
return nil if divergences.empty?
|
|
140
|
+
|
|
141
|
+
signature = Shrinker.signature(divergences)
|
|
142
|
+
return nil if match && !(match["mode"] == mode.to_s && Set.new(match["signature"]).subset?(signature))
|
|
143
|
+
|
|
144
|
+
{ "seed" => seed, "mode" => mode.to_s, "signature" => signature.to_a.sort, "steps" => sequence,
|
|
145
|
+
"divergences" => JSON.parse(JSON.generate(divergences)) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The step-level half of shrinking, in this same process: the domain
|
|
149
|
+
# is already as small as the parent could make it.
|
|
150
|
+
#
|
|
151
|
+
# @param domain_path [String] path to the (already domain-shrunk) domain
|
|
152
|
+
# directory
|
|
153
|
+
# @param found [Hash] the finding to shrink, as returned by `#finding`
|
|
154
|
+
# @param binary [String, nil] path to a compiled Rust conformance binary, or
|
|
155
|
+
# `nil` for a Ruby-only check
|
|
156
|
+
# @param differ [Object, nil] duck-typed comparison helper answering
|
|
157
|
+
# `RustConformanceHelpers`' interface (adapter-defined), or `nil`
|
|
158
|
+
# @param budget [Integer] step-level shrink attempts to spend
|
|
159
|
+
# @return [Array<Hash>] the smallest step list found that still reproduces
|
|
160
|
+
# `found`'s divergence signature
|
|
161
|
+
def shrink(domain_path, found, binary, differ, budget)
|
|
162
|
+
original = Set.new(found["signature"])
|
|
163
|
+
mode = found["mode"].to_sym
|
|
164
|
+
Shrinker.call(found["steps"], budget: budget) do |candidate|
|
|
165
|
+
divergences = outcomes(domain_path, candidate, binary, differ)[mode] || []
|
|
166
|
+
Shrinker.reproduces?(original, divergences)
|
|
167
|
+
end.steps
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|