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,178 @@
|
|
|
1
|
+
require_relative "state_codec"
|
|
2
|
+
require_relative "../../runtime/errors"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Ports
|
|
6
|
+
module Persistence
|
|
7
|
+
# No adapter can build an `Instance` from undecoded state (Phase 2,
|
|
8
|
+
# Track A, PR A3). Routing every adapter through `StateCodec` is a
|
|
9
|
+
# convention until something refuses the bypass; this is that
|
|
10
|
+
# something, and it is installed by `RepositoryFactory.build` — the
|
|
11
|
+
# one place every runtime repository is made — so no adapter can opt
|
|
12
|
+
# out of it.
|
|
13
|
+
#
|
|
14
|
+
# ## The mechanism, two halves
|
|
15
|
+
#
|
|
16
|
+
# 1. `guard!(adapter)` extends the adapter object (not its class,
|
|
17
|
+
# not a proxy — `is_a?`, `class`, and `===` stay the adapter's
|
|
18
|
+
# own) with a module that wraps every public method the adapter's
|
|
19
|
+
# class defines. Each call runs inside the boundary: a
|
|
20
|
+
# thread-local flag, re-entrant, restored on the way out. A block
|
|
21
|
+
# the caller passes (`transaction`, `with_write_lock`,
|
|
22
|
+
# `each_saga`) runs outside it — that block is the dispatch
|
|
23
|
+
# itself (hydrate, entity views, mutation), not adapter code.
|
|
24
|
+
# Reaching the adapter through `repository.adapter` (the query
|
|
25
|
+
# port, saga persistence, `bin/heki_compact`) is guarded all the
|
|
26
|
+
# same: the wrapper is on the object.
|
|
27
|
+
#
|
|
28
|
+
# 2. `Runtime::Instance#initialize` asks `check_state!` whenever it
|
|
29
|
+
# is handed `state:`. Outside the boundary that is a no-op; inside
|
|
30
|
+
# it, state `StateCodec.decoded?` rejects refuses by name. An
|
|
31
|
+
# `entries` answer is checked the same way on its way out, since a
|
|
32
|
+
# journal `Entry` is not an `Instance`.
|
|
33
|
+
#
|
|
34
|
+
# ## Checked, not silently decoded
|
|
35
|
+
#
|
|
36
|
+
# An adapter that forgets the codec is a bug in that adapter, and
|
|
37
|
+
# decoding for it here would hide the forgetting. Hydration does not
|
|
38
|
+
# respell keys either (A4) — `Value.hydrate` refuses a non-Symbol
|
|
39
|
+
# top-level key everywhere — so this boundary's deep check and
|
|
40
|
+
# hydration's shallow one agree.
|
|
41
|
+
module CodecBoundary
|
|
42
|
+
KEY = :hecks_persistence_codec_boundary
|
|
43
|
+
|
|
44
|
+
module_function
|
|
45
|
+
|
|
46
|
+
# Extends an adapter object so every public method its class defines runs inside the
|
|
47
|
+
# boundary; guarding an adapter twice wraps it once.
|
|
48
|
+
#
|
|
49
|
+
# @param adapter [Object] a driven persistence adapter instance
|
|
50
|
+
# @return [Object] the same adapter object, now including `Guarded`
|
|
51
|
+
def guard!(adapter)
|
|
52
|
+
adapter.extend(wrapper_for(adapter.class)) unless adapter.singleton_class.include?(Guarded)
|
|
53
|
+
adapter
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Reports whether the current thread is executing inside a guarded adapter call.
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean] true between entering a guarded adapter method and yielding to the
|
|
59
|
+
# caller's block or returning
|
|
60
|
+
def active? = Thread.current[KEY] == true
|
|
61
|
+
|
|
62
|
+
# Runs the block with the boundary switched on for this thread, restoring the earlier
|
|
63
|
+
# setting afterwards.
|
|
64
|
+
#
|
|
65
|
+
# @yield adapter code whose `Instance` construction is to be checked
|
|
66
|
+
# @return [Object] the block's value
|
|
67
|
+
def within(&) = with_flag(true, &)
|
|
68
|
+
|
|
69
|
+
# Runs the block with the boundary switched off for this thread, restoring the earlier
|
|
70
|
+
# setting afterwards.
|
|
71
|
+
#
|
|
72
|
+
# @yield caller code, such as a dispatch running inside the adapter's transaction
|
|
73
|
+
# @return [Object] the block's value
|
|
74
|
+
def outside(&) = with_flag(false, &)
|
|
75
|
+
|
|
76
|
+
# Refuses state an adapter is about to build an `Instance` from unless it is decoded;
|
|
77
|
+
# outside the boundary it checks nothing.
|
|
78
|
+
#
|
|
79
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the state
|
|
80
|
+
# belongs to
|
|
81
|
+
# @param state [Hash, Runtime::Value] the state handed to `Runtime::Instance.new`
|
|
82
|
+
# @return [nil] when the boundary is inactive or the state is decoded
|
|
83
|
+
# @raise [Runtime::WiringError] if the boundary is active and
|
|
84
|
+
# `StateCodec.decoded?` rejects the state
|
|
85
|
+
def check_state!(aggregate, state)
|
|
86
|
+
return unless active?
|
|
87
|
+
return if StateCodec.decoded?(aggregate, state)
|
|
88
|
+
|
|
89
|
+
raise Runtime::WiringError,
|
|
90
|
+
"a persistence adapter built a #{aggregate.name} record from undecoded state " \
|
|
91
|
+
"(#{state.keys.inspect}) — decode stored state through " \
|
|
92
|
+
"Hecks::Ports::Persistence::StateCodec.decode before handing it to Runtime::Instance"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Refuses a guarded adapter's `entries` answer if any entry's state is undecoded.
|
|
96
|
+
#
|
|
97
|
+
# @param adapter [Object] the guarded adapter, asked for its `aggregate` and class name
|
|
98
|
+
# @param entries [Array<Persistence::Entry>, Object] what the adapter's `entries`
|
|
99
|
+
# returned; anything that is not an Array passes through unchecked
|
|
100
|
+
# @return [Array<Persistence::Entry>, Object] `entries` itself
|
|
101
|
+
# @raise [Runtime::WiringError] if an `Entry` in the Array carries state
|
|
102
|
+
# `StateCodec.decoded?` rejects
|
|
103
|
+
def check_entries!(adapter, entries)
|
|
104
|
+
return entries unless entries.is_a?(Array)
|
|
105
|
+
|
|
106
|
+
entries.each do |entry|
|
|
107
|
+
next unless entry.is_a?(Entry) && !StateCodec.decoded?(adapter.aggregate, entry.state)
|
|
108
|
+
|
|
109
|
+
raise Runtime::WiringError,
|
|
110
|
+
"#{adapter.class} answered a #{adapter.aggregate.name} journal entry #{entry.id.inspect} with " \
|
|
111
|
+
"undecoded state — decode it through Hecks::Ports::Persistence::StateCodec.decode"
|
|
112
|
+
end
|
|
113
|
+
entries
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Sets the thread-local boundary flag for the length of the block, which is what makes
|
|
117
|
+
# the boundary re-entrant.
|
|
118
|
+
#
|
|
119
|
+
# @param value [Boolean] true to switch the boundary on, false to switch it off
|
|
120
|
+
# @yield the code to run under that setting
|
|
121
|
+
# @return [Object] the block's value; the earlier flag is restored even when the
|
|
122
|
+
# block raises
|
|
123
|
+
def with_flag(value)
|
|
124
|
+
previous = Thread.current[KEY]
|
|
125
|
+
Thread.current[KEY] = value
|
|
126
|
+
yield
|
|
127
|
+
ensure
|
|
128
|
+
Thread.current[KEY] = previous
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Marks a guarded adapter so a second `guard!` (a projection and an
|
|
132
|
+
# authoritative repository built over one object) never wraps twice.
|
|
133
|
+
module Guarded; end
|
|
134
|
+
|
|
135
|
+
# Filled lazily under `WRAPPERS_LOCK`, one entry per adapter class —
|
|
136
|
+
# a cache, deliberately mutable.
|
|
137
|
+
WRAPPERS = {} # rubocop:disable Style/MutableConstant
|
|
138
|
+
WRAPPERS_LOCK = Mutex.new
|
|
139
|
+
|
|
140
|
+
# Builds, or fetches from the cache, the module that guards one adapter class.
|
|
141
|
+
#
|
|
142
|
+
# One wrapper module per adapter class, built once: every public
|
|
143
|
+
# instance method the class (and its ancestors below Object)
|
|
144
|
+
# defines, each forwarding to `super` inside the boundary.
|
|
145
|
+
#
|
|
146
|
+
# @param klass [Class] the adapter's class
|
|
147
|
+
# @return [Module] an anonymous module including `Guarded`, meant to be `extend`ed onto
|
|
148
|
+
# instances of `klass`
|
|
149
|
+
def wrapper_for(klass)
|
|
150
|
+
WRAPPERS_LOCK.synchronize do
|
|
151
|
+
WRAPPERS[klass] ||= Module.new do
|
|
152
|
+
include Guarded
|
|
153
|
+
|
|
154
|
+
(klass.public_instance_methods - Object.public_instance_methods).each do |name|
|
|
155
|
+
define_method(name) do |*args, **kwargs, &block|
|
|
156
|
+
result = CodecBoundary.within { super(*args, **kwargs, &CodecBoundary.outside_block(block)) }
|
|
157
|
+
name == :entries ? CodecBoundary.check_entries!(self, result) : result
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Wraps the caller's own block so it runs outside the boundary
|
|
165
|
+
# whenever the adapter yields to it.
|
|
166
|
+
#
|
|
167
|
+
# @param block [Proc, nil] the block the caller passed to the adapter method
|
|
168
|
+
# @return [Proc, nil] a proc forwarding its arguments to `block` under `outside`; nil
|
|
169
|
+
# when no block was given
|
|
170
|
+
def outside_block(block)
|
|
171
|
+
return nil unless block
|
|
172
|
+
|
|
173
|
+
proc { |*yielded, **yielded_kwargs, &inner| outside { block.call(*yielded, **yielded_kwargs, &inner) } }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -10,6 +10,10 @@ module Hecks
|
|
|
10
10
|
# `Runtime::StaleWrite` (runtime/errors.rb) and `AppendOnly#save`.
|
|
11
11
|
STATUSES = %i[inserted replaced updated conflicted missing saved stale].freeze
|
|
12
12
|
|
|
13
|
+
# @param status [Symbol, String] one of `STATUSES`; stored as a Symbol
|
|
14
|
+
# @param instance [Runtime::Instance, Object, nil] the record the write concerned, as
|
|
15
|
+
# the repository reports it; nil when there is none
|
|
16
|
+
# @raise [ArgumentError] if `status` is not one of `STATUSES`
|
|
13
17
|
def initialize(status:, instance: nil)
|
|
14
18
|
normalized = status.to_sym
|
|
15
19
|
raise ArgumentError, "unknown persistence outcome #{status.inspect}" unless STATUSES.include?(normalized)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Ports
|
|
3
3
|
module Persistence
|
|
4
|
-
#
|
|
4
|
+
# **The no-op saga store** — what `Registry#saga_persistence` hands
|
|
5
5
|
# back for a domain whose resolved adapter doesn't implement the
|
|
6
6
|
# (optional) saga-persistence capability: Memory, deliberately
|
|
7
7
|
# (sagas stay in-memory-only, exactly as they always have), and
|
|
@@ -14,8 +14,19 @@ module Hecks
|
|
|
14
14
|
# `Ports::Persistence::AppendOnly` already gives adapters that
|
|
15
15
|
# don't implement `reset!`/`events`/`record_event`.
|
|
16
16
|
class NullSagaStore
|
|
17
|
+
# Accepts and discards a saga checkpoint, whatever keywords it carries.
|
|
18
|
+
#
|
|
19
|
+
# @return [nil] always; nothing is stored
|
|
17
20
|
def save_saga(**) = nil
|
|
21
|
+
|
|
22
|
+
# Accepts and ignores a request to forget a saga.
|
|
23
|
+
#
|
|
24
|
+
# @return [nil] always; there is nothing to delete
|
|
18
25
|
def delete_saga(**) = nil
|
|
26
|
+
|
|
27
|
+
# Yields nothing, because no saga is ever stored here.
|
|
28
|
+
#
|
|
29
|
+
# @return [nil] always; a block, if given, is never called
|
|
19
30
|
def each_saga(*) = nil
|
|
20
31
|
end
|
|
21
32
|
|
|
@@ -8,17 +8,17 @@ module Hecks
|
|
|
8
8
|
# a plugin makes itself known as a side effect of being `require`d, the
|
|
9
9
|
# same way an adapter's own `.rb` file already registers itself
|
|
10
10
|
# (`Runtime::Registry#add_adapter`) rather than core listing adapter
|
|
11
|
-
# names anywhere. `register_plugin` is the
|
|
11
|
+
# names anywhere. `register_plugin` is the entire contract a plugin
|
|
12
12
|
# must satisfy: anything responding to `contribute_boot_gates(registry,
|
|
13
13
|
# gates)` (a no-op is a valid implementation).
|
|
14
14
|
#
|
|
15
|
-
# This is deliberately
|
|
15
|
+
# This is deliberately not the same registry `Hecks::Projector` uses
|
|
16
16
|
# (ADR 0027) or `Runtime::BootGates` (ADR 0031) — each of those is
|
|
17
17
|
# scoped to its own seam (IR-in/artifact-out; per-boot phased gates).
|
|
18
18
|
# A shared base is deferred until a third registry actually wants one.
|
|
19
19
|
#
|
|
20
|
-
#
|
|
21
|
-
# instance per `Loader.boot` call, because gate
|
|
20
|
+
# **Name-keyed, process-wide, not per-boot** — unlike `BootGates` (one
|
|
21
|
+
# instance per `Loader.boot` call, because gate registration is
|
|
22
22
|
# capability-conditional per registry), a persistence plugin is either
|
|
23
23
|
# `require`d into this process or it isn't; there is no "this boot's
|
|
24
24
|
# registry doesn't need it" case to isolate against, so a plain
|
|
@@ -27,18 +27,36 @@ module Hecks
|
|
|
27
27
|
@plugins = {}
|
|
28
28
|
|
|
29
29
|
class << self
|
|
30
|
+
# Adds a plugin under a name, replacing any plugin already registered under it.
|
|
31
|
+
#
|
|
32
|
+
# @param name [Symbol, String] the plugin's name, such as `:era`; stored as a Symbol
|
|
33
|
+
# @param plugin [Object] anything responding to
|
|
34
|
+
# `contribute_boot_gates(registry, gates)`
|
|
35
|
+
# @return [Object] the plugin just registered
|
|
30
36
|
def register(name, plugin)
|
|
31
37
|
@plugins[name.to_sym] = plugin
|
|
32
38
|
end
|
|
33
39
|
|
|
40
|
+
# Reports whether a plugin of that name has been required into this process.
|
|
41
|
+
#
|
|
42
|
+
# @param name [Symbol, String] the plugin's name
|
|
43
|
+
# @return [Boolean] true when a plugin is registered under `name`
|
|
34
44
|
def registered?(name)
|
|
35
45
|
@plugins.key?(name.to_sym)
|
|
36
46
|
end
|
|
37
47
|
|
|
48
|
+
# Yields every registered plugin, in registration order.
|
|
49
|
+
#
|
|
50
|
+
# @yieldparam plugin [Object] a registered plugin
|
|
51
|
+
# @return [Hash{Symbol => Object}, Enumerator] the plugin table when a block is
|
|
52
|
+
# given, otherwise an Enumerator over the plugins
|
|
38
53
|
def each(&)
|
|
39
54
|
@plugins.each_value(&)
|
|
40
55
|
end
|
|
41
56
|
|
|
57
|
+
# Reports whether any persistence plugin is loaded at all.
|
|
58
|
+
#
|
|
59
|
+
# @return [Boolean] true when at least one plugin is registered
|
|
42
60
|
def any? = !@plugins.empty?
|
|
43
61
|
end
|
|
44
62
|
end
|
|
@@ -48,9 +66,29 @@ module Hecks
|
|
|
48
66
|
# directly for a plugin's own file to call at require-time.
|
|
49
67
|
module_function
|
|
50
68
|
|
|
69
|
+
# Registers a persistence plugin; a plugin's own file calls this when it is required.
|
|
70
|
+
#
|
|
71
|
+
# @param name [Symbol, String] the plugin's name, such as `:era`
|
|
72
|
+
# @param plugin [Object] anything responding to `contribute_boot_gates(registry, gates)`
|
|
73
|
+
# @return [Object] the plugin just registered
|
|
51
74
|
def register_plugin(name, plugin) = Plugin.register(name, plugin)
|
|
75
|
+
|
|
76
|
+
# Reports whether the named persistence plugin is loaded in this process.
|
|
77
|
+
#
|
|
78
|
+
# @param name [Symbol, String] the plugin's name
|
|
79
|
+
# @return [Boolean] true when a plugin is registered under `name`
|
|
52
80
|
def plugin?(name) = Plugin.registered?(name)
|
|
81
|
+
|
|
82
|
+
# Yields every loaded persistence plugin, in registration order.
|
|
83
|
+
#
|
|
84
|
+
# @yieldparam plugin [Object] a registered plugin
|
|
85
|
+
# @return [Hash{Symbol => Object}, Enumerator] the plugin table when a block is given,
|
|
86
|
+
# otherwise an Enumerator over the plugins
|
|
53
87
|
def each_plugin(&) = Plugin.each(&)
|
|
88
|
+
|
|
89
|
+
# Reports whether any persistence plugin is loaded in this process.
|
|
90
|
+
#
|
|
91
|
+
# @return [Boolean] true when at least one plugin is registered
|
|
54
92
|
def plugins_loaded? = Plugin.any?
|
|
55
93
|
end
|
|
56
94
|
end
|
|
@@ -7,7 +7,7 @@ require_relative "../../../../runtime/registry"
|
|
|
7
7
|
|
|
8
8
|
module Hecks
|
|
9
9
|
module Runtime
|
|
10
|
-
# The boot-time era gate, run for the adapters that
|
|
10
|
+
# The boot-time era gate, run for the adapters that have eras — the
|
|
11
11
|
# lineage-capable ones. An era is a fact about stored data that some
|
|
12
12
|
# adapter can carry across a shape change; an adapter that cannot
|
|
13
13
|
# translate has no era to hold, and holding one for it would record
|
|
@@ -15,7 +15,7 @@ module Hecks
|
|
|
15
15
|
#
|
|
16
16
|
# The whole domain is handed to the capable adapter's own era check
|
|
17
17
|
# (Postgres: hold, recognize, mint, or refuse toward the scaffold),
|
|
18
|
-
# which keeps its era facts as rows
|
|
18
|
+
# which keeps its era facts as rows beside its data. That
|
|
19
19
|
# co-location is load-bearing — a watermark only means something
|
|
20
20
|
# against the journal it was cut from, and an approval recorded
|
|
21
21
|
# anywhere but the reviewed database binds to nothing.
|
|
@@ -31,50 +31,84 @@ module Hecks
|
|
|
31
31
|
module EraCheck
|
|
32
32
|
module_function
|
|
33
33
|
|
|
34
|
-
#
|
|
34
|
+
# Runs both halves of the era gate over every bluebook in a registry.
|
|
35
|
+
#
|
|
36
|
+
# Each bluebook's own source, not one file read once and reused
|
|
35
37
|
# for every bluebook in the registry — true as long as a domain
|
|
36
38
|
# directory only ever held exactly one, and silently wrong the
|
|
37
39
|
# moment `uses_framework` made a second, differently-sourced
|
|
38
40
|
# bluebook (Governance, Identity — `lib/hecks/framework/bluebook/`,
|
|
39
41
|
# not the domain's own directory) share a boot with the first.
|
|
40
42
|
# Caught the hard way: three domains booted together, one real
|
|
41
|
-
# source text (the domain's own), and every
|
|
42
|
-
# held
|
|
43
|
+
# source text (the domain's own), and every other domain's era-1
|
|
44
|
+
# held that text under its own name — a shadow-parse of it later
|
|
43
45
|
# reconstructs a completely different shape, and every boot after
|
|
44
46
|
# the first refuses toward a scaffold that was never the real
|
|
45
47
|
# drift.
|
|
48
|
+
#
|
|
49
|
+
# @param registry [Runtime::Registry] the registry being booted
|
|
50
|
+
# @param directory [String] path of the domain's own bluebook directory, searched for
|
|
51
|
+
# each bluebook's `.bluebook` source
|
|
52
|
+
# @return [void]
|
|
53
|
+
# @raise [Runtime::WiringError] if a compute rule is bound to an adapter that is not
|
|
54
|
+
# lineage-capable, a persistence binding cannot be resolved, a lineage-bound bluebook
|
|
55
|
+
# has no findable source, or the adapter's own `era_check!` refuses the boot
|
|
46
56
|
def check!(registry, directory)
|
|
47
57
|
check_compute_rules_for_registry!(registry)
|
|
48
58
|
check_lineage!(registry, directory)
|
|
49
59
|
end
|
|
50
60
|
|
|
61
|
+
# Refuses the boot when any bluebook's compute rule is bound away from Postgres.
|
|
62
|
+
#
|
|
51
63
|
# The domain-agnostic half, split out for ADR 0031's boot-gate
|
|
52
64
|
# registry: a compute rule requires Postgres whatever adapter is
|
|
53
|
-
# actually bound, so this must run for
|
|
65
|
+
# actually bound, so this must run for every registry, the same way
|
|
54
66
|
# `registry.verify!` does — it is not conditional on any adapter
|
|
55
67
|
# being lineage-capable, and must never be skipped by
|
|
56
68
|
# `check_lineage!`'s own capability gate below.
|
|
69
|
+
#
|
|
70
|
+
# @param registry [Runtime::Registry] the registry whose bluebooks are all checked
|
|
71
|
+
# @return [void]
|
|
72
|
+
# @raise [Runtime::WiringError] if an aggregate carrying a compute rule is bound to an
|
|
73
|
+
# adapter that is not lineage-capable, or its persistence binding cannot be resolved
|
|
57
74
|
def check_compute_rules_for_registry!(registry)
|
|
58
75
|
registry.bluebooks.each_value { |bluebook| check_compute_rules!(registry, bluebook) }
|
|
59
76
|
end
|
|
60
77
|
|
|
78
|
+
# Hands every bluebook, with its own source text, to its adapter's era check.
|
|
79
|
+
#
|
|
61
80
|
# The capability-gated half — ADR 0031's registered `:era_check`
|
|
62
81
|
# gate. Registration is conditional on `lineage_capable_registry?`;
|
|
63
82
|
# `check_bluebook!` below still carries its own per-bluebook
|
|
64
83
|
# `lineage_capable?` return-early, unchanged, for a registry with a
|
|
65
84
|
# mix of lineage-capable and plain-adapter bluebooks.
|
|
85
|
+
#
|
|
86
|
+
# @param registry [Runtime::Registry] the registry being booted
|
|
87
|
+
# @param directory [String] path of the domain's own bluebook directory
|
|
88
|
+
# @return [void]
|
|
89
|
+
# @raise [Runtime::WiringError] if a persistence binding cannot be resolved, a
|
|
90
|
+
# lineage-bound bluebook has no findable source, or the adapter's own `era_check!`
|
|
91
|
+
# refuses the boot
|
|
66
92
|
def check_lineage!(registry, directory)
|
|
67
93
|
registry.bluebooks.each_value do |bluebook|
|
|
68
|
-
check_bluebook!(registry, bluebook, source_text_for(bluebook, directory), directory: directory)
|
|
94
|
+
check_bluebook!(registry, bluebook, source_text_for(bluebook, directory, registry: registry), directory: directory)
|
|
69
95
|
end
|
|
70
96
|
end
|
|
71
97
|
|
|
98
|
+
# Decides whether a registry binds anything that carries eras.
|
|
99
|
+
#
|
|
72
100
|
# The `:era_check` gate's own registration predicate: true iff at
|
|
73
101
|
# least one bluebook's own anchor (first) aggregate resolves to a
|
|
74
102
|
# lineage-capable adapter — mirrors `check_bluebook!`'s existing
|
|
75
103
|
# per-bluebook anchor check, just asked once, up front, of the
|
|
76
104
|
# whole registry, so a registry with nothing lineage-capable bound
|
|
77
105
|
# anywhere never registers the gate at all.
|
|
106
|
+
#
|
|
107
|
+
# @param registry [Runtime::Registry] the registry whose bluebooks are asked
|
|
108
|
+
# @return [Boolean] true when some bluebook's first aggregate is bound to a
|
|
109
|
+
# lineage-capable adapter; false for an empty registry or one with no such binding
|
|
110
|
+
# @raise [Runtime::WiringError] if a first aggregate's persistence binding is missing,
|
|
111
|
+
# ambiguous, or carries an unsupported role
|
|
78
112
|
def lineage_capable_registry?(registry)
|
|
79
113
|
registry.bluebooks.each_value.any? do |bluebook|
|
|
80
114
|
first = bluebook.aggregates.first
|
|
@@ -84,38 +118,170 @@ module Hecks
|
|
|
84
118
|
end
|
|
85
119
|
end
|
|
86
120
|
|
|
121
|
+
# Reads the source text one bluebook was declared in, wherever that source lives.
|
|
122
|
+
#
|
|
87
123
|
# The domain's own directory first, matched by name — a real app's
|
|
88
124
|
# directory may hold more than one file once `uses_framework`
|
|
89
125
|
# exists, so ".first" alone can no longer be trusted, the exact
|
|
90
126
|
# way it silently wasn't the day this was found: three domains
|
|
91
127
|
# booted together, one real source text read once (the domain's
|
|
92
|
-
# own, ".first"'d), and every
|
|
128
|
+
# own, ".first"'d), and every other domain's era-1 held that text
|
|
93
129
|
# under its own name — a later shadow-parse of it reconstructs a
|
|
94
130
|
# completely different shape, and every boot after the first
|
|
95
131
|
# refuses toward a scaffold that was never the real drift.
|
|
96
132
|
#
|
|
97
|
-
# A single-file directory whose one file names something
|
|
133
|
+
# A single-file directory whose one file names something else
|
|
98
134
|
# falls back to it anyway (a fixture may legitimately name its
|
|
99
|
-
# file differently from the `Hecks.bluebook` it declares) —
|
|
100
|
-
# this bluebook is a known framework member
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
|
|
135
|
+
# file differently from the `Hecks.bluebook` it declares) — unless
|
|
136
|
+
# this bluebook is a known framework member or a vendored
|
|
137
|
+
# embryonaut bluebook (see `vendored_source_for` below), in which
|
|
138
|
+
# case that one file is certainly some other domain's, not this
|
|
139
|
+
# one's, and the real registry is asked instead — the only other
|
|
140
|
+
# two places a bluebook in this registry could have come from, per
|
|
141
|
+
# `uses_framework` and `uses_embryonaut_bluebook`.
|
|
142
|
+
#
|
|
143
|
+
# The bug this guards against, found live: `Framework.members` was
|
|
144
|
+
# the only exclusion checked here, so a domain attaching a
|
|
145
|
+
# vendored bluebook instead (`uses_embryonaut_bluebook`, which has
|
|
146
|
+
# no equivalent registry — see embryonaut_bluebook.rb's own
|
|
147
|
+
# header) fell straight through the single-file fallback: a
|
|
148
|
+
# directory holding exactly one `.bluebook` file (the target
|
|
149
|
+
# domain's own) handed that same text back for the vendored
|
|
150
|
+
# bluebook's era check too. PostgresEra minted era 1 for the
|
|
151
|
+
# vendored domain with the target's own source stamped as its
|
|
152
|
+
# `held_text` — the label computed from it, too — so the very
|
|
153
|
+
# next boot re-derived the vendored domain's real shape, found it
|
|
154
|
+
# didn't match what got (wrongly) stored, and refused to boot
|
|
155
|
+
# toward a scaffold for drift that never actually happened.
|
|
156
|
+
# `registry:` is what lets this check the one thing
|
|
157
|
+
# `Framework.members` cannot: whether some hecksagon in this
|
|
158
|
+
# registry declared `uses_embryonaut_bluebook` for this exact
|
|
159
|
+
# bluebook name, the same way `EmbryonautBluebook.load!` itself
|
|
160
|
+
# already resolves the vendored package's own directory.
|
|
161
|
+
#
|
|
162
|
+
# @param bluebook [Bluebook::Chapter] the bluebook whose source is wanted
|
|
163
|
+
# @param directory [String] path of the domain's own bluebook directory
|
|
164
|
+
# @param registry [Runtime::Registry, nil] the registry asked whether some hecksagon
|
|
165
|
+
# vendored this bluebook; nil skips the vendored lookup
|
|
166
|
+
# @return [String, nil] the UTF-8 text of every matching file joined with `"\n"`; nil
|
|
167
|
+
# when no source file could be found
|
|
168
|
+
def source_text_for(bluebook, directory, registry: nil)
|
|
106
169
|
domain_files = Dir[File.join(directory, "*.bluebook")]
|
|
107
|
-
own = domain_files.select
|
|
108
|
-
|
|
109
|
-
line.match?(/\A\s*Hecks\.bluebook\s+#{Regexp.escape(bluebook.name.inspect)}/)
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
own = domain_files if own.empty? && domain_files.size == 1 && !Framework.members.key?(bluebook.name)
|
|
113
|
-
own = [Framework.members[bluebook.name]].compact if own.empty?
|
|
170
|
+
own = domain_files.select { |path| declares_bluebook?(path, bluebook.name) }
|
|
171
|
+
own = fallback_source_files(bluebook, directory, domain_files, registry) if own.empty?
|
|
114
172
|
return if own.empty?
|
|
115
173
|
|
|
116
174
|
own.map { |path| File.read(path, encoding: "UTF-8") }.join("\n")
|
|
117
175
|
end
|
|
118
176
|
|
|
177
|
+
# Scans a file for a `Hecks.bluebook "<name>"` line opening the named bluebook.
|
|
178
|
+
#
|
|
179
|
+
# @param path [String] path of the `.bluebook` file to scan, read as UTF-8
|
|
180
|
+
# @param bluebook_name [String] the declared bluebook name, matched in its `inspect`
|
|
181
|
+
# (double-quoted) spelling
|
|
182
|
+
# @return [Boolean] true when some line, after leading whitespace, starts with
|
|
183
|
+
# `Hecks.bluebook` followed by that quoted name
|
|
184
|
+
def declares_bluebook?(path, bluebook_name)
|
|
185
|
+
File.foreach(path, encoding: "UTF-8").any? do |line|
|
|
186
|
+
line.match?(/\A\s*Hecks\.bluebook\s+#{Regexp.escape(bluebook_name.inspect)}/)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Picks the files to read for a bluebook no file in the domain directory declares.
|
|
191
|
+
#
|
|
192
|
+
# Only reached once nothing in the domain's own directory actually
|
|
193
|
+
# declares this bluebook by name. Three remaining sources, in
|
|
194
|
+
# order: a genuinely single-file domain directory whose one file
|
|
195
|
+
# just happens to name something else (but only when this
|
|
196
|
+
# bluebook isn't already known to come from somewhere else
|
|
197
|
+
# entirely — the exact guard the bug below was missing half of);
|
|
198
|
+
# a framework member; a vendored embryonaut bluebook.
|
|
199
|
+
#
|
|
200
|
+
# @param bluebook [Bluebook::Chapter] the bluebook whose source is wanted
|
|
201
|
+
# @param directory [String] path of the domain's own bluebook directory
|
|
202
|
+
# @param domain_files [Array<String>] paths of every `.bluebook` file in `directory`
|
|
203
|
+
# @param registry [Runtime::Registry, nil] the registry asked for a vendored name; nil
|
|
204
|
+
# skips the vendored lookup
|
|
205
|
+
# @return [Array<String>] file paths to read, in load order; `[]` when the bluebook
|
|
206
|
+
# comes from none of the three sources
|
|
207
|
+
def fallback_source_files(bluebook, directory, domain_files, registry)
|
|
208
|
+
vendored_name = registry && vendored_bluebook_name_for(registry, bluebook.name)
|
|
209
|
+
framework_path = Framework.members[bluebook.name]
|
|
210
|
+
|
|
211
|
+
if domain_files.size == 1 && !framework_path && !vendored_name
|
|
212
|
+
domain_files
|
|
213
|
+
elsif framework_path
|
|
214
|
+
[framework_path]
|
|
215
|
+
elsif vendored_name
|
|
216
|
+
vendored_source_for(directory, vendored_name)
|
|
217
|
+
else
|
|
218
|
+
[]
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Recovers the vendored package name behind a bluebook, if some hecksagon vendored it.
|
|
223
|
+
#
|
|
224
|
+
# The name `uses_embryonaut_bluebook` was actually called with —
|
|
225
|
+
# recovered from whichever hecksagon in this registry recorded it
|
|
226
|
+
# (`HecksagonBuilder#uses_embryonaut_bluebook`'s own
|
|
227
|
+
# `@vendored_bluebooks`), matched the same way
|
|
228
|
+
# `EmbryonautBluebook.load!` itself decides idempotency: the
|
|
229
|
+
# vendored name Pascal-cases to this bluebook's own declared name.
|
|
230
|
+
# Nil for an ordinary bluebook nothing ever vendored.
|
|
231
|
+
#
|
|
232
|
+
# @param registry [Runtime::Registry] the registry whose hecksagons are searched
|
|
233
|
+
# @param bluebook_name [String] the bluebook's declared Pascal-case name
|
|
234
|
+
# @return [String, nil] the package name as written in `uses_embryonaut_bluebook`; nil
|
|
235
|
+
# when no hecksagon vendored a package whose name Pascal-cases to `bluebook_name`
|
|
236
|
+
def vendored_bluebook_name_for(registry, bluebook_name)
|
|
237
|
+
registry.hecksagons.each_value do |hecksagon|
|
|
238
|
+
match = hecksagon.vendored_bluebooks.find { |name| Naming.pascal(name) == bluebook_name }
|
|
239
|
+
return match if match
|
|
240
|
+
end
|
|
241
|
+
nil
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Lists the `.bluebook` files a vendored embryonaut package ships.
|
|
245
|
+
#
|
|
246
|
+
# The same path `EmbryonautBluebook.load!` itself resolves from —
|
|
247
|
+
# `<registry.root>/vendor/embryonaut_bluebooks/<name>/bluebook/`,
|
|
248
|
+
# rebuilt here from `directory` (the domain's own bluebook
|
|
249
|
+
# directory, always `registry.root`'s immediate child — see
|
|
250
|
+
# `Runtime::Loader.boot`) rather than threading `registry.root`
|
|
251
|
+
# through as a second parameter. Every `.bluebook` file the
|
|
252
|
+
# package ships, in the same `Dir.glob` order actually loaded at
|
|
253
|
+
# boot time — order matters here because this text is later
|
|
254
|
+
# re-parsed whole (`EraCheck::shadow`) to reconstruct the shape a
|
|
255
|
+
# held era claims, and that reconstruction must see the files in
|
|
256
|
+
# the order that actually built the live shape.
|
|
257
|
+
#
|
|
258
|
+
# @param directory [String] path of the domain's own bluebook directory, whose parent
|
|
259
|
+
# holds `vendor/embryonaut_bluebooks`
|
|
260
|
+
# @param name [String] the vendored package name, as `vendored_bluebook_name_for`
|
|
261
|
+
# returns it
|
|
262
|
+
# @return [Array<String>] paths of the package's `.bluebook` files in `Dir.glob` order;
|
|
263
|
+
# `[]` when the package directory is missing or empty
|
|
264
|
+
def vendored_source_for(directory, name)
|
|
265
|
+
dir = File.join(File.dirname(directory), "vendor", "embryonaut_bluebooks", name, "bluebook")
|
|
266
|
+
Dir.glob(File.join(dir, "*.bluebook"))
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Hands one bluebook to its adapter's `era_check!`, when that adapter carries eras.
|
|
270
|
+
#
|
|
271
|
+
# The bluebook's first aggregate is the anchor: its binding alone decides
|
|
272
|
+
# which adapter is asked, and a bluebook with no aggregates, or one bound
|
|
273
|
+
# to an adapter that is not lineage-capable, is skipped without a word.
|
|
274
|
+
#
|
|
275
|
+
# @param registry [Runtime::Registry] the registry being booted
|
|
276
|
+
# @param bluebook [Bluebook::Chapter] the bluebook to check
|
|
277
|
+
# @param current_text [String, nil] the bluebook's source, as `source_text_for` found
|
|
278
|
+
# it; nil means no source file was found
|
|
279
|
+
# @param directory [String, nil] the domain's bluebook directory, named in the
|
|
280
|
+
# missing-source refusal and passed through to the adapter
|
|
281
|
+
# @return [void]
|
|
282
|
+
# @raise [Runtime::WiringError] if the first aggregate's binding cannot be resolved,
|
|
283
|
+
# `current_text` is nil for a lineage-bound bluebook, or the adapter's `era_check!`
|
|
284
|
+
# refuses the boot
|
|
119
285
|
def check_bluebook!(registry, bluebook, current_text, directory: nil)
|
|
120
286
|
first = bluebook.aggregates.first
|
|
121
287
|
return unless first
|
|
@@ -136,10 +302,19 @@ module Hecks
|
|
|
136
302
|
)
|
|
137
303
|
end
|
|
138
304
|
|
|
139
|
-
#
|
|
305
|
+
# Refuses the boot when one bluebook's compute rule is bound away from Postgres.
|
|
306
|
+
#
|
|
307
|
+
# The per-rule capability gate — not an era fact, and so it
|
|
140
308
|
# survives on every adapter: a compute rule's SQL is its only
|
|
141
309
|
# implementation, so an aggregate carrying one cannot boot
|
|
142
310
|
# anywhere but Postgres, whatever any shape comparison would say.
|
|
311
|
+
#
|
|
312
|
+
# @param registry [Runtime::Registry] the registry holding the declared translations
|
|
313
|
+
# and the aggregate's bindings
|
|
314
|
+
# @param bluebook [Bluebook::Chapter] the bluebook whose aggregates are checked
|
|
315
|
+
# @return [void]
|
|
316
|
+
# @raise [Runtime::WiringError] if an aggregate carrying a compute rule is bound to an
|
|
317
|
+
# adapter that is not lineage-capable, or its persistence binding cannot be resolved
|
|
143
318
|
def check_compute_rules!(registry, bluebook)
|
|
144
319
|
bluebook.aggregates.each do |aggregate|
|
|
145
320
|
lineage = Ports::Persistence::Lineage.for(registry, bluebook.name, aggregate)
|
|
@@ -152,14 +327,32 @@ module Hecks
|
|
|
152
327
|
end
|
|
153
328
|
end
|
|
154
329
|
|
|
330
|
+
# Names the adapter an aggregate's authoritative persistence bind points at.
|
|
331
|
+
#
|
|
332
|
+
# @param registry [Runtime::Registry] the registry holding the domain's hecksagon
|
|
333
|
+
# @param domain [String, Symbol] name of the domain the aggregate belongs to
|
|
334
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose binding is resolved
|
|
335
|
+
# @return [String] the bound adapter's name, such as `"PostgresEra"`; `"Memory"` when
|
|
336
|
+
# the domain declares no hecksagon
|
|
337
|
+
# @raise [Runtime::WiringError] if the aggregate has no persistence bind, more than one
|
|
338
|
+
# authoritative bind, or a bind with a role the port does not support
|
|
155
339
|
def adapter_for(registry, domain, aggregate)
|
|
156
340
|
Ports::Persistence::BindingPolicy.resolve(registry, domain, aggregate).adapter
|
|
157
341
|
end
|
|
158
342
|
|
|
159
|
-
#
|
|
343
|
+
# Asks a named adapter whether it carries eras.
|
|
344
|
+
#
|
|
345
|
+
# The capability idiom: an adapter class that answers
|
|
160
346
|
# lineage_capable? with true carries eras and may act on drift
|
|
161
347
|
# (translate, fork, merge). Postgres alone does today; the seam is
|
|
162
348
|
# what lets a second one arrive without touching this file.
|
|
349
|
+
#
|
|
350
|
+
# @param registry [Runtime::Registry] the registry whose `adapters` must list the name
|
|
351
|
+
# @param adapter_name [String] the adapter's name, as `adapter_for` returns it
|
|
352
|
+
# @return [Boolean] the adapter's own `lineage_capable?` answer; false when the registry
|
|
353
|
+
# does not list the adapter, the adapter does not respond to `lineage_capable?`, or
|
|
354
|
+
# any `StandardError` is raised on the way (including an adapter with no Ruby
|
|
355
|
+
# implementation)
|
|
163
356
|
def lineage_capable?(registry, adapter_name)
|
|
164
357
|
adapter_class = registry.adapters[adapter_name] && registry.adapter_class(adapter_name)
|
|
165
358
|
adapter_class.respond_to?(:lineage_capable?) && adapter_class.lineage_capable?
|