hecks 1.3.0 → 1.5.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/lib/hecks/adapters/driven/claude_code.rb +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
|
@@ -5,7 +5,7 @@ require_relative "../../rendering"
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Runtime
|
|
7
7
|
class Value
|
|
8
|
-
#
|
|
8
|
+
# How a `list_of` attribute's own elements GET hydrated — entity-typed
|
|
9
9
|
# and value-object-typed alike — split out of `Coercion` (this file's
|
|
10
10
|
# sibling, extended into `Value` alongside it exactly the way
|
|
11
11
|
# `Admission` already is) once `Coercion` itself grew past
|
|
@@ -15,22 +15,28 @@ module Hecks
|
|
|
15
15
|
# own header, and this repo's `.rubocop_todo.yml` for the merge that
|
|
16
16
|
# made the split necessary). Cross-calls into `Coercion`'s own
|
|
17
17
|
# `for_attribute`/`build`/`fields_for`/`value_object_for`/
|
|
18
|
-
# `trusting_stored_state?` work unqualified here exactly as they
|
|
19
|
-
#
|
|
20
|
-
# singleton class once extended — `self` never has to know
|
|
21
|
-
# a sibling method actually lives in.
|
|
18
|
+
# `trusting_stored_state?` work unqualified here, exactly as they do
|
|
19
|
+
# within `Coercion` itself, because both modules land on the same
|
|
20
|
+
# `Value` singleton class once extended — `self` never has to know
|
|
21
|
+
# which file a sibling method actually lives in.
|
|
22
22
|
module EntityListCoercion
|
|
23
|
-
# S17, ADR 0026 —
|
|
23
|
+
# S17, ADR 0026 — searches the whole entity tree, not only the
|
|
24
24
|
# root's own direct children. `aggregate` here is always the
|
|
25
|
-
#
|
|
25
|
+
# root aggregate — `for_attribute`'s own `aggregate` argument is
|
|
26
26
|
# never reassigned as hydration recurses into a nested element,
|
|
27
27
|
# because coercion has to resolve value objects, and only the
|
|
28
28
|
# root answers `.value_object` at all (Entity's own header
|
|
29
|
-
# comment: an entity must
|
|
29
|
+
# comment: an entity must not answer to it, or `Value.
|
|
30
30
|
# for_attribute` could no longer tell a piece from a head). So
|
|
31
|
-
# a
|
|
31
|
+
# a nested entity — Dispatch, inside Handler — is not a direct
|
|
32
32
|
# child of the root the way Handler itself is, and a plain
|
|
33
33
|
# `aggregate.entities.find` stops one level short of it.
|
|
34
|
+
#
|
|
35
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct to
|
|
36
|
+
# search, and every entity nested under it
|
|
37
|
+
# @param name [String] the entity's declared `hecks_name` to find
|
|
38
|
+
# @return [Bluebook::Entity, nil] the matching entity anywhere in the tree, or
|
|
39
|
+
# nil if none matches
|
|
34
40
|
def find_entity(construct, name)
|
|
35
41
|
construct.entities.each do |candidate|
|
|
36
42
|
return candidate if candidate.hecks_name == name
|
|
@@ -41,39 +47,54 @@ module Hecks
|
|
|
41
47
|
nil
|
|
42
48
|
end
|
|
43
49
|
|
|
50
|
+
# Hydrates a `list_of` attribute's offered value — each element of a
|
|
51
|
+
# whole-list offering, or a single remove-target value, depending on
|
|
52
|
+
# `attribute.type` and `value`'s own shape.
|
|
53
|
+
#
|
|
44
54
|
# Frozen through: a list read back out of the store is an answer,
|
|
45
55
|
# not a handle on what is stored.
|
|
46
56
|
#
|
|
47
|
-
# ADR 0047 —
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
57
|
+
# ADR 0047 — bailing (`return value unless entity`) the moment
|
|
58
|
+
# `attribute.type` names a value object rather than an entity would
|
|
59
|
+
# hand back the raw, un-hydrated argument untouched. A `sets :field`
|
|
60
|
+
# mutation sourced from a whole-array argument (as opposed to
|
|
61
|
+
# element-by-element `append:`) goes straight through
|
|
52
62
|
# `for_attribute`'s `:list` branch, so `Banking::CardPayment.
|
|
53
|
-
# Authorize`'s own `sets :tags` (`list_of(Tag)`)
|
|
63
|
+
# Authorize`'s own `sets :tags` (`list_of(Tag)`) would store plain
|
|
54
64
|
# Ruby Hashes as its `tags` elements forever — never a real
|
|
55
65
|
# `Value`, never through `Tag`'s own `pattern:`/`invariant`
|
|
56
66
|
# checks. `remove:`'s `==` comparison (a real `Value` against a
|
|
57
|
-
# raw `Hash`) then always
|
|
58
|
-
# that isn't itself a compatible Hash — the bug ADR 0047
|
|
59
|
-
# full. Delegating to `hydrate_value_object_list` below
|
|
60
|
-
# that gap the same way the
|
|
61
|
-
# real, validated `Value` per element, reusing
|
|
62
|
-
# own composite-construction path rather than
|
|
63
|
-
# one.
|
|
64
|
-
# BUG#32 (QualityControl ledger) — `remove:`'s own
|
|
65
|
-
# value
|
|
66
|
-
# below
|
|
67
|
-
# `hydrate_value_object_list` sibling, whose
|
|
68
|
-
# check exists for exactly this shape (see
|
|
69
|
-
# comment). For an
|
|
70
|
-
# (`Ledger.Void`'s `sequence: EntrySequence`) is
|
|
71
|
-
# `Array(2)` merely
|
|
72
|
-
# but the wrapping itself
|
|
73
|
-
# removed`'s `element == value` then
|
|
74
|
-
# Hash against a one-element Array, which can
|
|
75
|
-
# Hash, so nothing
|
|
76
|
-
# either. Routed to `hydrate_entity_identity`
|
|
67
|
+
# raw `Hash`) would then always fail, since `Hash#==` refuses
|
|
68
|
+
# anything that isn't itself a compatible Hash — the bug ADR 0047
|
|
69
|
+
# traces in full. Delegating to `hydrate_value_object_list` below
|
|
70
|
+
# closes that gap the same way the entity branch already works:
|
|
71
|
+
# build a real, validated `Value` per element, reusing
|
|
72
|
+
# `for_attribute`'s own composite-construction path rather than
|
|
73
|
+
# inventing a second one.
|
|
74
|
+
# BUG#32 (QualityControl ledger) — without this, `remove:`'s own
|
|
75
|
+
# single-target value would fall straight into
|
|
76
|
+
# `Array(value).map { ... }` below unguarded, unlike this method's
|
|
77
|
+
# own delegated `hydrate_value_object_list` sibling, whose
|
|
78
|
+
# `value.is_a?(Array)` check exists for exactly this shape (see
|
|
79
|
+
# that method's own comment). For an entity-typed list, `remove:`'s
|
|
80
|
+
# scalar target (`Ledger.Void`'s `sequence: EntrySequence`) is
|
|
81
|
+
# never a Hash, so `Array(2)` would merely wrap it as `[2]` rather
|
|
82
|
+
# than shredding it — but the wrapping itself would still be wrong:
|
|
83
|
+
# `MutationApplier#removed`'s `element == value` would then compare
|
|
84
|
+
# a stored `Entry` Hash against a one-element Array, which can
|
|
85
|
+
# never be `==` a Hash, so nothing would ever be removed and
|
|
86
|
+
# nothing would ever refuse either. Routed to `hydrate_entity_identity`
|
|
87
|
+
# instead, below.
|
|
88
|
+
#
|
|
89
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate `attribute` is
|
|
90
|
+
# declared on
|
|
91
|
+
# @param attribute [Bluebook::Attribute] the `list_of` attribute being hydrated
|
|
92
|
+
# @param value [Array<Hash, Object>, Object] the offered value: a whole-list
|
|
93
|
+
# Array (a `sets`/hydrate load) or a single remove-target value
|
|
94
|
+
# @return [Object] a frozen Array of hydrated elements when `value` is a
|
|
95
|
+
# whole-list Array and `attribute.type` names an entity; otherwise whatever
|
|
96
|
+
# `hydrate_entity_identity` (a non-Array value naming an entity) or
|
|
97
|
+
# `hydrate_value_object_list` (`attribute.type` naming no entity) returns
|
|
77
98
|
def hydrate_entity_list(aggregate, attribute, value)
|
|
78
99
|
entity = find_entity(aggregate, attribute.type.to_s)
|
|
79
100
|
return hydrate_value_object_list(aggregate, attribute, value) unless entity
|
|
@@ -89,7 +110,7 @@ module Hecks
|
|
|
89
110
|
acc[key] = field ? for_attribute(aggregate, field, field_value) : field_value
|
|
90
111
|
end
|
|
91
112
|
end
|
|
92
|
-
# BUG#33 — only a genuine
|
|
113
|
+
# BUG#33 — only a genuine whole-list offering (`value.is_a?(Array)`)
|
|
93
114
|
# is a caller naming every element's own identity at once; the
|
|
94
115
|
# single-target shape this same method also hydrates (a `remove:`
|
|
95
116
|
# target, wrapped one level up by `Array()`) never reaches this
|
|
@@ -101,16 +122,16 @@ module Hecks
|
|
|
101
122
|
|
|
102
123
|
# `MutationApplier#check_entity_collision`'s own guard
|
|
103
124
|
# (mutation_applier.rb), extended from a single caller-supplied
|
|
104
|
-
#
|
|
125
|
+
# append (BUG#13) to a whole-list replace — `sets :entries` bare
|
|
105
126
|
# (`Ledger.ReplaceEntries`, the corpus's first `list_of(ENTITY)`
|
|
106
127
|
# command argument/mutation, qa/stress_domains/corrections). The
|
|
107
128
|
# array branch above rebuilds each offered element's own declared
|
|
108
|
-
# fields but never checked the
|
|
129
|
+
# fields but never checked the offered list itself for either way
|
|
109
130
|
# it can misname its own entities:
|
|
110
131
|
#
|
|
111
132
|
# - two elements sharing one identity — the same silent-duplicate
|
|
112
133
|
# hazard BUG#13's own comment describes: `EntityInterpreter#
|
|
113
|
-
# element_of`'s own `find_index` always matches the
|
|
134
|
+
# element_of`'s own `find_index` always matches the first
|
|
114
135
|
# match, so the second becomes permanently unaddressable by any
|
|
115
136
|
# later command.
|
|
116
137
|
# - an element missing its identity altogether — there is no
|
|
@@ -120,17 +141,30 @@ module Hecks
|
|
|
120
141
|
# whole state, offered at once), so an absent identity is
|
|
121
142
|
# refused rather than guessed.
|
|
122
143
|
#
|
|
123
|
-
#
|
|
144
|
+
# Non-composite identities only, exactly BUG#13's own scope
|
|
124
145
|
# (`entity.identified_by` answers a single Symbol only when
|
|
125
146
|
# `identity_heads.size == 1`, `Behaviour::Traits#derive_identity`) —
|
|
126
147
|
# a composite identity's own duplicate/missing question is the
|
|
127
148
|
# same pre-existing, narrower gap BUG#13 documented and left open,
|
|
128
149
|
# not widened here.
|
|
129
150
|
#
|
|
130
|
-
#
|
|
151
|
+
# Refuses a whole-list offering that misnames its own entities' identities —
|
|
152
|
+
# a duplicate identity across two elements, or an element missing one.
|
|
153
|
+
#
|
|
154
|
+
# Skipped under `trusting_stored_state?`, the same guard `validate!`
|
|
131
155
|
# already gives every value object (C6.3): a record already
|
|
132
156
|
# written is trusted as it was, so tightening this check can never
|
|
133
157
|
# make an old, already-persisted record unreadable.
|
|
158
|
+
#
|
|
159
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `entity` belongs to,
|
|
160
|
+
# named in a refusal
|
|
161
|
+
# @param entity [Bluebook::Entity] the entity type `elements` are shaped as
|
|
162
|
+
# @param elements [Array<Hash, Object>] the hydrated whole-list offering; a
|
|
163
|
+
# non-Hash element is skipped, not checked
|
|
164
|
+
# @return [void]
|
|
165
|
+
# @raise [Runtime::TypeMismatch] if `entity` declares a single-field identity
|
|
166
|
+
# and an element offers no value for it
|
|
167
|
+
# @raise [Runtime::AlreadyExists] if two elements offer the same identity value
|
|
134
168
|
def check_entity_list_identities(aggregate, entity, elements)
|
|
135
169
|
identity = entity.identified_by
|
|
136
170
|
return unless identity
|
|
@@ -144,32 +178,32 @@ module Hecks
|
|
|
144
178
|
offered = fields[identity]
|
|
145
179
|
if offered.nil?
|
|
146
180
|
raise TypeMismatch,
|
|
147
|
-
RefusalWording.
|
|
148
|
-
|
|
149
|
-
|
|
181
|
+
RefusalWording.render_site("TypeMismatch", "numeric_field",
|
|
182
|
+
type: entity.hecks_name, field: identity,
|
|
183
|
+
expected: field&.type, offered: "nil")
|
|
150
184
|
end
|
|
151
185
|
|
|
152
186
|
if seen.include?(offered)
|
|
153
187
|
raise AlreadyExists,
|
|
154
|
-
RefusalWording.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
188
|
+
RefusalWording.render_site("AlreadyExists", "entity_duplicate",
|
|
189
|
+
entity: entity.hecks_name, aggregate: aggregate.hecks_name,
|
|
190
|
+
identity: entity.identity_paths.join(", "),
|
|
191
|
+
offered: [Rendering.describe(offered)])
|
|
158
192
|
end
|
|
159
193
|
seen << offered
|
|
160
194
|
end
|
|
161
195
|
end
|
|
162
196
|
|
|
163
197
|
# BUG#32 — `remove:`'s own single-target value against an
|
|
164
|
-
#
|
|
165
|
-
#
|
|
198
|
+
# entity-typed list. An entity is never offered to `remove:`
|
|
199
|
+
# whole the way a value object is (`hydrate_value_object_list`'s
|
|
166
200
|
# own `element == value` full-value-equality shape) — an entity
|
|
167
201
|
# must never answer `.value_object` at all (`Entity`'s own header
|
|
168
202
|
# comment), so there is no whole-value shape here to rebuild in
|
|
169
|
-
# the first place, only the
|
|
170
|
-
# have to
|
|
203
|
+
# the first place, only the one field a caller would otherwise
|
|
204
|
+
# have to name to address that element any other way
|
|
171
205
|
# (`EntityElement#element_of`'s own `wants`). So this coerces the
|
|
172
|
-
# offered scalar against the entity's
|
|
206
|
+
# offered scalar against the entity's own identity field's
|
|
173
207
|
# declared type — not the entity's full shape — and hands back a
|
|
174
208
|
# real, correctly-typed `Value` ready to compare against each
|
|
175
209
|
# stored element's own identity field
|
|
@@ -177,15 +211,26 @@ module Hecks
|
|
|
177
211
|
# `MutationApplier#removed` and `EntityElement#
|
|
178
212
|
# removed_from_element` match against).
|
|
179
213
|
#
|
|
180
|
-
# A
|
|
214
|
+
# A composite identity (more than one head) has no single field a
|
|
181
215
|
# bare `remove:` target could mean, so this passes the value
|
|
182
|
-
# through
|
|
216
|
+
# through uncoerced rather than guessing which head — the same
|
|
183
217
|
# "nothing in this corpus needs it yet" boundary
|
|
184
218
|
# `MutationApplier#check_entity_collision`'s own header already
|
|
185
219
|
# draws for entity identity elsewhere in this runtime.
|
|
220
|
+
# Coerces a `remove:` target against an entity's own single-field
|
|
221
|
+
# identity type, rather than the entity's full shape.
|
|
222
|
+
#
|
|
186
223
|
# `list_element_match?` treats an uncoerced value the same way
|
|
187
224
|
# it always treated the pre-fix wrapped Array: never a match, a
|
|
188
225
|
# documented no-op rather than a crash.
|
|
226
|
+
#
|
|
227
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `entity` belongs to
|
|
228
|
+
# @param entity [Bluebook::Entity] the entity type `value` addresses
|
|
229
|
+
# @param value [Object] the offered remove-target value
|
|
230
|
+
# @return [Runtime::Value, Object] `value` already coerced (returned unchanged);
|
|
231
|
+
# the value coerced against the identity field's declared type when `entity`
|
|
232
|
+
# has a single identity head and a matching attribute; `value` unchanged
|
|
233
|
+
# otherwise (no identity head, a composite identity, or no matching attribute)
|
|
189
234
|
def hydrate_entity_identity(aggregate, entity, value)
|
|
190
235
|
return value if value.is_a?(self)
|
|
191
236
|
|
|
@@ -198,9 +243,12 @@ module Hecks
|
|
|
198
243
|
for_attribute(aggregate, field, value)
|
|
199
244
|
end
|
|
200
245
|
|
|
246
|
+
# Hydrates a `list_of` attribute's offered value when `attribute.type`
|
|
247
|
+
# names a value object rather than an entity.
|
|
248
|
+
#
|
|
201
249
|
# The value-object sibling of the entity branch above: an element
|
|
202
250
|
# already shaped like the target `Value` (or a `Hash`/scalar that
|
|
203
|
-
# `fields_for` can still open) is rebuilt through the
|
|
251
|
+
# `fields_for` can still open) is rebuilt through the same `build`
|
|
204
252
|
# a scalar composite attribute already uses (`for_attribute`'s own
|
|
205
253
|
# `coerced = ... build(value_object, fields_for(...), aggregate)`
|
|
206
254
|
# line) — same defaults, same `pattern:`/`admits:`/invariant
|
|
@@ -210,21 +258,32 @@ module Hecks
|
|
|
210
258
|
# element passes through unchanged, exactly as the entity branch's
|
|
211
259
|
# own non-Hash elements do.
|
|
212
260
|
#
|
|
213
|
-
#
|
|
261
|
+
# Not `Array(value).map` (unlike the entity branch above) — `value`
|
|
214
262
|
# here is not always genuinely list-shaped. `MutationApplier#
|
|
215
263
|
# removed`'s own `Value.for_attribute(aggregate, attribute, value)`
|
|
216
|
-
# call (`attribute` = the
|
|
217
|
-
# = the single
|
|
264
|
+
# call (`attribute` = the list attribute, `mutation.target`; `value`
|
|
265
|
+
# = the single remove-target argument, already a real `Value` by
|
|
218
266
|
# the time it gets here) reuses this exact branch — for `remove:`,
|
|
219
267
|
# not for a whole-list `sets`. `Array(a_real_Value)` alone would be
|
|
220
268
|
# harmless (`Value` defines neither `to_a` nor `to_ary`, so Kernel
|
|
221
|
-
# wraps it `[value]`), but `Array(a_Hash)` is
|
|
269
|
+
# wraps it `[value]`), but `Array(a_Hash)` is not harmless: Ruby's
|
|
222
270
|
# `Array()` opens a bare Hash into its own `[[k, v], ...]` pairs,
|
|
223
271
|
# not `[hash]` — silently shredding a single-element Hash-shaped
|
|
224
272
|
# target into garbage instead of hydrating it. Branching on
|
|
225
273
|
# `value.is_a?(Array)` up front (true only for a genuine whole-list
|
|
226
274
|
# `sets`/hydrate load) keeps the single-target shape a single
|
|
227
275
|
# target, hydrated the same way, never listified.
|
|
276
|
+
#
|
|
277
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `attribute` is declared
|
|
278
|
+
# on, or a construct that does not respond to `value_object` (an entity), in
|
|
279
|
+
# which case `value` passes through unchanged
|
|
280
|
+
# @param attribute [Bluebook::Attribute] the `list_of` attribute being hydrated
|
|
281
|
+
# @param value [Array<Hash, Object>, Object] the offered value: a whole-list
|
|
282
|
+
# Array, or a single remove-target value
|
|
283
|
+
# @return [Object] `value` unchanged when `aggregate` does not respond to
|
|
284
|
+
# `value_object`, or `attribute.type` names no value object; otherwise a
|
|
285
|
+
# frozen Array of built `Value`s for a whole-list Array, or a single built
|
|
286
|
+
# `Value` (via `hydrate_value_object_element`) otherwise
|
|
228
287
|
def hydrate_value_object_list(aggregate, attribute, value)
|
|
229
288
|
return value unless aggregate.respond_to?(:value_object)
|
|
230
289
|
|
|
@@ -237,6 +296,19 @@ module Hecks
|
|
|
237
296
|
Freezer.deep(hydrated)
|
|
238
297
|
end
|
|
239
298
|
|
|
299
|
+
# Rebuilds one `list_of` element into a real, validated `Value`, unless it
|
|
300
|
+
# already is one of `value_object`'s own type.
|
|
301
|
+
#
|
|
302
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `value_object` is
|
|
303
|
+
# resolved against
|
|
304
|
+
# @param attribute [Bluebook::Attribute] the `list_of` attribute `element`
|
|
305
|
+
# belongs to
|
|
306
|
+
# @param value_object [Class] the `Bluebook::ValueObject` subclass `element`
|
|
307
|
+
# is built as
|
|
308
|
+
# @param element [Object] the raw element to build: a Hash of fields, a scalar
|
|
309
|
+
# `fields_for` can still open, or an already-built `Value`
|
|
310
|
+
# @return [Runtime::Value] `element` unchanged if it is already a `Value` of
|
|
311
|
+
# `value_object`'s own type; otherwise a freshly built one
|
|
240
312
|
def hydrate_value_object_element(aggregate, attribute, value_object, element)
|
|
241
313
|
return element if element.is_a?(self) && element.type_name == value_object.hecks_name
|
|
242
314
|
|
data/lib/hecks/runtime/value.rb
CHANGED
|
@@ -8,7 +8,7 @@ require_relative "value/admission"
|
|
|
8
8
|
module Hecks
|
|
9
9
|
module Runtime
|
|
10
10
|
# A typed value object in hand: frozen fields, read by name. How one is
|
|
11
|
-
#
|
|
11
|
+
# made — coerced from a raw argument, checked against its declared
|
|
12
12
|
# numeric types, patterns and closed sets — is the class-side engine in
|
|
13
13
|
# value/coercion.rb, value/entity_list_coercion.rb (a `list_of`
|
|
14
14
|
# attribute's own elements — split out once growing Coercion tripped
|
|
@@ -22,10 +22,10 @@ module Hecks
|
|
|
22
22
|
|
|
23
23
|
attr_reader :value_object
|
|
24
24
|
|
|
25
|
-
#
|
|
25
|
+
# Frozen through, not just on top.
|
|
26
26
|
#
|
|
27
27
|
# `@fields.freeze` alone stops a key being added or removed and
|
|
28
|
-
# nothing else: the String, Array or Hash a field
|
|
28
|
+
# nothing else: the String, Array or Hash a field holds stays
|
|
29
29
|
# mutable, so `vo[:value] << "!"` edits a value object in place —
|
|
30
30
|
# demonstrated on a real dispatch before this was written, not
|
|
31
31
|
# supposed. Same shape as the three freezing bugs already fixed
|
|
@@ -33,29 +33,67 @@ module Hecks
|
|
|
33
33
|
# was frozen and the contents were not.
|
|
34
34
|
#
|
|
35
35
|
# A value object is the one thing in the domain that has no
|
|
36
|
-
# identity to change over — `with` already answers a
|
|
36
|
+
# identity to change over — `with` already answers a new one rather
|
|
37
37
|
# than mutating — so freezing it through is what it always claimed
|
|
38
38
|
# to be.
|
|
39
|
+
#
|
|
40
|
+
# @param value_object [Bluebook::ValueObject] the declared type this instance is one of
|
|
41
|
+
# @param fields [Hash] the type's fields, keyed by name (String or Symbol); deep-frozen
|
|
42
|
+
# and stored with Symbol keys
|
|
39
43
|
def initialize(value_object, fields)
|
|
40
44
|
@value_object = value_object
|
|
41
45
|
@fields = Freezer.deep(fields.transform_keys(&:to_sym))
|
|
42
46
|
freeze
|
|
43
47
|
end
|
|
44
48
|
|
|
49
|
+
# Reads the declared type's own name.
|
|
50
|
+
#
|
|
51
|
+
# @return [String] the value object's `hecks_name`
|
|
45
52
|
def type_name = @value_object.hecks_name
|
|
53
|
+
|
|
54
|
+
# Reads one field.
|
|
55
|
+
#
|
|
56
|
+
# @param field [String, Symbol] the field name; `:value` reads the sole field of a
|
|
57
|
+
# single-attribute value object, whatever it is actually named
|
|
58
|
+
# @return [Object, nil] the field's coerced value; nil if the field is not held
|
|
46
59
|
def [](field) = @fields[resolve_field(field)]
|
|
60
|
+
|
|
61
|
+
# Answers whether this value object holds the named field.
|
|
62
|
+
#
|
|
63
|
+
# @param field [String, Symbol] the field name; `:value` resolves the same way `[]` does
|
|
64
|
+
# @return [Boolean] true when the field is held
|
|
47
65
|
def key?(field) = @fields.key?(resolve_field(field))
|
|
48
66
|
def to_h = @fields.transform_values { |value| self.class.materialize(value) }
|
|
67
|
+
|
|
68
|
+
# Renders this value object as JSON, through the same shape `to_h` builds.
|
|
69
|
+
#
|
|
70
|
+
# @return [String] a JSON object of the materialized fields
|
|
49
71
|
def to_json(*) = JSON.generate(to_h)
|
|
50
72
|
|
|
51
73
|
def ==(other)
|
|
52
74
|
other.is_a?(self.class) && other.type_name == type_name && other.to_h == to_h
|
|
53
75
|
end
|
|
54
76
|
|
|
77
|
+
# Builds a new value object of the same type with one field replaced, re-validated.
|
|
78
|
+
#
|
|
79
|
+
# @param field [String, Symbol] the field to replace; `:value` resolves the same way
|
|
80
|
+
# `[]` does
|
|
81
|
+
# @param value [Object] the field's new, uncoerced value
|
|
82
|
+
# @return [Runtime::Value] a new instance of the same type, with `field` replaced
|
|
83
|
+
# @raise [Runtime::TypeMismatch] if the new fields do not satisfy the type's declared
|
|
84
|
+
# shape (an unknown field, a missing required one, a wrong numeric type, …)
|
|
85
|
+
# @raise [Runtime::InvariantViolation] if the new fields violate one of the type's own
|
|
86
|
+
# invariants, or are not a member of its closed set
|
|
55
87
|
def with(field, value)
|
|
56
88
|
self.class.build(@value_object, @fields.merge(resolve_field(field) => value))
|
|
57
89
|
end
|
|
58
90
|
|
|
91
|
+
# Recursively converts a `Runtime::Value` (and any nested inside a Hash or Array) to
|
|
92
|
+
# plain data.
|
|
93
|
+
#
|
|
94
|
+
# @param value [Object] the value to materialize; anything that is not a `Runtime::Value`,
|
|
95
|
+
# Array or Hash passes through unchanged
|
|
96
|
+
# @return [Object] `value` with every nested `Runtime::Value` replaced by its own `to_h`
|
|
59
97
|
def self.materialize(value)
|
|
60
98
|
case value
|
|
61
99
|
when self then value.to_h
|
|
@@ -68,27 +106,33 @@ module Hecks
|
|
|
68
106
|
# `materialize`, but a single-attribute value object (`sole_attribute`
|
|
69
107
|
# — [[feedback_name_the_scalar_field]]) recurses into its own bare
|
|
70
108
|
# field instead of building `{field: ...}` — a Board's own `label`
|
|
71
|
-
# unwraps to `"Kanban"`, not `{value: "Kanban"}`.
|
|
109
|
+
# unwraps to `"Kanban"`, not `{value: "Kanban"}`. Not a replacement
|
|
72
110
|
# for `materialize` itself: every existing report/query/command
|
|
73
111
|
# caller keeps the wrapped shape it already depends on (Banking's
|
|
74
112
|
# own `CustomerPortfolio` reads `payment[:amount][:cents]`, and
|
|
75
113
|
# changing that out from under it would be a real breaking change,
|
|
76
114
|
# not a bug fix). This is read_model_interpreter.rb's own opt-in,
|
|
77
|
-
# used
|
|
115
|
+
# used only for a `group_by`-declared head's own rows — grouping
|
|
78
116
|
# needs a real scalar to key by regardless, so a report already
|
|
79
117
|
# asking for that gets the unwrap for free.
|
|
118
|
+
#
|
|
119
|
+
# @param value [Object] the value to materialize; anything that is not a `Runtime::Value`,
|
|
120
|
+
# Array or Hash passes through unchanged
|
|
121
|
+
# @return [Object] `value` with every nested `Runtime::Value` replaced by its own sole
|
|
122
|
+
# field's value (recursively unwrapped), or by its own `Hash` of fields when it has
|
|
123
|
+
# more than one
|
|
80
124
|
def self.materialize_unwrapped(value)
|
|
81
125
|
case value
|
|
82
126
|
when self
|
|
83
127
|
sole = value.value_object.sole_attribute
|
|
84
128
|
return materialize_unwrapped(value[sole.name]) if sole
|
|
85
129
|
|
|
86
|
-
#
|
|
130
|
+
# Not `value.to_h` — `Value#to_h` materializes each field through
|
|
87
131
|
# plain `materialize`, so a VO nested inside a multi-attribute VO
|
|
88
132
|
# would already be a plain Hash by the time this method ever saw
|
|
89
133
|
# it, and never reach the `when self` branch above. Read each
|
|
90
134
|
# field straight off `value` instead, so recursion actually
|
|
91
|
-
# happens through
|
|
135
|
+
# happens through this method the whole way down.
|
|
92
136
|
value.value_object.attributes.to_h { |attr| [attr.name, materialize_unwrapped(value[attr.name])] }
|
|
93
137
|
when Array then value.map { |item| materialize_unwrapped(item) }
|
|
94
138
|
when Hash then value.transform_values { |item| materialize_unwrapped(item) }
|
|
@@ -96,22 +140,28 @@ module Hecks
|
|
|
96
140
|
end
|
|
97
141
|
end
|
|
98
142
|
|
|
99
|
-
#
|
|
143
|
+
# Reduces an append-only sub-log to its current state — the same
|
|
100
144
|
# "a later fact supersedes an earlier one" reduction this runtime
|
|
101
|
-
# already performs replaying an
|
|
145
|
+
# already performs replaying an aggregate's own command history
|
|
102
146
|
# into its current attributes, applied here to a single `list_of`
|
|
103
|
-
#
|
|
147
|
+
# field acting as its own miniature append-only log (a placement
|
|
104
148
|
# history, a tombstone-style soft-delete list, a versioned
|
|
105
149
|
# setting). `rows` is what a `list_of` attribute hands back — an
|
|
106
150
|
# Array of `Value`, in append order — and `key` names the field
|
|
107
151
|
# that identifies "the same logical thing" across entries.
|
|
108
152
|
#
|
|
109
|
-
#
|
|
153
|
+
# **Grouping only, never interpretation**. What counts as "removed,"
|
|
110
154
|
# how to order what survives — that meaning belongs to whichever
|
|
111
155
|
# domain declared the field, never here: a generic reduction that
|
|
112
156
|
# started guessing domain semantics would need to keep guessing
|
|
113
157
|
# forever, once per shape of "gone" any caller ever invents. The
|
|
114
158
|
# caller filters and sorts the result; this only groups it.
|
|
159
|
+
#
|
|
160
|
+
# @param rows [Array<Runtime::Value>] a `list_of` attribute's own elements, in append order
|
|
161
|
+
# @param key [Symbol] the method to call on each row to find "the same logical thing"
|
|
162
|
+
# across entries (typically a field reader)
|
|
163
|
+
# @return [Array<Runtime::Value>] one row per distinct `key` value, each the latest row
|
|
164
|
+
# that had it
|
|
115
165
|
def self.latest_by(rows, key)
|
|
116
166
|
rows.to_h { |row| [row.public_send(key), row] }.values
|
|
117
167
|
end
|
|
@@ -119,17 +169,17 @@ module Hecks
|
|
|
119
169
|
def method_missing(name, *args)
|
|
120
170
|
return @fields[name] if @fields.key?(name)
|
|
121
171
|
|
|
122
|
-
#
|
|
172
|
+
# The language rule, not a convenience: any value object with
|
|
123
173
|
# exactly one declared attribute answers `.value`, whatever that
|
|
124
174
|
# attribute is actually named — a single-attribute value object
|
|
125
|
-
# is a
|
|
175
|
+
# is a name for a scalar, not a genuine group
|
|
126
176
|
# ([[feedback_name_the_scalar_field]], `Behaviour::ValueObject#
|
|
127
177
|
# sole_attribute`), so `money.value` reads `Money`'s own `amount`
|
|
128
178
|
# exactly as `label.value` reads a shorthand-declared `value`.
|
|
129
|
-
#
|
|
130
|
-
# literally named `value` is already answered there (and
|
|
179
|
+
# After the real-field lookup above, on purpose: a field
|
|
180
|
+
# literally named `value` is already answered there (and is the
|
|
131
181
|
# sole attribute whenever the count is one), so this branch only
|
|
132
|
-
# ever aliases, never shadows. A
|
|
182
|
+
# ever aliases, never shadows. A multi-attribute value object
|
|
133
183
|
# keeps its NoMethodError — `sole_attribute` answers nil for it,
|
|
134
184
|
# and falling through to `super` is exactly the refusal it
|
|
135
185
|
# always gave: with two or more fields there is no single value
|
|
@@ -151,15 +201,15 @@ module Hecks
|
|
|
151
201
|
|
|
152
202
|
private
|
|
153
203
|
|
|
154
|
-
#
|
|
204
|
+
# **The `.value` alias for indexed access** — the same language rule
|
|
155
205
|
# `method_missing` above enforces for method reads, applied to
|
|
156
206
|
# `[]`/`key?`/`with`: `:value` names a single-attribute value
|
|
157
207
|
# object's sole field whatever that field is actually called. A
|
|
158
|
-
#
|
|
208
|
+
# real key always wins first (a field literally named `value` is
|
|
159
209
|
# its own answer, and is the sole attribute anyway whenever the
|
|
160
210
|
# count is one), so this only ever resolves a `:value` that would
|
|
161
|
-
# otherwise
|
|
162
|
-
# `with(:value, x)` in particular
|
|
211
|
+
# otherwise miss — it can never redirect a genuine field read.
|
|
212
|
+
# `with(:value, x)` in particular needs this: merging a literal
|
|
163
213
|
# `:value` key beside a sole field named `amount` would build a
|
|
164
214
|
# two-key hash for a one-field shape and be refused (or worse,
|
|
165
215
|
# stored) downstream — aliasing at the merge is what keeps the
|
data/lib/hecks/runtime.rb
CHANGED
|
@@ -6,24 +6,24 @@
|
|
|
6
6
|
# declaration is being collected into — sat on the top-level Hecks module
|
|
7
7
|
# beside the DSL words.
|
|
8
8
|
#
|
|
9
|
-
# What lives here is what
|
|
9
|
+
# What lives here is what runs a domain :
|
|
10
10
|
#
|
|
11
11
|
# Runtime.boot(path) load a directory and return its Dispatcher
|
|
12
12
|
# Runtime.with_registry(r) bind the ambient registry for the duration of a load
|
|
13
13
|
# Runtime.current_registry the registry a declaration is landing in, or nil
|
|
14
14
|
#
|
|
15
|
-
# What does
|
|
15
|
+
# What does not live here is what declares one — `Hecks.bluebook`,
|
|
16
16
|
# `.hecksagon`, `.port`, `.adapter`, `.world` are loading words and stay on the
|
|
17
17
|
# top-level module, which now reads as a facade over this.
|
|
18
18
|
#
|
|
19
19
|
# runtime = Hecks::Runtime.boot("examples/pizzas/bluebook")
|
|
20
20
|
# runtime.dispatch("Pizzas::Pizza.CreatePizza", name: "Margherita")
|
|
21
21
|
#
|
|
22
|
-
#
|
|
22
|
+
# Note, and it is the reason this file exists : `current_registry` is still
|
|
23
23
|
# process-global. Each boot builds a fresh Registry, and `Loader.bind_runtime`
|
|
24
|
-
# installs a fresh facade door whose modules close over
|
|
24
|
+
# installs a fresh facade door whose modules close over that boot's dispatcher
|
|
25
25
|
# — no class-level runtime binding remains, so two boots in one process no
|
|
26
|
-
# longer share dispatch state, only the top-level
|
|
26
|
+
# longer share dispatch state, only the top-level name (the last-bound door
|
|
27
27
|
# wins the constant, which is what per-boot install means). Owning the state
|
|
28
28
|
# here was the first move ; the dispatcher reachable per-runtime rather than
|
|
29
29
|
# through a constant was the second, and it is done — the door is the only
|
|
@@ -68,17 +68,40 @@ module Hecks
|
|
|
68
68
|
|
|
69
69
|
# Load a bluebook directory and return the Dispatcher bound to it.
|
|
70
70
|
# `install_facade:`, `environment:` — see Loader.boot.
|
|
71
|
+
#
|
|
72
|
+
# @param path [String] path to a domain directory, or a file inside one
|
|
73
|
+
# @param shared [String, nil] a shared-root override; see `Loader.boot`
|
|
74
|
+
# @param install_facade [Boolean] whether to install the `Widget::Item.Add`-style
|
|
75
|
+
# Ruby facade constants for this boot
|
|
76
|
+
# @param environment [String, nil] the environment name passed through to
|
|
77
|
+
# `Adapters::Folder#load_domain`
|
|
78
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the dispatcher bound
|
|
79
|
+
# to the booted domain
|
|
71
80
|
def boot(path, shared: nil, install_facade: true, environment: nil)
|
|
72
81
|
Loader.boot(path, shared: shared, install_facade: install_facade, environment: environment)
|
|
73
82
|
end
|
|
74
83
|
|
|
75
84
|
# `paths` form — see Loader.boot_files.
|
|
85
|
+
#
|
|
86
|
+
# @param paths [String, Array<String>] one or more file paths within the domain
|
|
87
|
+
# to load, instead of the whole directory
|
|
88
|
+
# @param shared [String, nil] a shared-root override; see `Loader.boot_files`
|
|
89
|
+
# @param install_facade [Boolean] whether to install the `Widget::Item.Add`-style
|
|
90
|
+
# Ruby facade constants for this boot
|
|
91
|
+
# @param environment [String, nil] the environment name passed through to the
|
|
92
|
+
# selected-file loader
|
|
93
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the dispatcher bound
|
|
94
|
+
# to the booted domain
|
|
76
95
|
def boot_files(paths, shared: nil, install_facade: true, environment: nil)
|
|
77
96
|
Loader.boot_files(paths, shared: shared, install_facade: install_facade, environment: environment)
|
|
78
97
|
end
|
|
79
98
|
|
|
80
99
|
# Bind the ambient registry for the duration of the block, restoring
|
|
81
100
|
# whatever was there before. Nesting is safe ; a raise still restores.
|
|
101
|
+
#
|
|
102
|
+
# @param registry [Runtime::Registry] the registry to make current for the block
|
|
103
|
+
# @yield the code that should see `registry` as `current_registry`
|
|
104
|
+
# @return [Object] the block's result
|
|
82
105
|
def with_registry(registry)
|
|
83
106
|
previous = @current_registry
|
|
84
107
|
@current_registry = registry
|
|
@@ -91,7 +114,7 @@ module Hecks
|
|
|
91
114
|
# the block — who a command's declared `role`, if any, is checked
|
|
92
115
|
# against.
|
|
93
116
|
#
|
|
94
|
-
# `as_of:` is
|
|
117
|
+
# `as_of:` is optional, same opt-in shape as `actor_id:` — a caller
|
|
95
118
|
# that wants a Governance `RoleAssignment`'s `starts_at` enforced
|
|
96
119
|
# passes `as_of: Ports::Clock.now(registry)` here, at the door,
|
|
97
120
|
# exactly where `cli_runner.rb` already merges `Clock.now` into a
|
|
@@ -99,11 +122,20 @@ module Hecks
|
|
|
99
122
|
# itself — see `Ports::Clock`'s own header for why — so an unbound
|
|
100
123
|
# `as_of` leaves `starts_at` unchecked, exactly as before.
|
|
101
124
|
#
|
|
102
|
-
# `scope:` is
|
|
125
|
+
# `scope:` is optional too — a caller that states which scope it is
|
|
103
126
|
# acting in gets that scope checked against the matching
|
|
104
127
|
# `RoleAssignment`'s own `scope`, not just its `role_name`. See
|
|
105
128
|
# `Runtime::Caller::Current`'s own header for why this lives here
|
|
106
129
|
# rather than as a command-level DSL construct.
|
|
130
|
+
# @param role [String, Symbol] the role to check the caller against
|
|
131
|
+
# @param actor_id [String, nil] who is calling, checked against a real Governance
|
|
132
|
+
# `RoleAssignment` when given; string-equality only against `role` when nil
|
|
133
|
+
# @param as_of [Integer, nil] Unix epoch seconds to check a matching `RoleAssignment`'s
|
|
134
|
+
# own `starts_at` against; unchecked when nil
|
|
135
|
+
# @param scope [String, nil] the scope to check a matching `RoleAssignment`'s own
|
|
136
|
+
# `scope` against; unchecked when nil
|
|
137
|
+
# @yield the code to run with this caller bound
|
|
138
|
+
# @return [Object] the block's result
|
|
107
139
|
def as_caller(role:, actor_id: nil, as_of: nil, scope: nil, &)
|
|
108
140
|
Caller.as(role: role, actor_id: actor_id, as_of: as_of, scope: scope, &)
|
|
109
141
|
end
|