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,906 @@
|
|
|
1
|
+
require_relative "word_gate"
|
|
2
|
+
module Hecks
|
|
3
|
+
module Bluebook
|
|
4
|
+
module DSL
|
|
5
|
+
class AggregateBuilder
|
|
6
|
+
GRAMMAR_CONTEXT = "Aggregate"
|
|
7
|
+
|
|
8
|
+
include AttributeCollector
|
|
9
|
+
include IdentityDeclaration
|
|
10
|
+
include RuleReference
|
|
11
|
+
include WordGate
|
|
12
|
+
|
|
13
|
+
def initialize(name, chapter_named_givens: {}, chapter_pending_givens: [])
|
|
14
|
+
@name = name
|
|
15
|
+
@value_objects = []
|
|
16
|
+
@commands = []
|
|
17
|
+
@invariants = []
|
|
18
|
+
@named_givens = {}
|
|
19
|
+
@projected_fields = []
|
|
20
|
+
@identity_paths = []
|
|
21
|
+
@entities = []
|
|
22
|
+
@queries = []
|
|
23
|
+
@policies = []
|
|
24
|
+
@reference_targets = []
|
|
25
|
+
# THE ROOT of the cross-entity given pool — see `#entity`'s own
|
|
26
|
+
# comment. ONE hash for the whole aggregate, threaded unchanged
|
|
27
|
+
# into every piece nested under it, however deep.
|
|
28
|
+
@entity_named_givens = {}
|
|
29
|
+
# ONE LEVEL WIDER STILL — the CHAPTER's own pool, threaded in
|
|
30
|
+
# from `BluebookBuilder#aggregate`, shared with every OTHER
|
|
31
|
+
# aggregate the same chapter builds. See `#given`'s own
|
|
32
|
+
# comment for what this closes.
|
|
33
|
+
@chapter_named_givens = chapter_named_givens
|
|
34
|
+
# A CHAPTER MAY BE SPLIT ACROSS FILES — threaded in the SAME
|
|
35
|
+
# way as `@chapter_named_givens`, one Array shared chapter-wide.
|
|
36
|
+
# See `#pending_chapter_given`'s own comment for what queues
|
|
37
|
+
# here and `BluebookBuilder#resolve_pending_chapter_givens!`
|
|
38
|
+
# for where it drains.
|
|
39
|
+
@chapter_pending_givens = chapter_pending_givens
|
|
40
|
+
# DEFERRED CONSTRUCTION — `entity`/`command`/`query` push a
|
|
41
|
+
# pending descriptor here instead of building immediately; see
|
|
42
|
+
# `#drain_pending!`'s own comment for why.
|
|
43
|
+
@pending_entities = []
|
|
44
|
+
@pending_commands = []
|
|
45
|
+
@pending_queries = []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def description(value)
|
|
49
|
+
# moved to the language: Description invariant, on Root.Declare
|
|
50
|
+
|
|
51
|
+
@description = value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# ORIGIN, not runtime identity — a concept adopted from a canonical
|
|
55
|
+
# source (§28) names where it came from without that fact ever
|
|
56
|
+
# touching `hecks_fqn`/dispatch. Captured raw, the same way
|
|
57
|
+
# `attribute ..., default: { value: "small" }` captures a literal
|
|
58
|
+
# Hash untouched — no re-parsing, no structure imposed beyond
|
|
59
|
+
# "whatever the author wrote."
|
|
60
|
+
# RENAMED FROM `provenance`/`projects`/`lifecycle`/`entity`/
|
|
61
|
+
# `query`/`policy`/`command` (all below) — item #13's full
|
|
62
|
+
# metaprogrammed dispatch (slice 4c). All bootstrap-reachable
|
|
63
|
+
# (used throughout the core/attached chapters), all in
|
|
64
|
+
# GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.
|
|
65
|
+
def provenance_impl(from:)
|
|
66
|
+
@provenance = from
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# `optional:` — matching `CommandBuilder#reference_to`'s own
|
|
70
|
+
# signature, which already had it; this one never forwarded it
|
|
71
|
+
# to `attribute_impl()` even though `attribute_impl()` itself
|
|
72
|
+
# already accepts it. A real gap: an aggregate that can point at
|
|
73
|
+
# ONE OF several targets (Item's own `personal_list_id`/
|
|
74
|
+
# `camping_list_id`, never both) needs each reference optional
|
|
75
|
+
# on the aggregate's own persisted schema, not just as a
|
|
76
|
+
# command's input.
|
|
77
|
+
# RENAMED FROM `reference_to` — item #13's full metaprogrammed
|
|
78
|
+
# dispatch (slice 4b). Bootstrap-reachable (every core/attached
|
|
79
|
+
# grammar chapter uses reference_to to describe itself), so also
|
|
80
|
+
# named in GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.
|
|
81
|
+
def reference_to_impl(type, as: nil, optional: false)
|
|
82
|
+
target = Naming.demodulise(type)
|
|
83
|
+
@reference_targets << target
|
|
84
|
+
relationship_attribute(target, :reference_to,
|
|
85
|
+
as || default_reference_name(target), optional: optional)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A RULE MAY ONLY READ WITHIN ITS OWN AGGREGATE BOUNDARY (S12,
|
|
89
|
+
# ADR 0025 — "Consistency across aggregate boundaries"). A
|
|
90
|
+
# `given`/`ensures`/`invariant` used to reach through a
|
|
91
|
+
# `reference_to` at RULE-EVALUATION TIME (`References#
|
|
92
|
+
# dereference`, a live query against another aggregate's own
|
|
93
|
+
# repository, unbounded and inconsistent with the "a rule reads
|
|
94
|
+
# only this record" model everywhere else) — `projects` is what
|
|
95
|
+
# replaces that: `projects :customer_status, from: :"customer.
|
|
96
|
+
# status"` declares that THIS aggregate holds its own copy of
|
|
97
|
+
# `Customer`'s own `:status`, kept fresh by a REBUILD SWEEP
|
|
98
|
+
# (`Runtime::ProjectionRebuild`) rather than read live. A rule
|
|
99
|
+
# then reads `customer_status` the same way it reads any other
|
|
100
|
+
# local field — no dot, no reference walk.
|
|
101
|
+
#
|
|
102
|
+
# `from:` NAMES THE LOCAL REFERENCE, not the target aggregate —
|
|
103
|
+
# `customer`, the attribute THIS aggregate's own `reference_to
|
|
104
|
+
# Customer` already minted, not `Customer` the type — so two
|
|
105
|
+
# references to the same aggregate (aliased differently) can
|
|
106
|
+
# each carry their own projection without ambiguity. The TARGET
|
|
107
|
+
# field's own existence cannot be checked here: the target
|
|
108
|
+
# aggregate does not exist yet while THIS one is still being
|
|
109
|
+
# declared (the same reason a query's own hop tail is checked
|
|
110
|
+
# by `BluebookBuilder#validate_query_hops!`, once every
|
|
111
|
+
# aggregate in the chapter is real, not by `AggregateBuilder`
|
|
112
|
+
# itself) — `validate_projected_fields!` is where that half
|
|
113
|
+
# happens.
|
|
114
|
+
def projects_impl(name, from:)
|
|
115
|
+
reference, _, remote_field = from.to_s.rpartition(".")
|
|
116
|
+
|
|
117
|
+
if reference.empty? || remote_field.empty?
|
|
118
|
+
raise Malformed,
|
|
119
|
+
"#{@name}.projects :#{name} names #{from.inspect}, which is not " \
|
|
120
|
+
"reference.field — say which reference and which field on it, e.g. " \
|
|
121
|
+
"from: :\"customer.status\""
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
@projected_fields << ProjectedField.new(name: name.to_sym, reference: reference.to_sym,
|
|
125
|
+
remote_field: remote_field.to_sym)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# `has_many`/`has_one`/`belongs_to` were LEGACY (ADR 0025,
|
|
129
|
+
# "References") — sugar over `reference_to` that collapsed to an
|
|
130
|
+
# anonymous reference and, for `has_many`, LIED (singularised its
|
|
131
|
+
# target and minted one scalar, so `film.backers` read `nil` and
|
|
132
|
+
# never `[]`). Wave 6 (identity-and-relationships arc) un-deprecates
|
|
133
|
+
# all three for real: a relationship word now retains the author's
|
|
134
|
+
# domain concept in IR — still stored as one or more target
|
|
135
|
+
# identities, but no longer collapsed to a bare `reference_to`
|
|
136
|
+
# during assembly. `MetaValidator.shadow_parsing?` still routes to
|
|
137
|
+
# `legacy_has_many`/`legacy_has_one` so frozen era text written
|
|
138
|
+
# under the OLD (lying/collapsing) meaning still parses the way it
|
|
139
|
+
# did when it was written — real, if rare corpus: "Combined corpus
|
|
140
|
+
# uses: one."
|
|
141
|
+
#
|
|
142
|
+
# RENAMED FROM `has_many`/`has_one`/`belongs_to` — item #13's full
|
|
143
|
+
# metaprogrammed dispatch (slice 4). Each Keyword row's own
|
|
144
|
+
# `calls:` names the matching `_impl`; not bootstrap-reachable
|
|
145
|
+
# (no core/attached chapter uses one of these to describe itself),
|
|
146
|
+
# so no BOOTSTRAP_CALLS_FALLBACK entry is needed, unlike
|
|
147
|
+
# `attribute`/`role`.
|
|
148
|
+
def has_many_impl(type, as: nil, **legacy_options)
|
|
149
|
+
if MetaValidator.shadow_parsing?
|
|
150
|
+
return legacy_has_many(type, as: as, optional: legacy_options.fetch(:optional, false))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
unless legacy_options.empty?
|
|
154
|
+
raise Malformed, "#{@name}.has_many takes no #{legacy_options.keys.first}: — an empty list already means none"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
plural = Naming.demodulise(type)
|
|
158
|
+
target = Naming.singularize(plural)
|
|
159
|
+
@reference_targets << target
|
|
160
|
+
relationship_attribute(target, :has_many, as || Naming.snake(plural).to_sym,
|
|
161
|
+
list: true)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def has_one_impl(type, as: nil, optional: false)
|
|
165
|
+
return legacy_has_one(type, as: as, optional: optional) if MetaValidator.shadow_parsing?
|
|
166
|
+
|
|
167
|
+
target = Naming.demodulise(type)
|
|
168
|
+
@reference_targets << target
|
|
169
|
+
relationship_attribute(target, :has_one, as || Naming.snake(target).to_sym,
|
|
170
|
+
optional: optional)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def belongs_to_impl(type, as: nil, optional: false)
|
|
174
|
+
return legacy_has_one(type, as: as, optional: optional) if MetaValidator.shadow_parsing?
|
|
175
|
+
|
|
176
|
+
target = Naming.demodulise(type)
|
|
177
|
+
@reference_targets << target
|
|
178
|
+
relationship_attribute(target, :belongs_to, as || Naming.snake(target).to_sym,
|
|
179
|
+
optional: optional)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def lifecycle_impl(field, default:, &block)
|
|
183
|
+
@lifecycle = LifecycleBuilder.build(field, default: default, &block)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# A piece is declared IN this aggregate — its owner is stamped by
|
|
187
|
+
# `Aggregate#initialize`, once the aggregate exists. Its own
|
|
188
|
+
# commands were given the piece as their owner when it was declared,
|
|
189
|
+
# so the chain closes as chapter -> aggregate -> entity -> command.
|
|
190
|
+
# NOT built here — see `#drain_pending!`'s own comment for why
|
|
191
|
+
# this only queues a descriptor.
|
|
192
|
+
#
|
|
193
|
+
# A PRECONDITION SHARED ACROSS SIBLING PIECES, DECLARED ONCE — one
|
|
194
|
+
# level wider than round 4's own `EntityBuilder#given` (shared
|
|
195
|
+
# across ONE piece's own commands): `@entity_named_givens` is the
|
|
196
|
+
# SAME hash threaded into EVERY piece this aggregate builds, so a
|
|
197
|
+
# piece's own entity-level `given(desc) { block }` write-throughs
|
|
198
|
+
# into it, and any OTHER piece's own command can reference it back
|
|
199
|
+
# bare, the identical description/canonical, evaluated in ITS OWN
|
|
200
|
+
# `parent`-relative context. Real, live corpus this closes:
|
|
201
|
+
# `SafeDepositBox`'s `Visit`/`KeyIssuance` — two DIFFERENT pieces
|
|
202
|
+
# under one head, each independently typing `given("customer is
|
|
203
|
+
# active") { parent.customer.status == "active" }` byte for byte,
|
|
204
|
+
# which neither the aggregate's OWN "customer is active" (a
|
|
205
|
+
# DIFFERENT canonical — bare `customer.status`, not
|
|
206
|
+
# `parent.customer.status`, wrong scope for a piece's own command
|
|
207
|
+
# to evaluate) nor round 4's single-piece `given` could reach.
|
|
208
|
+
def entity_impl(name, &block)
|
|
209
|
+
@pending_entities << [name, block]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def query_impl(name, &block)
|
|
213
|
+
@pending_queries << [name, block]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def policy_impl(name, &block)
|
|
217
|
+
reaction = PolicyBuilder.build(name, &block)
|
|
218
|
+
reaction.aggregate = @name
|
|
219
|
+
@policies << reaction
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# `builder.closed_sets` TOO, not only `builder.build` — a REAL,
|
|
223
|
+
# previously-unreachable gap this exact fix exposed: a
|
|
224
|
+
# value_object's own INLINE `attribute :x, one_of(...)` (now legal
|
|
225
|
+
# — S3, ADR 0025 removed the wrong-arity collision that used to
|
|
226
|
+
# make this crash before it could ever matter) synthesises its own
|
|
227
|
+
# anonymous value object via the SAME `AttributeCollector#closed_
|
|
228
|
+
# sets` mechanism an aggregate's own attributes already use — and
|
|
229
|
+
# nothing installed it anywhere. `Box.attributes` said `size:
|
|
230
|
+
# "Size"` while no "Size" value object existed in the whole
|
|
231
|
+
# domain: a dangling type name, not a working closed set. Flattened
|
|
232
|
+
# into THIS aggregate's own `@value_objects`, the identical move
|
|
233
|
+
# `@value_objects + closed_sets` already makes for the aggregate's
|
|
234
|
+
# own direct attributes (see this file's other 5 call sites).
|
|
235
|
+
def value_object(name, &block)
|
|
236
|
+
builder = ValueObjectBuilder.new(name, owner_value_objects: @value_objects + closed_sets)
|
|
237
|
+
builder.instance_eval(&block) if block
|
|
238
|
+
@value_objects << builder.build
|
|
239
|
+
@value_objects.concat(builder.closed_sets)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# `from:` — LIFECYCLE STATE BECOMES A COMMAND GUARD (S10, ADR
|
|
243
|
+
# 0025) — `command "Debit", from: "open"` replaces `given
|
|
244
|
+
# ("account is open") { status == "open" }`, written 35 times
|
|
245
|
+
# in two wordings across the corpus. Checked against THIS
|
|
246
|
+
# aggregate's own lifecycle field (`Admissibility#enforce_
|
|
247
|
+
# lifecycle_guard`) — never a target state, never a transition:
|
|
248
|
+
# the lifecycle already declares which states exist, so naming
|
|
249
|
+
# the legal ones is checkable against it, where a free-text
|
|
250
|
+
# given could drift out of sync with the state machine and did.
|
|
251
|
+
def command_impl(name, from: nil, &block)
|
|
252
|
+
# The verb is declared ON this aggregate — the owner `acts_on` answers
|
|
253
|
+
# with — stamped by `Aggregate#initialize` once the aggregate
|
|
254
|
+
# exists. An ENTITY's commands take the entity as their owner instead,
|
|
255
|
+
# at the entity's own declaration. NOT built here — see
|
|
256
|
+
# `#drain_pending!`'s own comment for why this only queues a
|
|
257
|
+
# descriptor.
|
|
258
|
+
@pending_commands << [name, from, block]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# A PRECONDITION SHARED ACROSS COMMANDS, DECLARED ONCE (S10, ADR
|
|
262
|
+
# 0025) — an aggregate-level `given`, block required, stored by
|
|
263
|
+
# its own description rather than appended anywhere: a command
|
|
264
|
+
# names it back (`given("customer is active")`, no block of its
|
|
265
|
+
# own) rather than re-typing the predicate, so there is one
|
|
266
|
+
# description and therefore one refusal message no matter which
|
|
267
|
+
# command a caller hits. DECLARE BEFORE THE COMMANDS THAT
|
|
268
|
+
# REFERENCE IT — resolution happens at the referencing command's
|
|
269
|
+
# OWN build time (`CommandBuilder#given`), against whatever this
|
|
270
|
+
# aggregate has declared SO FAR, the one ordering constraint this
|
|
271
|
+
# word carries that `identified_by`/`attribute` do not.
|
|
272
|
+
# BARE — NO BLOCK — REFERENCES a SIBLING AGGREGATE's own
|
|
273
|
+
# already-declared precondition, one level wider than the
|
|
274
|
+
# existing bare-command-references-its-own-aggregate shape
|
|
275
|
+
# (`CommandBuilder#reference_named_given`): `SafeDepositBox`/
|
|
276
|
+
# `OnboardingCase` both name back `Account`'s own "customer is
|
|
277
|
+
# active" rather than retyping `customer.status == "active"` a
|
|
278
|
+
# third and fourth time. Resolved against `@chapter_named_givens`
|
|
279
|
+
# — see `BluebookBuilder#aggregate`'s own comment for how that
|
|
280
|
+
# pool is threaded, and `docs/implemented/resolution-rules/chapter-given.md`
|
|
281
|
+
# for the full algorithm and its known limitations (a bare
|
|
282
|
+
# reference trusts its own author to have verified the SAME
|
|
283
|
+
# canonical predicate applies — this mechanism does not, and
|
|
284
|
+
# cannot, check that itself; see that doc for which real corpus
|
|
285
|
+
# cases do and do not qualify).
|
|
286
|
+
#
|
|
287
|
+
# `declared_by:` DISAMBIGUATES the same description meaning TWO
|
|
288
|
+
# genuinely different predicates chapter-wide — real, live:
|
|
289
|
+
# `Account`'s own "customer is active" reads bare
|
|
290
|
+
# `customer.status` (a DIRECT `reference_to Customer`); `ATMCard`'s
|
|
291
|
+
# own (shared onward with `CardPayment`/`ExternalTransfer`/
|
|
292
|
+
# `ScheduledPayment`/`Statement`) reads `account.customer.status`
|
|
293
|
+
# (reached THROUGH `Account`) — the identical business fact, a
|
|
294
|
+
# genuinely different runtime path, correctly kept as the SAME
|
|
295
|
+
# domain wording rather than invented a second spelling for "the
|
|
296
|
+
# same idea, one more hop away" (S10, ADR 0025's own "one idea,
|
|
297
|
+
# one spelling"). Omit it when the description is unambiguous
|
|
298
|
+
# chapter-wide (the common case, and the ONLY case this took
|
|
299
|
+
# before this parameter existed) — required only once a SECOND,
|
|
300
|
+
# textually-different canonical registers under the same
|
|
301
|
+
# description; see `reference_named_chapter_given`'s own
|
|
302
|
+
# ambiguity error for how that surfaces.
|
|
303
|
+
# RENAMED FROM `given` — item #13's full metaprogrammed dispatch
|
|
304
|
+
# (slice 4b), same reasoning as reference_to_impl above:
|
|
305
|
+
# bootstrap-reachable, in BOOTSTRAP_CALLS_FALLBACK.
|
|
306
|
+
def given_impl(description, declared_by: nil, &predicate)
|
|
307
|
+
return reference_named_chapter_given(description, declared_by: declared_by) unless predicate
|
|
308
|
+
|
|
309
|
+
named = build_rule(Given, description, predicate, owner_name: @name, word: "given",
|
|
310
|
+
extraction_failure: "its source could not be read, so no other runtime could ever evaluate it")
|
|
311
|
+
@named_givens[description] = named
|
|
312
|
+
# WRITE-THROUGH, first-declared-wins PER OWNER — keyed by
|
|
313
|
+
# [description, this aggregate's own name], not description
|
|
314
|
+
# alone: two DIFFERENT aggregates independently declaring the
|
|
315
|
+
# SAME description are two DISTINCT candidates a later bare
|
|
316
|
+
# reference chooses between (via `declared_by:` once there is
|
|
317
|
+
# more than one), never silently merged into one slot the way
|
|
318
|
+
# a bare description-only key would.
|
|
319
|
+
@chapter_named_givens[description] ||= {}
|
|
320
|
+
@chapter_named_givens[description][@name] ||= named
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
private
|
|
324
|
+
|
|
325
|
+
# PRIMITIVE 2 (RuleReference#resolve_owner_keyed) — see that
|
|
326
|
+
# method's own comment for the pool shape; the three branches
|
|
327
|
+
# below (exact owner / unambiguous single candidate / ambiguous)
|
|
328
|
+
# are this construct's OWN refusal wording, not shared, since
|
|
329
|
+
# `declared_by:` only exists here so far. UNRESOLVED (no
|
|
330
|
+
# candidate yet, or `declared_by:` naming an aggregate that
|
|
331
|
+
# hasn't declared it yet) is no longer a fourth branch that
|
|
332
|
+
# raises HERE — see `#pending_chapter_given`, below, for why:
|
|
333
|
+
# a chapter split across files can genuinely reference a
|
|
334
|
+
# precondition a LATER file declares, and "not found among
|
|
335
|
+
# what's loaded so far" cannot tell that apart from "genuinely
|
|
336
|
+
# never declared" until every file has.
|
|
337
|
+
def reference_named_chapter_given(description, declared_by:)
|
|
338
|
+
verify_resolves_via!("given", "Aggregate", "owner_keyed")
|
|
339
|
+
candidates = resolve_owner_keyed(@chapter_named_givens, description)
|
|
340
|
+
|
|
341
|
+
named =
|
|
342
|
+
if declared_by
|
|
343
|
+
owner = Naming.demodulise(declared_by)
|
|
344
|
+
candidates[owner] || pending_chapter_given(description, declared_by: owner)
|
|
345
|
+
elsif candidates.size == 1
|
|
346
|
+
candidates.values.first
|
|
347
|
+
elsif candidates.empty?
|
|
348
|
+
pending_chapter_given(description, declared_by: nil)
|
|
349
|
+
else
|
|
350
|
+
raise(Malformed,
|
|
351
|
+
"#{@name}'s given #{description.inspect} is ambiguous in this chapter — " \
|
|
352
|
+
"#{candidates.keys.join(', ')} each declare a DIFFERENT predicate under " \
|
|
353
|
+
"this same description; name which one with declared_by: (e.g. " \
|
|
354
|
+
"given(#{description.inspect}, declared_by: #{candidates.keys.first}))")
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
@named_givens[description] = named
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# A CHAPTER MAY BE SPLIT ACROSS FILES — the SAME reason a query
|
|
361
|
+
# hop's own cross-file target, a correlation key's own emitting
|
|
362
|
+
# command, and an event's own declared shape are all resolved
|
|
363
|
+
# once the whole chapter is assembled rather than refused the
|
|
364
|
+
# moment one file's own bare reference outruns what's loaded so
|
|
365
|
+
# far (`BluebookBuilder.validate_assembled!`'s own comment).
|
|
366
|
+
#
|
|
367
|
+
# Unlike those, though, a chapter-given's resolved value is not
|
|
368
|
+
# a pass/fail check on an already-built IR — it IS part of the
|
|
369
|
+
# referencing aggregate's own IR (`preconditions:` below), built
|
|
370
|
+
# and handed off the moment THIS aggregate's own file finishes
|
|
371
|
+
# loading, long before a later file might declare the real
|
|
372
|
+
# thing. So this hands back a PLACEHOLDER `Given` — embedded
|
|
373
|
+
# exactly where the resolved one would be, by Ruby object
|
|
374
|
+
# reference, in this aggregate's own `preconditions` AND in any
|
|
375
|
+
# command in this SAME aggregate that separately bare-references
|
|
376
|
+
# the same description (`CommandBuilder#given`'s own hash-chain
|
|
377
|
+
# read of this aggregate's `@named_givens`, the identical key) —
|
|
378
|
+
# and queues the request in `@chapter_pending_givens`.
|
|
379
|
+
# `BluebookBuilder#resolve_pending_chapter_givens!` MUTATES this
|
|
380
|
+
# exact object in place, once every file has loaded, so every
|
|
381
|
+
# existing reference to it (there is only ever the one object,
|
|
382
|
+
# never a copy) sees the resolved fields simultaneously. Safe
|
|
383
|
+
# because every real reader of a `Given` — refusal wording at
|
|
384
|
+
# dispatch, `Aggregate`'s own lazy `-> { preconditions.map { ... } }`
|
|
385
|
+
# IR accessor, docs — runs strictly after boot completes, never
|
|
386
|
+
# mid-load; `judge_deferred!` resolves every pending chapter-given
|
|
387
|
+
# before anything else touches this chapter's assembled IR.
|
|
388
|
+
def pending_chapter_given(description, declared_by:)
|
|
389
|
+
placeholder = Given.new(description: description, canonical: nil, predicate: nil)
|
|
390
|
+
@chapter_pending_givens << { aggregate: @name, description: description,
|
|
391
|
+
declared_by: declared_by, placeholder: placeholder }
|
|
392
|
+
placeholder
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
public
|
|
396
|
+
|
|
397
|
+
# THE AGGREGATE BOUNDARY IS WHAT AN INVARIANT DEFINES (S10, ADR
|
|
398
|
+
# 0025 — "Rules") — checked after every command, before save,
|
|
399
|
+
# the same way a value object's already is
|
|
400
|
+
# (`ValueObjectBuilder#invariant`, whose own shape this mirrors
|
|
401
|
+
# exactly). Today `invariant` lived only inside `value_object`;
|
|
402
|
+
# an aggregate-level rule had nowhere to live, so "the balance
|
|
403
|
+
# never goes negative" was three different `given`/`ensures`
|
|
404
|
+
# texts across banking's six balance-moving commands, and the
|
|
405
|
+
# four that only increase it said nothing at all — completeness
|
|
406
|
+
# depended on someone noticing which commands could decrease it.
|
|
407
|
+
# RENAMED FROM `invariant` — item #13's full metaprogrammed
|
|
408
|
+
# dispatch (slice 4b), same reasoning as given_impl above.
|
|
409
|
+
def invariant_impl(description, &predicate)
|
|
410
|
+
@invariants << build_rule(Invariant, description, predicate, owner_name: @name, word: "invariant",
|
|
411
|
+
extraction_failure: "it would be a rule the IR cannot carry")
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def build
|
|
415
|
+
drain_pending!
|
|
416
|
+
resolve_pending_identity!
|
|
417
|
+
seal_mutation_targets
|
|
418
|
+
seal_query_targets
|
|
419
|
+
seal_defaults
|
|
420
|
+
seal_lifecycle_guards
|
|
421
|
+
seal_projected_fields
|
|
422
|
+
|
|
423
|
+
ir = Aggregate.new(
|
|
424
|
+
name: @name,
|
|
425
|
+
description: @description,
|
|
426
|
+
attributes: attributes,
|
|
427
|
+
value_objects: @value_objects + closed_sets,
|
|
428
|
+
commands: @commands,
|
|
429
|
+
invariants: @invariants,
|
|
430
|
+
preconditions: @named_givens.values,
|
|
431
|
+
projected_fields: @projected_fields,
|
|
432
|
+
identified_by: @identity_paths,
|
|
433
|
+
lifecycle: @lifecycle,
|
|
434
|
+
entities: @entities,
|
|
435
|
+
queries: @queries,
|
|
436
|
+
policies: @policies,
|
|
437
|
+
reference_targets: @reference_targets + entity_reference_targets,
|
|
438
|
+
provenance: @provenance
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# After the IR exists, on purpose : a reference is declared IN the
|
|
442
|
+
# aggregate, and the aggregate the IR graph knows is `ir`, not the
|
|
443
|
+
# builder.
|
|
444
|
+
stamp_references(ir)
|
|
445
|
+
ir
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def self.build(name, chapter_named_givens: {}, chapter_pending_givens: [], &block)
|
|
449
|
+
builder = new(name, chapter_named_givens: chapter_named_givens, chapter_pending_givens: chapter_pending_givens)
|
|
450
|
+
builder.instance_eval(&block) if block
|
|
451
|
+
builder.build
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
private
|
|
455
|
+
|
|
456
|
+
# DEFERRED CONSTRUCTION — `entity`/`command`/`query` used to build
|
|
457
|
+
# immediately, INLINE, the moment their own DSL line ran during
|
|
458
|
+
# `instance_eval` — meaning a command's own resolution (`sets
|
|
459
|
+
# :field` importing the owner's own attribute, `given("desc")`
|
|
460
|
+
# referencing an aggregate-level precondition, a query's own
|
|
461
|
+
# positional-param resolution) only ever saw whatever `@entities`/
|
|
462
|
+
# `attributes`/`@named_givens`/`@value_objects` held AS OF THAT
|
|
463
|
+
# EXACT TEXTUAL LINE — never what the aggregate's block would go
|
|
464
|
+
# on to declare after it. Three real, confirmed cases in the
|
|
465
|
+
# self-hosted meta-domain violate the "declare before you
|
|
466
|
+
# reference" convention every other resolution rule relies on
|
|
467
|
+
# (`command "Handler"` before `entity "Handler"`, same for
|
|
468
|
+
# Member/Dispatch — see docs/resolution-rules/
|
|
469
|
+
# implicit-append-fields.md's own "Known limitations").
|
|
470
|
+
#
|
|
471
|
+
# This is the SAME move `BluebookBuilder` already makes one level
|
|
472
|
+
# UP, at the CHAPTER level — build every aggregate first, THEN run
|
|
473
|
+
# cross-referential validation (`validate_query_hops!`,
|
|
474
|
+
# `validate_projected_fields!`, `validate_no_bidirectional_
|
|
475
|
+
# references!`) once `@aggregates` is fully populated — extended
|
|
476
|
+
# one level down: `entity`/`command`/`query` now only QUEUE a
|
|
477
|
+
# descriptor (`@pending_entities`/`@pending_commands`/
|
|
478
|
+
# `@pending_queries`, each preserving its own declared order),
|
|
479
|
+
# and `#build` drains them here, in this exact order, BEFORE any
|
|
480
|
+
# of the existing `seal_*` validations (which already assume
|
|
481
|
+
# `@commands`/`@entities`/`@queries` are the real, final, built
|
|
482
|
+
# objects) — entities FIRST and fully, since a command's own
|
|
483
|
+
# `sets :list, append: {...}` needs a list's element entity
|
|
484
|
+
# already built (`.attributes` populated) to resolve against, not
|
|
485
|
+
# just named.
|
|
486
|
+
#
|
|
487
|
+
# `attribute`/`value_object`/`identified_by`/`given` (block form)
|
|
488
|
+
# are NOT deferred — they still build eagerly during
|
|
489
|
+
# `instance_eval`, unchanged. Nothing reads `@entities`/
|
|
490
|
+
# `@commands`/`@queries` from anywhere OTHER than `#build` and its
|
|
491
|
+
# own private helpers (checked directly), so nothing else in this
|
|
492
|
+
# file needed to change for this to be safe.
|
|
493
|
+
def drain_pending!
|
|
494
|
+
@entities = @pending_entities.map do |name, block|
|
|
495
|
+
EntityBuilder.build(name, owner_value_objects: @value_objects + closed_sets,
|
|
496
|
+
owner_named_givens: @entity_named_givens,
|
|
497
|
+
identity_name_prefix: "#{Naming.demodulise(@name)}#{Naming.demodulise(name)}",
|
|
498
|
+
identity_value_object_installer: ->(value_object) { @value_objects << value_object },
|
|
499
|
+
&block)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
@commands = @pending_commands.map do |name, from, block|
|
|
503
|
+
CommandBuilder.build(name, owner: @name, from: from, named_givens: @named_givens,
|
|
504
|
+
owner_attributes: attributes,
|
|
505
|
+
owner_constructs: @value_objects + closed_sets + @entities, &block)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
@queries = @pending_queries.map do |name, block|
|
|
509
|
+
QueryBuilder.build(name, owner_attributes: attributes, &block)
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# `identified_by`'s own resolution pool (AttributeCollector#resolve_
|
|
514
|
+
# pending_identity!'s hook, S9) — an aggregate resolves a bare
|
|
515
|
+
# field's own value-object type against everything it declares
|
|
516
|
+
# itself, own inline closed sets included.
|
|
517
|
+
def identity_pool = @value_objects + closed_sets
|
|
518
|
+
|
|
519
|
+
def identity_value_object_name = "#{Naming.demodulise(@name)}Identity"
|
|
520
|
+
|
|
521
|
+
def install_identity_value_object!(value_object)
|
|
522
|
+
@value_objects << value_object
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# LEGACY — see `has_many`/`has_one`/`belongs_to`'s own comment;
|
|
526
|
+
# byte-identical to what those three did before this slice.
|
|
527
|
+
def legacy_has_many(type, as:, optional: false)
|
|
528
|
+
plural = Naming.demodulise(type)
|
|
529
|
+
reference_to_impl(Naming.singularize(plural), as: as || Naming.snake(plural).to_sym, optional: optional)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def legacy_has_one(type, as:, optional: false)
|
|
533
|
+
reference_to_impl(type, as: as || Naming.snake(Naming.demodulise(type)).to_sym, optional: optional)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Every reference is told which Aggregate declares it, so it can
|
|
537
|
+
# find the chapter and resolve its target.
|
|
538
|
+
#
|
|
539
|
+
# Stamped HERE, at build, rather than at `reference_to`, because a command
|
|
540
|
+
# builder does not hold the aggregate and should not learn to. And
|
|
541
|
+
# deliberately across every list that can carry one — a reference the walk
|
|
542
|
+
# missed would resolve to nil, and `resolve_references` SKIPS a nil target,
|
|
543
|
+
# so the guarantee would go quiet instead of going red. That is the exact
|
|
544
|
+
# shape of the bug that let an Account belong to an unregistered customer
|
|
545
|
+
# fourteen times over.
|
|
546
|
+
def stamp_references(ir)
|
|
547
|
+
reference_bearing_attributes.each { |attribute| attribute.type.declared_in = ir }
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# AN OWNED PIECE'S OWN `reference_to` IS AN EDGE THIS AGGREGATE
|
|
551
|
+
# POINTS ACROSS TOO (S9, ADR 0025 — "entity/aggregate shared
|
|
552
|
+
# vocabulary") — a ring closing through a contained piece (Board
|
|
553
|
+
# -> Board::Card -> Product -> Board) is the same "no boundary
|
|
554
|
+
# anyone can reason about alone" `validate_no_bidirectional_
|
|
555
|
+
# references!` already refuses for a direct aggregate-to-
|
|
556
|
+
# aggregate ring; it was invisible before this because only
|
|
557
|
+
# `AggregateBuilder#reference_to` ever fed `@reference_targets`,
|
|
558
|
+
# never `EntityBuilder#reference_to`. Command/query reference
|
|
559
|
+
# ARGUMENTS are deliberately excluded — they are data flowing
|
|
560
|
+
# through a dispatch, not persisted state the graph a cycle
|
|
561
|
+
# means anything over.
|
|
562
|
+
def entity_reference_targets
|
|
563
|
+
@entities.flat_map { |entity| entity.attributes.select(&:reference?).map { |a| a.type.target_name.to_s } }
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def reference_bearing_attributes
|
|
567
|
+
lists = [attributes, *@commands.map(&:attributes), *@queries.map(&:attributes)]
|
|
568
|
+
@entities.each do |entity|
|
|
569
|
+
lists << entity.attributes
|
|
570
|
+
lists.concat(entity.commands.map(&:attributes))
|
|
571
|
+
lists.concat(entity.queries.map(&:attributes))
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
lists.flatten.select(&:reference?)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# A mutation must name a field the aggregate actually HAS.
|
|
578
|
+
#
|
|
579
|
+
# NOT moved to the language, and deliberately so. The language says only
|
|
580
|
+
# `given("a mutation names a target") { !target.value.to_s.empty? }` —
|
|
581
|
+
# non-emptiness — because saying more means reaching a list that lives on
|
|
582
|
+
# a DIFFERENT root : a command's changes hang off Command, the fields they
|
|
583
|
+
# name hang off Aggregate, and a given is a closed predicate over its own
|
|
584
|
+
# state. Aggregate.Seal is the right shape and cannot see commands ; the
|
|
585
|
+
# reference trick that rescued "attributes use value-object types" needs a
|
|
586
|
+
# root to point at, and an aggregate's fields are a value-object list, not
|
|
587
|
+
# roots. This is the second rule that cannot port for that reason — the
|
|
588
|
+
# first is read-model uniqueness — and both wait on the same thing : a
|
|
589
|
+
# quantifier, or fields promoted to roots.
|
|
590
|
+
#
|
|
591
|
+
# So it lives here, at build, where every declaration is present. Found by
|
|
592
|
+
# writing `then_set :disputed_by` on CardPayment before the field existed :
|
|
593
|
+
# it wrote into nothing, refused nothing, and every check stayed green.
|
|
594
|
+
# A DEFAULT FILLS THE SHAPE IT IS DECLARED ON, or it fills nothing.
|
|
595
|
+
#
|
|
596
|
+
# `attribute :cover, one_of("covered", "open"), default: "open"` builds
|
|
597
|
+
# cleanly and then refuses EVERY create at dispatch — "cover is a Cover,
|
|
598
|
+
# pass its fields as an object" — because the value object wants its
|
|
599
|
+
# fields and got a bare string. The bluebook is wrong at the line where
|
|
600
|
+
# it is written and says so nowhere near it.
|
|
601
|
+
#
|
|
602
|
+
# It cost a corpus member 33 refusals out of 40 steps, with every gate
|
|
603
|
+
# green throughout: the refusals were perfectly consistent, which is
|
|
604
|
+
# consistency about nothing. `till.bluebook` has always had the right shape
|
|
605
|
+
# — `default: { cents: 0 }`.
|
|
606
|
+
#
|
|
607
|
+
# A PRIMITIVE takes a scalar and a VALUE OBJECT takes its fields, so the
|
|
608
|
+
# test is simply which one the type names. Nothing here guesses at the
|
|
609
|
+
# keys: a default that is a Hash is left to `Value.for_attribute`, which
|
|
610
|
+
# is where a wrong FIELD belongs.
|
|
611
|
+
def seal_defaults
|
|
612
|
+
shapes = @value_objects.map { |shape| shape.hecks_name.to_s }
|
|
613
|
+
|
|
614
|
+
attributes.each do |attribute|
|
|
615
|
+
next if attribute.default.nil? || attribute.default.is_a?(Hash)
|
|
616
|
+
next unless shapes.include?(attribute.type.to_s)
|
|
617
|
+
|
|
618
|
+
raise Malformed,
|
|
619
|
+
"#{@name}.#{attribute.name} defaults to #{attribute.default.inspect}, but " \
|
|
620
|
+
"#{attribute.type} is a value object — a default fills its FIELDS " \
|
|
621
|
+
"(default: { ... }), and a bare value refuses every create instead"
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
# A command's `from:` guard needs a lifecycle field to check
|
|
626
|
+
# against — declared at BUILD time (S10, ADR 0025), the same
|
|
627
|
+
# point every other "does this actually resolve" check in this
|
|
628
|
+
# file runs, rather than left to crash `enforce_lifecycle_
|
|
629
|
+
# guard` the first time such a command is ever dispatched.
|
|
630
|
+
def seal_lifecycle_guards
|
|
631
|
+
return if @lifecycle
|
|
632
|
+
|
|
633
|
+
@commands.each do |command|
|
|
634
|
+
next unless command.from
|
|
635
|
+
|
|
636
|
+
raise Malformed,
|
|
637
|
+
"#{@name}.#{command.hecks_name} guards from: #{Array(command.from).inspect}, but " \
|
|
638
|
+
"#{@name} declares no lifecycle — from: checks a lifecycle field, and there is " \
|
|
639
|
+
"none here to check"
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# `projects`'s OWN half of "does this actually resolve" (S12,
|
|
644
|
+
# ADR 0025) — the LOCAL half only: `reference` must name a real
|
|
645
|
+
# reference-typed attribute this aggregate declares, and
|
|
646
|
+
# `name` must not collide with an attribute already declared
|
|
647
|
+
# (a projected field is its own kind of field, never a second
|
|
648
|
+
# spelling of one that already exists). The TARGET aggregate's
|
|
649
|
+
# own field is checked separately, once every aggregate in the
|
|
650
|
+
# chapter is real — see BluebookBuilder#validate_projected_
|
|
651
|
+
# fields!'s own comment for why that half cannot happen here.
|
|
652
|
+
def seal_projected_fields
|
|
653
|
+
declared = attributes.map { |attribute| attribute.name.to_sym }
|
|
654
|
+
|
|
655
|
+
@projected_fields.each do |field|
|
|
656
|
+
if declared.include?(field.name)
|
|
657
|
+
raise Malformed,
|
|
658
|
+
"#{@name}.projects :#{field.name} names a field #{@name} already declares — " \
|
|
659
|
+
"a projected field is never a second spelling of one that already exists"
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
reference_attribute = attributes.find { |attribute| attribute.name == field.reference }
|
|
663
|
+
unless reference_attribute&.reference?
|
|
664
|
+
raise Malformed,
|
|
665
|
+
"#{@name}.projects :#{field.name} reads through #{field.reference.inspect}, which " \
|
|
666
|
+
"#{@name} never declares as a reference_to — projects reads through a REFERENCE, " \
|
|
667
|
+
"never a value object or a scalar"
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def seal_mutation_targets
|
|
673
|
+
known = attributes.map { |attribute| attribute.name.to_sym }
|
|
674
|
+
known << @lifecycle.field.to_sym if @lifecycle
|
|
675
|
+
|
|
676
|
+
@commands.each do |command|
|
|
677
|
+
command.mutations.each do |mutation|
|
|
678
|
+
# `:delegate` — CommandBuilder#delegates_to's own comment —
|
|
679
|
+
# targets no field of THIS aggregate at all; its `target`
|
|
680
|
+
# names an "Entity.Command" pair instead, checked when the
|
|
681
|
+
# command builds (`delegates_to`'s own `rpartition` guard)
|
|
682
|
+
# and again at dispatch time (`CommandInterpreter
|
|
683
|
+
# #step_delegate_to_entity`, which refuses a real one that
|
|
684
|
+
# names no such entity or command). Sealing THIS check
|
|
685
|
+
# against it would refuse every delegating command outright.
|
|
686
|
+
next if mutation.op == :delegate
|
|
687
|
+
next if known.include?(mutation.target.to_sym)
|
|
688
|
+
|
|
689
|
+
raise Malformed,
|
|
690
|
+
"#{@name}.#{command.hecks_name} sets #{mutation.target}, which #{@name} " \
|
|
691
|
+
"never declares — a mutation into a field that does not exist " \
|
|
692
|
+
"writes nothing and refuses nothing"
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
# A query must ask about a field the aggregate actually HAS — the same
|
|
698
|
+
# seal `then_set` gets, closing the same silence: a where over a field
|
|
699
|
+
# nothing declares matches nothing and refuses nothing, forever, on
|
|
700
|
+
# every adapter. Three more silences close with it. A dotted path may
|
|
701
|
+
# reach through the value-object graph but must LAND on a scalar
|
|
702
|
+
# member (QuerySpecification::FieldPath is the one walk every engine
|
|
703
|
+
# now shares) — landing on a value object hands SQL a JSON object
|
|
704
|
+
# where the reference interpreter unwraps a hash. An ordered
|
|
705
|
+
# comparator (lt/gt/gte/lte) must land on a numeric leaf — over text
|
|
706
|
+
# the reference interpreter quietly matches no rows while SQL
|
|
707
|
+
# compares lexicographically. And a :symbol value must name one of
|
|
708
|
+
# the query's own declared arguments, or it resolves to nil at
|
|
709
|
+
# dispatch and matches nothing.
|
|
710
|
+
ORDERED_COMPARATORS = %i[lt lte gt gte].freeze
|
|
711
|
+
|
|
712
|
+
def seal_query_targets
|
|
713
|
+
query_surfaces.each do |owner, fields, lifecycle, queries|
|
|
714
|
+
queries.each do |query|
|
|
715
|
+
query.wheres.each do |clause|
|
|
716
|
+
seal_query_field(owner, query, fields, lifecycle, clause.field)
|
|
717
|
+
seal_ordered_comparator(owner, query, fields, clause)
|
|
718
|
+
infer_local_query_argument(query, fields, lifecycle, clause)
|
|
719
|
+
seal_query_argument(owner, query, clause.value) unless clause.field.to_s.include?("/")
|
|
720
|
+
end
|
|
721
|
+
seal_query_field(owner, query, fields, lifecycle, query.order_by.field, ordering: true) if query.order_by
|
|
722
|
+
seal_query_argument(owner, query, query.limit&.value)
|
|
723
|
+
seal_query_argument(owner, query, query.offset&.value)
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def query_surfaces
|
|
729
|
+
[[@name, attributes, @lifecycle, @queries]] +
|
|
730
|
+
@entities.map { |entity| ["#{@name}::#{entity.hecks_name}", entity.attributes, entity.lifecycle, entity.queries] }
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
# `/` CROSSES INTO ANOTHER RECORD, `.` WALKS FIELDS INSIDE THIS
|
|
734
|
+
# ONE (ADR 0025, "References") — the operator answers which
|
|
735
|
+
# kind of path this is now, not a name collision to arbitrate,
|
|
736
|
+
# so a hop is routed to its own method before any `.`-splitting
|
|
737
|
+
# runs at all; `seal_query_hop` below never sees a field this
|
|
738
|
+
# one would also have tried to resolve as a local dotted walk.
|
|
739
|
+
def seal_query_field(owner, query, fields, lifecycle, field, ordering: false)
|
|
740
|
+
return seal_query_hop(owner, query, fields, field, ordering: ordering) if field.to_s.include?("/")
|
|
741
|
+
|
|
742
|
+
name, *nested = field.to_s.split(".")
|
|
743
|
+
attribute = fields.find { |candidate| candidate.name.to_s == name }
|
|
744
|
+
if nested.empty? && attribute
|
|
745
|
+
refuse_ambiguous_comparison!(owner, query, field, attribute)
|
|
746
|
+
return
|
|
747
|
+
end
|
|
748
|
+
return if nested.empty? && lifecycle&.field.to_s == name
|
|
749
|
+
return if nested.any? && attribute && scalar_path?(attribute, nested)
|
|
750
|
+
|
|
751
|
+
if nested.any? && attribute && resolves?(attribute, nested)
|
|
752
|
+
raise Malformed,
|
|
753
|
+
"#{owner}.#{query.hecks_name} asks about #{field}, which lands on a " \
|
|
754
|
+
"value object, not a scalar — a dotted query path ends on a scalar " \
|
|
755
|
+
"member, or the engines answer it differently"
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
raise Malformed,
|
|
759
|
+
"#{owner}.#{query.hecks_name} asks about #{field}, which #{owner} " \
|
|
760
|
+
"never declares — a query over a field that does not exist " \
|
|
761
|
+
"matches nothing and refuses nothing"
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# ORDER BY refuses a hop OUTRIGHT, right here — unlike a WHERE
|
|
765
|
+
# hop (deferred below), this doesn't need the target's shape to
|
|
766
|
+
# answer: an ask is ordered by what its own answering rows
|
|
767
|
+
# hold, and a hop answers with a candidate set, not a sort key
|
|
768
|
+
# (see Runtime::ReferenceHop).
|
|
769
|
+
#
|
|
770
|
+
# A WHERE hop is only RECOGNISED here, and CHECKED LATER. The
|
|
771
|
+
# head names one of this aggregate's own references, which is
|
|
772
|
+
# answerable now — a Reference knows its own target_name at
|
|
773
|
+
# declaration. What it points AT is not: stamp_references has
|
|
774
|
+
# already run by this point, but the chapter (Bluebook, and the
|
|
775
|
+
# owning aggregate's OWN place in it) does not exist yet, so
|
|
776
|
+
# Reference#resolve would answer nil for every target in the
|
|
777
|
+
# file, including ones declared above this one. The tail, and
|
|
778
|
+
# whether the target even exists, are BluebookBuilder's
|
|
779
|
+
# business — see validate_query_hops!, which runs once the
|
|
780
|
+
# chapter is real, for exactly the reason
|
|
781
|
+
# validate_no_bidirectional_references! already gives for
|
|
782
|
+
# living at that same later point.
|
|
783
|
+
def seal_query_hop(owner, query, fields, field, ordering:)
|
|
784
|
+
unless QuerySpecification::HopPath.hop_head?(field, fields)
|
|
785
|
+
raise Malformed,
|
|
786
|
+
"#{owner}.#{query.hecks_name} asks about #{field}, which #{owner} " \
|
|
787
|
+
"never declares — a query over a field that does not exist " \
|
|
788
|
+
"matches nothing and refuses nothing"
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
return unless ordering
|
|
792
|
+
|
|
793
|
+
raise Malformed,
|
|
794
|
+
"#{owner}.#{query.hecks_name} orders by #{field}, which hops through " \
|
|
795
|
+
"a reference — an ask is ordered by what its own answering rows " \
|
|
796
|
+
"hold, and a hop answers with a candidate set, not a sort key"
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
def seal_ordered_comparator(owner, query, fields, clause)
|
|
800
|
+
return unless ORDERED_COMPARATORS.include?(clause.op.to_s.to_sym)
|
|
801
|
+
|
|
802
|
+
# A WHERE clause hopping through a reference with an ordered
|
|
803
|
+
# comparator is legitimate ("client whose balance > 500") —
|
|
804
|
+
# unlike ORDER BY (refused outright in seal_query_field, see
|
|
805
|
+
# its own comment), a where-clause hop answers a real
|
|
806
|
+
# candidate set either way, ordered or not. Deferred for the
|
|
807
|
+
# same reason any other hop is: whether the tail is even
|
|
808
|
+
# numeric is BluebookBuilder#validate_query_hops!'s question
|
|
809
|
+
# to ask of the TARGET's shape, not this aggregate's own.
|
|
810
|
+
return if clause.field.to_s.include?("/") && QuerySpecification::HopPath.hop_head?(clause.field, fields)
|
|
811
|
+
|
|
812
|
+
name, *nested = clause.field.to_s.split(".")
|
|
813
|
+
attribute = fields.find { |candidate| candidate.name.to_s == name }
|
|
814
|
+
return if attribute &&
|
|
815
|
+
QuerySpecification::FieldPath.numeric?(attribute, nested) { |type| declared_value_object(type) }
|
|
816
|
+
|
|
817
|
+
held = attribute ? "holds no number" : "is the lifecycle field, which holds text"
|
|
818
|
+
raise Malformed,
|
|
819
|
+
"#{owner}.#{query.hecks_name} compares #{clause.field} with #{clause.op}, " \
|
|
820
|
+
"but #{clause.field} #{held} — an ordered comparison needs a numeric " \
|
|
821
|
+
"field, and over anything else the adapters answer differently or not at all"
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
def seal_query_argument(owner, query, value)
|
|
825
|
+
return unless value.is_a?(Symbol)
|
|
826
|
+
return if query.attribute(value)
|
|
827
|
+
|
|
828
|
+
raise Malformed,
|
|
829
|
+
"#{owner}.#{query.hecks_name} resolves :#{value} from its arguments, " \
|
|
830
|
+
"but declares no #{value} attribute — an argument that does not exist " \
|
|
831
|
+
"resolves to nil and matches nothing"
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
# A symbolic right-hand side is a query input. When the compared path
|
|
835
|
+
# lands on this owner's declared shape, its type is already known and
|
|
836
|
+
# repeating an `attribute` line inside the query adds no information.
|
|
837
|
+
# Reference hops are resolved only after the whole chapter has been
|
|
838
|
+
# owner-stamped; BluebookBuilder performs the identical inference for
|
|
839
|
+
# those deferred paths.
|
|
840
|
+
def infer_local_query_argument(query, fields, lifecycle, clause)
|
|
841
|
+
name = clause.value
|
|
842
|
+
return unless name.is_a?(Symbol)
|
|
843
|
+
return if query.attribute(name)
|
|
844
|
+
return if clause.field.to_s.include?("/")
|
|
845
|
+
|
|
846
|
+
head, *nested = clause.field.to_s.split(".")
|
|
847
|
+
leaf = if nested.empty? && lifecycle&.field.to_s == head
|
|
848
|
+
Attribute.new(name: name, type: String)
|
|
849
|
+
else
|
|
850
|
+
root = fields.find { |candidate| candidate.name.to_s == head }
|
|
851
|
+
found = root && QuerySpecification::FieldPath.leaf_attribute(root, nested) do |type|
|
|
852
|
+
declared_value_object(type)
|
|
853
|
+
end
|
|
854
|
+
found && Attribute.new(name: name, type: found.type, list: found.list?)
|
|
855
|
+
end
|
|
856
|
+
query.attributes << leaf if leaf
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
# A BARE FIELD NAMING A VALUE OBJECT HAS TO SAY WHICH MEMBER IT
|
|
860
|
+
# MEANS, when more than one could answer. The dotted case above
|
|
861
|
+
# already refuses a path that lands on a value object rather than
|
|
862
|
+
# a scalar; a bare name was returning unconditionally, so
|
|
863
|
+
# `where(frequency: ...)` against a StatementFrequency
|
|
864
|
+
# (cadence, retention_months, paper_fee_cents) compiled — and the
|
|
865
|
+
# engines then disagreed about which member it meant, one taking
|
|
866
|
+
# the FIRST numeric and another declining to unwrap at all.
|
|
867
|
+
#
|
|
868
|
+
# Unambiguous is: exactly one member, whatever its type, or
|
|
869
|
+
# exactly one NUMERIC member among several (Money's `cents`
|
|
870
|
+
# beside its `currency` — the reading every engine already
|
|
871
|
+
# shared, and what the corpus relies on). Anything else names
|
|
872
|
+
# its member with a dotted path, which already works.
|
|
873
|
+
#
|
|
874
|
+
# A list is exempt: `contains` over a `list_of` reads element
|
|
875
|
+
# membership, not a scalar comparison, and has its own agreed
|
|
876
|
+
# reading across the engines.
|
|
877
|
+
def refuse_ambiguous_comparison!(owner, query, field, attribute)
|
|
878
|
+
return if attribute.list?
|
|
879
|
+
|
|
880
|
+
value_object = declared_value_object(attribute.type.to_s)
|
|
881
|
+
return unless value_object
|
|
882
|
+
|
|
883
|
+
members = QuerySpecification::Common::Comparison.ambiguous_members(value_object)
|
|
884
|
+
return if members.empty?
|
|
885
|
+
|
|
886
|
+
raise Malformed,
|
|
887
|
+
"#{owner}.#{query.hecks_name} asks about #{field}, which names #{attribute.type} — " \
|
|
888
|
+
"it has #{members.size} members (#{members.join(', ')}) and no single one a " \
|
|
889
|
+
"comparison can mean; name the member (#{field}.#{members.first})"
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
def scalar_path?(attribute, nested)
|
|
893
|
+
QuerySpecification::FieldPath.scalar_leaf?(attribute, nested) { |type| declared_value_object(type) }
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
def resolves?(attribute, nested)
|
|
897
|
+
!QuerySpecification::FieldPath.leaf_attribute(attribute, nested) { |type| declared_value_object(type) }.nil?
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
def declared_value_object(type_name)
|
|
901
|
+
(@value_objects + closed_sets).find { |shape| shape.hecks_name.to_s == type_name }
|
|
902
|
+
end
|
|
903
|
+
end
|
|
904
|
+
end
|
|
905
|
+
end
|
|
906
|
+
end
|