hecks 0.2.0 → 1.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 +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 +427 -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 +101 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +179 -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 +145 -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 +88 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +438 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +17 -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 +80 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +306 -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 +54 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +125 -0
- data/lib/hecks/behaviors/expectations.rb +340 -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 +438 -0
- data/lib/hecks/bluebook/assembly/marks.rb +228 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +91 -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 +68 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -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 +78 -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 +1018 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +714 -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 +430 -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 +135 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -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 +173 -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 +228 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +117 -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 +123 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
- data/lib/hecks/bluebook/expression/projection.json +218 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +781 -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 +621 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -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 +542 -0
- data/lib/hecks/bluebook/model_check.rb +445 -0
- data/lib/hecks/bluebook/pattern_subset.rb +184 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +128 -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 +66 -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 +410 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +119 -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 +166 -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 +341 -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 +177 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +84 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +161 -0
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +119 -0
- data/lib/hecks/forms/record_table.rb +68 -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 +145 -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/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
- data/lib/hecks/fuzzing/properties.rb +1192 -0
- data/lib/hecks/fuzzing/replay.rb +668 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
- data/lib/hecks/fuzzing/value_generator.rb +200 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +324 -0
- data/lib/hecks/grammar/expression.bluebook +420 -0
- data/lib/hecks/grammar/expression_operators.json +1648 -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 +471 -0
- data/lib/hecks/language/bluebook/entity.bluebook +392 -0
- data/lib/hecks/language/bluebook/policy.bluebook +189 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +381 -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 +448 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -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 +333 -0
- data/lib/hecks/language/port.bluebook +120 -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 +174 -0
- data/lib/hecks/ports/access_control.port +9 -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 +8 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +6 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +7 -0
- data/lib/hecks/ports/authorization.rb +62 -0
- data/lib/hecks/ports/clock.port +5 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +5 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +5 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +5 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +5 -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 +138 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +23 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -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 +96 -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 +642 -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 +44 -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 +446 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +158 -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 +411 -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 +92 -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 +107 -0
- data/lib/hecks/query_specification/hop_path.rb +132 -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/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +66 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
- data/lib/hecks/runtime/command_interpreter.rb +497 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +189 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +245 -0
- data/lib/hecks/runtime/dispatcher.rb +301 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +299 -0
- data/lib/hecks/runtime/errors.rb +122 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +156 -0
- data/lib/hecks/runtime/instance.rb +175 -0
- data/lib/hecks/runtime/interpreting.rb +90 -0
- data/lib/hecks/runtime/loader.rb +184 -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 +286 -0
- data/lib/hecks/runtime/reaction_invocation.rb +253 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +341 -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 +126 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
- data/lib/hecks/runtime/registry/verification.rb +246 -0
- data/lib/hecks/runtime/registry.rb +279 -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 +503 -0
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -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 +130 -0
- data/lib/hecks/runtime/value/coercion.rb +565 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +107 -0
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +218 -0
- data/lib/hecks.rb +123 -6
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +412 -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,286 @@
|
|
|
1
|
+
require_relative "../naming"
|
|
2
|
+
require_relative "../ports/query"
|
|
3
|
+
require_relative "../ports/query/ordering"
|
|
4
|
+
require_relative "../query_specification/field_path"
|
|
5
|
+
require_relative "../query_specification/common/comparison"
|
|
6
|
+
require_relative "errors"
|
|
7
|
+
require_relative "reference_hop"
|
|
8
|
+
require_relative "refusal_wording"
|
|
9
|
+
require_relative "tenant_scope"
|
|
10
|
+
require_relative "value"
|
|
11
|
+
|
|
12
|
+
module Hecks
|
|
13
|
+
module Runtime
|
|
14
|
+
class QueryInterpreter
|
|
15
|
+
attr_reader :registry
|
|
16
|
+
|
|
17
|
+
def initialize(registry)
|
|
18
|
+
@registry = registry
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(domain, aggregate, query_name, args)
|
|
22
|
+
return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
|
|
23
|
+
|
|
24
|
+
declared = declared_query(aggregate, query_name)
|
|
25
|
+
args = normalize_args(aggregate, declared, args)
|
|
26
|
+
declared = TenantScope.apply(declared, args)
|
|
27
|
+
# AFTER TenantScope, so its synthetic clause is already present
|
|
28
|
+
# in `.wheres` and rides through as an ordinary LOCAL clause on
|
|
29
|
+
# the OUTER query. It does not reach the hop's own inner
|
|
30
|
+
# sub-query against the TARGET aggregate — a hop's target may
|
|
31
|
+
# not even declare the same tenant boundary, and propagating one
|
|
32
|
+
# aggregate's tenant scope onto an unrelated aggregate's own
|
|
33
|
+
# query is a real design question of its own, not answered here.
|
|
34
|
+
declared = ReferenceHop.apply(declared, args, registry: @registry, domain: domain, aggregate: aggregate)
|
|
35
|
+
|
|
36
|
+
repository = @registry.repository(domain, aggregate)
|
|
37
|
+
# `registry:` is threaded through so a `none_in_state` where-clause
|
|
38
|
+
# can look its target aggregate up — see
|
|
39
|
+
# QuerySpecification::Common::Comparison#none_in_state? for the
|
|
40
|
+
# comparator itself, and for why an ordinary aggregate-level
|
|
41
|
+
# Memory query needs the registry passed explicitly rather than
|
|
42
|
+
# closing over an instance variable.
|
|
43
|
+
if (native = Ports::Query.execute(repository, declared, args,
|
|
44
|
+
context: { domain: domain, aggregate: aggregate, registry: @registry }))
|
|
45
|
+
records = native
|
|
46
|
+
# `record.state.merge(id: record.id)` — id LAST, not first. See
|
|
47
|
+
# Instance#to_h's own comment: an aggregate free to declare its
|
|
48
|
+
# own attribute literally named `id` has that attribute's own
|
|
49
|
+
# wrapped value sitting in `record.state[:id]` already; merging
|
|
50
|
+
# it OVER a `{id:}.merge(state)` used to let it silently
|
|
51
|
+
# clobber the correct bare identity this row is supposed to
|
|
52
|
+
# carry.
|
|
53
|
+
# A QUERY ROW IS AN ANSWER, NOT A HANDLE. Mutating one edits
|
|
54
|
+
# nobody's state and silently disagrees with the store.
|
|
55
|
+
return Freezer.deep(records.map { |record| record.state.merge(id: record.id) })
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Freezer.deep(interpret(repository.all, declared, args, domain: domain))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The REFERENCE answer — this interpreter's own evaluation, never an
|
|
62
|
+
# adapter's native hook. The fuzzer's query oracle replays every
|
|
63
|
+
# generated ask through both paths and treats a difference as a
|
|
64
|
+
# finding: the differential gate the retired cross-runtime harness
|
|
65
|
+
# should always have been, aimed where the divergence actually
|
|
66
|
+
# lives — between the engines inside this one runtime.
|
|
67
|
+
#
|
|
68
|
+
# A hop clause is answered here by its OWN, deliberately naive
|
|
69
|
+
# walk (reference_where_holds?) — never Runtime::ReferenceHop's
|
|
70
|
+
# partition/fold/IN-clause. Sharing that algorithm would have made
|
|
71
|
+
# this oracle blind to exactly the code the hop feature adds: every
|
|
72
|
+
# PHASE of a shared fold would still get diffed against the native
|
|
73
|
+
# adapters, but the fold itself — the empty candidate set, a
|
|
74
|
+
# duplicate id, a dangling reference, a chain's inside-out
|
|
75
|
+
# resolution order — would only ever be compared against itself.
|
|
76
|
+
def reference_call(domain, aggregate, query_name, args)
|
|
77
|
+
return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
|
|
78
|
+
|
|
79
|
+
declared = declared_query(aggregate, query_name)
|
|
80
|
+
args = normalize_args(aggregate, declared, args)
|
|
81
|
+
declared = TenantScope.apply(declared, args)
|
|
82
|
+
reference_interpret(@registry.repository(domain, aggregate).all, declared, args,
|
|
83
|
+
domain: domain, shape: aggregate)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def declared_query(aggregate, query_name)
|
|
89
|
+
aggregate.query(query_name) ||
|
|
90
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_query",
|
|
91
|
+
aggregate: aggregate.hecks_name, query: query_name.inspect))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def interpret(records, declared, args, domain: nil)
|
|
95
|
+
matched = records.select { |r| declared.wheres.all? { |w| where_holds?(w, r, args, domain: domain) } }
|
|
96
|
+
ordered = ordered(matched, declared.order_by, declared.null_semantics)
|
|
97
|
+
# OFFSET FIRST, THEN LIMIT — the order SQL means by `LIMIT n
|
|
98
|
+
# OFFSET m`, and the order Ports::Query::InMemory#execute already
|
|
99
|
+
# applies (see that file's own comment). This interpreter used to
|
|
100
|
+
# never read declared.offset at all — offset silently vanished for
|
|
101
|
+
# any query answered here, not just come out reversed.
|
|
102
|
+
skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
|
|
103
|
+
capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
|
|
104
|
+
|
|
105
|
+
# id LAST — see the native-path comment above; same clobbering
|
|
106
|
+
# risk for the in-memory reference interpreter's own rows.
|
|
107
|
+
capped.map { |r| r.state.merge(id: r.id) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# `interpret`'s own twin, for reference_call alone — same
|
|
111
|
+
# select/order/limit shape, but a clause that hops through a
|
|
112
|
+
# reference is answered by reference_where_holds? instead of the
|
|
113
|
+
# plain FieldPath.dig(record, field) `where_holds?` uses (which
|
|
114
|
+
# has no concept of a reference at all — it would just read the
|
|
115
|
+
# raw id straight off the record and compare THAT).
|
|
116
|
+
def reference_interpret(records, declared, args, domain:, shape:)
|
|
117
|
+
matched = records.select { |r|
|
|
118
|
+
declared.wheres.all? { |w|
|
|
119
|
+
reference_where_holds?(w, r, args, domain: domain, shape: shape)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
ordered = ordered(matched, declared.order_by, declared.null_semantics)
|
|
123
|
+
# OFFSET FIRST, THEN LIMIT — same fix, same reasoning, as
|
|
124
|
+
# #interpret's own rows above.
|
|
125
|
+
skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
|
|
126
|
+
capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
|
|
127
|
+
|
|
128
|
+
# id LAST — same reasoning, same fix, as interpret's own rows.
|
|
129
|
+
capped.map { |r| r.state.merge(id: r.id) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# THE NAIVE READING OF A HOP: not a fold, not an id set — for
|
|
133
|
+
# each candidate row, walk the reference by hand and dig the
|
|
134
|
+
# field out of whatever it actually points at. A nil reference,
|
|
135
|
+
# or one that resolves to nothing (a dangling id), makes the
|
|
136
|
+
# WHOLE clause false outright, whatever the comparator — "points
|
|
137
|
+
# at a client that is not active" is false for a proposal with no
|
|
138
|
+
# client at all, the same way it is false for one whose client
|
|
139
|
+
# really is active; falling through to holds?(clause, nil, args)
|
|
140
|
+
# instead would answer `ne` wrong (nil != "active" is true).
|
|
141
|
+
def reference_where_holds?(clause, record, args, domain:, shape:)
|
|
142
|
+
step = QuerySpecification::HopPath.next_hop(clause.field, shape.attributes)
|
|
143
|
+
return where_holds?(clause, record, args) unless step
|
|
144
|
+
|
|
145
|
+
hop, rest = step
|
|
146
|
+
inner = QuerySpecification::Common::WhereClause.new(field: rest, op: clause.op, value: clause.value)
|
|
147
|
+
held = record[hop.attribute.name]
|
|
148
|
+
reference_ids = hop.attribute.list? ? Array(held) : [held]
|
|
149
|
+
|
|
150
|
+
reference_ids.compact.any? do |reference_id|
|
|
151
|
+
target_record = @registry.repository(domain, hop.target).find(reference_id)
|
|
152
|
+
next false unless target_record
|
|
153
|
+
|
|
154
|
+
reference_where_holds?(inner, target_record, args, domain: domain, shape: hop.target)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def entity_rows(domain, aggregate, dotted, args)
|
|
159
|
+
entity_name, query_name = Naming.split_dotted(dotted)
|
|
160
|
+
entity = aggregate.entities.find { |piece| piece.hecks_name == entity_name } ||
|
|
161
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_unknown",
|
|
162
|
+
aggregate: aggregate.hecks_name, entity: entity_name.inspect))
|
|
163
|
+
declared = entity.query(query_name) ||
|
|
164
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_query_missing",
|
|
165
|
+
entity: entity_name, query: query_name.inspect))
|
|
166
|
+
declared = TenantScope.apply(declared, args)
|
|
167
|
+
list_attr = aggregate.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
|
|
168
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_holds_no_list",
|
|
169
|
+
aggregate: aggregate.hecks_name, entity: entity_name))
|
|
170
|
+
|
|
171
|
+
parent_key = Naming.reference_key(aggregate.hecks_name)
|
|
172
|
+
rows = @registry.repository(domain, aggregate).all.flat_map do |record|
|
|
173
|
+
Array(record[list_attr.name])
|
|
174
|
+
.select { |el| declared.wheres.all? { |w| element_where_holds?(w, el, args) } }
|
|
175
|
+
.map { |el| { parent_key => record.id }.merge(el) }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
ordered = ordered_elements(rows, declared.order_by, declared.null_semantics,
|
|
179
|
+
parent_key, entity.identity_heads)
|
|
180
|
+
# OFFSET FIRST, THEN LIMIT — same fix, same reasoning, as
|
|
181
|
+
# #interpret's own rows above. `entity_rows` is the ONLY engine
|
|
182
|
+
# for entity/sub-list queries, so a declared offset here silently
|
|
183
|
+
# vanished for every entity query, not merely one path among
|
|
184
|
+
# several.
|
|
185
|
+
skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
|
|
186
|
+
declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# FieldPath.dig, not a raw `element[clause.field.to_sym]` — an
|
|
190
|
+
# entity sub-list row is a plain hash merged from stored state
|
|
191
|
+
# (mixed string/symbol keys depending on adapter, per `#cell`'s own
|
|
192
|
+
# comment below), and a dotted `where` (`where "price.cents" < 100`)
|
|
193
|
+
# needs the same segment-by-segment walk every other query path
|
|
194
|
+
# already gets. Reading only the symbol spelling of the WHOLE
|
|
195
|
+
# dotted string as one key always missed — `element[:"price.cents"]`
|
|
196
|
+
# is never a real key — so a dotted where on an entity query
|
|
197
|
+
# silently matched nothing, on the only engine entity queries have.
|
|
198
|
+
def element_where_holds?(clause, element, args)
|
|
199
|
+
holds?(clause, QuerySpecification::FieldPath.dig(element, clause.field), args)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# A row's own key, however the store spells it. A sub-list row is a plain hash
|
|
203
|
+
# merged from stored state, so its keys arrive as strings from one adapter and
|
|
204
|
+
# symbols from another — and reading only one spelling gave every row the SAME
|
|
205
|
+
# identity, which is a tie, which is the exact nondeterminism this tier exists
|
|
206
|
+
# to remove. It rides `comparable` for the same reason a where-clause does : an
|
|
207
|
+
# identity is a value object, and `to_s` on one is an OBJECT ADDRESS — a sort key
|
|
208
|
+
# that differs run to run, which is worse than the store order it replaced.
|
|
209
|
+
# `key?` decides which spelling answers, never `||` — a row whose
|
|
210
|
+
# value is a genuinely-held `false` must not fall through to the
|
|
211
|
+
# other spelling (usually absent) and land on `nil`.
|
|
212
|
+
def cell(row, key)
|
|
213
|
+
sym = key.to_sym
|
|
214
|
+
row.key?(sym) ? row[sym] : row[key.to_s]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# A sub-list row is identified by its PARENT and then its own key : two
|
|
218
|
+
# entities under different parents can share a sequence, so the parent has
|
|
219
|
+
# to lead or the tie is not broken at all.
|
|
220
|
+
#
|
|
221
|
+
# EVERY KEY THE PIECE IS KNOWN BY, in declaration order, for the same
|
|
222
|
+
# reason the parent leads: a part that ties is a part that breaks no tie.
|
|
223
|
+
# This took `identified_by`, which is the SINGLE head and is nil the
|
|
224
|
+
# moment an identity has two parts — and `cell(row, nil)` calls
|
|
225
|
+
# `nil.to_sym`, so a query against a composite piece did not sort wrongly,
|
|
226
|
+
# it raised. A piece known by one key sorts exactly as it did.
|
|
227
|
+
def ordered_elements(rows, order_by, null_semantics, parent_key, entity_keys)
|
|
228
|
+
field = order_by&.field
|
|
229
|
+
Ports::Query::Ordering.apply(
|
|
230
|
+
rows, order_by, null_semantics,
|
|
231
|
+
identity: lambda { |row|
|
|
232
|
+
[row[parent_key].to_s, *Array(entity_keys).map { |key| comparable(cell(row, key)) }]
|
|
233
|
+
}
|
|
234
|
+
) { |row| comparable(QuerySpecification::FieldPath.dig(row, field)) }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def where_holds?(clause, record, args, domain: nil)
|
|
238
|
+
holds?(clause, QuerySpecification::FieldPath.dig(record, clause.field), args, record: record, domain: domain)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# The comparator table itself lives in
|
|
242
|
+
# QuerySpecification::Common::Comparison. This method and
|
|
243
|
+
# Ports::Query::InMemory#holds? used to carry a copy each and the
|
|
244
|
+
# two drifted — `none_in_state` reached only one of them, and
|
|
245
|
+
# `comparable` disagreed about value objects with two numeric
|
|
246
|
+
# members. What stays here is how a value is REACHED for this
|
|
247
|
+
# path: the registry is instance state rather than an argument.
|
|
248
|
+
def holds?(clause, held, args, record: nil, domain: nil)
|
|
249
|
+
QuerySpecification::Common::Comparison.holds?(
|
|
250
|
+
clause.op, comparable(held), comparable(resolve_query_value(clause.value, args)), registry: @registry
|
|
251
|
+
)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def resolve_query_value(value, args)
|
|
255
|
+
value.is_a?(Symbol) ? args[value] : value
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def normalize_args(aggregate, declared, args)
|
|
259
|
+
declared.attributes.each_with_object(args.dup) do |attribute, normalized|
|
|
260
|
+
next unless normalized.key?(attribute.name)
|
|
261
|
+
|
|
262
|
+
normalized[attribute.name] = Value.for_attribute(aggregate, attribute, normalized[attribute.name])
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def comparable(value) = QuerySpecification::Common::Comparison.comparable(value)
|
|
267
|
+
|
|
268
|
+
# FieldPath.dig, not a raw `record[field]` — `record` is an Instance
|
|
269
|
+
# here, and a dotted order_by (`order_by "price.cents"`) is a
|
|
270
|
+
# single symbol key (`:"price.cents"`) that never matches anything
|
|
271
|
+
# `Instance#[]` actually holds, so a dotted order_by silently sorted
|
|
272
|
+
# by all-nil (the identity tier alone deciding every tie) on this,
|
|
273
|
+
# the reference/no-native-hook engine — the same bug already fixed
|
|
274
|
+
# for `where` (see `where_holds?` above) and for entity rows (see
|
|
275
|
+
# `ordered_elements` below), just not yet for this, the aggregate-
|
|
276
|
+
# level order_by.
|
|
277
|
+
def ordered(records, order_by, null_semantics = nil)
|
|
278
|
+
field = order_by&.field
|
|
279
|
+
Ports::Query::Ordering.apply(records, order_by, null_semantics,
|
|
280
|
+
identity: ->(record) { record.id.to_s }) { |record|
|
|
281
|
+
comparable(QuerySpecification::FieldPath.dig(record, field))
|
|
282
|
+
}
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
require_relative "../naming"
|
|
2
|
+
require_relative "errors"
|
|
3
|
+
require_relative "identity"
|
|
4
|
+
require_relative "value"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Runtime
|
|
8
|
+
# Turns facts selected by a policy or process manager into the same
|
|
9
|
+
# receiver/payload envelope an outside caller uses. Reaction declarations
|
|
10
|
+
# historically selected both through one `with:` map, so this is the one
|
|
11
|
+
# compatibility seam that separates receiver identities from facts after
|
|
12
|
+
# resolving the declaration and before re-entering the dispatcher.
|
|
13
|
+
module ReactionInvocation
|
|
14
|
+
Target = Struct.new(:aggregate, :entities, :command, keyword_init: true)
|
|
15
|
+
# :facts, not :values — Struct.new already defines #values (every
|
|
16
|
+
# member's own value, in order); naming this field :values silently
|
|
17
|
+
# shadowed that with the single Hash it actually holds.
|
|
18
|
+
Scope = Struct.new(:name, :facts, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# The holding IR historically represented both an omitted projection and
|
|
23
|
+
# an explicitly empty `with: {}` as the same empty array. Builders now
|
|
24
|
+
# preserve declaration presence off-wire; reconstructed/legacy IR falls
|
|
25
|
+
# back to the old non-empty reading.
|
|
26
|
+
def projection_declared?(declaration)
|
|
27
|
+
if declaration.instance_variable_defined?(:@projection_declared)
|
|
28
|
+
declaration.instance_variable_get(:@projection_declared)
|
|
29
|
+
else
|
|
30
|
+
!declaration.with_spec.to_a.empty?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# A reaction's source names resolve lexically, not globally. Policies
|
|
35
|
+
# supply one event/row scope. Process managers supply current event then
|
|
36
|
+
# opening-event memory, while correlation is an explicit binding ahead
|
|
37
|
+
# of both. Missing names are refused here rather than materialized as nil
|
|
38
|
+
# and accidentally presented as target command facts.
|
|
39
|
+
def resolve_mapping(with_spec:, scopes:, bindings: {}, label: "reaction")
|
|
40
|
+
normalized_bindings = bindings.transform_keys(&:to_sym)
|
|
41
|
+
normalized_scopes = scopes.map do |scope|
|
|
42
|
+
scope = Scope.new(name: scope.first, facts: scope.last) unless scope.is_a?(Scope)
|
|
43
|
+
Scope.new(name: scope.name.to_s, facts: scope.facts.transform_keys(&:to_sym))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
with_spec.to_h do |key, source|
|
|
47
|
+
value = if !source.is_a?(Symbol)
|
|
48
|
+
source
|
|
49
|
+
elsif normalized_bindings.key?(source)
|
|
50
|
+
normalized_bindings.fetch(source)
|
|
51
|
+
else
|
|
52
|
+
visible = normalized_scopes.find { |scope| scope.facts.key?(source) }
|
|
53
|
+
unless visible
|
|
54
|
+
names = normalized_scopes.map(&:name).join(" then ")
|
|
55
|
+
# WHAT IS VISIBLE, NAMED. A refusal that only says which name is
|
|
56
|
+
# missing sent a modeler guessing field after field
|
|
57
|
+
# (`number`, `reference`…) at a fan-out row that is addressed
|
|
58
|
+
# by ONE key — `account`, the lowercase aggregate — which
|
|
59
|
+
# nothing else in the domain spells out. The names each scope
|
|
60
|
+
# actually offers are the whole diagnosis; the refusal now
|
|
61
|
+
# lists them, scope by scope.
|
|
62
|
+
offered = normalized_scopes.map { |scope| "#{scope.name}: #{scope.facts.keys.sort.join(', ')}" }.join("; ")
|
|
63
|
+
raise UnknownArgument,
|
|
64
|
+
"#{label}'s with: reads :#{source}, which is not visible in #{names} (visible — #{offered})"
|
|
65
|
+
end
|
|
66
|
+
visible.facts.fetch(source)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
[key.to_sym, Value.materialize(value)]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A reaction without an explicit projection retains wholesale legacy
|
|
74
|
+
# forwarding. Choosing `with:` opts into the strict envelope: only target
|
|
75
|
+
# command attributes enter `with:`, while identities become `to:`.
|
|
76
|
+
def build(registry:, verb:, projected:, explicit:, passthrough: [], source_receiver: nil)
|
|
77
|
+
args = projected.transform_keys(&:to_sym)
|
|
78
|
+
unless explicit
|
|
79
|
+
return args unless source_receiver
|
|
80
|
+
|
|
81
|
+
# Compatibility calls still belong to Dispatcher when the target is
|
|
82
|
+
# absent: it owns the canonical UnknownVerb wording. Resolution here
|
|
83
|
+
# is only an optional opportunity to lift same-aggregate Event.id.
|
|
84
|
+
begin
|
|
85
|
+
target = resolve_target(registry, verb)
|
|
86
|
+
rescue UnknownVerb
|
|
87
|
+
return args
|
|
88
|
+
end
|
|
89
|
+
inherited_receiver = source_receiver_for(target, source_receiver)
|
|
90
|
+
return inherited_receiver ? args.merge(to: inherited_receiver) : args
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
target = resolve_target(registry, verb)
|
|
94
|
+
inherited_receiver = source_receiver_for(target, source_receiver)
|
|
95
|
+
facts = command_facts(target.command, args)
|
|
96
|
+
consumed = facts.keys
|
|
97
|
+
|
|
98
|
+
if target.entities.empty? && target.command.creates?
|
|
99
|
+
refuse_unconsumed!(target.command, args, consumed, passthrough)
|
|
100
|
+
return { with: facts }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
aggregate_identity, aggregate_keys = identity_for(
|
|
104
|
+
target.aggregate,
|
|
105
|
+
args,
|
|
106
|
+
aliases: aggregate_aliases(target),
|
|
107
|
+
value_owner: target.aggregate
|
|
108
|
+
)
|
|
109
|
+
aggregate_identity ||= inherited_receiver
|
|
110
|
+
require_identity!(verb, target.aggregate, aggregate_identity)
|
|
111
|
+
consumed.concat(aggregate_keys)
|
|
112
|
+
|
|
113
|
+
entity_identities = target.entities.map do |entity|
|
|
114
|
+
identity, keys = identity_for(
|
|
115
|
+
entity,
|
|
116
|
+
args,
|
|
117
|
+
aliases: [Naming.reference_key(entity.hecks_name)],
|
|
118
|
+
value_owner: target.aggregate
|
|
119
|
+
)
|
|
120
|
+
require_identity!(verb, entity, identity)
|
|
121
|
+
consumed.concat(keys)
|
|
122
|
+
identity
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
refuse_unconsumed!(target.command, args, consumed, passthrough)
|
|
126
|
+
|
|
127
|
+
route = if entity_identities.empty?
|
|
128
|
+
aggregate_identity
|
|
129
|
+
else
|
|
130
|
+
{ aggregate: aggregate_identity, entities: entity_identities }
|
|
131
|
+
end
|
|
132
|
+
{ to: route, with: facts }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def resolve_target(registry, verb)
|
|
136
|
+
domain, aggregate_name, command_path = Naming.split_verb(verb)
|
|
137
|
+
raise UnknownVerb, "reaction target #{verb.inspect} is not a qualified command" unless command_path
|
|
138
|
+
|
|
139
|
+
bluebook = registry.bluebook(domain)
|
|
140
|
+
aggregate = bluebook&.aggregate(aggregate_name)
|
|
141
|
+
raise UnknownVerb, "reaction target #{verb.inspect} does not resolve to an aggregate" unless aggregate
|
|
142
|
+
|
|
143
|
+
*entity_names, command_name = command_path.split(".")
|
|
144
|
+
owner = aggregate
|
|
145
|
+
entities = entity_names.map do |entity_name|
|
|
146
|
+
entity = owner.entities.find { |candidate| candidate.hecks_name == entity_name }
|
|
147
|
+
raise UnknownVerb, "reaction target #{verb.inspect} does not resolve entity #{entity_name.inspect}" unless entity
|
|
148
|
+
|
|
149
|
+
owner = entity
|
|
150
|
+
entity
|
|
151
|
+
end
|
|
152
|
+
command = owner.command(command_name)
|
|
153
|
+
raise UnknownVerb, "reaction target #{verb.inspect} does not resolve to a command" unless command
|
|
154
|
+
|
|
155
|
+
Target.new(aggregate: aggregate, entities: entities, command: command)
|
|
156
|
+
end
|
|
157
|
+
private_class_method :resolve_target
|
|
158
|
+
|
|
159
|
+
def command_facts(command, args)
|
|
160
|
+
declared = command.attributes.map { |attribute| attribute.name.to_sym }
|
|
161
|
+
args.slice(*declared)
|
|
162
|
+
end
|
|
163
|
+
private_class_method :command_facts
|
|
164
|
+
|
|
165
|
+
def aggregate_aliases(target)
|
|
166
|
+
aliases = [:aggregate, Naming.reference_key(target.aggregate.name)]
|
|
167
|
+
aliases.unshift(target.command.addressing_key_for(target.aggregate.name)) if target.entities.empty?
|
|
168
|
+
aliases.compact.uniq
|
|
169
|
+
end
|
|
170
|
+
private_class_method :aggregate_aliases
|
|
171
|
+
|
|
172
|
+
# Event.id names the aggregate that emitted the event. It can therefore
|
|
173
|
+
# supply only the receiver of a non-creating command on that SAME root;
|
|
174
|
+
# it cannot address another aggregate, invent an entity identity, or turn
|
|
175
|
+
# a creation into a mutation. An explicit projected receiver is resolved
|
|
176
|
+
# first and remains authoritative.
|
|
177
|
+
def source_receiver_for(target, source_receiver)
|
|
178
|
+
return nil unless source_receiver
|
|
179
|
+
return nil unless target.entities.empty?
|
|
180
|
+
return nil if target.command.creates?
|
|
181
|
+
|
|
182
|
+
source = source_receiver.transform_keys(&:to_sym)
|
|
183
|
+
source_aggregate = source[:aggregate].to_s
|
|
184
|
+
same_aggregate = source_aggregate == if source_aggregate.include?("::")
|
|
185
|
+
target.aggregate.hecks_fqn
|
|
186
|
+
else
|
|
187
|
+
target.aggregate.hecks_name
|
|
188
|
+
end
|
|
189
|
+
return nil unless same_aggregate
|
|
190
|
+
|
|
191
|
+
identity = source[:identity]
|
|
192
|
+
identity.to_s unless identity.nil? || identity.to_s.empty?
|
|
193
|
+
end
|
|
194
|
+
private_class_method :source_receiver_for
|
|
195
|
+
|
|
196
|
+
def identity_for(construct, args, aliases:, value_owner:)
|
|
197
|
+
identity = Identity.of(construct, args, value_owner: value_owner)
|
|
198
|
+
return [identity, construct.identity_heads] if identity
|
|
199
|
+
|
|
200
|
+
aliases.each do |key|
|
|
201
|
+
key = key.to_sym
|
|
202
|
+
next unless args.key?(key)
|
|
203
|
+
|
|
204
|
+
identity = identity_from_alias(construct, args[key], value_owner)
|
|
205
|
+
return [identity, [key]] if identity
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
return [args[:id].to_s, [:id]] if args.key?(:id) && !args[:id].to_s.empty?
|
|
209
|
+
|
|
210
|
+
[nil, []]
|
|
211
|
+
end
|
|
212
|
+
private_class_method :identity_for
|
|
213
|
+
|
|
214
|
+
def identity_from_alias(construct, held, value_owner)
|
|
215
|
+
materialized = Value.materialize(held)
|
|
216
|
+
if materialized.is_a?(Hash)
|
|
217
|
+
keyed = materialized.transform_keys(&:to_sym)
|
|
218
|
+
nested = Identity.of(construct, keyed, value_owner: value_owner)
|
|
219
|
+
return nested if nested
|
|
220
|
+
|
|
221
|
+
if construct.identity_paths.one?
|
|
222
|
+
scalar = Identity.scalar(construct.identity_paths.first, keyed)
|
|
223
|
+
return scalar.to_s unless scalar.nil? || scalar.to_s.empty?
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
return nil
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
materialized.to_s unless materialized.nil? || materialized.to_s.empty?
|
|
230
|
+
end
|
|
231
|
+
private_class_method :identity_from_alias
|
|
232
|
+
|
|
233
|
+
def require_identity!(verb, construct, identity)
|
|
234
|
+
return if identity
|
|
235
|
+
|
|
236
|
+
raise TypeMismatch,
|
|
237
|
+
"reaction target #{verb} needs #{construct.hecks_name}'s receiver identity outside command facts"
|
|
238
|
+
end
|
|
239
|
+
private_class_method :require_identity!
|
|
240
|
+
|
|
241
|
+
def refuse_unconsumed!(command, args, consumed, passthrough)
|
|
242
|
+
allowed = consumed.map(&:to_sym) + Array(passthrough).map(&:to_sym)
|
|
243
|
+
unknown = args.keys - allowed
|
|
244
|
+
return if unknown.empty?
|
|
245
|
+
|
|
246
|
+
raise UnknownArgument,
|
|
247
|
+
"#{command.hecks_name} reaction projection contains neither receiver identity nor declared command facts: " \
|
|
248
|
+
"#{unknown.sort.join(', ')}"
|
|
249
|
+
end
|
|
250
|
+
private_class_method :refuse_unconsumed!
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|