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
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
require_relative "errors"
|
|
2
|
+
require_relative "refusal_wording"
|
|
3
|
+
require_relative "routing"
|
|
4
|
+
require_relative "../rendering"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Runtime
|
|
8
|
+
# **One dispatch, as data** — the verb, the receiver it is addressed to
|
|
9
|
+
# (`target`, a `Routing::Envelope` or nil), and every fact the caller did
|
|
10
|
+
# or did not offer. Built once per dispatch by `Invocation.from_call`, the
|
|
11
|
+
# only place the runtime interprets the shape of a call (`to:` vs `with:`
|
|
12
|
+
# vs the flat facts hash `dispatch_flat` carries, a port operation's
|
|
13
|
+
# reference attribute
|
|
14
|
+
# lifted into `to:`). `Runtime::Routing.envelope`/`.payload` delegate here
|
|
15
|
+
# and no longer hold that logic themselves.
|
|
16
|
+
#
|
|
17
|
+
# `facts` maps a fact name to exactly one of:
|
|
18
|
+
#
|
|
19
|
+
# - `Invocation::Absent` — the key was never offered (every declared
|
|
20
|
+
# attribute the caller left out appears so)
|
|
21
|
+
# - `Invocation::Null` — the key was offered with an explicit nil
|
|
22
|
+
# - `Invocation::Present` — the key was offered with a value
|
|
23
|
+
#
|
|
24
|
+
# Keys are kept exactly as offered: `with:` keys are symbolized (as they
|
|
25
|
+
# always were), a flat facts hash keeps whatever key the wire used. Undeclared keys a caller
|
|
26
|
+
# offered are kept too — refusing them is
|
|
27
|
+
# still `refuse_unknown_arguments`' job, a dispatch step, not this one.
|
|
28
|
+
#
|
|
29
|
+
# I1 changes no behavior: every interpreter still reads `ctx.args`,
|
|
30
|
+
# which is `#to_args` — the same Hash `Routing.payload` returns.
|
|
31
|
+
# Roadmap I2 moves the Ruby `decode_arguments` step onto `facts` itself.
|
|
32
|
+
Invocation = Data.define(:verb, :target, :facts)
|
|
33
|
+
|
|
34
|
+
# The fact markers, the fact readers, and `from_call` — see above.
|
|
35
|
+
class Invocation
|
|
36
|
+
# A fact the caller offered with a real (non-nil) value.
|
|
37
|
+
Present = Data.define(:value) do
|
|
38
|
+
def inspect = "#<Invocation::Present #{value.inspect}>"
|
|
39
|
+
alias_method :to_s, :inspect
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The class of the two frozen marker singletons below — never
|
|
43
|
+
# instantiated anywhere else.
|
|
44
|
+
class Marker
|
|
45
|
+
# @param name [String] the marker's own label, used by #inspect/#to_s
|
|
46
|
+
def initialize(name)
|
|
47
|
+
@name = name
|
|
48
|
+
freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def inspect = "Invocation::#{@name}"
|
|
52
|
+
alias to_s inspect
|
|
53
|
+
end
|
|
54
|
+
private_constant :Marker
|
|
55
|
+
|
|
56
|
+
Absent = Marker.new("Absent")
|
|
57
|
+
Null = Marker.new("Null")
|
|
58
|
+
|
|
59
|
+
# @param verb [String] the fully qualified verb this invocation dispatches
|
|
60
|
+
# @param target [Routing::Envelope, nil] the resolved receiver, or nil when
|
|
61
|
+
# the facts carry the identity themselves
|
|
62
|
+
# @param facts [Hash{String, Symbol => Object}] every candidate fact, keyed by
|
|
63
|
+
# offered (or declared) name; frozen, deep-duped, before being stored
|
|
64
|
+
def initialize(verb:, target:, facts:)
|
|
65
|
+
super(verb: verb, target: target, facts: facts.dup.freeze)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Reads the fact recorded under `name`.
|
|
69
|
+
#
|
|
70
|
+
# The fact recorded under `name` — `Absent` for a name never offered
|
|
71
|
+
# and never declared either.
|
|
72
|
+
#
|
|
73
|
+
# @param name [String, Symbol] the fact name to look up
|
|
74
|
+
# @return [Invocation::Present, Invocation::Null, Invocation::Absent] the
|
|
75
|
+
# recorded marker for `name`
|
|
76
|
+
def fact(name) = facts.fetch(name, Absent)
|
|
77
|
+
|
|
78
|
+
# Reports whether `name` was offered with a real, non-nil value.
|
|
79
|
+
#
|
|
80
|
+
# @param name [String, Symbol] the fact name to check
|
|
81
|
+
# @return [Boolean] true if `name` was offered with a non-nil value
|
|
82
|
+
def present?(name) = fact(name).is_a?(Present)
|
|
83
|
+
|
|
84
|
+
# Reports whether `name` was offered with an explicit nil.
|
|
85
|
+
#
|
|
86
|
+
# @param name [String, Symbol] the fact name to check
|
|
87
|
+
# @return [Boolean] true if `name` was offered with an explicit nil
|
|
88
|
+
def null?(name) = fact(name).equal?(Null)
|
|
89
|
+
|
|
90
|
+
# Reports whether `name` was never offered.
|
|
91
|
+
#
|
|
92
|
+
# @param name [String, Symbol] the fact name to check
|
|
93
|
+
# @return [Boolean] true if `name` was never offered (and, if declared, left out)
|
|
94
|
+
def absent?(name) = fact(name).equal?(Absent)
|
|
95
|
+
|
|
96
|
+
# A Present fact's value; nil for an explicit Null. Raises KeyError for
|
|
97
|
+
# an Absent fact — "never offered" has no value, and answering nil
|
|
98
|
+
# would re-conflate it with an explicit null, the very ambiguity this
|
|
99
|
+
# type exists to remove. Ask `absent?`/`present?` first.
|
|
100
|
+
#
|
|
101
|
+
# @param name [String, Symbol] the fact name to read
|
|
102
|
+
# @return [Object, nil] the offered value; nil for an explicit Null fact
|
|
103
|
+
# @raise [KeyError] if `name` names an Absent fact (never offered)
|
|
104
|
+
def value(name)
|
|
105
|
+
case (found = fact(name))
|
|
106
|
+
when Present then found.value
|
|
107
|
+
when Null then nil
|
|
108
|
+
else raise KeyError, "#{verb} was not given #{name.inspect}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The flat args hash, byte for byte what `Routing.payload` returned
|
|
113
|
+
# before this type existed: offered keys in offered order, Absent keys
|
|
114
|
+
# omitted, Null keys mapped to nil. A fresh Hash every call.
|
|
115
|
+
#
|
|
116
|
+
# @return [Hash{String, Symbol => Object}] the offered facts by name; a fact
|
|
117
|
+
# offered as nil is kept as nil
|
|
118
|
+
def to_args
|
|
119
|
+
facts.each_with_object({}) do |(name, found), args|
|
|
120
|
+
next if found.equal?(Absent)
|
|
121
|
+
|
|
122
|
+
args[name] = found.equal?(Null) ? nil : found.value
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
class << self
|
|
127
|
+
# The one reading of a call's shape. `receiver:` picks which of the
|
|
128
|
+
# three dispatch shapes this is, because each has always checked its
|
|
129
|
+
# parts in its own order and a malformed call's refusal depends on
|
|
130
|
+
# that order:
|
|
131
|
+
#
|
|
132
|
+
# :aggregate — facts first (`with:` checks), then `to:`
|
|
133
|
+
# :entity — `to:` first, then the block (which resolves the
|
|
134
|
+
# entity chain, UnknownVerb), then facts
|
|
135
|
+
# :port — a reference attribute / identity field lifted into
|
|
136
|
+
# `to:`, then `to:` (which a port operation requires),
|
|
137
|
+
# then facts
|
|
138
|
+
#
|
|
139
|
+
# The block answers the declaring command or port operation (anything
|
|
140
|
+
# with `hecks_name` and `attributes`); it is called exactly once, at
|
|
141
|
+
# the point in that order where the caller always resolved it.
|
|
142
|
+
# `aggregate:` is the owning aggregate construct, read for `:port`
|
|
143
|
+
# only.
|
|
144
|
+
#
|
|
145
|
+
# @param verb [String] the fully qualified verb being dispatched
|
|
146
|
+
# @param to [String, Hash, nil] a bare aggregate identity, or a Hash with
|
|
147
|
+
# `aggregate:` and `entity:`/`entities:`; nil for no explicit receiver
|
|
148
|
+
# @param with [Hash, nil] the command's facts, keyed by attribute name; nil
|
|
149
|
+
# when the caller offers `flat` instead
|
|
150
|
+
# @param flat [Hash] the command's facts as a flat args hash, used when `with`
|
|
151
|
+
# is nil; for `:port`, may also carry the receiver identity under a
|
|
152
|
+
# reference attribute's own name
|
|
153
|
+
# @param receiver [Symbol] which of the three dispatch shapes this is:
|
|
154
|
+
# `:aggregate`, `:entity` or `:port`
|
|
155
|
+
# @param entity_depth [Integer] the number of entity-hop identities `to:`
|
|
156
|
+
# must carry, for `:entity` only
|
|
157
|
+
# @param aggregate [Bluebook::Aggregate, nil] the owning aggregate construct,
|
|
158
|
+
# read for `:port` only
|
|
159
|
+
# @yieldreturn [Class] the declaring command or port-operation class (anything
|
|
160
|
+
# with `hecks_name` and `attributes`)
|
|
161
|
+
# @return [Runtime::Invocation] the built invocation
|
|
162
|
+
# @raise [Runtime::TypeMismatch] if `to:` is malformed, its entity count is
|
|
163
|
+
# wrong, or (`:port` only) the operation resolves no receiving aggregate
|
|
164
|
+
# @raise [Runtime::UnknownArgument] if `with:` names an attribute the command
|
|
165
|
+
# does not declare
|
|
166
|
+
# @raise [Runtime::AbsentArgument] if `with:` omits a required attribute
|
|
167
|
+
# @raise [ArgumentError] if `receiver` is none of `:aggregate`, `:entity` or `:port`
|
|
168
|
+
def from_call(verb, to:, with:, flat:, receiver: :aggregate, entity_depth: 0, aggregate: nil, &declaring)
|
|
169
|
+
case receiver
|
|
170
|
+
when :aggregate
|
|
171
|
+
command = declaring.call
|
|
172
|
+
facts = facts_for(command, with: with, flat: flat)
|
|
173
|
+
new(verb: verb, target: route(to), facts: facts)
|
|
174
|
+
when :entity
|
|
175
|
+
target = route(to, entity_depth: entity_depth)
|
|
176
|
+
command = declaring.call
|
|
177
|
+
new(verb: verb, target: target, facts: facts_for(command, with: with, flat: flat))
|
|
178
|
+
when :port
|
|
179
|
+
port_call(verb, aggregate, declaring.call, to: to, with: with, flat: flat)
|
|
180
|
+
else
|
|
181
|
+
raise ArgumentError, "unknown receiver #{receiver.inspect}"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# `to:` as a `Routing::Envelope`, or nil when no `to:` was given.
|
|
186
|
+
#
|
|
187
|
+
# @param to [String, Hash, nil] a bare aggregate identity, or a Hash with
|
|
188
|
+
# `aggregate:` and `entity:`/`entities:`; nil for no receiver
|
|
189
|
+
# @param entity_depth [Integer] the number of entity-hop identities `to`
|
|
190
|
+
# must carry
|
|
191
|
+
# @return [Routing::Envelope, nil] the resolved envelope, or nil when `to`
|
|
192
|
+
# is nil
|
|
193
|
+
# @raise [Runtime::TypeMismatch] if `to` is malformed, or its entity count
|
|
194
|
+
# does not match `entity_depth`
|
|
195
|
+
def route(to, entity_depth: 0)
|
|
196
|
+
return nil if to.nil?
|
|
197
|
+
|
|
198
|
+
aggregate, entities = to.is_a?(Hash) ? envelope_hash(to) : scalar_envelope(to)
|
|
199
|
+
|
|
200
|
+
raise TypeMismatch, "to: must name the receiving aggregate identity" if aggregate.nil? || aggregate.to_s.empty?
|
|
201
|
+
if entities.size != entity_depth
|
|
202
|
+
raise TypeMismatch,
|
|
203
|
+
"to: for an entity command needs #{entity_depth} entity " \
|
|
204
|
+
"#{entity_depth == 1 ? 'identity' : 'identities'} after the aggregate — got #{entities.size}"
|
|
205
|
+
end
|
|
206
|
+
raise TypeMismatch, "to: contains a blank entity identity" if entities.any? do |identity|
|
|
207
|
+
identity.nil? || identity.to_s.empty?
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
Routing::Envelope.new(aggregate: aggregate, entities: entities)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# The offered facts for `declaring`, as Absent/Null/Present — offered
|
|
214
|
+
# keys first, in offered order, then every declared attribute that was
|
|
215
|
+
# not offered, as Absent.
|
|
216
|
+
#
|
|
217
|
+
# @param declaring [Class] the command or port-operation class (anything
|
|
218
|
+
# with `hecks_name` and `attributes`) whose declared attributes are read
|
|
219
|
+
# @param with [Hash, nil] the command's facts, keyed by attribute name; nil
|
|
220
|
+
# when the caller offers `flat` instead
|
|
221
|
+
# @param flat [Hash] the command's facts as a flat args hash, used when `with`
|
|
222
|
+
# is nil
|
|
223
|
+
# @return [Hash{String, Symbol => Invocation::Absent, Invocation::Null,
|
|
224
|
+
# Invocation::Present}] each candidate fact keyed by its offered (or
|
|
225
|
+
# declared) name
|
|
226
|
+
# @raise [Runtime::TypeMismatch] if both `with` and a non-empty `flat` are
|
|
227
|
+
# given, or if `with` is not a Hash
|
|
228
|
+
# @raise [Runtime::UnknownArgument] if `with:` names an attribute `declaring`
|
|
229
|
+
# does not declare
|
|
230
|
+
# @raise [Runtime::AbsentArgument] if `with:` omits a required attribute
|
|
231
|
+
def facts_for(declaring, with:, flat:)
|
|
232
|
+
offered = offered_facts(declaring, with: with, flat: flat)
|
|
233
|
+
facts = offered.each_with_object({}) do |(name, value), found|
|
|
234
|
+
found[name] = value.nil? ? Null : Present.new(value: value)
|
|
235
|
+
end
|
|
236
|
+
declaring.attributes.each do |attribute|
|
|
237
|
+
name = attribute.name.to_sym
|
|
238
|
+
facts[name] = Absent unless facts.key?(name)
|
|
239
|
+
end
|
|
240
|
+
facts
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
private
|
|
244
|
+
|
|
245
|
+
# **The port operation shape** — moved from `Dispatcher#port_invocation`.
|
|
246
|
+
#
|
|
247
|
+
# A Reference-typed attribute naming the owning aggregate is routing,
|
|
248
|
+
# not a fact: lifted out of the flat facts into `to:` when no `to:`
|
|
249
|
+
# was given. A `to:`-declared operation carries no Reference
|
|
250
|
+
# attribute at all (PortOperationBuilder#initialize's own comment),
|
|
251
|
+
# so its receiver is read — not removed — from the plain attribute
|
|
252
|
+
# named for the owner's first `identified_by` field: that attribute
|
|
253
|
+
# is still a declared external fact `refuse_absent_arguments`
|
|
254
|
+
# expects to find (Rust's own comment: "declare only external facts
|
|
255
|
+
# with attribute"; a real AbsentArgument confirmed this before `[]`
|
|
256
|
+
# replaced `delete`). Composite identity is not attempted — `.first`
|
|
257
|
+
# only, no domain in the corpus needs more for a port operation.
|
|
258
|
+
def port_call(verb, aggregate, operation, to:, with:, flat:)
|
|
259
|
+
flat = flat.dup
|
|
260
|
+
identity = operation.identity_attribute(aggregate.hecks_name)
|
|
261
|
+
if to.nil? && identity && flat.key?(identity.name)
|
|
262
|
+
to = flat.delete(identity.name)
|
|
263
|
+
elsif to.nil? && operation.to == aggregate.hecks_name
|
|
264
|
+
identity_name = Array(aggregate.identified_by).first
|
|
265
|
+
to = flat[identity_name] if identity_name && flat.key?(identity_name)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
target = route(to)
|
|
269
|
+
raise TypeMismatch, "#{operation.hecks_name} requires its receiving aggregate in to:" unless target
|
|
270
|
+
|
|
271
|
+
new(verb: verb, target: target, facts: facts_for(operation, with: with, flat: flat))
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# BUG#7 — a non-Hash `to:` must be a String, matching Rust's
|
|
275
|
+
# `RoutingEnvelope::from_json` (kernel/routing.rs), which refuses
|
|
276
|
+
# anything neither a JSON string nor object before the domain payload
|
|
277
|
+
# is examined. Surfaced on `examples/roster`'s `Mark`, whose own
|
|
278
|
+
# attribute is literally named `to`: a flat-facts dispatch steals
|
|
279
|
+
# that key into this parameter, and without this check an out-of-range
|
|
280
|
+
# Integer would be accepted as the aggregate identity, leaving Mark's
|
|
281
|
+
# `to` fact absent (AbsentArgument in Ruby, TypeMismatch in Rust). See
|
|
282
|
+
# spec/runtime/routing_envelope_shape_spec.rb.
|
|
283
|
+
def scalar_envelope(to)
|
|
284
|
+
return [to, []] if to.is_a?(String)
|
|
285
|
+
|
|
286
|
+
raise TypeMismatch, "to: must be a string aggregate identity or an entity route, got #{Rendering.describe(to)}"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def envelope_hash(to)
|
|
290
|
+
hash = to.transform_keys(&:to_sym)
|
|
291
|
+
unknown = hash.keys - %i[aggregate entity entities]
|
|
292
|
+
raise TypeMismatch, "to: does not recognize #{unknown.sort.join(', ')}" unless unknown.empty?
|
|
293
|
+
|
|
294
|
+
[hash[:aggregate], entity_identities(hash)]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# BUG#18 — an entity route naming no entity (`entities: []`, or
|
|
298
|
+
# neither key) refuses here, unconditionally, before entity_depth is
|
|
299
|
+
# consulted: for an aggregate-level command (depth 0), without this,
|
|
300
|
+
# `[].size == 0` would satisfy the depth check and let the degenerate
|
|
301
|
+
# Hash reach the command's own validation. Rust's `RoutingEnvelope::
|
|
302
|
+
# from_json` always refused it at this point. A bare aggregate identity
|
|
303
|
+
# String remains the ordinary aggregate-command shape.
|
|
304
|
+
def entity_identities(hash)
|
|
305
|
+
raise TypeMismatch, "to: takes entity: or entities:, not both" if hash.key?(:entities) && hash.key?(:entity)
|
|
306
|
+
|
|
307
|
+
identities = hash.key?(:entities) ? Array(hash[:entities]) : Array(hash[:entity])
|
|
308
|
+
raise TypeMismatch, "to: entity route requires at least one entity identity" if identities.empty?
|
|
309
|
+
|
|
310
|
+
identities
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# `with:` is deliberately strict: a caller choosing the explicit
|
|
314
|
+
# envelope cannot smuggle receiver identity back into the payload,
|
|
315
|
+
# and may not mix it with a flat facts hash. Without `with:`
|
|
316
|
+
# (nil or false) the flat facts are the facts, unread —
|
|
317
|
+
# whether `to:` was given never enters this decision (BUG#17).
|
|
318
|
+
def offered_facts(declaring, with:, flat:)
|
|
319
|
+
if with && !flat.empty?
|
|
320
|
+
raise TypeMismatch,
|
|
321
|
+
"dispatch takes command facts in with:, not both with: and a flat facts hash"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
return flat unless with
|
|
325
|
+
raise TypeMismatch, "with: must be a hash of command facts" unless with.is_a?(Hash)
|
|
326
|
+
|
|
327
|
+
offered = with.transform_keys(&:to_sym)
|
|
328
|
+
declared = declaring.attributes.map { |attribute| attribute.name.to_sym }
|
|
329
|
+
refuse_unknown_facts!(declaring, offered, declared)
|
|
330
|
+
refuse_absent_facts!(declaring, offered, declared)
|
|
331
|
+
offered
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Unknown before absent — a `with:` that is both still refuses
|
|
335
|
+
# UnknownArgument first.
|
|
336
|
+
def refuse_unknown_facts!(declaring, offered, declared)
|
|
337
|
+
unknown = (offered.keys - declared).sort
|
|
338
|
+
return if unknown.empty?
|
|
339
|
+
|
|
340
|
+
raise UnknownArgument,
|
|
341
|
+
RefusalWording.render_site("UnknownArgument", "unknown_args",
|
|
342
|
+
command: declaring.hecks_name, unknown: unknown,
|
|
343
|
+
declared: declared)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def refuse_absent_facts!(declaring, offered, declared)
|
|
347
|
+
absent = declaring.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym } - offered.keys
|
|
348
|
+
return if absent.empty?
|
|
349
|
+
|
|
350
|
+
raise AbsentArgument,
|
|
351
|
+
RefusalWording.render_site("AbsentArgument", "absent_args",
|
|
352
|
+
command: declaring.hecks_name, absent: absent,
|
|
353
|
+
declared: declared)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
data/lib/hecks/runtime/loader.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Hecks
|
|
|
19
19
|
# `Widget::Item.Add(...)` sugar. A caller that only ever dispatches
|
|
20
20
|
# by FQN string (`SmokeTest`, the one caller so far) can pass
|
|
21
21
|
# `false` to skip it: `Facade::Surface.install` puts a bare global
|
|
22
|
-
# Ruby constant on `Object` per domain
|
|
22
|
+
# Ruby constant on `Object` per domain and per aggregate name, with
|
|
23
23
|
# no scoping and no cleanup hook, so a tool booting arbitrary
|
|
24
24
|
# throwaway domains under generic names ("Widget", "Item", "Tag")
|
|
25
25
|
# would otherwise leak those names into the rest of the process —
|
|
@@ -37,6 +37,20 @@ module Hecks
|
|
|
37
37
|
# its own env var name and passes the resulting string straight
|
|
38
38
|
# through, e.g. `Hecks.boot(path, environment:
|
|
39
39
|
# ENV.fetch("MYAPP_ENV", "development"))`.
|
|
40
|
+
#
|
|
41
|
+
# @param path [String] a domain directory to boot, resolved through
|
|
42
|
+
# `Ports::Loading#bluebook_directory`
|
|
43
|
+
# @param shared [String, nil] an explicit shared ports/adapters root override; nil
|
|
44
|
+
# resolves it by walking up from the domain directory
|
|
45
|
+
# @param install_facade [Boolean] whether to install the `Widget::Item.Add(...)`
|
|
46
|
+
# facade sugar
|
|
47
|
+
# @param environment [String, nil] the environment overlay to load after the
|
|
48
|
+
# domain's own files (e.g. `"production"`); nil loads none
|
|
49
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted dispatcher
|
|
50
|
+
# @raise [Errno::ENOENT] if `path` names neither a domain directory nor one holding
|
|
51
|
+
# a `bluebook/` subdirectory
|
|
52
|
+
# @raise [Runtime::WiringError] if a boot gate finds a wiring problem (an undeclared
|
|
53
|
+
# bind, a compute/rekey rule with no persistence plugin loaded, …)
|
|
40
54
|
def self.boot(path, shared: nil, install_facade: true, environment: nil)
|
|
41
55
|
loading = Ports::Loading.bootstrap
|
|
42
56
|
directory = loading.bluebook_directory(path)
|
|
@@ -52,10 +66,11 @@ module Hecks
|
|
|
52
66
|
run_boot_gates!(registry, directory)
|
|
53
67
|
dispatcher = dispatcher_for(registry)
|
|
54
68
|
redrive_outbox!(dispatcher)
|
|
69
|
+
seed_privacy_markings!(dispatcher, registry)
|
|
55
70
|
install_facade ? bind_runtime(dispatcher) : dispatcher
|
|
56
71
|
end
|
|
57
72
|
|
|
58
|
-
#
|
|
73
|
+
# The outbox's boot-time reconciliation — after the dispatcher
|
|
59
74
|
# exists (a row's consumer runs through its interpreters, so this
|
|
60
75
|
# cannot be a plain registry gate the way saga rehydration is) and
|
|
61
76
|
# after saga rehydration (a redriven row may advance a saga, which
|
|
@@ -63,13 +78,44 @@ module Hecks
|
|
|
63
78
|
# provably never started; `claimed` rows are surfaced, never
|
|
64
79
|
# auto-redriven — see `Runtime::Outbox`. A remote dispatcher has
|
|
65
80
|
# no local stores to scan.
|
|
81
|
+
#
|
|
82
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the
|
|
83
|
+
# just-booted dispatcher; a no-op unless it exposes an `outbox`
|
|
84
|
+
# @return [void]
|
|
66
85
|
def self.redrive_outbox!(dispatcher)
|
|
67
86
|
return unless dispatcher.respond_to?(:outbox)
|
|
68
87
|
|
|
69
88
|
dispatcher.outbox.redrive!
|
|
70
89
|
end
|
|
71
90
|
|
|
72
|
-
#
|
|
91
|
+
# Turns every `.hecksagon`-declared `AggregateDoor#mark_sensitive` fact into a real
|
|
92
|
+
# `Privacy::Marking.Mark` — after the dispatcher exists (dispatch needs a bound
|
|
93
|
+
# repository), idempotent across reboots (a marking already present is never
|
|
94
|
+
# re-dispatched, the same "provably never started" restraint `redrive_outbox!`
|
|
95
|
+
# holds itself to above). A no-op when nothing declared one, or when the domain
|
|
96
|
+
# never attached Privacy at all.
|
|
97
|
+
#
|
|
98
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the just-booted
|
|
99
|
+
# dispatcher `Privacy::Marking.Mark` dispatches through
|
|
100
|
+
# @param registry [Runtime::Registry] the just-booted registry `pending_privacy_markings`
|
|
101
|
+
# was recorded on
|
|
102
|
+
# @return [void]
|
|
103
|
+
def self.seed_privacy_markings!(dispatcher, registry)
|
|
104
|
+
return if registry.pending_privacy_markings.empty?
|
|
105
|
+
return unless registry.bluebook("Privacy")
|
|
106
|
+
|
|
107
|
+
already_marked_by_domain = registry.pending_privacy_markings.map { |marking| marking[:domain] }.uniq.to_h do |domain|
|
|
108
|
+
[domain, dispatcher.query("Privacy::Marking.ForDomain", domain: domain).map { |row| row[:attribute_path][:value] }]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
registry.pending_privacy_markings.each do |marking|
|
|
112
|
+
next if already_marked_by_domain[marking[:domain]].include?(marking[:attribute_path])
|
|
113
|
+
|
|
114
|
+
dispatcher.dispatch_flat("Privacy::Marking.Mark", marking)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The explicit-file form — `paths` names the exact bluebook/hecksagon/
|
|
73
119
|
# world files to boot, in place, wherever they actually live. `boot`
|
|
74
120
|
# above only ever takes a directory and globs it; that is the right
|
|
75
121
|
# shape for a real deployment (`examples/banking`, a domain someone
|
|
@@ -79,18 +125,30 @@ module Hecks
|
|
|
79
125
|
# (`Hecks::Behaviors`), but this carries no behaviors-specific
|
|
80
126
|
# logic and is not gated behind requiring that module.
|
|
81
127
|
#
|
|
82
|
-
#
|
|
128
|
+
# No copying, no temp directory. A prior port of this same idea
|
|
83
129
|
# (vendored into a downstream consumer, read before writing this)
|
|
84
130
|
# scoped a per-test boot by copying files into `Dir.mktmpdir` — which
|
|
85
131
|
# destroys real relative paths, and worse, makes a `persisted_by`
|
|
86
|
-
# path resolve against the
|
|
132
|
+
# path resolve against the temp copy's root instead of the project's
|
|
87
133
|
# own (confirmed there: a file adapter kept reading and writing the
|
|
88
134
|
# same deterministic tmp copy across an entire session, because
|
|
89
135
|
# `Hecks.boot`'s own `root` is always `File.dirname` of whatever
|
|
90
136
|
# directory it was handed). `directory` here is `File.dirname` of the
|
|
91
|
-
#
|
|
137
|
+
# first real path in `paths` — genuinely on disk, not a copy — so
|
|
92
138
|
# every downstream path (`EraCheck`, `persisted_by`, `shared_root`)
|
|
93
139
|
# resolves exactly as an ordinary directory boot's would.
|
|
140
|
+
#
|
|
141
|
+
# @param paths [Array<String>, String] the exact bluebook/hecksagon/world file paths
|
|
142
|
+
# to boot, in the order they should load
|
|
143
|
+
# @param shared [String, nil] an explicit shared ports/adapters root override; nil
|
|
144
|
+
# resolves it by walking up from `File.dirname` of the first path
|
|
145
|
+
# @param install_facade [Boolean] whether to install the `Widget::Item.Add(...)`
|
|
146
|
+
# facade sugar
|
|
147
|
+
# @param environment [String, nil] the environment overlay to load after the
|
|
148
|
+
# named files (e.g. `"production"`); nil loads none
|
|
149
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted dispatcher
|
|
150
|
+
# @raise [Runtime::WiringError] if a boot gate finds a wiring problem (an undeclared
|
|
151
|
+
# bind, a compute/rekey rule with no persistence plugin loaded, …)
|
|
94
152
|
def self.boot_files(paths, shared: nil, install_facade: true, environment: nil)
|
|
95
153
|
loading = Ports::Loading.bootstrap
|
|
96
154
|
files = Array(paths).map { |path| File.expand_path(path) }
|
|
@@ -109,21 +167,30 @@ module Hecks
|
|
|
109
167
|
install_facade ? bind_runtime(dispatcher) : dispatcher
|
|
110
168
|
end
|
|
111
169
|
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
170
|
+
# Runs every registered boot gate against `registry`, in order.
|
|
171
|
+
#
|
|
172
|
+
# ADR 0031 — a per-boot `BootGates` instance holds exactly the gates this
|
|
173
|
+
# registry's own bound adapters have a capability for, rather than two
|
|
174
|
+
# hardcoded, unconditional calls. Ordering is
|
|
115
175
|
# preserved: era-checking (when a persistence plugin contributes one)
|
|
116
176
|
# still runs before `verify!`, saga rehydration still runs after
|
|
117
177
|
# (conservative — see `SagaPersistence#rehydrate_sagas!`'s own
|
|
118
178
|
# comment).
|
|
119
179
|
#
|
|
120
180
|
# ADR 0033 — this loader no longer names `EraCheck`, or any other
|
|
121
|
-
# era-specific class, at all. Every
|
|
181
|
+
# era-specific class, at all. Every loaded persistence plugin
|
|
122
182
|
# (`Ports::Persistence.each_plugin` — nothing here if nothing was
|
|
123
183
|
# ever `require`d) is asked to contribute its own `:pre_verify`/
|
|
124
184
|
# `:post_verify` gates generically; `:saga_rehydration` is the one
|
|
125
185
|
# gate core still registers directly, because ADR 0031 already
|
|
126
186
|
# proved it's not era-specific.
|
|
187
|
+
#
|
|
188
|
+
# @param registry [Runtime::Registry] the registry mid-boot, with its bluebooks,
|
|
189
|
+
# hecksagons, ports, adapters and worlds already loaded
|
|
190
|
+
# @param directory [String] the domain directory the gates report against in a refusal
|
|
191
|
+
# @return [Runtime::BootGates] the gates instance built and run for this boot
|
|
192
|
+
# @raise [Runtime::WiringError] if `registry.verify!` or any contributed gate finds
|
|
193
|
+
# a wiring problem
|
|
127
194
|
def self.run_boot_gates!(registry, directory)
|
|
128
195
|
gates = BootGates.new
|
|
129
196
|
Ports::Persistence.each_plugin { |plugin| plugin.contribute_boot_gates(registry, gates) }
|
|
@@ -144,11 +211,17 @@ module Hecks
|
|
|
144
211
|
# `TranslationCompute`/`TranslationRekey` data (`bluebook/
|
|
145
212
|
# translation.rb`, core, no era-specific class involved), so this
|
|
146
213
|
# needs nothing plugin-specific to ask "does anything declare a
|
|
147
|
-
# compute/rekey rule at all." A
|
|
214
|
+
# compute/rekey rule at all." A loaded persistence plugin (e.g. the
|
|
148
215
|
# era plugin's own `:era_compute_rules` gate, registered above) runs
|
|
149
216
|
# the real, adapter-aware version of this check and refuses by name
|
|
150
217
|
# ("...is bound to Memory") long before this ever would; this only
|
|
151
218
|
# fires when nothing did, because nothing was loaded to.
|
|
219
|
+
#
|
|
220
|
+
# @param registry [Runtime::Registry] the registry mid-boot, whose declared
|
|
221
|
+
# `translations` are checked
|
|
222
|
+
# @return [void]
|
|
223
|
+
# @raise [Runtime::WiringError] if any translation declares a `computes`/`rekeys`
|
|
224
|
+
# rule while no persistence plugin is loaded to interpret it
|
|
152
225
|
def self.check_compute_rules_backstop!(registry)
|
|
153
226
|
return if Ports::Persistence.plugins_loaded?
|
|
154
227
|
|
|
@@ -165,7 +238,7 @@ module Hecks
|
|
|
165
238
|
end
|
|
166
239
|
|
|
167
240
|
# `RemoteDispatcher` for a domain routed through Lambda,
|
|
168
|
-
# `Dispatcher` otherwise — the
|
|
241
|
+
# `Dispatcher` otherwise — the one place this decision gets
|
|
169
242
|
# made, so everything built on top (`Handle`, `AggregateDoor`,
|
|
170
243
|
# `Facade::Surface`) never has to know which class it's holding.
|
|
171
244
|
# `registry.bluebooks.keys.first` is the just-booted domain's own
|
|
@@ -173,7 +246,7 @@ module Hecks
|
|
|
173
246
|
# any `uses_framework` chapter, `bin/project_rust`'s own header
|
|
174
247
|
# draws the identical distinction), not a directory basename.
|
|
175
248
|
#
|
|
176
|
-
# `dispatched_by("Lambda")` is its
|
|
249
|
+
# `dispatched_by("Lambda")` is its own, explicit verb — not
|
|
177
250
|
# inferred from `deployed_to("AwsLambda")`'s mere presence. Both
|
|
178
251
|
# Banking's and Embryonaut's `.world` files already declare
|
|
179
252
|
# `deployed_to("AwsLambda")` (it only means "a deploy target
|
|
@@ -183,19 +256,32 @@ module Hecks
|
|
|
183
256
|
# this landed. A domain opts in explicitly, the same way
|
|
184
257
|
# `persisted_by("PostgresEra")` is never inferred from anything
|
|
185
258
|
# else either.
|
|
259
|
+
#
|
|
260
|
+
# @param registry [Runtime::Registry] the just-booted registry, whose just-booted
|
|
261
|
+
# domain's `.world` is checked for `dispatched_by("Lambda")`
|
|
262
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] a `RemoteDispatcher`
|
|
263
|
+
# bound to `registry` if the domain declares `dispatched_by("Lambda")`, a plain
|
|
264
|
+
# `Dispatcher` otherwise
|
|
186
265
|
def self.dispatcher_for(registry)
|
|
187
266
|
domain = registry.bluebooks.keys.first
|
|
188
267
|
settings = registry.world(domain)&.for_verb("dispatched_by") || {}
|
|
189
268
|
return Dispatcher.new(registry) unless settings[:adapter] == "Lambda"
|
|
190
269
|
|
|
191
|
-
RemoteDispatcher.new(registry, region: settings.fetch(:region, "us-east-1"))
|
|
270
|
+
RemoteDispatcher.new(registry, region: settings.fetch(:region, "us-east-1"), function: settings[:function])
|
|
192
271
|
end
|
|
193
272
|
|
|
194
|
-
#
|
|
195
|
-
# dispatcher
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
# binding lives in the surface a boot
|
|
273
|
+
# Installs the facade sugar (`Widget::Item.Add(...)`), closed over
|
|
274
|
+
# `dispatcher`, and hands `dispatcher` back.
|
|
275
|
+
#
|
|
276
|
+
# The door is installed here, not stamped: the facade's modules close
|
|
277
|
+
# over this dispatcher, so the binding lives in the surface a boot
|
|
278
|
+
# installs rather than in a class-level global (`ruby_class.runtime =`
|
|
279
|
+
# on every aggregate's class) that would make two boots in one process
|
|
280
|
+
# share one name.
|
|
281
|
+
#
|
|
282
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the
|
|
283
|
+
# just-booted dispatcher the facade should call into
|
|
284
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] `dispatcher`, unchanged
|
|
199
285
|
def self.bind_runtime(dispatcher)
|
|
200
286
|
Facade::Surface.install(dispatcher)
|
|
201
287
|
dispatcher
|