hecks 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +403 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +148 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +131 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +391 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
- data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +285 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +36 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +96 -0
- data/lib/hecks/behaviors/expectations.rb +280 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
- data/lib/hecks/bluebook/assembly/marks.rb +227 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +78 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +68 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
- data/lib/hecks/bluebook/expression/projection.json +170 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +630 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +452 -0
- data/lib/hecks/bluebook/model_check.rb +318 -0
- data/lib/hecks/bluebook/pattern_subset.rb +120 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +86 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +58 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +389 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +116 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +154 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +300 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +170 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +53 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +132 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +115 -0
- data/lib/hecks/forms/record_table.rb +63 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
- data/lib/hecks/fuzzing/properties.rb +1146 -0
- data/lib/hecks/fuzzing/replay.rb +635 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
- data/lib/hecks/fuzzing/value_generator.rb +177 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +291 -0
- data/lib/hecks/grammar/expression.bluebook +417 -0
- data/lib/hecks/grammar/expression_operators.json +1152 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +437 -0
- data/lib/hecks/language/bluebook/entity.bluebook +316 -0
- data/lib/hecks/language/bluebook/policy.bluebook +172 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +313 -0
- data/lib/hecks/language/port.bluebook +92 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +121 -0
- data/lib/hecks/ports/access_control.port +4 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +4 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +4 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +4 -0
- data/lib/hecks/ports/authorization.rb +59 -0
- data/lib/hecks/ports/clock.port +4 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +4 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +4 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +4 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +4 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +102 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +18 -0
- data/lib/hecks/ports/persistence/lineage.rb +277 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +76 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +413 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +31 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +437 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +141 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +392 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +80 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +89 -0
- data/lib/hecks/query_specification/hop_path.rb +130 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/caller.rb +50 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
- data/lib/hecks/runtime/command_interpreter.rb +413 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +181 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +231 -0
- data/lib/hecks/runtime/dispatcher.rb +286 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +228 -0
- data/lib/hecks/runtime/era_check.rb +136 -0
- data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/runtime/era_guard.rb +193 -0
- data/lib/hecks/runtime/era_tamper.rb +61 -0
- data/lib/hecks/runtime/errors.rb +100 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +128 -0
- data/lib/hecks/runtime/instance.rb +120 -0
- data/lib/hecks/runtime/interpreting.rb +69 -0
- data/lib/hecks/runtime/loader.rb +143 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +254 -0
- data/lib/hecks/runtime/reaction_invocation.rb +245 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +124 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
- data/lib/hecks/runtime/registry/verification.rb +158 -0
- data/lib/hecks/runtime/registry.rb +218 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +301 -0
- data/lib/hecks/runtime/storage_shape.rb +120 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +112 -0
- data/lib/hecks/runtime/value/coercion.rb +500 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +95 -0
- data/lib/hecks/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/translation/audit/layer_one.rb +37 -0
- data/lib/hecks/translation/audit/layer_two.rb +77 -0
- data/lib/hecks/translation/audit/unfed_report.rb +42 -0
- data/lib/hecks/translation/audit.rb +70 -0
- data/lib/hecks/translation/reattest.rb +72 -0
- data/lib/hecks/translation/rule_compiler.rb +120 -0
- data/lib/hecks/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/translation/scaffold.rb +30 -0
- data/lib/hecks/translation.rb +11 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +213 -0
- data/lib/hecks.rb +118 -6
- metadata +401 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
require_relative "errors"
|
|
2
|
+
require_relative "refusal_wording"
|
|
3
|
+
require_relative "caller"
|
|
4
|
+
require_relative "routing"
|
|
5
|
+
require_relative "command_rules"
|
|
6
|
+
require_relative "command_interpreter"
|
|
7
|
+
require_relative "entity_interpreter"
|
|
8
|
+
require_relative "query_interpreter"
|
|
9
|
+
require_relative "read_model_interpreter"
|
|
10
|
+
require_relative "policy_interpreter"
|
|
11
|
+
require_relative "saga_interpreter"
|
|
12
|
+
require_relative "../naming"
|
|
13
|
+
|
|
14
|
+
module Hecks
|
|
15
|
+
module Runtime
|
|
16
|
+
class Dispatcher
|
|
17
|
+
MAX_REACTION_DEPTH = 5
|
|
18
|
+
|
|
19
|
+
Result = Struct.new(:verb, :instance, :events, :execution_plan, :persistence_outcome, keyword_init: true) do
|
|
20
|
+
# `instance` is nil for a port operation dispatched by verb (below)
|
|
21
|
+
# — nothing was hydrated or saved, the same reason
|
|
22
|
+
# `PortOperationInterpreter#emit`'s own comment gives for sourcing
|
|
23
|
+
# `id:` off the operation's reference attribute instead. `&.`, not
|
|
24
|
+
# a raised error: a caller that dispatches a port verb and then
|
|
25
|
+
# asks this Result for `.id`/`.state` made a category error the
|
|
26
|
+
# domain itself already told it about (there is no record here),
|
|
27
|
+
# not a crash-worthy one.
|
|
28
|
+
def id = instance&.id
|
|
29
|
+
def state = instance&.to_h
|
|
30
|
+
|
|
31
|
+
def to_s
|
|
32
|
+
announced = events.empty? ? "no events" : events.map(&:name).join(", ")
|
|
33
|
+
"#{verb} → #{instance.inspect} | #{announced}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def inspect = "#<Result #{self}>"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_reader :registry
|
|
40
|
+
|
|
41
|
+
def initialize(registry)
|
|
42
|
+
@registry = registry
|
|
43
|
+
rules = CommandRules.new(registry)
|
|
44
|
+
@commands = CommandInterpreter.new(registry, rules: rules)
|
|
45
|
+
@port_ops = PortOperationInterpreter.new(registry, rules: rules)
|
|
46
|
+
@entities = EntityInterpreter.new(registry, rules: rules)
|
|
47
|
+
@queries = QueryInterpreter.new(registry)
|
|
48
|
+
@read_models = ReadModelInterpreter.new(registry)
|
|
49
|
+
@policies = PolicyInterpreter.new(registry, door: self)
|
|
50
|
+
@sagas = SagaInterpreter.new(registry, door: self)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def events = @registry.event_log
|
|
54
|
+
|
|
55
|
+
def reactions = @registry.reaction_log
|
|
56
|
+
|
|
57
|
+
def sagas = @registry.saga_log
|
|
58
|
+
def saga_dispatches = @registry.saga_dispatch_log
|
|
59
|
+
def policy_dispatches = @registry.policy_dispatch_log
|
|
60
|
+
def verbs = @registry.verbs
|
|
61
|
+
|
|
62
|
+
def dispatch(verb, to: nil, with: nil, saga_correlation: nil, **legacy_args)
|
|
63
|
+
domain, aggregate_name, command_name = parse(verb)
|
|
64
|
+
aggregate = resolve_aggregate(domain, aggregate_name, verb)
|
|
65
|
+
|
|
66
|
+
instance, announced, execution_plan, persistence_outcome =
|
|
67
|
+
if command_name.include?(".")
|
|
68
|
+
head, sub = command_name.split(".", 2)
|
|
69
|
+
port = aggregate.port(head)
|
|
70
|
+
# A PORT OPERATION, reached by the SAME verb shape an entity
|
|
71
|
+
# command already uses ("Domain::Aggregate.Head.Rest") — ports
|
|
72
|
+
# are checked first, so an aggregate that ever declared both a
|
|
73
|
+
# port and an entity of the same name would resolve to the
|
|
74
|
+
# port; no domain in this corpus does, and `dispatch_port`'s
|
|
75
|
+
# own header already named this as an open wire-spelling
|
|
76
|
+
# question this resolves, not silently avoids. No `instance`
|
|
77
|
+
# comes back — nothing is hydrated or saved by a port
|
|
78
|
+
# operation (`PortOperationInterpreter`'s own header) — so
|
|
79
|
+
# `Result#id`/`#state` are nil-safe (above) for exactly this
|
|
80
|
+
# path.
|
|
81
|
+
if port
|
|
82
|
+
operation = port.operation(sub) ||
|
|
83
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "port_no_operation",
|
|
84
|
+
port: head, operation: sub.inspect))
|
|
85
|
+
route, args = port_invocation(aggregate, operation, to: to, with: with, legacy: legacy_args)
|
|
86
|
+
[nil, @port_ops.call(domain, aggregate, operation, args, route: route), nil, nil]
|
|
87
|
+
else
|
|
88
|
+
entity_depth = command_name.split(".").size - 1
|
|
89
|
+
route = Routing.envelope(to, entity_depth: entity_depth)
|
|
90
|
+
@entities.call(domain, aggregate, command_name, legacy_args, route: route, with: with)
|
|
91
|
+
end
|
|
92
|
+
else
|
|
93
|
+
command = aggregate.command(command_name) ||
|
|
94
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "aggregate_no_command",
|
|
95
|
+
aggregate: aggregate_name, command: command_name.inspect))
|
|
96
|
+
args = Routing.payload(command, with: with, legacy: legacy_args)
|
|
97
|
+
route = Routing.envelope(to)
|
|
98
|
+
@commands.call(domain, aggregate, command, args, saga_correlation, route: route)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Correlation is SET AT CONSTRUCTION now, not merged on here —
|
|
102
|
+
# it is part of the transaction, known from this method's own
|
|
103
|
+
# argument before a single event exists. It used to be stamped
|
|
104
|
+
# onto already-emitted events, which is what kept an event
|
|
105
|
+
# mutable after it had happened.
|
|
106
|
+
#
|
|
107
|
+
# The ordering this note used to guard still holds, and more
|
|
108
|
+
# simply: `SagaInterpreter#advance` runs on THIS domain's
|
|
109
|
+
# `announced` events within this very call, and finds the
|
|
110
|
+
# correlation already there because it was never absent.
|
|
111
|
+
|
|
112
|
+
announced.each { |event| @policies.react(event, domain) }
|
|
113
|
+
|
|
114
|
+
announced.each { |event| @sagas.advance(event, domain) }
|
|
115
|
+
|
|
116
|
+
Result.new(verb: verb, instance: instance, events: announced,
|
|
117
|
+
execution_plan: execution_plan, persistence_outcome: persistence_outcome)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# "IF THIS WERE DISPATCHED RIGHT NOW, WOULD IT SUCCEED" — the same
|
|
121
|
+
# pipeline #dispatch itself runs (arguments coerced, givens checked,
|
|
122
|
+
# mutations applied IN MEMORY, ensures checked against the settled
|
|
123
|
+
# result), except `step_save`/`step_emit` never run, and neither do
|
|
124
|
+
# policies or sagas afterward: nothing here is committed, so nothing
|
|
125
|
+
# should react to it. Built for exactly the shape a whole-board
|
|
126
|
+
# postcondition needs to be tested against (a downstream project's
|
|
127
|
+
# own chess domain, checking "does this move leave my own king in
|
|
128
|
+
# check" — the alternative was dispatching a real, unrelated piece's
|
|
129
|
+
# own move purely to trigger the check, which then had to avoid
|
|
130
|
+
# interfering with the very position being tested).
|
|
131
|
+
#
|
|
132
|
+
# RAISES THE SAME REFUSALS #dispatch does — a DomainRefusal
|
|
133
|
+
# subclass propagates normally, so a caller checking "would this be
|
|
134
|
+
# legal" writes the identical rescue clause a real dispatch already
|
|
135
|
+
# needs; this returns `true` only when nothing was refused.
|
|
136
|
+
#
|
|
137
|
+
# NEVER A PORT VERB — `PortOperationInterpreter`'s own side effects
|
|
138
|
+
# (an external gateway call, say) have no meaningful in-memory-only
|
|
139
|
+
# form, so this refuses one outright rather than silently running
|
|
140
|
+
# it for real, which "dry" would otherwise quietly lie about.
|
|
141
|
+
def dry_run?(verb, **args)
|
|
142
|
+
domain, aggregate_name, command_name = parse(verb)
|
|
143
|
+
aggregate = resolve_aggregate(domain, aggregate_name, verb)
|
|
144
|
+
|
|
145
|
+
if command_name.include?(".")
|
|
146
|
+
head, = command_name.split(".", 2)
|
|
147
|
+
if aggregate.port(head)
|
|
148
|
+
raise WiringError,
|
|
149
|
+
"#{verb} names a port operation — dry_run? has no in-memory form for one, " \
|
|
150
|
+
"only for aggregate and entity commands"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
@entities.call(domain, aggregate, command_name, args, dry_run: true)
|
|
154
|
+
else
|
|
155
|
+
command = aggregate.command(command_name) ||
|
|
156
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "aggregate_no_command",
|
|
157
|
+
aggregate: aggregate_name, command: command_name.inspect))
|
|
158
|
+
@commands.call(domain, aggregate, command, args, dry_run: true)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# THE DOOR AN ADAPTER OUTSIDE THE BLUEBOOK CALLS THROUGH — never the
|
|
165
|
+
# domain itself. `port_name`/`operation_name` are separate arguments
|
|
166
|
+
# rather than one packed verb string on purpose: there is no established
|
|
167
|
+
# wire spelling for "domain, aggregate, port, operation" yet, and
|
|
168
|
+
# inventing one is a bigger decision than this call needs to make.
|
|
169
|
+
#
|
|
170
|
+
# No adapter-to-port binding lookup happens here — that is
|
|
171
|
+
# `Hecks.adapter`'s existing job (unchanged by this), and wiring "which
|
|
172
|
+
# adapter may call this port" through is the next piece, not this one.
|
|
173
|
+
def dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, **legacy_args)
|
|
174
|
+
aggregate = resolve_aggregate(domain, aggregate_name, "#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}")
|
|
175
|
+
port = aggregate.port(port_name) ||
|
|
176
|
+
raise(UnknownVerb, "#{aggregate_name} has no port #{port_name.inspect}")
|
|
177
|
+
operation = port.operation(operation_name) ||
|
|
178
|
+
raise(UnknownVerb, "#{port_name} has no operation #{operation_name.inspect}")
|
|
179
|
+
|
|
180
|
+
route, args = port_invocation(aggregate, operation, to: to, with: with, legacy: legacy_args)
|
|
181
|
+
announced = @port_ops.call(domain, aggregate, operation, args, route: route)
|
|
182
|
+
|
|
183
|
+
announced.each { |event| @policies.react(event, domain) }
|
|
184
|
+
announced.each { |event| @sagas.advance(event, domain) }
|
|
185
|
+
|
|
186
|
+
announced
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def port_invocation(aggregate, operation, to:, with:, legacy:)
|
|
190
|
+
legacy = legacy.dup
|
|
191
|
+
identity = operation.identity_attribute(aggregate.hecks_name)
|
|
192
|
+
if to.nil? && identity && legacy.key?(identity.name)
|
|
193
|
+
to = legacy.delete(identity.name)
|
|
194
|
+
elsif to.nil? && operation.to == aggregate.hecks_name
|
|
195
|
+
# `to:`-DECLARED OPERATIONS carry no Reference-typed attribute at
|
|
196
|
+
# all (PortOperationBuilder#initialize's own comment on why —
|
|
197
|
+
# genuine routing metadata, not an attribute), so `identity`
|
|
198
|
+
# above is always nil for these; this is the second, purely
|
|
199
|
+
# additive lookup they need instead. The routing value sits in
|
|
200
|
+
# a PLAIN external-fact attribute, named for the owning
|
|
201
|
+
# aggregate's own identified_by field — the domain author's job
|
|
202
|
+
# to match, same discipline reference_to's own `as:` always
|
|
203
|
+
# required. Composite identity (more than one identified_by
|
|
204
|
+
# component) isn't attempted here — `.first` only, no domain in
|
|
205
|
+
# the real corpus has needed more for a port operation yet.
|
|
206
|
+
#
|
|
207
|
+
# READ, NOT deleted — unlike the Reference-attribute branch
|
|
208
|
+
# above, this is a genuine declared operation attribute (Rust's
|
|
209
|
+
# own comment: "declare only external facts with attribute"),
|
|
210
|
+
# not synthetic routing-only state; the operation's own
|
|
211
|
+
# attributes still expect to find it in the payload a few steps
|
|
212
|
+
# later (refuse_absent_arguments), and a real, live
|
|
213
|
+
# AbsentArgument confirmed this the hard way before `[]`
|
|
214
|
+
# replaced `delete`.
|
|
215
|
+
identity_name = Array(aggregate.identified_by).first
|
|
216
|
+
to = legacy[identity_name] if identity_name && legacy.key?(identity_name)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
route = Routing.envelope(to)
|
|
220
|
+
raise TypeMismatch, "#{operation.hecks_name} requires its receiving aggregate in to:" unless route
|
|
221
|
+
|
|
222
|
+
[route, Routing.payload(operation, with: with, legacy: legacy)]
|
|
223
|
+
end
|
|
224
|
+
private :port_invocation
|
|
225
|
+
|
|
226
|
+
def query(verb, **args)
|
|
227
|
+
domain, query_name = verb.to_s.split(".", 2)
|
|
228
|
+
if query_name && !domain.include?("::")
|
|
229
|
+
bluebook = @registry.bluebook(domain) ||
|
|
230
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_domain", domain: domain.inspect, verb: verb))
|
|
231
|
+
model = bluebook.read_model(query_name) ||
|
|
232
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_read_model",
|
|
233
|
+
domain: domain, query: query_name.inspect))
|
|
234
|
+
return @read_models.call(domain, model, args)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
domain, aggregate_name, query_name = parse(verb)
|
|
238
|
+
aggregate = resolve_aggregate(domain, aggregate_name, verb)
|
|
239
|
+
|
|
240
|
+
@queries.call(domain, aggregate, query_name, args)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# The same ask, answered by the reference interpreter alone — never
|
|
244
|
+
# the bound adapter's native hook. Read models have no reference
|
|
245
|
+
# twin, so only the aggregate-query form answers here; the fuzzer's
|
|
246
|
+
# query oracle diffs this against #query's answer.
|
|
247
|
+
def reference_query(verb, **args)
|
|
248
|
+
domain, aggregate_name, query_name = parse(verb)
|
|
249
|
+
aggregate = resolve_aggregate(domain, aggregate_name, verb)
|
|
250
|
+
|
|
251
|
+
@queries.reference_call(domain, aggregate, query_name, args)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# A reaction is the SYSTEM acting, not the caller who happened to be
|
|
255
|
+
# on the stack when the triggering command ran — the ambient caller
|
|
256
|
+
# is cleared for the reaction's own dispatch, so a triggering
|
|
257
|
+
# caller's role can neither satisfy nor block a reaction command it
|
|
258
|
+
# has nothing to do with (Runtime::Caller.without).
|
|
259
|
+
def reenter(verb, saga_correlation: nil, **args)
|
|
260
|
+
depth = @reaction_depth.to_i
|
|
261
|
+
@reaction_depth = depth + 1
|
|
262
|
+
Caller.without { dispatch(verb, saga_correlation: saga_correlation, **args) }
|
|
263
|
+
ensure
|
|
264
|
+
@reaction_depth = depth
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def reaction_depth_reached? = @reaction_depth.to_i >= MAX_REACTION_DEPTH
|
|
268
|
+
def max_reaction_depth = MAX_REACTION_DEPTH
|
|
269
|
+
|
|
270
|
+
private
|
|
271
|
+
|
|
272
|
+
def parse(verb)
|
|
273
|
+
Naming.split_verb(verb) ||
|
|
274
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "not_fully_qualified", verb: verb.inspect))
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def resolve_aggregate(domain, aggregate_name, verb)
|
|
278
|
+
bluebook = @registry.bluebook(domain) ||
|
|
279
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_domain", domain: domain.inspect, verb: verb))
|
|
280
|
+
bluebook.aggregate(aggregate_name) ||
|
|
281
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_aggregate",
|
|
282
|
+
domain: domain, aggregate: aggregate_name.inspect))
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
require_relative "../naming"
|
|
2
|
+
require_relative "../freezer"
|
|
3
|
+
require_relative "value"
|
|
4
|
+
require_relative "refusal_wording"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "identity"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Runtime
|
|
10
|
+
# ONE ENTITY ELEMENT, LOCATED AND MUTATED — the walk-and-write half of
|
|
11
|
+
# dispatching into a piece an aggregate holds, factored out of
|
|
12
|
+
# `EntityInterpreter` so a SECOND caller (`CommandInterpreter`'s own
|
|
13
|
+
# `delegate_to_entity` step) can locate and mutate the SAME element the
|
|
14
|
+
# same way, against an aggregate record it already holds in memory
|
|
15
|
+
# rather than one freshly loaded from a repository. `EntityInterpreter`
|
|
16
|
+
# keeps its own dispatch order and Context; this is the part underneath
|
|
17
|
+
# that both now share, so it exists in exactly one place rather than two
|
|
18
|
+
# that could only ever drift (the same reasoning `Runtime::Identity`'s
|
|
19
|
+
# own header gives for the join/dig/reading trio it centralizes).
|
|
20
|
+
#
|
|
21
|
+
# `rules` (a `CommandRules` instance) is passed explicitly throughout
|
|
22
|
+
# rather than closed over, since this module has no instance of its own
|
|
23
|
+
# to hold one.
|
|
24
|
+
module EntityElement
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# ONE HOP PER CHAIN ENTRY. `container` starts as `instance` (the root
|
|
28
|
+
# aggregate record) and becomes each just-located element in turn —
|
|
29
|
+
# Dispatch's own element is found INSIDE the Handler element
|
|
30
|
+
# `locate_chain` located the step before, never inside `instance`
|
|
31
|
+
# directly. `owner` is whichever construct's OWN attribute declares
|
|
32
|
+
# the list being searched (Handler declares `dispatches` ; the root
|
|
33
|
+
# aggregate declares `handlers`) — `root_aggregate` stays the ROOT
|
|
34
|
+
# the whole way through instead, passed to `element_of` separately,
|
|
35
|
+
# because coercion (`Value.for_attribute`) resolves value objects
|
|
36
|
+
# against the root's own namespace only ; an entity must never
|
|
37
|
+
# answer `.value_object` (Entity's own header comment) so handing
|
|
38
|
+
# it an intermediate owner instead would break every VO-typed
|
|
39
|
+
# identity field a nested entity declares.
|
|
40
|
+
#
|
|
41
|
+
# `route`, if given, is the routing envelope's own entity chain
|
|
42
|
+
# (`ctx.route.entities`, one entry per hop) — the routing/payload
|
|
43
|
+
# split's own consequence: an entity's own identity may arrive via
|
|
44
|
+
# `to:` rather than duplicated into `args`, so each hop is offered
|
|
45
|
+
# its routed identity ahead of falling back to `args`.
|
|
46
|
+
def locate_chain(root_aggregate, chain, instance, args, command_name, route = nil)
|
|
47
|
+
container = instance
|
|
48
|
+
owner = root_aggregate
|
|
49
|
+
chain.each_with_index do |entity, index|
|
|
50
|
+
container = element_of(root_aggregate, owner, entity, command_name, container, args,
|
|
51
|
+
route&.entities&.fetch(index))
|
|
52
|
+
owner = entity
|
|
53
|
+
end
|
|
54
|
+
container
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# ONE ELEMENT, MATCHED ON EVERY PART OF ITS IDENTITY — not just the first.
|
|
58
|
+
# A piece's identity may be several paths, the same shape a head's can be,
|
|
59
|
+
# so a dispatch that names the element has to supply every part and every
|
|
60
|
+
# part has to agree with the stored one. `routed_identity`, when given,
|
|
61
|
+
# matches by the element's own minted identity string directly instead
|
|
62
|
+
# (`element_identity`, below) — the routing envelope already resolved
|
|
63
|
+
# which element it means, so re-deriving `wants` from `args` would be
|
|
64
|
+
# redundant at best and wrong if `args` no longer carries that identity
|
|
65
|
+
# at all.
|
|
66
|
+
def element_of(root_aggregate, owner, entity, command_name, container, args, routed_identity = nil)
|
|
67
|
+
entity_name = entity.hecks_name
|
|
68
|
+
list_attr = owner.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
|
|
69
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_holds_no_list",
|
|
70
|
+
aggregate: owner.hecks_name, entity: entity_name))
|
|
71
|
+
|
|
72
|
+
wants = unless routed_identity
|
|
73
|
+
entity.identity_paths.map do |path|
|
|
74
|
+
head = path.to_s.split(".").first.to_sym
|
|
75
|
+
raw = args[head] ||
|
|
76
|
+
raise(NotFound, RefusalWording.render("NotFound", "entity_element_no_identity",
|
|
77
|
+
command: command_name, entity: entity_name,
|
|
78
|
+
identity: Identity.reading(entity)))
|
|
79
|
+
|
|
80
|
+
[head, path, Value.for_attribute(root_aggregate, entity.attribute(head), raw)]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
original = Array(container[list_attr.name])
|
|
85
|
+
position = if routed_identity
|
|
86
|
+
original.find_index { |element| element_identity(entity, element).to_s == routed_identity.to_s }
|
|
87
|
+
else
|
|
88
|
+
original.find_index { |el| wants.all? { |head, _path, want| el[head] == want } }
|
|
89
|
+
end
|
|
90
|
+
unless position
|
|
91
|
+
raise NotFound, RefusalWording.render(
|
|
92
|
+
"NotFound", "entity_element_missing",
|
|
93
|
+
entity: entity_name, identity: Identity.reading(entity),
|
|
94
|
+
wants: wants&.map { |_h, path, want| Identity.scalar(path, want) }&.join(", "),
|
|
95
|
+
aggregate: owner.hecks_name,
|
|
96
|
+
parent_id: container.respond_to?(:id) ? container.id.inspect : Rendering.describe(container)
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# ONE LEVEL DEEPER THAN Instance#dup, for the same reason: a list
|
|
101
|
+
# attribute holds Hashes, and `apply_to_element` mutates the found
|
|
102
|
+
# one IN PLACE — the update mechanism for an entity, not a bug. But
|
|
103
|
+
# in place means aliased with the adapter's own record until this
|
|
104
|
+
# copies the array and the target element before handing either
|
|
105
|
+
# back, and writes the fresh array into `container` so the copy is
|
|
106
|
+
# what persists on success and NOTHING aliased survives a refusal.
|
|
107
|
+
# `container[list_attr.name] = copied` reaches `instance` itself
|
|
108
|
+
# when this is the FIRST hop, and reaches the (already copied)
|
|
109
|
+
# PARENT element when it is a later one — either way it is the
|
|
110
|
+
# SAME already-fresh object `locate_chain` is about to hand back
|
|
111
|
+
# as `container` for the next hop, so nothing further has to
|
|
112
|
+
# propagate a write back up the chain by hand.
|
|
113
|
+
copied = original.dup
|
|
114
|
+
element = copied[position].dup
|
|
115
|
+
copied[position] = element
|
|
116
|
+
container[list_attr.name] = copied
|
|
117
|
+
element
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# THE ELEMENT'S OWN IDENTITY, joined from its parts — the entity-level
|
|
121
|
+
# twin of `Identity.of`, reading off the STORED ELEMENT (a Hash) rather
|
|
122
|
+
# than a dispatch payload. An id is a SCALAR, and the PATH is how it is
|
|
123
|
+
# reached — never by opening a value object and taking whatever single
|
|
124
|
+
# field is inside. That unwrapping is gone from the language: a piece
|
|
125
|
+
# that does not name its fields is refused when the bluebook loads ("an
|
|
126
|
+
# entity says what it is known by", "an identity part names something"),
|
|
127
|
+
# so by the time a dispatch arrives here there is always a path to dig.
|
|
128
|
+
def element_identity(entity, element)
|
|
129
|
+
parts = entity.identity_paths.map do |path|
|
|
130
|
+
head = path.to_s.split(".").first.to_sym
|
|
131
|
+
Identity.scalar(path, element[head])
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
Naming.identity(parts)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# S17, ADR 0026 — `:append`/`:remove`/`:multiply`/`:clamp`, an
|
|
138
|
+
# entity-scoped mirror of `CommandInterpreter::MutationApplier
|
|
139
|
+
# #apply`'s own four (that module's own header names each one's
|
|
140
|
+
# origin). Missing until now — an entity-owned command declaring
|
|
141
|
+
# `sets :some_list, append: {...}` matched no `when` here and
|
|
142
|
+
# silently no-opped, the one place this language otherwise
|
|
143
|
+
# refuses what it cannot check applying nothing instead. `Member`/
|
|
144
|
+
# `Dispatch` (S17) are the first real callers: both need to
|
|
145
|
+
# append a value-object-typed element (`Pair`/`Binding`) onto a
|
|
146
|
+
# list attribute THEY OWN, once they become entities of
|
|
147
|
+
# `ValueObject`/`ProcessManager` rather than separate aggregates.
|
|
148
|
+
#
|
|
149
|
+
# `:increment`/`:decrement`/`:multiply` ALSO fixed here, found
|
|
150
|
+
# while proving this method against a real fixture: they wrapped
|
|
151
|
+
# `amount` unconditionally whenever `attribute` existed, the same
|
|
152
|
+
# asymmetric-wrapping shape `MutationApplier#rewrap_arithmetic_
|
|
153
|
+
# result`'s own comment documents fixing at the aggregate level
|
|
154
|
+
# (migration plan task 9) — a phantom-created VO-typed field's
|
|
155
|
+
# `current` reads back a raw, unwrapped default, `amount` was
|
|
156
|
+
# wrapped anyway, and the two sides of one arithmetic call
|
|
157
|
+
# disagreed on Value-ness. Confirmed live, not theoretical: this
|
|
158
|
+
# method's own fixture (TaggedList.Bump, a VO-typed `count` with
|
|
159
|
+
# `default: 0`) raised exactly this TypeMismatch on its first
|
|
160
|
+
# real run.
|
|
161
|
+
def apply_to_element(rules, aggregate, entity, element, mutation, args)
|
|
162
|
+
case mutation.op
|
|
163
|
+
when :set
|
|
164
|
+
value = rules.resolve_source(mutation.source, args)
|
|
165
|
+
attribute = entity.attribute(mutation.target)
|
|
166
|
+
element[mutation.target] = attribute ? Value.for_attribute(aggregate, attribute, value) : value
|
|
167
|
+
when :append
|
|
168
|
+
element[mutation.target] = appended_to_element(aggregate, entity, element, mutation, args)
|
|
169
|
+
when :remove
|
|
170
|
+
element[mutation.target] = removed_from_element(rules, aggregate, entity, element, mutation, args)
|
|
171
|
+
when :increment, :decrement
|
|
172
|
+
attribute = entity.attribute(mutation.target)
|
|
173
|
+
amount = rules.resolve_source(mutation.source, args)
|
|
174
|
+
current = element[mutation.target]
|
|
175
|
+
amount = Value.for_attribute(aggregate, attribute, amount) if attribute && current.is_a?(Value)
|
|
176
|
+
result = rules.arithmetic(current, amount, mutation.target, rules.sign_of(mutation.op))
|
|
177
|
+
element[mutation.target] = rewrap_arithmetic_result(aggregate, attribute, current, result)
|
|
178
|
+
when :multiply
|
|
179
|
+
attribute = entity.attribute(mutation.target)
|
|
180
|
+
amount = rules.resolve_source(mutation.source, args)
|
|
181
|
+
current = element[mutation.target]
|
|
182
|
+
amount = Value.for_attribute(aggregate, attribute, amount) if attribute && current.is_a?(Value)
|
|
183
|
+
result = rules.multiply(current, amount, mutation.target)
|
|
184
|
+
element[mutation.target] = rewrap_arithmetic_result(aggregate, attribute, current, result)
|
|
185
|
+
when :clamp
|
|
186
|
+
element[mutation.target] = rules.clamp(element[mutation.target], mutation.source, mutation.target)
|
|
187
|
+
else
|
|
188
|
+
# The aggregate-level twin's own backstop
|
|
189
|
+
# (MutationApplier#apply), for the same reason: applying
|
|
190
|
+
# nothing and refusing nothing would be the one silent
|
|
191
|
+
# no-op in a language that otherwise refuses what it cannot
|
|
192
|
+
# check.
|
|
193
|
+
raise WiringError, "no entity mutation applier handles :#{mutation.op} — add one before declaring it"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# `MutationApplier#rewrap_arithmetic_result`'s own entity-scoped
|
|
198
|
+
# twin, byte-for-byte the same fix — see that method's own
|
|
199
|
+
# comment for the full "phantom-field asymmetric wrapping" story.
|
|
200
|
+
# A no-op whenever `current` was already a Value (the arithmetic
|
|
201
|
+
# call already returned one) or the mutation targets no declared
|
|
202
|
+
# attribute at all.
|
|
203
|
+
def rewrap_arithmetic_result(aggregate, attribute, current, result)
|
|
204
|
+
return result if current.is_a?(Value) || attribute.nil? || result.is_a?(Value)
|
|
205
|
+
|
|
206
|
+
Value.for_attribute(aggregate, attribute, result)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# `MutationApplier#resolve_append_source`'s own entity-scoped
|
|
210
|
+
# twin — a caller-supplied ARG first, falling back to the
|
|
211
|
+
# ELEMENT's own current field (never the parent instance's) when
|
|
212
|
+
# it isn't one.
|
|
213
|
+
def resolve_element_append_source(source, element, args)
|
|
214
|
+
return source unless source.is_a?(Symbol)
|
|
215
|
+
return args[source] if args.key?(source)
|
|
216
|
+
|
|
217
|
+
element[source]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# `MutationApplier#appended`'s own entity-scoped twin. VALUE-
|
|
221
|
+
# OBJECT elements only — an entity's own list, appended to by an
|
|
222
|
+
# entity-owned command, holds a value object (`Member.pairs`'
|
|
223
|
+
# own `Pair`, `Dispatch.with_spec`'s own `Binding`) the same way
|
|
224
|
+
# every real corpus append does; entity-in-entity nesting (a
|
|
225
|
+
# list of ANOTHER entity, owned by this one) is out of scope —
|
|
226
|
+
# `MutationApplier#entity_element`'s own fallback is deliberately
|
|
227
|
+
# not mirrored here, since nothing in this language's own
|
|
228
|
+
# `EntityBuilder` can declare a nested entity to need it (see
|
|
229
|
+
# S17's own scoping note on why Dispatch flattens under
|
|
230
|
+
# ProcessManager instead of nesting under Handler).
|
|
231
|
+
def appended_to_element(aggregate, entity, element, mutation, args)
|
|
232
|
+
fields = mutation.source.transform_values { |source| resolve_element_append_source(source, element, args) }
|
|
233
|
+
element_type = entity.attribute(mutation.target)&.type
|
|
234
|
+
value_object = aggregate.value_object(element_type)
|
|
235
|
+
value_object&.attributes&.each do |attribute|
|
|
236
|
+
fields[attribute.name] = Value.scalar(fields[attribute.name]) if fields[attribute.name].is_a?(Value)
|
|
237
|
+
end
|
|
238
|
+
appended = value_object ? Value.build(value_object, fields, aggregate) : fields
|
|
239
|
+
Freezer.deep(Array(element[mutation.target]) + [appended])
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# `MutationApplier#removed`'s own entity-scoped twin — matches by
|
|
243
|
+
# VALUE EQUALITY, element-wise, the same "so a concurrent Add can
|
|
244
|
+
# never be lost" reasoning that method's own comment gives.
|
|
245
|
+
def removed_from_element(rules, aggregate, entity, element, mutation, args)
|
|
246
|
+
value = rules.resolve_source(mutation.source, args)
|
|
247
|
+
attribute = entity.attribute(mutation.target)
|
|
248
|
+
value = Value.for_attribute(aggregate, attribute, value) if attribute
|
|
249
|
+
Array(element[mutation.target]).reject { |candidate| candidate == value }
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|