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
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module QuerySpecification
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# `record[field]` lookup
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# implementations of "what does
|
|
10
|
-
# more than a language gets to
|
|
3
|
+
# One reading of a dotted query-field path, shared by every place that
|
|
4
|
+
# would otherwise invent its own. Left to themselves they disagree: a
|
|
5
|
+
# reference interpreter doing a bare `record[field]` lookup silently
|
|
6
|
+
# matches nothing for a dotted path, an SQL side that splits dots but
|
|
7
|
+
# judges numericness from the first nested segment only quietly
|
|
8
|
+
# compares a two-level path as text, and a build seal walks the
|
|
9
|
+
# declaration graph a third way. Three implementations of "what does
|
|
10
|
+
# pizza.price_cents.cents mean" is two more than a language gets to
|
|
11
|
+
# have.
|
|
11
12
|
#
|
|
12
13
|
# Two sides, deliberately in one file so they cannot drift:
|
|
13
14
|
#
|
|
14
|
-
# dig(holder, field) —
|
|
15
|
+
# dig(holder, field) — value side: walk a record's held state,
|
|
15
16
|
# segment by segment, through Value
|
|
16
17
|
# objects and plain hashes alike.
|
|
17
|
-
# leaf_attribute / numeric? —
|
|
18
|
+
# leaf_attribute / numeric? — declaration side: walk the declared
|
|
18
19
|
# shape to the attribute a path lands on.
|
|
19
20
|
# Callers supply value-object lookup as a
|
|
20
21
|
# block, because an Aggregate and a
|
|
@@ -26,22 +27,39 @@ module Hecks
|
|
|
26
27
|
NUMERIC_PRIMITIVES = %w[Integer Float].freeze
|
|
27
28
|
SCALAR_PRIMITIVES = %w[String Integer Float TrueClass FalseClass].freeze
|
|
28
29
|
|
|
30
|
+
# Reads the value a dotted field path names out of a record's held state.
|
|
31
|
+
#
|
|
29
32
|
# The held value a dotted field names, or nil — never a raise. The
|
|
30
33
|
# first segment reads off the record (an Instance, a Value, or a
|
|
31
34
|
# plain row hash); the rest read through whatever each step holds. A
|
|
32
35
|
# stored nested value object is a plain hash by the time it is read
|
|
33
36
|
# back, keyed by symbol in memory and by string off a wire decode,
|
|
34
37
|
# so both spellings are tried — `key?` first, never `||`, because
|
|
35
|
-
# `||` falls through a genuinely-stored `false` to the
|
|
38
|
+
# `||` falls through a genuinely-stored `false` to the other
|
|
36
39
|
# spelling (usually absent) and returns `nil` instead. The seal
|
|
37
40
|
# admits boolean leaves (`SCALAR_PRIMITIVES` below), so a `false`
|
|
38
41
|
# here is a real, held answer, not a missing one.
|
|
42
|
+
#
|
|
43
|
+
# @param holder [Runtime::Instance, Runtime::Value, Hash, nil] the record, value
|
|
44
|
+
# object or row Hash the first segment is read from
|
|
45
|
+
# @param field [String, Symbol, nil] the path, segments separated by `.`, such as
|
|
46
|
+
# `"price.cents"`; a bare field name is a one-segment path
|
|
47
|
+
# @return [Object, nil] the value held at the end of the path; `nil` when `field` is
|
|
48
|
+
# `nil`, a segment is absent, or a step lands on `nil` or an Array
|
|
39
49
|
def dig(holder, field)
|
|
40
50
|
return nil if field.nil?
|
|
41
51
|
|
|
42
52
|
field.to_s.split(".").reduce(holder) { |current, segment| read(current, segment) }
|
|
43
53
|
end
|
|
44
54
|
|
|
55
|
+
# Reads one path segment off whatever the previous step held — a single
|
|
56
|
+
# step of `dig`.
|
|
57
|
+
#
|
|
58
|
+
# @param current [Runtime::Instance, Runtime::Value, Hash, Array, nil] the value the
|
|
59
|
+
# walk has reached; a Hash is tried by Symbol key, then by String key
|
|
60
|
+
# @param segment [String] one segment of the dotted path
|
|
61
|
+
# @return [Object, nil] the member named `segment`; `nil` when `current` is `nil` or
|
|
62
|
+
# an Array, or holds no such member
|
|
45
63
|
def read(current, segment)
|
|
46
64
|
return nil if current.nil?
|
|
47
65
|
|
|
@@ -54,7 +72,7 @@ module Hecks
|
|
|
54
72
|
# an Array broke it: `Array#[]` demands an Integer index, so
|
|
55
73
|
# `current[segment]` (a String) raised `TypeError` straight
|
|
56
74
|
# through `dig` instead of answering nil. A dotted path stepping
|
|
57
|
-
#
|
|
75
|
+
# into a list_of attribute (`where "tags.name" == "x"` against a
|
|
58
76
|
# bare list, rather than each element) has no single member a
|
|
59
77
|
# bare index would name anyway — nil is the honest answer, the
|
|
60
78
|
# same one a dangling reference or a missing key already gets.
|
|
@@ -63,11 +81,23 @@ module Hecks
|
|
|
63
81
|
current[segment]
|
|
64
82
|
end
|
|
65
83
|
|
|
84
|
+
# Walks the declared shape to the attribute a dotted path lands on.
|
|
85
|
+
#
|
|
66
86
|
# The declared attribute a path lands on, or nil. `segments` is the
|
|
67
87
|
# dotted tail — for a bare field it is empty and the root attribute
|
|
68
88
|
# is its own leaf. The walk stops dead at a reference (an id is a
|
|
69
89
|
# scalar; nothing nests under it) and at any member the declared
|
|
70
90
|
# value object does not carry.
|
|
91
|
+
#
|
|
92
|
+
# @param attribute [Bluebook::Attribute, nil] the root attribute the path's first
|
|
93
|
+
# segment names; `nil` (no such attribute) answers `nil`
|
|
94
|
+
# @param segments [Array<String>] the path's remaining segments, `[]` for a bare field
|
|
95
|
+
# @yield looks a value object up by name in the caller's own declarations
|
|
96
|
+
# @yieldparam type [String] the type name of the attribute being stepped into
|
|
97
|
+
# @yieldreturn [Class<Bluebook::ValueObject>, nil] the declared shape, or `nil` when
|
|
98
|
+
# the type is not a value object the caller knows
|
|
99
|
+
# @return [Bluebook::Attribute, nil] the attribute at the end of the path; `nil` when
|
|
100
|
+
# a step crosses a reference, an undeclared type or a member that does not exist
|
|
71
101
|
def leaf_attribute(attribute, segments)
|
|
72
102
|
current = attribute
|
|
73
103
|
segments.each do |segment|
|
|
@@ -79,12 +109,24 @@ module Hecks
|
|
|
79
109
|
current
|
|
80
110
|
end
|
|
81
111
|
|
|
112
|
+
# Decides from the declared shape whether a field path holds a number.
|
|
113
|
+
#
|
|
82
114
|
# May an ordered comparator (lt/lte/gt/gte) land here, and may an
|
|
83
|
-
# ORDER BY cast here numerically? A bare field keeps the one-level
|
|
115
|
+
# `ORDER BY` cast here numerically? A bare field keeps the one-level
|
|
84
116
|
# convention every adapter already implements: a numeric primitive,
|
|
85
117
|
# or a value object carrying at least one numeric member. A dotted
|
|
86
118
|
# path must land on a numeric primitive itself — the convention does
|
|
87
|
-
# not reach through a named path, it
|
|
119
|
+
# not reach through a named path, it is the absence of one.
|
|
120
|
+
#
|
|
121
|
+
# @param attribute [Bluebook::Attribute, nil] the root attribute the path starts at;
|
|
122
|
+
# `nil` answers `false`
|
|
123
|
+
# @param segments [Array<String>] the path's remaining segments, `[]` for a bare field
|
|
124
|
+
# @yield looks a value object up by name, as for `leaf_attribute`
|
|
125
|
+
# @yieldparam type [String] the type name to look up
|
|
126
|
+
# @yieldreturn [Class<Bluebook::ValueObject>, nil] the declared shape, or `nil`
|
|
127
|
+
# @return [Boolean] `true` when the leaf is an `Integer` or `Float`, or a bare field
|
|
128
|
+
# naming a value object with at least one such member; `false` for a list, a
|
|
129
|
+
# reference, or a path that lands nowhere
|
|
88
130
|
def numeric?(attribute, segments, &)
|
|
89
131
|
leaf = leaf_attribute(attribute, segments, &)
|
|
90
132
|
return false if leaf.nil? || leaf.list? || leaf.reference?
|
|
@@ -95,9 +137,21 @@ module Hecks
|
|
|
95
137
|
!shape.nil? && shape.attributes.any? { |member| NUMERIC_PRIMITIVES.include?(member.type.to_s) }
|
|
96
138
|
end
|
|
97
139
|
|
|
98
|
-
#
|
|
140
|
+
# Decides whether a path ends on a scalar primitive that every engine
|
|
141
|
+
# compares the same way.
|
|
142
|
+
#
|
|
143
|
+
# A dotted path must end on a scalar member — landing on a value
|
|
99
144
|
# object would hand SQL a JSON object where the reference
|
|
100
145
|
# interpreter unwraps a hash, and the two would answer differently.
|
|
146
|
+
#
|
|
147
|
+
# @param attribute [Bluebook::Attribute, nil] the root attribute the path starts at;
|
|
148
|
+
# `nil` answers `false`
|
|
149
|
+
# @param segments [Array<String>] the path's remaining segments, `[]` for a bare field
|
|
150
|
+
# @yield looks a value object up by name, as for `leaf_attribute`
|
|
151
|
+
# @yieldparam type [String] the type name to look up
|
|
152
|
+
# @yieldreturn [Class<Bluebook::ValueObject>, nil] the declared shape, or `nil`
|
|
153
|
+
# @return [Boolean] `true` when the leaf is a non-list, non-reference attribute typed
|
|
154
|
+
# `String`, `Integer`, `Float`, `TrueClass` or `FalseClass`
|
|
101
155
|
def scalar_leaf?(attribute, segments, &)
|
|
102
156
|
leaf = leaf_attribute(attribute, segments, &)
|
|
103
157
|
!leaf.nil? && !leaf.list? && !leaf.reference? && SCALAR_PRIMITIVES.include?(leaf.type.to_s)
|
|
@@ -2,25 +2,25 @@ require_relative "../naming"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module QuerySpecification
|
|
5
|
-
#
|
|
5
|
+
# One reading of a dotted query-field path that hops through a
|
|
6
6
|
# reference into another aggregate's own shape — FieldPath's
|
|
7
|
-
# sibling, not its member. FieldPath walks a
|
|
7
|
+
# sibling, not its member. FieldPath walks a shape, which cannot
|
|
8
8
|
# loop, and answers nil, never raising, because there is nothing
|
|
9
|
-
# left to say beyond "not found." HopPath walks the
|
|
10
|
-
#
|
|
11
|
-
# refuses any reference
|
|
9
|
+
# left to say beyond "not found." HopPath walks the reference
|
|
10
|
+
# graph instead — `BluebookBuilder#validate_no_bidirectional_references!`
|
|
11
|
+
# refuses any reference cycle at declaration time (ADR 0025,
|
|
12
12
|
# "References" — widened from a direct pair to any ring, DFS over
|
|
13
13
|
# the chapter's own reference graph), so what's left to guard here
|
|
14
14
|
# is depth, not cycles: MAX_HOPS below, and this module's own
|
|
15
15
|
# `refusal` states for a target the chapter doesn't resolve. A walk
|
|
16
|
-
# here still needs to say
|
|
16
|
+
# here still needs to say why it stopped, not just that it did.
|
|
17
17
|
#
|
|
18
|
-
# Every method below takes an
|
|
18
|
+
# Every method below takes an attribute array, never a "shape"
|
|
19
19
|
# object — deliberately, because the two real callers hold their
|
|
20
20
|
# attributes differently. `AggregateBuilder` (tier-1 seal, mid-
|
|
21
21
|
# build) exposes `attributes` as a plain reader, but its own
|
|
22
22
|
# `attribute(name, type = String, ...)` is the DSL's attribute-
|
|
23
|
-
#
|
|
23
|
+
# declaring method — calling it as a finder would silently mint a
|
|
24
24
|
# new String attribute instead of looking one up. A sealed
|
|
25
25
|
# `Aggregate` (tier-2 seal, and every runtime target) has a
|
|
26
26
|
# real `attribute(name)` finder, but taking the array either way
|
|
@@ -37,37 +37,51 @@ module Hecks
|
|
|
37
37
|
Plan = Struct.new(:hops, :tail, :refusal, keyword_init: true)
|
|
38
38
|
|
|
39
39
|
# A hop chain long enough to matter is long enough to be a
|
|
40
|
-
# mistake —
|
|
40
|
+
# mistake — not a guard against a walk that cannot terminate.
|
|
41
41
|
# Nothing here loops forever regardless of how the reference
|
|
42
42
|
# graph is shaped: a hop chain is a literal dotted string, fixed
|
|
43
43
|
# at declaration time, and every step consumes exactly one of
|
|
44
|
-
# its own segments — the walk is bounded by what was
|
|
44
|
+
# its own segments — the walk is bounded by what was typed, not
|
|
45
45
|
# by the graph. A self-referential aggregate hopping through
|
|
46
46
|
# itself more than once (`"parent.parent.name"`, a grandparent
|
|
47
47
|
# query) is real, common, and perfectly safe; this exists only
|
|
48
48
|
# to refuse a chain nobody meant to write this long.
|
|
49
49
|
MAX_HOPS = 8
|
|
50
50
|
|
|
51
|
+
# Names the path segment a reference attribute answers to.
|
|
52
|
+
#
|
|
51
53
|
# The segment name a Reference answers to in a hop path — its own
|
|
52
54
|
# declared attribute name, unchanged (ADR 0025, "References":
|
|
53
|
-
# `reference_to` mints that bare name
|
|
55
|
+
# `reference_to` mints that bare name, no `_id`, so there is
|
|
54
56
|
# no derivation left to apply). `proposal/client` (a query hop)
|
|
55
57
|
# and `proposal.client` (the Ruby accessor,
|
|
56
58
|
# `Facade::Handle#define_reference_accessors`) name the same
|
|
57
59
|
# concept the same way.
|
|
60
|
+
#
|
|
61
|
+
# @param attribute [Bluebook::Attribute] a reference-typed attribute
|
|
62
|
+
# @return [String] the attribute's declared name
|
|
58
63
|
def hop_name(attribute) = attribute.name.to_s
|
|
59
64
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
65
|
+
# Decides whether a field path starts by hopping through one of the given
|
|
66
|
+
# references, without resolving the reference's target.
|
|
67
|
+
#
|
|
68
|
+
# Does this path's head cross into another record via `/`? The
|
|
69
|
+
# operator is the answer now, not a name collision to arbitrate —
|
|
62
70
|
# `.` walks fields inside this record, `/` crosses into another
|
|
63
71
|
# one, so a path with no `/` is never a hop, full stop, and the
|
|
64
|
-
#
|
|
72
|
+
# old "a real local attribute wins first" rule (needed only
|
|
65
73
|
# because `.` was overloaded for both meanings, and `client_id`
|
|
66
74
|
# vs `client` was how the two were told apart) has nothing left
|
|
67
75
|
# to arbitrate. Answerable from `attributes` alone — a Reference
|
|
68
76
|
# knows its own `target_name` at declaration, before it can
|
|
69
|
-
# `resolve` it — which is what lets the
|
|
77
|
+
# `resolve` it — which is what lets the aggregate seal recognise
|
|
70
78
|
# a hop it cannot yet check.
|
|
79
|
+
#
|
|
80
|
+
# @param field [String, Symbol] the query field path, such as `:"client/status"`
|
|
81
|
+
# @param attributes [Array<Bluebook::Attribute>] the attributes of the shape the path
|
|
82
|
+
# starts from
|
|
83
|
+
# @return [Boolean] `true` when the path has a `/` and the segment before the first
|
|
84
|
+
# one names a reference attribute
|
|
71
85
|
def hop_head?(field, attributes)
|
|
72
86
|
head, rest = field.to_s.split("/", 2)
|
|
73
87
|
return false unless rest
|
|
@@ -75,16 +89,27 @@ module Hecks
|
|
|
75
89
|
attributes.any? { |candidate| candidate.reference? && hop_name(candidate) == head }
|
|
76
90
|
end
|
|
77
91
|
|
|
78
|
-
#
|
|
92
|
+
# Resolves the first hop of a field path and hands back what is left to walk.
|
|
93
|
+
#
|
|
94
|
+
# **One step**: does `field`'s head hop through one of `attributes`'
|
|
79
95
|
# own references? Answers the resolved `Hop` plus the string
|
|
80
96
|
# still left to walk (itself possibly another `/`-hop, against
|
|
81
|
-
# the
|
|
97
|
+
# the target's own attributes, or a plain `.`-dotted field walk
|
|
82
98
|
# once the hops run out) — or nil, when the head names nothing or
|
|
83
99
|
# the path has no `/` at all. This is the one primitive
|
|
84
100
|
# Runtime::ReferenceHop needs: it recurses hop by hop through its
|
|
85
101
|
# own `apply`, one ordinary same-aggregate query at a time, and
|
|
86
102
|
# never needs the whole chain resolved up front the way a seal
|
|
87
103
|
# does.
|
|
104
|
+
#
|
|
105
|
+
# @param field [String, Symbol] the query field path, such as `"client/region/name"`
|
|
106
|
+
# @param attributes [Array<Bluebook::Attribute>] the attributes of the shape the path
|
|
107
|
+
# starts from
|
|
108
|
+
# @return [Array(Hop, String), nil] the hop and the rest of the path after the first
|
|
109
|
+
# `/`; the hop's `target` is `nil` when the referenced aggregate is not in the
|
|
110
|
+
# declaring chapter. `nil` when the path has no `/` or its head names no reference
|
|
111
|
+
# @raise [Bluebook::DSL::Malformed] if the reference has no `declared_in` aggregate
|
|
112
|
+
# to resolve its target through
|
|
88
113
|
def next_hop(field, attributes)
|
|
89
114
|
head, rest = field.to_s.split("/", 2)
|
|
90
115
|
return nil unless rest
|
|
@@ -96,10 +121,22 @@ module Hecks
|
|
|
96
121
|
[hop, rest]
|
|
97
122
|
end
|
|
98
123
|
|
|
99
|
-
#
|
|
124
|
+
# Resolves every hop of a field path in order, stopping with a reason at
|
|
125
|
+
# the first one it cannot follow.
|
|
126
|
+
#
|
|
127
|
+
# The whole chain, resolved — every hop's target found, in
|
|
100
128
|
# order — for the one caller that needs it all at once:
|
|
101
|
-
# BluebookBuilder#validate_query_hops
|
|
129
|
+
# `BluebookBuilder#validate_query_hops!`, checking a hop chain
|
|
102
130
|
# before anything ever dispatches it.
|
|
131
|
+
#
|
|
132
|
+
# @param field [String, Symbol] the query field path, such as `"client/region/name"`
|
|
133
|
+
# @param attributes [Array<Bluebook::Attribute>] the attributes of the shape the path
|
|
134
|
+
# starts from
|
|
135
|
+
# @return [Plan] `hops` walked so far; `tail` the remaining `.`-dotted field (`nil`
|
|
136
|
+
# on refusal); `refusal` `nil` when clean, `:unresolvable` when the last hop's
|
|
137
|
+
# target is not found, `:too_deep` when the chain exceeds `MAX_HOPS`
|
|
138
|
+
# @raise [Bluebook::DSL::Malformed] if a reference on the path has no `declared_in`
|
|
139
|
+
# aggregate to resolve its target through
|
|
103
140
|
def plan(field, attributes)
|
|
104
141
|
hops = []
|
|
105
142
|
remaining = field.to_s
|
|
@@ -113,7 +150,7 @@ module Hecks
|
|
|
113
150
|
return Plan.new(hops: hops, tail: nil, refusal: :too_deep) if hops.size >= MAX_HOPS
|
|
114
151
|
|
|
115
152
|
# Pushed even unresolved — a caller reporting :unresolvable
|
|
116
|
-
# needs
|
|
153
|
+
# needs this hop's own target_name (real, known at
|
|
117
154
|
# declaration, regardless of whether resolve succeeded), not
|
|
118
155
|
# whatever hop came before it. `.target` is nil on this one
|
|
119
156
|
# entry; every caller checking `hops.last.target` already has
|
|
@@ -9,6 +9,10 @@ module Hecks
|
|
|
9
9
|
class Specification < Common::Options
|
|
10
10
|
attr_reader :joins
|
|
11
11
|
|
|
12
|
+
# @param joins [Array<Hash{Symbol => Object}>] one row per projected aggregate head,
|
|
13
|
+
# keyed `:aggregate` (String target name), `:as` (Symbol output name) and `:many`
|
|
14
|
+
# (Boolean), as `ReadModelBuilder` builds them
|
|
15
|
+
# @param ** [Hash] the shared query options, passed to `Common::Options#initialize`
|
|
12
16
|
def initialize(joins: [], **)
|
|
13
17
|
super(**)
|
|
14
18
|
@joins = joins
|
data/lib/hecks/rendering.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# How a value reads inside a refusal, in one place, because a refusal is an
|
|
5
|
+
# answer and its wording is contract — pinned byte-for-byte by the corpus.
|
|
6
6
|
#
|
|
7
7
|
# `inspect` and JSON agree on scalars and disagree on everything composite :
|
|
8
8
|
# a hash is `{"cents"=>100}` against `{"cents":100}`, an array is `["a", "b"]`
|
|
@@ -17,6 +17,12 @@ module Hecks
|
|
|
17
17
|
module Rendering
|
|
18
18
|
module_function
|
|
19
19
|
|
|
20
|
+
# Renders a value the way it should read inside a refusal message.
|
|
21
|
+
#
|
|
22
|
+
# @param value [Object] the value to render
|
|
23
|
+
# @return [String] `"nil"` for nil, JSON for a Hash/Array or a duck-typed value
|
|
24
|
+
# object (unwrapped to its bare scalar when it has exactly one field), or
|
|
25
|
+
# `value.inspect` for anything else
|
|
20
26
|
def describe(value)
|
|
21
27
|
case value
|
|
22
28
|
when nil then "nil"
|
|
@@ -29,7 +35,7 @@ module Hecks
|
|
|
29
35
|
# ("#<Hecks::Runtime::Value:0x...>") into an otherwise
|
|
30
36
|
# correct domain refusal. Duck-typed on `respond_to?(:to_h)`
|
|
31
37
|
# rather than naming `Runtime::Value` directly — `Runtime::Value`
|
|
32
|
-
# itself requires
|
|
38
|
+
# itself requires this file (`runtime/value.rb`'s own
|
|
33
39
|
# `require_relative "../rendering"`), so naming it here would be
|
|
34
40
|
# circular. A single-field wrapper (the overwhelming common case
|
|
35
41
|
# — an amount, an id, a lifecycle field) unwraps to its bare
|
|
@@ -12,6 +12,13 @@ module Hecks
|
|
|
12
12
|
# to the router, pinned to this specific FQN version rather than
|
|
13
13
|
# the router's default resolution.
|
|
14
14
|
class OptionsProxy
|
|
15
|
+
# @param router [Router] router to dispatch resolved calls through
|
|
16
|
+
# @param realm [String, nil] realm segment of the FQN this proxy pins
|
|
17
|
+
# @param domain [String] domain segment of the FQN this proxy pins
|
|
18
|
+
# @param aggregate [String, nil] aggregate segment, or nil for a domain-level query
|
|
19
|
+
# @param options [Hash{Symbol => Object}] the `.options(...)` call's keywords; only
|
|
20
|
+
# `:version` is accepted
|
|
21
|
+
# @raise [ArgumentError] if `options` has a key other than `:version`
|
|
15
22
|
def initialize(router:, realm:, domain:, aggregate:, options:)
|
|
16
23
|
unknown = options.keys - [:version]
|
|
17
24
|
raise ArgumentError, "unknown router options: #{unknown.join(', ')}" unless unknown.empty?
|
|
@@ -45,10 +52,16 @@ module Hecks
|
|
|
45
52
|
end
|
|
46
53
|
end
|
|
47
54
|
|
|
55
|
+
# @param router [Router] router whose current routes get Ruby constants and methods
|
|
48
56
|
def initialize(router)
|
|
49
57
|
@router = router
|
|
50
58
|
end
|
|
51
59
|
|
|
60
|
+
# Installs every current-version route as a namespace constant/method,
|
|
61
|
+
# plus the aggregate `find`/`all`/`count`/`events`/`repository` door and
|
|
62
|
+
# short `Aggregate.verb` shortcuts.
|
|
63
|
+
#
|
|
64
|
+
# @return [self]
|
|
52
65
|
def install!
|
|
53
66
|
current_entries.each { |entry| install_namespace_entry(entry) }
|
|
54
67
|
install_shortcuts!
|
|
@@ -77,16 +90,16 @@ module Hecks
|
|
|
77
90
|
end
|
|
78
91
|
end
|
|
79
92
|
|
|
80
|
-
# `.find`/`.all`/`.count`/`.events`/`.repository` — the
|
|
93
|
+
# `.find`/`.all`/`.count`/`.events`/`.repository` — the same read/CRUD
|
|
81
94
|
# surface `Facade::Surface::AggregateDoor` gives a plain `Hecks
|
|
82
95
|
# .boot`, missing here until now: `install_namespace_entry` above
|
|
83
|
-
# installs
|
|
96
|
+
# installs one method per declared verb, so an aggregate with no
|
|
84
97
|
# commands or queries of its own shape (or simply never asked for
|
|
85
98
|
# a `find`-shaped query) had no way to look up one record by id
|
|
86
99
|
# through the router surface at all — real gap, hit live building
|
|
87
100
|
# a `List` aggregate meant to be read this way. Grouped by
|
|
88
101
|
# (realm, domain, aggregate) rather than installed per-verb,
|
|
89
|
-
# because unlike a command or query this is the
|
|
102
|
+
# because unlike a command or query this is the same five methods
|
|
90
103
|
# regardless of which verb happened to trigger this aggregate's
|
|
91
104
|
# own namespace module into existing.
|
|
92
105
|
def install_aggregate_doors!
|
data/lib/hecks/router.rb
CHANGED
|
@@ -9,31 +9,60 @@ module Hecks
|
|
|
9
9
|
|
|
10
10
|
attr_reader :register
|
|
11
11
|
|
|
12
|
+
# Loads a project's bluebooks and builds a router over them, without
|
|
13
|
+
# installing any Ruby namespace shortcuts.
|
|
14
|
+
#
|
|
15
|
+
# @param root [String] project root to discover bluebooks under
|
|
16
|
+
# @return [Router] the built router
|
|
12
17
|
def self.load(root) = new(Bluebook::ProjectLoader.load(root))
|
|
13
18
|
|
|
14
19
|
# Install one project router for ordinary application calls. The explicit
|
|
15
20
|
# `Router.load` API remains useful for tests and embedded hosts.
|
|
21
|
+
#
|
|
22
|
+
# @param root [String] project root to discover bluebooks under
|
|
23
|
+
# @return [Router] the booted router, also stored as `.default`
|
|
16
24
|
def self.boot(root)
|
|
17
25
|
router = load(root)
|
|
18
26
|
router.install_namespace!
|
|
19
27
|
@default = router
|
|
20
28
|
end
|
|
21
29
|
|
|
30
|
+
# The process-wide router `.boot` installed.
|
|
31
|
+
#
|
|
32
|
+
# @return [Router] the router `.boot` installed
|
|
33
|
+
# @raise [NotBooted] if `.boot` has not been called yet
|
|
22
34
|
def self.default
|
|
23
35
|
@default || raise(NotBooted, "no project router is booted — call Hecks::Router.boot(root) first")
|
|
24
36
|
end
|
|
25
37
|
|
|
38
|
+
# (see #dispatch)
|
|
26
39
|
def self.dispatch(address, **args) = default.dispatch(address, **args)
|
|
40
|
+
# (see #query)
|
|
27
41
|
def self.query(address, **args) = default.query(address, **args)
|
|
28
42
|
|
|
43
|
+
# @param register [Bluebook::ProjectRegister, Bluebook::ProjectLoader] the FQN
|
|
44
|
+
# catalogue to resolve addresses against
|
|
29
45
|
def initialize(register)
|
|
30
46
|
@register = register
|
|
31
47
|
end
|
|
32
48
|
|
|
49
|
+
# Lists every routed FQN entry.
|
|
50
|
+
#
|
|
51
|
+
# @return [Array<Bluebook::ProjectRegister::Entry>] every routed FQN entry
|
|
33
52
|
def available = register.entries.values
|
|
34
53
|
|
|
54
|
+
# Installs Ruby namespace constants and shortcut methods for every
|
|
55
|
+
# current-version route.
|
|
56
|
+
#
|
|
57
|
+
# @return [NamespaceInstaller] the installer that performed the install
|
|
35
58
|
def install_namespace! = NamespaceInstaller.new(self).install!
|
|
36
59
|
|
|
60
|
+
# Resolves an address to its routed FQN entry.
|
|
61
|
+
#
|
|
62
|
+
# @param address [String] a fully-qualified command or query address, realm included
|
|
63
|
+
# @return [Bluebook::ProjectRegister::Entry] the routed entry
|
|
64
|
+
# @raise [UnknownAddress] if `address` has no realm, or names no known route
|
|
65
|
+
# @raise [Fqn::Invalid] if `address` is not a well-formed FQN
|
|
37
66
|
def resolve(address)
|
|
38
67
|
fqn = Fqn.parse(address)
|
|
39
68
|
raise UnknownAddress, "router addresses require a realm: #{address.inspect}" unless fqn.realm
|
|
@@ -43,13 +72,39 @@ module Hecks
|
|
|
43
72
|
raise UnknownAddress, "no Bluebook route for #{address.inspect}"
|
|
44
73
|
end
|
|
45
74
|
|
|
75
|
+
# Dispatches a command to its resolved aggregate.
|
|
76
|
+
#
|
|
77
|
+
# @param address [String] a fully-qualified command address, realm included
|
|
78
|
+
# @param args [Hash] command facts, plus the dispatcher's optional `:to`, `:with`,
|
|
79
|
+
# and `:saga_correlation` keys
|
|
80
|
+
# @return [Runtime::Dispatcher::Result] the dispatch result
|
|
81
|
+
# @raise [UnknownAddress] if `address` has no realm, or names no known route
|
|
82
|
+
# @raise [WrongVerbKind] if `address` names a query
|
|
83
|
+
# @raise [Runtime::UnknownVerb] if the resolved verb names something undeclared
|
|
84
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the domain refuses
|
|
85
|
+
# the call
|
|
86
|
+
# @raise [Runtime::StaleWrite] if concurrent writers beat this one through every retry
|
|
87
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
46
88
|
def dispatch(address, **args)
|
|
47
89
|
entry = resolve(address)
|
|
48
90
|
raise WrongVerbKind, "#{address.inspect} names a query; use #query" unless entry.command?
|
|
49
91
|
|
|
50
|
-
entry.dispatcher.
|
|
92
|
+
entry.dispatcher.dispatch_flat(local_verb(entry), args)
|
|
51
93
|
end
|
|
52
94
|
|
|
95
|
+
# Queries a resolved aggregate, entity, or read model.
|
|
96
|
+
#
|
|
97
|
+
# @param address [String] a fully-qualified query address, realm included
|
|
98
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
99
|
+
# @return [Array<Hash>] one row Hash per match; see `Runtime::Dispatcher#query`
|
|
100
|
+
# for the exact shape per address kind
|
|
101
|
+
# @raise [UnknownAddress] if `address` has no realm, or names no known route
|
|
102
|
+
# @raise [WrongVerbKind] if `address` names a command
|
|
103
|
+
# @raise [Runtime::UnknownVerb] if the resolved verb names something undeclared
|
|
104
|
+
# @raise [Runtime::NotFound] if a read model's root reference names no record
|
|
105
|
+
# @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
|
|
106
|
+
# @raise [KeyError] if a rooted read model is asked without its reference argument
|
|
107
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
53
108
|
def query(address, **args)
|
|
54
109
|
entry = resolve(address)
|
|
55
110
|
raise WrongVerbKind, "#{address.inspect} names a command; use #dispatch" unless entry.query?
|
|
@@ -1,40 +1,49 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Runtime
|
|
3
|
-
# A
|
|
4
|
-
# mechanism for every adapter that does
|
|
3
|
+
# **A process-wide, striped mutex registry** — the concurrency-control
|
|
4
|
+
# mechanism for every adapter that does not declare
|
|
5
5
|
# `:optimistic_concurrency` (Heki, Memory today; see
|
|
6
6
|
# `CommandInterpreter#call`/`EntityInterpreter#call`, which choose
|
|
7
7
|
# between this and Postgres's CAS+retry purely off
|
|
8
8
|
# `repository.capabilities`).
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# Why a lock suffices here and CAS is not needed: both adapters hold
|
|
11
11
|
# process-local data. `Adapters::Memory.tenant_capable?`'s own comment
|
|
12
12
|
# states the confirmed fact this relies on — two `Runtime.boot` calls
|
|
13
|
-
# get two entirely separate adapter instances; there is never a
|
|
14
|
-
#
|
|
15
|
-
# possibly other
|
|
13
|
+
# get two entirely separate adapter instances; there is never a second
|
|
14
|
+
# process writing the same Heki file or the same Memory Hash, only
|
|
15
|
+
# possibly other threads within this one process. A `Mutex` held for
|
|
16
16
|
# the full hydrate-through-save critical section closes the identical
|
|
17
17
|
# lost-update gap CAS closes for Postgres, with no schema, no version
|
|
18
18
|
# column, and no retry loop — the second thread simply doesn't start
|
|
19
19
|
# its own hydrate until the first thread's save has landed.
|
|
20
20
|
#
|
|
21
|
-
#
|
|
22
|
-
# id]`, so two dispatches against two
|
|
21
|
+
# **Striped, not one global lock**: keyed by `[domain, aggregate.hecks_name,
|
|
22
|
+
# id]`, so two dispatches against two different records never block
|
|
23
23
|
# each other. The registry Hash itself is guarded by its own top-level
|
|
24
24
|
# Mutex only for the moment a new per-key Mutex is created — two
|
|
25
|
-
# threads locking
|
|
25
|
+
# threads locking different keys for the first time never wait on one
|
|
26
26
|
# another beyond that brief creation window.
|
|
27
27
|
module AggregateLock
|
|
28
28
|
@registry_lock = Mutex.new
|
|
29
29
|
@locks = {}
|
|
30
30
|
|
|
31
31
|
class << self
|
|
32
|
+
# Returns the Mutex striped to one record, creating it on first use.
|
|
33
|
+
#
|
|
32
34
|
# `AggregateLock.for(domain, aggregate, id).synchronize { ... }`
|
|
33
35
|
# `id: nil` — identity could not be resolved yet (see
|
|
34
|
-
# `Identity.best_effort`) — locks by aggregate
|
|
36
|
+
# `Identity.best_effort`) — locks by aggregate type alone, coarser
|
|
35
37
|
# (every record of this aggregate serializes against every other)
|
|
36
|
-
# but still correct: it can only ever make dispatch
|
|
38
|
+
# but still correct: it can only ever make dispatch more
|
|
37
39
|
# conservative than a resolved id would.
|
|
40
|
+
#
|
|
41
|
+
# @param domain [String] the domain the aggregate belongs to
|
|
42
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate being dispatched
|
|
43
|
+
# @param id [String, nil] the resolved record id, or nil to stripe by aggregate type
|
|
44
|
+
# alone
|
|
45
|
+
# @return [Mutex] the mutex for this `[domain, aggregate, id]` key, held for the full
|
|
46
|
+
# hydrate-through-save critical section
|
|
38
47
|
def for(domain, aggregate, id = nil)
|
|
39
48
|
key = id.nil? ? [domain.to_s, aggregate.hecks_name] : [domain.to_s, aggregate.hecks_name, id.to_s]
|
|
40
49
|
@registry_lock.synchronize { @locks[key] ||= Mutex.new }
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Runtime
|
|
3
3
|
# A boot's own small set of phase-tagged, conditionally-present gates —
|
|
4
|
-
# ADR 0031. Registration is instance-scoped,
|
|
5
|
-
#
|
|
4
|
+
# ADR 0031. Registration is instance-scoped, one per `Loader.boot`
|
|
5
|
+
# call, never a module-level singleton: a process that boots more than
|
|
6
6
|
# one registry in its lifetime (every spec suite does) must never let
|
|
7
7
|
# one boot's capability profile leak into the next boot's gate list.
|
|
8
8
|
#
|
|
@@ -13,26 +13,44 @@ module Hecks
|
|
|
13
13
|
# order — today exactly one gate per phase, so ordering among
|
|
14
14
|
# same-phase gates has never been exercised.
|
|
15
15
|
#
|
|
16
|
-
# This is deliberately
|
|
16
|
+
# This is deliberately not the same registry `Hecks::Projector` uses
|
|
17
17
|
# (ADR 0027) — that one is a process-wide, static IR-in/artifact-out
|
|
18
18
|
# registry with no bindings and no live-state mutation; this one is
|
|
19
19
|
# per-boot and gates real I/O (a Postgres mint, a saga-store read).
|
|
20
20
|
# Sharing a primitive between them is deferred until a third consumer
|
|
21
21
|
# actually wants it (0031's own Rejected Alternatives).
|
|
22
22
|
class BootGates
|
|
23
|
+
# @return [void]
|
|
23
24
|
def initialize
|
|
24
25
|
@gates = Hash.new { |h, k| h[k] = [] }
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
# Adds a gate to `phase`, run in registration order alongside any other gate there.
|
|
29
|
+
#
|
|
30
|
+
# @param name [Symbol] the gate's name, checked by `registered?`
|
|
31
|
+
# @param gate [Proc, Method] the gate; called as `gate.call(registry, directory)`
|
|
32
|
+
# @param phase [Symbol] the phase this gate runs under, such as `:pre_verify` or
|
|
33
|
+
# `:post_verify`
|
|
34
|
+
# @return [Hecks::Runtime::BootGates] self, for chaining
|
|
27
35
|
def register(name, gate, phase:)
|
|
28
36
|
@gates[phase] << [name, gate]
|
|
29
37
|
self
|
|
30
38
|
end
|
|
31
39
|
|
|
40
|
+
# Reports whether a gate named `name` has been registered, under any phase.
|
|
41
|
+
#
|
|
42
|
+
# @param name [Symbol] the gate name to look for, across every phase
|
|
43
|
+
# @return [Boolean] true if a gate named `name` was registered under any phase
|
|
32
44
|
def registered?(name)
|
|
33
45
|
@gates.values.flatten(1).any? { |registered_name, _gate| registered_name == name }
|
|
34
46
|
end
|
|
35
47
|
|
|
48
|
+
# Runs every gate registered under `phase`, in registration order.
|
|
49
|
+
#
|
|
50
|
+
# @param phase [Symbol] the phase to run, such as `:pre_verify` or `:post_verify`
|
|
51
|
+
# @param registry [Runtime::Registry] the booted registry, passed to each gate
|
|
52
|
+
# @param directory [String] the boot directory, passed to each gate
|
|
53
|
+
# @return [void]
|
|
36
54
|
def run!(phase, registry, directory)
|
|
37
55
|
@gates[phase].each { |pair| pair.last.call(registry, directory) }
|
|
38
56
|
end
|