hecks 2.8.0 → 3.0.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/exe/hecks +50 -7
- data/lib/hecks/adapters/driven/claude_code.rb +4 -104
- data/lib/hecks/adapters/driven/d1.rb +44 -237
- data/lib/hecks/adapters/driven/folder.rb +17 -76
- data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
- data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
- data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
- data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
- data/lib/hecks/adapters/driven/heki.rb +13 -42
- data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
- data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
- data/lib/hecks/adapters/driven/lambda.rb +10 -84
- data/lib/hecks/adapters/driven/local_storage.rb +5 -40
- data/lib/hecks/adapters/driven/memory.rb +32 -100
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
- data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
- data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +10 -32
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
- data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
- data/lib/hecks/adapters/driven/postgres.rb +149 -136
- data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
- data/lib/hecks/adapters/driven/prism.rb +48 -38
- data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
- data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
- data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
- data/lib/hecks/adapters/driven/sqlite.rb +128 -65
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +76 -62
- data/lib/hecks/adapters/driven.rb +5 -36
- data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
- data/lib/hecks/behaviors/dsl.rb +7 -25
- data/lib/hecks/behaviors/expectations.rb +32 -142
- data/lib/hecks/behaviors/ir.rb +7 -20
- data/lib/hecks/behaviors/rspec.rb +6 -17
- data/lib/hecks/behaviors/runner.rb +10 -34
- data/lib/hecks/behaviors.rb +4 -8
- data/lib/hecks/bench/cli.rb +62 -0
- data/lib/hecks/bench/environment.rb +71 -0
- data/lib/hecks/bench/postgres_probe.rb +30 -0
- data/lib/hecks/bench/report.rb +76 -0
- data/lib/hecks/bench/ruby_runner.rb +93 -0
- data/lib/hecks/bench/run.rb +55 -0
- data/lib/hecks/bench/rust_runner.rb +101 -0
- data/lib/hecks/bench/stats.rb +46 -0
- data/lib/hecks/bench/suite.rb +98 -0
- data/lib/hecks/bench/workload.rb +89 -0
- data/lib/hecks/bench.rb +17 -0
- data/lib/hecks/bluebook/aggregate.rb +9 -75
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
- data/lib/hecks/bluebook/assembly/build.rb +4 -15
- data/lib/hecks/bluebook/assembly/contract.rb +15 -136
- data/lib/hecks/bluebook/assembly/contracts.rb +56 -174
- data/lib/hecks/bluebook/assembly/marks.rb +31 -223
- data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
- data/lib/hecks/bluebook/assembly.rb +6 -45
- data/lib/hecks/bluebook/attribute.rb +12 -61
- data/lib/hecks/bluebook/behaviour/aggregate.rb +39 -39
- data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
- data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
- data/lib/hecks/bluebook/behaviour/command.rb +14 -66
- data/lib/hecks/bluebook/behaviour/domain_port.rb +11 -20
- data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
- data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
- data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
- data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
- data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
- data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
- data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
- data/lib/hecks/bluebook/capabilities.rb +17 -49
- data/lib/hecks/bluebook/chapter.rb +16 -50
- data/lib/hecks/bluebook/command.rb +23 -81
- data/lib/hecks/bluebook/domain_port.rb +49 -67
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +96 -708
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +63 -169
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +3 -1
- data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
- data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +42 -89
- data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +148 -197
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
- data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
- data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
- data/lib/hecks/bluebook/dsl/query_builder.rb +38 -43
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
- data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
- data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
- data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
- data/lib/hecks/bluebook/dsl/world_builder.rb +46 -111
- data/lib/hecks/bluebook/entity.rb +13 -71
- data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
- data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
- data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
- data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
- data/lib/hecks/bluebook/expression/resolver.rb +39 -661
- data/lib/hecks/bluebook/hexagon.rb +18 -33
- data/lib/hecks/bluebook/lifecycle.rb +5 -16
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
- data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
- data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +25 -176
- data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -306
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
- data/lib/hecks/bluebook/meta_validator.rb +87 -352
- data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
- data/lib/hecks/bluebook/model_check.rb +122 -596
- data/lib/hecks/bluebook/pattern_subset.rb +49 -69
- data/lib/hecks/bluebook/process_manager.rb +13 -83
- data/lib/hecks/bluebook/project_loader.rb +3 -39
- data/lib/hecks/bluebook/project_register.rb +12 -60
- data/lib/hecks/bluebook/query.rb +27 -51
- data/lib/hecks/bluebook/read_model.rb +57 -81
- data/lib/hecks/bluebook/reference.rb +7 -41
- data/lib/hecks/bluebook/smoke_test.rb +9 -99
- data/lib/hecks/bluebook/synthesizer.rb +13 -62
- data/lib/hecks/bluebook/translation.rb +26 -62
- data/lib/hecks/bluebook/value_object.rb +11 -36
- data/lib/hecks/bluebook.rb +5 -22
- data/lib/hecks/cache_dir.rb +4 -23
- data/lib/hecks/canonical_json.rb +30 -0
- data/lib/hecks/chapters.rb +102 -0
- data/lib/hecks/cli/behaviors.rb +61 -0
- data/lib/hecks/cli/canonicalise.rb +25 -0
- data/lib/hecks/cli/check_engine_agreement.rb +32 -0
- data/lib/hecks/cli/check_era.rb +88 -0
- data/lib/hecks/cli/console.rb +66 -0
- data/lib/hecks/cli/deploy_template_diff.rb +40 -0
- data/lib/hecks/cli/doc_coverage.rb +32 -0
- data/lib/hecks/cli/document.rb +5 -23
- data/lib/hecks/cli/expression_projection.rb +41 -0
- data/lib/hecks/cli/follow.rb +88 -0
- data/lib/hecks/cli/history.rb +44 -0
- data/lib/hecks/cli/ir.rb +4 -6
- data/lib/hecks/cli/mcp.rb +5 -6
- data/lib/hecks/cli/model_check.rb +24 -88
- data/lib/hecks/cli/pattern_cases.rb +47 -0
- data/lib/hecks/cli/present.rb +39 -0
- data/lib/hecks/cli/project_cli.rb +223 -64
- data/lib/hecks/cli/project_diagrams.rb +4 -10
- data/lib/hecks/cli/project_glossary.rb +44 -0
- data/lib/hecks/cli/reference.rb +46 -0
- data/lib/hecks/cli/refresh_projections.rb +44 -0
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
- data/lib/hecks/cli/release.rb +70 -0
- data/lib/hecks/cli/release_gem.rb +59 -0
- data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
- data/lib/hecks/cli/rspec_shard_files.rb +66 -0
- data/lib/hecks/cli/run.rb +15 -61
- data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
- data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
- data/lib/hecks/cli/shape.rb +70 -0
- data/lib/hecks/cli/smoke_http.rb +230 -0
- data/lib/hecks/cli/smoke_test.rb +7 -19
- data/lib/hecks/cli/spec_example.rb +27 -0
- data/lib/hecks/cli/statements.rb +32 -0
- data/lib/hecks/cli/stores.rb +4 -6
- data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
- data/lib/hecks/cli.rb +216 -73
- data/lib/hecks/codemod.rb +324 -0
- data/lib/hecks/construct.rb +8 -34
- data/lib/hecks/corpus.rb +423 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +617 -158
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -11
- data/lib/hecks/deploy/oidc.json +82 -2
- data/lib/hecks/doc/reference.rb +351 -0
- data/lib/hecks/doors/cli_door.rb +196 -0
- data/lib/hecks/doors/cli_runner.rb +297 -0
- data/lib/hecks/{facade → doors}/command_request.rb +14 -34
- data/lib/hecks/doors/handle.rb +206 -0
- data/lib/hecks/doors/json_door.rb +132 -0
- data/lib/hecks/doors/launcher_options.rb +129 -0
- data/lib/hecks/{cli → doors}/mcp_door.rb +25 -120
- data/lib/hecks/doors/mcp_door_scope.rb +97 -0
- data/lib/hecks/doors/ruby_door/aggregate_door.rb +162 -0
- data/lib/hecks/doors/ruby_door/chapter.rb +72 -0
- data/lib/hecks/doors/ruby_door.rb +77 -0
- data/lib/hecks/doors.rb +61 -0
- data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
- data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
- data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
- data/lib/hecks/embryonaut_bluebook.rb +5 -72
- data/lib/hecks/engine_agreement.rb +156 -0
- data/lib/hecks/facade.rb +23 -51
- data/lib/hecks/forms/app.rb +21 -76
- data/lib/hecks/forms/banking_presentation.rb +50 -0
- data/lib/hecks/forms/command_form_renderer.rb +10 -29
- data/lib/hecks/forms/field_renderer.rb +17 -156
- data/lib/hecks/forms/field_shape.rb +31 -222
- data/lib/hecks/forms/html.rb +12 -47
- data/lib/hecks/forms/index_renderer.rb +1 -4
- data/lib/hecks/forms/page.rb +3 -9
- data/lib/hecks/forms/params.rb +14 -60
- data/lib/hecks/forms/port_argument.rb +2 -19
- data/lib/hecks/forms/query_form_renderer.rb +4 -79
- data/lib/hecks/forms/record_renderer.rb +8 -75
- data/lib/hecks/forms/record_table.rb +7 -50
- data/lib/hecks/forms/reference_options.rb +3 -30
- data/lib/hecks/forms/value_object_shape.rb +8 -35
- data/lib/hecks/forms.rb +5 -33
- data/lib/hecks/fqn.rb +5 -12
- data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
- data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
- data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
- data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
- data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
- data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
- data/lib/hecks/framework.rb +11 -80
- data/lib/hecks/freezer.rb +5 -39
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
- data/lib/hecks/fuzzing/combination_miner.rb +86 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
- data/lib/hecks/fuzzing/differential.rb +156 -0
- data/lib/hecks/fuzzing/domain_generator.rb +615 -0
- data/lib/hecks/fuzzing/era_boundary.rb +71 -0
- data/lib/hecks/fuzzing/form_census.rb +136 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
- data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
- data/lib/hecks/fuzzing/outside_world.rb +45 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
- data/lib/hecks/fuzzing/properties/guards.rb +190 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
- data/lib/hecks/fuzzing/properties/querying.rb +175 -0
- data/lib/hecks/fuzzing/properties.rb +162 -0
- data/lib/hecks/fuzzing/qa_settings.rb +123 -0
- data/lib/hecks/fuzzing/replay.rb +505 -0
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
- data/lib/hecks/fuzzing/self_consistency.rb +339 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
- data/lib/hecks/fuzzing/shrinker.rb +166 -0
- data/lib/hecks/fuzzing/structural_skips.rb +33 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
- data/lib/hecks/fuzzing/value_generator.rb +216 -0
- data/lib/hecks/fuzzing.rb +23 -0
- data/lib/hecks/grammar/evolve.rb +336 -0
- data/lib/hecks/grammar/expression.bluebook +15 -40
- data/lib/hecks/grammar/translation.bluebook +3 -6
- data/lib/hecks/grammar.rb +16 -41
- data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
- data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
- data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
- data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
- data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
- data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
- data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
- data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
- data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
- data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
- data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
- data/lib/hecks/hecks/adapters/git.adapter +7 -0
- data/lib/hecks/hecks/adapters/git.rb +102 -0
- data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
- data/lib/hecks/hecks/adapters/host_http.rb +65 -0
- data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
- data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
- data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
- data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
- data/lib/hecks/hecks/adapters/local_files.rb +46 -0
- data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
- data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
- data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
- data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
- data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
- data/lib/hecks/hecks/adapters/shell.adapter +7 -0
- data/lib/hecks/hecks/adapters/shell.rb +49 -0
- data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
- data/lib/hecks/hecks/adapters/terminal.rb +74 -0
- data/lib/hecks/hecks/codebase.behaviors +172 -0
- data/lib/hecks/hecks/codebase.bluebook +2453 -0
- data/lib/hecks/hecks/context_map.hecksagon +13 -0
- data/lib/hecks/hecks/custodian.behaviors +179 -0
- data/lib/hecks/hecks/custodian.bluebook +1847 -0
- data/lib/hecks/hecks/deploy.behaviors +74 -0
- data/lib/hecks/hecks/deploy.world +12 -0
- data/lib/hecks/hecks/environments/memory.world +10 -0
- data/lib/hecks/hecks/governance.world +7 -0
- data/lib/hecks/hecks/hecks.behaviors +62 -0
- data/lib/hecks/hecks/hecks.bluebook +124 -0
- data/lib/hecks/hecks/hecks.hecksagon +390 -0
- data/lib/hecks/hecks/hecks.world +42 -0
- data/lib/hecks/ir.rb +14 -83
- data/lib/hecks/kernel_capabilities.rb +194 -0
- data/lib/hecks/language/adapter.bluebook +2 -7
- data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
- data/lib/hecks/language/bluebook/bluebook.bluebook +26 -83
- data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
- data/lib/hecks/language/bluebook/command.bluebook +27 -144
- data/lib/hecks/language/bluebook/entity.bluebook +24 -97
- data/lib/hecks/language/bluebook/policy.bluebook +16 -67
- data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
- data/lib/hecks/language/bluebook/projection.bluebook +126 -60
- data/lib/hecks/language/bluebook/query.bluebook +19 -43
- data/lib/hecks/language/bluebook/shape.bluebook +23 -73
- data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
- data/lib/hecks/language/bluebook/vocabulary.bluebook +83 -198
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
- data/lib/hecks/language/hecksagon/domain_port.bluebook +14 -19
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +26 -25
- data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
- data/lib/hecks/language/oidc.json +35 -0
- data/lib/hecks/language/port.bluebook +4 -17
- data/lib/hecks/language/translation/translation.bluebook +3 -9
- data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
- data/lib/hecks/language/world/wiring.bluebook +1 -3
- data/lib/hecks/language/world/world.bluebook +5 -7
- data/lib/hecks/literal.rb +8 -50
- data/lib/hecks/mcp_stdio_guard.rb +4 -41
- data/lib/hecks/naming.rb +29 -137
- data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
- data/lib/hecks/persistence_legacy_fixture.rb +113 -0
- data/lib/hecks/ports/access_control.rb +7 -23
- data/lib/hecks/ports/agent/answers.rb +5 -23
- data/lib/hecks/ports/agent.rb +47 -172
- data/lib/hecks/ports/authentication.rb +12 -39
- data/lib/hecks/ports/authorization.rb +16 -43
- data/lib/hecks/ports/clock.rb +5 -45
- data/lib/hecks/ports/extraction.rb +7 -12
- data/lib/hecks/ports/identity_assignment.rb +8 -29
- data/lib/hecks/ports/identity_generation.rb +6 -29
- data/lib/hecks/ports/identity_resolution.rb +2 -10
- data/lib/hecks/ports/key_vault.rb +9 -45
- data/lib/hecks/ports/persistence/append_only.rb +61 -64
- data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
- data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
- data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
- data/lib/hecks/ports/persistence/plugin.rb +13 -42
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +44 -144
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +20 -85
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +18 -24
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +19 -76
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +43 -127
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +131 -555
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +50 -47
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
- data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
- data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
- data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
- data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
- data/lib/hecks/ports/persistence/state_codec.rb +47 -108
- data/lib/hecks/ports/persistence.rb +7 -14
- data/lib/hecks/ports/projection.rb +42 -84
- data/lib/hecks/ports/query/in_memory.rb +10 -58
- data/lib/hecks/ports/query/ordering.rb +11 -32
- data/lib/hecks/ports/query.rb +14 -37
- data/lib/hecks/ports.rb +2 -8
- data/lib/hecks/projection_files.rb +285 -0
- data/lib/hecks/projections/bootstrap_table.rb +12 -44
- data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
- data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
- data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
- data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
- data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
- data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
- data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
- data/lib/hecks/projections/deploy/fargate.rb +40 -207
- data/lib/hecks/projections/deploy/lambda.rb +191 -708
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
- data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
- data/lib/hecks/projections/deploy/preview/script.rb +2 -14
- data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
- data/lib/hecks/projections/deploy/preview/template.rb +3 -59
- data/lib/hecks/projections/deploy/preview.rb +2 -67
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
- data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/scripts.rb +4 -57
- data/lib/hecks/projections/deploy/shared.rb +48 -147
- data/lib/hecks/projections/deploy/smoke.rb +4 -61
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
- data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
- data/lib/hecks/projections/deploy/template_diff.rb +5 -57
- data/lib/hecks/projections/diagrams.rb +22 -584
- data/lib/hecks/projections/glossary/html.rb +14 -130
- data/lib/hecks/projections/glossary/markdown.rb +8 -69
- data/lib/hecks/projections/glossary/mermaid.rb +9 -66
- data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
- data/lib/hecks/projections/glossary/sentences.rb +14 -159
- data/lib/hecks/projections/glossary.rb +30 -222
- data/lib/hecks/projections/ir.rb +3 -10
- data/lib/hecks/projections/model/deviations.rb +16 -43
- data/lib/hecks/projections/model.rb +12 -51
- data/lib/hecks/projections/oidc.rb +12 -56
- data/lib/hecks/projections/parser_table.rb +13 -32
- data/lib/hecks/projections/reference.rb +4 -18
- data/lib/hecks/projections/rust_vocabulary.rb +13 -251
- data/lib/hecks/projections/shape.rb +3 -29
- data/lib/hecks/projections/statements.rb +7 -86
- data/lib/hecks/projections/vocabulary.rb +6 -22
- data/lib/hecks/projections.rb +2 -16
- data/lib/hecks/projector/cli_projector.rb +154 -400
- data/lib/hecks/projector/docs_projector.rb +10 -201
- data/lib/hecks/projector/exporter.rb +42 -134
- data/lib/hecks/projector/ir_projector.rb +2 -9
- data/lib/hecks/projector/narrate_projector.rb +15 -189
- data/lib/hecks/projector/target.rb +13 -76
- data/lib/hecks/projector.rb +30 -121
- data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
- data/lib/hecks/quality_control/adapters/agent.rb +58 -0
- data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
- data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
- data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
- data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
- data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
- data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
- data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
- data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
- data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
- data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
- data/lib/hecks/quality_control/cli/child.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
- data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
- data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
- data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
- data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
- data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
- data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
- data/lib/hecks/query_ir.rb +346 -0
- data/lib/hecks/query_ir_mcp.rb +198 -0
- data/lib/hecks/query_specification/common/comparators.rb +5 -26
- data/lib/hecks/query_specification/common/comparison.rb +35 -143
- data/lib/hecks/query_specification/common/dsl.rb +8 -24
- data/lib/hecks/query_specification/common/null_policy.rb +20 -70
- data/lib/hecks/query_specification/common/options.rb +2 -6
- data/lib/hecks/query_specification/common/where_clause.rb +2 -10
- data/lib/hecks/query_specification/field_path.rb +37 -77
- data/lib/hecks/query_specification/hop_path.rb +12 -76
- data/lib/hecks/release/gem_pin.rb +2 -26
- data/lib/hecks/release/runner/ci_publisher.rb +5 -8
- data/lib/hecks/release/runner/clean_tree.rb +42 -0
- data/lib/hecks/release/runner/commands.rb +2 -4
- data/lib/hecks/release/runner/console.rb +6 -0
- data/lib/hecks/release/runner/gem_publisher.rb +15 -9
- data/lib/hecks/release/runner/npm_publisher.rb +9 -53
- data/lib/hecks/release/runner/preflight.rb +29 -20
- data/lib/hecks/release/runner/published.rb +8 -26
- data/lib/hecks/release/runner/tagger.rb +31 -23
- data/lib/hecks/release/runner.rb +60 -38
- data/lib/hecks/rendering.rb +4 -26
- data/lib/hecks/router/namespace_installer.rb +15 -39
- data/lib/hecks/router.rb +4 -8
- data/lib/hecks/runtime/adapter_lookup.rb +124 -0
- data/lib/hecks/runtime/aggregate_lock.rb +8 -33
- data/lib/hecks/runtime/boot_gates.rb +3 -18
- data/lib/hecks/runtime/caller.rb +19 -59
- data/lib/hecks/runtime/capability_graph.rb +5 -21
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +27 -160
- data/lib/hecks/runtime/command_interpreter.rb +124 -306
- data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
- data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
- data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
- data/lib/hecks/runtime/command_rules/emission.rb +8 -21
- data/lib/hecks/runtime/command_rules/references.rb +23 -202
- data/lib/hecks/runtime/dependency_planning.rb +50 -104
- data/lib/hecks/runtime/dispatcher.rb +134 -243
- data/lib/hecks/runtime/entity_element.rb +40 -462
- data/lib/hecks/runtime/entity_interpreter.rb +64 -219
- data/lib/hecks/runtime/errors.rb +32 -115
- data/lib/hecks/runtime/event.rb +3 -25
- data/lib/hecks/runtime/identity.rb +18 -154
- data/lib/hecks/runtime/instance.rb +18 -150
- data/lib/hecks/runtime/interpreting.rb +14 -70
- data/lib/hecks/runtime/invocation.rb +28 -164
- data/lib/hecks/runtime/loader.rb +109 -194
- data/lib/hecks/runtime/outbox.rb +39 -262
- data/lib/hecks/runtime/policy_interpreter.rb +43 -202
- data/lib/hecks/runtime/port_operation_interpreter.rb +28 -97
- data/lib/hecks/runtime/query_interpreter.rb +122 -187
- data/lib/hecks/runtime/reaction_invocation.rb +36 -138
- data/lib/hecks/runtime/reaction_outcome.rb +89 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
- data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
- data/lib/hecks/runtime/reference_hop.rb +8 -81
- data/lib/hecks/runtime/refusal_wording.rb +28 -87
- data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
- data/lib/hecks/runtime/registry/verification.rb +213 -227
- data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
- data/lib/hecks/runtime/registry.rb +156 -247
- data/lib/hecks/runtime/remote_dispatcher.rb +68 -154
- data/lib/hecks/runtime/routing.rb +4 -15
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
- data/lib/hecks/runtime/saga_interpreter.rb +79 -263
- data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
- data/lib/hecks/runtime/tenant_check.rb +6 -68
- data/lib/hecks/runtime/tenant_scope.rb +3 -28
- data/lib/hecks/runtime/value/admission.rb +16 -67
- data/lib/hecks/runtime/value/coercion.rb +162 -760
- data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
- data/lib/hecks/runtime/value/field_checks.rb +172 -0
- data/lib/hecks/runtime/value.rb +15 -84
- data/lib/hecks/runtime.rb +35 -69
- data/lib/hecks/rust_build/conformance.rb +158 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
- data/lib/hecks/rust_build/coverage.rb +218 -0
- data/lib/hecks/rust_build/native_build.rb +124 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
- data/lib/hecks/rust_build/project_rust.rb +332 -0
- data/lib/hecks/rust_build/wasm.rb +105 -0
- data/lib/hecks/rust_build/wasm_browser.rb +113 -0
- data/lib/hecks/rust_build.rb +144 -0
- data/lib/hecks/storehouse.rb +155 -582
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +10 -34
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
- data/lib/hecks/three_zero/forms.yml +127 -0
- data/lib/hecks/three_zero.rb +12 -0
- data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
- data/lib/hecks/tools/comment_style.rb +1070 -0
- data/lib/hecks/tools/corpus_report.rb +87 -0
- data/lib/hecks/tools/deploy_recipe.rb +255 -0
- data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
- data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
- data/lib/hecks/tools/era_projection_backfill.rb +94 -0
- data/lib/hecks/tools/era_reattest.rb +146 -0
- data/lib/hecks/tools/evolve_run.rb +189 -0
- data/lib/hecks/tools/fuzz_sweep.rb +373 -0
- data/lib/hecks/tools/heki_compaction.rb +100 -0
- data/lib/hecks/tools/hoist_local_givens.rb +246 -0
- data/lib/hecks/tools/journal_compaction.rb +89 -0
- data/lib/hecks/tools/oidc_manifests.rb +70 -0
- data/lib/hecks/tools/query_ir_run.rb +81 -0
- data/lib/hecks/tools/regeneration_run.rb +166 -0
- data/lib/hecks/tools/rust_comment_style.rb +642 -0
- data/lib/hecks/tools/sequence_script.rb +62 -0
- data/lib/hecks/tools/tail_merge.rb +117 -0
- data/lib/hecks/tools/tenant_provisioning.rb +142 -0
- data/lib/hecks/tools/translation_audit.rb +208 -0
- data/lib/hecks/tools/translation_scaffold.rb +131 -0
- data/lib/hecks/tools.rb +86 -0
- data/lib/hecks/vendoring/git_environment.rb +2 -8
- data/lib/hecks/vendoring/git_source.rb +4 -10
- data/lib/hecks/vendoring.rb +7 -46
- data/lib/hecks/version.rb +3 -14
- data/lib/hecks.rb +52 -90
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
- data/qa/settings.yml +66 -0
- data/rust/.cargo/config.toml +6 -0
- data/rust/.gitignore +2 -0
- data/rust/Cargo.lock +54 -0
- data/rust/Cargo.toml +61 -0
- data/rust/build/.cargo/config.toml +3 -0
- data/rust/build/.gitignore +1 -0
- data/rust/build/Cargo.lock +7 -0
- data/rust/build/Cargo.toml +19 -0
- data/rust/build/src/build_artifact.rs +47 -0
- data/rust/build/src/cargo_sync.rs +248 -0
- data/rust/build/src/json.rs +421 -0
- data/rust/build/src/lineage_pass.rs +285 -0
- data/rust/build/src/main.rs +51 -0
- data/rust/build/src/optional_pass.rs +150 -0
- data/rust/build/src/pipeline.rs +264 -0
- data/rust/build/src/reserved_names.rs +76 -0
- data/rust/build/src/resolve.rs +124 -0
- data/rust/build/src/root.rs +25 -0
- data/rust/build/src/sidecars.rs +73 -0
- data/rust/build/src/subprocess.rs +51 -0
- data/rust/build/src/tmp.rs +31 -0
- data/rust/codegen/.cargo/config.toml +3 -0
- data/rust/codegen/.gitignore +1 -0
- data/rust/codegen/Cargo.lock +7 -0
- data/rust/codegen/Cargo.toml +20 -0
- data/rust/codegen/src/attr.rs +32 -0
- data/rust/codegen/src/bridging.rs +481 -0
- data/rust/codegen/src/commands.rs +1226 -0
- data/rust/codegen/src/constraints.rs +109 -0
- data/rust/codegen/src/dependency_planning.rs +209 -0
- data/rust/codegen/src/domain_generator.rs +1288 -0
- data/rust/codegen/src/exemplar.rs +230 -0
- data/rust/codegen/src/expr_emitter.rs +87 -0
- data/rust/codegen/src/fielded.rs +192 -0
- data/rust/codegen/src/hecks_naming.rs +33 -0
- data/rust/codegen/src/json.rs +335 -0
- data/rust/codegen/src/json_codec.rs +697 -0
- data/rust/codegen/src/literal.rs +207 -0
- data/rust/codegen/src/main.rs +290 -0
- data/rust/codegen/src/manifest.rs +227 -0
- data/rust/codegen/src/mutations.rs +784 -0
- data/rust/codegen/src/naming.rs +286 -0
- data/rust/codegen/src/ports.rs +208 -0
- data/rust/codegen/src/prelude.rs +116 -0
- data/rust/codegen/src/queries.rs +786 -0
- data/rust/codegen/src/reactions.rs +532 -0
- data/rust/codegen/src/read_models.rs +697 -0
- data/rust/codegen/src/reference_specs.rs +40 -0
- data/rust/codegen/src/registry.rs +966 -0
- data/rust/codegen/src/reserved_names.rs +76 -0
- data/rust/codegen/src/shared.rs +93 -0
- data/rust/codegen/src/skip_reason.rs +25 -0
- data/rust/codegen/src/types.rs +362 -0
- data/rust/host/.cargo/config.toml +3 -0
- data/rust/host/.gitignore +1 -0
- data/rust/host/Cargo.lock +4297 -0
- data/rust/host/Cargo.toml +61 -0
- data/rust/host/HECKS_RELEASE +1 -0
- data/rust/host/rds-ca-bundle.pem +2736 -0
- data/rust/host/src/api.rs +1596 -0
- data/rust/host/src/approval.rs +652 -0
- data/rust/host/src/auth.rs +1287 -0
- data/rust/host/src/bin/expr_harness.rs +46 -0
- data/rust/host/src/bin/lineage_harness.rs +119 -0
- data/rust/host/src/bin/mint_harness.rs +135 -0
- data/rust/host/src/checkout.rs +265 -0
- data/rust/host/src/dispatch.rs +1443 -0
- data/rust/host/src/expr_json.rs +1117 -0
- data/rust/host/src/field_hints.rs +52 -0
- data/rust/host/src/ir.rs +733 -0
- data/rust/host/src/journal.rs +874 -0
- data/rust/host/src/lambda_client.rs +378 -0
- data/rust/host/src/log.rs +147 -0
- data/rust/host/src/main.rs +474 -0
- data/rust/host/src/mint.rs +1865 -0
- data/rust/host/src/payments/keystore.rs +330 -0
- data/rust/host/src/payments/tests.rs +1400 -0
- data/rust/host/src/payments.rs +767 -0
- data/rust/host/src/presentation.rs +698 -0
- data/rust/host/src/presentation_write.rs +1431 -0
- data/rust/host/src/query_step.rs +149 -0
- data/rust/host/src/rate_limit.rs +904 -0
- data/rust/host/src/reference_transform.rs +241 -0
- data/rust/host/src/reference_validate.rs +338 -0
- data/rust/host/src/resend.rs +296 -0
- data/rust/host/src/secrets.rs +106 -0
- data/rust/host/src/server.rs +663 -0
- data/rust/host/src/storage_shape.rs +175 -0
- data/rust/host/src/test_pg.rs +40 -0
- data/rust/host/src/ui_schema.rs +1351 -0
- data/rust/host/src/wasm_runner.rs +191 -0
- data/rust/host/src/web/newsletter/cooldown.rs +126 -0
- data/rust/host/src/web/newsletter/tests.rs +104 -0
- data/rust/host/src/web/newsletter.rs +310 -0
- data/rust/host/src/web/newsletter_send.rs +357 -0
- data/rust/host/src/web/registrations.rs +1505 -0
- data/rust/host/src/web.rs +2937 -0
- data/rust/lsp/.gitignore +1 -0
- data/rust/lsp/Cargo.lock +7 -0
- data/rust/lsp/Cargo.toml +16 -0
- data/rust/lsp/README.md +133 -0
- data/rust/lsp/src/diagnostics.rs +165 -0
- data/rust/lsp/src/json.rs +385 -0
- data/rust/lsp/src/main.rs +399 -0
- data/rust/lsp/src/outline.rs +177 -0
- data/rust/lsp/src/rpc.rs +67 -0
- data/rust/parser/.cargo/config.toml +5 -0
- data/rust/parser/.gitignore +1 -0
- data/rust/parser/Cargo.lock +7 -0
- data/rust/parser/Cargo.toml +21 -0
- data/rust/parser/src/build/closed_sets.rs +27 -0
- data/rust/parser/src/build/identity.rs +276 -0
- data/rust/parser/src/build/mod.rs +12 -0
- data/rust/parser/src/build/naming.rs +141 -0
- data/rust/parser/src/build/pattern_subset.rs +161 -0
- data/rust/parser/src/build/query_derive.rs +72 -0
- data/rust/parser/src/build/query_inference.rs +269 -0
- data/rust/parser/src/build/query_options.rs +100 -0
- data/rust/parser/src/build/read_model.rs +94 -0
- data/rust/parser/src/build/references.rs +32 -0
- data/rust/parser/src/canonical.rs +114 -0
- data/rust/parser/src/diag.rs +55 -0
- data/rust/parser/src/emit.rs +953 -0
- data/rust/parser/src/expr/ast_json.rs +147 -0
- data/rust/parser/src/expr/evaluator.rs +81 -0
- data/rust/parser/src/expr/mod.rs +84 -0
- data/rust/parser/src/expr/resolver.rs +497 -0
- data/rust/parser/src/ir.rs +387 -0
- data/rust/parser/src/keywords.rs +585 -0
- data/rust/parser/src/lex.rs +662 -0
- data/rust/parser/src/main.rs +252 -0
- data/rust/parser/src/parse/aggregate.rs +612 -0
- data/rust/parser/src/parse/chapter.rs +517 -0
- data/rust/parser/src/parse/command.rs +637 -0
- data/rust/parser/src/parse/domain_port.rs +127 -0
- data/rust/parser/src/parse/entity.rs +527 -0
- data/rust/parser/src/parse/file.rs +67 -0
- data/rust/parser/src/parse/hecksagon.rs +231 -0
- data/rust/parser/src/parse/lifecycle.rs +162 -0
- data/rust/parser/src/parse/mod.rs +1262 -0
- data/rust/parser/src/parse/policy.rs +87 -0
- data/rust/parser/src/parse/process_manager.rs +305 -0
- data/rust/parser/src/parse/query.rs +100 -0
- data/rust/parser/src/parse/read_model.rs +214 -0
- data/rust/parser/src/parse/value_object.rs +250 -0
- data/rust/parser/src/ruby_value.rs +422 -0
- data/rust/project/bridging.rb +257 -0
- data/rust/project/commands.rb +877 -0
- data/rust/project/constraints.rb +86 -0
- data/rust/project/dependency_planning.rb +178 -0
- data/rust/project/domain_generator.rb +980 -0
- data/rust/project/exemplar.rb +148 -0
- data/rust/project/expr_emitter.rb +72 -0
- data/rust/project/fielded.rb +141 -0
- data/rust/project/json_codec.rb +641 -0
- data/rust/project/mutations.rb +721 -0
- data/rust/project/naming.rb +205 -0
- data/rust/project/ports.rb +65 -0
- data/rust/project/queries.rb +493 -0
- data/rust/project/reactions.rb +317 -0
- data/rust/project/read_models.rb +690 -0
- data/rust/project/reference_specs.rb +27 -0
- data/rust/project/registry.rb +396 -0
- data/rust/project/skip_reason.rb +29 -0
- data/rust/project/types.rb +225 -0
- data/rust/project/write_if_changed.rb +83 -0
- data/rust/project.rb +30 -0
- data/rust/project_rust_pipeline.rb +383 -0
- data/rust/src/exemplar/commands.rs +279 -0
- data/rust/src/exemplar/constraints.rs +18 -0
- data/rust/src/exemplar/fielded.rs +280 -0
- data/rust/src/exemplar/json.rs +277 -0
- data/rust/src/exemplar/mod.rs +17 -0
- data/rust/src/exemplar/mutations.rs +80 -0
- data/rust/src/exemplar/queries.rs +33 -0
- data/rust/src/exemplar/reactions.rs +88 -0
- data/rust/src/exemplar/read_models.rs +48 -0
- data/rust/src/exemplar/registry.rs +166 -0
- data/rust/src/exemplar/types.rs +44 -0
- data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
- data/rust/src/kernel/attribute_shapes/list.rs +18 -0
- data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
- data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
- data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
- data/rust/src/kernel/cli.rs +585 -0
- data/rust/src/kernel/dispatch.rs +1090 -0
- data/rust/src/kernel/expr.rs +458 -0
- data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
- data/rust/src/kernel/expression_operators/comparison.rs +48 -0
- data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
- data/rust/src/kernel/expression_operators/logical.rs +13 -0
- data/rust/src/kernel/expression_operators/membership.rs +18 -0
- data/rust/src/kernel/expression_operators/mod.rs +68 -0
- data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
- data/rust/src/kernel/expression_operators/positional.rs +126 -0
- data/rust/src/kernel/expression_operators/presence.rs +101 -0
- data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
- data/rust/src/kernel/expression_operators/sized.rs +35 -0
- data/rust/src/kernel/expression_operators/text.rs +79 -0
- data/rust/src/kernel/expression_operators/to_string.rs +18 -0
- data/rust/src/kernel/json.rs +779 -0
- data/rust/src/kernel/mod.rs +133 -0
- data/rust/src/kernel/named_query.rs +219 -0
- data/rust/src/kernel/orchestrate.rs +1810 -0
- data/rust/src/kernel/pattern.rs +307 -0
- data/rust/src/kernel/query_comparators.rs +206 -0
- data/rust/src/kernel/query_ordering.rs +103 -0
- data/rust/src/kernel/read_model.rs +914 -0
- data/rust/src/kernel/reference_lookup.rs +149 -0
- data/rust/src/kernel/repository.rs +468 -0
- data/rust/src/kernel/routing.rs +612 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
- data/rust/src/kernel/vocab/field_hint.rs +70 -0
- data/rust/src/kernel/vocab/mod.rs +20 -0
- data/rust/src/kernel/vocab/query_comparator.rs +65 -0
- data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
- data/rust/src/lib.rs +8 -0
- data/rust/src/main.rs +16 -0
- data/rust/web/.gitignore +1 -0
- data/rust/web/Cargo.lock +119 -0
- data/rust/web/Cargo.toml +18 -0
- data/rust/web/src/lib.rs +8 -0
- metadata +478 -12
- data/lib/hecks/facade/cli_door.rb +0 -178
- data/lib/hecks/facade/cli_runner.rb +0 -271
- data/lib/hecks/facade/handle.rb +0 -293
- data/lib/hecks/facade/json_door.rb +0 -247
- data/lib/hecks/facade/surface/aggregate_door.rb +0 -222
- data/lib/hecks/facade/surface/chapter.rb +0 -116
- data/lib/hecks/facade/surface.rb +0 -61
- data/lib/hecks/mcp_door_scope.rb +0 -176
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
module Properties
|
|
4
|
+
# Guard and authorization properties over a replayed history: refusals name declared rules,
|
|
5
|
+
# tenant scoping holds, and guard violations are refused.
|
|
6
|
+
module Guards
|
|
7
|
+
# Checks tenant-scoped query answers and refusals against TenantScope.apply's contract.
|
|
8
|
+
#
|
|
9
|
+
# Uses hand-built fixtures: the only corpus query declaring `authorize` takes no
|
|
10
|
+
# attributes, so the generator cannot supply a `tenant:`.
|
|
11
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
12
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
13
|
+
#
|
|
14
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
15
|
+
# @return [true, String] true if every answer and refusal agrees with the contract
|
|
16
|
+
def authorize_scopes_or_refuses(history)
|
|
17
|
+
bluebooks = history.fetch(:bluebooks)
|
|
18
|
+
|
|
19
|
+
offenders = history.fetch(:queries).filter_map do |asked|
|
|
20
|
+
next unless asked[:query].is_a?(String) && asked[:query].include?("::")
|
|
21
|
+
|
|
22
|
+
declared = query_for_verb(bluebooks, asked[:query])
|
|
23
|
+
authorization = declared&.authorization
|
|
24
|
+
tenant = authorization&.tenant&.to_sym
|
|
25
|
+
next unless tenant
|
|
26
|
+
|
|
27
|
+
args = asked[:args] || {}
|
|
28
|
+
tenant_given = args.key?(tenant)
|
|
29
|
+
|
|
30
|
+
if asked[:error]
|
|
31
|
+
next if tenant_given
|
|
32
|
+
next if asked[:error].to_s.include?("declares authorize with tenant: #{tenant}")
|
|
33
|
+
|
|
34
|
+
"#{asked[:query]} #{args.inspect} refused with no #{tenant}: given, but not with the declared " \
|
|
35
|
+
"tenant_required wording (#{asked[:error]})"
|
|
36
|
+
elsif !tenant_given
|
|
37
|
+
"#{asked[:query]} #{args.inspect} answered successfully with no #{tenant}: given, but #{declared.name} " \
|
|
38
|
+
"declares authorize with tenant: #{tenant}"
|
|
39
|
+
else
|
|
40
|
+
wanted = args[tenant].to_s
|
|
41
|
+
mismatched = asked[:rows].find do |row|
|
|
42
|
+
Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(row, tenant)).to_s != wanted
|
|
43
|
+
end
|
|
44
|
+
next unless mismatched
|
|
45
|
+
|
|
46
|
+
"#{asked[:query]} #{args.inspect} answered a row whose #{tenant} disagrees with the given " \
|
|
47
|
+
"#{wanted.inspect}: #{mismatched.inspect}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
offenders.empty? || offenders.join("; ")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Raised classes that mark a guard refusal; the message alone cannot tell it from
|
|
55
|
+
# the other "X refused — Y" wordings.
|
|
56
|
+
GUARD_REFUSAL_KINDS = %w[Hecks::Runtime::GivenNotMet Hecks::Runtime::EnsuresNotMet].freeze
|
|
57
|
+
|
|
58
|
+
# Checks that every given/ensures refusal quotes a description its command declares.
|
|
59
|
+
def guard_refusals_are_declared(history)
|
|
60
|
+
bluebooks = history.fetch(:bluebooks)
|
|
61
|
+
|
|
62
|
+
offenders = history.fetch(:refusals).filter_map do |refusal|
|
|
63
|
+
next unless GUARD_REFUSAL_KINDS.include?(refusal[:kind])
|
|
64
|
+
|
|
65
|
+
match = refusal[:error].to_s.match(/\A(.+) refused — (.+)\z/)
|
|
66
|
+
next "#{refusal[:verb]} raised #{refusal[:kind]} with unparseable message #{refusal[:error].inspect}" unless match
|
|
67
|
+
|
|
68
|
+
command = command_for_verb(bluebooks, refusal[:verb])
|
|
69
|
+
next "#{refusal[:verb]} raised #{refusal[:kind]}, but no declared command resolves that verb" unless command
|
|
70
|
+
|
|
71
|
+
declared = effective_guard_descriptions(bluebooks, refusal[:verb], command)
|
|
72
|
+
next if declared.include?(match[2])
|
|
73
|
+
|
|
74
|
+
"#{refusal[:verb]} refused — #{match[2].inspect} — but #{command.hecks_name} declares no given " \
|
|
75
|
+
"or ensures with that description (it declares #{declared.inspect})"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
offenders.empty? || offenders.join("; ")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Checks that no stored record references a record carrying a different tenant value.
|
|
82
|
+
#
|
|
83
|
+
# Commands cannot declare `authorize`, so nothing refuses a cross-tenant write.
|
|
84
|
+
# Tenants compare via Query::InMemory.comparable: value objects with different
|
|
85
|
+
# declared names are unequal under Value#== even for the same tenant.
|
|
86
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
87
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
88
|
+
def commands_respect_tenant_scope(history)
|
|
89
|
+
bluebooks = history.fetch(:bluebooks)
|
|
90
|
+
instances = history.fetch(:instances)
|
|
91
|
+
|
|
92
|
+
offenders = instances.flat_map do |key, state|
|
|
93
|
+
domain_name = key.split("::").first
|
|
94
|
+
aggregate_name = key.split("::").last.split("#").first
|
|
95
|
+
aggregate = bluebooks[domain_name]&.aggregate(aggregate_name)
|
|
96
|
+
next [] unless aggregate
|
|
97
|
+
|
|
98
|
+
own_tenant_field = tenant_field_for(aggregate)
|
|
99
|
+
next [] unless own_tenant_field && state.key?(own_tenant_field)
|
|
100
|
+
|
|
101
|
+
own_tenant = Ports::Query::InMemory.comparable(state[own_tenant_field])
|
|
102
|
+
|
|
103
|
+
aggregate.attributes.filter_map do |attribute|
|
|
104
|
+
next unless attribute.type.is_a?(Bluebook::Reference)
|
|
105
|
+
|
|
106
|
+
target = bluebooks[domain_name]&.aggregate(attribute.type.target_name)
|
|
107
|
+
target_tenant_field = target && tenant_field_for(target)
|
|
108
|
+
next unless target_tenant_field
|
|
109
|
+
|
|
110
|
+
target_id = state[attribute.name]
|
|
111
|
+
next unless target_id
|
|
112
|
+
|
|
113
|
+
target_state = instances["#{domain_name}::#{target.name}##{target_id}"]
|
|
114
|
+
next unless target_state&.key?(target_tenant_field)
|
|
115
|
+
|
|
116
|
+
target_tenant = Ports::Query::InMemory.comparable(target_state[target_tenant_field])
|
|
117
|
+
next if target_tenant == own_tenant
|
|
118
|
+
|
|
119
|
+
"#{key} (#{own_tenant_field}: #{own_tenant.inspect}) references #{attribute.name}: #{target_id.inspect}, " \
|
|
120
|
+
"but #{domain_name}::#{target.name}##{target_id} carries #{target_tenant_field}: " \
|
|
121
|
+
"#{target_tenant.inspect} — a cross-tenant write nothing refused"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
offenders.empty? || offenders.join("; ")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The field named by `authorize ..., tenant:` on any of the aggregate's queries, or nil.
|
|
129
|
+
def tenant_field_for(aggregate)
|
|
130
|
+
authorization = aggregate.queries.filter_map(&:authorization).find(&:tenant)
|
|
131
|
+
authorization&.tenant&.to_sym
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# A command's own guard descriptions plus those of every command it delegates to.
|
|
135
|
+
#
|
|
136
|
+
# A delegating door refuses with its target's words, so both sets count. Resolved
|
|
137
|
+
# against the full `bluebooks` map: a verb's domain is not always `history[:bluebook]`.
|
|
138
|
+
def effective_guard_descriptions(bluebooks, verb, command)
|
|
139
|
+
own = command.guard_descriptions
|
|
140
|
+
delegated = command.mutations.select { |m| m.op == :delegate }.flat_map do |delegation|
|
|
141
|
+
domain, aggregate_name, = Naming.split_verb(verb)
|
|
142
|
+
target = command_for_verb(bluebooks, "#{domain}::#{aggregate_name}.#{delegation.target}")
|
|
143
|
+
target ? target.guard_descriptions : []
|
|
144
|
+
end
|
|
145
|
+
own + delegated
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Resolves a dispatched verb to the declared command it names, or nil.
|
|
149
|
+
def command_for_verb(bluebooks, verb)
|
|
150
|
+
domain, aggregate_name, command_path = Naming.split_verb(verb)
|
|
151
|
+
return nil unless command_path
|
|
152
|
+
|
|
153
|
+
bluebook = bluebooks[domain]
|
|
154
|
+
return nil unless bluebook
|
|
155
|
+
|
|
156
|
+
aggregate = bluebook.aggregate(aggregate_name)
|
|
157
|
+
return nil unless aggregate
|
|
158
|
+
|
|
159
|
+
if command_path.include?(".")
|
|
160
|
+
entity_name, sub = command_path.split(".", 2)
|
|
161
|
+
entity = aggregate.entities.find { |e| e.hecks_name == entity_name }
|
|
162
|
+
entity&.command(sub)
|
|
163
|
+
else
|
|
164
|
+
aggregate.command(command_path)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Recomputes enforce_givens/enforce_lifecycle_guard against Replay's pre-dispatch
|
|
169
|
+
# snapshot and compares the result with what the real dispatch did.
|
|
170
|
+
#
|
|
171
|
+
# Catches a guard that silently stopped firing, which never appears in
|
|
172
|
+
# history[:refusals]. Any other refusal class, or a success, counts as "did not fire".
|
|
173
|
+
#
|
|
174
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
175
|
+
# @return [true, String] true if the recomputed and actual outcomes agree for every check
|
|
176
|
+
def lifecycle_guard_and_given_violations_are_refused(history)
|
|
177
|
+
offenders = history.fetch(:guard_checks).filter_map do |check|
|
|
178
|
+
next if check[:recomputed_refused] == check[:actual_refused]
|
|
179
|
+
|
|
180
|
+
"#{check[:verb]} — independently recomputing enforce_givens/enforce_lifecycle_guard against the " \
|
|
181
|
+
"pre-dispatch state says #{check[:recomputed_refused] ? "refused (#{check[:recomputed_kind]})" : 'admitted'}, " \
|
|
182
|
+
"but the real dispatch #{check[:actual_refused] ? "refused (#{check[:actual_kind]})" : 'admitted it'}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
offenders.empty? || offenders.join("; ")
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
module Properties
|
|
4
|
+
# Stored-record, saga-rehydration, fan-out, and read-model-aggregation
|
|
5
|
+
# properties, extended into Properties.
|
|
6
|
+
module InvariantsAndAggregation
|
|
7
|
+
# Every stored record — and every entity nested inside it — still
|
|
8
|
+
# satisfies its own declared invariants, re-checked independently of
|
|
9
|
+
# whichever call site (Admissibility#enforce_invariants) was supposed
|
|
10
|
+
# to have refused a violation live.
|
|
11
|
+
def stored_records_satisfy_declared_invariants(history)
|
|
12
|
+
bluebooks = history.fetch(:bluebooks)
|
|
13
|
+
|
|
14
|
+
offenders = history.fetch(:instances).filter_map do |key, state|
|
|
15
|
+
domain_name = key.split("::").first
|
|
16
|
+
aggregate_name = key.split("::").last.split("#").first
|
|
17
|
+
bluebook = bluebooks[domain_name]
|
|
18
|
+
aggregate = bluebook&.aggregate(aggregate_name)
|
|
19
|
+
next unless aggregate
|
|
20
|
+
|
|
21
|
+
violated = aggregate.invariants.find do |invariant|
|
|
22
|
+
!Bluebook::Expression::Evaluator.call(invariant.canonical, state)
|
|
23
|
+
end
|
|
24
|
+
next "#{key} violates #{aggregate_name}'s own declared invariant #{violated.description.inspect}" if violated
|
|
25
|
+
|
|
26
|
+
check_piece_invariants(aggregate, state, key)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
offenders.empty? || offenders.join("; ")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A piece's own invariant, checked against every element its owner's
|
|
33
|
+
# `list_of` field holds — reapplies Admissibility#check_entity_invariants'
|
|
34
|
+
# own lookup against a plain Hash state rather than a live Instance.
|
|
35
|
+
def check_piece_invariants(owner_construct, owner_state, key)
|
|
36
|
+
owner_construct.entities.each do |entity|
|
|
37
|
+
next if entity.invariants.empty?
|
|
38
|
+
|
|
39
|
+
list_attr = owner_construct.attributes.find { |a| a.list? && a.type.to_s == entity.hecks_name }
|
|
40
|
+
next unless list_attr
|
|
41
|
+
|
|
42
|
+
Array(owner_state[list_attr.name]).each do |element|
|
|
43
|
+
violated = entity.invariants.find do |invariant|
|
|
44
|
+
!Bluebook::Expression::Evaluator.call(invariant.canonical, element)
|
|
45
|
+
end
|
|
46
|
+
if violated
|
|
47
|
+
return "#{key}'s own #{entity.hecks_name} violates its declared invariant " \
|
|
48
|
+
"#{violated.description.inspect}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
nested = check_piece_invariants(entity, element, key)
|
|
52
|
+
return nested if nested
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A saga instance's own state is one the process manager declares, and
|
|
59
|
+
# its memory survives the same JSON round-trip `SagaInterpreter#checkpoint`'s
|
|
60
|
+
# own `deep_copy` performs (mirrored here, a private instance method
|
|
61
|
+
# with no registry to hand it).
|
|
62
|
+
def sagas_rehydrate_cleanly(history)
|
|
63
|
+
bluebook = history.fetch(:bluebook)
|
|
64
|
+
process_managers = bluebook.process_managers.to_h { |pm| [pm.name, pm] }
|
|
65
|
+
|
|
66
|
+
offenders = history.fetch(:saga_instances).flat_map do |pm_name, conversations|
|
|
67
|
+
pm = process_managers[pm_name]
|
|
68
|
+
|
|
69
|
+
conversations.filter_map do |correlation, instance|
|
|
70
|
+
problems = []
|
|
71
|
+
|
|
72
|
+
problems << "holds state #{instance[:state].inspect}, which #{pm_name} never declares" \
|
|
73
|
+
if pm && !pm.declares_state?(instance[:state])
|
|
74
|
+
|
|
75
|
+
rehydrated = JSON.parse(JSON.generate(instance[:memory]), symbolize_names: true)
|
|
76
|
+
if rehydrated != instance[:memory]
|
|
77
|
+
problems << "memory does not survive its own checkpoint round-trip " \
|
|
78
|
+
"(checkpointed #{instance[:memory].inspect}, rehydrated #{rehydrated.inspect})"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
next if problems.empty?
|
|
82
|
+
|
|
83
|
+
"#{pm_name}##{correlation.inspect}: #{problems.join(' and ')}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
offenders.empty? || offenders.join("; ")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A `for_each` policy dispatches exactly once per row its declared query
|
|
91
|
+
# answers, checked against `Replay.expected_fan_out_rows`'s independent
|
|
92
|
+
# computation. `expected_row_ids` is `nil`, not `[]`, when `policy.where`
|
|
93
|
+
# never held — no dispatch is the claim then, not "dispatched to zero rows."
|
|
94
|
+
def fanout_dispatches_once_per_matching_row(history)
|
|
95
|
+
offenders = history.fetch(:fan_outs).filter_map do |finding|
|
|
96
|
+
expected = finding[:expected_row_ids]
|
|
97
|
+
actual = finding[:actual_row_ids].sort
|
|
98
|
+
|
|
99
|
+
if expected.nil?
|
|
100
|
+
next if actual.empty?
|
|
101
|
+
|
|
102
|
+
next "#{finding[:policy]} on #{finding[:on]}: where did not hold, but dispatched to #{actual.inspect}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
next if actual == expected
|
|
106
|
+
|
|
107
|
+
"#{finding[:policy]} on #{finding[:on]}: for_each answered #{expected.inspect}, " \
|
|
108
|
+
"but the reaction log shows dispatches to #{actual.inspect}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
offenders.empty? || offenders.join("; ")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A `count`/`median` report's reduced scalar matches the same reduction
|
|
115
|
+
# done independently over the same eligible rows, reusing FieldPath.dig
|
|
116
|
+
# and InMemory.comparable/.holds? — the same calls the interpreter
|
|
117
|
+
# itself makes, so this oracle can't drift from what a field read or a
|
|
118
|
+
# `where` clause means without the interpreter drifting identically.
|
|
119
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
120
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
121
|
+
def aggregation_matches_recompute(history)
|
|
122
|
+
bluebook = history.fetch(:bluebook)
|
|
123
|
+
|
|
124
|
+
offenders = history.fetch(:queries).filter_map do |asked|
|
|
125
|
+
next if asked[:error]
|
|
126
|
+
|
|
127
|
+
domain, name = asked[:query].to_s.split(".", 2)
|
|
128
|
+
next unless name && domain == bluebook.name
|
|
129
|
+
|
|
130
|
+
model = bluebook.read_model(name)
|
|
131
|
+
next unless model&.reducing?
|
|
132
|
+
|
|
133
|
+
reduced_head = model.aggregate_heads.find { |head| head[:many] }
|
|
134
|
+
next unless reduced_head
|
|
135
|
+
|
|
136
|
+
rows = eligible_rows(bluebook, asked.fetch(:instances_at), domain, model, reduced_head, asked[:args] || {})
|
|
137
|
+
expected = recompute_reduction(model, rows)
|
|
138
|
+
actual = asked[:rows]&.first&.dig(reduced_head[:as])
|
|
139
|
+
next if actual == expected
|
|
140
|
+
|
|
141
|
+
"#{asked[:query]} #{asked[:args].inspect} answered #{actual.inspect} for #{reduced_head[:as]}, " \
|
|
142
|
+
"but recomputing independently from #{rows.length} eligible row(s) gives #{expected.inspect}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
offenders.empty? || offenders.join("; ")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# ADR 0078's siblings of `count`/`median`, each an independent fold over the same
|
|
149
|
+
# eligible rows — never reusing ReadModelInterpreter#reduce's own arithmetic, only the
|
|
150
|
+
# shared field-reading primitives `recompute_median` already relied on.
|
|
151
|
+
def recompute_reduction(model, rows)
|
|
152
|
+
return rows.length if model.count?
|
|
153
|
+
return recompute_median(rows, model.median_field) if model.median_field
|
|
154
|
+
return recompute_values(rows, model.sum_field).sum if model.sum_field
|
|
155
|
+
return recompute_avg(rows, model.avg_field) if model.avg_field
|
|
156
|
+
return recompute_values(rows, model.min_field).min if model.min_field
|
|
157
|
+
return recompute_values(rows, model.max_field).max if model.max_field
|
|
158
|
+
return recompute_percentile(rows, model.percentile_field, model.percentile_at) if model.percentile_field
|
|
159
|
+
return recompute_values(rows, model.any_field).any? if model.any_field
|
|
160
|
+
|
|
161
|
+
recompute_values(rows, model.all_field).all?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def recompute_values(rows, field)
|
|
165
|
+
rows.map { |state| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, field)) }.compact
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def recompute_avg(rows, field)
|
|
169
|
+
values = recompute_values(rows, field)
|
|
170
|
+
values.empty? ? nil : values.sum.to_f / values.length
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Same linear-interpolation formula as `ReadModelInterpreter#percentile`, written
|
|
174
|
+
# independently here rather than called, for the same reason `recompute_median`
|
|
175
|
+
# already is: an oracle that calls the interpreter's own fold agrees with a bug in it
|
|
176
|
+
# by construction (ADR 0061's `nest_rows` lesson).
|
|
177
|
+
def recompute_percentile(rows, field, at)
|
|
178
|
+
values = recompute_values(rows, field).sort
|
|
179
|
+
return nil if values.empty?
|
|
180
|
+
|
|
181
|
+
position = at * (values.length - 1)
|
|
182
|
+
lower = position.floor
|
|
183
|
+
fraction = position - lower
|
|
184
|
+
fraction.zero? ? values[lower] : values[lower] + (fraction * (values[lower + 1] - values[lower]))
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# aggregation_matches_recompute's own shape, extended from reducing a
|
|
188
|
+
# many-side head to a scalar to nesting it (ADR 0061, decision D1): a
|
|
189
|
+
# group_by leaf holds one row, so two eligible rows sharing a full key
|
|
190
|
+
# path not covering identity means the ask must have refused.
|
|
191
|
+
def group_by_matches_recompute(history)
|
|
192
|
+
bluebook = history.fetch(:bluebook)
|
|
193
|
+
|
|
194
|
+
offenders = history.fetch(:queries).filter_map do |asked|
|
|
195
|
+
domain, name = asked[:query].to_s.split(".", 2)
|
|
196
|
+
next unless name && domain == bluebook.name
|
|
197
|
+
|
|
198
|
+
model = bluebook.read_model(name)
|
|
199
|
+
next unless model&.group_by&.any?
|
|
200
|
+
|
|
201
|
+
grouped_head = model.aggregate_heads.find { |head| head[:many] }
|
|
202
|
+
next unless grouped_head
|
|
203
|
+
|
|
204
|
+
group_by_offense(bluebook, asked, model, grouped_head)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
offenders.empty? || offenders.join("; ")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Judges one `group_by` ask against the rows it was eligible to see:
|
|
211
|
+
# refused exactly when two of them share a checked key path, and
|
|
212
|
+
# otherwise nested exactly as `nest_rows` nests them.
|
|
213
|
+
def group_by_offense(bluebook, asked, model, grouped_head)
|
|
214
|
+
rows = eligible_rows(bluebook, asked.fetch(:instances_at), bluebook.name, model, grouped_head, asked[:args] || {})
|
|
215
|
+
materialized = rows.map { |state| Runtime::Value.materialize_unwrapped(state) }
|
|
216
|
+
shared = shared_key_paths(materialized, model.group_by_fields)
|
|
217
|
+
checked = !model.groups_by_identity?(bluebook.aggregate(grouped_head[:aggregate]))
|
|
218
|
+
return collision_offense(asked, model.group_by_fields, shared.length, checked) if shared.any?
|
|
219
|
+
return nil if asked[:error]
|
|
220
|
+
return nil if asked[:rows]&.first&.dig(grouped_head[:as]) == nest_rows(materialized, model.group_by_fields)
|
|
221
|
+
|
|
222
|
+
"#{asked[:query]} #{asked[:args].inspect} answered a #{grouped_head[:as]} grouping that disagrees " \
|
|
223
|
+
"with independently nesting group_by #{model.group_by_fields.inspect} over #{rows.length} " \
|
|
224
|
+
"eligible row(s)"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Judges an ask whose eligible rows share at least one full key path:
|
|
228
|
+
# a checked key path must have refused, and an identity-covering one
|
|
229
|
+
# cannot be shared by rows that hold their identity.
|
|
230
|
+
def collision_offense(asked, fields, shared, checked)
|
|
231
|
+
return nil if checked && asked[:error]
|
|
232
|
+
|
|
233
|
+
"#{asked[:query]} #{asked[:args].inspect} #{checked ? 'answered' : 'reached'}, but #{shared} key " \
|
|
234
|
+
"path(s) of group_by #{fields.inspect} are shared by more than one eligible row, " \
|
|
235
|
+
"#{checked ? 'so the ask must refuse' : 'though they cover the identity'}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Every full `group_by` key path more than one row reaches, found by
|
|
239
|
+
# tallying each row's tuple of grouped values, with no nesting at all.
|
|
240
|
+
def shared_key_paths(rows, fields)
|
|
241
|
+
rows.map { |row| fields.map { |field| row[field] } }.tally.select { |_, count| count > 1 }.keys
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# One level of nesting per `group_by` field in declared order; the leaf
|
|
245
|
+
# is the row with every grouped field stripped. Called only when no key
|
|
246
|
+
# path is shared (`shared_key_paths` rules that out), so each leaf holds
|
|
247
|
+
# exactly one row; a second row raises rather than being picked over.
|
|
248
|
+
def nest_rows(rows, fields)
|
|
249
|
+
field, *rest = fields
|
|
250
|
+
rows.group_by { |row| row[field] }.transform_values do |group|
|
|
251
|
+
stripped = group.map { |row| row.reject { |key, _| key == field } }
|
|
252
|
+
next nest_rows(stripped, rest) unless rest.empty?
|
|
253
|
+
raise ArgumentError, "nest_rows reached a key path #{stripped.length} rows share" unless stripped.length == 1
|
|
254
|
+
|
|
255
|
+
stripped[0]
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# The eligible rows a `count`/`median` head reduces: every instance of
|
|
260
|
+
# the reduced head's own aggregate, FK-matched against the report's root
|
|
261
|
+
# reference (if any), then narrowed by the report's own `where` clauses.
|
|
262
|
+
def eligible_rows(bluebook, instances, domain, model, reduced_head, args)
|
|
263
|
+
aggregate = bluebook.aggregate(reduced_head[:aggregate])
|
|
264
|
+
prefix = "#{domain}::#{reduced_head[:aggregate]}#"
|
|
265
|
+
# `id:` merged in, the same shape every live head row carries —
|
|
266
|
+
# count/median never read it, but group_by's own nesting does.
|
|
267
|
+
rows = instances.filter_map { |key, state| state.merge(id: key.split("#").last) if key.start_with?(prefix) }
|
|
268
|
+
|
|
269
|
+
if model.reference_target
|
|
270
|
+
reference_id = args[model.reference_name].to_s
|
|
271
|
+
fk_fields = aggregate.attributes.select do |attribute|
|
|
272
|
+
attribute.reference? && attribute.type.target_name == model.reference_target.to_s
|
|
273
|
+
end.map(&:name)
|
|
274
|
+
|
|
275
|
+
rows = rows.select { |state| fk_fields.any? { |field| state[field].to_s == reference_id } }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
rows.select do |state|
|
|
279
|
+
model.wheres.all? do |clause|
|
|
280
|
+
held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, clause.field))
|
|
281
|
+
Ports::Query::InMemory.holds?(clause, held, args)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# ReadModelInterpreter#median's own definition, reproduced: the true
|
|
287
|
+
# middle for an odd count, the average of the two middles for an even
|
|
288
|
+
# count, `nil` for empty — never zero, so "nothing" isn't "zero."
|
|
289
|
+
def recompute_median(rows, field)
|
|
290
|
+
values = rows.map { |state| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, field)) }
|
|
291
|
+
.compact.sort
|
|
292
|
+
return nil if values.empty?
|
|
293
|
+
|
|
294
|
+
middle = values.length / 2
|
|
295
|
+
values.length.odd? ? values[middle] : (values[middle - 1] + values[middle]) / 2.0
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
require_relative "../nondeterministic"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Fuzzing
|
|
5
|
+
module Properties
|
|
6
|
+
# Properties over declared lifecycle states, saga edges and replay determinism.
|
|
7
|
+
module LifecycleAndReplay
|
|
8
|
+
# Every lifecycle field a replay leaves holds one of the aggregate's declared states.
|
|
9
|
+
#
|
|
10
|
+
# Uses the full state set (`ModelCheck.full_states`), not just `Lifecycle#states`.
|
|
11
|
+
#
|
|
12
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
13
|
+
# @return [true, String] true, or a message naming each offending instance
|
|
14
|
+
def lifecycle_values_are_declared(history)
|
|
15
|
+
bluebook = history.fetch(:bluebook)
|
|
16
|
+
declared = {}
|
|
17
|
+
bluebook.aggregates.each do |aggregate|
|
|
18
|
+
declared[aggregate.hecks_name] = Bluebook::ModelCheck.full_states(aggregate.lifecycle) if aggregate.lifecycle
|
|
19
|
+
end
|
|
20
|
+
return true if declared.empty?
|
|
21
|
+
|
|
22
|
+
offenders = history.fetch(:instances).filter_map do |key, state|
|
|
23
|
+
aggregate_name = key.split("::").last.split("#").first
|
|
24
|
+
states = declared[aggregate_name]
|
|
25
|
+
next unless states
|
|
26
|
+
|
|
27
|
+
lifecycle = bluebook.aggregate(aggregate_name).lifecycle
|
|
28
|
+
value = state[lifecycle.field]
|
|
29
|
+
next if value.nil? || states.include?(value.to_s)
|
|
30
|
+
|
|
31
|
+
"#{key} holds #{lifecycle.field}=#{value.inspect}, which #{aggregate_name} never declares as a state"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
offenders.empty? || offenders.join("; ")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Every logged saga advance follows a `(from_state, to_state)` pair some handler declares.
|
|
38
|
+
#
|
|
39
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
40
|
+
# @return [true, String] true, or a message naming the process manager and undeclared pair
|
|
41
|
+
def saga_advances_follow_declared_handlers(history)
|
|
42
|
+
bluebook = history.fetch(:bluebook)
|
|
43
|
+
edges = Hash.new { |h, k| h[k] = [] }
|
|
44
|
+
bluebook.process_managers.each do |pm|
|
|
45
|
+
pm.handlers.each { |handler| edges[pm.name] << [handler.from_state, handler.to_state] }
|
|
46
|
+
end
|
|
47
|
+
return true if edges.empty?
|
|
48
|
+
|
|
49
|
+
offenders = history.fetch(:sagas).filter_map do |entry|
|
|
50
|
+
next unless entry[:advanced]
|
|
51
|
+
|
|
52
|
+
pair = [entry[:from], entry[:to]]
|
|
53
|
+
next if edges[entry[:process_manager]].include?(pair)
|
|
54
|
+
|
|
55
|
+
"#{entry[:process_manager]} advanced #{pair.inspect}, which no declared handler names"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
offenders.empty? || offenders.join("; ")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Replaying the same steps on a fresh boot yields identical events, refusals and instances.
|
|
62
|
+
#
|
|
63
|
+
# The runtime mints no identity, so drift is nondeterminism (a clock read, hash order).
|
|
64
|
+
# Two independent replays are compared so a corrupted first run cannot agree with itself.
|
|
65
|
+
#
|
|
66
|
+
# @param domain_path [String] the domain directory to boot, such as `"examples/pizzas"`
|
|
67
|
+
# @param steps [Array<Hash>] the step list to replay twice
|
|
68
|
+
# @param adapter [Symbol] persistence adapter (`:memory`, `:postgres`, or `:postgres_era`)
|
|
69
|
+
# @return [true, String] true, or a message naming the step count that diverged
|
|
70
|
+
def replay_is_deterministic(domain_path, steps, adapter: :memory)
|
|
71
|
+
first = Replay.call(domain_path, steps, adapter: adapter)
|
|
72
|
+
second = Replay.call(domain_path, steps, adapter: adapter)
|
|
73
|
+
|
|
74
|
+
# What is stripped is declared in `Nondeterministic::FIELDS`. Outbox rows are stripped
|
|
75
|
+
# per row, not dropped, so status, consumer and event payload stay checked.
|
|
76
|
+
strip_outbox_nondeterminism = lambda do |history|
|
|
77
|
+
traces = Array(history[:outbox_traces]).map do |trace|
|
|
78
|
+
trace.merge(rows: trace[:rows].map do |row|
|
|
79
|
+
Nondeterministic.strip(row, :outbox_row).merge(event: Nondeterministic.strip(row[:event], :event))
|
|
80
|
+
end)
|
|
81
|
+
end
|
|
82
|
+
history.merge(outbox_traces: traces)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
comparable = ->(history) { Nondeterministic.strip(strip_outbox_nondeterminism.call(history), :history) }
|
|
86
|
+
return true if comparable.call(first) == comparable.call(second)
|
|
87
|
+
|
|
88
|
+
"two replays of the same #{steps.length} steps produced different histories"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require_relative "../../bluebook/expression/evaluator"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Fuzzing
|
|
5
|
+
module Properties
|
|
6
|
+
# Checks a replay's `history[:outbox_traces]` against the transactional outbox contract
|
|
7
|
+
# (`Runtime::Outbox`): inline delivery, and no silent policy drops.
|
|
8
|
+
#
|
|
9
|
+
# A `saga:` row is exempt from the reaction_log check: `begin_saga` and `end_saga` have
|
|
10
|
+
# ordinary no-op paths that log nothing, so a delivered saga row without one is fine.
|
|
11
|
+
module Outbox
|
|
12
|
+
# Every outbox row drained inline and no delivered policy row lacks its reaction.
|
|
13
|
+
#
|
|
14
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
15
|
+
# @return [true, String] true, or a message naming each offending row
|
|
16
|
+
def outbox_rows_match_reactions(history)
|
|
17
|
+
bluebooks = history.fetch(:bluebooks, {})
|
|
18
|
+
|
|
19
|
+
offenders = Array(history[:outbox_traces]).flat_map do |trace|
|
|
20
|
+
trace[:rows].flat_map { |row| outbox_row_offenders(row, trace, bluebooks) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
offenders.empty? || offenders.join("; ")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Offending messages for one row: zero or one, empty when the status names no check.
|
|
27
|
+
def outbox_row_offenders(row, trace, bluebooks)
|
|
28
|
+
on = row.dig(:event, :name)
|
|
29
|
+
|
|
30
|
+
case row[:status]
|
|
31
|
+
when "pending", "claimed"
|
|
32
|
+
["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) never drained inline — status stayed " \
|
|
33
|
+
"#{row[:status].inspect} though delivery is inline by contract (Runtime::Outbox's own header)"]
|
|
34
|
+
when "failed"
|
|
35
|
+
["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) failed to deliver: #{row[:error]} — " \
|
|
36
|
+
"a domain refusal never reaches this far; a failed row names a defect in the relay's own consumer " \
|
|
37
|
+
"resolution"]
|
|
38
|
+
when "delivered"
|
|
39
|
+
outbox_delivered_policy_offenders(row, on, trace, bluebooks)
|
|
40
|
+
else
|
|
41
|
+
[]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Offending messages for a delivered `policy:` row that has no `reaction_log` entry.
|
|
46
|
+
#
|
|
47
|
+
# Legitimate only when the policy's `where` does not hold; a saga row gets no such check.
|
|
48
|
+
def outbox_delivered_policy_offenders(row, on, trace, bluebooks)
|
|
49
|
+
kind, fqn = row[:consumer].to_s.split(":", 2)
|
|
50
|
+
return [] unless kind == "policy"
|
|
51
|
+
|
|
52
|
+
home, name = fqn.to_s.split("::", 2)
|
|
53
|
+
return [] if trace[:reactions].any? { |entry| entry[:policy] == name && entry[:on] == on }
|
|
54
|
+
|
|
55
|
+
policy = bluebooks[home]&.policies&.find { |candidate| candidate.name == name }
|
|
56
|
+
# Undeclared policy: inconclusive, not a mismatch.
|
|
57
|
+
return [] unless policy
|
|
58
|
+
# Fan-out counts belong to fanout_dispatches_once_per_matching_row.
|
|
59
|
+
return [] if policy.fans_out?
|
|
60
|
+
|
|
61
|
+
held = independently_re_evaluate_policy_where(policy, row[:event])
|
|
62
|
+
# False or inconclusive (the where raised): never a mismatch.
|
|
63
|
+
return [] if held != true
|
|
64
|
+
|
|
65
|
+
["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) drained as delivered, but no matching " \
|
|
66
|
+
"reaction_log entry exists and the policy's own where clause independently re-evaluates true — " \
|
|
67
|
+
"PolicyInterpreter#deliver only ever returns nil (no reaction_log entry) when where does not hold"]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Re-evaluates the policy's `where` against the row's recorded payload.
|
|
71
|
+
#
|
|
72
|
+
# Reproduces `PolicyInterpreter#where_holds?` rather than calling it, which would only
|
|
73
|
+
# agree with itself. Rescues to nil: an unevaluable where is inconclusive, not false.
|
|
74
|
+
#
|
|
75
|
+
# @param policy [Bluebook::Policy] the policy whose `where` clause is re-evaluated
|
|
76
|
+
# @param event [Hash] the outbox row's own recorded event, read for `:payload`
|
|
77
|
+
# @return [Boolean, nil] whether `where` holds, or nil if it cannot be re-evaluated
|
|
78
|
+
def independently_re_evaluate_policy_where(policy, event)
|
|
79
|
+
return true if policy.where.to_s.empty?
|
|
80
|
+
|
|
81
|
+
payload = (event[:payload] || {}).transform_keys(&:to_sym)
|
|
82
|
+
Bluebook::Expression::Evaluator.call(policy.where, {}, payload)
|
|
83
|
+
rescue StandardError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|