hecks 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +427 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +179 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +145 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +438 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +306 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +54 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +125 -0
- data/lib/hecks/behaviors/expectations.rb +340 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
- data/lib/hecks/bluebook/assembly/marks.rb +228 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +91 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +78 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
- data/lib/hecks/bluebook/expression/projection.json +218 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +781 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +542 -0
- data/lib/hecks/bluebook/model_check.rb +445 -0
- data/lib/hecks/bluebook/pattern_subset.rb +184 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +128 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +66 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +410 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +119 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +166 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +341 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +177 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +84 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +161 -0
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +119 -0
- data/lib/hecks/forms/record_table.rb +68 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
- data/lib/hecks/fuzzing/properties.rb +1192 -0
- data/lib/hecks/fuzzing/replay.rb +668 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
- data/lib/hecks/fuzzing/value_generator.rb +200 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +324 -0
- data/lib/hecks/grammar/expression.bluebook +420 -0
- data/lib/hecks/grammar/expression_operators.json +1648 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +471 -0
- data/lib/hecks/language/bluebook/entity.bluebook +392 -0
- data/lib/hecks/language/bluebook/policy.bluebook +189 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +333 -0
- data/lib/hecks/language/port.bluebook +120 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +174 -0
- data/lib/hecks/ports/access_control.port +9 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +8 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +6 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +7 -0
- data/lib/hecks/ports/authorization.rb +62 -0
- data/lib/hecks/ports/clock.port +5 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +5 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +5 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +5 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +5 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +138 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +23 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +96 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +642 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +44 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +446 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +158 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +411 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +92 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +107 -0
- data/lib/hecks/query_specification/hop_path.rb +132 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +66 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
- data/lib/hecks/runtime/command_interpreter.rb +497 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +189 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +245 -0
- data/lib/hecks/runtime/dispatcher.rb +301 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +299 -0
- data/lib/hecks/runtime/errors.rb +122 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +156 -0
- data/lib/hecks/runtime/instance.rb +175 -0
- data/lib/hecks/runtime/interpreting.rb +90 -0
- data/lib/hecks/runtime/loader.rb +184 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +286 -0
- data/lib/hecks/runtime/reaction_invocation.rb +253 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +126 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
- data/lib/hecks/runtime/registry/verification.rb +246 -0
- data/lib/hecks/runtime/registry.rb +279 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +503 -0
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +130 -0
- data/lib/hecks/runtime/value/coercion.rb +565 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +107 -0
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +218 -0
- data/lib/hecks.rb +123 -6
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +412 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
require_relative "../rendering"
|
|
2
|
+
require_relative "errors"
|
|
3
|
+
require_relative "value"
|
|
4
|
+
require_relative "refusal_wording"
|
|
5
|
+
require_relative "tenant_scope"
|
|
6
|
+
require_relative "../ports/query/in_memory"
|
|
7
|
+
require_relative "../query_specification/field_path"
|
|
8
|
+
|
|
9
|
+
module Hecks
|
|
10
|
+
module Runtime
|
|
11
|
+
class ReadModelInterpreter
|
|
12
|
+
def initialize(registry) = @registry = registry
|
|
13
|
+
|
|
14
|
+
def call(domain, model, args)
|
|
15
|
+
project(domain, model, args)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def project(domain, model, args)
|
|
21
|
+
bluebook = @registry.bluebook(domain)
|
|
22
|
+
rootless = model.reference_target.nil?
|
|
23
|
+
# BEFORE the adapter early-return below, so the SQLite path inherits it.
|
|
24
|
+
# Without this a stale caller passing a wrapped reference gets a
|
|
25
|
+
# path-dependent answer — an adapter could quietly open the wrapped
|
|
26
|
+
# reference while the in-process path reads it whole and finds nothing.
|
|
27
|
+
# Refused up front, identically on every path. Nothing to refuse for
|
|
28
|
+
# a rootless model — there's no reference argument to have offered
|
|
29
|
+
# wrong at all.
|
|
30
|
+
refuse_object_reference(model, args) unless rootless
|
|
31
|
+
reference_id = reference(args.fetch(model.reference_name)) unless rootless
|
|
32
|
+
# Computed off the ORIGINAL model, before TenantScope wraps it — the
|
|
33
|
+
# "which head do options apply to" question is about what the
|
|
34
|
+
# bluebook author declared, not about the synthetic tenant clause
|
|
35
|
+
# the wrapper adds underneath.
|
|
36
|
+
eligible = model.filtered_head_name
|
|
37
|
+
model = TenantScope.apply(model, args)
|
|
38
|
+
# A ROOTLESS, `group_by`-declared, or `count`/`median`-declared
|
|
39
|
+
# model skips the SQLite native escape hatch entirely (there is
|
|
40
|
+
# no root aggregate to look up a repository FOR when rootless,
|
|
41
|
+
# and `query_read_model` knows nothing about grouping or
|
|
42
|
+
# reducing) — always runs the in-process loop below instead.
|
|
43
|
+
# Correct everywhere ; not SQL-pushed-down for a SQLite-backed
|
|
44
|
+
# aggregate yet, a real, named limit, not a silent one — the
|
|
45
|
+
# same one `group_by` already accepted, extended here rather
|
|
46
|
+
# than narrowed.
|
|
47
|
+
unless rootless || model.group_by.any? || model.count? || model.median_field
|
|
48
|
+
repository = @registry.read_repository(domain, bluebook.aggregate(model.reference_target))
|
|
49
|
+
return repository.query_read_model(domain, model, args, bluebook) if repository.respond_to?(:query_read_model) && repository.adapter.respond_to?(:query_read_model)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# ROOT FIRST, ALWAYS — regardless of `include` order in the
|
|
53
|
+
# bluebook. `read_model_builder.rb`'s own `include` is
|
|
54
|
+
# documented "Order-independent" (the `:many` flag is resolved
|
|
55
|
+
# at build time, once `@reference_target` is known), but that
|
|
56
|
+
# promise was never kept HERE: this loop used to run heads in
|
|
57
|
+
# their literal declared order and match each "many" head
|
|
58
|
+
# against whatever was ALREADY in `projected` — empty, the
|
|
59
|
+
# very first time through, if a many-side head happened to be
|
|
60
|
+
# declared before the root. A real, live bug (not a guess):
|
|
61
|
+
# `include Promotion` before `include Item` on a read model
|
|
62
|
+
# whose root IS Item silently returned an empty array for
|
|
63
|
+
# Promotion — no error, just a wrong, too-small answer — while
|
|
64
|
+
# the reverse order worked purely by accident. `partition`,
|
|
65
|
+
# not `sort_by`: Ruby's `sort_by` is not guaranteed stable,
|
|
66
|
+
# and correctness here must not depend on it being so by
|
|
67
|
+
# chance on the current MRI build.
|
|
68
|
+
root_heads, other_heads = model.aggregate_heads.partition { |head| head[:aggregate] == model.reference_target }
|
|
69
|
+
projected = []
|
|
70
|
+
rows_by_as = {}
|
|
71
|
+
(root_heads + order_other_heads(bluebook, root_heads, other_heads)).each do |head|
|
|
72
|
+
rows = if head[:aggregate] == model.reference_target
|
|
73
|
+
[fetch(bluebook, domain, head[:aggregate], reference_id)]
|
|
74
|
+
elsif rootless
|
|
75
|
+
# No root to FK-match against — a rootless model reads
|
|
76
|
+
# each of its own heads WHOLE, independently. (Multiple
|
|
77
|
+
# heads on one rootless model aren't cross-joined
|
|
78
|
+
# against each other either — each is its own bulk
|
|
79
|
+
# read. A real, deliberate scope limit for now.)
|
|
80
|
+
records(bluebook, domain, head[:aggregate])
|
|
81
|
+
else
|
|
82
|
+
matching(records(bluebook, domain, head[:aggregate])) do |record|
|
|
83
|
+
projected.any? do |source|
|
|
84
|
+
reference_fields(bluebook.aggregate(head[:aggregate]), source[:aggregate]).any? do |field|
|
|
85
|
+
source[:rows].any? { |parent| reference(record[field]) == parent.id }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
rows = Ports::Query::InMemory.execute(rows, model, args) if head[:as] == eligible
|
|
91
|
+
projected << { aggregate: head[:aggregate], rows: rows }
|
|
92
|
+
rows_by_as[head[:as]] = head[:many] ? rows : rows.first
|
|
93
|
+
end
|
|
94
|
+
# Declared order preserved in the OUTPUT — only the
|
|
95
|
+
# computation above needed reordering, not what a caller sees
|
|
96
|
+
# back.
|
|
97
|
+
heads = model.aggregate_heads.each_with_object({}) { |head, report| report[head[:as]] = rows_by_as[head[:as]] }
|
|
98
|
+
grouped_head = group_by_target(model, bluebook)
|
|
99
|
+
reduced_head = aggregation_target(model, bluebook)
|
|
100
|
+
[heads.each_with_object({}) do |(as, value), out|
|
|
101
|
+
out[as] = if grouped_head && as == grouped_head[:as]
|
|
102
|
+
nest(value.map { |record| Value.materialize_unwrapped(row(record)) }, model.group_by_fields)
|
|
103
|
+
elsif reduced_head && as == reduced_head[:as] && model.count?
|
|
104
|
+
value.length
|
|
105
|
+
elsif reduced_head && as == reduced_head[:as]
|
|
106
|
+
median(value, model.median_field)
|
|
107
|
+
elsif value.is_a?(Array)
|
|
108
|
+
value.map { |record| Value.materialize(row(record)) }
|
|
109
|
+
else
|
|
110
|
+
Value.materialize(row(value))
|
|
111
|
+
end
|
|
112
|
+
end]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# THE ROOT-FIRST FIX'S OWN FIX — root-first alone only reaches one
|
|
116
|
+
# level: it guarantees the root is in `projected` before any other
|
|
117
|
+
# head is matched, but a CHAIN of non-root heads (a head that
|
|
118
|
+
# references another non-root head, not the root) is still
|
|
119
|
+
# matched against whatever declaration order happened to put in
|
|
120
|
+
# `projected` so far. `include Coupon` before `include Promotion`
|
|
121
|
+
# on a read model rooted at Item, where Coupon references
|
|
122
|
+
# Promotion (which references Item), silently returned an empty
|
|
123
|
+
# `coupons` array — Coupon's match ran while `projected` held only
|
|
124
|
+
# Item, one level short of what it needed.
|
|
125
|
+
#
|
|
126
|
+
# Fixed the same way root-first was: not by asking bluebook authors
|
|
127
|
+
# to declare `include` in dependency order (the same promise
|
|
128
|
+
# `read_model_builder.rb` already makes and this file is the one
|
|
129
|
+
# place obligated to keep), but by topologically sorting the
|
|
130
|
+
# non-root heads on their OWN declared reference fields before
|
|
131
|
+
# this method's runtime matching ever runs — Kahn's algorithm,
|
|
132
|
+
# picking ready heads in DECLARED order at each step so declaring
|
|
133
|
+
# order still governs whenever there is no dependency to break a
|
|
134
|
+
# tie. This generalizes root-first (a chain of length 1) to a
|
|
135
|
+
# chain of any depth, and to a head depending on more than one
|
|
136
|
+
# other head at once (not just a straight chain).
|
|
137
|
+
#
|
|
138
|
+
# A cycle among non-root heads (A references B which references A)
|
|
139
|
+
# has no valid topological order at all — falls back to the
|
|
140
|
+
# remaining heads' declared order rather than looping forever, the
|
|
141
|
+
# same "whichever runs first finds nothing" behaviour this method
|
|
142
|
+
# had for every non-root head before root-first existed.
|
|
143
|
+
def order_other_heads(bluebook, root_heads, other_heads)
|
|
144
|
+
resolved = root_heads.map { |head| head[:aggregate] }
|
|
145
|
+
remaining = other_heads.dup
|
|
146
|
+
ordered = []
|
|
147
|
+
until remaining.empty?
|
|
148
|
+
ready, blocked = remaining.partition do |head|
|
|
149
|
+
depends_on(bluebook, head, other_heads).all? { |target| resolved.include?(target) }
|
|
150
|
+
end
|
|
151
|
+
if ready.empty?
|
|
152
|
+
ordered.concat(remaining)
|
|
153
|
+
break
|
|
154
|
+
end
|
|
155
|
+
ordered.concat(ready)
|
|
156
|
+
resolved.concat(ready.map { |head| head[:aggregate] })
|
|
157
|
+
remaining = blocked
|
|
158
|
+
end
|
|
159
|
+
ordered
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Which OTHER declared (non-root) heads a head's own aggregate
|
|
163
|
+
# holds a reference field toward — the same relationship this
|
|
164
|
+
# file's runtime matching checks record-by-record, asked here
|
|
165
|
+
# statically, once, to order heads before any record is read.
|
|
166
|
+
#
|
|
167
|
+
# `head[:aggregate]` names whatever `include` was given — and
|
|
168
|
+
# `include` accepts a nested ENTITY (Member, nested under
|
|
169
|
+
# ValueObject ; Handler and Dispatch, nested under ProcessManager
|
|
170
|
+
# — bluebook.bluebook's own `WholeBluebook` read model includes
|
|
171
|
+
# all three) just as readily as a top-level aggregate.
|
|
172
|
+
# `bluebook.aggregate` only ever finds the latter
|
|
173
|
+
# (Behaviour::Chapter#aggregate searches `@aggregates`, which
|
|
174
|
+
# holds no entities), so it returns nil for an entity-headed
|
|
175
|
+
# include — a real case, not a malformed one. `records`, below,
|
|
176
|
+
# already treats that nil as "no rows of its own to fetch" ;
|
|
177
|
+
# a head with no rows of its own has nothing to check for a
|
|
178
|
+
# reference field either, so it depends on nothing here, the
|
|
179
|
+
# same as it always silently read empty before this file's
|
|
180
|
+
# topological sort existed.
|
|
181
|
+
def depends_on(bluebook, head, other_heads)
|
|
182
|
+
aggregate = bluebook.aggregate(head[:aggregate])
|
|
183
|
+
return [] unless aggregate
|
|
184
|
+
|
|
185
|
+
other_heads.reject { |other| other[:aggregate] == head[:aggregate] }
|
|
186
|
+
.select { |other| reference_fields(aggregate, other[:aggregate]).any? }
|
|
187
|
+
.map { |other| other[:aggregate] }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# `group_by`'s own declared fields, checked against the ONE
|
|
191
|
+
# many-side head they apply to (`seal_group_by` already refuses
|
|
192
|
+
# zero or several) — resolved here, once, rather than re-derived
|
|
193
|
+
# per row. Raises loudly on a typo'd field name rather than
|
|
194
|
+
# silently grouping every row into one bucket keyed `nil`.
|
|
195
|
+
def group_by_target(model, bluebook)
|
|
196
|
+
return nil unless model.group_by.any?
|
|
197
|
+
|
|
198
|
+
target = model.aggregate_heads.find { |head| head[:many] }
|
|
199
|
+
aggregate = bluebook.aggregate(target[:aggregate])
|
|
200
|
+
model.group_by_fields.each do |field|
|
|
201
|
+
next if aggregate.attribute(field)
|
|
202
|
+
# THE LIFECYCLE FIELD IS A FIELD, and refusing it here was a drift
|
|
203
|
+
# between two halves of the same language: `where(status: "logged")`
|
|
204
|
+
# has always been legal on the same aggregate, because a lifecycle
|
|
205
|
+
# state is stored on the record like anything else — it is simply
|
|
206
|
+
# declared by `lifecycle :status` rather than by `attribute`.
|
|
207
|
+
#
|
|
208
|
+
# It is also the grouping anybody actually wants. "How are we doing"
|
|
209
|
+
# over a bug ledger IS the count per status, and a report that could
|
|
210
|
+
# group by every field EXCEPT that one could not answer the question
|
|
211
|
+
# reports exist for.
|
|
212
|
+
next if aggregate.lifecycle && aggregate.lifecycle.field.to_sym == field.to_sym
|
|
213
|
+
|
|
214
|
+
raise ArgumentError,
|
|
215
|
+
"#{model.name}'s group_by names #{field.inspect}, but #{target[:aggregate]} " \
|
|
216
|
+
"declares no such attribute (it declares #{aggregate.attributes.map(&:name).join(', ')})"
|
|
217
|
+
end
|
|
218
|
+
target
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# One level of nesting per field, in `group_by`'s own declared
|
|
222
|
+
# order — the leaf is the row with every grouped field removed
|
|
223
|
+
# (already spent, as the keys that reached it). ASSUMES the full
|
|
224
|
+
# `group_by` path uniquely identifies one row (true for grouping by
|
|
225
|
+
# an aggregate's own full identity, ConsoleSettings' own real use)
|
|
226
|
+
# — `leaves.first` silently keeps only the first row when several
|
|
227
|
+
# share the same full key path. A real, deliberate scope limit:
|
|
228
|
+
# true multi-row-per-leaf grouping would change the leaf shape
|
|
229
|
+
# from "one row" to "an array of rows", which no caller needs yet.
|
|
230
|
+
def nest(rows, fields)
|
|
231
|
+
field, *rest = fields
|
|
232
|
+
rows.group_by { |row| row[field] }.transform_values do |group|
|
|
233
|
+
# Strip ONLY the field just grouped by, not the whole remaining
|
|
234
|
+
# list — `rest`'s own fields have to survive into the recursive
|
|
235
|
+
# call below, or the NEXT level groups by a key that's already
|
|
236
|
+
# gone (found by trying it: a two-field group_by's own second
|
|
237
|
+
# level came back keyed `nil` for every group, every time).
|
|
238
|
+
stripped = group.map { |row| row.reject { |key, _| key == field } }
|
|
239
|
+
rest.empty? ? stripped.first : nest(stripped, rest)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# `count`/`median`'s own declared target — the SAME single
|
|
244
|
+
# many-side head `group_by_target` resolves, for the same reason
|
|
245
|
+
# (`seal_aggregation` already refuses zero or several many-side
|
|
246
|
+
# heads, and refuses count/median declared alongside group_by, so
|
|
247
|
+
# there is exactly one to name whenever this is asked at all).
|
|
248
|
+
# Raises loudly on a `median` field that doesn't exist, or exists
|
|
249
|
+
# but isn't numeric, rather than silently comparing garbage or
|
|
250
|
+
# averaging strings.
|
|
251
|
+
def aggregation_target(model, bluebook)
|
|
252
|
+
return nil unless model.count? || model.median_field
|
|
253
|
+
|
|
254
|
+
target = model.aggregate_heads.find { |head| head[:many] }
|
|
255
|
+
return target unless model.median_field
|
|
256
|
+
|
|
257
|
+
aggregate = bluebook.aggregate(target[:aggregate])
|
|
258
|
+
attribute = aggregate.attribute(model.median_field)
|
|
259
|
+
unless attribute
|
|
260
|
+
raise ArgumentError,
|
|
261
|
+
"#{model.name}'s median names #{model.median_field.inspect}, but #{target[:aggregate]} " \
|
|
262
|
+
"declares no such attribute (it declares #{aggregate.attributes.map(&:name).join(', ')})"
|
|
263
|
+
end
|
|
264
|
+
unless QuerySpecification::FieldPath.numeric?(attribute, []) { |type| aggregate.value_object(type) }
|
|
265
|
+
raise ArgumentError,
|
|
266
|
+
"#{model.name}'s median names #{model.median_field.inspect} on #{target[:aggregate]}, " \
|
|
267
|
+
"which is not numeric — median needs a numeric field (a bare number, or a " \
|
|
268
|
+
"value object carrying one)"
|
|
269
|
+
end
|
|
270
|
+
target
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# THE STANDARD DEFINITION. An ODD count's median is its one true
|
|
274
|
+
# middle value, sorted ; an EVEN count's median is the AVERAGE of
|
|
275
|
+
# its two middle values — the common convention (as opposed to,
|
|
276
|
+
# say, always taking the lower of the two), and the one this
|
|
277
|
+
# session's own task named explicitly as the deliberate choice.
|
|
278
|
+
# An EMPTY collection has no median: nil, not zero, so a caller
|
|
279
|
+
# cannot mistake "nothing to average" for "the values averaged to
|
|
280
|
+
# zero". Reuses `Ports::Query::InMemory`'s own field reading
|
|
281
|
+
# (`FieldPath.dig` + `comparable`) — the same unwrap `where`/
|
|
282
|
+
# `order_by` already give a value-object-carrying field, so
|
|
283
|
+
# `median :daily_limit` on a `DailyLimit{cents}` field reads the
|
|
284
|
+
# same scalar an `order_by :daily_limit` comparison already would.
|
|
285
|
+
def median(rows, field)
|
|
286
|
+
values = rows.map { |record| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(row(record), field)) }
|
|
287
|
+
.compact.sort
|
|
288
|
+
return nil if values.empty?
|
|
289
|
+
|
|
290
|
+
middle = values.length / 2
|
|
291
|
+
values.length.odd? ? values[middle] : (values[middle - 1] + values[middle]) / 2.0
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def fetch(bluebook, domain, aggregate_name, id)
|
|
295
|
+
@registry.read_repository(domain, bluebook.aggregate(aggregate_name)).find(id) ||
|
|
296
|
+
raise(NotFound, RefusalWording.render("NotFound", "read_model_reference_missing",
|
|
297
|
+
aggregate: aggregate_name, offered: Rendering.describe(id)))
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def records(bluebook, domain, aggregate_name)
|
|
301
|
+
aggregate = bluebook.aggregate(aggregate_name)
|
|
302
|
+
aggregate ? @registry.read_repository(domain, aggregate).all : []
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# A stored reference holds the target's id inside the REFERENCE
|
|
306
|
+
# ATTRIBUTE's own declared shape, so reading it is reading that shape —
|
|
307
|
+
# a different thing from the identity unwrap that was removed. An
|
|
308
|
+
# IDENTITY is declared as a path and followed (Runtime::Identity) ; a
|
|
309
|
+
# reference has no path of its own, and `Value.scalar` refuses a
|
|
310
|
+
# composite rather than guessing which field was meant.
|
|
311
|
+
#
|
|
312
|
+
# Storing the scalar itself would remove this reading altogether. That
|
|
313
|
+
# is a change to how references are STORED, not to how identities are
|
|
314
|
+
# declared, so it is not made here.
|
|
315
|
+
# An ask names itself where a command would name itself, and says the
|
|
316
|
+
# same thing about the same shape. `Value.refuse_object_reference` is
|
|
317
|
+
# not reused because it speaks of a COMMAND and its attribute ; a read
|
|
318
|
+
# model has a query name and one declared reference.
|
|
319
|
+
def refuse_object_reference(model, args)
|
|
320
|
+
offered = args.fetch(model.reference_name, nil)
|
|
321
|
+
return unless offered.is_a?(Hash) || offered.is_a?(Value)
|
|
322
|
+
|
|
323
|
+
raise TypeMismatch,
|
|
324
|
+
RefusalWording.render("TypeMismatch", "read_model_object_reference",
|
|
325
|
+
query: model.query_name, field: model.reference_name)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# A reference is the id, in the argument and in the stored row alike.
|
|
329
|
+
def reference(value) = value.to_s
|
|
330
|
+
|
|
331
|
+
def reference_fields(aggregate, target)
|
|
332
|
+
aggregate.attributes
|
|
333
|
+
.select { |attribute| attribute.reference? && attribute.type.target_name == target.to_s }
|
|
334
|
+
.map(&:name)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def matching(records) = records.select { |record| yield(record) }.sort_by(&:id)
|
|
338
|
+
def row(record) = record.to_h
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Runtime
|
|
3
|
+
# THE OUT-OF-BAND HALF OF `projects` (S12, ADR 0025 — "Consistency
|
|
4
|
+
# across aggregate boundaries"). A projected field is never written
|
|
5
|
+
# by the command that reads it — nothing at dispatch time takes a
|
|
6
|
+
# live cross-aggregate read the way `CommandRules::References
|
|
7
|
+
# #dereference` still does — so this is the one place a projected
|
|
8
|
+
# field's value actually gets copied over: walk every record of the
|
|
9
|
+
# OWNING aggregate, resolve each of its own `projected_fields`
|
|
10
|
+
# through the reference it names, and `save` the local copy.
|
|
11
|
+
#
|
|
12
|
+
# EXPLICIT AND CALLABLE, NOT AUTOMATIC — no on-boot detection of a
|
|
13
|
+
# freshly-declared `projects` with no held-era precedent, no
|
|
14
|
+
# generated `Policy#for_each` reaction keeping it live in real
|
|
15
|
+
# time as the target changes. Both are real extensions this same
|
|
16
|
+
# ADR section describes; both are deliberately deferred. This is
|
|
17
|
+
# the mechanism a caller reaches for by hand — `bin/rebuild
|
|
18
|
+
# Domain::Aggregate`, a scheduled job, whatever the deployment
|
|
19
|
+
# needs — proven to work end to end before either automatic
|
|
20
|
+
# trigger is built on top of it.
|
|
21
|
+
#
|
|
22
|
+
# NEEDS NO NEW ADAPTER CAPABILITY. `find`/`all`/`save` are the same
|
|
23
|
+
# three primitives every real adapter already answers identically
|
|
24
|
+
# (`Ports::Persistence::AppendOnly#save` — append, then project,
|
|
25
|
+
# the same for Memory/Postgres/SQLite/D1/Heki) — confirmed by
|
|
26
|
+
# direct reading before this was built, not assumed.
|
|
27
|
+
module RebuildSweep
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# One aggregate's own projected fields, refreshed across every
|
|
31
|
+
# record it holds. Returns how many records actually changed —
|
|
32
|
+
# `save` only runs when a projected value would differ from what
|
|
33
|
+
# is already stored, so re-running a sweep with nothing having
|
|
34
|
+
# moved on the target side touches the append log not at all.
|
|
35
|
+
def call(registry, domain, aggregate)
|
|
36
|
+
return 0 if aggregate.projected_fields.empty?
|
|
37
|
+
|
|
38
|
+
repository = registry.repository(domain, aggregate)
|
|
39
|
+
repository.all.count { |record| refresh(registry, domain, aggregate, record, repository) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def refresh(registry, domain, aggregate, record, repository)
|
|
43
|
+
changed = false
|
|
44
|
+
|
|
45
|
+
aggregate.projected_fields.each do |field|
|
|
46
|
+
value = remote_value(registry, domain, aggregate, record, field)
|
|
47
|
+
next if value.nil?
|
|
48
|
+
next if record.key?(field.name) && record[field.name] == value
|
|
49
|
+
|
|
50
|
+
record[field.name] = value
|
|
51
|
+
changed = true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
repository.save(record) if changed
|
|
55
|
+
changed
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# `nil` when the reference itself does not resolve in this
|
|
59
|
+
# chapter (a cross-domain target left "unfollowed" the same way
|
|
60
|
+
# References#dereference already leaves one) or when the record
|
|
61
|
+
# names no target at all — an optional reference nobody set.
|
|
62
|
+
def remote_value(registry, domain, aggregate, record, field)
|
|
63
|
+
target = aggregate.attribute(field.reference)&.type&.resolve
|
|
64
|
+
return nil unless target
|
|
65
|
+
|
|
66
|
+
id = record[field.reference]
|
|
67
|
+
return nil if id.nil?
|
|
68
|
+
|
|
69
|
+
remote = registry.repository(domain, target).find(id.to_s)
|
|
70
|
+
remote&.[](field.remote_field)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require "delegate"
|
|
2
|
+
require_relative "../query_specification/common/where_clause"
|
|
3
|
+
require_relative "../query_specification/common/options"
|
|
4
|
+
require_relative "../query_specification/hop_path"
|
|
5
|
+
require_relative "../ports/query"
|
|
6
|
+
require_relative "registry"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Runtime
|
|
10
|
+
# `TenantScope`'s sibling — a hop folds to a synthetic `in`
|
|
11
|
+
# where-clause the exact same way a tenant boundary folds to a
|
|
12
|
+
# synthetic `eq` one, so every engine that already reads `.wheres`
|
|
13
|
+
# answers a hop for free, with no per-engine code and no way for
|
|
14
|
+
# one engine to forget it.
|
|
15
|
+
#
|
|
16
|
+
# A hop's own filtering never happens here — RESOLVING one hop
|
|
17
|
+
# means running one ordinary, adapter-agnostic query against the
|
|
18
|
+
# hop's TARGET aggregate (through the same Ports::Query boundary
|
|
19
|
+
# any other query goes through), and folding the ids it answers
|
|
20
|
+
# back in as a local membership check. A multi-hop chain resolves
|
|
21
|
+
# from the FAR END inward: `fold` only ever peels off the head hop
|
|
22
|
+
# (QuerySpecification::HopPath.next_hop, the one-step primitive),
|
|
23
|
+
# and hands everything still left in the tail to a recursive
|
|
24
|
+
# `apply` call — so hop 2, hop 3, and so on each get resolved by
|
|
25
|
+
# their own ordinary query against their own target, one recursion
|
|
26
|
+
# level at a time, never by this module trying to see the whole
|
|
27
|
+
# chain at once. It terminates because each recursion is over a
|
|
28
|
+
# strictly shorter dotted string, and
|
|
29
|
+
# BluebookBuilder#validate_query_hops! already refused any chain
|
|
30
|
+
# deep enough to matter before this ever runs.
|
|
31
|
+
module ReferenceHop
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
def apply(declared, args, registry:, domain:, aggregate:)
|
|
35
|
+
hopped, local = declared.wheres.partition { |clause| QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes) }
|
|
36
|
+
return declared if hopped.empty?
|
|
37
|
+
|
|
38
|
+
folded = hopped.map { |clause| fold(clause, args, registry: registry, domain: domain, aggregate: aggregate) }
|
|
39
|
+
Folded.new(declared, local + folded)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def fold(clause, args, registry:, domain:, aggregate:)
|
|
43
|
+
step = QuerySpecification::HopPath.next_hop(clause.field, aggregate.attributes)
|
|
44
|
+
hop, rest = step
|
|
45
|
+
|
|
46
|
+
# A guarantee BluebookBuilder#validate_query_hops! already gave
|
|
47
|
+
# this exact clause before the domain ever booted. Held anyway:
|
|
48
|
+
# an unresolvable target folding silently to "matches
|
|
49
|
+
# everything" is precisely the failure shape this whole feature
|
|
50
|
+
# exists to close, and a hop that can no longer resolve (a
|
|
51
|
+
# target aggregate unloaded after boot, say) is a real fact
|
|
52
|
+
# worth a real error, not a query that quietly stops filtering.
|
|
53
|
+
unless hop&.target
|
|
54
|
+
raise WiringError,
|
|
55
|
+
"#{clause.field} hops through a reference this domain cannot resolve " \
|
|
56
|
+
"right now — the chapter seal already checked this once; something " \
|
|
57
|
+
"changed between then and this dispatch"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
inner = QuerySpecification::Common::WhereClause.new(field: rest, op: clause.op, value: clause.value)
|
|
61
|
+
ids = matching_ids(domain, hop.target, [inner], args, registry: registry)
|
|
62
|
+
|
|
63
|
+
QuerySpecification::Common::WhereClause.new(field: hop.attribute.name, op: "in", value: ids)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Every id the inner clause admits on the hop's TARGET — one
|
|
67
|
+
# whole, ordinary query against the target's own repository,
|
|
68
|
+
# through the very same Ports::Query boundary the outer ask
|
|
69
|
+
# uses, so a hop is answered by whatever engine the target
|
|
70
|
+
# aggregate is actually bound to (which may not be the engine
|
|
71
|
+
# the OUTER aggregate is bound to at all) rather than by a
|
|
72
|
+
# second reading of the comparators.
|
|
73
|
+
def matching_ids(domain, target, wheres, args, registry:)
|
|
74
|
+
spec = apply(QuerySpecification::Common::Options.new(wheres: wheres), args,
|
|
75
|
+
registry: registry, domain: domain, aggregate: target)
|
|
76
|
+
repository = registry.repository(domain, target)
|
|
77
|
+
rows = Ports::Query.execute(repository, spec, args, context: { domain: domain, aggregate: target }) ||
|
|
78
|
+
Ports::Query::InMemory.execute(repository.all, spec, args)
|
|
79
|
+
|
|
80
|
+
rows.map { |row| row.id.to_s }.uniq
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Never returned to a caller that might call an IR-level method
|
|
84
|
+
# (`to_h`, …) whose OWN internal `wheres` read would resolve
|
|
85
|
+
# against the original object, not this override — the exact
|
|
86
|
+
# caution TenantScope::Scoped's own comment gives, for the exact
|
|
87
|
+
# same reason: SimpleDelegator only intercepts calls made
|
|
88
|
+
# directly on the wrapper.
|
|
89
|
+
class Folded < SimpleDelegator
|
|
90
|
+
def initialize(declared, wheres)
|
|
91
|
+
super(declared)
|
|
92
|
+
@wheres = wheres
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def wheres = @wheres
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Runtime
|
|
3
|
+
# Every DomainRefusal wording that is not already data — `given`/
|
|
4
|
+
# `ensures`/a declared `invariant` already carry their own description,
|
|
5
|
+
# read at dispatch time off the command or value object that declared
|
|
6
|
+
# them. These are different in kind: LANGUAGE-LEVEL refusals, the same
|
|
7
|
+
# wording for every domain, not authored per-bluebook.
|
|
8
|
+
#
|
|
9
|
+
# Declared the same way in Vocabulary::RefusalTemplate
|
|
10
|
+
# (language/bluebook/vocabulary.bluebook) — spec/refusal_wording_
|
|
11
|
+
# conformance_spec holds this table equal to the language, both
|
|
12
|
+
# directions. Hand-typed rather than read live off the meta-domain at
|
|
13
|
+
# every dispatch, the same reason Expression::Evaluator::
|
|
14
|
+
# SIGN_TEST_OPERATORS is hand-typed beside Vocabulary::SignTest.
|
|
15
|
+
module RefusalWording
|
|
16
|
+
TEMPLATES = {
|
|
17
|
+
%w[NotFound creating_no_identity] =>
|
|
18
|
+
"{command} creates a {aggregate} — pass {identity}:",
|
|
19
|
+
%w[AlreadyExists creating_duplicate] =>
|
|
20
|
+
"{command} creates a {aggregate} that already exists — {identity} {offered}",
|
|
21
|
+
%w[AlreadyExists entity_duplicate] =>
|
|
22
|
+
"a {entity} already exists on {aggregate} — {identity} {offered}",
|
|
23
|
+
%w[NotFound acting_no_identity] =>
|
|
24
|
+
"{command} acts on an existing {aggregate} — pass {identity}:",
|
|
25
|
+
%w[NotFound record_missing] =>
|
|
26
|
+
"no {aggregate} with {identity} {offered}",
|
|
27
|
+
%w[NotFound entity_parent_no_identity] =>
|
|
28
|
+
"{command} acts on a {aggregate}'s {entity} — pass {identity}:",
|
|
29
|
+
%w[UnknownVerb entity_unknown] =>
|
|
30
|
+
"{aggregate} has no entity {entity}",
|
|
31
|
+
%w[NotFound entity_element_no_identity] =>
|
|
32
|
+
"{command} acts on one {entity} — pass {identity}:",
|
|
33
|
+
%w[NotFound entity_element_missing] =>
|
|
34
|
+
"no {entity} with {identity} {wants} on {aggregate} {parent_id}",
|
|
35
|
+
%w[NotFound reference_target_missing] =>
|
|
36
|
+
"no {target} with {heads} {key}",
|
|
37
|
+
%w[NotFound read_model_reference_missing] =>
|
|
38
|
+
"no {aggregate} with reference {offered}",
|
|
39
|
+
%w[TypeMismatch read_model_object_reference] =>
|
|
40
|
+
"{query} refused — a reference is an id, and {field} arrived as an object",
|
|
41
|
+
%w[UnknownVerb no_query] =>
|
|
42
|
+
"{aggregate} has no query {query}",
|
|
43
|
+
%w[UnknownVerb entity_query_missing] =>
|
|
44
|
+
"{entity} has no query {query}",
|
|
45
|
+
%w[UnknownVerb entity_holds_no_list] =>
|
|
46
|
+
"{aggregate} holds no list of {entity}",
|
|
47
|
+
%w[UnknownVerb entity_no_command] =>
|
|
48
|
+
"{entity} has no command {command}",
|
|
49
|
+
%w[UnknownVerb aggregate_no_command] =>
|
|
50
|
+
"{aggregate} has no command {command}",
|
|
51
|
+
%w[UnknownVerb port_no_operation] =>
|
|
52
|
+
"{port} has no operation {operation}",
|
|
53
|
+
%w[UnknownVerb no_domain] =>
|
|
54
|
+
"no domain {domain} loaded (verb {verb})",
|
|
55
|
+
%w[UnknownVerb no_read_model] =>
|
|
56
|
+
"{domain} has no read model {query}",
|
|
57
|
+
%w[UnknownVerb not_fully_qualified] =>
|
|
58
|
+
"{verb} is not a fully-qualified verb (Domain::Aggregate.Command)",
|
|
59
|
+
%w[UnknownVerb no_aggregate] =>
|
|
60
|
+
"{domain} has no aggregate {aggregate}",
|
|
61
|
+
%w[LifecycleRefused transition_blocked] =>
|
|
62
|
+
"{command} refused — {field} is {current}, and {command} moves it only from {allowed}",
|
|
63
|
+
%w[TypeMismatch value_object_shape] =>
|
|
64
|
+
"{name} is a {type} — pass its fields as an object, not {offered}",
|
|
65
|
+
%w[TypeMismatch reference_as_object] =>
|
|
66
|
+
"{command} refused — a reference is an id, and {attribute} arrived as an object{known_by}",
|
|
67
|
+
%w[TypeMismatch multi_field_scalar] =>
|
|
68
|
+
"{type} has multiple fields and cannot stand in for a scalar",
|
|
69
|
+
%w[TypeMismatch composite_identity] =>
|
|
70
|
+
"{type} is a composite identity — an identity must have exactly one field",
|
|
71
|
+
%w[TypeMismatch numeric_field] =>
|
|
72
|
+
"{type}.{field} expects {expected}, got {offered}",
|
|
73
|
+
%w[TypeMismatch non_finite_field] =>
|
|
74
|
+
"{type}.{field} must be a finite number, got {offered}",
|
|
75
|
+
%w[TypeMismatch pattern_mismatch] =>
|
|
76
|
+
"{type}.{field} must match {pattern}, got {offered}",
|
|
77
|
+
%w[TypeMismatch arithmetic_amount] =>
|
|
78
|
+
"{op} of {target} needs an Integer, got {offered}",
|
|
79
|
+
%w[TypeMismatch arithmetic_current] =>
|
|
80
|
+
"{op} of {target} needs an Integer {target}, got {offered}",
|
|
81
|
+
%w[TypeMismatch arithmetic_shared_field] =>
|
|
82
|
+
"{op} of {target} needs a value object with one shared Integer field",
|
|
83
|
+
%w[UnknownArgument unknown_args] =>
|
|
84
|
+
"{command} does not declare {unknown} — it takes {declared}",
|
|
85
|
+
%w[AbsentArgument absent_args] =>
|
|
86
|
+
"{command} was not given {absent} — it takes {declared}",
|
|
87
|
+
%w[InvariantViolation closed_set_member] =>
|
|
88
|
+
"{type} admits {admitted} — got {offered}",
|
|
89
|
+
%w[InvariantViolation value_object_invariant] =>
|
|
90
|
+
"{name} invariant violated — {description} (given {offered})",
|
|
91
|
+
%w[InvariantViolation admits_declared_set] =>
|
|
92
|
+
"{name} admits {admits} — {admitted} — got {offered}",
|
|
93
|
+
%w[InvariantViolation undeclared_set] =>
|
|
94
|
+
"{name} admits {admits}, which this chapter does not declare — a closed set is named " \
|
|
95
|
+
"Aggregate::SetName, and it must be one the bluebook actually holds",
|
|
96
|
+
%w[Unauthorized tenant_required] =>
|
|
97
|
+
"{query} declares authorize with tenant: {field} — pass {field}: to name which " \
|
|
98
|
+
"{field} this ask is scoped to",
|
|
99
|
+
%w[Unauthorized role_mismatch] =>
|
|
100
|
+
"{command} refused — role: {role}, and the caller stated {caller_role}",
|
|
101
|
+
%w[AttributeAbsent absent_read] =>
|
|
102
|
+
"{aggregate} {field} is absent on this record — declared, not optional, and " \
|
|
103
|
+
"added since it was written. Backfill it in a translation (backfill :{field}, " \
|
|
104
|
+
"default: ...), or declare it optional: true",
|
|
105
|
+
%w[ProjectionAbsent absent_read] =>
|
|
106
|
+
"{aggregate} {field} is not yet projected on this record — declared via " \
|
|
107
|
+
"projects :{field}, but no rebuild sweep has populated it. Run the sweep, " \
|
|
108
|
+
"or read {reference}.{remote_field} directly if this rule cannot wait"
|
|
109
|
+
}.freeze
|
|
110
|
+
|
|
111
|
+
module_function
|
|
112
|
+
|
|
113
|
+
# Plain text substitution, never expression syntax — a template is
|
|
114
|
+
# read, not evaluated. `render` computes the placeholder VALUES via
|
|
115
|
+
# whatever the call site already had (a joined list, a rendered
|
|
116
|
+
# identity reading, …) and this only replaces the markers.
|
|
117
|
+
def render(refusal, site, **values)
|
|
118
|
+
template = TEMPLATES.fetch([refusal, site]) do
|
|
119
|
+
raise KeyError, "no refusal template for #{refusal}/#{site} — declare it in " \
|
|
120
|
+
"Vocabulary::RefusalTemplate first"
|
|
121
|
+
end
|
|
122
|
+
values.reduce(template) { |text, (key, value)| text.gsub("{#{key}}", value.to_s) }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|