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,366 @@
|
|
|
1
|
+
require_relative "../naming"
|
|
2
|
+
require_relative "errors"
|
|
3
|
+
require_relative "query_interpreter"
|
|
4
|
+
require_relative "reaction_invocation"
|
|
5
|
+
require_relative "refusal_wording"
|
|
6
|
+
require_relative "value"
|
|
7
|
+
require_relative "../bluebook/expression/evaluator"
|
|
8
|
+
|
|
9
|
+
module Hecks
|
|
10
|
+
module Runtime
|
|
11
|
+
class PolicyInterpreter
|
|
12
|
+
attr_reader :registry
|
|
13
|
+
|
|
14
|
+
def initialize(registry, door:)
|
|
15
|
+
@registry = registry
|
|
16
|
+
@door = door
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# `deliver` returns `nil` for a policy whose `where` did not hold —
|
|
20
|
+
# SILENTLY, the same as a policy `policies_for` never selected at all
|
|
21
|
+
# (an `event_qualifier` miss carries no reaction_log entry either) —
|
|
22
|
+
# so nothing is appended for it. A `for_each` policy answers an ARRAY
|
|
23
|
+
# (one record per matched row) rather than one record ; `Array(...)`
|
|
24
|
+
# is wrong here (it would explode a plain record Hash into its own
|
|
25
|
+
# key/value pairs), so the two shapes are told apart explicitly.
|
|
26
|
+
def react(event, domain)
|
|
27
|
+
policies_for(event).each do |policy, home_domain|
|
|
28
|
+
result = deliver(policy, event, home_domain)
|
|
29
|
+
next if result.nil?
|
|
30
|
+
|
|
31
|
+
(result.is_a?(Array) ? result : [result]).each { |record| @registry.reaction_log << record }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# SCANS EVERY LOADED BLUEBOOK, not just the emitting command's own —
|
|
38
|
+
# a policy commonly lives in the CONSUMER's bluebook, reacting
|
|
39
|
+
# passively to an event a different domain's aggregate emits (the
|
|
40
|
+
# corpus-standard shape : `world/conception/bluebook/domain_cell.bluebook`'s
|
|
41
|
+
# ConceiveOnAbsorption reacts `on "DomainAbsorbed"`, an event
|
|
42
|
+
# `body/organs/bluebook/gut.bluebook`'s Gut.Absorb emits — two
|
|
43
|
+
# different bluebooks, joined only by the event's NAME). Restricting
|
|
44
|
+
# the scan to the emitting domain's own bluebook (the previous
|
|
45
|
+
# shape) silently drops every cross-domain reaction : the policy is
|
|
46
|
+
# never even a candidate, no refusal, no log entry, nothing —
|
|
47
|
+
# confirmed against the corpus's own .behaviors fixtures, several of
|
|
48
|
+
# which encode this exact cross-bluebook cascade as their contract.
|
|
49
|
+
#
|
|
50
|
+
# Returns [policy, home_domain] pairs rather than bare policies —
|
|
51
|
+
# `deliver`/`deliver_for_each` fall back to the REACTING policy's
|
|
52
|
+
# OWN domain (not the emitting one) when a trigger or for_each route
|
|
53
|
+
# is bare, and that fallback has to travel with each match now that
|
|
54
|
+
# a single event can surface policies from several different homes.
|
|
55
|
+
def policies_for(event)
|
|
56
|
+
emitting = Naming.demodulise(event.aggregate)
|
|
57
|
+
|
|
58
|
+
@registry.bluebooks.each_value.flat_map do |bluebook|
|
|
59
|
+
matching = bluebook.policies.select do |policy|
|
|
60
|
+
policy.event_name == event.name &&
|
|
61
|
+
(policy.event_qualifier.nil? || policy.event_qualifier == emitting)
|
|
62
|
+
end
|
|
63
|
+
matching.map { |policy| [policy, bluebook.name] }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# THE GUARD, evaluated against the triggering event's own payload —
|
|
68
|
+
# a policy has no aggregate instance of its own to read state from,
|
|
69
|
+
# so `state` is empty and every bare name a `where` resolves comes
|
|
70
|
+
# from `attrs` (Expression::Resolver#fetch checks `attrs` before
|
|
71
|
+
# `state`, so this is exactly the shape `enforce_givens` already
|
|
72
|
+
# gives a command's own given, minus the settled-record half a
|
|
73
|
+
# policy simply has none of). Called from inside `deliver`'s own
|
|
74
|
+
# rescue-guarded body (both callers, below) — never guarded here —
|
|
75
|
+
# so an EvaluationError (an unresolvable field, a bad comparison) is
|
|
76
|
+
# caught the SAME way any other reaction defect is, not swallowed as
|
|
77
|
+
# though the policy had merely declined to fire.
|
|
78
|
+
def where_holds?(policy, event)
|
|
79
|
+
return true if policy.where.to_s.empty?
|
|
80
|
+
|
|
81
|
+
Bluebook::Expression::Evaluator.call(policy.where, {}, event.payload.transform_keys(&:to_sym))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def deliver(policy, event, domain)
|
|
85
|
+
# `record` ASSIGNED BEFORE ANY BRANCH THAT CAN RAISE, same reason
|
|
86
|
+
# `deliver_for_each`'s own header gives : both rescue clauses below
|
|
87
|
+
# call `.merge` on it, and a defect raised before it existed would
|
|
88
|
+
# be caught here only to raise a second, different NoMethodError
|
|
89
|
+
# trying to record the first one.
|
|
90
|
+
target = "#{policy.target_domain || domain}::#{policy.trigger_command}"
|
|
91
|
+
record = { policy: policy.name, on: event.name, trigger: target }
|
|
92
|
+
|
|
93
|
+
return deliver_for_each(policy, event, domain, target, record) unless policy.for_each.to_s.empty?
|
|
94
|
+
return nil unless where_holds?(policy, event)
|
|
95
|
+
|
|
96
|
+
if @door.reaction_depth_reached?
|
|
97
|
+
return record.merge(delivered: false,
|
|
98
|
+
reason: "reaction depth #{@door.max_reaction_depth} reached")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
args = trigger_args(policy, event)
|
|
102
|
+
@door.reenter(target, **reaction_invocation(target, args, policy, event))
|
|
103
|
+
record.merge(delivered: true)
|
|
104
|
+
rescue *DOMAIN_REFUSALS => error
|
|
105
|
+
# The target refused — a fact about the domain, recorded and not
|
|
106
|
+
# fatal to the command that emitted the event.
|
|
107
|
+
record.merge(delivered: false, reason: error.message)
|
|
108
|
+
rescue StandardError => error
|
|
109
|
+
# A DEFECT, not a refusal — a NoMethodError in an interpreter, a
|
|
110
|
+
# NameError from a missing constant, a TypeError from a bad
|
|
111
|
+
# assumption : exactly the class of thing DOMAIN_REFUSALS
|
|
112
|
+
# (errors.rb, see the comment above that constant) deliberately
|
|
113
|
+
# excludes, and for the reason that comment gives at length —
|
|
114
|
+
# folding a crash into the same `delivered: false` shape as an
|
|
115
|
+
# ordinary refusal makes a broken runtime read as normal operation
|
|
116
|
+
# in the log. This clause does not reopen that hole: it is a
|
|
117
|
+
# SECOND, narrower rescue, tried only once the first one above has
|
|
118
|
+
# already declined to match, so a legitimate refusal still takes
|
|
119
|
+
# the branch above and a defect always takes this one.
|
|
120
|
+
#
|
|
121
|
+
# Catching it HERE is safe for a fact this method's caller cannot
|
|
122
|
+
# see from where it sits: by the time `react` runs, the command
|
|
123
|
+
# that EMITTED `event` has already succeeded and PERSISTED —
|
|
124
|
+
# `Dispatcher#dispatch` calls `@policies.react` only after its own
|
|
125
|
+
# `announced` events are already in hand. Letting this exception
|
|
126
|
+
# keep propagating would not undo that command (nothing here is
|
|
127
|
+
# transactional across aggregates) — it would only blow up the
|
|
128
|
+
# ORIGINAL caller's `dispatch` call for a failure that happened in
|
|
129
|
+
# a DIFFERENT command, one the caller never asked to run and has no
|
|
130
|
+
# way to compensate for. So the defect is recorded, distinguishably
|
|
131
|
+
# (`defect: true`, plus the error's own class — nothing here is
|
|
132
|
+
# allowed to read like an ordinary refusal), warned to STDERR so it
|
|
133
|
+
# is never silent, and left exactly where it happened for a human
|
|
134
|
+
# to find — never re-raised, and never swallowed either.
|
|
135
|
+
warn "[hecks] defect in reaction — policy #{policy.name} on #{event.name} " \
|
|
136
|
+
"firing #{target}: #{error.class}: #{error.message}"
|
|
137
|
+
record.merge(delivered: false, reason: error.message, defect: true, error_class: error.class.name)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# THE FAN-OUT — `policy.for_each` names a query ; this runs it
|
|
141
|
+
# against the triggering event's own payload (the same source
|
|
142
|
+
# `deliver`'s own ordinary path forwards to `trigger` wholesale) and
|
|
143
|
+
# fires `trigger` once per row, merging each row's own id into the
|
|
144
|
+
# forwarded payload under whichever key THE TARGET COMMAND ITSELF
|
|
145
|
+
# expects to be addressed by (`Behaviour::Command#addressing_key_for`
|
|
146
|
+
# — never a guessed, one-size mint: `Account.Freeze`, addressed by
|
|
147
|
+
# `account` because it is declared ON Account and self-references
|
|
148
|
+
# it, refused every dispatch for as long as this hardcoded
|
|
149
|
+
# `<aggregate>_id` instead — a real bug, found wiring `for_each`
|
|
150
|
+
# into a real domain for the first time, not a hypothetical). A
|
|
151
|
+
# refusal is recorded per row and the fan-out continues ; a crash
|
|
152
|
+
# resolving the QUERY ITSELF, or the TARGET COMMAND'S OWN inability
|
|
153
|
+
# to address this aggregate at all (`addressing_key_for` answering
|
|
154
|
+
# `nil` — a domain-authoring mistake, not a data problem), is a
|
|
155
|
+
# single top-level defect for the policy, the same shape `deliver`'s
|
|
156
|
+
# own outer rescue already gives every other reaction.
|
|
157
|
+
def deliver_for_each(policy, event, domain, target, record)
|
|
158
|
+
return nil unless where_holds?(policy, event)
|
|
159
|
+
|
|
160
|
+
query_domain, aggregate_name, query_name = policy.for_each_route(domain)
|
|
161
|
+
aggregate = resolve_query_aggregate(query_domain, aggregate_name, policy.for_each)
|
|
162
|
+
# THE QUERY READS THE EVENT, never the projection — `with:` says
|
|
163
|
+
# what the TRIGGER is given, and the fan-out's query is asking a
|
|
164
|
+
# different question (WHICH rows) in the event's own vocabulary.
|
|
165
|
+
query_args = for_each_query_args(aggregate.query(query_name), event)
|
|
166
|
+
rows = QueryInterpreter.new(@registry).call(query_domain, aggregate, query_name, query_args)
|
|
167
|
+
reference_key = addressing_key_for(target, aggregate_name)
|
|
168
|
+
|
|
169
|
+
Array(rows).map do |row|
|
|
170
|
+
deliver_for_each_row(target, record, trigger_args(policy, event, reference_key => row[:id]), row, policy, event)
|
|
171
|
+
end
|
|
172
|
+
rescue *DOMAIN_REFUSALS => error
|
|
173
|
+
record.merge(delivered: false, reason: error.message)
|
|
174
|
+
rescue StandardError => error
|
|
175
|
+
warn "[hecks] defect in reaction — policy #{policy.name} on #{event.name} " \
|
|
176
|
+
"resolving for_each #{policy.for_each}: #{error.class}: #{error.message}"
|
|
177
|
+
record.merge(delivered: false, reason: error.message, defect: true, error_class: error.class.name)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# THE EVENT'S OWN IDENTITY IS A FACT TOO, not only its payload. A
|
|
181
|
+
# for_each query commonly filters by the EMITTING record's own
|
|
182
|
+
# identity (`OpenForCustomer`'s own `reference:`, scoping by the
|
|
183
|
+
# very customer who was just suspended) — which used to arrive for
|
|
184
|
+
# free because LEGACY dispatch left the self-addressing key riding
|
|
185
|
+
# along in `event.payload` unfiltered. Routing separated from
|
|
186
|
+
# payload (`to:`/`with:`, what the facade's own bang-methods always
|
|
187
|
+
# use) correctly stopped carrying it there, which left this query
|
|
188
|
+
# silently seeing NEITHER the field it needs NOR any error saying
|
|
189
|
+
# why — an empty result read as "nothing to freeze" instead of "the
|
|
190
|
+
# customer" the whole reaction exists to catch.
|
|
191
|
+
#
|
|
192
|
+
# Merged in ONLY when the query declares an argument by that exact
|
|
193
|
+
# name AND the emitting aggregate's own identity is genuinely what
|
|
194
|
+
# that name means (`construct.identity_heads`) — the same guard
|
|
195
|
+
# `SagaInterpreter::Correlation#self_identified?` uses for the
|
|
196
|
+
# identical shape one call away, so an unrelated aggregate sharing
|
|
197
|
+
# an argument name by coincidence never gets misread as this one.
|
|
198
|
+
# Never overrides a value the payload already supplied.
|
|
199
|
+
def for_each_query_args(query, event)
|
|
200
|
+
args = event.payload.transform_keys(&:to_sym)
|
|
201
|
+
return args unless query
|
|
202
|
+
|
|
203
|
+
domain, bare_name = event.aggregate.to_s.split("::", 2)
|
|
204
|
+
construct = bare_name && @registry.bluebook(domain)&.aggregate(bare_name)
|
|
205
|
+
return args unless construct
|
|
206
|
+
|
|
207
|
+
heads = construct.identity_heads.map(&:to_s)
|
|
208
|
+
query.attributes.each do |attribute|
|
|
209
|
+
next if args.key?(attribute.name)
|
|
210
|
+
next unless heads.include?(attribute.name.to_s)
|
|
211
|
+
|
|
212
|
+
args[attribute.name] = event.id
|
|
213
|
+
end
|
|
214
|
+
args
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# WHAT THE TRIGGER IS GIVEN. Undeclared, the event's whole payload
|
|
218
|
+
# forwards verbatim — the behaviour every policy had before `with:`
|
|
219
|
+
# existed, and still the right default for a trigger shaped like
|
|
220
|
+
# its event.
|
|
221
|
+
#
|
|
222
|
+
# Declared, it is the same reading a saga's own `dispatch ...,
|
|
223
|
+
# with:` gets (`SagaInterpreter#dispatch_args`): a Symbol names a
|
|
224
|
+
# field on the SOURCE below, anything else is a literal the policy
|
|
225
|
+
# supplies itself. A saga additionally resolves against its own
|
|
226
|
+
# memory and correlation key; a policy has neither — it holds
|
|
227
|
+
# nothing between events — so the source is the event, plus:
|
|
228
|
+
#
|
|
229
|
+
# `extra` is a FAN-OUT'S ROW KEY, merged into the source BEFORE the
|
|
230
|
+
# projection rather than after it. That is what lets a `for_each`
|
|
231
|
+
# policy name the row it is acting on — `with: { account: :account }`
|
|
232
|
+
# — and therefore what lets one send the row and NOTHING ELSE. A
|
|
233
|
+
# trigger needing only which record to act on is the ordinary case
|
|
234
|
+
# for a fan-out, and before this it could not be written: the whole
|
|
235
|
+
# event rode along, and the target had to declare every field of it
|
|
236
|
+
# whether it read them or not.
|
|
237
|
+
def trigger_args(policy, event, extra = {})
|
|
238
|
+
payload = event.payload.transform_keys(&:to_sym).merge(extra)
|
|
239
|
+
return payload unless ReactionInvocation.projection_declared?(policy)
|
|
240
|
+
|
|
241
|
+
payload = emitter_identity(event).merge(payload)
|
|
242
|
+
|
|
243
|
+
args = ReactionInvocation.resolve_mapping(
|
|
244
|
+
with_spec: policy.with_spec,
|
|
245
|
+
scopes: [["event payload and fan-out row", payload]],
|
|
246
|
+
label: "#{policy.name}'s trigger"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# THE RAW INPUTS `args` WAS RESOLVED FROM — same additive,
|
|
250
|
+
# Ruby-only shape SagaInterpreter#deliver_saga_dispatch's own
|
|
251
|
+
# saga_dispatch_log gets, for Properties.dispatch_binding_
|
|
252
|
+
# fidelity's own independent re-derivation of Policy#with_spec's
|
|
253
|
+
# 2-branch resolution (Symbol → payload lookup, anything else →
|
|
254
|
+
# literal — a policy holds no correlation and no memory, so
|
|
255
|
+
# `payload` — the merged event-payload-plus-fan-out-row source —
|
|
256
|
+
# is the WHOLE source, unlike a saga's own 4-branch one).
|
|
257
|
+
@registry.policy_dispatch_log << { policy: policy.name, on: event.name, payload: payload,
|
|
258
|
+
with_spec: policy.with_spec, args: args }
|
|
259
|
+
args
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# THE EMITTING RECORD'S OWN IDENTITY IS A FACT A PROJECTION MAY READ
|
|
263
|
+
# — the same reasoning `for_each_query_args` gives one method up,
|
|
264
|
+
# extended from the fan-out's QUERY to the trigger's own `with:`.
|
|
265
|
+
# Routing separated from payload (`to:`/`with:`) stopped carrying
|
|
266
|
+
# the emitting aggregate's identity in the payload, which is right
|
|
267
|
+
# for the event (a KnightCaptured is a fact about a knight, not a
|
|
268
|
+
# re-statement of which game) but left a CROSS-aggregate reaction
|
|
269
|
+
# with no way to say which record to address: chess's Graveyard is
|
|
270
|
+
# one-per-game, fed by policy from every piece's own Captured
|
|
271
|
+
# event, and its burials had no way to name the game — the
|
|
272
|
+
# dispatcher fell through to the captured piece's `id` as the
|
|
273
|
+
# graveyard's identity and refused every one ("no Graveyard with
|
|
274
|
+
# label.value \"bb\""). Same-aggregate targets were already covered
|
|
275
|
+
# (`ReactionInvocation.source_receiver_for` lifts Event.id), so
|
|
276
|
+
# this is the OTHER aggregate's half of that.
|
|
277
|
+
#
|
|
278
|
+
# Offered under the emitting aggregate's own identity heads, only
|
|
279
|
+
# to an EXPLICIT projection (a legacy wholesale forward keeps its
|
|
280
|
+
# exact old payload), and never over a value the payload itself
|
|
281
|
+
# carries. `event.id` is the scalar the identity resolves to, so a
|
|
282
|
+
# projection naming it as the target's own identity head routes it
|
|
283
|
+
# as the receiver (`Identity.of` coerces a scalar against a
|
|
284
|
+
# single-field VO head), and one naming it as a declared attribute
|
|
285
|
+
# carries it as a fact. The build-time validator admits the same
|
|
286
|
+
# names (`BluebookBuilder.check_with_spec!`).
|
|
287
|
+
def emitter_identity(event)
|
|
288
|
+
return {} if event.id.nil? || event.id.to_s.empty?
|
|
289
|
+
|
|
290
|
+
domain, bare_name = event.aggregate.to_s.split("::", 2)
|
|
291
|
+
construct = bare_name && @registry.bluebook(domain)&.aggregate(bare_name)
|
|
292
|
+
return {} unless construct
|
|
293
|
+
|
|
294
|
+
construct.identity_heads.to_h { |head| [head.to_sym, event.id] }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# RESOLVES `target` ("Domain::Aggregate.Command", the same shape
|
|
298
|
+
# `deliver`'s own caller already built) back to its OWN declared
|
|
299
|
+
# command, then asks IT how it expects to be addressed by a row of
|
|
300
|
+
# `aggregate_name` — see `Behaviour::Command#addressing_key_for`'s
|
|
301
|
+
# own comment for the two shapes that answers. Raises (caught by
|
|
302
|
+
# `deliver_for_each`'s own outer `rescue StandardError`, the same
|
|
303
|
+
# "a defect, not a refusal" treatment `resolve_query_aggregate`'s
|
|
304
|
+
# own `UnknownVerb` already gets one level up) rather than
|
|
305
|
+
# silently falling back on a guess when the target command cannot
|
|
306
|
+
# be resolved, or genuinely cannot be addressed by this aggregate
|
|
307
|
+
# at all — either is a domain-authoring mistake worth surfacing
|
|
308
|
+
# loudly, not a row this fan-out simply skips.
|
|
309
|
+
def addressing_key_for(target, aggregate_name)
|
|
310
|
+
target_domain, target_aggregate_name, target_command_name = Naming.split_verb(target)
|
|
311
|
+
command = @registry.bluebook(target_domain)&.aggregate(target_aggregate_name)&.command(target_command_name)
|
|
312
|
+
raise UnknownVerb, "for_each's own trigger #{target.inspect} does not resolve to a declared command" unless command
|
|
313
|
+
|
|
314
|
+
key = command.addressing_key_for(aggregate_name)
|
|
315
|
+
return key if key
|
|
316
|
+
|
|
317
|
+
raise ArgumentError,
|
|
318
|
+
"#{target} cannot be addressed by a row of #{aggregate_name} — it declares no self-reference to " \
|
|
319
|
+
"#{aggregate_name} and no reference-typed attribute targeting it"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def deliver_for_each_row(target, record, args, row, policy, event)
|
|
323
|
+
row_record = record.merge(for_row: row[:id])
|
|
324
|
+
|
|
325
|
+
if @door.reaction_depth_reached?
|
|
326
|
+
return row_record.merge(delivered: false,
|
|
327
|
+
reason: "reaction depth #{@door.max_reaction_depth} reached")
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# ALREADY MERGED, by `trigger_args` — the row key belongs in the
|
|
331
|
+
# source a `with:` projection reads FROM, not bolted onto its
|
|
332
|
+
# result, or a projection could never name the row it acts on.
|
|
333
|
+
@door.reenter(target, **reaction_invocation(target, args, policy, event))
|
|
334
|
+
row_record.merge(delivered: true)
|
|
335
|
+
rescue *DOMAIN_REFUSALS => error
|
|
336
|
+
row_record.merge(delivered: false, reason: error.message)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def reaction_invocation(target, args, policy, event)
|
|
340
|
+
ReactionInvocation.build(
|
|
341
|
+
registry: @registry,
|
|
342
|
+
verb: target,
|
|
343
|
+
projected: args,
|
|
344
|
+
explicit: ReactionInvocation.projection_declared?(policy),
|
|
345
|
+
source_receiver: { aggregate: event.aggregate, identity: event.id }
|
|
346
|
+
)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# `for_each`'s own QUERY route moved onto the Policy itself
|
|
350
|
+
# (Behaviour::Policy#for_each_route) — one reading the interpreter
|
|
351
|
+
# and the fuzzer's fan-out property both call, rather than the
|
|
352
|
+
# same split spelled twice. The reference-KEY the dispatch itself
|
|
353
|
+
# uses is `addressing_key_for`, above — a property of the TARGET
|
|
354
|
+
# COMMAND, not of the policy, so it lives on `Behaviour::Command`
|
|
355
|
+
# instead.
|
|
356
|
+
|
|
357
|
+
def resolve_query_aggregate(domain, aggregate_name, verb)
|
|
358
|
+
bluebook = @registry.bluebook(domain) ||
|
|
359
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_domain", domain: domain.inspect, verb: verb))
|
|
360
|
+
bluebook.aggregate(aggregate_name) ||
|
|
361
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_aggregate",
|
|
362
|
+
domain: domain, aggregate: aggregate_name.inspect))
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
require_relative "interpreting"
|
|
2
|
+
require_relative "command_interpreter/argument_gate"
|
|
3
|
+
require_relative "event"
|
|
4
|
+
require_relative "value"
|
|
5
|
+
require_relative "../naming"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Runtime
|
|
9
|
+
# The dispatch pipeline for a port operation — called by an adapter living
|
|
10
|
+
# outside the bluebook entirely, never by the domain itself. Deliberately
|
|
11
|
+
# a trimmed CommandInterpreter: the same payload gate and coercion
|
|
12
|
+
# (ArgumentGate, Interpreting#normalize_args), but no `given`, no
|
|
13
|
+
# `mutations`, no lifecycle, no save. A port operation does not act on an
|
|
14
|
+
# aggregate instance — it translates an external call into an event in
|
|
15
|
+
# this domain's own vocabulary, and whatever mutation follows happens
|
|
16
|
+
# wherever a `policy` reacts to that event, exactly as it would for any
|
|
17
|
+
# command-emitted one.
|
|
18
|
+
class PortOperationInterpreter
|
|
19
|
+
include Interpreting
|
|
20
|
+
include CommandInterpreter::ArgumentGate
|
|
21
|
+
|
|
22
|
+
Context = Struct.new(:domain, :aggregate, :operation, :args, :route, :instance, :result)
|
|
23
|
+
|
|
24
|
+
DISPATCH_ORDER = %i[
|
|
25
|
+
refuse_unknown_arguments refuse_absent_arguments normalize_args resolve_references resolve_route emit
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
def initialize(registry, rules:)
|
|
29
|
+
@registry = registry
|
|
30
|
+
@rules = rules
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call(domain, aggregate, operation, args, route:)
|
|
34
|
+
ctx = Context.new(domain, aggregate, operation, args)
|
|
35
|
+
ctx.route = route
|
|
36
|
+
run_dispatch_order(DISPATCH_ORDER, ctx)
|
|
37
|
+
ctx.result
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def step_refuse_unknown_arguments(ctx)
|
|
43
|
+
step(:refuse_unknown_arguments) { refuse_unknown_arguments(ctx.domain, ctx.aggregate, ctx.operation, ctx.args) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def step_refuse_absent_arguments(ctx)
|
|
47
|
+
step(:refuse_absent_arguments) { refuse_absent_arguments(ctx.operation, ctx.args, aggregate: ctx.aggregate) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def step_normalize_args(ctx)
|
|
51
|
+
ctx.args = step(:normalize_args) { normalize_args(ctx.aggregate, ctx.operation, ctx.args) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def step_resolve_references(ctx)
|
|
55
|
+
step(:resolve_references) { @rules.resolve_references(ctx.domain, ctx.operation, ctx.args) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def step_resolve_route(ctx)
|
|
59
|
+
ctx.instance = step(:resolve_route) do
|
|
60
|
+
@registry.repository(ctx.domain, ctx.aggregate).find(ctx.route.aggregate) ||
|
|
61
|
+
raise(NotFound, "#{ctx.aggregate.hecks_name} #{ctx.route.aggregate.inspect} does not exist")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def step_emit(ctx)
|
|
66
|
+
ctx.result = step(:emit) { ctx.operation.outbound? ? ask(ctx) : emit(ctx) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# THE DOMAIN CALLING OUT, AND BOTH ENDINGS RECORDED.
|
|
70
|
+
#
|
|
71
|
+
# The adapter is found the same way every other port's is — by name,
|
|
72
|
+
# across whatever adapters this boot loaded — so an `asks` is bound by
|
|
73
|
+
# an adapter declaring `port "IssueTracker"` and nothing new to learn.
|
|
74
|
+
#
|
|
75
|
+
# EVERY FAILURE IS AN ANSWER. A raise from the far side of a boundary is
|
|
76
|
+
# not an exception in this domain's terms, it is the outside saying no,
|
|
77
|
+
# and the chapter already named the word for that. So the rescue is
|
|
78
|
+
# deliberately wide: a timeout, a bad credential, an adapter that does
|
|
79
|
+
# not exist, a nil where a number was wanted — all of them become the
|
|
80
|
+
# `refuses` event, carrying what was said. A policy reacts to it, a
|
|
81
|
+
# retry counter reads it, and nothing has to catch anything.
|
|
82
|
+
#
|
|
83
|
+
# AN ASK IS HANDED THE RECORD IT IS ABOUT.
|
|
84
|
+
#
|
|
85
|
+
# An INBOUND operation deliberately cannot read state — it is the
|
|
86
|
+
# anti-corruption boundary, translating a fact from outside, and letting
|
|
87
|
+
# it read the aggregate would make it a second place rules live. That
|
|
88
|
+
# rule was written for that direction and does not survive the crossing.
|
|
89
|
+
#
|
|
90
|
+
# An outbound one almost always needs the record. `asks "File"` names
|
|
91
|
+
# `reference_to Ticket` and the adapter needs the ticket's repository,
|
|
92
|
+
# title and body — which are ON the ticket, and which the policy that
|
|
93
|
+
# triggered this cannot supply because a command's event payload is its
|
|
94
|
+
# ARGUMENTS, not its state. Without this, every ask would have to have
|
|
95
|
+
# its data re-passed through the command that fired it, so the same text
|
|
96
|
+
# would live in two places and could differ.
|
|
97
|
+
#
|
|
98
|
+
# ARGUMENTS WIN over state, because an argument is what THIS call said
|
|
99
|
+
# and state is what the record happens to hold.
|
|
100
|
+
def ask(ctx)
|
|
101
|
+
payload = held_state(ctx).merge(materialise(ctx.args))
|
|
102
|
+
answer = adapter_for(ctx).public_send(Naming.snake(ctx.operation.hecks_name), **payload)
|
|
103
|
+
announce(ctx, ctx.operation.answers, ctx.args.merge(spread(answer)))
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
announce(ctx, ctx.operation.refuses, ctx.args.merge(refusal: { value: "#{e.class}: #{e.message}" }))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# THE ANSWER IS SPREAD, NOT NESTED — and that is what makes the loop
|
|
109
|
+
# close. A policy re-enters its target with the event payload VERBATIM;
|
|
110
|
+
# it cannot reach inside a key. So an answer tucked under `answered:`
|
|
111
|
+
# can be read by a human and by nothing else, and the command that
|
|
112
|
+
# should record the issue number never gets one.
|
|
113
|
+
#
|
|
114
|
+
# Spread, the adapter's own keys ARE the arguments of whatever command
|
|
115
|
+
# reacts to the answering event. Which is a real contract on the adapter
|
|
116
|
+
# — it must return what that command takes, in the shape the runtime
|
|
117
|
+
# coerces (`{ number: { value: 43 } }`, not `43`) — and naming it here
|
|
118
|
+
# is cheaper than a mapping layer nobody could see into.
|
|
119
|
+
#
|
|
120
|
+
# A non-Hash answer keeps the old shape: a port that returns a URL
|
|
121
|
+
# string has nothing to spread, and `answered:` is the honest word for
|
|
122
|
+
# a single unnamed value.
|
|
123
|
+
def spread(answer)
|
|
124
|
+
return { answered: answer } unless answer.is_a?(Hash)
|
|
125
|
+
|
|
126
|
+
answer.to_h { |key, value| [key.to_sym, deep_symbolize(value)] }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def deep_symbolize(value)
|
|
130
|
+
case value
|
|
131
|
+
when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
|
|
132
|
+
when Array then value.map { |element| deep_symbolize(element) }
|
|
133
|
+
else value
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# THE RECORD, IF THERE IS ONE. A record that does not exist yet is not
|
|
138
|
+
# an error here — the ask still goes, carrying only its arguments, and
|
|
139
|
+
# whatever the adapter makes of that is its own business. Refusing
|
|
140
|
+
# would put a second existence check behind the one `resolve_references`
|
|
141
|
+
# already performed.
|
|
142
|
+
def held_state(ctx)
|
|
143
|
+
ctx.instance ? Value.materialize(ctx.instance.state) : {}
|
|
144
|
+
rescue StandardError
|
|
145
|
+
{}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# THE PORT THIS OPERATION BELONGS TO, found by asking the aggregate
|
|
149
|
+
# rather than threading it through the call — the dispatcher already
|
|
150
|
+
# resolved it once to get here, and a second parameter carried purely so
|
|
151
|
+
# this method can read it would be a parameter every OTHER step ignores.
|
|
152
|
+
def port_name_for(ctx)
|
|
153
|
+
owning = ctx.aggregate.ports.find { |port| port.operations.any? { |op| op.equal?(ctx.operation) } }
|
|
154
|
+
owning&.name or raise WiringError,
|
|
155
|
+
"#{ctx.operation.hecks_name} belongs to no port on #{ctx.aggregate.hecks_name}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def adapter_for(ctx)
|
|
159
|
+
name = port_name_for(ctx)
|
|
160
|
+
implementations = @registry.adapters.values.select { |adapter| adapter.port == name }
|
|
161
|
+
|
|
162
|
+
case implementations.size
|
|
163
|
+
when 1 then Adapters.const_get(implementations.first.name).new
|
|
164
|
+
when 0 then raise WiringError, "no adapter implements the #{name} port — nothing can answer #{ctx.operation.hecks_name}"
|
|
165
|
+
else raise WiringError,
|
|
166
|
+
"#{implementations.size} adapters implement the #{name} port " \
|
|
167
|
+
"(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# A Value never crosses the boundary — an adapter is somebody else's
|
|
172
|
+
# code and should be handed plain data, the same reasoning `JsonDoor`
|
|
173
|
+
# gives for materialising before it hands anything to an HTTP caller.
|
|
174
|
+
def materialise(args) = Value.materialize(args)
|
|
175
|
+
|
|
176
|
+
def announce(ctx, event_name, payload)
|
|
177
|
+
event = Event.new(
|
|
178
|
+
name: event_name,
|
|
179
|
+
aggregate: "#{ctx.domain}::#{ctx.aggregate.hecks_name}",
|
|
180
|
+
id: ctx.route.aggregate,
|
|
181
|
+
payload: payload,
|
|
182
|
+
occurred_at: Time.now.utc.iso8601
|
|
183
|
+
)
|
|
184
|
+
@registry.event_log << event
|
|
185
|
+
[event]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# THE ONE PLACE THIS DIFFERS FROM CommandRules::Emission — there is no
|
|
189
|
+
# mutated instance to read an id off, because nothing was hydrated or
|
|
190
|
+
# saved. The record this event is ABOUT is named by whichever attribute
|
|
191
|
+
# is a reference to the owning aggregate (PortOperationBuilder#build
|
|
192
|
+
# already refused to build an operation with none), so its coerced
|
|
193
|
+
# value — already a plain id, never an object, per
|
|
194
|
+
# Value::Coercion#refuse_object_reference — is what stamps the event.
|
|
195
|
+
def emit(ctx)
|
|
196
|
+
ctx.operation.emits.map do |event_name|
|
|
197
|
+
event = Event.new(
|
|
198
|
+
name: event_name,
|
|
199
|
+
aggregate: "#{ctx.domain}::#{ctx.aggregate.hecks_name}",
|
|
200
|
+
id: ctx.route.aggregate,
|
|
201
|
+
payload: ctx.args,
|
|
202
|
+
occurred_at: Time.now.utc.iso8601
|
|
203
|
+
)
|
|
204
|
+
@registry.event_log << event.emit!
|
|
205
|
+
event
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|