hecks 2.7.0 → 2.9.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 +4 -4
- data/exe/hecks +11 -0
- data/lib/hecks/adapters/driven/claude_code.rb +4 -104
- data/lib/hecks/adapters/driven/d1.rb +44 -237
- data/lib/hecks/adapters/driven/folder.rb +17 -76
- data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
- data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
- data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
- data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
- data/lib/hecks/adapters/driven/heki.rb +13 -42
- data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
- data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
- data/lib/hecks/adapters/driven/lambda.rb +10 -84
- data/lib/hecks/adapters/driven/local_storage.rb +5 -40
- data/lib/hecks/adapters/driven/memory.rb +18 -99
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
- data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
- data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
- data/lib/hecks/adapters/driven/postgres.rb +145 -134
- data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
- data/lib/hecks/adapters/driven/prism.rb +7 -30
- data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
- data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
- data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
- data/lib/hecks/adapters/driven/sqlite.rb +128 -65
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +15 -37
- data/lib/hecks/adapters/driven.rb +5 -36
- data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
- data/lib/hecks/behaviors/dsl.rb +7 -25
- data/lib/hecks/behaviors/expectations.rb +32 -142
- data/lib/hecks/behaviors/ir.rb +7 -20
- data/lib/hecks/behaviors/rspec.rb +6 -17
- data/lib/hecks/behaviors/runner.rb +10 -34
- data/lib/hecks/behaviors.rb +4 -8
- data/lib/hecks/bluebook/aggregate.rb +9 -75
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
- data/lib/hecks/bluebook/assembly/build.rb +4 -15
- data/lib/hecks/bluebook/assembly/contract.rb +15 -136
- data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
- data/lib/hecks/bluebook/assembly/marks.rb +31 -223
- data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
- data/lib/hecks/bluebook/assembly.rb +6 -45
- data/lib/hecks/bluebook/attribute.rb +12 -61
- data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
- data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
- data/lib/hecks/bluebook/behaviour/chapter.rb +17 -42
- data/lib/hecks/bluebook/behaviour/command.rb +14 -66
- data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
- data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
- data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
- data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
- data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
- data/lib/hecks/bluebook/behaviour/read_model.rb +38 -26
- data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
- data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
- data/lib/hecks/bluebook/capabilities.rb +17 -49
- data/lib/hecks/bluebook/chapter.rb +10 -48
- data/lib/hecks/bluebook/command.rb +23 -81
- data/lib/hecks/bluebook/domain_port.rb +14 -63
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +17 -65
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
- data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
- data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -189
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
- data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
- data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
- data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
- data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
- data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
- data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
- data/lib/hecks/bluebook/dsl/world_builder.rb +19 -106
- data/lib/hecks/bluebook/entity.rb +13 -71
- data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
- data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
- data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
- data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
- data/lib/hecks/bluebook/expression/resolver.rb +37 -660
- data/lib/hecks/bluebook/hexagon.rb +5 -28
- data/lib/hecks/bluebook/lifecycle.rb +5 -16
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
- data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
- data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
- data/lib/hecks/bluebook/meta_validator/shapes.rb +54 -268
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -308
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
- data/lib/hecks/bluebook/meta_validator.rb +79 -349
- data/lib/hecks/bluebook/model_check/client_profile.rb +10 -91
- data/lib/hecks/bluebook/model_check.rb +102 -600
- data/lib/hecks/bluebook/pattern_subset.rb +11 -69
- data/lib/hecks/bluebook/process_manager.rb +13 -83
- data/lib/hecks/bluebook/project_loader.rb +3 -39
- data/lib/hecks/bluebook/project_register.rb +12 -60
- data/lib/hecks/bluebook/query.rb +9 -48
- data/lib/hecks/bluebook/read_model.rb +57 -81
- data/lib/hecks/bluebook/reference.rb +7 -41
- data/lib/hecks/bluebook/smoke_test.rb +8 -98
- data/lib/hecks/bluebook/synthesizer.rb +13 -62
- data/lib/hecks/bluebook/translation.rb +26 -62
- data/lib/hecks/bluebook/value_object.rb +11 -36
- data/lib/hecks/bluebook.rb +5 -22
- data/lib/hecks/cache_dir.rb +84 -0
- data/lib/hecks/cli/document.rb +86 -0
- data/lib/hecks/cli/ir.rb +45 -0
- data/lib/hecks/cli/mcp.rb +31 -0
- data/lib/hecks/cli/mcp_door.rb +366 -0
- data/lib/hecks/cli/model_check.rb +203 -0
- data/lib/hecks/cli/project_cli.rb +90 -0
- data/lib/hecks/cli/project_diagrams.rb +36 -0
- data/lib/hecks/cli/run.rb +173 -0
- data/lib/hecks/cli/smoke_test.rb +45 -0
- data/lib/hecks/cli/stores.rb +53 -0
- data/lib/hecks/cli.rb +160 -0
- data/lib/hecks/construct.rb +8 -34
- data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
- data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
- data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
- data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
- data/lib/hecks/embryonaut_bluebook.rb +4 -71
- data/lib/hecks/facade/cli_door.rb +24 -106
- data/lib/hecks/facade/cli_runner.rb +28 -172
- data/lib/hecks/facade/command_request.rb +13 -33
- data/lib/hecks/facade/handle.rb +34 -121
- data/lib/hecks/facade/json_door.rb +31 -146
- data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
- data/lib/hecks/facade/surface/chapter.rb +10 -54
- data/lib/hecks/facade/surface.rb +8 -23
- data/lib/hecks/facade.rb +10 -25
- data/lib/hecks/forms/app.rb +15 -70
- data/lib/hecks/forms/command_form_renderer.rb +10 -29
- data/lib/hecks/forms/field_renderer.rb +17 -156
- data/lib/hecks/forms/field_shape.rb +30 -222
- data/lib/hecks/forms/html.rb +12 -47
- data/lib/hecks/forms/index_renderer.rb +1 -4
- data/lib/hecks/forms/page.rb +3 -9
- data/lib/hecks/forms/params.rb +14 -60
- data/lib/hecks/forms/port_argument.rb +2 -19
- data/lib/hecks/forms/query_form_renderer.rb +4 -79
- data/lib/hecks/forms/record_renderer.rb +8 -75
- data/lib/hecks/forms/record_table.rb +7 -50
- data/lib/hecks/forms/reference_options.rb +3 -30
- data/lib/hecks/forms/value_object_shape.rb +8 -35
- data/lib/hecks/forms.rb +5 -33
- data/lib/hecks/fqn.rb +5 -12
- data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
- data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
- data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
- data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
- data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
- data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
- data/lib/hecks/framework.rb +11 -80
- data/lib/hecks/freezer.rb +5 -39
- data/lib/hecks/grammar/expression.bluebook +15 -40
- data/lib/hecks/grammar/translation.bluebook +3 -6
- data/lib/hecks/grammar.rb +16 -41
- data/lib/hecks/ir.rb +14 -83
- data/lib/hecks/language/adapter.bluebook +2 -7
- data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
- data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
- data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
- data/lib/hecks/language/bluebook/command.bluebook +27 -144
- data/lib/hecks/language/bluebook/entity.bluebook +24 -97
- data/lib/hecks/language/bluebook/policy.bluebook +16 -67
- data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
- data/lib/hecks/language/bluebook/projection.bluebook +126 -60
- data/lib/hecks/language/bluebook/query.bluebook +12 -43
- data/lib/hecks/language/bluebook/shape.bluebook +23 -73
- data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
- data/lib/hecks/language/bluebook/vocabulary.bluebook +98 -197
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
- data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
- data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
- data/lib/hecks/language/oidc.json +35 -0
- data/lib/hecks/language/port.bluebook +4 -17
- data/lib/hecks/language/translation/translation.bluebook +3 -9
- data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
- data/lib/hecks/language/world/wiring.bluebook +1 -3
- data/lib/hecks/language/world/world.bluebook +5 -7
- data/lib/hecks/literal.rb +8 -50
- data/lib/hecks/mcp_door_scope.rb +95 -0
- data/lib/hecks/mcp_stdio_guard.rb +4 -41
- data/lib/hecks/naming.rb +29 -137
- data/lib/hecks/ports/access_control.rb +7 -23
- data/lib/hecks/ports/agent/answers.rb +5 -23
- data/lib/hecks/ports/agent.rb +47 -172
- data/lib/hecks/ports/authentication.rb +12 -39
- data/lib/hecks/ports/authorization.rb +16 -43
- data/lib/hecks/ports/clock.rb +5 -45
- data/lib/hecks/ports/extraction.rb +7 -12
- data/lib/hecks/ports/identity_assignment.rb +8 -29
- data/lib/hecks/ports/identity_generation.rb +6 -29
- data/lib/hecks/ports/identity_resolution.rb +2 -10
- data/lib/hecks/ports/key_vault.rb +9 -45
- data/lib/hecks/ports/persistence/append_only.rb +62 -65
- data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
- data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
- data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
- data/lib/hecks/ports/persistence/plugin.rb +13 -42
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +7 -26
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +22 -122
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
- data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
- data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
- data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
- data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
- data/lib/hecks/ports/persistence/state_codec.rb +23 -103
- data/lib/hecks/ports/persistence.rb +7 -14
- data/lib/hecks/ports/projection.rb +42 -84
- data/lib/hecks/ports/query/in_memory.rb +10 -58
- data/lib/hecks/ports/query/ordering.rb +11 -32
- data/lib/hecks/ports/query.rb +14 -37
- data/lib/hecks/ports.rb +2 -8
- data/lib/hecks/projections/bootstrap_table.rb +11 -43
- data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
- data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
- data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
- data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
- data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
- data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
- data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
- data/lib/hecks/projections/deploy/fargate.rb +35 -202
- data/lib/hecks/projections/deploy/lambda.rb +151 -679
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
- data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
- data/lib/hecks/projections/deploy/preview/script.rb +2 -14
- data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
- data/lib/hecks/projections/deploy/preview/template.rb +2 -58
- data/lib/hecks/projections/deploy/preview.rb +2 -67
- data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
- data/lib/hecks/projections/deploy/scripts.rb +4 -57
- data/lib/hecks/projections/deploy/shared.rb +47 -146
- data/lib/hecks/projections/deploy/smoke.rb +4 -61
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
- data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
- data/lib/hecks/projections/deploy/template_diff.rb +5 -57
- data/lib/hecks/projections/diagrams.rb +20 -582
- data/lib/hecks/projections/glossary/html.rb +14 -130
- data/lib/hecks/projections/glossary/markdown.rb +8 -69
- data/lib/hecks/projections/glossary/mermaid.rb +9 -66
- data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
- data/lib/hecks/projections/glossary/sentences.rb +14 -159
- data/lib/hecks/projections/glossary.rb +30 -222
- data/lib/hecks/projections/ir.rb +3 -10
- data/lib/hecks/projections/model/deviations.rb +15 -42
- data/lib/hecks/projections/model.rb +12 -51
- data/lib/hecks/projections/oidc.rb +12 -56
- data/lib/hecks/projections/parser_table.rb +9 -28
- data/lib/hecks/projections/reference.rb +9 -16
- data/lib/hecks/projections/rust_vocabulary.rb +12 -250
- data/lib/hecks/projections/shape.rb +3 -29
- data/lib/hecks/projections/statements.rb +7 -86
- data/lib/hecks/projections/vocabulary.rb +6 -22
- data/lib/hecks/projections.rb +2 -16
- data/lib/hecks/projector/cli_projector.rb +60 -375
- data/lib/hecks/projector/docs_projector.rb +10 -201
- data/lib/hecks/projector/exporter.rb +46 -138
- data/lib/hecks/projector/ir_projector.rb +2 -9
- data/lib/hecks/projector/narrate_projector.rb +15 -189
- data/lib/hecks/projector/target.rb +13 -76
- data/lib/hecks/projector.rb +30 -121
- data/lib/hecks/query_specification/common/comparators.rb +5 -26
- data/lib/hecks/query_specification/common/comparison.rb +35 -143
- data/lib/hecks/query_specification/common/dsl.rb +8 -24
- data/lib/hecks/query_specification/common/null_policy.rb +20 -70
- data/lib/hecks/query_specification/common/options.rb +2 -6
- data/lib/hecks/query_specification/common/where_clause.rb +2 -10
- data/lib/hecks/query_specification/field_path.rb +37 -77
- data/lib/hecks/query_specification/hop_path.rb +12 -76
- data/lib/hecks/release/gem_pin.rb +2 -26
- data/lib/hecks/release/runner/ci_publisher.rb +93 -0
- data/lib/hecks/release/runner/commands.rb +66 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +43 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +75 -0
- data/lib/hecks/release/runner/preflight.rb +104 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +80 -0
- data/lib/hecks/release/runner.rb +215 -0
- data/lib/hecks/rendering.rb +4 -26
- data/lib/hecks/router/namespace_installer.rb +12 -36
- data/lib/hecks/router.rb +4 -8
- data/lib/hecks/runtime/aggregate_lock.rb +8 -33
- data/lib/hecks/runtime/boot_gates.rb +3 -18
- data/lib/hecks/runtime/caller.rb +19 -59
- data/lib/hecks/runtime/capability_graph.rb +5 -21
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
- data/lib/hecks/runtime/command_interpreter.rb +125 -307
- data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
- data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
- data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
- data/lib/hecks/runtime/command_rules/emission.rb +8 -21
- data/lib/hecks/runtime/command_rules/references.rb +23 -202
- data/lib/hecks/runtime/dependency_planning.rb +30 -102
- data/lib/hecks/runtime/dispatcher.rb +82 -241
- data/lib/hecks/runtime/entity_element.rb +40 -462
- data/lib/hecks/runtime/entity_interpreter.rb +65 -220
- data/lib/hecks/runtime/errors.rb +32 -115
- data/lib/hecks/runtime/event.rb +3 -25
- data/lib/hecks/runtime/identity.rb +18 -154
- data/lib/hecks/runtime/instance.rb +18 -150
- data/lib/hecks/runtime/interpreting.rb +11 -70
- data/lib/hecks/runtime/invocation.rb +28 -164
- data/lib/hecks/runtime/loader.rb +48 -186
- data/lib/hecks/runtime/outbox.rb +38 -261
- data/lib/hecks/runtime/policy_interpreter.rb +38 -200
- data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
- data/lib/hecks/runtime/query_interpreter.rb +45 -187
- data/lib/hecks/runtime/reaction_invocation.rb +36 -138
- data/lib/hecks/runtime/read_model_interpreter.rb +139 -208
- data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
- data/lib/hecks/runtime/reference_hop.rb +8 -81
- data/lib/hecks/runtime/refusal_wording.rb +8 -89
- data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
- data/lib/hecks/runtime/registry/verification.rb +52 -225
- data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
- data/lib/hecks/runtime/registry.rb +103 -244
- data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
- data/lib/hecks/runtime/routing.rb +4 -15
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
- data/lib/hecks/runtime/saga_interpreter.rb +79 -263
- data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
- data/lib/hecks/runtime/tenant_check.rb +6 -68
- data/lib/hecks/runtime/tenant_scope.rb +3 -28
- data/lib/hecks/runtime/value/admission.rb +16 -67
- data/lib/hecks/runtime/value/coercion.rb +155 -641
- data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
- data/lib/hecks/runtime/value.rb +12 -84
- data/lib/hecks/runtime.rb +17 -65
- data/lib/hecks/storehouse.rb +146 -567
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
- data/lib/hecks/vendoring/git_environment.rb +2 -8
- data/lib/hecks/vendoring/git_source.rb +4 -10
- data/lib/hecks/vendoring.rb +7 -46
- data/lib/hecks/version.rb +3 -14
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +32 -82
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
- metadata +30 -59
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
|
@@ -8,31 +8,35 @@ require_relative "../query_specification/field_path"
|
|
|
8
8
|
|
|
9
9
|
module Hecks
|
|
10
10
|
module Runtime
|
|
11
|
-
# Interprets one declared `read_model`: resolves
|
|
12
|
-
# `
|
|
13
|
-
# matches non-root heads to already-projected rows by reference
|
|
14
|
-
# field, and applies group_by/count/median reduction where declared.
|
|
15
|
-
# Prefers a native SQLite escape hatch (#query_read_model) when the
|
|
16
|
-
# model is simple enough for one; otherwise runs the whole join
|
|
17
|
-
# in-process against loaded records.
|
|
11
|
+
# Interprets one declared `read_model`: resolves its `include`d heads and applies any
|
|
12
|
+
# `group_by` or reduction (`REDUCTION_WORD`), preferring a native SQLite path when it can.
|
|
18
13
|
class ReadModelInterpreter
|
|
14
|
+
# The word each non-`count` reduction ivar reads back as in a refusal message.
|
|
15
|
+
REDUCTION_WORD = {
|
|
16
|
+
median_field: "median", sum_field: "sum", avg_field: "avg", min_field: "min",
|
|
17
|
+
max_field: "max", percentile_field: "percentile", any_field: "any", all_field: "all"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# `sum`/`avg` refuse a Float field (ADR 0078): summing floats cannot be made to agree,
|
|
21
|
+
# byte for byte, between Ruby and Rust, so v1 admits only the Integer case.
|
|
22
|
+
INTEGER_ONLY_REDUCTIONS = %i[sum_field avg_field].freeze
|
|
23
|
+
BOOLEAN_REDUCTIONS = %i[any_field all_field].freeze
|
|
24
|
+
|
|
19
25
|
# @param registry [Runtime::Registry] the booted registry whose repositories
|
|
20
26
|
# this interpreter reads
|
|
21
27
|
def initialize(registry) = @registry = registry
|
|
22
28
|
|
|
23
29
|
# Runs one declared read model and returns its projected rows.
|
|
24
|
-
#
|
|
25
30
|
# @param domain [String, Symbol] the domain the read model is declared in
|
|
26
31
|
# @param model [Bluebook::ReadModel] the read model to run
|
|
27
|
-
# @param args [Hash
|
|
28
|
-
# @return [Array<Hash>] a one-element
|
|
29
|
-
#
|
|
30
|
-
# @raise [Runtime::TypeMismatch] if the reference argument is offered as a whole
|
|
31
|
-
# object rather than a plain identity, or a `median` field is not numeric
|
|
32
|
+
# @param args [Hash] the query's declared arguments
|
|
33
|
+
# @return [Array<Hash>] a one-element array of head name => projected rows
|
|
34
|
+
# @raise [Runtime::TypeMismatch] if a reference is a whole object
|
|
32
35
|
# @raise [Runtime::NotFound] if the reference argument names no record
|
|
33
36
|
# @raise [KeyError] if a rooted read model is asked without its reference argument
|
|
34
|
-
# @raise [ArgumentError] if `group_by` or
|
|
35
|
-
#
|
|
37
|
+
# @raise [ArgumentError] if `group_by` or a reduction names an undeclared field, or one
|
|
38
|
+
# of the wrong type
|
|
39
|
+
# @raise [Runtime::InvariantViolation] if two rows collide on a full `group_by` key
|
|
36
40
|
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
37
41
|
def call(domain, model, args)
|
|
38
42
|
project(domain, model, args)
|
|
@@ -40,15 +44,8 @@ module Hecks
|
|
|
40
44
|
|
|
41
45
|
private
|
|
42
46
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
# current order fixes (the reference/TenantScope refusal ordering
|
|
46
|
-
# above, the root-first head processing below). Splitting this
|
|
47
|
-
# into smaller methods would scatter that ordering across method
|
|
48
|
-
# boundaries where a future editor could silently break it, and
|
|
49
|
-
# would force threading `rootless`/`reference_id`/`eligible`/
|
|
50
|
-
# `projected`/`rows_by_as` between the pieces as parameters and
|
|
51
|
-
# return values.
|
|
47
|
+
# Splitting this into smaller methods would scatter the root-first/SQLite/join
|
|
48
|
+
# ordering across boundaries where a future editor could silently break it.
|
|
52
49
|
# rubocop:disable-next Metrics/AbcSize
|
|
53
50
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
54
51
|
# rubocop:disable-next Metrics/MethodLength
|
|
@@ -56,32 +53,19 @@ module Hecks
|
|
|
56
53
|
def project(domain, model, args)
|
|
57
54
|
bluebook = @registry.bluebook(domain)
|
|
58
55
|
rootless = model.reference_target.nil?
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
# path-dependent answer — an adapter could quietly open the wrapped
|
|
62
|
-
# reference while the in-process path reads it whole and finds nothing.
|
|
63
|
-
# Refused up front, identically on every path. Nothing to refuse for
|
|
64
|
-
# a rootless model — there's no reference argument to have offered
|
|
65
|
-
# wrong at all.
|
|
56
|
+
# Refused before the adapter early-return, so both paths refuse identically
|
|
57
|
+
# rather than one silently opening a wrapped reference the other reads whole.
|
|
66
58
|
refuse_object_reference(model, args) unless rootless
|
|
67
59
|
reference_id = reference(args.fetch(model.reference_name)) unless rootless
|
|
68
|
-
# Computed off the original model, before TenantScope wraps it
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# the wrapper adds underneath. Plural (ADR 0055) — `on:` lets more
|
|
72
|
-
# than one many-side head be eligible at once.
|
|
60
|
+
# Computed off the original model, before TenantScope wraps it, so this
|
|
61
|
+
# reflects what the bluebook author declared, not the synthetic tenant
|
|
62
|
+
# clause added underneath (ADR 0055; `on:` allows more than one head).
|
|
73
63
|
eligible = model.filtered_head_names
|
|
74
64
|
model = TenantScope.apply(model, args)
|
|
75
|
-
# A rootless, `group_by
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
# reducing) — always runs the in-process loop below instead.
|
|
80
|
-
# Correct everywhere ; not SQL-pushed-down for a SQLite-backed
|
|
81
|
-
# aggregate yet, a real, named limit, not a silent one — the
|
|
82
|
-
# same one `group_by` already accepted, extended here rather
|
|
83
|
-
# than narrowed.
|
|
84
|
-
unless rootless || model.group_by.any? || model.count? || model.median_field
|
|
65
|
+
# A rootless model, or one declaring `group_by` or any reduction, always runs
|
|
66
|
+
# the in-process loop below — none of those are pushed down into
|
|
67
|
+
# `query_read_model`, a known limit, not a silent one.
|
|
68
|
+
unless rootless || model.group_by.any? || model.reducing?
|
|
85
69
|
repository = @registry.read_repository(domain, bluebook.aggregate(model.reference_target))
|
|
86
70
|
if repository.respond_to?(:query_read_model) && repository.adapter.respond_to?(:query_read_model)
|
|
87
71
|
return repository.query_read_model(domain, model, args,
|
|
@@ -89,22 +73,9 @@ module Hecks
|
|
|
89
73
|
end
|
|
90
74
|
end
|
|
91
75
|
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
# at build time, once `@reference_target` is known), but that
|
|
96
|
-
# promise is not kept without this: running heads in
|
|
97
|
-
# their literal declared order and matching each "many" head
|
|
98
|
-
# against whatever was already in `projected` would leave it
|
|
99
|
-
# empty, the very first time through, if a many-side head happened
|
|
100
|
-
# to be declared before the root. A real, live bug (not a guess):
|
|
101
|
-
# `include Promotion` before `include Item` on a read model
|
|
102
|
-
# whose root is Item silently returned an empty array for
|
|
103
|
-
# Promotion — no error, just a wrong, too-small answer — while
|
|
104
|
-
# the reverse order worked purely by accident. `partition`,
|
|
105
|
-
# not `sort_by`: Ruby's `sort_by` is not guaranteed stable,
|
|
106
|
-
# and correctness here must not depend on it being so by
|
|
107
|
-
# chance on the current MRI build.
|
|
76
|
+
# Root heads run first regardless of declared `include` order — a many-side
|
|
77
|
+
# head declared before its root would otherwise match against an empty
|
|
78
|
+
# `projected`. `partition`, not `sort_by`, which is not guaranteed stable.
|
|
108
79
|
root_heads, other_heads = model.aggregate_heads.partition { |head| head[:aggregate] == model.reference_target }
|
|
109
80
|
projected = []
|
|
110
81
|
rows_by_as = {}
|
|
@@ -112,20 +83,9 @@ module Hecks
|
|
|
112
83
|
rows = if head[:aggregate] == model.reference_target
|
|
113
84
|
[fetch(bluebook, domain, head[:aggregate], reference_id)]
|
|
114
85
|
elsif rootless
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
# heads
|
|
118
|
-
# against each other, and there is no DSL to declare
|
|
119
|
-
# one if you wanted to — `ReadModelBuilder#include_impl`
|
|
120
|
-
# takes only `type`/`as:` (checked directly, not
|
|
121
|
-
# assumed), and `group_by` groups this bulk read's own
|
|
122
|
-
# output, it names no predicate between two heads.
|
|
123
|
-
# Building a cross-join here would mean choosing a join
|
|
124
|
-
# semantics (equality on which fields?) nobody has
|
|
125
|
-
# declared — a real, deliberate scope limit pending a
|
|
126
|
-
# future `include ..., joins: ...`-shaped grammar
|
|
127
|
-
# addition (with its own Rust mirror), not a gap this
|
|
128
|
-
# interpreter can quietly grow into on its own.
|
|
86
|
+
# A rootless model has no root to FK-match against, so each head
|
|
87
|
+
# reads independently; there is no DSL for cross-joining rootless
|
|
88
|
+
# heads together, a deliberate scope limit, not a gap to grow into.
|
|
129
89
|
records(bluebook, domain, head[:aggregate])
|
|
130
90
|
else
|
|
131
91
|
matching(records(bluebook, domain, head[:aggregate])) do |record|
|
|
@@ -140,19 +100,19 @@ module Hecks
|
|
|
140
100
|
projected << { aggregate: head[:aggregate], rows: rows }
|
|
141
101
|
rows_by_as[head[:as]] = head[:many] ? rows : rows.first
|
|
142
102
|
end
|
|
143
|
-
# Declared order preserved in the output — only the
|
|
144
|
-
#
|
|
145
|
-
# back.
|
|
103
|
+
# Declared order is preserved in the output — only the computation above
|
|
104
|
+
# needed reordering.
|
|
146
105
|
heads = model.aggregate_heads.to_h { |head| [head[:as], rows_by_as[head[:as]]] }
|
|
147
106
|
grouped_head = group_by_target(model, bluebook)
|
|
148
107
|
reduced_head = aggregation_target(model, bluebook)
|
|
149
108
|
[heads.each_with_object({}) do |(as, value), out|
|
|
150
109
|
out[as] = if grouped_head && as == grouped_head[:as]
|
|
151
|
-
nest(value.map { |record| Value.materialize_unwrapped(row(record)) }, model.group_by_fields
|
|
110
|
+
nest(value.map { |record| Value.materialize_unwrapped(row(record)) }, model.group_by_fields,
|
|
111
|
+
collision_check(model, bluebook, grouped_head))
|
|
152
112
|
elsif reduced_head && as == reduced_head[:as] && model.count?
|
|
153
113
|
value.length
|
|
154
114
|
elsif reduced_head && as == reduced_head[:as]
|
|
155
|
-
|
|
115
|
+
reduce(model, value)
|
|
156
116
|
elsif value.is_a?(Array)
|
|
157
117
|
value.map { |record| Value.materialize(row(record)) }
|
|
158
118
|
else
|
|
@@ -161,34 +121,8 @@ module Hecks
|
|
|
161
121
|
end]
|
|
162
122
|
end
|
|
163
123
|
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
# head is matched, but a chain of non-root heads (a head that
|
|
167
|
-
# references another non-root head, not the root) is still
|
|
168
|
-
# matched against whatever declaration order happened to put in
|
|
169
|
-
# `projected` so far. `include Coupon` before `include Promotion`
|
|
170
|
-
# on a read model rooted at Item, where Coupon references
|
|
171
|
-
# Promotion (which references Item), silently returned an empty
|
|
172
|
-
# `coupons` array — Coupon's match ran while `projected` held only
|
|
173
|
-
# Item, one level short of what it needed.
|
|
174
|
-
#
|
|
175
|
-
# Fixed the same way root-first was: not by asking bluebook authors
|
|
176
|
-
# to declare `include` in dependency order (the same promise
|
|
177
|
-
# `read_model_builder.rb` already makes and this file is the one
|
|
178
|
-
# place obligated to keep), but by topologically sorting the
|
|
179
|
-
# non-root heads on their own declared reference fields before
|
|
180
|
-
# this method's runtime matching ever runs — Kahn's algorithm,
|
|
181
|
-
# picking ready heads in declared order at each step so declaring
|
|
182
|
-
# order still governs whenever there is no dependency to break a
|
|
183
|
-
# tie. This generalizes root-first (a chain of length 1) to a
|
|
184
|
-
# chain of any depth, and to a head depending on more than one
|
|
185
|
-
# other head at once (not just a straight chain).
|
|
186
|
-
#
|
|
187
|
-
# A cycle among non-root heads (A references B which references A)
|
|
188
|
-
# has no valid topological order at all — falls back to the
|
|
189
|
-
# remaining heads' declared order rather than looping forever, the
|
|
190
|
-
# same "whichever runs first finds nothing" behaviour this method
|
|
191
|
-
# had for every non-root head before root-first existed.
|
|
124
|
+
# Topologically sorts non-root heads by their declared reference fields
|
|
125
|
+
# (Kahn's algorithm); a cycle falls back to declared order rather than looping.
|
|
192
126
|
def order_other_heads(bluebook, root_heads, other_heads)
|
|
193
127
|
resolved = root_heads.map { |head| head[:aggregate] }
|
|
194
128
|
remaining = other_heads.dup
|
|
@@ -208,25 +142,8 @@ module Hecks
|
|
|
208
142
|
ordered
|
|
209
143
|
end
|
|
210
144
|
|
|
211
|
-
# Which other declared
|
|
212
|
-
#
|
|
213
|
-
# file's runtime matching checks record-by-record, asked here
|
|
214
|
-
# statically, once, to order heads before any record is read.
|
|
215
|
-
#
|
|
216
|
-
# `head[:aggregate]` names whatever `include` was given — and
|
|
217
|
-
# `include` accepts a nested entity (Member, nested under
|
|
218
|
-
# ValueObject ; Handler and Dispatch, nested under ProcessManager
|
|
219
|
-
# — bluebook.bluebook's own `WholeBluebook` read model includes
|
|
220
|
-
# all three) just as readily as a top-level aggregate.
|
|
221
|
-
# `bluebook.aggregate` only ever finds the latter
|
|
222
|
-
# (Behaviour::Chapter#aggregate searches `@aggregates`, which
|
|
223
|
-
# holds no entities), so it returns nil for an entity-headed
|
|
224
|
-
# include — a real case, not a malformed one. `records`, below,
|
|
225
|
-
# already treats that nil as "no rows of its own to fetch" ;
|
|
226
|
-
# a head with no rows of its own has nothing to check for a
|
|
227
|
-
# reference field either, so it depends on nothing here, the
|
|
228
|
-
# same as it always silently read empty before this file's
|
|
229
|
-
# topological sort existed.
|
|
145
|
+
# Which other declared heads a head's own aggregate holds a reference field
|
|
146
|
+
# toward; an entity-headed include resolves no aggregate and so depends on nothing.
|
|
230
147
|
def depends_on(bluebook, head, other_heads)
|
|
231
148
|
aggregate = bluebook.aggregate(head[:aggregate])
|
|
232
149
|
return [] unless aggregate
|
|
@@ -236,11 +153,8 @@ module Hecks
|
|
|
236
153
|
.map { |other| other[:aggregate] }
|
|
237
154
|
end
|
|
238
155
|
|
|
239
|
-
# `group_by`'s
|
|
240
|
-
#
|
|
241
|
-
# zero or several) — resolved here, once, rather than re-derived
|
|
242
|
-
# per row. Raises loudly on a typo'd field name rather than
|
|
243
|
-
# silently grouping every row into one bucket keyed `nil`.
|
|
156
|
+
# Resolves `group_by`'s target head and validates its fields once, raising on
|
|
157
|
+
# a typo'd field name rather than silently grouping every row under `nil`.
|
|
244
158
|
def group_by_target(model, bluebook)
|
|
245
159
|
return nil unless model.group_by.any?
|
|
246
160
|
|
|
@@ -248,16 +162,9 @@ module Hecks
|
|
|
248
162
|
aggregate = bluebook.aggregate(target[:aggregate])
|
|
249
163
|
model.group_by_fields.each do |field|
|
|
250
164
|
next if aggregate.attribute(field)
|
|
251
|
-
#
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
# state is stored on the record like anything else — it is simply
|
|
255
|
-
# declared by `lifecycle :status` rather than by `attribute`.
|
|
256
|
-
#
|
|
257
|
-
# It is also the grouping anybody actually wants. "How are we doing"
|
|
258
|
-
# over a bug ledger is the count per status, and a report that could
|
|
259
|
-
# group by every field except that one could not answer the question
|
|
260
|
-
# reports exist for.
|
|
165
|
+
# A lifecycle field is a real field too — it's stored on the record like
|
|
166
|
+
# any attribute, just declared via `lifecycle :status`, and it's usually
|
|
167
|
+
# exactly the field a report wants to group by.
|
|
261
168
|
next if aggregate.lifecycle && aggregate.lifecycle.field.to_sym == field.to_sym
|
|
262
169
|
|
|
263
170
|
raise ArgumentError,
|
|
@@ -267,77 +174,113 @@ module Hecks
|
|
|
267
174
|
target
|
|
268
175
|
end
|
|
269
176
|
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
#
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
def nest(rows, fields)
|
|
177
|
+
# Whether `group_by` leaves must hold one row, decided from the declaration
|
|
178
|
+
# alone (ADR 0061 D1): nil when the key path already covers the identity.
|
|
179
|
+
def collision_check(model, bluebook, grouped_head)
|
|
180
|
+
model.groups_by_identity?(bluebook.aggregate(grouped_head[:aggregate])) ? nil : model
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Nests one level per `group_by` field; a leaf holding more than one row
|
|
184
|
+
# is a `group_by` collision (ADR 0061 D1), refused rather than picking one.
|
|
185
|
+
def nest(rows, fields, checked, reached = [])
|
|
280
186
|
field, *rest = fields
|
|
281
|
-
rows.group_by { |row| row[field] }.
|
|
187
|
+
rows.group_by { |row| row[field] }.to_h do |key, group|
|
|
282
188
|
# Strip only the field just grouped by, not the whole remaining
|
|
283
189
|
# list — `rest`'s own fields have to survive into the recursive
|
|
284
|
-
# call below, or the next level groups by a key that's already
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
rest.empty? ? stripped.first : nest(stripped, rest)
|
|
190
|
+
# call below, or the next level groups by a key that's already gone.
|
|
191
|
+
stripped = group.map { |row| row.reject { |name, _| name == field } }
|
|
192
|
+
path = reached + [[field, key]]
|
|
193
|
+
[key, rest.empty? ? leaf(stripped, checked, path) : nest(stripped, rest, checked, path)]
|
|
289
194
|
end
|
|
290
195
|
end
|
|
291
196
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
197
|
+
def leaf(rows, checked, path)
|
|
198
|
+
return rows.first unless checked && rows.size > 1
|
|
199
|
+
|
|
200
|
+
raise InvariantViolation,
|
|
201
|
+
RefusalWording.render_site("InvariantViolation", "group_by_collision",
|
|
202
|
+
read_model: checked.name, fields: checked.group_by_fields,
|
|
203
|
+
ids: rows.map { |row| row[:id] },
|
|
204
|
+
key: path.map { |field, value| "#{field} = #{value}" }.join(", "))
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Resolves a reduction's single many-side head, raising on a field that doesn't
|
|
208
|
+
# exist or has the wrong type rather than comparing garbage.
|
|
300
209
|
def aggregation_target(model, bluebook)
|
|
301
|
-
return nil unless model.
|
|
210
|
+
return nil unless model.reducing?
|
|
302
211
|
|
|
303
212
|
target = model.aggregate_heads.find { |head| head[:many] }
|
|
304
|
-
return target
|
|
213
|
+
return target if model.count?
|
|
305
214
|
|
|
215
|
+
ivar, word = REDUCTION_WORD.find { |name, _| model.public_send(name) }
|
|
216
|
+
field = model.public_send(ivar)
|
|
306
217
|
aggregate = bluebook.aggregate(target[:aggregate])
|
|
307
|
-
attribute = aggregate.attribute(
|
|
218
|
+
attribute = aggregate.attribute(field)
|
|
308
219
|
unless attribute
|
|
309
220
|
raise ArgumentError,
|
|
310
|
-
"#{model.name}'s
|
|
221
|
+
"#{model.name}'s #{word} names #{field.inspect}, but #{target[:aggregate]} " \
|
|
311
222
|
"declares no such attribute (it declares #{aggregate.attributes.map(&:name).join(', ')})"
|
|
312
223
|
end
|
|
313
|
-
|
|
314
|
-
raise ArgumentError,
|
|
315
|
-
"#{model.name}'s median names #{model.median_field.inspect} on #{target[:aggregate]}, " \
|
|
316
|
-
"which is not numeric — median needs a numeric field (a bare number, or a " \
|
|
317
|
-
"value object carrying one)"
|
|
318
|
-
end
|
|
224
|
+
validate_reduction_type!(model, word, field, target[:aggregate], aggregate, attribute, ivar)
|
|
319
225
|
target
|
|
320
226
|
end
|
|
321
227
|
|
|
322
|
-
#
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
228
|
+
# @raise [ArgumentError] if `attribute` does not have the type `ivar`'s reduction needs
|
|
229
|
+
def validate_reduction_type!(model, word, field, aggregate_name, aggregate, attribute, ivar)
|
|
230
|
+
wrap = ->(type) { aggregate.value_object(type) }
|
|
231
|
+
ok = if BOOLEAN_REDUCTIONS.include?(ivar)
|
|
232
|
+
QuerySpecification::FieldPath.boolean?(attribute, [], &wrap)
|
|
233
|
+
elsif INTEGER_ONLY_REDUCTIONS.include?(ivar)
|
|
234
|
+
QuerySpecification::FieldPath.integer?(attribute, [], &wrap)
|
|
235
|
+
else
|
|
236
|
+
QuerySpecification::FieldPath.numeric?(attribute, [], &wrap)
|
|
237
|
+
end
|
|
238
|
+
return if ok
|
|
239
|
+
|
|
240
|
+
kind = BOOLEAN_REDUCTIONS.include?(ivar) ? "boolean" : "numeric"
|
|
241
|
+
raise ArgumentError,
|
|
242
|
+
"#{model.name}'s #{word} names #{field.inspect} on #{aggregate_name}, " \
|
|
243
|
+
"which is not #{kind} — #{word} needs a #{kind == 'boolean' ? 'true/false' : 'numeric'} field"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Dispatches to the one reduction `model` declares, over the eligible collection's own
|
|
247
|
+
# `comparable`-mapped values (or raw booleans for `any`/`all`) — the interpretation of
|
|
248
|
+
# `Runtime::ReadModelInterpreter#project`'s `reduced_head` branch.
|
|
249
|
+
def reduce(model, rows)
|
|
250
|
+
return boolean_values(rows, model.any_field).any? if model.any_field
|
|
251
|
+
return boolean_values(rows, model.all_field).all? if model.all_field
|
|
252
|
+
return numeric_values(rows, model.sum_field).sum if model.sum_field
|
|
253
|
+
return average(numeric_values(rows, model.avg_field)) if model.avg_field
|
|
254
|
+
return numeric_values(rows, model.min_field).min if model.min_field
|
|
255
|
+
return numeric_values(rows, model.max_field).max if model.max_field
|
|
256
|
+
return percentile(numeric_values(rows, model.percentile_field), model.percentile_at) if model.percentile_field
|
|
257
|
+
|
|
258
|
+
percentile(numeric_values(rows, model.median_field), 0.5)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Shared by every reduction: `comparable` unwraps a value-object-wrapped field down to
|
|
262
|
+
# its sole member (numeric or boolean alike) exactly as `where`/`order_by` already read
|
|
263
|
+
# it, so `flagged: { value: true }` reduces on the boolean, not the ever-truthy Hash.
|
|
264
|
+
def reduction_values(rows, field)
|
|
265
|
+
# `compact`, not `filter_map`: a stored `false` is a real value, not an absent one.
|
|
266
|
+
rows.map { |record| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(row(record), field)) }.compact
|
|
267
|
+
end
|
|
268
|
+
alias numeric_values reduction_values
|
|
269
|
+
alias boolean_values reduction_values
|
|
270
|
+
|
|
271
|
+
# `nil` on no rows: a rate of nothing is undefined, not zero.
|
|
272
|
+
def average(values) = values.empty? ? nil : values.sum.to_f / values.length
|
|
273
|
+
|
|
274
|
+
# The value at one interpolated rank (`0.0`..`1.0`); `at: 0.5` is the standard median
|
|
275
|
+
# (middle value when odd, average of the two middle values when even). `nil` on no rows.
|
|
276
|
+
def percentile(values, at)
|
|
337
277
|
return nil if values.empty?
|
|
338
278
|
|
|
339
|
-
|
|
340
|
-
|
|
279
|
+
sorted = values.sort
|
|
280
|
+
position = at * (sorted.length - 1)
|
|
281
|
+
lower = position.floor
|
|
282
|
+
fraction = position - lower
|
|
283
|
+
fraction.zero? ? sorted[lower] : sorted[lower] + (fraction * (sorted[lower + 1] - sorted[lower]))
|
|
341
284
|
end
|
|
342
285
|
|
|
343
286
|
def fetch(bluebook, domain, aggregate_name, id)
|
|
@@ -351,20 +294,8 @@ module Hecks
|
|
|
351
294
|
aggregate ? @registry.read_repository(domain, aggregate).all : []
|
|
352
295
|
end
|
|
353
296
|
|
|
354
|
-
# A
|
|
355
|
-
#
|
|
356
|
-
# a different thing from the identity unwrap that was removed. An
|
|
357
|
-
# identity is declared as a path and followed (Runtime::Identity) ; a
|
|
358
|
-
# reference has no path of its own, and `Value.scalar` refuses a
|
|
359
|
-
# composite rather than guessing which field was meant.
|
|
360
|
-
#
|
|
361
|
-
# Storing the scalar itself would remove this reading altogether. That
|
|
362
|
-
# is a change to how references are stored, not to how identities are
|
|
363
|
-
# declared, so it is not made here.
|
|
364
|
-
# An ask names itself where a command would name itself, and says the
|
|
365
|
-
# same thing about the same shape. `Value.refuse_object_reference` is
|
|
366
|
-
# not reused because it speaks of a command and its attribute ; a read
|
|
367
|
-
# model has a query name and one declared reference.
|
|
297
|
+
# A reference has no path of its own (unlike an identity), so `Value.scalar`
|
|
298
|
+
# refuses a composite rather than guessing which field was meant.
|
|
368
299
|
def refuse_object_reference(model, args)
|
|
369
300
|
offered = args.fetch(model.reference_name, nil)
|
|
370
301
|
return unless offered.is_a?(Hash) || offered.is_a?(Value)
|
|
@@ -1,37 +1,12 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Runtime
|
|
3
|
-
#
|
|
4
|
-
#
|
|
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.
|
|
3
|
+
# Copies `projects` field values from their referenced records into the owning
|
|
4
|
+
# aggregate's records; called explicitly, never at dispatch time (ADR 0025).
|
|
27
5
|
module RebuildSweep
|
|
28
6
|
module_function
|
|
29
7
|
|
|
30
|
-
#
|
|
31
|
-
#
|
|
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.
|
|
8
|
+
# Refreshes one aggregate's projected fields across every record it holds.
|
|
9
|
+
# Saves only records whose projected value differs from what is stored.
|
|
35
10
|
#
|
|
36
11
|
# @param registry [Runtime::Registry] the booted registry to read repositories from
|
|
37
12
|
# @param domain [String] the domain the aggregate belongs to
|
|
@@ -45,16 +20,9 @@ module Hecks
|
|
|
45
20
|
repository.all.count { |record| refresh(registry, domain, aggregate, record, repository) }
|
|
46
21
|
end
|
|
47
22
|
|
|
48
|
-
# Refreshes one record's
|
|
23
|
+
# Refreshes one record's projected fields in place, saving it if any changed.
|
|
49
24
|
#
|
|
50
|
-
# @
|
|
51
|
-
# repository from
|
|
52
|
-
# @param domain [String] the domain the aggregate belongs to
|
|
53
|
-
# @param aggregate [Bluebook::Aggregate] the aggregate `record` is an instance of
|
|
54
|
-
# @param record [Runtime::Instance] the record to refresh, mutated in place
|
|
55
|
-
# @param repository [Ports::Persistence::AppendOnly] the repository to save `record`
|
|
56
|
-
# through when it changes
|
|
57
|
-
# @return [Boolean] true if any projected field's value changed and `record` was saved
|
|
25
|
+
# @return [Boolean] true if `record` was saved
|
|
58
26
|
def refresh(registry, domain, aggregate, record, repository)
|
|
59
27
|
changed = false
|
|
60
28
|
|
|
@@ -71,22 +39,8 @@ module Hecks
|
|
|
71
39
|
changed
|
|
72
40
|
end
|
|
73
41
|
|
|
74
|
-
# Reads the current value of
|
|
75
|
-
#
|
|
76
|
-
# `nil` when the reference itself does not resolve in this
|
|
77
|
-
# chapter (a cross-domain target left "unfollowed" the same way
|
|
78
|
-
# References#dereference already leaves one) or when the record
|
|
79
|
-
# names no target at all — an optional reference nobody set.
|
|
80
|
-
#
|
|
81
|
-
# @param registry [Runtime::Registry] the booted registry to read the target's
|
|
82
|
-
# repository from
|
|
83
|
-
# @param domain [String] the domain the aggregate belongs to
|
|
84
|
-
# @param aggregate [Bluebook::Aggregate] the aggregate declaring `field`
|
|
85
|
-
# @param record [Runtime::Instance] the record holding the reference to follow
|
|
86
|
-
# @param field [Bluebook::ProjectedField] the projected field to resolve
|
|
87
|
-
# @return [Object, nil] the target record's own `field.remote_field` value; nil if the
|
|
88
|
-
# reference type does not resolve, the record names no target, or the target record
|
|
89
|
-
# cannot be found
|
|
42
|
+
# Reads the current value of a projected field's remote target field.
|
|
43
|
+
# Nil when the reference does not resolve here, names no target, or finds no record.
|
|
90
44
|
def remote_value(registry, domain, aggregate, record, field)
|
|
91
45
|
target = aggregate.attribute(field.reference)&.type&.resolve
|
|
92
46
|
return nil unless target
|