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,1018 @@
|
|
|
1
|
+
require_relative "word_gate"
|
|
2
|
+
module Hecks
|
|
3
|
+
module Bluebook
|
|
4
|
+
module DSL
|
|
5
|
+
class BluebookBuilder
|
|
6
|
+
GRAMMAR_CONTEXT = "Bluebook"
|
|
7
|
+
|
|
8
|
+
include WordGate
|
|
9
|
+
|
|
10
|
+
attr_reader :classification
|
|
11
|
+
|
|
12
|
+
def initialize(name, version: nil)
|
|
13
|
+
@name = name
|
|
14
|
+
@version = version
|
|
15
|
+
@aggregates = []
|
|
16
|
+
@read_models = []
|
|
17
|
+
@policies = []
|
|
18
|
+
@process_managers = []
|
|
19
|
+
# THE ROOT of the CHAPTER-WIDE given pool — one level wider
|
|
20
|
+
# than `AggregateBuilder`'s own `@entity_named_givens` (S10
|
|
21
|
+
# extended across an aggregate's whole entity tree, earlier
|
|
22
|
+
# this arc). See `#given`'s own comment for what this closes.
|
|
23
|
+
@chapter_named_givens = {}
|
|
24
|
+
# EVERY BARE CHAPTER-GIVEN REFERENCE THIS CHAPTER'S OWN FILES
|
|
25
|
+
# LEFT UNRESOLVED SO FAR — threaded into every aggregate the
|
|
26
|
+
# same way `@chapter_named_givens` is. See
|
|
27
|
+
# `AggregateBuilder#pending_chapter_given`'s own comment for
|
|
28
|
+
# what queues here and `#resolve_pending_chapter_givens!`,
|
|
29
|
+
# below, for where it drains.
|
|
30
|
+
@chapter_pending_givens = []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Chapter metadata belongs to the composed folder, not whichever file
|
|
34
|
+
# happened to sort first. A later concept file may be the one carrying
|
|
35
|
+
# the version header; adopt it into the already-open chapter builder.
|
|
36
|
+
# Two different versions are a real contradiction, not load order.
|
|
37
|
+
def adopt_version(version)
|
|
38
|
+
return if version.nil?
|
|
39
|
+
if @version && @version.to_s != version.to_s
|
|
40
|
+
raise Malformed, "#{@name} declares both version #{@version.inspect} and #{version.inspect}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@version = version
|
|
44
|
+
end
|
|
45
|
+
private :adopt_version
|
|
46
|
+
|
|
47
|
+
def vision(value)
|
|
48
|
+
# moved to the language: Vision invariant, on Chapter.Declare
|
|
49
|
+
|
|
50
|
+
@vision = value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A domain's own identity can change — this names what it used to
|
|
54
|
+
# be, so the storage layer can recognize its own history under the
|
|
55
|
+
# old name instead of minting a brand-new lineage from nothing.
|
|
56
|
+
def formerly_known_as(value) = @formerly_known_as = value.to_s
|
|
57
|
+
|
|
58
|
+
# A SUB-LANGUAGE NAMES WHERE IT LANDS. ADR 0026's own seam: the core
|
|
59
|
+
# grammar does not name its extension points, so this chapter names
|
|
60
|
+
# ITSELF onto them instead — the core contexts (e.g. "Query",
|
|
61
|
+
# "ReadModel") whose own admitted words this chapter's `Syntax`
|
|
62
|
+
# aggregate contributes rows for. Variadic, and accumulating across
|
|
63
|
+
# calls the same reason `identified_by`/`group_by` are: nothing here
|
|
64
|
+
# requires one call to name every context at once.
|
|
65
|
+
# RENAMED FROM `attaches_to` — item #13's full metaprogrammed
|
|
66
|
+
# dispatch (slice 4c). Not bootstrap-reachable (only sub-language
|
|
67
|
+
# chapters like Paging use it; the CORE chapters never describe
|
|
68
|
+
# themselves with it).
|
|
69
|
+
def attaches_to_impl(*contexts) = (@attaches_to ||= []).concat(contexts.map(&:to_s))
|
|
70
|
+
|
|
71
|
+
def core = @classification = :core
|
|
72
|
+
def supporting = @classification = :supporting
|
|
73
|
+
def generic = @classification = :generic
|
|
74
|
+
|
|
75
|
+
# `@chapter_named_givens` is threaded into every aggregate this
|
|
76
|
+
# chapter builds — see `AggregateBuilder#given`'s own comment
|
|
77
|
+
# for the sharing this enables; NOT a new top-level DSL word
|
|
78
|
+
# itself (an aggregate's own EXISTING `given` already both
|
|
79
|
+
# declares locally and write-throughs here as a side effect,
|
|
80
|
+
# the identical shape `EntityBuilder#given`'s own write-through
|
|
81
|
+
# to its owner aggregate's pool already takes — no new spelling
|
|
82
|
+
# for "declare a precondition," one level wider, same word).
|
|
83
|
+
# RENAMED FROM `aggregate` — item #13's full metaprogrammed
|
|
84
|
+
# dispatch (slice 4c). Bootstrap-reachable (every core/attached
|
|
85
|
+
# chapter's own top-level shape is written with it), so also
|
|
86
|
+
# named in GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.
|
|
87
|
+
def aggregate_impl(name, &block)
|
|
88
|
+
@aggregates << AggregateBuilder.build(name, chapter_named_givens: @chapter_named_givens,
|
|
89
|
+
chapter_pending_givens: @chapter_pending_givens, &block)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# `read_model` is the word (ADR 0025 reverts `report` — the IR
|
|
93
|
+
# construct, the registry API, and the docs filename all said
|
|
94
|
+
# `read_model` the whole time; no era was ever minted under
|
|
95
|
+
# `report`, so this is history and source agreeing again). `report`
|
|
96
|
+
# stays answered under `MetaValidator.shadow_parsing?` (S0a's own
|
|
97
|
+
# bridge) for the same reason `has_many` does — frozen era text
|
|
98
|
+
# that used it must keep booting; live source refuses it, naming
|
|
99
|
+
# the replacement.
|
|
100
|
+
def read_model(name, &block)
|
|
101
|
+
# A read model gathers heads from SEVERAL aggregates, so no single head
|
|
102
|
+
# declares it — the chapter does. Its owner is stamped in `build`, where
|
|
103
|
+
# the chapter namespace exists.
|
|
104
|
+
@read_models << ReadModelBuilder.build(name, &block)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def report(name, &block)
|
|
108
|
+
return read_model(name, &block) if MetaValidator.shadow_parsing?
|
|
109
|
+
|
|
110
|
+
raise Malformed, "report is gone — read_model is the word now"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def policy(name, &block)
|
|
114
|
+
@policies << PolicyBuilder.build(name, &block)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def process_manager(name, &block)
|
|
118
|
+
@process_managers << ProcessManagerBuilder.build(name, &block)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build
|
|
122
|
+
# The chapter is the top of the construct chain — `Bluebook` is a
|
|
123
|
+
# ROOT, and its constructor stamps every aggregate and read model with
|
|
124
|
+
# itself as owner, so every `hecks_fqn` below resolves by walking up
|
|
125
|
+
# to it. No constants are installed at load time : the public door is
|
|
126
|
+
# a per-boot projection, installed by `Loader.bind_runtime` once a
|
|
127
|
+
# dispatcher exists to close over (facade/surface.rb).
|
|
128
|
+
bluebook = Bluebook::Chapter.new(name: @name, version: @version, vision: @vision,
|
|
129
|
+
aggregates: @aggregates,
|
|
130
|
+
read_models: @read_models,
|
|
131
|
+
policies: @aggregates.flat_map(&:policies) + @policies,
|
|
132
|
+
process_managers: @process_managers,
|
|
133
|
+
classification: @classification,
|
|
134
|
+
formerly_known_as: @formerly_known_as,
|
|
135
|
+
attaches_to: @attaches_to || [])
|
|
136
|
+
|
|
137
|
+
# SAME REASON, SAME GATE — a bare chapter-given may still be
|
|
138
|
+
# pending (see `AggregateBuilder#pending_chapter_given`) if a
|
|
139
|
+
# file that would resolve it hasn't loaded yet; resolving now
|
|
140
|
+
# would see the same incomplete `@chapter_named_givens`
|
|
141
|
+
# `validate_assembled!` below would. Deferred to
|
|
142
|
+
# `MetaValidator.judge_deferred!` the same way, and BEFORE
|
|
143
|
+
# `validate_assembled!` there — nothing downstream should ever
|
|
144
|
+
# read an unresolved placeholder's fields.
|
|
145
|
+
resolve_pending_chapter_givens! unless MetaValidator.deferring?
|
|
146
|
+
|
|
147
|
+
# A CHAPTER MAY BE SPLIT ACROSS FILES (see `self.build`'s own
|
|
148
|
+
# comment). Every check below needs the WHOLE chapter present —
|
|
149
|
+
# a hop, a projection, a correlation key or an event shape can
|
|
150
|
+
# equally name a construct declared in a file that has not
|
|
151
|
+
# loaded yet, and `@aggregates`/`@process_managers` here are
|
|
152
|
+
# only ever as complete as whatever has loaded SO FAR. So,
|
|
153
|
+
# exactly like `MetaValidator.call` below, this is skipped
|
|
154
|
+
# while `MetaValidator.defer` is loading the chapter's files
|
|
155
|
+
# and run once instead — by `MetaValidator.judge_deferred!`,
|
|
156
|
+
# against the fully assembled chapter — after the last one
|
|
157
|
+
# loads. A single-file chapter (still the common case) never
|
|
158
|
+
# sees `deferring?` true here at all, so its own checks still
|
|
159
|
+
# run inline, exactly as before.
|
|
160
|
+
self.class.validate_assembled!(bluebook) unless MetaValidator.deferring?
|
|
161
|
+
|
|
162
|
+
# The language judges the bluebook, in the language. Last, so the
|
|
163
|
+
# meta-domain sees a fully built IR — the whole-document rules need
|
|
164
|
+
# every declaration present, which is why they cannot be givens fired
|
|
165
|
+
# at declaration time.
|
|
166
|
+
MetaValidator.call(bluebook)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# THE OTHER HALF OF A CHAPTER-WIDE `given` REFERENCE —
|
|
170
|
+
# `AggregateBuilder#pending_chapter_given` recognised an
|
|
171
|
+
# unresolved bare reference and deferred it here, unable to
|
|
172
|
+
# check further: a later file in this SAME chapter might still
|
|
173
|
+
# declare the real thing. Runs once every file has loaded,
|
|
174
|
+
# against the now-complete `@chapter_named_givens` pool — the
|
|
175
|
+
# IDENTICAL lookup `reference_named_chapter_given` already does,
|
|
176
|
+
# just late enough to see every aggregate's own declarations,
|
|
177
|
+
# not only the ones loaded before the referencing one.
|
|
178
|
+
#
|
|
179
|
+
# MUTATES each placeholder `Given` IN PLACE rather than
|
|
180
|
+
# replacing it — it is already embedded, by Ruby object
|
|
181
|
+
# reference, in the referencing aggregate's own `preconditions`
|
|
182
|
+
# and in any command (same aggregate) that separately
|
|
183
|
+
# bare-referenced the same description, so there is nothing
|
|
184
|
+
# downstream holding a second, now-stale copy to update.
|
|
185
|
+
# Instance-level (not `self.`, unlike `validate_assembled!`) —
|
|
186
|
+
# unlike that battery, this needs `@chapter_named_givens` itself,
|
|
187
|
+
# which only exists on the builder instance still open for this
|
|
188
|
+
# chapter (`MetaValidator.judge_deferred!` reaches it via
|
|
189
|
+
# `registry.bluebook_builder(name)`, guaranteed already present).
|
|
190
|
+
def resolve_pending_chapter_givens!
|
|
191
|
+
@chapter_pending_givens.each do |entry|
|
|
192
|
+
resolved = resolve_pending_chapter_given(entry)
|
|
193
|
+
entry[:placeholder].description = resolved.description
|
|
194
|
+
entry[:placeholder].canonical = resolved.canonical
|
|
195
|
+
entry[:placeholder].predicate = resolved.predicate
|
|
196
|
+
end
|
|
197
|
+
@chapter_pending_givens.clear
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def resolve_pending_chapter_given(entry)
|
|
201
|
+
description = entry[:description]
|
|
202
|
+
candidates = RuleReference.resolve_owner_keyed(@chapter_named_givens, description)
|
|
203
|
+
|
|
204
|
+
if entry[:declared_by]
|
|
205
|
+
candidates[entry[:declared_by]] ||
|
|
206
|
+
raise(Malformed,
|
|
207
|
+
"#{entry[:aggregate]}'s given #{description.inspect} names no precondition " \
|
|
208
|
+
"#{entry[:declared_by]} declares in this chapter — #{entry[:declared_by]} " \
|
|
209
|
+
"either hasn't declared #{description.inspect}, or declared_by: named the " \
|
|
210
|
+
"wrong aggregate")
|
|
211
|
+
elsif candidates.size == 1
|
|
212
|
+
candidates.values.first
|
|
213
|
+
elsif candidates.empty?
|
|
214
|
+
raise(Malformed,
|
|
215
|
+
"#{entry[:aggregate]}'s given #{description.inspect} names no precondition " \
|
|
216
|
+
"any aggregate in this chapter ever declares — declare it once with a block " \
|
|
217
|
+
"(some aggregate's own given(#{description.inspect}) { ... })")
|
|
218
|
+
else
|
|
219
|
+
raise(Malformed,
|
|
220
|
+
"#{entry[:aggregate]}'s given #{description.inspect} is ambiguous in this " \
|
|
221
|
+
"chapter — #{candidates.keys.join(', ')} each declare a DIFFERENT predicate " \
|
|
222
|
+
"under this same description; name which one with declared_by: (e.g. " \
|
|
223
|
+
"given(#{description.inspect}, declared_by: #{candidates.keys.first}))")
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
private :resolve_pending_chapter_given
|
|
227
|
+
|
|
228
|
+
# EVERY WHOLE-CHAPTER CHECK, IN ONE PLACE — the battery `#build`
|
|
229
|
+
# used to run inline, now a pure function of an assembled
|
|
230
|
+
# `Bluebook::Chapter` so `MetaValidator.judge_deferred!` can run
|
|
231
|
+
# it too, once, on a chapter whose files have ALL loaded (see
|
|
232
|
+
# `#build`'s own comment for why that split exists at all).
|
|
233
|
+
# Public, not `private_class_method`'d, for exactly that second
|
|
234
|
+
# caller — `MetaValidator` needs to reach this with no builder
|
|
235
|
+
# instance in hand, only the chapter `judge_deferred!` already
|
|
236
|
+
# read back out of the registry.
|
|
237
|
+
def self.validate_assembled!(bluebook)
|
|
238
|
+
# moved to the language: an attribute type is a reference to its Shape,
|
|
239
|
+
# so an undeclared value object fails reference resolution
|
|
240
|
+
validate_reference_value_objects!(bluebook.aggregates)
|
|
241
|
+
validate_correlation_keys!(bluebook.process_managers, bluebook.aggregates)
|
|
242
|
+
validate_no_bidirectional_references!(bluebook.aggregates)
|
|
243
|
+
unless MetaValidator.shadow_parsing?
|
|
244
|
+
validate_event_shapes!(bluebook.aggregates)
|
|
245
|
+
validate_with_projections!(bluebook.policies, bluebook.process_managers, bluebook.aggregates)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Every hop AggregateBuilder#seal_query_field recognised and
|
|
249
|
+
# deferred gets checked for real here — the earliest point a
|
|
250
|
+
# hop CAN be checked, for exactly the reason
|
|
251
|
+
# validate_no_bidirectional_references! above already gives:
|
|
252
|
+
# `Bluebook.new` just stamped `hecks_owner` on every
|
|
253
|
+
# aggregate, so `Reference#resolve` finally has a chapter to
|
|
254
|
+
# walk. Before this line every target in the file (including
|
|
255
|
+
# ones declared ABOVE the aggregate doing the asking) would
|
|
256
|
+
# have resolved to nil.
|
|
257
|
+
infer_hop_query_arguments!(bluebook)
|
|
258
|
+
validate_query_hops!(bluebook)
|
|
259
|
+
|
|
260
|
+
# Same precondition, same reason: a `projects` declaration's
|
|
261
|
+
# own reference cannot resolve until every aggregate in the
|
|
262
|
+
# chapter is real and owner-stamped (S12, ADR 0025).
|
|
263
|
+
validate_projected_fields!(bluebook)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# AN ENTITY COMMAND MAY NOT NAME ITSELF AS ITS ROOT.
|
|
267
|
+
#
|
|
268
|
+
# That is the whole of what is left here, and it needs saying plainly
|
|
269
|
+
# because the sentence this used to raise — "references must target
|
|
270
|
+
# aggregate heads" — was never what it checked.
|
|
271
|
+
#
|
|
272
|
+
# `CommandBuilder#reference_to` sets `references` ONLY when the target's
|
|
273
|
+
# bare name equals the owner's ; anything else becomes a reference
|
|
274
|
+
# ATTRIBUTE. So on an aggregate command `references` is always a copy of
|
|
275
|
+
# that aggregate's own name, and looking it up in an index of aggregates
|
|
276
|
+
# is a TAUTOLOGY — that branch never refused anything and structurally
|
|
277
|
+
# could not. Verified across all eight golden chapters before deleting it.
|
|
278
|
+
#
|
|
279
|
+
# On a PIECE's command the owner is the entity, and an entity is not a
|
|
280
|
+
# head, so what this actually refuses is `reference_to <its own name>`
|
|
281
|
+
# written inside `entity do … end`. A piece is reached THROUGH its
|
|
282
|
+
# aggregate ; a command on one addresses the aggregate, never the piece.
|
|
283
|
+
#
|
|
284
|
+
# Reference ATTRIBUTES are the language's business now — offered as the
|
|
285
|
+
# head's own id and resolved as references, so `Aggregate.Reference` and
|
|
286
|
+
# `Command.Reference` refuse an undeclared head with no predicate at all.
|
|
287
|
+
def self.validate_reference_value_objects!(aggregates)
|
|
288
|
+
heads = aggregates.map(&:hecks_name)
|
|
289
|
+
|
|
290
|
+
violations = aggregates.flat_map do |aggregate|
|
|
291
|
+
aggregate.entities.flat_map do |entity|
|
|
292
|
+
entity.commands.filter_map do |command|
|
|
293
|
+
next unless command.references
|
|
294
|
+
next if heads.include?(command.references.to_s)
|
|
295
|
+
|
|
296
|
+
"#{aggregate.hecks_name}.#{entity.hecks_name}.#{command.hecks_name} names itself as its root"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
return if violations.empty?
|
|
302
|
+
|
|
303
|
+
raise Malformed,
|
|
304
|
+
"an entity command is addressed through its aggregate; #{violations.uniq.join('; ')}"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# EVENTS ARE FIRST-CLASS BY CONVENTION, NOT BY DECLARATION (ADR
|
|
308
|
+
# 0025, "events and reactions" — "a domain event is a value
|
|
309
|
+
# object with its own attributes, not a label"). No new `event
|
|
310
|
+
# do ... end` construct exists to hand-author and keep in step
|
|
311
|
+
# with every emitting command by hand — an event's own known
|
|
312
|
+
# shape IS whichever command(s) declare `emits` for its name, and
|
|
313
|
+
# this is the ONE thing that has to hold for that convention to
|
|
314
|
+
# mean anything: every command that emits a given name has to
|
|
315
|
+
# agree on what it carries. An event is one fact; a fact does not
|
|
316
|
+
# carry two different truths depending on who is telling it.
|
|
317
|
+
#
|
|
318
|
+
# STRUCTURAL fields only (name/type/list/optional) — `pattern:`/
|
|
319
|
+
# `admits:`/`default:` are refinements ON a field, not a second
|
|
320
|
+
# claim about what the payload holds, so two emitting commands
|
|
321
|
+
# are free to differ there without actually disagreeing about
|
|
322
|
+
# the event's own shape.
|
|
323
|
+
def self.validate_event_shapes!(aggregates)
|
|
324
|
+
event_emitters(aggregates).each do |event_name, pairs|
|
|
325
|
+
next if pairs.size == 1
|
|
326
|
+
|
|
327
|
+
shapes = pairs.map { |(owner, command)| event_shape(command, owner_aggregate(owner, aggregates)) }.uniq
|
|
328
|
+
next if shapes.size == 1
|
|
329
|
+
|
|
330
|
+
named = pairs.map { |(owner, command)| "#{owner}.#{command.hecks_name}" }.sort
|
|
331
|
+
raise Malformed,
|
|
332
|
+
"#{event_name.inspect} is emitted with different shapes by #{named.join(' and ')} — " \
|
|
333
|
+
"an event is one fact, and every command that emits it must declare the same fields"
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# THE "EXPENSIVE HALF" the ADR names: "with: { account: :account }
|
|
338
|
+
# projecting into a reaction that has no declared contract ...
|
|
339
|
+
# breaks at dispatch rather than at load." Checked here, now that
|
|
340
|
+
# `validate_event_shapes!` (above) guarantees at most one real
|
|
341
|
+
# shape per event name, and command references being first-class
|
|
342
|
+
# (`Naming.command_ref`) means the TARGET side is a real
|
|
343
|
+
# resolvable command, not a string that might be a typo.
|
|
344
|
+
#
|
|
345
|
+
# SAME-CHAPTER ONLY, ON PURPOSE — a `with:` whose source event or
|
|
346
|
+
# target command lives outside this chapter (an `across` policy
|
|
347
|
+
# reacting to another domain's event entirely) is silently left
|
|
348
|
+
# unchecked rather than refused: there is nothing here yet to
|
|
349
|
+
# check it against, and "unresolvable" is not the same claim as
|
|
350
|
+
# "wrong."
|
|
351
|
+
#
|
|
352
|
+
# A FOR_EACH POLICY'S SOURCE ISN'T THE EVENT AT ALL — a fan-out
|
|
353
|
+
# `with:`'s symbols read the QUERY ROW `for_each` answers
|
|
354
|
+
# (FreezeAccountsOnSuspension's own comment: "`account` is the
|
|
355
|
+
# key the fan-out merges for each row"), which this has no shape
|
|
356
|
+
# for; the SOURCE half is skipped for those, the TARGET half
|
|
357
|
+
# (does the dispatched command actually declare the field) still
|
|
358
|
+
# runs, since that half is true regardless of where the value
|
|
359
|
+
# came from.
|
|
360
|
+
def self.validate_with_projections!(policies, process_managers, aggregates)
|
|
361
|
+
lookup = command_lookup(aggregates)
|
|
362
|
+
heads = correlation_heads(process_managers)
|
|
363
|
+
|
|
364
|
+
policies.each do |policy|
|
|
365
|
+
next if policy.with_spec.to_a.empty?
|
|
366
|
+
|
|
367
|
+
source_event = policy.for_each.to_s.empty? ? policy.on_event : nil
|
|
368
|
+
check_with_spec!(policy.trigger_command, source_event, policy.with_spec, lookup,
|
|
369
|
+
"#{policy.name}'s trigger", aggregates, heads)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
process_managers.each do |pm|
|
|
373
|
+
pm.handlers.each do |handler|
|
|
374
|
+
handler.dispatches.each do |dispatch|
|
|
375
|
+
next if dispatch.with_spec.to_a.empty?
|
|
376
|
+
|
|
377
|
+
check_with_spec!(dispatch.command_name, handler.event_type, dispatch.with_spec, lookup,
|
|
378
|
+
"#{pm.name}'s dispatch #{dispatch.command_name}", aggregates, heads, pm: pm)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# `pm:` is present only for a process manager's own dispatch — a
|
|
385
|
+
# saga leg's source symbol resolves against the CURRENT triggering
|
|
386
|
+
# event first, same as a policy, but falls all the way back to the
|
|
387
|
+
# saga's own MEMORY when the current event does not carry it
|
|
388
|
+
# (`SagaInterpreter#dispatch_args`, its own last `else`) — and
|
|
389
|
+
# memory starts as the OPENING event's payload
|
|
390
|
+
# (`SagaInterpreter#instance = { ..., memory: event.payload }`,
|
|
391
|
+
# never updated after), never the leg's own. Settlement's own
|
|
392
|
+
# comment names exactly this: "the credit leg reads a destination
|
|
393
|
+
# no event carried" — `AccountDebited` never declares `:reference`,
|
|
394
|
+
# only `TransferRequested` (`pm.starts_on`) does, and that is
|
|
395
|
+
# where the value is genuinely still coming from.
|
|
396
|
+
def self.check_with_spec!(command_ref, event_name, with_spec, lookup, label, aggregates, correlation_heads, pm: nil)
|
|
397
|
+
target = lookup[command_ref]
|
|
398
|
+
source_shape = event_name && event_shape_for(event_name, aggregates)
|
|
399
|
+
memory_shape = pm && event_shape_for(pm.starts_on, aggregates)
|
|
400
|
+
correlation = pm && pm.correlates_by && pm.correlation_head
|
|
401
|
+
# A POLICY'S SOURCE ALSO CARRIES THE EMITTER'S OWN IDENTITY —
|
|
402
|
+
# `PolicyInterpreter#emitter_identity`, the runtime half of this.
|
|
403
|
+
# An entity command's event never declares its aggregate's
|
|
404
|
+
# identity (it arrives through `reference_to`, not an
|
|
405
|
+
# `attribute`), so before this a policy on `KnightCaptured` could
|
|
406
|
+
# not spell `with: { label: :label }` at all — "reads :label off
|
|
407
|
+
# KnightCaptured, which does not declare it" — and chess's
|
|
408
|
+
# AdvancePly grew optional, unread attributes just to survive a
|
|
409
|
+
# wholesale forward. Policies only: a saga leg's own source is
|
|
410
|
+
# `SagaInterpreter#dispatch_args`, which merges no such thing.
|
|
411
|
+
identity_sources = pm.nil? && event_name ? event_identity_heads_for(event_name, aggregates) : []
|
|
412
|
+
|
|
413
|
+
with_spec.each do |field, source|
|
|
414
|
+
raise Malformed, "#{label}'s with: names #{field.inspect}, which #{command_ref} does not declare" if target && !command_declares?(target, field, aggregates, correlation_heads)
|
|
415
|
+
|
|
416
|
+
next unless source.is_a?(::Symbol)
|
|
417
|
+
next if source == correlation
|
|
418
|
+
next if identity_sources.include?(source)
|
|
419
|
+
next unless source_shape || memory_shape
|
|
420
|
+
|
|
421
|
+
found = [source_shape, memory_shape].compact.any? { |shape| shape.any? { |name, *| name == source } }
|
|
422
|
+
next if found
|
|
423
|
+
|
|
424
|
+
raise Malformed, "#{label}'s with: reads :#{source} off #{event_name.inspect}, which does not declare it"
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# A command's OWN `reference_to` (bare, no `as:`) never lands in
|
|
429
|
+
# `attributes` — `CommandBuilder#reference_to`'s self-reference
|
|
430
|
+
# branch sets `command.references` instead (S2), and mints no new
|
|
431
|
+
# field at all. What addresses it is not one name but the SAME
|
|
432
|
+
# SET `CommandInterpreter::ArgumentGate#refuse_unknown_arguments`
|
|
433
|
+
# already accepts at dispatch time — `:id`, the owning aggregate's
|
|
434
|
+
# own `identity_heads` (real corpus proof — `Account.Debit`
|
|
435
|
+
# dispatched everywhere as `number: ...`, `Account`'s own
|
|
436
|
+
# `identified_by`), AND `Naming.reference_key(command.references)`
|
|
437
|
+
# (real corpus proof — `FreezeAccountsOnSuspension`'s `for_each`
|
|
438
|
+
# fan-out, whose own comment reads "`account` is the key the
|
|
439
|
+
# fan-out merges for each row it answers"). Both are simultaneously
|
|
440
|
+
# legal there, not context-dependent alternatives, so both are
|
|
441
|
+
# legal here : this mirrors that gate rather than re-deriving a
|
|
442
|
+
# narrower rule that would refuse one of two real, already-shipped
|
|
443
|
+
# dispatch conventions.
|
|
444
|
+
def self.command_declares?(command, field, aggregates, correlation_heads)
|
|
445
|
+
return true if command.attributes.any? { |a| a.name == field }
|
|
446
|
+
return true if field == :id
|
|
447
|
+
return true if correlation_heads.include?(field)
|
|
448
|
+
return false unless command.references
|
|
449
|
+
|
|
450
|
+
referenced = aggregates.find { |a| a.hecks_name == command.references }
|
|
451
|
+
return false unless referenced
|
|
452
|
+
|
|
453
|
+
referenced.identity_heads.include?(field) || Naming.reference_key(command.references) == field
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# THE FOURTH addressing key `ArgumentGate#refuse_unknown_arguments`
|
|
457
|
+
# accepts, alongside `:id`/`identity_heads`/`reference_key` — every
|
|
458
|
+
# saga in THIS domain's own `correlates_by` head, carried through
|
|
459
|
+
# every dispatch as pure passthrough (Settlement's own comment:
|
|
460
|
+
# "`reference:` carries the correlation forward... this is pure
|
|
461
|
+
# passthrough, not an addressing key"). A command declaring none of
|
|
462
|
+
# its attributes named this is not a gap; the correlation key rides
|
|
463
|
+
# through commands that never read it, same as it does at runtime.
|
|
464
|
+
def self.correlation_heads(process_managers)
|
|
465
|
+
process_managers.filter_map { |pm| pm.correlates_by && pm.correlation_head }
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Every command this chapter declares, an aggregate's own AND
|
|
469
|
+
# every entity nested inside one, paired with a name for what
|
|
470
|
+
# declares it — shared by `validate_event_shapes!` and
|
|
471
|
+
# `validate_with_projections!`'s own command lookup, the same
|
|
472
|
+
# reach `HecksagonBuilder#commands_in` needs one level up (S8).
|
|
473
|
+
def self.each_command(aggregates)
|
|
474
|
+
return enum_for(:each_command, aggregates) unless block_given?
|
|
475
|
+
|
|
476
|
+
aggregates.each do |aggregate|
|
|
477
|
+
aggregate.commands.each { |command| yield aggregate.hecks_name, command }
|
|
478
|
+
aggregate.entities.each do |entity|
|
|
479
|
+
entity.commands.each { |command| yield "#{aggregate.hecks_name}.#{entity.hecks_name}", command }
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# NOT MEMOISED — this used to be `@event_emitters ||=` on the
|
|
485
|
+
# builder instance, which is safe for a one-file chapter but
|
|
486
|
+
# wrong for one split across several: the FIRST file's build()
|
|
487
|
+
# call would compute and cache it from whatever `@aggregates`
|
|
488
|
+
# held at that moment, and every later file's own validation
|
|
489
|
+
# would keep reading that same stale snapshot, silently missing
|
|
490
|
+
# any command a later file adds. Recomputed fresh every call
|
|
491
|
+
# instead — this walks the whole chapter once per `#build`, not
|
|
492
|
+
# a hot path worth memoising at that cost.
|
|
493
|
+
def self.event_emitters(aggregates)
|
|
494
|
+
each_command(aggregates).each_with_object(Hash.new { |h, k| h[k] = [] }) do |(owner, command), index|
|
|
495
|
+
command.emits.each { |event_name| index[event_name] << [owner, command] }
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# STRUCTURAL, NOT NOMINAL. Two commands on two different
|
|
500
|
+
# aggregates that both `emits "SameEvent"` are free to type a
|
|
501
|
+
# field through two DIFFERENT, locally-scoped wrapper value
|
|
502
|
+
# objects (e.g. one aggregate's own `value: SomeText` vs
|
|
503
|
+
# another's `value: OtherText`, exactly the per-aggregate "own
|
|
504
|
+
# text VO" convention every aggregate in this grammar already
|
|
505
|
+
# follows for everything from `RuleText` to `FieldRef`) without
|
|
506
|
+
# actually disagreeing about the event's shape — comparing
|
|
507
|
+
# `a.type` by NAME would flag that as a violation for no real
|
|
508
|
+
# reason: an event is one fact, and two isomorphic wrapper types
|
|
509
|
+
# tell an identical one. So a value-object type is unwrapped to
|
|
510
|
+
# its OWN attribute shape (recursively — a wrapper could itself
|
|
511
|
+
# wrap another) before comparing, and only a primitive type
|
|
512
|
+
# (nothing left to unwrap) or two VOs that truly differ once
|
|
513
|
+
# unwrapped still counts as a real mismatch. `owner` carries the
|
|
514
|
+
# type's `value_object` lookup — a command's own attributes only
|
|
515
|
+
# know their type's NAME, never the aggregate that declared it,
|
|
516
|
+
# and two sibling aggregates in one chapter each keep a
|
|
517
|
+
# same-named VO private to themselves, so the unwrap has to ask
|
|
518
|
+
# the SAME aggregate the field's own command belongs to, never a
|
|
519
|
+
# neighbor's.
|
|
520
|
+
def self.event_shape(command, owner)
|
|
521
|
+
command.attributes.map { |a| [a.name, unwrap_shape(owner, a.type.to_s), a.list?, a.optional?] }.sort
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def self.unwrap_shape(owner, type_name, seen = [])
|
|
525
|
+
return type_name if owner.nil? # owner couldn't be resolved -- compare by name, same as before this unwrap existed
|
|
526
|
+
return type_name if Attribute::PRIMITIVES.include?(type_name)
|
|
527
|
+
return type_name if seen.include?(type_name) # a self-referential VO bottoms out on its own name, not an infinite unwrap
|
|
528
|
+
|
|
529
|
+
shape = owner.value_object(type_name)
|
|
530
|
+
return type_name unless shape # not this owner's own VO (a reference type, say) -- nothing further to unwrap
|
|
531
|
+
|
|
532
|
+
shape.attributes.map { |a| [a.name, unwrap_shape(owner, a.type.to_s, seen + [type_name]), a.list?, a.optional?] }.sort
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# `owner` (from `each_command`) is a plain STRING — the aggregate's
|
|
536
|
+
# `hecks_name` alone, or `"Aggregate.Entity"` for an entity's own
|
|
537
|
+
# command. Either way the VALUE OBJECTS a command's fields can be
|
|
538
|
+
# typed with are the AGGREGATE's own (`Entity` carries no
|
|
539
|
+
# `value_object` lookup of its own — the whole rest of this file
|
|
540
|
+
# already resolves hop/type lookups only at the aggregate level,
|
|
541
|
+
# e.g. `validate_hop_tail!`'s `target.value_object(type)`), so only
|
|
542
|
+
# the first segment ever matters here.
|
|
543
|
+
def self.owner_aggregate(owner, aggregates)
|
|
544
|
+
aggregates.find { |a| a.hecks_name == owner.to_s.split(".").first }
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def self.event_shape_for(event_name, aggregates)
|
|
548
|
+
pairs = event_emitters(aggregates).fetch(event_name.to_s, [])
|
|
549
|
+
return nil if pairs.empty?
|
|
550
|
+
|
|
551
|
+
owner_name, command = pairs.first
|
|
552
|
+
event_shape(command, owner_aggregate(owner_name, aggregates))
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# The identity heads of the aggregate that emits `event_name` — an
|
|
556
|
+
# entity's event is stamped with its OWNING aggregate's identity
|
|
557
|
+
# (`Event#id` is the parent's), so an owner spelled "Game.Knight"
|
|
558
|
+
# answers Game's heads.
|
|
559
|
+
def self.event_identity_heads_for(event_name, aggregates)
|
|
560
|
+
pairs = event_emitters(aggregates).fetch(event_name.to_s, [])
|
|
561
|
+
return [] if pairs.empty?
|
|
562
|
+
|
|
563
|
+
owner_name, = pairs.first
|
|
564
|
+
aggregate = owner_aggregate(owner_name, aggregates)
|
|
565
|
+
return [] unless aggregate
|
|
566
|
+
|
|
567
|
+
heads = aggregate.identity_heads.map(&:to_sym)
|
|
568
|
+
# AN ENTITY'S EVENT ALSO CARRIES THE PIECE'S OWN IDENTITY — the
|
|
569
|
+
# args a piece was addressed by are the args its event announces
|
|
570
|
+
# (`Emission#emit`: `payload: args`), so `id`-shaped heads are
|
|
571
|
+
# genuinely there at runtime even though no `attribute` line on
|
|
572
|
+
# the entity command declares them.
|
|
573
|
+
entity_names = owner_name.to_s.split(".").drop(1)
|
|
574
|
+
entity = entity_names.reduce(aggregate) { |owner, name| owner&.entities&.find { |e| e.hecks_name == name } }
|
|
575
|
+
heads + (entity ? entity.identity_heads.map(&:to_sym) : [])
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def self.command_lookup(aggregates)
|
|
579
|
+
each_command(aggregates).each_with_object({}) do |(owner, command), index|
|
|
580
|
+
index["#{owner}.#{command.hecks_name}"] = command
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# A REFERENCE RING IS NOT A MODELLING CHOICE, IT IS A MISSING ONE
|
|
585
|
+
# — a DDD aggregate is a consistency boundary precisely because
|
|
586
|
+
# something outside it can only ever point IN, by id, never the
|
|
587
|
+
# other way. A caller must be able to reason about one aggregate
|
|
588
|
+
# alone ; a ring back to where it started means no aggregate in
|
|
589
|
+
# it is a boundary anyone can reason about without the rest of
|
|
590
|
+
# the ring, and the whole ring is really one aggregate wearing
|
|
591
|
+
# several names.
|
|
592
|
+
#
|
|
593
|
+
# Checked at the bluebook level, not inside `AggregateBuilder`
|
|
594
|
+
# itself, because seeing a cycle needs every end declared — an
|
|
595
|
+
# aggregate finishes building long before it can know whether
|
|
596
|
+
# some later aggregate in the same file points back at it.
|
|
597
|
+
#
|
|
598
|
+
# ACYCLIC WITHIN A CHAPTER (ADR 0025, "References") — widened
|
|
599
|
+
# from the direct pair (A -> B -> A) this used to catch alone to
|
|
600
|
+
# any ring, however long (A -> B -> C -> A), the same DFS
|
|
601
|
+
# coloring a reference graph needs for any cycle. A cross-chapter
|
|
602
|
+
# reference is UNREACHABLE here rather than unchecked:
|
|
603
|
+
# `Reference#resolve` is scoped to its own chapter by
|
|
604
|
+
# construction, so a target this chapter never declares is a
|
|
605
|
+
# dangling name, not an edge — `edges.key?` below is what keeps
|
|
606
|
+
# the walk from ever leaving this chapter's own aggregates.
|
|
607
|
+
# Self-reference stays legal (`parent.parent.name` for a
|
|
608
|
+
# hierarchy is real and safe) — excluded the same way the
|
|
609
|
+
# direct-pair check already excluded it.
|
|
610
|
+
def self.validate_no_bidirectional_references!(aggregates)
|
|
611
|
+
edges = aggregates.each_with_object({}) do |aggregate, index|
|
|
612
|
+
index[aggregate.hecks_name] = aggregate.reference_targets.uniq.reject { |target| target == aggregate.hecks_name }
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
cycle = find_reference_cycle(edges)
|
|
616
|
+
return unless cycle
|
|
617
|
+
|
|
618
|
+
ring = "#{cycle.join(' -> ')} -> #{cycle.first}"
|
|
619
|
+
raise Malformed,
|
|
620
|
+
"reference cycle: #{ring} — an aggregate points at another by id, and a " \
|
|
621
|
+
"ring back to where it started means no aggregate in it is a boundary " \
|
|
622
|
+
"anyone can reason about alone ; break the ring, or let one side be found " \
|
|
623
|
+
"through a query instead of a reference pointing back"
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Plain DFS with a visiting/done coloring, over the reference
|
|
627
|
+
# graph THIS chapter's own aggregates declare. Returns the ring
|
|
628
|
+
# itself (in the order it closes), or nil.
|
|
629
|
+
def self.find_reference_cycle(edges)
|
|
630
|
+
state = {}
|
|
631
|
+
|
|
632
|
+
edges.each_key do |start|
|
|
633
|
+
cycle = reference_cycle_from(start, edges, state, [])
|
|
634
|
+
return cycle if cycle
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
nil
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def self.reference_cycle_from(node, edges, state, path)
|
|
641
|
+
return nil if state[node] == :done
|
|
642
|
+
return path[path.index(node)..] if state[node] == :visiting
|
|
643
|
+
|
|
644
|
+
state[node] = :visiting
|
|
645
|
+
path.push(node)
|
|
646
|
+
|
|
647
|
+
edges[node].each do |target|
|
|
648
|
+
next unless edges.key?(target) # a name this chapter never declares is dangling, not an edge
|
|
649
|
+
|
|
650
|
+
found = reference_cycle_from(target, edges, state, path)
|
|
651
|
+
return found if found
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
path.pop
|
|
655
|
+
state[node] = :done
|
|
656
|
+
nil
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# THE OTHER HALF OF A HOP — AggregateBuilder#seal_query_field
|
|
660
|
+
# recognised the HEAD of a dotted where-field that names one of
|
|
661
|
+
# its own references and deferred it here, unable to check
|
|
662
|
+
# further: it cannot yet resolve what the reference points AT.
|
|
663
|
+
# This runs once every aggregate exists in one chapter, so it
|
|
664
|
+
# can.
|
|
665
|
+
#
|
|
666
|
+
# Only WHERE clauses ever reach here — a hop on ORDER BY is
|
|
667
|
+
# refused outright, immediately, back in seal_query_field
|
|
668
|
+
# itself (that answer never needed the target's shape).
|
|
669
|
+
#
|
|
670
|
+
# AN ENTITY'S OWN QUERIES DID reach `EntityBuilder#reference_to`
|
|
671
|
+
# (added after this comment first claimed otherwise — S9, ADR
|
|
672
|
+
# 0025) without ever reaching HERE: tier-1 sealing
|
|
673
|
+
# (`AggregateBuilder#query_surfaces`) already recognises a hop
|
|
674
|
+
# on an entity's own field and DEFERS it exactly like an
|
|
675
|
+
# aggregate's, but nothing ever walked entity queries at tier 2
|
|
676
|
+
# to check the deferral — a bad hop, or even a well-formed one,
|
|
677
|
+
# built silently and then matched nothing at runtime
|
|
678
|
+
# (`QueryInterpreter#entity_rows` reads an element's fields by
|
|
679
|
+
# literal hash key, never follows a reference). Refused outright
|
|
680
|
+
# here instead of taught to follow the hop for real: no corpus
|
|
681
|
+
# member needs an entity query to cross a reference yet, and a
|
|
682
|
+
# named refusal beats a runtime that resolves nothing while
|
|
683
|
+
# looking like it might.
|
|
684
|
+
def self.validate_query_hops!(bluebook)
|
|
685
|
+
bluebook.aggregates.each do |aggregate|
|
|
686
|
+
aggregate.queries.each do |query|
|
|
687
|
+
query.wheres.each do |clause|
|
|
688
|
+
next unless QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes)
|
|
689
|
+
|
|
690
|
+
validate_hop_clause!(aggregate, query, clause)
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
aggregate.entities.each { |entity| refuse_entity_query_hops!(aggregate, entity) }
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# The chapter-wide half of AggregateBuilder's local query-argument
|
|
699
|
+
# inference. A hop cannot resolve while its aggregate is still being
|
|
700
|
+
# built; here every Reference has an owner and target, so a symbolic
|
|
701
|
+
# comparison can inherit the type of the scalar it compares without a
|
|
702
|
+
# duplicate query-local declaration.
|
|
703
|
+
def self.infer_hop_query_arguments!(bluebook)
|
|
704
|
+
bluebook.aggregates.each do |aggregate|
|
|
705
|
+
aggregate.queries.each do |query|
|
|
706
|
+
query.wheres.each do |clause|
|
|
707
|
+
name = clause.value
|
|
708
|
+
next unless name.is_a?(Symbol)
|
|
709
|
+
next if query.attribute(name)
|
|
710
|
+
next unless QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes)
|
|
711
|
+
|
|
712
|
+
plan = QuerySpecification::HopPath.plan(clause.field, aggregate.attributes)
|
|
713
|
+
next if plan.refusal || plan.hops.empty?
|
|
714
|
+
|
|
715
|
+
target = plan.hops.last.target
|
|
716
|
+
head, *nested = plan.tail.to_s.split(".")
|
|
717
|
+
leaf = if nested.empty? && target.lifecycle&.field.to_s == head
|
|
718
|
+
Attribute.new(name: name, type: String)
|
|
719
|
+
else
|
|
720
|
+
root = target.attributes.find { |candidate| candidate.name.to_s == head }
|
|
721
|
+
found = root && QuerySpecification::FieldPath.leaf_attribute(root, nested) do |type|
|
|
722
|
+
target.value_object(type)
|
|
723
|
+
end
|
|
724
|
+
found && Attribute.new(name: name, type: found.type, list: found.list?)
|
|
725
|
+
end
|
|
726
|
+
next unless leaf
|
|
727
|
+
|
|
728
|
+
query.attributes << leaf
|
|
729
|
+
end
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
def self.refuse_entity_query_hops!(aggregate, entity)
|
|
735
|
+
entity.queries.each do |query|
|
|
736
|
+
query.wheres.each do |clause|
|
|
737
|
+
next unless QuerySpecification::HopPath.hop_head?(clause.field, entity.attributes)
|
|
738
|
+
|
|
739
|
+
raise Malformed,
|
|
740
|
+
"#{aggregate.hecks_name}::#{entity.hecks_name}.#{query.hecks_name} asks about " \
|
|
741
|
+
"#{clause.field}, which hops through #{entity.hecks_name}'s own reference — " \
|
|
742
|
+
"an entity query does not follow a hop the way an aggregate's own does; ask " \
|
|
743
|
+
"through the aggregate's own query instead, or open the target directly"
|
|
744
|
+
end
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def self.validate_hop_clause!(aggregate, query, clause)
|
|
749
|
+
plan = QuerySpecification::HopPath.plan(clause.field, aggregate.attributes)
|
|
750
|
+
|
|
751
|
+
case plan.refusal
|
|
752
|
+
when :unresolvable
|
|
753
|
+
# HopPath.plan pushes even an unresolved hop onto `hops`
|
|
754
|
+
# before reporting this, specifically so `target_name` —
|
|
755
|
+
# real, known at declaration, independent of whether
|
|
756
|
+
# `resolve` succeeded — is always here to name.
|
|
757
|
+
raise Malformed,
|
|
758
|
+
"#{aggregate.hecks_name}.#{query.hecks_name} asks about #{clause.field}, " \
|
|
759
|
+
"which hops to #{plan.hops.last.target_name}, which this chapter never " \
|
|
760
|
+
"declares — a hop into an aggregate this chapter cannot see resolves to " \
|
|
761
|
+
"nothing, and a where that resolves to nothing matches nothing and " \
|
|
762
|
+
"refuses nothing"
|
|
763
|
+
when :too_deep
|
|
764
|
+
raise Malformed,
|
|
765
|
+
"#{aggregate.hecks_name}.#{query.hecks_name} asks about #{clause.field}, " \
|
|
766
|
+
"whose hop chain reaches #{QuerySpecification::HopPath::MAX_HOPS} " \
|
|
767
|
+
"references deep without landing — a chain this long is refused as a " \
|
|
768
|
+
"likely mistake, not a structural limit"
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
target = plan.hops.last.target
|
|
772
|
+
validate_hop_tail!(aggregate, query, clause, target, plan.tail)
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
# The same three-way answer seal_query_field gives for its OWN
|
|
776
|
+
# aggregate's fields — landing on a real scalar (fine), landing
|
|
777
|
+
# on a value object (refused by name), or naming nothing at all
|
|
778
|
+
# (refused by name) — asked instead of the hop's TARGET aggregate,
|
|
779
|
+
# since that is whose shape the tail actually has to answer for.
|
|
780
|
+
def self.validate_hop_tail!(aggregate, query, clause, target, tail)
|
|
781
|
+
name, *nested = tail.to_s.split(".")
|
|
782
|
+
attribute = target.attributes.find { |candidate| candidate.name.to_s == name }
|
|
783
|
+
return validate_hop_comparator!(aggregate, query, clause, target, attribute, nested) if
|
|
784
|
+
nested.empty? && (attribute || target.lifecycle&.field.to_s == name)
|
|
785
|
+
return validate_hop_comparator!(aggregate, query, clause, target, attribute, nested) if
|
|
786
|
+
nested.any? && attribute &&
|
|
787
|
+
QuerySpecification::FieldPath.scalar_leaf?(attribute, nested) { |type| target.value_object(type) }
|
|
788
|
+
|
|
789
|
+
if nested.any? && attribute &&
|
|
790
|
+
!QuerySpecification::FieldPath.leaf_attribute(attribute, nested) { |type| target.value_object(type) }.nil?
|
|
791
|
+
raise Malformed,
|
|
792
|
+
"#{aggregate.hecks_name}.#{query.hecks_name} asks about #{clause.field}, " \
|
|
793
|
+
"which hops to #{target.hecks_name} and then asks about #{tail}, which " \
|
|
794
|
+
"lands on a value object, not a scalar — a dotted query path ends on a " \
|
|
795
|
+
"scalar member, or the engines answer it differently"
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
raise Malformed,
|
|
799
|
+
"#{aggregate.hecks_name}.#{query.hecks_name} asks about #{clause.field}, " \
|
|
800
|
+
"which hops to #{target.hecks_name} and then asks about #{tail}, which " \
|
|
801
|
+
"#{target.hecks_name} never declares — a query over a field that does " \
|
|
802
|
+
"not exist matches nothing and refuses nothing"
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
# A WHERE hop with an ordered comparator is legitimate ("client
|
|
806
|
+
# whose balance > 500") — AggregateBuilder#seal_ordered_comparator
|
|
807
|
+
# already deferred this exact check for the same reason every
|
|
808
|
+
# other hop check is deferred, and this is where it gets asked,
|
|
809
|
+
# against the hop's TARGET instead of the querying aggregate.
|
|
810
|
+
def self.validate_hop_comparator!(aggregate, query, clause, target, attribute, nested)
|
|
811
|
+
return unless AggregateBuilder::ORDERED_COMPARATORS.include?(clause.op.to_s.to_sym)
|
|
812
|
+
return if attribute &&
|
|
813
|
+
QuerySpecification::FieldPath.numeric?(attribute, nested) { |type| target.value_object(type) }
|
|
814
|
+
|
|
815
|
+
held = attribute ? "holds no number" : "is the lifecycle field, which holds text"
|
|
816
|
+
raise Malformed,
|
|
817
|
+
"#{aggregate.hecks_name}.#{query.hecks_name} compares #{clause.field} with " \
|
|
818
|
+
"#{clause.op} after hopping to #{target.hecks_name}, but the field it lands " \
|
|
819
|
+
"on #{held} — an ordered comparison needs a numeric field, and over " \
|
|
820
|
+
"anything else the adapters answer differently or not at all"
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
# THE TARGET HALF of `projects` validation (S12, ADR 0025) —
|
|
824
|
+
# `AggregateBuilder#seal_projected_fields` already checked the
|
|
825
|
+
# LOCAL half at declare time (the reference names a real
|
|
826
|
+
# `reference_to` on THIS aggregate); this checks the reference
|
|
827
|
+
# actually resolves to a real aggregate in this chapter, and
|
|
828
|
+
# that aggregate really declares `remote_field` as a scalar.
|
|
829
|
+
#
|
|
830
|
+
# Reuses `QuerySpecification::HopPath` rather than re-deriving
|
|
831
|
+
# hop resolution a second way — `"reference/remote_field"` is
|
|
832
|
+
# the same single-hop shape a query's own `/`-spelled hop
|
|
833
|
+
# resolves, even though `projects`'s own DSL spelling is dotted
|
|
834
|
+
# (`from: :"customer.status"`): two constructs, two spellings,
|
|
835
|
+
# one resolution primitive. A single hop can never reach
|
|
836
|
+
# HopPath::MAX_HOPS, so :too_deep is structurally unreachable
|
|
837
|
+
# here and is not special-cased.
|
|
838
|
+
def self.validate_projected_fields!(bluebook)
|
|
839
|
+
bluebook.aggregates.each do |aggregate|
|
|
840
|
+
aggregate.projected_fields.each { |field| validate_projected_field!(aggregate, field) }
|
|
841
|
+
end
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
def self.validate_projected_field!(aggregate, field)
|
|
845
|
+
plan = QuerySpecification::HopPath.plan("#{field.reference}/#{field.remote_field}", aggregate.attributes)
|
|
846
|
+
|
|
847
|
+
if plan.refusal == :unresolvable
|
|
848
|
+
raise Malformed,
|
|
849
|
+
"#{aggregate.hecks_name}.projects :#{field.name} reads through :#{field.reference}, " \
|
|
850
|
+
"which hops to #{plan.hops.last.target_name}, which this chapter never declares — " \
|
|
851
|
+
"a projection through an aggregate this chapter cannot see resolves to nothing"
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
target = plan.hops.last.target
|
|
855
|
+
remote_attribute = target.attributes.find { |candidate| candidate.name.to_s == plan.tail }
|
|
856
|
+
|
|
857
|
+
# THE WORKED EXAMPLE ITSELF (ADR 0025) reads through a
|
|
858
|
+
# LIFECYCLE field — banking's Customer.status is `lifecycle
|
|
859
|
+
# :status`, never a plain `attribute` — the same fallback
|
|
860
|
+
# validate_hop_tail! already gives a query's own hop tail. A
|
|
861
|
+
# lifecycle field is always a plain string by construction ;
|
|
862
|
+
# nothing further to check once it matches by name.
|
|
863
|
+
return if remote_attribute.nil? && target.lifecycle&.field.to_s == plan.tail
|
|
864
|
+
|
|
865
|
+
unless remote_attribute
|
|
866
|
+
raise Malformed,
|
|
867
|
+
"#{aggregate.hecks_name}.projects :#{field.name} reads #{target.hecks_name}'s own " \
|
|
868
|
+
"#{plan.tail.inspect}, which #{target.hecks_name} never declares"
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
return if projectable_scalar?(target, remote_attribute)
|
|
872
|
+
|
|
873
|
+
raise Malformed,
|
|
874
|
+
"#{aggregate.hecks_name}.projects :#{field.name} reads #{target.hecks_name}'s own " \
|
|
875
|
+
"#{plan.tail.inspect}, which is not a scalar — a projected field copies a single " \
|
|
876
|
+
"value, never a reference, a value object, or a list"
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def self.projectable_scalar?(target, attribute)
|
|
880
|
+
!attribute.list? && !attribute.reference? && target.value_object(attribute.type).nil?
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
# `correlates_by` NAMES A SCALAR, NOW CHECKED RATHER THAN TRUSTED.
|
|
884
|
+
#
|
|
885
|
+
# ProcessManagerBuilder#validate! already refuses a bare, undotted
|
|
886
|
+
# spelling — a SYNTACTIC guarantee that the declaration cannot leave
|
|
887
|
+
# the question open. It cannot go further: a process manager is built
|
|
888
|
+
# in isolation, before this chapter's aggregates exist to check
|
|
889
|
+
# against. Here, with the whole document assembled, the dotted path
|
|
890
|
+
# is walked for real — against whichever command actually emits an
|
|
891
|
+
# event this process manager reacts to — so a path that still lands
|
|
892
|
+
# on a value object is refused before the runtime ever has to decide
|
|
893
|
+
# what a non-scalar correlation key even means: a saga keys off this
|
|
894
|
+
# value directly, and a value object carries no guaranteed-stable
|
|
895
|
+
# identity to key on the way a scalar does.
|
|
896
|
+
#
|
|
897
|
+
# A command that does not declare the path's first segment at all is
|
|
898
|
+
# silently skipped, not refused — correlation has two other fallback
|
|
899
|
+
# tiers below the payload dig (a correlation stamp, then the emitting
|
|
900
|
+
# aggregate's own reference key; saga_interpreter/correlation.rb), so
|
|
901
|
+
# an absent field is not this check's business. Only a field that
|
|
902
|
+
# resolves, and resolves to something other than a scalar, is.
|
|
903
|
+
def self.validate_correlation_keys!(process_managers, aggregates)
|
|
904
|
+
process_managers.each do |pm|
|
|
905
|
+
next unless pm.correlates_by
|
|
906
|
+
|
|
907
|
+
reason = correlation_key_violation(pm, aggregates)
|
|
908
|
+
next unless reason
|
|
909
|
+
|
|
910
|
+
raise ProcessManagerBuilder::InvalidProcessManager,
|
|
911
|
+
"#{pm.name} correlates_by #{pm.correlates_by.inspect}, but #{reason}"
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
def self.correlation_key_violation(pm, aggregates)
|
|
916
|
+
head, *rest = pm.correlates_by.to_s.split(".")
|
|
917
|
+
events = reacted_events(pm)
|
|
918
|
+
|
|
919
|
+
emitting_commands(events, aggregates).each do |owner, command|
|
|
920
|
+
attribute = command.attributes.find { |a| a.name == head.to_sym }
|
|
921
|
+
next unless attribute
|
|
922
|
+
|
|
923
|
+
reason = list_or_scalar_violation(owner, attribute, rest)
|
|
924
|
+
return reason if reason
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
nil
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
def self.reacted_events(pm)
|
|
931
|
+
([pm.starts_on, pm.ends_on] + pm.handlers.map(&:event_type))
|
|
932
|
+
.compact
|
|
933
|
+
.reject { |event| event == ProcessManager::REFUSED }
|
|
934
|
+
.map { |event| event.to_s.split("::").last }
|
|
935
|
+
.uniq
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
def self.emitting_commands(events, aggregates)
|
|
939
|
+
aggregates.flat_map do |aggregate|
|
|
940
|
+
commands = aggregate.commands + aggregate.entities.flat_map(&:commands)
|
|
941
|
+
commands.select { |command| (command.emits.map(&:to_s) & events).any? }
|
|
942
|
+
.map { |command| [aggregate, command] }
|
|
943
|
+
end
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
def self.list_or_scalar_violation(owner, attribute, segments)
|
|
947
|
+
return "#{attribute.name} is a list — a correlation key must name one instance's own field, " \
|
|
948
|
+
"not a whole collection" if attribute.list?
|
|
949
|
+
|
|
950
|
+
walk_scalar(owner, attribute.type.to_s, segments)
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
# Walks the remaining dotted segments through nested value objects.
|
|
954
|
+
# `type_name` starts as the head attribute's own declared type ; each
|
|
955
|
+
# step either bottoms out at a real scalar (nil — no violation) or
|
|
956
|
+
# names why it cannot: still a value object with no more path left,
|
|
957
|
+
# a value object this domain never declared, a field that value
|
|
958
|
+
# object does not have, or a segment left over after already
|
|
959
|
+
# reaching a scalar.
|
|
960
|
+
def self.walk_scalar(owner, type_name, segments)
|
|
961
|
+
if segments.empty?
|
|
962
|
+
return nil if Attribute::PRIMITIVES.include?(type_name)
|
|
963
|
+
|
|
964
|
+
return "#{type_name} is a value object, not a scalar — name one of its own fields, " \
|
|
965
|
+
"e.g. #{type_name.downcase}.value"
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
return "#{type_name} is already a scalar — #{segments.join('.')} has nothing left to reach" if Attribute::PRIMITIVES.include?(type_name)
|
|
969
|
+
|
|
970
|
+
shape = owner.value_object(type_name)
|
|
971
|
+
return "#{type_name} is not a value object this domain declares" unless shape
|
|
972
|
+
|
|
973
|
+
segment, *rest = segments
|
|
974
|
+
attribute = shape.attributes.find { |a| a.name == segment.to_sym }
|
|
975
|
+
return "#{type_name} has no field #{segment.inspect}" unless attribute
|
|
976
|
+
return "#{type_name}.#{segment} is a list — a correlation key must name one instance's own field, " \
|
|
977
|
+
"not a whole collection" if attribute.list?
|
|
978
|
+
|
|
979
|
+
walk_scalar(owner, attribute.type.to_s, rest)
|
|
980
|
+
end
|
|
981
|
+
|
|
982
|
+
# A CHAPTER MAY BE DECLARED IN SEVERAL FILES, meant to merge into ONE
|
|
983
|
+
# domain — `lib/hecks/language/bluebook/*.bluebook` all open
|
|
984
|
+
# `Hecks.bluebook "Bluebook" do ... end`. Each `Hecks.bluebook` call used to
|
|
985
|
+
# mint a fresh builder, so a second file with the same chapter name
|
|
986
|
+
# silently replaced the first's aggregates instead of adding to them.
|
|
987
|
+
#
|
|
988
|
+
# The registry now holds the builder OPEN across calls : the first file
|
|
989
|
+
# for a name creates it, every later file for the same name reuses the
|
|
990
|
+
# same instance, so `@aggregates`/`@read_models` accumulate. `#build` is
|
|
991
|
+
# safe to call once per file on the same builder — it constructs a fresh
|
|
992
|
+
# `Bluebook` from whatever is currently held and re-`Namespace.install`s
|
|
993
|
+
# over the previous one, so the LAST file's call leaves every aggregate
|
|
994
|
+
# seen so far reachable, and each call's IR is a strict superset of the
|
|
995
|
+
# one before. `Registry#add_bluebook` still simply stores by name — with
|
|
996
|
+
# this in place, "last write wins" is the cumulative, correct write.
|
|
997
|
+
def self.build(name, version: nil, &block)
|
|
998
|
+
registry = Hecks.current_registry
|
|
999
|
+
builder = registry ? registry.bluebook_builder(name) { new(name, version: version) } : new(name, version: version)
|
|
1000
|
+
builder.__send__(:adopt_version, version)
|
|
1001
|
+
# A bare constant in a bluebook — `attribute :name, PizzaName` — is a NAME,
|
|
1002
|
+
# not a reference to something Ruby has heard of. `const_missing` hands
|
|
1003
|
+
# over a `ConstShim::ScopedConstant` (S0b, const_shim.rb's own comment),
|
|
1004
|
+
# and that is still the whole answer for a bare name: `Attribute` spells
|
|
1005
|
+
# it with `to_s`, so the `TypeName` wrapper this used to build existed
|
|
1006
|
+
# only long enough to be stringified. The concept still has a home — the
|
|
1007
|
+
# language declares `value_object "TypeName"` — it just needed no Ruby
|
|
1008
|
+
# class of its own. A Module rather than a Symbol is what also lets
|
|
1009
|
+
# `Account::Debit`/`admits: Account::LedgerDirection` answer their OWN
|
|
1010
|
+
# `::` — a plain Symbol cannot.
|
|
1011
|
+
resolver = ->(const) { ConstShim::ScopedConstant.for(const) }
|
|
1012
|
+
ConstShim.with(resolver) { builder.instance_eval(&block) } if block
|
|
1013
|
+
builder.build
|
|
1014
|
+
end
|
|
1015
|
+
end
|
|
1016
|
+
end
|
|
1017
|
+
end
|
|
1018
|
+
end
|