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,245 @@
|
|
|
1
|
+
require_relative "../naming"
|
|
2
|
+
require_relative "errors"
|
|
3
|
+
require_relative "identity"
|
|
4
|
+
require_relative "value"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Runtime
|
|
8
|
+
# Turns facts selected by a policy or process manager into the same
|
|
9
|
+
# receiver/payload envelope an outside caller uses. Reaction declarations
|
|
10
|
+
# historically selected both through one `with:` map, so this is the one
|
|
11
|
+
# compatibility seam that separates receiver identities from facts after
|
|
12
|
+
# resolving the declaration and before re-entering the dispatcher.
|
|
13
|
+
module ReactionInvocation
|
|
14
|
+
Target = Struct.new(:aggregate, :entities, :command, keyword_init: true)
|
|
15
|
+
# :facts, not :values — Struct.new already defines #values (every
|
|
16
|
+
# member's own value, in order); naming this field :values silently
|
|
17
|
+
# shadowed that with the single Hash it actually holds.
|
|
18
|
+
Scope = Struct.new(:name, :facts, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# The holding IR historically represented both an omitted projection and
|
|
23
|
+
# an explicitly empty `with: {}` as the same empty array. Builders now
|
|
24
|
+
# preserve declaration presence off-wire; reconstructed/legacy IR falls
|
|
25
|
+
# back to the old non-empty reading.
|
|
26
|
+
def projection_declared?(declaration)
|
|
27
|
+
if declaration.instance_variable_defined?(:@projection_declared)
|
|
28
|
+
declaration.instance_variable_get(:@projection_declared)
|
|
29
|
+
else
|
|
30
|
+
!declaration.with_spec.to_a.empty?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# A reaction's source names resolve lexically, not globally. Policies
|
|
35
|
+
# supply one event/row scope. Process managers supply current event then
|
|
36
|
+
# opening-event memory, while correlation is an explicit binding ahead
|
|
37
|
+
# of both. Missing names are refused here rather than materialized as nil
|
|
38
|
+
# and accidentally presented as target command facts.
|
|
39
|
+
def resolve_mapping(with_spec:, scopes:, bindings: {}, label: "reaction")
|
|
40
|
+
normalized_bindings = bindings.transform_keys(&:to_sym)
|
|
41
|
+
normalized_scopes = scopes.map do |scope|
|
|
42
|
+
scope = Scope.new(name: scope.first, facts: scope.last) unless scope.is_a?(Scope)
|
|
43
|
+
Scope.new(name: scope.name.to_s, facts: scope.facts.transform_keys(&:to_sym))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
with_spec.to_h do |key, source|
|
|
47
|
+
value = if !source.is_a?(Symbol)
|
|
48
|
+
source
|
|
49
|
+
elsif normalized_bindings.key?(source)
|
|
50
|
+
normalized_bindings.fetch(source)
|
|
51
|
+
else
|
|
52
|
+
visible = normalized_scopes.find { |scope| scope.facts.key?(source) }
|
|
53
|
+
unless visible
|
|
54
|
+
names = normalized_scopes.map(&:name).join(" then ")
|
|
55
|
+
raise UnknownArgument,
|
|
56
|
+
"#{label}'s with: reads :#{source}, which is not visible in #{names}"
|
|
57
|
+
end
|
|
58
|
+
visible.facts.fetch(source)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
[key.to_sym, Value.materialize(value)]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A reaction without an explicit projection retains wholesale legacy
|
|
66
|
+
# forwarding. Choosing `with:` opts into the strict envelope: only target
|
|
67
|
+
# command attributes enter `with:`, while identities become `to:`.
|
|
68
|
+
def build(registry:, verb:, projected:, explicit:, passthrough: [], source_receiver: nil)
|
|
69
|
+
args = projected.transform_keys(&:to_sym)
|
|
70
|
+
unless explicit
|
|
71
|
+
return args unless source_receiver
|
|
72
|
+
|
|
73
|
+
# Compatibility calls still belong to Dispatcher when the target is
|
|
74
|
+
# absent: it owns the canonical UnknownVerb wording. Resolution here
|
|
75
|
+
# is only an optional opportunity to lift same-aggregate Event.id.
|
|
76
|
+
begin
|
|
77
|
+
target = resolve_target(registry, verb)
|
|
78
|
+
rescue UnknownVerb
|
|
79
|
+
return args
|
|
80
|
+
end
|
|
81
|
+
inherited_receiver = source_receiver_for(target, source_receiver)
|
|
82
|
+
return inherited_receiver ? args.merge(to: inherited_receiver) : args
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
target = resolve_target(registry, verb)
|
|
86
|
+
inherited_receiver = source_receiver_for(target, source_receiver)
|
|
87
|
+
facts = command_facts(target.command, args)
|
|
88
|
+
consumed = facts.keys
|
|
89
|
+
|
|
90
|
+
if target.entities.empty? && target.command.creates?
|
|
91
|
+
refuse_unconsumed!(target.command, args, consumed, passthrough)
|
|
92
|
+
return { with: facts }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
aggregate_identity, aggregate_keys = identity_for(
|
|
96
|
+
target.aggregate,
|
|
97
|
+
args,
|
|
98
|
+
aliases: aggregate_aliases(target),
|
|
99
|
+
value_owner: target.aggregate
|
|
100
|
+
)
|
|
101
|
+
aggregate_identity ||= inherited_receiver
|
|
102
|
+
require_identity!(verb, target.aggregate, aggregate_identity)
|
|
103
|
+
consumed.concat(aggregate_keys)
|
|
104
|
+
|
|
105
|
+
entity_identities = target.entities.map do |entity|
|
|
106
|
+
identity, keys = identity_for(
|
|
107
|
+
entity,
|
|
108
|
+
args,
|
|
109
|
+
aliases: [Naming.reference_key(entity.hecks_name)],
|
|
110
|
+
value_owner: target.aggregate
|
|
111
|
+
)
|
|
112
|
+
require_identity!(verb, entity, identity)
|
|
113
|
+
consumed.concat(keys)
|
|
114
|
+
identity
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
refuse_unconsumed!(target.command, args, consumed, passthrough)
|
|
118
|
+
|
|
119
|
+
route = if entity_identities.empty?
|
|
120
|
+
aggregate_identity
|
|
121
|
+
else
|
|
122
|
+
{ aggregate: aggregate_identity, entities: entity_identities }
|
|
123
|
+
end
|
|
124
|
+
{ to: route, with: facts }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def resolve_target(registry, verb)
|
|
128
|
+
domain, aggregate_name, command_path = Naming.split_verb(verb)
|
|
129
|
+
raise UnknownVerb, "reaction target #{verb.inspect} is not a qualified command" unless command_path
|
|
130
|
+
|
|
131
|
+
bluebook = registry.bluebook(domain)
|
|
132
|
+
aggregate = bluebook&.aggregate(aggregate_name)
|
|
133
|
+
raise UnknownVerb, "reaction target #{verb.inspect} does not resolve to an aggregate" unless aggregate
|
|
134
|
+
|
|
135
|
+
*entity_names, command_name = command_path.split(".")
|
|
136
|
+
owner = aggregate
|
|
137
|
+
entities = entity_names.map do |entity_name|
|
|
138
|
+
entity = owner.entities.find { |candidate| candidate.hecks_name == entity_name }
|
|
139
|
+
raise UnknownVerb, "reaction target #{verb.inspect} does not resolve entity #{entity_name.inspect}" unless entity
|
|
140
|
+
|
|
141
|
+
owner = entity
|
|
142
|
+
entity
|
|
143
|
+
end
|
|
144
|
+
command = owner.command(command_name)
|
|
145
|
+
raise UnknownVerb, "reaction target #{verb.inspect} does not resolve to a command" unless command
|
|
146
|
+
|
|
147
|
+
Target.new(aggregate: aggregate, entities: entities, command: command)
|
|
148
|
+
end
|
|
149
|
+
private_class_method :resolve_target
|
|
150
|
+
|
|
151
|
+
def command_facts(command, args)
|
|
152
|
+
declared = command.attributes.map { |attribute| attribute.name.to_sym }
|
|
153
|
+
args.slice(*declared)
|
|
154
|
+
end
|
|
155
|
+
private_class_method :command_facts
|
|
156
|
+
|
|
157
|
+
def aggregate_aliases(target)
|
|
158
|
+
aliases = [:aggregate, Naming.reference_key(target.aggregate.name)]
|
|
159
|
+
aliases.unshift(target.command.addressing_key_for(target.aggregate.name)) if target.entities.empty?
|
|
160
|
+
aliases.compact.uniq
|
|
161
|
+
end
|
|
162
|
+
private_class_method :aggregate_aliases
|
|
163
|
+
|
|
164
|
+
# Event.id names the aggregate that emitted the event. It can therefore
|
|
165
|
+
# supply only the receiver of a non-creating command on that SAME root;
|
|
166
|
+
# it cannot address another aggregate, invent an entity identity, or turn
|
|
167
|
+
# a creation into a mutation. An explicit projected receiver is resolved
|
|
168
|
+
# first and remains authoritative.
|
|
169
|
+
def source_receiver_for(target, source_receiver)
|
|
170
|
+
return nil unless source_receiver
|
|
171
|
+
return nil unless target.entities.empty?
|
|
172
|
+
return nil if target.command.creates?
|
|
173
|
+
|
|
174
|
+
source = source_receiver.transform_keys(&:to_sym)
|
|
175
|
+
source_aggregate = source[:aggregate].to_s
|
|
176
|
+
same_aggregate = source_aggregate == if source_aggregate.include?("::")
|
|
177
|
+
target.aggregate.hecks_fqn
|
|
178
|
+
else
|
|
179
|
+
target.aggregate.hecks_name
|
|
180
|
+
end
|
|
181
|
+
return nil unless same_aggregate
|
|
182
|
+
|
|
183
|
+
identity = source[:identity]
|
|
184
|
+
identity.to_s unless identity.nil? || identity.to_s.empty?
|
|
185
|
+
end
|
|
186
|
+
private_class_method :source_receiver_for
|
|
187
|
+
|
|
188
|
+
def identity_for(construct, args, aliases:, value_owner:)
|
|
189
|
+
identity = Identity.of(construct, args, value_owner: value_owner)
|
|
190
|
+
return [identity, construct.identity_heads] if identity
|
|
191
|
+
|
|
192
|
+
aliases.each do |key|
|
|
193
|
+
key = key.to_sym
|
|
194
|
+
next unless args.key?(key)
|
|
195
|
+
|
|
196
|
+
identity = identity_from_alias(construct, args[key], value_owner)
|
|
197
|
+
return [identity, [key]] if identity
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
return [args[:id].to_s, [:id]] if args.key?(:id) && !args[:id].to_s.empty?
|
|
201
|
+
|
|
202
|
+
[nil, []]
|
|
203
|
+
end
|
|
204
|
+
private_class_method :identity_for
|
|
205
|
+
|
|
206
|
+
def identity_from_alias(construct, held, value_owner)
|
|
207
|
+
materialized = Value.materialize(held)
|
|
208
|
+
if materialized.is_a?(Hash)
|
|
209
|
+
keyed = materialized.transform_keys(&:to_sym)
|
|
210
|
+
nested = Identity.of(construct, keyed, value_owner: value_owner)
|
|
211
|
+
return nested if nested
|
|
212
|
+
|
|
213
|
+
if construct.identity_paths.one?
|
|
214
|
+
scalar = Identity.scalar(construct.identity_paths.first, keyed)
|
|
215
|
+
return scalar.to_s unless scalar.nil? || scalar.to_s.empty?
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
return nil
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
materialized.to_s unless materialized.nil? || materialized.to_s.empty?
|
|
222
|
+
end
|
|
223
|
+
private_class_method :identity_from_alias
|
|
224
|
+
|
|
225
|
+
def require_identity!(verb, construct, identity)
|
|
226
|
+
return if identity
|
|
227
|
+
|
|
228
|
+
raise TypeMismatch,
|
|
229
|
+
"reaction target #{verb} needs #{construct.hecks_name}'s receiver identity outside command facts"
|
|
230
|
+
end
|
|
231
|
+
private_class_method :require_identity!
|
|
232
|
+
|
|
233
|
+
def refuse_unconsumed!(command, args, consumed, passthrough)
|
|
234
|
+
allowed = consumed.map(&:to_sym) + Array(passthrough).map(&:to_sym)
|
|
235
|
+
unknown = args.keys - allowed
|
|
236
|
+
return if unknown.empty?
|
|
237
|
+
|
|
238
|
+
raise UnknownArgument,
|
|
239
|
+
"#{command.hecks_name} reaction projection contains neither receiver identity nor declared command facts: " \
|
|
240
|
+
"#{unknown.sort.join(', ')}"
|
|
241
|
+
end
|
|
242
|
+
private_class_method :refuse_unconsumed!
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,266 @@
|
|
|
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 + 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
|
+
# `group_by`'s own declared fields, checked against the ONE
|
|
116
|
+
# many-side head they apply to (`seal_group_by` already refuses
|
|
117
|
+
# zero or several) — resolved here, once, rather than re-derived
|
|
118
|
+
# per row. Raises loudly on a typo'd field name rather than
|
|
119
|
+
# silently grouping every row into one bucket keyed `nil`.
|
|
120
|
+
def group_by_target(model, bluebook)
|
|
121
|
+
return nil unless model.group_by.any?
|
|
122
|
+
|
|
123
|
+
target = model.aggregate_heads.find { |head| head[:many] }
|
|
124
|
+
aggregate = bluebook.aggregate(target[:aggregate])
|
|
125
|
+
model.group_by_fields.each do |field|
|
|
126
|
+
next if aggregate.attribute(field)
|
|
127
|
+
# THE LIFECYCLE FIELD IS A FIELD, and refusing it here was a drift
|
|
128
|
+
# between two halves of the same language: `where(status: "logged")`
|
|
129
|
+
# has always been legal on the same aggregate, because a lifecycle
|
|
130
|
+
# state is stored on the record like anything else — it is simply
|
|
131
|
+
# declared by `lifecycle :status` rather than by `attribute`.
|
|
132
|
+
#
|
|
133
|
+
# It is also the grouping anybody actually wants. "How are we doing"
|
|
134
|
+
# over a bug ledger IS the count per status, and a report that could
|
|
135
|
+
# group by every field EXCEPT that one could not answer the question
|
|
136
|
+
# reports exist for.
|
|
137
|
+
next if aggregate.lifecycle && aggregate.lifecycle.field.to_sym == field.to_sym
|
|
138
|
+
|
|
139
|
+
raise ArgumentError,
|
|
140
|
+
"#{model.name}'s group_by names #{field.inspect}, but #{target[:aggregate]} " \
|
|
141
|
+
"declares no such attribute (it declares #{aggregate.attributes.map(&:name).join(', ')})"
|
|
142
|
+
end
|
|
143
|
+
target
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# One level of nesting per field, in `group_by`'s own declared
|
|
147
|
+
# order — the leaf is the row with every grouped field removed
|
|
148
|
+
# (already spent, as the keys that reached it). ASSUMES the full
|
|
149
|
+
# `group_by` path uniquely identifies one row (true for grouping by
|
|
150
|
+
# an aggregate's own full identity, ConsoleSettings' own real use)
|
|
151
|
+
# — `leaves.first` silently keeps only the first row when several
|
|
152
|
+
# share the same full key path. A real, deliberate scope limit:
|
|
153
|
+
# true multi-row-per-leaf grouping would change the leaf shape
|
|
154
|
+
# from "one row" to "an array of rows", which no caller needs yet.
|
|
155
|
+
def nest(rows, fields)
|
|
156
|
+
field, *rest = fields
|
|
157
|
+
rows.group_by { |row| row[field] }.transform_values do |group|
|
|
158
|
+
# Strip ONLY the field just grouped by, not the whole remaining
|
|
159
|
+
# list — `rest`'s own fields have to survive into the recursive
|
|
160
|
+
# call below, or the NEXT level groups by a key that's already
|
|
161
|
+
# gone (found by trying it: a two-field group_by's own second
|
|
162
|
+
# level came back keyed `nil` for every group, every time).
|
|
163
|
+
stripped = group.map { |row| row.reject { |key, _| key == field } }
|
|
164
|
+
rest.empty? ? stripped.first : nest(stripped, rest)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# `count`/`median`'s own declared target — the SAME single
|
|
169
|
+
# many-side head `group_by_target` resolves, for the same reason
|
|
170
|
+
# (`seal_aggregation` already refuses zero or several many-side
|
|
171
|
+
# heads, and refuses count/median declared alongside group_by, so
|
|
172
|
+
# there is exactly one to name whenever this is asked at all).
|
|
173
|
+
# Raises loudly on a `median` field that doesn't exist, or exists
|
|
174
|
+
# but isn't numeric, rather than silently comparing garbage or
|
|
175
|
+
# averaging strings.
|
|
176
|
+
def aggregation_target(model, bluebook)
|
|
177
|
+
return nil unless model.count? || model.median_field
|
|
178
|
+
|
|
179
|
+
target = model.aggregate_heads.find { |head| head[:many] }
|
|
180
|
+
return target unless model.median_field
|
|
181
|
+
|
|
182
|
+
aggregate = bluebook.aggregate(target[:aggregate])
|
|
183
|
+
attribute = aggregate.attribute(model.median_field)
|
|
184
|
+
unless attribute
|
|
185
|
+
raise ArgumentError,
|
|
186
|
+
"#{model.name}'s median names #{model.median_field.inspect}, but #{target[:aggregate]} " \
|
|
187
|
+
"declares no such attribute (it declares #{aggregate.attributes.map(&:name).join(', ')})"
|
|
188
|
+
end
|
|
189
|
+
unless QuerySpecification::FieldPath.numeric?(attribute, []) { |type| aggregate.value_object(type) }
|
|
190
|
+
raise ArgumentError,
|
|
191
|
+
"#{model.name}'s median names #{model.median_field.inspect} on #{target[:aggregate]}, " \
|
|
192
|
+
"which is not numeric — median needs a numeric field (a bare number, or a " \
|
|
193
|
+
"value object carrying one)"
|
|
194
|
+
end
|
|
195
|
+
target
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# THE STANDARD DEFINITION. An ODD count's median is its one true
|
|
199
|
+
# middle value, sorted ; an EVEN count's median is the AVERAGE of
|
|
200
|
+
# its two middle values — the common convention (as opposed to,
|
|
201
|
+
# say, always taking the lower of the two), and the one this
|
|
202
|
+
# session's own task named explicitly as the deliberate choice.
|
|
203
|
+
# An EMPTY collection has no median: nil, not zero, so a caller
|
|
204
|
+
# cannot mistake "nothing to average" for "the values averaged to
|
|
205
|
+
# zero". Reuses `Ports::Query::InMemory`'s own field reading
|
|
206
|
+
# (`FieldPath.dig` + `comparable`) — the same unwrap `where`/
|
|
207
|
+
# `order_by` already give a value-object-carrying field, so
|
|
208
|
+
# `median :daily_limit` on a `DailyLimit{cents}` field reads the
|
|
209
|
+
# same scalar an `order_by :daily_limit` comparison already would.
|
|
210
|
+
def median(rows, field)
|
|
211
|
+
values = rows.map { |record| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(row(record), field)) }
|
|
212
|
+
.compact.sort
|
|
213
|
+
return nil if values.empty?
|
|
214
|
+
|
|
215
|
+
middle = values.length / 2
|
|
216
|
+
values.length.odd? ? values[middle] : (values[middle - 1] + values[middle]) / 2.0
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def fetch(bluebook, domain, aggregate_name, id)
|
|
220
|
+
@registry.read_repository(domain, bluebook.aggregate(aggregate_name)).find(id) ||
|
|
221
|
+
raise(NotFound, RefusalWording.render("NotFound", "read_model_reference_missing",
|
|
222
|
+
aggregate: aggregate_name, offered: Rendering.describe(id)))
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def records(bluebook, domain, aggregate_name)
|
|
226
|
+
aggregate = bluebook.aggregate(aggregate_name)
|
|
227
|
+
aggregate ? @registry.read_repository(domain, aggregate).all : []
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# A stored reference holds the target's id inside the REFERENCE
|
|
231
|
+
# ATTRIBUTE's own declared shape, so reading it is reading that shape —
|
|
232
|
+
# a different thing from the identity unwrap that was removed. An
|
|
233
|
+
# IDENTITY is declared as a path and followed (Runtime::Identity) ; a
|
|
234
|
+
# reference has no path of its own, and `Value.scalar` refuses a
|
|
235
|
+
# composite rather than guessing which field was meant.
|
|
236
|
+
#
|
|
237
|
+
# Storing the scalar itself would remove this reading altogether. That
|
|
238
|
+
# is a change to how references are STORED, not to how identities are
|
|
239
|
+
# declared, so it is not made here.
|
|
240
|
+
# An ask names itself where a command would name itself, and says the
|
|
241
|
+
# same thing about the same shape. `Value.refuse_object_reference` is
|
|
242
|
+
# not reused because it speaks of a COMMAND and its attribute ; a read
|
|
243
|
+
# model has a query name and one declared reference.
|
|
244
|
+
def refuse_object_reference(model, args)
|
|
245
|
+
offered = args.fetch(model.reference_name, nil)
|
|
246
|
+
return unless offered.is_a?(Hash) || offered.is_a?(Value)
|
|
247
|
+
|
|
248
|
+
raise TypeMismatch,
|
|
249
|
+
RefusalWording.render("TypeMismatch", "read_model_object_reference",
|
|
250
|
+
query: model.query_name, field: model.reference_name)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# A reference is the id, in the argument and in the stored row alike.
|
|
254
|
+
def reference(value) = value.to_s
|
|
255
|
+
|
|
256
|
+
def reference_fields(aggregate, target)
|
|
257
|
+
aggregate.attributes
|
|
258
|
+
.select { |attribute| attribute.reference? && attribute.type.target_name == target.to_s }
|
|
259
|
+
.map(&:name)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def matching(records) = records.select { |record| yield(record) }.sort_by(&:id)
|
|
263
|
+
def row(record) = record.to_h
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
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
|