hecks 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +403 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +148 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +131 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +391 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
- data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +285 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +36 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +96 -0
- data/lib/hecks/behaviors/expectations.rb +280 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
- data/lib/hecks/bluebook/assembly/marks.rb +227 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +78 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +68 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
- data/lib/hecks/bluebook/expression/projection.json +170 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +630 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +452 -0
- data/lib/hecks/bluebook/model_check.rb +318 -0
- data/lib/hecks/bluebook/pattern_subset.rb +120 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +86 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +58 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +389 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +116 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +154 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +300 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +170 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +53 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +132 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +115 -0
- data/lib/hecks/forms/record_table.rb +63 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
- data/lib/hecks/fuzzing/properties.rb +1146 -0
- data/lib/hecks/fuzzing/replay.rb +635 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
- data/lib/hecks/fuzzing/value_generator.rb +177 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +291 -0
- data/lib/hecks/grammar/expression.bluebook +417 -0
- data/lib/hecks/grammar/expression_operators.json +1152 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +437 -0
- data/lib/hecks/language/bluebook/entity.bluebook +316 -0
- data/lib/hecks/language/bluebook/policy.bluebook +172 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +313 -0
- data/lib/hecks/language/port.bluebook +92 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +121 -0
- data/lib/hecks/ports/access_control.port +4 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +4 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +4 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +4 -0
- data/lib/hecks/ports/authorization.rb +59 -0
- data/lib/hecks/ports/clock.port +4 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +4 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +4 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +4 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +4 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +102 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +18 -0
- data/lib/hecks/ports/persistence/lineage.rb +277 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +76 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +413 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +31 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +437 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +141 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +392 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +80 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +89 -0
- data/lib/hecks/query_specification/hop_path.rb +130 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/caller.rb +50 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
- data/lib/hecks/runtime/command_interpreter.rb +413 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +181 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +231 -0
- data/lib/hecks/runtime/dispatcher.rb +286 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +228 -0
- data/lib/hecks/runtime/era_check.rb +136 -0
- data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/runtime/era_guard.rb +193 -0
- data/lib/hecks/runtime/era_tamper.rb +61 -0
- data/lib/hecks/runtime/errors.rb +100 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +128 -0
- data/lib/hecks/runtime/instance.rb +120 -0
- data/lib/hecks/runtime/interpreting.rb +69 -0
- data/lib/hecks/runtime/loader.rb +143 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +254 -0
- data/lib/hecks/runtime/reaction_invocation.rb +245 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +124 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
- data/lib/hecks/runtime/registry/verification.rb +158 -0
- data/lib/hecks/runtime/registry.rb +218 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +301 -0
- data/lib/hecks/runtime/storage_shape.rb +120 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +112 -0
- data/lib/hecks/runtime/value/coercion.rb +500 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +95 -0
- data/lib/hecks/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/translation/audit/layer_one.rb +37 -0
- data/lib/hecks/translation/audit/layer_two.rb +77 -0
- data/lib/hecks/translation/audit/unfed_report.rb +42 -0
- data/lib/hecks/translation/audit.rb +70 -0
- data/lib/hecks/translation/reattest.rb +72 -0
- data/lib/hecks/translation/rule_compiler.rb +120 -0
- data/lib/hecks/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/translation/scaffold.rb +30 -0
- data/lib/hecks/translation.rb +11 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +213 -0
- data/lib/hecks.rb +118 -6
- metadata +401 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "tmpdir"
|
|
3
|
+
require_relative "isolated_boot"
|
|
4
|
+
require_relative "../query_specification/common/comparators"
|
|
5
|
+
require_relative "../query_specification/common/where_clause"
|
|
6
|
+
require_relative "../query_specification/field_path"
|
|
7
|
+
require_relative "../ports/query/in_memory"
|
|
8
|
+
|
|
9
|
+
module Hecks
|
|
10
|
+
module Fuzzing
|
|
11
|
+
# A step list, replayed IN-PROCESS against a fresh boot — the same
|
|
12
|
+
# copy-to-tmp-and-reset preamble SequenceGenerator#call uses, and the
|
|
13
|
+
# same observable surface bin/run prints (instances, events,
|
|
14
|
+
# refusals, reactions, sagas, queries), but returned as data rather
|
|
15
|
+
# than JSON on stdout.
|
|
16
|
+
#
|
|
17
|
+
# NOT what SequenceGenerator itself dispatches through while
|
|
18
|
+
# generating — that inline execution feeds the picker's own
|
|
19
|
+
# known_ids tracking and stays exactly as it is. This exists for
|
|
20
|
+
# everything ELSE that needs "given a step list, boot fresh and tell
|
|
21
|
+
# me what happened": bin/fuzz recomputing a shrink candidate's TRUE
|
|
22
|
+
# event count (removing a step changes what the sequence actually
|
|
23
|
+
# produces, so a shrunk candidate cannot reuse the original claim),
|
|
24
|
+
# and the declared-property checks in properties.rb. Both want it
|
|
25
|
+
# in-process — a property or a shrink-candidate check is a question
|
|
26
|
+
# this boot can answer itself, and paying for a subprocess and a
|
|
27
|
+
# second boot to ask it would be ~100x the cost for nothing.
|
|
28
|
+
#
|
|
29
|
+
# Refuses the same way SequenceGenerator's own safe_call does: a
|
|
30
|
+
# DOMAIN_REFUSAL or an EvaluationError is the domain declining a
|
|
31
|
+
# step, recorded and not fatal to the replay. Anything else
|
|
32
|
+
# propagates — a step that breaks the interpreter is a defect, not
|
|
33
|
+
# an observation to fold quietly into the history.
|
|
34
|
+
module Replay
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
# THE AD HOC FILTER'S OWN COMPARATOR ROSTER — read directly from
|
|
38
|
+
# QuerySpecification::Common::COMPARATORS (the same nine names
|
|
39
|
+
# Vocabulary::QueryComparator declares), never re-typed. A
|
|
40
|
+
# declared bluebook query never sees an `op:` outside this set —
|
|
41
|
+
# `admits: "Vocabulary::QueryComparator"` refuses one at DECLARE
|
|
42
|
+
# time — but a `"filter"`-shaped query step (below) has no
|
|
43
|
+
# declare-time gate at all, so this method gates it here instead.
|
|
44
|
+
#
|
|
45
|
+
# NOT rust/src/kernel/query_comparators.rs's own ground truth —
|
|
46
|
+
# that hand-maintained Rust enum is missing `none_in_state` (the
|
|
47
|
+
# 9th comparator, added after the enum was written) and has
|
|
48
|
+
# already drifted; do not treat it as authoritative until item #9
|
|
49
|
+
# of the whole-project table-unification survey closes that gap.
|
|
50
|
+
FILTER_COMPARATORS = Hecks::QuerySpecification::Common::COMPARATORS.map(&:to_s).freeze
|
|
51
|
+
|
|
52
|
+
# THE TWO CLASSES `#enforce_givens`/`#enforce_lifecycle_guard`
|
|
53
|
+
# themselves ever raise — see Admissibility's own doc comment,
|
|
54
|
+
# `command_rules/admissibility.rb`. Any OTHER DOMAIN_REFUSAL a step
|
|
55
|
+
# raises (TypeMismatch, EnsuresNotMet, InvariantViolation, ...)
|
|
56
|
+
# proves the guard itself did NOT fire, since it runs first in
|
|
57
|
+
# DISPATCH_ORDER.
|
|
58
|
+
GUARD_REFUSAL_CLASSES = [Runtime::GivenNotMet, Runtime::LifecycleRefused].freeze
|
|
59
|
+
|
|
60
|
+
def call(domain_path, steps)
|
|
61
|
+
# See isolated_boot.rb's own header: resets data/ AND rebinds
|
|
62
|
+
# persistence to Memory, since a Postgres-bound domain's real store
|
|
63
|
+
# lives outside the copied directory and cannot be reached by
|
|
64
|
+
# resetting data/ alone.
|
|
65
|
+
IsolatedBoot.call(domain_path) do |copy|
|
|
66
|
+
runtime = Hecks.boot(copy)
|
|
67
|
+
|
|
68
|
+
refusals = []
|
|
69
|
+
queries = []
|
|
70
|
+
dry_runs = []
|
|
71
|
+
fan_outs = []
|
|
72
|
+
guard_checks = []
|
|
73
|
+
mutation_traces = []
|
|
74
|
+
|
|
75
|
+
# EVERY AGGREGATE A `for_each` COULD EVER QUERY, resolved ONCE —
|
|
76
|
+
# `[domain, aggregate_name]` pairs, gleaned from every loaded
|
|
77
|
+
# bluebook's own fanning-out policies. Empty for every domain
|
|
78
|
+
# with no `for_each` at all (every example this corpus ships
|
|
79
|
+
# today), so the snapshot below costs nothing until a domain
|
|
80
|
+
# actually declares one.
|
|
81
|
+
fan_out_targets = runtime.registry.bluebooks.each_with_object({}) do |(domain, bluebook), targets|
|
|
82
|
+
bluebook.policies.select(&:fans_out?).each do |policy|
|
|
83
|
+
query_domain, aggregate_name, = policy.for_each_route(domain)
|
|
84
|
+
targets[[query_domain, aggregate_name]] ||= runtime.registry.bluebook(query_domain)&.aggregate(aggregate_name)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
steps.each do |step|
|
|
89
|
+
step = step.transform_keys(&:to_s)
|
|
90
|
+
args = (step["args"] || {}).transform_keys(&:to_sym)
|
|
91
|
+
|
|
92
|
+
if (question = step["query"])
|
|
93
|
+
# THE AD HOC, SINGLE-COMPARATOR FILTER — a "query" step whose
|
|
94
|
+
# own value is a Hash, not a name: `{aggregate:, field:, op:,
|
|
95
|
+
# value:}`, the SAME wire shape kernel/cli.rs's new object-
|
|
96
|
+
# form "query" step reads on the Rust side (that file's own
|
|
97
|
+
# header explains why this shape exists at all: it bypasses
|
|
98
|
+
# the bluebook query DSL entirely, so it needs no generated
|
|
99
|
+
# per-domain codegen to prove for real). Answered here by
|
|
100
|
+
# calling `Ports::Query::InMemory` DIRECTLY — the real
|
|
101
|
+
# production comparator engine, not a second, hand-rewritten
|
|
102
|
+
# copy of it — against the raw repository, never through
|
|
103
|
+
# `runtime.query`, which only ever resolves a NAMED, declared
|
|
104
|
+
# ask.
|
|
105
|
+
if question.is_a?(Hash)
|
|
106
|
+
begin
|
|
107
|
+
rows = run_filter(runtime, question)
|
|
108
|
+
queries << { query: question, rows: rows, instances_at: snapshot_instances(runtime) }
|
|
109
|
+
rescue => e
|
|
110
|
+
refusals << { verb: filter_label(question), error: e.message }
|
|
111
|
+
end
|
|
112
|
+
next
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
begin
|
|
116
|
+
rows = runtime.query(question, **args)
|
|
117
|
+
# THE QUERY ORACLE'S OTHER HALF — the same ask at the same
|
|
118
|
+
# instant, answered by the reference interpreter instead of
|
|
119
|
+
# the bound adapter's native hook. Recorded side by side so
|
|
120
|
+
# Properties.query_answers_match_reference can treat any
|
|
121
|
+
# difference as a finding. Read-model asks (bare domain
|
|
122
|
+
# form, no "::") have no reference twin and record only the
|
|
123
|
+
# one answer.
|
|
124
|
+
reference = question.include?("::") ? runtime.reference_query(question, **args) : nil
|
|
125
|
+
queries << { query: question, args: args, rows: rows, reference_rows: reference,
|
|
126
|
+
instances_at: snapshot_instances(runtime) }
|
|
127
|
+
rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
|
|
128
|
+
queries << { query: question, args: args, error: e.message }
|
|
129
|
+
refusals << { verb: question, error: e.message, kind: e.class.name }
|
|
130
|
+
end
|
|
131
|
+
next
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# `{"dry_run": verb, "args": …}` — `Dispatcher#dry_run?`: the command
|
|
135
|
+
# evaluated hypothetically, nothing saved or emitted, no reaction.
|
|
136
|
+
# Recorded, never a refusal: a refused dry run is an ANSWER.
|
|
137
|
+
if (hypothetical = step["dry_run"])
|
|
138
|
+
begin
|
|
139
|
+
runtime.dry_run?(hypothetical, **args)
|
|
140
|
+
dry_runs << { verb: hypothetical, ok: true }
|
|
141
|
+
rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
|
|
142
|
+
dry_runs << { verb: hypothetical, ok: false, error: e.message }
|
|
143
|
+
end
|
|
144
|
+
next
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
begin
|
|
148
|
+
# THE FAN-OUT ORACLE'S OWN LOW-WATER MARK — taken before
|
|
149
|
+
# dispatch, so any reaction this ONE step's own announced
|
|
150
|
+
# events produce (`reaction_log` grows in place, the same
|
|
151
|
+
# Array `runtime.reactions` already exposes) can be sliced
|
|
152
|
+
# out after, and matched against an INDEPENDENT recomputation
|
|
153
|
+
# of what a `for_each` policy should have fanned out over —
|
|
154
|
+
# the query oracle's own shape (two engines, compared, never
|
|
155
|
+
# one graded against itself), aimed at fan-out instead of a
|
|
156
|
+
# named ask.
|
|
157
|
+
reaction_mark = runtime.reactions.size
|
|
158
|
+
|
|
159
|
+
# THE SNAPSHOT A `for_each` QUERY WOULD HAVE SEEN — taken
|
|
160
|
+
# BEFORE this step's own dispatch, not after. The real
|
|
161
|
+
# `deliver_for_each` runs its query SYNCHRONOUSLY, inside
|
|
162
|
+
# this SAME dispatch, before this call even returns — so an
|
|
163
|
+
# oracle that re-reads the live repository AFTER `dispatch`
|
|
164
|
+
# answers sees whatever the fan-out's OWN dispatched
|
|
165
|
+
# commands already mutated (an Account a `Review` leg just
|
|
166
|
+
# moved out of "open," say), not what the query actually
|
|
167
|
+
# matched. A measured bug, not a hypothetical one — this
|
|
168
|
+
# exact ordering is what `spec/fuzzing/fan_out_spec.rb`'s
|
|
169
|
+
# own adversarial run against a live `for_each` policy
|
|
170
|
+
# caught the first time this shipped without it.
|
|
171
|
+
fan_out_snapshot = fan_out_targets.each_with_object({}) do |((fdomain, faggregate_name), aggregate), snap|
|
|
172
|
+
next unless aggregate
|
|
173
|
+
|
|
174
|
+
snap[[fdomain, faggregate_name]] =
|
|
175
|
+
runtime.registry.repository(fdomain, aggregate).all.to_h { |record| [record.id, record.state.dup] }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# THE GUARD ORACLE'S OWN PRE-DISPATCH READ — same idiom,
|
|
179
|
+
# same placement, same reason as fan_out_snapshot right
|
|
180
|
+
# above: `Admissibility#enforce_givens` (which itself calls
|
|
181
|
+
# `#enforce_lifecycle_guard` when `declaring:` is passed)
|
|
182
|
+
# is called a SECOND time here, independently, against the
|
|
183
|
+
# record exactly as CommandInterpreter#hydrate's own acting
|
|
184
|
+
# branch would find it (`repository.find(id).dup` — the
|
|
185
|
+
# identical three-tier id fallback, Identity.of/.from,
|
|
186
|
+
# reproduced read-only) — BEFORE this step's real dispatch
|
|
187
|
+
# can mutate anything a cross-aggregate given dereferences
|
|
188
|
+
# (`customer.status`). A pure predicate read, side-effect
|
|
189
|
+
# free, so calling it twice changes nothing this step
|
|
190
|
+
# itself observes.
|
|
191
|
+
guard_check = build_guard_check(runtime, step["verb"], args)
|
|
192
|
+
|
|
193
|
+
# THE MUTATION ORACLE'S OWN PRE-DISPATCH READ — same
|
|
194
|
+
# idiom again: an ENTITY-DISPATCHED command's own
|
|
195
|
+
# `append`/`remove`/`multiply`/`clamp` mutations (S17's
|
|
196
|
+
# fixture, spec/fixtures/entity_list_mutations, now a real
|
|
197
|
+
# bootable domain) act on the entity's OWN attributes, so
|
|
198
|
+
# the element addressed by this step's own identity args
|
|
199
|
+
# is snapshotted BEFORE dispatch, materialized to plain
|
|
200
|
+
# data — `nil` for anything out of scope (an aggregate-
|
|
201
|
+
# level command, an entity command with no mutations at
|
|
202
|
+
# all, or one whose identity args don't resolve).
|
|
203
|
+
mutation_trace = build_mutation_trace(runtime, step["verb"], args)
|
|
204
|
+
|
|
205
|
+
# `role:` — an OPTIONAL per-step key, absent on every one of
|
|
206
|
+
# the 231 existing `spec/corpus/*.json` steps (their own
|
|
207
|
+
# unwrapped `runtime.dispatch` call, unchanged, so nothing
|
|
208
|
+
# already pinned changes behavior). Binds the SAME ambient
|
|
209
|
+
# caller `refuse_role_mismatch` reads (`Hecks.as_caller`,
|
|
210
|
+
# `Runtime::Caller.as`) for exactly the one dispatch this
|
|
211
|
+
# step makes, then unbinds — mirrors `Caller.as`'s own
|
|
212
|
+
# `ensure`-restore, so back-to-back steps with different (or
|
|
213
|
+
# no) `role:` never leak into each other.
|
|
214
|
+
result = if step["role"]
|
|
215
|
+
Hecks.as_caller(role: step["role"]) { runtime.dispatch(step["verb"], **args) }
|
|
216
|
+
else
|
|
217
|
+
runtime.dispatch(step["verb"], **args)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
fan_outs.concat(fan_out_findings(runtime, fan_out_snapshot, result.events, runtime.reactions[reaction_mark..]))
|
|
221
|
+
guard_checks << guard_check.merge(actual_refused: false, actual_kind: nil) if guard_check
|
|
222
|
+
# AFTER — only on SUCCESS ; a refused step mutated nothing,
|
|
223
|
+
# so there is no "after" to compare (and #build_mutation_
|
|
224
|
+
# trace already skipped anything with no mutations to
|
|
225
|
+
# trace in the first place).
|
|
226
|
+
if mutation_trace
|
|
227
|
+
mutation_traces << mutation_trace.merge(after: read_mutation_after(runtime, mutation_trace))
|
|
228
|
+
end
|
|
229
|
+
rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
|
|
230
|
+
# `kind:` — the RAISED CLASS, not re-derived from the message.
|
|
231
|
+
# `GivenNotMet`/`EnsuresNotMet` share their exact wording
|
|
232
|
+
# ("<command> refused — <description>") with FOUR other
|
|
233
|
+
# refusal templates (Vocabulary's own LifecycleRefused/
|
|
234
|
+
# TypeMismatch/Unauthorized entries) — a property that told
|
|
235
|
+
# a guard refusal apart by pattern-matching the string alone
|
|
236
|
+
# would misread every one of those as a guard, or a guard as
|
|
237
|
+
# one of those. The class is unambiguous where the string
|
|
238
|
+
# is not.
|
|
239
|
+
refusals << { verb: step["verb"], error: e.message, kind: e.class.name }
|
|
240
|
+
# ONLY a refusal raised BY THE GUARD ITSELF counts here —
|
|
241
|
+
# measured, not assumed: a step whose args were simply
|
|
242
|
+
# incomplete (AbsentArgument, from normalize_args — which
|
|
243
|
+
# runs BEFORE enforce_givens in DISPATCH_ORDER) never
|
|
244
|
+
# reached the guard at all, and this oracle's own first
|
|
245
|
+
# live run against real generated pizzas data caught
|
|
246
|
+
# exactly that case as a false positive (a malformed-args
|
|
247
|
+
# step the generator deliberately produces, `amount:`
|
|
248
|
+
# dropped entirely) before this comment existed. Whether a
|
|
249
|
+
# refusal from a stage AFTER enforce_givens (TypeMismatch
|
|
250
|
+
# on a mutation, EnsuresNotMet, InvariantViolation) proves
|
|
251
|
+
# the guard passed can't be told apart from a same-shaped
|
|
252
|
+
# refusal from a stage BEFORE it by class alone (TypeMismatch
|
|
253
|
+
# can come from either), so anything that isn't one of the
|
|
254
|
+
# two guard classes is left OUT of guard_checks entirely —
|
|
255
|
+
# inconclusive, not a claimed pass.
|
|
256
|
+
if guard_check && GUARD_REFUSAL_CLASSES.include?(e.class)
|
|
257
|
+
guard_checks << guard_check.merge(actual_refused: true, actual_kind: e.class.name)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
instances = snapshot_instances(runtime)
|
|
263
|
+
|
|
264
|
+
events = runtime.events.map { |event|
|
|
265
|
+
{ name: event.name, aggregate: event.aggregate, id: event.id, payload: event.payload }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# THE LIVE PROCESS-MANAGER STORE, materialised to inert data —
|
|
269
|
+
# `{ pm_name => { correlation => { state:, memory: } } }`, the
|
|
270
|
+
# SAME shape SagaInterpreter#checkpoint hands its persistence
|
|
271
|
+
# adapter (state plus a `Value.materialize`d memory, which is
|
|
272
|
+
# exactly what `deep_copy` there serialises). Captured here
|
|
273
|
+
# because Replay returns the history, not the runtime, and the
|
|
274
|
+
# runtime goes out of scope with the boot — so the saga-durability
|
|
275
|
+
# property (Properties.sagas_rehydrate_cleanly) reads this rather
|
|
276
|
+
# than reaching into a store the Memory rebind leaves as the
|
|
277
|
+
# no-op NULL_SAGA_STORE. Materialised, not raw, so the history
|
|
278
|
+
# stays plain data AND the round-trip check sees exactly the
|
|
279
|
+
# bytes a real adapter would have persisted.
|
|
280
|
+
saga_instances = runtime.registry.saga_instances.each_with_object({}) do |(pm_name, conversations), out|
|
|
281
|
+
out[pm_name] = conversations.each_with_object({}) do |(correlation, instance), rows|
|
|
282
|
+
rows[correlation] = { state: instance[:state], memory: Runtime::Value.materialize(instance[:memory]) }
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# The booted chapter rides along — the boot already happened, and
|
|
287
|
+
# properties.rb's lifecycle/saga checks need the declared IR
|
|
288
|
+
# (state sets, handler graphs) beside the history it produced.
|
|
289
|
+
# Free: no second boot, just the object the first one already
|
|
290
|
+
# built.
|
|
291
|
+
#
|
|
292
|
+
# `bluebook:` (singular) stays the FIRST-loaded chapter — every
|
|
293
|
+
# existing property scopes itself to "only what we have the
|
|
294
|
+
# grammar for" against exactly this one, deliberately (see
|
|
295
|
+
# lifecycle_values_are_declared's own comment). `bluebooks:`
|
|
296
|
+
# (plural) is the FULL map, keyed by domain name — a domain
|
|
297
|
+
# under fuzz commonly composes more than one bluebook (banking
|
|
298
|
+
# alone loads Banking + Governance + Identity), and a refusal
|
|
299
|
+
# or an event can legitimately come from ANY of them, not only
|
|
300
|
+
# whichever one happened to load first. A property that needs
|
|
301
|
+
# to resolve a verb back to its OWN declaring bluebook — not
|
|
302
|
+
# "the" bluebook — reads this instead.
|
|
303
|
+
{ instances: instances, events: events, refusals: refusals,
|
|
304
|
+
reactions: runtime.reactions, sagas: runtime.sagas, saga_instances: saga_instances,
|
|
305
|
+
queries: queries, dry_runs: dry_runs, fan_outs: fan_outs, guard_checks: guard_checks,
|
|
306
|
+
mutation_traces: mutation_traces,
|
|
307
|
+
saga_dispatches: runtime.saga_dispatches, policy_dispatches: runtime.policy_dispatches,
|
|
308
|
+
bluebook: runtime.registry.bluebooks.values.first,
|
|
309
|
+
bluebooks: runtime.registry.bluebooks.dup }
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# THE GUARD ORACLE'S OWN RESOLUTION — "which record, if any, is
|
|
314
|
+
# this step about, and would enforce_givens/enforce_lifecycle_guard
|
|
315
|
+
# have refused it against that record's PRE-DISPATCH state" —
|
|
316
|
+
# reproduced read-only from already-public pieces
|
|
317
|
+
# (Naming.split_verb, registry.bluebook/.aggregate/.command,
|
|
318
|
+
# Runtime::Identity.of/.from, repository.find), the exact same
|
|
319
|
+
# three-tier fallback CommandInterpreter#hydrate's OWN acting
|
|
320
|
+
# branch uses, minus its creating/duplicate-checking logic (a
|
|
321
|
+
# creating command has no pre-existing record to snapshot, and
|
|
322
|
+
# every real target this closes — Debit/CloseAccount/Credit/
|
|
323
|
+
# FreezeAccount — already acts on one).
|
|
324
|
+
#
|
|
325
|
+
# `nil` for anything out of scope: an entity/port verb (a dotted
|
|
326
|
+
# command_name — EntityInterpreter's own enforce_givens call,
|
|
327
|
+
# `parent:`-shaped, is a different call signature this does not
|
|
328
|
+
# reproduce), a creating command, an unresolvable id, or a command
|
|
329
|
+
# that declares neither `givens` nor `from` at all (nothing to
|
|
330
|
+
# check — logging a guaranteed, tautological pass would be noise,
|
|
331
|
+
# not a finding, the same reason `aggregation_matches_recompute`
|
|
332
|
+
# skips a read model with no count/median declared).
|
|
333
|
+
#
|
|
334
|
+
# Never lets a resolution surprise (a malformed verb, a dangling
|
|
335
|
+
# reference) become the step's own real dispatch outcome — this
|
|
336
|
+
# is a SEPARATE, best-effort read, not part of the step's own
|
|
337
|
+
# control flow.
|
|
338
|
+
# THE SAME SHAPE `call`'s own end-of-replay block used to build
|
|
339
|
+
# inline — every persisted record, keyed the way `query_eligible_rows`/
|
|
340
|
+
# `#eligible_rows` (properties.rb) already expect. Now ALSO called
|
|
341
|
+
# once PER QUERY STEP (see `call`, above), not only once at the very
|
|
342
|
+
# end: a query asked at step 1 of a script whose LATER steps go on
|
|
343
|
+
# to create more records was being checked, by every property that
|
|
344
|
+
# independently recomputes "the eligible rows," against the FINAL
|
|
345
|
+
# snapshot — the records that existed AFTER the whole replay, not
|
|
346
|
+
# the ones that existed when the query actually ran. Found live:
|
|
347
|
+
# `Banking.accounts_by_kind`, asked as literally the first step of a
|
|
348
|
+
# 3-step script, correctly answered against zero accounts (none
|
|
349
|
+
# existed yet) while `group_by_matches_recompute`'s own independent
|
|
350
|
+
# recompute claimed "1 eligible row" — the ONE account the script's
|
|
351
|
+
# later two steps went on to create. Each query step now carries
|
|
352
|
+
# its own `instances_at:` snapshot, taken at the moment it ran, so
|
|
353
|
+
# every property that recomputes against "the eligible rows" reads
|
|
354
|
+
# the state as that query actually saw it, not a shared final one.
|
|
355
|
+
def snapshot_instances(runtime)
|
|
356
|
+
instances = {}
|
|
357
|
+
runtime.registry.bluebooks.each do |domain_name, bluebook|
|
|
358
|
+
bluebook.aggregates.each do |aggregate|
|
|
359
|
+
runtime.registry.repository(domain_name, aggregate).all.each do |record|
|
|
360
|
+
instances["#{domain_name}::#{aggregate.name}##{record.id}"] = record.state
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
instances
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def build_guard_check(runtime, verb, args)
|
|
368
|
+
domain_name, aggregate_name, command_name = Naming.split_verb(verb)
|
|
369
|
+
return nil unless command_name && !command_name.include?(".")
|
|
370
|
+
|
|
371
|
+
aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
|
|
372
|
+
command = aggregate&.command(command_name)
|
|
373
|
+
return nil unless aggregate && command && !command.creates?
|
|
374
|
+
|
|
375
|
+
# NOTHING TO CHECK, genuinely — not "nothing THIS reproduces yet".
|
|
376
|
+
# A transition-only guard (no per-command `from:`, no `given`,
|
|
377
|
+
# only an aggregate `lifecycle do transition ... end` block
|
|
378
|
+
# naming this command — `Admit`/`Reject`'s own shape) still
|
|
379
|
+
# counts as something to check now that `admissible_transition`
|
|
380
|
+
# is reproduced below; skipping it here would just move the
|
|
381
|
+
# exact gap that call was added to close one line earlier.
|
|
382
|
+
has_transition = aggregate.lifecycle && aggregate.lifecycle.transitions_for(command.hecks_name).any?
|
|
383
|
+
return nil if command.givens.empty? && !command.from && !has_transition
|
|
384
|
+
|
|
385
|
+
reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
|
|
386
|
+
id = Runtime::Identity.of(aggregate, args) ||
|
|
387
|
+
Runtime::Identity.from(aggregate, args, :id) ||
|
|
388
|
+
(reference_key && Runtime::Identity.from(aggregate, args, reference_key))
|
|
389
|
+
return nil unless id
|
|
390
|
+
|
|
391
|
+
record = runtime.registry.repository(domain_name, aggregate).find(id)
|
|
392
|
+
return nil unless record
|
|
393
|
+
|
|
394
|
+
rules = Runtime::CommandRules.new(runtime.registry)
|
|
395
|
+
recomputed_kind = begin
|
|
396
|
+
rules.enforce_givens(record.dup, command, args, domain: domain_name, declaring: aggregate)
|
|
397
|
+
|
|
398
|
+
# A SECOND, SEPARATE DISPATCH_ORDER STEP — `enforce_givens`
|
|
399
|
+
# (just above) only ever checks a per-COMMAND `from:` clause
|
|
400
|
+
# (its own trailing `enforce_lifecycle_guard(declaring, ...)
|
|
401
|
+
# if declaring` call) — the aggregate's own `lifecycle do
|
|
402
|
+
# transition "X" => Y, from: Z end` block is a WHOLLY separate
|
|
403
|
+
# method (`admissible_transition`), called as its own later
|
|
404
|
+
# DISPATCH_ORDER step (`:enforce_givens` then
|
|
405
|
+
# `:admissible_transition` — Vocabulary.symbols
|
|
406
|
+
# ("AggregateDispatchOrder")), not reached from inside
|
|
407
|
+
# `enforce_givens` at all. Missing this call meant a command
|
|
408
|
+
# declared with NO per-command `from:` of its own — every real
|
|
409
|
+
# transition-guarded command in this corpus, `Admit`/`Reject`
|
|
410
|
+
# included — always recomputed "admitted" no matter the
|
|
411
|
+
# record's actual state, because the ONE check that would
|
|
412
|
+
# have refused it was never run. Found live: `Expression::
|
|
413
|
+
# Expression.Admit`, fuzzed against `lib/hecks/grammar`
|
|
414
|
+
# (a domain the property's own hand-verification — Banking,
|
|
415
|
+
# Pizzas — never happened to exercise a transition-guarded,
|
|
416
|
+
# no-per-command-`from:` command against). Called only when
|
|
417
|
+
# `enforce_givens` didn't already refuse, mirroring the real
|
|
418
|
+
# pipeline's own "first refusal wins" order exactly.
|
|
419
|
+
rules.admissible_transition(aggregate, command, record.dup)
|
|
420
|
+
nil
|
|
421
|
+
rescue *GUARD_REFUSAL_CLASSES => e
|
|
422
|
+
e.class.name
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
{ verb: verb, domain: domain_name, aggregate: aggregate_name, command: command_name, id: id,
|
|
426
|
+
recomputed_refused: !recomputed_kind.nil?, recomputed_kind: recomputed_kind }
|
|
427
|
+
rescue StandardError
|
|
428
|
+
nil
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# THE MUTATION ORACLE'S OWN PRE-DISPATCH READ — scoped, on
|
|
432
|
+
# purpose, to ENTITY-DISPATCHED commands only (a dotted
|
|
433
|
+
# command_name): the one place `append`/`remove`/`multiply`/
|
|
434
|
+
# `clamp` are known to act on an entity's OWN attributes
|
|
435
|
+
# (spec/fixtures/entity_list_mutations' own TaggedList — `tags`
|
|
436
|
+
# a value-object list, `count` a VO-typed scalar), never on
|
|
437
|
+
# ANOTHER nested entity list — so this never needs to reproduce
|
|
438
|
+
# `MutationApplier#entity_element`'s own auto-mint/collision logic
|
|
439
|
+
# (item 1's own fix) at all. An aggregate-level command whose OWN
|
|
440
|
+
# mutation appends an ENTITY (`Board.AddList`, `SafeDepositBox.
|
|
441
|
+
# LogVisit`) is a DIFFERENT, already-covered case — item 1's own
|
|
442
|
+
# collision property, not this one.
|
|
443
|
+
#
|
|
444
|
+
# `nil` for anything out of scope: an aggregate-level command, an
|
|
445
|
+
# entity command with no mutations at all, or one whose identity
|
|
446
|
+
# args (parent OR element) don't resolve.
|
|
447
|
+
def build_mutation_trace(runtime, verb, args)
|
|
448
|
+
domain_name, aggregate_name, command_name = Naming.split_verb(verb)
|
|
449
|
+
return nil unless command_name && command_name.include?(".")
|
|
450
|
+
|
|
451
|
+
aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
|
|
452
|
+
return nil unless aggregate
|
|
453
|
+
|
|
454
|
+
entity_name, entity_command_name = command_name.split(".", 2)
|
|
455
|
+
entity = aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
|
|
456
|
+
command = entity&.command(entity_command_name)
|
|
457
|
+
return nil unless command && command.mutations.any?
|
|
458
|
+
|
|
459
|
+
reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
|
|
460
|
+
parent_id = Runtime::Identity.of(aggregate, args) ||
|
|
461
|
+
Runtime::Identity.from(aggregate, args, :id) ||
|
|
462
|
+
(reference_key && Runtime::Identity.from(aggregate, args, reference_key))
|
|
463
|
+
return nil unless parent_id
|
|
464
|
+
|
|
465
|
+
record = runtime.registry.repository(domain_name, aggregate).find(parent_id)
|
|
466
|
+
return nil unless record
|
|
467
|
+
|
|
468
|
+
list_attr = aggregate.attributes.find { |a| a.list? && a.type.to_s == entity.hecks_name }
|
|
469
|
+
return nil unless list_attr
|
|
470
|
+
|
|
471
|
+
wants = entity.identity_paths.map do |path|
|
|
472
|
+
head = path.to_s.split(".").first.to_sym
|
|
473
|
+
raw = args[head]
|
|
474
|
+
return nil if raw.nil?
|
|
475
|
+
|
|
476
|
+
[head, Runtime::Value.for_attribute(aggregate, entity.attribute(head), raw)]
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
element = Array(record.state[list_attr.name]).find { |el| wants.all? { |head, want| el[head] == want } }
|
|
480
|
+
return nil unless element
|
|
481
|
+
|
|
482
|
+
{ verb: verb, domain: domain_name, aggregate: aggregate_name, command: command_name,
|
|
483
|
+
parent_id: parent_id, list_attr: list_attr.name, element_wants: wants,
|
|
484
|
+
before: Runtime::Value.materialize(element), args: args }
|
|
485
|
+
rescue StandardError
|
|
486
|
+
nil
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# THE SAME ELEMENT, RE-LOCATED, AFTER dispatch — by identity, not
|
|
490
|
+
# position (an append could have changed the array's own length
|
|
491
|
+
# or order relative to it). `nil` if it somehow vanished (not
|
|
492
|
+
# expected for any op this fixture declares — none of them
|
|
493
|
+
# remove the acted-on element itself — but a property comparing
|
|
494
|
+
# against `nil` fails loudly rather than crashing this replay).
|
|
495
|
+
def read_mutation_after(runtime, trace)
|
|
496
|
+
aggregate = runtime.registry.bluebook(trace[:domain])&.aggregate(trace[:aggregate])
|
|
497
|
+
return nil unless aggregate
|
|
498
|
+
|
|
499
|
+
record = runtime.registry.repository(trace[:domain], aggregate).find(trace[:parent_id])
|
|
500
|
+
return nil unless record
|
|
501
|
+
|
|
502
|
+
element = Array(record.state[trace[:list_attr]]).find do |el|
|
|
503
|
+
trace[:element_wants].all? { |head, want| el[head] == want }
|
|
504
|
+
end
|
|
505
|
+
element && Runtime::Value.materialize(element)
|
|
506
|
+
rescue StandardError
|
|
507
|
+
nil
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# THE FAN-OUT ORACLE — one finding per (event, for_each policy) this
|
|
511
|
+
# step's own announced events could have triggered, independent of
|
|
512
|
+
# `PolicyInterpreter#deliver_for_each`: the SAME `where` evaluator
|
|
513
|
+
# every given/ensures already runs through, but the QUERY answered
|
|
514
|
+
# by `Ports::Query::InMemory.holds?` directly against the live
|
|
515
|
+
# repository (`Replay.run_filter`'s own idiom), never by calling
|
|
516
|
+
# `QueryInterpreter` — sharing that call would make this oracle
|
|
517
|
+
# blind to exactly the code the fan-out feature adds.
|
|
518
|
+
#
|
|
519
|
+
# `expected_row_ids` is `nil` when `where` did not hold — no dispatch
|
|
520
|
+
# is the claim, not "dispatched to zero rows," so a property
|
|
521
|
+
# comparing this against the reaction log needs to tell the two
|
|
522
|
+
# apart. Recomputed once per event, not once per policy-and-event,
|
|
523
|
+
# because a `Chapter` de-duplicates on nothing this loop cannot
|
|
524
|
+
# cheaply repeat.
|
|
525
|
+
def fan_out_findings(runtime, snapshot, announced, reactions_since)
|
|
526
|
+
announced.each_with_object([]) do |event, findings|
|
|
527
|
+
# `event.aggregate` is domain-qualified ("Banking::Account" —
|
|
528
|
+
# see command_rules/emission.rb's own Event.new) — the SAME
|
|
529
|
+
# source `PolicyInterpreter#policies_for` reads, split the
|
|
530
|
+
# same two ways: `Naming.demodulise` for the emitting
|
|
531
|
+
# aggregate's bare name, plain `split("::")` for the domain.
|
|
532
|
+
domain = event.aggregate.to_s.split("::").first
|
|
533
|
+
bluebook = runtime.registry.bluebook(domain)
|
|
534
|
+
next unless bluebook
|
|
535
|
+
|
|
536
|
+
emitting = Naming.demodulise(event.aggregate)
|
|
537
|
+
|
|
538
|
+
bluebook.policies.each do |policy|
|
|
539
|
+
next unless policy.fans_out? && policy.event_name == event.name
|
|
540
|
+
next unless policy.event_qualifier.nil? || policy.event_qualifier == emitting
|
|
541
|
+
|
|
542
|
+
findings << fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
|
|
548
|
+
payload = event.payload.transform_keys(&:to_sym)
|
|
549
|
+
held = policy.where.to_s.empty? ||
|
|
550
|
+
Bluebook::Expression::Evaluator.call(policy.where, {}, payload)
|
|
551
|
+
|
|
552
|
+
expected = held ? expected_fan_out_rows(runtime, snapshot, policy, domain, payload) : nil
|
|
553
|
+
|
|
554
|
+
actual = reactions_since.select { |r| r[:policy] == policy.name && r[:on] == event.name }
|
|
555
|
+
.filter_map { |r| r[:for_row] }
|
|
556
|
+
|
|
557
|
+
{ policy: policy.name, on: event.name, expected_row_ids: expected, actual_row_ids: actual }
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# THE INDEPENDENT RECOMPUTATION — `policy.for_each`'s declared query,
|
|
561
|
+
# answered against the PRE-DISPATCH snapshot (see the snapshot's
|
|
562
|
+
# own comment at its capture site: the real fan-out's query runs
|
|
563
|
+
# synchronously, before its own dispatched commands can mutate
|
|
564
|
+
# anything the query would have matched, so this has to read the
|
|
565
|
+
# same "before" state or it grades the wrong moment). Comparators
|
|
566
|
+
# are `Ports::Query::InMemory.holds?`, never `QueryInterpreter` —
|
|
567
|
+
# sharing that call would make this oracle blind to exactly the
|
|
568
|
+
# code the fan-out feature adds. A `Symbol` where-value binds to
|
|
569
|
+
# the triggering event's own payload (the same binding
|
|
570
|
+
# `OpenForCustomer`'s `customer_id: :customer_id` relies on); a
|
|
571
|
+
# literal is compared as declared.
|
|
572
|
+
def expected_fan_out_rows(runtime, snapshot, policy, domain, payload)
|
|
573
|
+
query_domain, aggregate_name, query_name = policy.for_each_route(domain)
|
|
574
|
+
aggregate = runtime.registry.bluebook(query_domain)&.aggregate(aggregate_name)
|
|
575
|
+
query = aggregate&.query(query_name)
|
|
576
|
+
return [] unless query
|
|
577
|
+
|
|
578
|
+
rows = snapshot[[query_domain, aggregate_name]] || {}
|
|
579
|
+
matched = rows.select do |_id, state|
|
|
580
|
+
query.wheres.all? do |clause|
|
|
581
|
+
held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, clause.field))
|
|
582
|
+
Ports::Query::InMemory.holds?(clause, held, payload)
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
matched.keys.map(&:to_s).sort
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Answers ONE ad hoc filter step for real — the mirror image of
|
|
590
|
+
# kernel/cli.rs's own `run_filter`, deliberately calling the exact
|
|
591
|
+
# SAME production module that method's Rust port stands in for
|
|
592
|
+
# (`Ports::Query::InMemory`, lib/hecks/ports/query/in_memory.rb)
|
|
593
|
+
# rather than re-deriving comparator behavior by hand. `field` walks
|
|
594
|
+
# through `QuerySpecification::FieldPath.dig` (the same reading a
|
|
595
|
+
# declared where-clause gets), `comparable`/`holds?` are the same
|
|
596
|
+
# two calls `InMemory.execute` itself makes per candidate record —
|
|
597
|
+
# this is that method's own filter/select step, inlined, because
|
|
598
|
+
# there is no DECLARED `Query` object here to hand `execute` (an ad
|
|
599
|
+
# hoc filter has no `order_by`/`limit`/`offset` at all, so nothing
|
|
600
|
+
# about `execute`'s own ordering/paging logic even applies).
|
|
601
|
+
# Sorted by id ascending regardless — `Ports::Query::Ordering`'s own
|
|
602
|
+
# header explains why an ask with no declared order still needs
|
|
603
|
+
# this tier ("the identity tier is what makes an ask total").
|
|
604
|
+
def run_filter(runtime, filter)
|
|
605
|
+
aggregate_ref = filter["aggregate"].to_s
|
|
606
|
+
field = filter["field"].to_s
|
|
607
|
+
op = filter["op"].to_s
|
|
608
|
+
value = filter["value"]
|
|
609
|
+
|
|
610
|
+
raise "unknown query comparator #{op.inspect}" unless FILTER_COMPARATORS.include?(op)
|
|
611
|
+
|
|
612
|
+
domain_name, aggregate_name = aggregate_ref.split("::", 2)
|
|
613
|
+
aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
|
|
614
|
+
raise "unknown aggregate #{aggregate_ref.inspect}" unless aggregate
|
|
615
|
+
|
|
616
|
+
clause = QuerySpecification::Common::WhereClause.new(field: field, op: op, value: value)
|
|
617
|
+
records = runtime.registry.repository(domain_name, aggregate).all
|
|
618
|
+
matched = records.select do |record|
|
|
619
|
+
held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(record, field))
|
|
620
|
+
Ports::Query::InMemory.holds?(clause, held, {})
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
matched.sort_by { |record| record.id.to_s }.map { |record| { id: record.id }.merge(record.state) }
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# The `refusals` entry's own "verb" column for a REFUSED ad hoc
|
|
627
|
+
# filter — there is no real verb to report (a filter step carries
|
|
628
|
+
# none), so this builds the SAME descriptive label kernel/cli.rs's
|
|
629
|
+
# own `filter_label` builds from the same three raw fields, tolerant
|
|
630
|
+
# of any of them being missing (Ruby's own nil-to-"" interpolation)
|
|
631
|
+
# the same way that Rust port is.
|
|
632
|
+
def filter_label(filter) = "filter #{filter["aggregate"]}.#{filter["field"]} #{filter["op"]}"
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
end
|