hecks 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +427 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +179 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +145 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +438 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +306 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +54 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +125 -0
- data/lib/hecks/behaviors/expectations.rb +340 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
- data/lib/hecks/bluebook/assembly/marks.rb +228 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +91 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +78 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
- data/lib/hecks/bluebook/expression/projection.json +218 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +781 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +542 -0
- data/lib/hecks/bluebook/model_check.rb +445 -0
- data/lib/hecks/bluebook/pattern_subset.rb +184 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +128 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +66 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +410 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +119 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +166 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +341 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +177 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +84 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +161 -0
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +119 -0
- data/lib/hecks/forms/record_table.rb +68 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
- data/lib/hecks/fuzzing/properties.rb +1192 -0
- data/lib/hecks/fuzzing/replay.rb +668 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
- data/lib/hecks/fuzzing/value_generator.rb +200 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +324 -0
- data/lib/hecks/grammar/expression.bluebook +420 -0
- data/lib/hecks/grammar/expression_operators.json +1648 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +471 -0
- data/lib/hecks/language/bluebook/entity.bluebook +392 -0
- data/lib/hecks/language/bluebook/policy.bluebook +189 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +333 -0
- data/lib/hecks/language/port.bluebook +120 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +174 -0
- data/lib/hecks/ports/access_control.port +9 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +8 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +6 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +7 -0
- data/lib/hecks/ports/authorization.rb +62 -0
- data/lib/hecks/ports/clock.port +5 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +5 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +5 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +5 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +5 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +138 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +23 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +96 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +642 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +44 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +446 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +158 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +411 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +92 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +107 -0
- data/lib/hecks/query_specification/hop_path.rb +132 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +66 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
- data/lib/hecks/runtime/command_interpreter.rb +497 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +189 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +245 -0
- data/lib/hecks/runtime/dispatcher.rb +301 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +299 -0
- data/lib/hecks/runtime/errors.rb +122 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +156 -0
- data/lib/hecks/runtime/instance.rb +175 -0
- data/lib/hecks/runtime/interpreting.rb +90 -0
- data/lib/hecks/runtime/loader.rb +184 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +286 -0
- data/lib/hecks/runtime/reaction_invocation.rb +253 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +126 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
- data/lib/hecks/runtime/registry/verification.rb +246 -0
- data/lib/hecks/runtime/registry.rb +279 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +503 -0
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +130 -0
- data/lib/hecks/runtime/value/coercion.rb +565 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +107 -0
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +218 -0
- data/lib/hecks.rb +123 -6
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +412 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
require_relative "../saga_pending_dispatch"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Runtime
|
|
5
|
+
class Registry
|
|
6
|
+
# SAGA PERSISTENCE — no new DSL verb, no new world setting.
|
|
7
|
+
# Whatever adapter a domain's own aggregates already use for
|
|
8
|
+
# durability, its sagas use the same one, automatically: three
|
|
9
|
+
# OPTIONAL adapter methods (`save_saga`/`delete_saga`/`each_saga`),
|
|
10
|
+
# `respond_to?`-checked the same way `Ports::Persistence::AppendOnly`
|
|
11
|
+
# already treats an adapter's own optional methods
|
|
12
|
+
# (`find`/`all`/`count`/`reset!`/`events`/`record_event`).
|
|
13
|
+
module SagaPersistence
|
|
14
|
+
# Resolved and memoized per domain — the FIRST real aggregate
|
|
15
|
+
# declared in the domain's own bluebook is the resolution
|
|
16
|
+
# anchor. `Ports::Persistence::BindingPolicy.resolve` for that
|
|
17
|
+
# aggregate already falls back to a domain-level default bind
|
|
18
|
+
# (§0's `Hecksagon#bind_for`) before falling back to Memory, so
|
|
19
|
+
# this needs no separate "was a default declared" branch of its
|
|
20
|
+
# own — whatever adapter that anchor resolves to already IS the
|
|
21
|
+
# domain's own default in the normal case (every aggregate
|
|
22
|
+
# shares one adapter), and is the honest, documented fallback
|
|
23
|
+
# for the rarer domain genuinely split across more than one
|
|
24
|
+
# local adapter with no default declared.
|
|
25
|
+
#
|
|
26
|
+
# GENUINELY LAZY, GENUINELY POST-BOOT — unlike `rehydrate_sagas!`
|
|
27
|
+
# below (boot-only), this is called from live dispatch
|
|
28
|
+
# (`SagaInterpreter#checkpoint`/`#end_saga`, on every saga
|
|
29
|
+
# transition), so the FIRST call for a given domain can come from
|
|
30
|
+
# any dispatching thread, not just the boot thread. `@saga_persistence`
|
|
31
|
+
# itself is a plain Hash stood up once in `Registry#initialize` (no
|
|
32
|
+
# race on the container), but `resolve_saga_persistence` is real work
|
|
33
|
+
# (a `BindingPolicy.resolve` plus a lazy `repository` build) whose
|
|
34
|
+
# RESULT — the actual adapter instance a domain's sagas persist
|
|
35
|
+
# through — must be the SAME object for every caller: two threads
|
|
36
|
+
# racing the first lookup and each building their own adapter would
|
|
37
|
+
# silently split one domain's saga writes across two adapter
|
|
38
|
+
# instances (worse than `Dispatcher#reaction_depth`'s M20 — that bug
|
|
39
|
+
# corrupted a counter; this one can corrupt WHICH STORE a saga's
|
|
40
|
+
# state lands in). Double-checked locking against a DEDICATED mutex
|
|
41
|
+
# — never `@saga_mutex` — see `Registry#initialize`'s own comment for
|
|
42
|
+
# why reusing that one would deadlock.
|
|
43
|
+
def saga_persistence(domain)
|
|
44
|
+
key = domain.to_s
|
|
45
|
+
@saga_persistence[key] || @saga_persistence_mutex.synchronize { @saga_persistence[key] ||= resolve_saga_persistence(key) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# WALKS EVERY LOADED DOMAIN, repopulating `saga_instances` from
|
|
49
|
+
# whatever `saga_persistence(domain)` resolves to — a real store
|
|
50
|
+
# for a domain whose adapter answers the capability, `each_saga`
|
|
51
|
+
# yielding real rows; `NULL_SAGA_STORE`'s own `each_saga` for
|
|
52
|
+
# everything else, which never yields at all, so this needs no
|
|
53
|
+
# `respond_to?` branch of its own — the SAME reason every other
|
|
54
|
+
# call site in this capability never needs one. Called once at
|
|
55
|
+
# boot (`Loader.boot`, between `verify!` and dispatcher
|
|
56
|
+
# construction) — a process that's been running has no reason to
|
|
57
|
+
# re-walk its own already-current `saga_instances`.
|
|
58
|
+
#
|
|
59
|
+
# BOOT-TIME-ONLY (OR ITS TEST-RUNNER EQUIVALENT) — this method's
|
|
60
|
+
# only callers are `Loader.run_boot_gates!` (single-threaded, before
|
|
61
|
+
# `dispatcher_for` ever exists) and `Registry#reset_runtime_state!`
|
|
62
|
+
# (single-threaded test runner — see that method's own comment).
|
|
63
|
+
# Never called from live dispatch, so `@saga_instances` mutation
|
|
64
|
+
# here has no concurrent caller to race, unlike its OTHER two write
|
|
65
|
+
# points inside `@saga_mutex.synchronize` blocks (`saga_interpreter.
|
|
66
|
+
# rb`), which genuinely do and are guarded accordingly.
|
|
67
|
+
# rubocop:disable-next Hecks/ThreadSharedIvarMutation
|
|
68
|
+
def rehydrate_sagas!
|
|
69
|
+
@hecksagons.each_key do |domain|
|
|
70
|
+
saga_persistence(domain).each_saga do |process_manager, correlation, state, memory, completed_compensations = []|
|
|
71
|
+
pending = memory.delete(SAGA_PENDING_DISPATCH_KEY)
|
|
72
|
+
@saga_instances[process_manager][correlation] =
|
|
73
|
+
{ state: state, memory: memory, completed_compensations: completed_compensations || [] }
|
|
74
|
+
warn_stalled_saga(domain, process_manager, correlation, state, pending) if pending
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# THE OTHER HALF OF THE OUTBOX-SHAPED FIX (see saga_pending_
|
|
83
|
+
# dispatch.rb) — `SAGA_PENDING_DISPATCH_KEY`, if the crashed
|
|
84
|
+
# process left it standing, means the row's own `state` was
|
|
85
|
+
# checkpointed but the dispatch cascade that justifies it may
|
|
86
|
+
# never have run. Stripped out of `memory` before it becomes
|
|
87
|
+
# this instance's LIVE `:memory` (so nothing downstream — a
|
|
88
|
+
# `given`, a `with:` mapping, the fuzzer — ever sees it), and
|
|
89
|
+
# surfaced loudly instead: a WARNING plus a `saga_log` entry,
|
|
90
|
+
# never an automatic redrive (see saga_pending_dispatch.rb for
|
|
91
|
+
# why redriving without idempotent delivery would be worse than
|
|
92
|
+
# the stall). This is real, durable crash-recovery VISIBILITY —
|
|
93
|
+
# still not the reconciliation itself, which stays a human's
|
|
94
|
+
# call until hecks has idempotent redelivery to make it safe.
|
|
95
|
+
# rubocop:disable-next Hecks/ThreadSharedIvarMutation -- same
|
|
96
|
+
# justification as `rehydrate_sagas!`'s own disable comment
|
|
97
|
+
# above: this method's ONLY caller is that boot-time-only walk,
|
|
98
|
+
# never live dispatch, so `@saga_log` has no concurrent writer
|
|
99
|
+
# to race here.
|
|
100
|
+
def warn_stalled_saga(domain, process_manager, correlation, state, pending)
|
|
101
|
+
# `.transform_keys(&:to_sym)` — Heki's own `each_saga` only
|
|
102
|
+
# symbolizes `memory`'s TOP-level keys (`SagaStore#each_saga`'s
|
|
103
|
+
# own `transform_keys`, one level deep); a value NESTED under
|
|
104
|
+
# one of those keys, like this marker, comes back with plain
|
|
105
|
+
# string keys from Heki specifically, symbol keys already from
|
|
106
|
+
# Postgres/SQLite/D1's own `symbolize_names: true` parse. Normalizing
|
|
107
|
+
# here, once, is simpler than teaching every adapter's own
|
|
108
|
+
# shallow/deep parsing convention about this one reserved key.
|
|
109
|
+
pending = pending.transform_keys(&:to_sym)
|
|
110
|
+
dispatches = Array(pending[:dispatches]).join(", ")
|
|
111
|
+
warn "[hecks] #{domain} rehydrated #{process_manager} instance #{correlation.inspect} in state " \
|
|
112
|
+
"#{state.inspect} with a dispatch left pending from before the last crash/restart — " \
|
|
113
|
+
"#{dispatches} (on #{pending[:on].inspect}, #{pending[:from].inspect} -> #{pending[:to].inspect}) " \
|
|
114
|
+
"may or may not have actually run. hecks does not auto-redrive a pending saga dispatch (that " \
|
|
115
|
+
"needs idempotent delivery, which this pipeline doesn't have yet); reconcile this instance by hand."
|
|
116
|
+
@saga_log << { process_manager: process_manager, instance: correlation, rehydrated_stalled: true,
|
|
117
|
+
state: state, pending: pending }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def resolve_saga_persistence(domain)
|
|
121
|
+
anchor = hecksagon(domain) && bluebook(domain)&.aggregates&.first
|
|
122
|
+
return Ports::Persistence::NULL_SAGA_STORE unless anchor
|
|
123
|
+
|
|
124
|
+
bind = Ports::Persistence::BindingPolicy.resolve(self, domain, anchor)
|
|
125
|
+
return Ports::Persistence::NULL_SAGA_STORE if adapter_class(bind.adapter) <= Ports::Persistence::RemoteRuntime
|
|
126
|
+
|
|
127
|
+
adapter = repository(domain, anchor).adapter
|
|
128
|
+
adapter.respond_to?(:save_saga) ? adapter : Ports::Persistence::NULL_SAGA_STORE
|
|
129
|
+
rescue WiringError
|
|
130
|
+
# A domain not cleanly wired for its own anchor aggregate's
|
|
131
|
+
# persistence (a test fixture binding only the aggregates it
|
|
132
|
+
# exercises, say) degrades saga persistence to the same no-op
|
|
133
|
+
# default an unbound domain gets — never raises out of a
|
|
134
|
+
# dispatch that would otherwise have succeeded. Matches this
|
|
135
|
+
# capability's own framing throughout: automatic wherever it's
|
|
136
|
+
# cleanly supported, silently absent wherever it isn't.
|
|
137
|
+
Ports::Persistence::NULL_SAGA_STORE
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
require_relative "../../bluebook/hexagon"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Runtime
|
|
5
|
+
class Registry
|
|
6
|
+
# The wiring gate: every bind names a declared aggregate, every
|
|
7
|
+
# adapter satisfies the verb its port declares, every world setting is
|
|
8
|
+
# a field the adapter admits, and the default adapter is usable at
|
|
9
|
+
# all. Included into Registry — `verify!` is what a boot calls after
|
|
10
|
+
# loading, and the smaller checks are also called piecemeal by the
|
|
11
|
+
# repository factory.
|
|
12
|
+
module Verification
|
|
13
|
+
def verify!
|
|
14
|
+
verify_default_adapter!
|
|
15
|
+
verify_singleton_port_answers!
|
|
16
|
+
|
|
17
|
+
@hecksagons.each_value do |hexagon|
|
|
18
|
+
refuse_ungoverned_roles!(hexagon)
|
|
19
|
+
|
|
20
|
+
hexagon.binds.each do |bind|
|
|
21
|
+
# A domain-level default (§0) — `persisted_by "Heki"` bare,
|
|
22
|
+
# applying to whichever aggregates don't override it — names
|
|
23
|
+
# no aggregate of its own, so there's nothing to look up in
|
|
24
|
+
# the bluebook for THIS row specifically. Still validate its
|
|
25
|
+
# own adapter/verb shape (the same reason
|
|
26
|
+
# `verify_default_adapter!` checks the framework-wide
|
|
27
|
+
# default the same way, aggregate-less). Coverage of real
|
|
28
|
+
# aggregates that only resolve THROUGH this default comes
|
|
29
|
+
# from their own dispatch-time `BindingPolicy.resolve` —
|
|
30
|
+
# deliberately not required to be exhaustive here, the same
|
|
31
|
+
# leniency this method already extended to any aggregate
|
|
32
|
+
# left out of an explicit bind list entirely (real test
|
|
33
|
+
# fixtures bind only the aggregates they exercise).
|
|
34
|
+
if bind.aggregate.nil?
|
|
35
|
+
check_verb(bind)
|
|
36
|
+
next
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
aggregate = bluebook(hexagon.domain)&.aggregate(bind.aggregate_name)
|
|
40
|
+
raise WiringError, "#{bind.aggregate} is bound but not declared in the bluebook" unless aggregate
|
|
41
|
+
|
|
42
|
+
check_verb(bind)
|
|
43
|
+
|
|
44
|
+
repository(hexagon.domain, aggregate)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
warn_undurable_sagas!(hexagon)
|
|
48
|
+
end
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def verify_default_adapter!
|
|
53
|
+
name = Ports::Persistence::DEFAULT_ADAPTER
|
|
54
|
+
|
|
55
|
+
check_verb(
|
|
56
|
+
Bluebook::Bind.new(
|
|
57
|
+
aggregate: "(default)",
|
|
58
|
+
verb: Ports::Persistence::VERB,
|
|
59
|
+
adapter: name
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
adapter_class(name)
|
|
63
|
+
self
|
|
64
|
+
rescue WiringError => error
|
|
65
|
+
raise WiringError,
|
|
66
|
+
"the default persistence adapter (#{name}) is not usable, so an " \
|
|
67
|
+
"aggregate with no bind could not be given one: #{error.message}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def check_verb(bind)
|
|
71
|
+
port = port_for(bind)
|
|
72
|
+
check_answers(port, bind.adapter)
|
|
73
|
+
return if port.verb.to_s == bind.verb.to_s
|
|
74
|
+
|
|
75
|
+
raise WiringError,
|
|
76
|
+
"#{bind.adapter} implements the #{port.name} port (verb #{port.verb}) " \
|
|
77
|
+
"and cannot satisfy #{bind.verb}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# THE METHOD CONTRACT A `.port` FILE'S `verb`/`signal` NEVER
|
|
81
|
+
# CARRIED — an adapter can name the right port, satisfy the right
|
|
82
|
+
# verb, and admit every `.world` setting `check_settings` checks,
|
|
83
|
+
# and still be missing the one method a live dispatch will
|
|
84
|
+
# actually call. `answers` is optional per port (an empty list is
|
|
85
|
+
# today's pre-existing behavior, unchecked), so this only ever
|
|
86
|
+
# tightens a port that opted in.
|
|
87
|
+
def check_answers(port, adapter_name)
|
|
88
|
+
answers = Array(port.answers)
|
|
89
|
+
return if answers.empty?
|
|
90
|
+
|
|
91
|
+
klass = adapter_class(adapter_name)
|
|
92
|
+
missing = answers.reject { |method_name| klass.respond_to?(method_name) }
|
|
93
|
+
return if missing.empty?
|
|
94
|
+
|
|
95
|
+
raise WiringError,
|
|
96
|
+
"#{adapter_name} declares the #{port.name} port but does not respond to " \
|
|
97
|
+
"#{missing.map(&:inspect).join(', ')} — #{port.name}.port declares answers " \
|
|
98
|
+
"#{answers.map(&:inspect).join(', ')}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# THE NINE SINGLETON PORTS' OWN GAP — `persistence`, `projection`
|
|
102
|
+
# and `loading` are per-aggregate bindings, checked above through
|
|
103
|
+
# every real `bind` a hexagon declares; a singleton port
|
|
104
|
+
# (`clock`, `authorization`, …) is never bound to an aggregate at
|
|
105
|
+
# all, so nothing above ever resolves one and nothing above ever
|
|
106
|
+
# ran `check_answers` against it. Each one's own `Ports::*.adapter`
|
|
107
|
+
# already refuses zero or multiple implementations, live, at
|
|
108
|
+
# first dispatch — that stays exactly as-is here (0 or 2+ is
|
|
109
|
+
# ambiguity, not a method-contract question, and asserting every
|
|
110
|
+
# declared port MUST have exactly one adapter would wrongly
|
|
111
|
+
# refuse a boot that simply never wires a port it doesn't use).
|
|
112
|
+
# This only ever tightens the ONE case those checks don't cover:
|
|
113
|
+
# exactly one adapter, wired, missing a method `answers` names.
|
|
114
|
+
PER_AGGREGATE_PORTS = %w[persistence projection loading].freeze
|
|
115
|
+
|
|
116
|
+
def verify_singleton_port_answers!
|
|
117
|
+
@ports.each_value do |port|
|
|
118
|
+
next if PER_AGGREGATE_PORTS.include?(port.name)
|
|
119
|
+
next if Array(port.answers).empty?
|
|
120
|
+
|
|
121
|
+
implementations = @adapters.values.select { |a| a.port == port.name }
|
|
122
|
+
next unless implementations.size == 1
|
|
123
|
+
|
|
124
|
+
check_answers(port, implementations.first.name)
|
|
125
|
+
end
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def check_settings(bind, settings)
|
|
130
|
+
adapter = @adapters[bind.adapter]
|
|
131
|
+
return unless adapter
|
|
132
|
+
|
|
133
|
+
declared = settings.keys - [:adapter]
|
|
134
|
+
unknown = declared.reject { |field| adapter.declares?(field) }
|
|
135
|
+
return if unknown.empty?
|
|
136
|
+
|
|
137
|
+
raise WiringError,
|
|
138
|
+
"#{bind.adapter} does not declare #{unknown.map(&:inspect).join(', ')} — " \
|
|
139
|
+
"it declares #{adapter.all_fields.map(&:inspect).join(', ')}. " \
|
|
140
|
+
"Add the field to the adapter, or remove it from the world."
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def port_for(bind)
|
|
144
|
+
adapter = @adapters[bind.adapter]
|
|
145
|
+
raise WiringError, "unknown adapter #{bind.adapter.inspect}" unless adapter
|
|
146
|
+
|
|
147
|
+
@ports[adapter.port] ||
|
|
148
|
+
raise(WiringError, "adapter #{bind.adapter} declares unknown port #{adapter.port.inspect}")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def adapter_class(name)
|
|
152
|
+
Adapters.const_get(name)
|
|
153
|
+
rescue NameError
|
|
154
|
+
raise WiringError, "no Ruby adapter implementation for #{name.inspect} " \
|
|
155
|
+
"(expected Hecks::Adapters::#{name})"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
# `role` IS REAL ACCESS CONTROL ONLY WHEN GOVERNANCE CAN CHECK IT
|
|
161
|
+
# AGAINST SOMETHING — a command that declares a role but whose
|
|
162
|
+
# domain never attaches Governance would leave that role forever
|
|
163
|
+
# unchecked, exactly the defect ADR 0025 §9 names ("role gates
|
|
164
|
+
# access control by exact string equality ... Governance ...
|
|
165
|
+
# connected to none of it"). Checked here, at `verify!` — RECOVERED
|
|
166
|
+
# and MOVED, not new: this used to run per-block, at HECKSAGON
|
|
167
|
+
# build time (Bluebook::DSL::HecksagonBuilder#build), which broke
|
|
168
|
+
# the moment a domain could be split across multiple hecksagon
|
|
169
|
+
# blocks (base + an `environments/<name>.hecksagon` overlay,
|
|
170
|
+
# Runtime::Loader.boot's `environment:` — see its own comment for
|
|
171
|
+
# the recovery provenance): every block but the one declaring
|
|
172
|
+
# `uses_framework "Governance"` would be refused there, even
|
|
173
|
+
# though `Registry#add_hecksagon` merges every block for a domain
|
|
174
|
+
# into ONE Hecksagon before anything ever dispatches against it.
|
|
175
|
+
# Checking the MERGED result once, here, after every file for
|
|
176
|
+
# this domain has loaded, is both more permissive (no need to
|
|
177
|
+
# repeat `uses_framework` in every file) and strictly more
|
|
178
|
+
# correct (a check against an incomplete, not-yet-merged
|
|
179
|
+
# hecksagon can never see the real final shape).
|
|
180
|
+
#
|
|
181
|
+
# GOVERNANCE ITSELF IS EXEMPT — it cannot `uses_framework` its own
|
|
182
|
+
# aggregates, and it IS the source of truth a role check runs
|
|
183
|
+
# against, the same self-reference `CommandRules::Authorization
|
|
184
|
+
# #governance_attached?` grants it at dispatch time.
|
|
185
|
+
def refuse_ungoverned_roles!(hexagon)
|
|
186
|
+
return if hexagon.domain == "Governance"
|
|
187
|
+
return if hexagon.framework_members.include?("Governance")
|
|
188
|
+
|
|
189
|
+
bluebook_ir = bluebook(hexagon.domain)
|
|
190
|
+
return unless bluebook_ir
|
|
191
|
+
|
|
192
|
+
offender = commands_in(bluebook_ir).find { |command| !command.role.to_s.empty? }
|
|
193
|
+
return unless offender
|
|
194
|
+
|
|
195
|
+
raise WiringError,
|
|
196
|
+
"#{offender.hecks_fqn} declares role #{offender.role.inspect}, but " \
|
|
197
|
+
"#{hexagon.domain}'s hecksagon never uses_framework \"Governance\" — role is only " \
|
|
198
|
+
"real access control once Governance is attached to check it against; without that " \
|
|
199
|
+
"it is silent decoration, the exact defect this refusal exists to catch"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Every command this domain declares, an aggregate's own AND every
|
|
203
|
+
# entity nested inside one — the same reach `refuse_role_mismatch`
|
|
204
|
+
# itself needs at dispatch time, just walked ahead of time here.
|
|
205
|
+
def commands_in(bluebook_ir)
|
|
206
|
+
bluebook_ir.aggregates.flat_map { |aggregate| aggregate.commands + aggregate.entities.flat_map(&:commands) }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# A domain that declares a `process_manager` but whose
|
|
210
|
+
# `saga_persistence` resolves to `NULL_SAGA_STORE` (no anchor
|
|
211
|
+
# aggregate, a RemoteRuntime-shaped adapter, an adapter that
|
|
212
|
+
# doesn't `respond_to?(:save_saga)`, or a rescued WiringError —
|
|
213
|
+
# see `SagaPersistence#resolve_saga_persistence`) gets sagas that
|
|
214
|
+
# advance correctly in-process and vanish on restart: no
|
|
215
|
+
# checkpoint written, nothing for `rehydrate_sagas!` to find, no
|
|
216
|
+
# compensation ever replayed. That is silent right up until the
|
|
217
|
+
# process actually dies mid-saga — the same "consistency/
|
|
218
|
+
# freshness defect applied to access control, failing open" ADR
|
|
219
|
+
# 0025 named for an unchecked `role`, here applied to saga
|
|
220
|
+
# durability instead.
|
|
221
|
+
#
|
|
222
|
+
# A WARNING, NOT A REFUSAL — unlike `refuse_ungoverned_roles!`,
|
|
223
|
+
# running sagas on a store with no `save_saga` is legitimate on
|
|
224
|
+
# purpose in a fast in-memory test/dev boot (this project's own
|
|
225
|
+
# `saga_durability_spec.rb` boots a process manager on `Memory`
|
|
226
|
+
# specifically to exercise the saga_mutex without real I/O), so
|
|
227
|
+
# refusing the boot outright would break a choice an author made
|
|
228
|
+
# deliberately. What a deploy needs is for the gap to be loud and
|
|
229
|
+
# undeniable, not for local dev/test to become impossible.
|
|
230
|
+
def warn_undurable_sagas!(hexagon)
|
|
231
|
+
bluebook_ir = bluebook(hexagon.domain)
|
|
232
|
+
return unless bluebook_ir
|
|
233
|
+
return if bluebook_ir.process_managers.empty?
|
|
234
|
+
return unless saga_persistence(hexagon.domain).equal?(Ports::Persistence::NULL_SAGA_STORE)
|
|
235
|
+
|
|
236
|
+
names = bluebook_ir.process_managers.map(&:name).join(", ")
|
|
237
|
+
warn "[hecks] #{hexagon.domain} declares process_manager(s) #{names} but its resolved " \
|
|
238
|
+
"persistence adapter has no save_saga — saga state advances correctly in-process " \
|
|
239
|
+
"and is LOST on restart (no checkpoint, no rehydration, no compensation replay). " \
|
|
240
|
+
"Bind this domain to an adapter that implements save_saga if this process_manager " \
|
|
241
|
+
"must survive a crash."
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
require_relative "registry/verification"
|
|
2
|
+
require_relative "registry/saga_persistence"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Runtime
|
|
6
|
+
class WiringError < StandardError; end
|
|
7
|
+
|
|
8
|
+
# The collections a boot gathers — bluebooks, hexagons, ports, adapters,
|
|
9
|
+
# worlds, the logs — and how a repository is resolved from them. The
|
|
10
|
+
# wiring gate lives in registry/verification.rb, saga persistence
|
|
11
|
+
# resolution in registry/saga_persistence.rb.
|
|
12
|
+
class Registry
|
|
13
|
+
include Verification
|
|
14
|
+
include SagaPersistence
|
|
15
|
+
|
|
16
|
+
attr_reader :root, :bluebooks, :hecksagons, :ports, :adapters, :worlds, :event_log,
|
|
17
|
+
:reaction_log, :saga_log, :saga_instances, :translations, :saga_mutex,
|
|
18
|
+
:saga_dispatch_log, :policy_dispatch_log
|
|
19
|
+
|
|
20
|
+
def initialize(root: nil)
|
|
21
|
+
@root = root
|
|
22
|
+
@bluebooks = {}
|
|
23
|
+
@hecksagons = {}
|
|
24
|
+
@ports = {}
|
|
25
|
+
@adapters = {}
|
|
26
|
+
@worlds = {}
|
|
27
|
+
@translations = []
|
|
28
|
+
@event_log = []
|
|
29
|
+
@reaction_log = []
|
|
30
|
+
@saga_log = []
|
|
31
|
+
# ADDITIVE, RUBY-ONLY — never merged into saga_log/reaction_log.
|
|
32
|
+
# rust/src/kernel/orchestrate.rs ports THOSE two arrays' exact
|
|
33
|
+
# shape byte-for-byte (spec/rust_conformance_spec.rb's own
|
|
34
|
+
# equality check) — a landmine found by reading that spec before
|
|
35
|
+
# touching anything, not by hitting it. These carry the raw
|
|
36
|
+
# inputs a dispatch's own argument binding was resolved from
|
|
37
|
+
# (SagaInterpreter#deliver_saga_dispatch / PolicyInterpreter#
|
|
38
|
+
# trigger_args), for Properties.dispatch_binding_fidelity's own
|
|
39
|
+
# independent re-derivation — a fact neither existing log
|
|
40
|
+
# records at all, so there is nothing here for Rust to have
|
|
41
|
+
# matched or drifted from.
|
|
42
|
+
@saga_dispatch_log = []
|
|
43
|
+
@policy_dispatch_log = []
|
|
44
|
+
@saga_instances = Hash.new { |h, k| h[k] = {} }
|
|
45
|
+
# GUARDS `saga_instances`' OWN mutation+checkpoint sequence
|
|
46
|
+
# (`SagaInterpreter`'s 4 write points, §7) — the same shape of
|
|
47
|
+
# hazard this codebase's own prior audit already flagged for
|
|
48
|
+
# `Dispatcher#reenter`'s reaction-depth counter (M20: a
|
|
49
|
+
# thread-shared ivar with no lock, since fixed by moving it to
|
|
50
|
+
# `Thread.current`, dispatcher.rb), made meaningfully easier to
|
|
51
|
+
# hit here once a persistence write sits in the same critical
|
|
52
|
+
# section. Held across the in-memory
|
|
53
|
+
# mutation AND the checkpoint write together, never across a
|
|
54
|
+
# saga's own dispatch cascade — see `SagaInterpreter#advance_saga`'s
|
|
55
|
+
# own comment for why that distinction matters (non-reentrant
|
|
56
|
+
# Mutex, recursive re-entry is real).
|
|
57
|
+
@saga_mutex = Mutex.new
|
|
58
|
+
@repositories = {}
|
|
59
|
+
@projection_repositories = {}
|
|
60
|
+
@bluebook_builders = {}
|
|
61
|
+
# EAGER, NOT LAZY — see `#resolved_eras`'s own comment for why. Built
|
|
62
|
+
# here rather than `@resolved_eras ||= {}` on first access so there is
|
|
63
|
+
# no window, post-boot, where two concurrently dispatching threads
|
|
64
|
+
# could race creating this Hash (Hecks/ThreadSharedIvarMutation; the
|
|
65
|
+
# same shape of hazard `Dispatcher#reenter`'s `@reaction_depth` was
|
|
66
|
+
# fixed for). Every WRITE into it still only ever happens at boot,
|
|
67
|
+
# single-threaded (`EraResolver.check!`, a `:pre_verify` boot gate) —
|
|
68
|
+
# this only removes the race on standing up the container itself for
|
|
69
|
+
# a boot with no era-plugin domain at all, whose first touch would
|
|
70
|
+
# otherwise be a live dispatch's own `RepositoryFactory.build` read.
|
|
71
|
+
@resolved_eras = {}
|
|
72
|
+
# EAGER, NOT LAZY — see `#capability_graph`'s own comment for why.
|
|
73
|
+
# `CapabilityGraph.new` only stores the registry reference; there is
|
|
74
|
+
# no reason to defer it, and doing so removes the exact same
|
|
75
|
+
# first-access race `#resolved_eras` above does, while preserving the
|
|
76
|
+
# "same instance every call" identity `spec/runtime/capability_graph_
|
|
77
|
+
# spec.rb` already requires.
|
|
78
|
+
@capability_graph = CapabilityGraph.new(self)
|
|
79
|
+
# `@saga_persistence` itself is eager (see `#saga_persistence`'s own
|
|
80
|
+
# comment) — only the PER-DOMAIN resolution inside it is genuinely
|
|
81
|
+
# expensive and lazy, guarded by this dedicated mutex. NOT the same
|
|
82
|
+
# mutex as `@saga_mutex`: `checkpoint` (saga_interpreter.rb) calls
|
|
83
|
+
# `saga_persistence(domain)` from INSIDE an `@saga_mutex.synchronize`
|
|
84
|
+
# block, so reusing `@saga_mutex` here would deadlock the very first
|
|
85
|
+
# time a saga advanced (a `Mutex` is not reentrant — the exact
|
|
86
|
+
# warning `@saga_mutex`'s own comment already gives for a different
|
|
87
|
+
# reason).
|
|
88
|
+
@saga_persistence = {}
|
|
89
|
+
@saga_persistence_mutex = Mutex.new
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# THE BUILDER STAYS OPEN FOR THE LIFE OF THIS REGISTRY, keyed by chapter
|
|
93
|
+
# name — see the comment on `BluebookBuilder.build`. A chapter split across
|
|
94
|
+
# several files (`language/bluebook/*.bluebook`, all `Hecks.bluebook "Bluebook"`)
|
|
95
|
+
# needs its declarations to accumulate into ONE builder rather than each
|
|
96
|
+
# file minting its own and silently discarding the one before.
|
|
97
|
+
def bluebook_builder(name)
|
|
98
|
+
@bluebook_builders[name.to_s] ||= yield
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# BOOT-TIME-ONLY, SINGLE-THREADED — every `add_*` below (through
|
|
102
|
+
# `add_translation`) is called exclusively from `Hecks.collect`
|
|
103
|
+
# (hecks.rb), which is what `Hecks.bluebook`/`.hecksagon`/`.port`/
|
|
104
|
+
# `.adapter`/`.world`/`.translation` run inside while a `.bluebook`/
|
|
105
|
+
# `.hecksagon`/`.world` file is being `Kernel.load`ed — i.e. strictly
|
|
106
|
+
# during `Loader.boot`/`.boot_files`, before `dispatcher_for` ever
|
|
107
|
+
# hands this registry to a live, multi-threaded caller. Nothing
|
|
108
|
+
# downstream of boot ever calls these — verified by grepping every
|
|
109
|
+
# call site in lib/ and spec/ before writing this — so unlike
|
|
110
|
+
# `#resolved_eras`/`#capability_graph`/`#saga_persistence` (each
|
|
111
|
+
# reachable from live dispatch, and fixed for real above/in
|
|
112
|
+
# registry/saga_persistence.rb) there is no concurrent caller for
|
|
113
|
+
# `Hecks/ThreadSharedIvarMutation` to actually be warning about here.
|
|
114
|
+
# rubocop:disable Hecks/ThreadSharedIvarMutation
|
|
115
|
+
def add_bluebook(item) = @bluebooks[item.name] = item
|
|
116
|
+
|
|
117
|
+
# MERGED, NOT REPLACED — RECOVERED, not new (see Runtime::Loader
|
|
118
|
+
# .boot's own comment for the provenance). A domain's hecksagon can
|
|
119
|
+
# now load in more than one block for the same domain (base file
|
|
120
|
+
# plus an `environments/<name>.hecksagon` overlay), and the second
|
|
121
|
+
# block should ADD to what the first declared, not silently
|
|
122
|
+
# discard it.
|
|
123
|
+
def add_hecksagon(item)
|
|
124
|
+
existing = @hecksagons[item.domain]
|
|
125
|
+
@hecksagons[item.domain] = existing ? merge_hecksagons(existing, item) : item
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def add_port(item) = @ports[item.name] = item
|
|
129
|
+
def add_adapter(item) = @adapters[item.name] = item
|
|
130
|
+
|
|
131
|
+
# MERGED, NOT REPLACED — the same generalization for `World` that
|
|
132
|
+
# `add_hecksagon` above recovers for `Hecksagon`: an
|
|
133
|
+
# `environments/<name>.world` overlay (or a host-owned tenancy
|
|
134
|
+
# overlay world, same mechanism) can now add or override settings
|
|
135
|
+
# for a domain a base `.world` file already declared, without
|
|
136
|
+
# restating everything the base file said. Settings merge shallow,
|
|
137
|
+
# keyed exactly the way WorldBuilder already stores them (both the
|
|
138
|
+
# bare verb key and the `"verb:adapter"` qualified key point at the
|
|
139
|
+
# same resolved hash) — an overlay's key wins over the base's same
|
|
140
|
+
# key; a key only the base declares survives untouched.
|
|
141
|
+
def add_world(item)
|
|
142
|
+
existing = @worlds[item.domain]
|
|
143
|
+
@worlds[item.domain] = existing ? merge_worlds(existing, item) : item
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def add_translation(item) = @translations << item
|
|
147
|
+
# rubocop:enable Hecks/ThreadSharedIvarMutation
|
|
148
|
+
|
|
149
|
+
# {domain name => era ordinal} as resolved by the boot-time era
|
|
150
|
+
# gate. A lineage adapter writes into ITS OWN era's partition —
|
|
151
|
+
# which, for an old checkout booting a held-but-superseded shape,
|
|
152
|
+
# is not the newest one. The Hash itself is stood up in `initialize`
|
|
153
|
+
# (see that comment) — this is a plain reader, not a memoizer;
|
|
154
|
+
# `Hecks/ThreadSharedIvarMutation` is the reason there is no `||=`
|
|
155
|
+
# left here to flag.
|
|
156
|
+
attr_reader :resolved_eras
|
|
157
|
+
|
|
158
|
+
def bluebook(name) = @bluebooks[name.to_s]
|
|
159
|
+
def hecksagon(name) = @hecksagons[name.to_s]
|
|
160
|
+
def world(name) = @worlds[name.to_s]
|
|
161
|
+
|
|
162
|
+
def verbs = @bluebooks.values.flat_map(&:verbs).sort
|
|
163
|
+
|
|
164
|
+
def repository(domain, aggregate)
|
|
165
|
+
@repositories[[domain.to_s, aggregate.hecks_name]] ||= Ports::Persistence.repository(self, domain, aggregate)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# EVERYTHING A DISPATCH WROTE, CLEARED; NOTHING A BOOT DECLARED,
|
|
169
|
+
# TOUCHED. Bluebooks, hecksagons, ports, adapters, worlds and the
|
|
170
|
+
# resolved eras are what loading the files produced and stay as
|
|
171
|
+
# they are; the logs, the saga instances and the repositories are
|
|
172
|
+
# what running commands against them produced, and go back to
|
|
173
|
+
# exactly what a fresh boot of the same files hands out. Dropping
|
|
174
|
+
# the repositories (rather than emptying each) is deliberate: a
|
|
175
|
+
# fresh boot's own repositories are new adapter instances too, so
|
|
176
|
+
# a Memory adapter starts empty and a durable one sees whatever
|
|
177
|
+
# it persisted — the same reading either way. Sagas rehydrate off
|
|
178
|
+
# that store again, the way `Loader.boot_files` does after
|
|
179
|
+
# `verify!`.
|
|
180
|
+
#
|
|
181
|
+
# What this is for: a test runner that used to boot a runtime per
|
|
182
|
+
# test to get isolation (`Behaviors::Expectations.run_one`) — ~2s a
|
|
183
|
+
# boot, 76 chess behaviours = two and a half minutes of booting the
|
|
184
|
+
# same two files — can now boot once and reset between tests.
|
|
185
|
+
#
|
|
186
|
+
# SINGLE-THREADED CALLER, THE SAME REASON THE `add_*` CLUSTER ABOVE
|
|
187
|
+
# IS EXEMPT — `Behaviors::Expectations.run_one` is this method's ONLY
|
|
188
|
+
# caller (verified by grep before writing this), and it runs one
|
|
189
|
+
# test at a time: `Runner#run` maps over tests sequentially, and
|
|
190
|
+
# `Behaviors.rspec`'s generated examples run under RSpec's own
|
|
191
|
+
# single-threaded example loop. No production dispatch path calls
|
|
192
|
+
# this at all — a live Puma worker pool never resets a registry out
|
|
193
|
+
# from under itself mid-flight.
|
|
194
|
+
# rubocop:disable-next Hecks/ThreadSharedIvarMutation
|
|
195
|
+
def reset_runtime_state!
|
|
196
|
+
@event_log.clear
|
|
197
|
+
@reaction_log.clear
|
|
198
|
+
@saga_log.clear
|
|
199
|
+
@saga_dispatch_log.clear
|
|
200
|
+
@policy_dispatch_log.clear
|
|
201
|
+
@saga_instances.clear
|
|
202
|
+
@repositories = {}
|
|
203
|
+
@projection_repositories = {}
|
|
204
|
+
rehydrate_sagas!
|
|
205
|
+
self
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Built eagerly in `initialize` (see that comment) — this is a plain
|
|
209
|
+
# reader, not a memoizer; `spec/runtime/capability_graph_spec.rb`
|
|
210
|
+
# asserts the SAME instance comes back every call, which this still
|
|
211
|
+
# gives, just without a lazy `||=` race on standing it up.
|
|
212
|
+
attr_reader :capability_graph
|
|
213
|
+
|
|
214
|
+
def read_repository(domain, aggregate)
|
|
215
|
+
key = [domain.to_s, aggregate.hecks_name]
|
|
216
|
+
binding = Ports::Projection.binds_for(self, domain, aggregate).first
|
|
217
|
+
return repository(domain, aggregate) unless binding
|
|
218
|
+
|
|
219
|
+
projection = (@projection_repositories[key] ||= begin
|
|
220
|
+
projection = Ports::Persistence::RepositoryFactory.build(self, domain, aggregate, binding,
|
|
221
|
+
recover: true, settings_verb: Ports::Projection::VERB)
|
|
222
|
+
projection
|
|
223
|
+
end)
|
|
224
|
+
authoritative = repository(domain, aggregate)
|
|
225
|
+
projection_current?(projection, authoritative) ? projection : authoritative
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def projection_current?(projection, authoritative)
|
|
229
|
+
projected_entries = projection.entries
|
|
230
|
+
source_entries = authoritative.entries
|
|
231
|
+
return false unless projected_entries.length == source_entries.length
|
|
232
|
+
return false unless projected_entries.zip(source_entries).all? do |projected, source|
|
|
233
|
+
projected.operation == source.operation && projected.id == source.id && projected.state == source.state
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
projected_rows = projection.all.map(&:to_h).sort_by { |row| row.fetch(:id).to_s }
|
|
237
|
+
source_rows = authoritative.all.map(&:to_h).sort_by { |row| row.fetch(:id).to_s }
|
|
238
|
+
projected_rows == source_rows
|
|
239
|
+
rescue StandardError
|
|
240
|
+
false
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# RECOVERED — see `add_hecksagon`'s own comment for provenance.
|
|
244
|
+
# Concatenates every list-shaped fact; `binds` in particular is
|
|
245
|
+
# additive because an overlay REBINDING an aggregate (a new
|
|
246
|
+
# `persisted_by` for the same aggregate/verb) is meant to shadow
|
|
247
|
+
# the base's own bind at resolution time, not erase it outright —
|
|
248
|
+
# `Ports::Persistence::BindingPolicy.resolve`'s own "exactly one
|
|
249
|
+
# authoritative bind" check is what actually catches a genuine
|
|
250
|
+
# double-bind; this merge only concatenates, it does not itself
|
|
251
|
+
# decide which of two binds for the same aggregate wins.
|
|
252
|
+
def merge_hecksagons(a, b)
|
|
253
|
+
Bluebook::Hecksagon.new(
|
|
254
|
+
domain: a.domain,
|
|
255
|
+
binds: a.binds + b.binds,
|
|
256
|
+
subscriptions: a.subscriptions + b.subscriptions,
|
|
257
|
+
framework_members: a.framework_members + b.framework_members,
|
|
258
|
+
vendored_bluebooks: a.vendored_bluebooks + b.vendored_bluebooks
|
|
259
|
+
)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# RECOVERED AND GENERALIZED — see `add_world`'s own comment. `realm`/
|
|
263
|
+
# `latest` are scalars, so the overlay's value wins when present,
|
|
264
|
+
# else the base's survives; `settings` is a shallow merge keyed by
|
|
265
|
+
# verb (and `"verb:adapter"`) — an overlay entry for a key the base
|
|
266
|
+
# also declares REPLACES that key's whole resolved hash (the same
|
|
267
|
+
# all-or-nothing shape `WorldBuilder#method_missing` already builds
|
|
268
|
+
# each entry as), it does not deep-merge field by field within it.
|
|
269
|
+
def merge_worlds(a, b)
|
|
270
|
+
Bluebook::World.new(
|
|
271
|
+
domain: a.domain,
|
|
272
|
+
realm: b.realm || a.realm,
|
|
273
|
+
latest: b.latest || a.latest,
|
|
274
|
+
settings: a.settings.merge(b.settings)
|
|
275
|
+
)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|