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,299 @@
|
|
|
1
|
+
require_relative "interpreting"
|
|
2
|
+
require_relative "../naming"
|
|
3
|
+
require_relative "../rendering"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "identity"
|
|
6
|
+
require_relative "instance"
|
|
7
|
+
require_relative "value"
|
|
8
|
+
require_relative "refusal_wording"
|
|
9
|
+
require_relative "routing"
|
|
10
|
+
require_relative "dependency_planning"
|
|
11
|
+
require_relative "../ports/persistence/execution"
|
|
12
|
+
require_relative "entity_element"
|
|
13
|
+
require_relative "command_interpreter/argument_gate"
|
|
14
|
+
|
|
15
|
+
module Hecks
|
|
16
|
+
module Runtime
|
|
17
|
+
class EntityInterpreter
|
|
18
|
+
include Interpreting
|
|
19
|
+
# THE SAME PAYLOAD GATE aggregate commands and port operations already
|
|
20
|
+
# run — bug audit H1 (docs/audits/2026-08-10-main-bug-audit.md): this
|
|
21
|
+
# class used to run NEITHER refuse_unknown_arguments NOR
|
|
22
|
+
# refuse_absent_arguments, on a comment claiming "an entity inherits
|
|
23
|
+
# its aggregate's own gate." Nothing on the entity dispatch path ever
|
|
24
|
+
# ran one — confirmed live, `LedgerEntry.Reverse` accepted an
|
|
25
|
+
# unrecognized `bogus_arg:` outright, and dispatching it with no
|
|
26
|
+
# `narrative:` silently overwrote the stored narrative with `nil`
|
|
27
|
+
# (`sets :narrative`'s bare self-referential form reads `args[:narrative]`
|
|
28
|
+
# unconditionally). See `step_refuse_unknown_arguments`/
|
|
29
|
+
# `step_refuse_absent_arguments`, below, for how the shared gate is
|
|
30
|
+
# reused rather than reimplemented.
|
|
31
|
+
include CommandInterpreter::ArgumentGate
|
|
32
|
+
|
|
33
|
+
attr_reader :registry
|
|
34
|
+
|
|
35
|
+
# THE DECLARED ORDER, HAND-TYPED — mirrors Vocabulary::EntityDispatchOrder
|
|
36
|
+
# (language/bluebook/vocabulary.bluebook:217-232), held equal to it by
|
|
37
|
+
# spec/vocabulary_conformance_spec.rb the same way CommandInterpreter's
|
|
38
|
+
# own DISPATCH_ORDER is; see that constant's doc comment for why this is
|
|
39
|
+
# hand-typed rather than read live off the meta-domain at every dispatch.
|
|
40
|
+
# `refuse_unknown_arguments`/`refuse_absent_arguments` now lead it, same
|
|
41
|
+
# position `AggregateDispatchOrder` holds them at (H1, above) — the only
|
|
42
|
+
# remaining difference from the aggregate order is no
|
|
43
|
+
# `assign_creation_attributes` (an entity is never created through this
|
|
44
|
+
# path).
|
|
45
|
+
DISPATCH_ORDER = Hecks::Vocabulary.symbols("EntityDispatchOrder")
|
|
46
|
+
|
|
47
|
+
# Same safety valve as `CommandInterpreter::MAX_STALE_WRITE_RETRIES` —
|
|
48
|
+
# see that constant's own comment.
|
|
49
|
+
MAX_STALE_WRITE_RETRIES = 5
|
|
50
|
+
|
|
51
|
+
# `instance` is the PARENT aggregate record (what gets saved and
|
|
52
|
+
# returned) ; `element`/`view` are the entity piece itself — `view`
|
|
53
|
+
# wraps `element` as it stood at `locate_element`, pre-mutation, and
|
|
54
|
+
# `enforce_ensures` builds its own settled wrapper off `element` as it
|
|
55
|
+
# stands after, the same split the original sequential code made.
|
|
56
|
+
#
|
|
57
|
+
# `chain` — S17, ADR 0026 — every entity the dotted verb passes
|
|
58
|
+
# through, root-first (`[Handler, Dispatch]` for `Handler.Dispatch.
|
|
59
|
+
# Bind`) ; `entity`/`entity_name` stay the CHAIN'S OWN LAST entry,
|
|
60
|
+
# the one a command actually belongs to and a mutation actually
|
|
61
|
+
# targets, so every step written before this ADR (enforce_givens,
|
|
62
|
+
# apply_mutations, advance_lifecycle, element_identity, ...) reads
|
|
63
|
+
# exactly as it always has. Only `locate_element` walks the chain.
|
|
64
|
+
Context = Struct.new(:domain, :aggregate, :entity, :entity_name, :command, :command_name,
|
|
65
|
+
:args, :repository, :instance, :chain, :element, :view, :transition,
|
|
66
|
+
:old_element, :result, :route, :plan, :persistence_outcome, :dry_run)
|
|
67
|
+
|
|
68
|
+
def initialize(registry, rules:)
|
|
69
|
+
@registry = registry
|
|
70
|
+
@rules = rules
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# `dry_run:` — CommandInterpreter#call's own twin, see that method's
|
|
74
|
+
# comment for the shared reasoning (Dispatcher#dry_run?'s own entry
|
|
75
|
+
# point). `step_save`/`step_emit` are the only two steps here that
|
|
76
|
+
# read it either.
|
|
77
|
+
# RETRIES THE WHOLE METHOD BODY on `StaleWrite` — same reasoning as
|
|
78
|
+
# `CommandInterpreter#call`'s own retry: a fresh `ctx`, a fresh
|
|
79
|
+
# `step_hydrate_parent`/`step_locate_element` re-reading current
|
|
80
|
+
# state.
|
|
81
|
+
def call(domain, aggregate, dotted, legacy_args, route: nil, with: nil, dry_run: false)
|
|
82
|
+
*entity_names, command_name = dotted.to_s.split(".")
|
|
83
|
+
if entity_names.empty?
|
|
84
|
+
raise UnknownVerb, RefusalWording.render("UnknownVerb", "entity_unknown",
|
|
85
|
+
aggregate: aggregate.hecks_name, entity: dotted.to_s.inspect)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
chain = walk_entity_chain(aggregate, entity_names)
|
|
89
|
+
entity = chain.last
|
|
90
|
+
command = entity.command(command_name) ||
|
|
91
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_no_command",
|
|
92
|
+
entity: entity.hecks_name, command: command_name.inspect))
|
|
93
|
+
|
|
94
|
+
args = Routing.payload(command, with: with, legacy: legacy_args)
|
|
95
|
+
attempt = 0
|
|
96
|
+
begin
|
|
97
|
+
ctx = Context.new(domain, aggregate, entity, entity_names.join("."), command, command_name, args)
|
|
98
|
+
ctx.chain = chain
|
|
99
|
+
ctx.route = route
|
|
100
|
+
ctx.dry_run = dry_run
|
|
101
|
+
ctx.plan = DependencyPlanning::Analyzer.call(aggregate: entity, command: command)
|
|
102
|
+
# RESOLVED HERE, ONCE — see CommandInterpreter#call's own comment;
|
|
103
|
+
# `step_hydrate_parent` reads `ctx.repository` without re-fetching.
|
|
104
|
+
ctx.repository = @registry.repository(domain, aggregate)
|
|
105
|
+
lock_id = Identity.best_effort(aggregate, args, route)
|
|
106
|
+
run_dispatch_order_with_isolation(DISPATCH_ORDER, ctx, lock_key_id: lock_id)
|
|
107
|
+
[ctx.instance, ctx.result, ctx.plan, ctx.persistence_outcome]
|
|
108
|
+
rescue StaleWrite
|
|
109
|
+
attempt += 1
|
|
110
|
+
retry if attempt < MAX_STALE_WRITE_RETRIES
|
|
111
|
+
raise
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# ONE HOP PER DOTTED SEGMENT — `ProcessManager.Handler.Dispatch.Bind`
|
|
118
|
+
# (once the dispatcher has already stripped "Domain::Aggregate.")
|
|
119
|
+
# walks Handler off the aggregate, then Dispatch off Handler, each
|
|
120
|
+
# step reading `.entities` exactly the way the single-level case
|
|
121
|
+
# always did — a nested entity is "structurally interchangeable
|
|
122
|
+
# with an aggregate" (Entity's own header) for precisely this
|
|
123
|
+
# reason. Two levels is what Handler/Dispatch need today ; nothing
|
|
124
|
+
# here assumes it stops at two.
|
|
125
|
+
def walk_entity_chain(aggregate, entity_names)
|
|
126
|
+
owner = aggregate
|
|
127
|
+
entity_names.map do |name|
|
|
128
|
+
found = owner.entities.find { |piece| piece.hecks_name == name } ||
|
|
129
|
+
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_unknown",
|
|
130
|
+
aggregate: owner.hecks_name, entity: name.inspect))
|
|
131
|
+
owner = found
|
|
132
|
+
found
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# `extra_identity_heads:` — every entity `ctx.chain` walks through, not
|
|
137
|
+
# just the root aggregate `ArgumentGate` already knows about. A
|
|
138
|
+
# two-hop dispatch (`Handler.Dispatch.Bind`) is addressed by BOTH
|
|
139
|
+
# hops' own identity, each read straight out of `args` by
|
|
140
|
+
# `EntityElement#element_of` — refusing those as unknown would refuse
|
|
141
|
+
# every legitimate nested-entity dispatch there is, the same reasoning
|
|
142
|
+
# `ArgumentGate#refuse_unknown_arguments`'s own header gives for `:id`/
|
|
143
|
+
# the root's `identity_heads`.
|
|
144
|
+
def step_refuse_unknown_arguments(ctx)
|
|
145
|
+
step(:refuse_unknown_arguments) {
|
|
146
|
+
refuse_unknown_arguments(ctx.domain, ctx.aggregate, ctx.command, ctx.args,
|
|
147
|
+
extra_identity_heads: ctx.chain.flat_map(&:identity_heads))
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# No `aggregate:` exemption to pass — that kwarg exists only for a
|
|
152
|
+
# port operation's own self-address (`ArgumentGate#refuse_absent_
|
|
153
|
+
# arguments`'s own comment); an entity command's chain identity never
|
|
154
|
+
# reaches `command.attributes` in the first place (resolved as
|
|
155
|
+
# addressing above, not as a declared fact), so there is nothing here
|
|
156
|
+
# for the exemption to need to strip.
|
|
157
|
+
def step_refuse_absent_arguments(ctx)
|
|
158
|
+
step(:refuse_absent_arguments) { refuse_absent_arguments(ctx.command, ctx.args) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def step_normalize_args(ctx)
|
|
162
|
+
ctx.args = step(:normalize_args) { normalize_args(ctx.aggregate, ctx.command, ctx.args) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def step_refuse_role_mismatch(ctx)
|
|
166
|
+
step(:refuse_role_mismatch) { @rules.refuse_role_mismatch(ctx.command, ctx.domain) }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def step_resolve_references(ctx)
|
|
170
|
+
step(:resolve_references) { @rules.resolve_references(ctx.domain, ctx.command, ctx.args) }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def step_hydrate_parent(ctx)
|
|
174
|
+
# `ctx.repository` is resolved once, in `#call`, before the
|
|
175
|
+
# isolation decision — not here any more.
|
|
176
|
+
ctx.instance = step(:hydrate_parent) {
|
|
177
|
+
parent(ctx.repository, ctx.aggregate, ctx.entity_name, ctx.command_name, ctx.args, ctx.route)
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def step_locate_element(ctx)
|
|
182
|
+
ctx.element = step(:locate_element) {
|
|
183
|
+
EntityElement.locate_chain(ctx.aggregate, ctx.chain, ctx.instance, ctx.args, ctx.command_name, ctx.route)
|
|
184
|
+
}
|
|
185
|
+
# `view` was hydrated ONCE, here, into its OWN state hash
|
|
186
|
+
# (Value.hydrate builds a fresh Hash — never aliased with `element`)
|
|
187
|
+
# — exactly right for enforce_givens, which must read pre-mutation.
|
|
188
|
+
ctx.view = Instance.new(aggregate: ctx.entity, id: EntityElement.element_identity(ctx.entity, ctx.element).to_s,
|
|
189
|
+
state: ctx.element)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def step_enforce_givens(ctx)
|
|
193
|
+
step(:enforce_givens) {
|
|
194
|
+
@rules.enforce_givens(ctx.view, ctx.command, ctx.args, domain: ctx.domain, declaring: ctx.entity, parent: ctx.instance)
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def step_admissible_transition(ctx)
|
|
199
|
+
ctx.transition = step(:admissible_transition) { @rules.admissible_transition(ctx.entity, ctx.command, ctx.view) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def step_apply_mutations(ctx)
|
|
203
|
+
ctx.old_element = ctx.element.dup unless ctx.command.ensures.empty?
|
|
204
|
+
step(:apply_mutations) {
|
|
205
|
+
ctx.command.mutations.each { |mutation|
|
|
206
|
+
EntityElement.apply_to_element(@rules, ctx.aggregate, ctx.entity, ctx.element, mutation, ctx.args)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def step_advance_lifecycle(ctx)
|
|
212
|
+
return unless ctx.transition
|
|
213
|
+
|
|
214
|
+
step(:advance_lifecycle) { ctx.element[ctx.entity.lifecycle.field] = ctx.transition.target }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# An ensures reads the SETTLED record, so it needs a view hydrated from
|
|
218
|
+
# `element` as it stands now, mutations included — unlike `view` above,
|
|
219
|
+
# built once and read pre-mutation by enforce_givens.
|
|
220
|
+
def step_enforce_ensures(ctx)
|
|
221
|
+
step(:enforce_ensures) do
|
|
222
|
+
settled = Instance.new(aggregate: ctx.entity, id: ctx.view.id, state: ctx.element)
|
|
223
|
+
@rules.enforce_ensures(settled, ctx.command, ctx.args, old: ctx.old_element, domain: ctx.domain, parent: ctx.instance)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# THE PARENT AGGREGATE's own invariants — `ctx.instance` is the
|
|
228
|
+
# parent record an entity mutation writes into (this file's own
|
|
229
|
+
# `Context` comment), the SAME boundary an aggregate-level
|
|
230
|
+
# invariant guards regardless of which interpreter changed it. No
|
|
231
|
+
# separate "entity invariant" exists (S10, ADR 0025 scopes
|
|
232
|
+
# `invariant` to the aggregate only) — see `Admissibility#
|
|
233
|
+
# enforce_invariants`'s own comment.
|
|
234
|
+
def step_enforce_invariants(ctx)
|
|
235
|
+
step(:enforce_invariants) { @rules.enforce_invariants(ctx.instance, ctx.aggregate, domain: ctx.domain) }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# `dry_run:` skips this — see CommandInterpreter#step_save's own
|
|
239
|
+
# comment, same reasoning and the same precedent
|
|
240
|
+
# (`step_assign_creation_attributes`'s own conditional-skip).
|
|
241
|
+
def step_save(ctx)
|
|
242
|
+
return if ctx.dry_run
|
|
243
|
+
|
|
244
|
+
step(:save) do
|
|
245
|
+
@rules.resolve_state_references(ctx.domain, ctx.aggregate, ctx.instance.state)
|
|
246
|
+
# `expected_version:` — see CommandInterpreter#step_save's own
|
|
247
|
+
# comment: nil for a repository that isn't CAS-capable, or an
|
|
248
|
+
# instance never read from storage, either of which falls
|
|
249
|
+
# through to a plain save inside `AppendOnly#save`.
|
|
250
|
+
ctx.persistence_outcome = ctx.repository.save(ctx.instance, expected_version: ctx.instance.version)
|
|
251
|
+
if ctx.persistence_outcome.status == :stale
|
|
252
|
+
# NOT a `RefusalWording.render` call — see
|
|
253
|
+
# `CommandInterpreter#step_save`'s identical branch and
|
|
254
|
+
# `Runtime::StaleWrite`'s own comment.
|
|
255
|
+
raise(StaleWrite,
|
|
256
|
+
"#{ctx.command.hecks_name} on #{ctx.aggregate.hecks_name} " \
|
|
257
|
+
"(#{Identity.reading(ctx.aggregate)}: #{Rendering.describe(ctx.instance.id)}) lost a race — " \
|
|
258
|
+
"another write committed against this record after it was read")
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# `dry_run:` skips this too — nothing was committed, so `ctx.result`
|
|
264
|
+
# stays nil and `Dispatcher#dry_run?` never reads it.
|
|
265
|
+
def step_emit(ctx)
|
|
266
|
+
return if ctx.dry_run
|
|
267
|
+
|
|
268
|
+
ctx.result = step(:emit) { @rules.emit(ctx.command, ctx.domain, ctx.aggregate, ctx.instance, ctx.args, ctx.repository) }
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# THE PARENT AGGREGATE, addressed exactly as `CommandInterpreter#hydrate`
|
|
272
|
+
# addresses one acting on itself — derive from the declared identity first
|
|
273
|
+
# (`Identity.of`), and let a bare `id:` name an already-derived record when
|
|
274
|
+
# the identity itself is not what the caller is holding.
|
|
275
|
+
def parent(repository, aggregate, entity_name, command_name, args, route = nil)
|
|
276
|
+
parent_id = route&.aggregate ||
|
|
277
|
+
Identity.of(aggregate, args) ||
|
|
278
|
+
Identity.from(aggregate, args, :id) ||
|
|
279
|
+
raise(NotFound, RefusalWording.render("NotFound", "entity_parent_no_identity",
|
|
280
|
+
command: command_name, aggregate: aggregate.hecks_name,
|
|
281
|
+
entity: entity_name, identity: Identity.reading(aggregate)))
|
|
282
|
+
found = repository.find(parent_id) ||
|
|
283
|
+
raise(NotFound, RefusalWording.render("NotFound", "record_missing",
|
|
284
|
+
aggregate: aggregate.hecks_name,
|
|
285
|
+
identity: Identity.reading(aggregate),
|
|
286
|
+
offered: Rendering.describe(parent_id)))
|
|
287
|
+
found.dup
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# `locate_chain`/`element_of`/`element_identity`/`apply_to_element` and
|
|
291
|
+
# their own helpers used to live here — moved to `Runtime::EntityElement`
|
|
292
|
+
# (see that file's own header) so `CommandInterpreter`'s own
|
|
293
|
+
# `delegate_to_entity` step can locate and mutate the same element the
|
|
294
|
+
# same way, against an aggregate record already held in memory. `call`,
|
|
295
|
+
# above, and every `step_*` method reach them through that module now;
|
|
296
|
+
# nothing about the STEPS themselves changed.
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
require_relative "value/invariant_violation"
|
|
2
|
+
require_relative "../vocabulary"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Runtime
|
|
6
|
+
class UnknownVerb < StandardError; end
|
|
7
|
+
class EnsuresNotMet < StandardError; end
|
|
8
|
+
class GivenNotMet < StandardError; end
|
|
9
|
+
class NotFound < StandardError; end
|
|
10
|
+
class LifecycleRefused < StandardError; end
|
|
11
|
+
class TypeMismatch < StandardError; end
|
|
12
|
+
# An argument the command does not declare. Sibling of TypeMismatch : that one
|
|
13
|
+
# is the right name carrying the wrong thing, this one is a name the command
|
|
14
|
+
# never had. Both are the payload gate refusing before any rule runs.
|
|
15
|
+
class UnknownArgument < StandardError; end
|
|
16
|
+
# The third of the trio, and the one that was missing : a name the command DOES
|
|
17
|
+
# declare, absent. TypeMismatch is the right name carrying the wrong thing,
|
|
18
|
+
# UnknownArgument a name that was never declared, AbsentArgument a declared name
|
|
19
|
+
# that never arrived. Between them they say a command takes exactly the
|
|
20
|
+
# arguments it declares — no others, and all of them.
|
|
21
|
+
class AbsentArgument < StandardError; end
|
|
22
|
+
# A creating command whose derived identity already names a record. The
|
|
23
|
+
# record it would have overwritten stands ; the dispatch that tried to
|
|
24
|
+
# mint a second one over it is what refuses.
|
|
25
|
+
class AlreadyExists < StandardError; end
|
|
26
|
+
# A declared, non-optional attribute a record predates — added since
|
|
27
|
+
# it was written, with no default: to fill it and no translation
|
|
28
|
+
# declaring what an old record should read there. `GuardState`
|
|
29
|
+
# (command_rules/admissibility.rb) is the one place this is raised: a
|
|
30
|
+
# `given`/`ensures`/`invariant` that reads the field would otherwise
|
|
31
|
+
# evaluate against a value nobody wrote, which is the same silent-
|
|
32
|
+
# wrong-answer class as an unpopulated projection reading "not
|
|
33
|
+
# active" (ADR 0025, "Added attributes and absence"). An OPTIONAL
|
|
34
|
+
# attribute in the same spot reads nil instead — that is what
|
|
35
|
+
# optional means, and this refusal is deliberately narrower than the
|
|
36
|
+
# nil-read it sits beside, not a replacement for it.
|
|
37
|
+
class AttributeAbsent < StandardError; end
|
|
38
|
+
# THE SAME SILENT-WRONG-ANSWER CLASS AS ABOVE, one line up — a
|
|
39
|
+
# `projects` field (S12, ADR 0025) this record predates, or that no
|
|
40
|
+
# rebuild sweep has populated yet, read by a `given`/`ensures`/
|
|
41
|
+
# `invariant` as though it carried a real value. `GuardState` is
|
|
42
|
+
# the one place this is raised, the same way AttributeAbsent is —
|
|
43
|
+
# a DECLARED field the record does not yet carry, distinguished
|
|
44
|
+
# from that one only in WHY: an ordinary attribute is absent
|
|
45
|
+
# because nobody backfilled it, a projected field is absent
|
|
46
|
+
# because nobody has swept it yet.
|
|
47
|
+
class ProjectionAbsent < StandardError; end
|
|
48
|
+
# A query or read model declares `authorize policy, tenant: :field` and
|
|
49
|
+
# the caller did not pass that field — the one half of `authorize` this
|
|
50
|
+
# runtime can enforce without a caller-identity system: the boundary
|
|
51
|
+
# itself, not whether the caller actually holds `policy`. See
|
|
52
|
+
# Runtime::TenantScope.
|
|
53
|
+
class Unauthorized < StandardError; end
|
|
54
|
+
# `corrects` names a past event this record must have already emitted
|
|
55
|
+
# (CommandBuilder#corrects_impl's own comment) — a fact the expression
|
|
56
|
+
# evaluator cannot check (it is not a predicate over the record's OWN
|
|
57
|
+
# fields, it is "did this exact record ever announce this"), so it is
|
|
58
|
+
# raised structurally, the same way AlreadyExists/NotFound are, rather
|
|
59
|
+
# than being expressible as an ordinary `given`. Raised by
|
|
60
|
+
# `CommandRules::Admissibility#enforce_correction_target`.
|
|
61
|
+
class NothingToCorrect < StandardError; end
|
|
62
|
+
|
|
63
|
+
# A RUNTIME FAULT, NOT A DOMAIN REFUSAL — deliberately absent from
|
|
64
|
+
# `DOMAIN_REFUSALS` below and from `vocabulary.bluebook`'s own
|
|
65
|
+
# `DomainRefusal` list. Raised when an optimistic-concurrency CAS write
|
|
66
|
+
# (`AppendOnly#save`'s `expected_version:`) finds the stored version has
|
|
67
|
+
# moved since this instance was read — someone else's write committed
|
|
68
|
+
# in between. `CommandInterpreter#call`/`EntityInterpreter#call` catch
|
|
69
|
+
# this themselves and retry the whole dispatch from a fresh hydrate, so
|
|
70
|
+
# `enforce_givens` re-evaluates against the now-current state; a caller
|
|
71
|
+
# only ever sees this escape when every retry is exhausted under
|
|
72
|
+
# sustained contention — an operational condition (many concurrent
|
|
73
|
+
# writers hammering one aggregate), not a business rule a domain author
|
|
74
|
+
# declared. See docs/decisions/ (concurrency-control ADR).
|
|
75
|
+
class StaleWrite < StandardError; end
|
|
76
|
+
|
|
77
|
+
# A Lambda-routed domain's own refusal (rust/host, `Runtime::
|
|
78
|
+
# RemoteDispatcher`), carrying Rust's own refusal text verbatim —
|
|
79
|
+
# NOT yet mapped back to the specific matching class above
|
|
80
|
+
# (GivenNotMet vs. EnsuresNotMet vs. ...), a real, known,
|
|
81
|
+
# documented gap: the WASM projector's own event/refusal-wording
|
|
82
|
+
# parity work (ADR 0021) makes the TEXT match Ruby's, but nothing
|
|
83
|
+
# yet parses that text back into a typed Ruby exception the way a
|
|
84
|
+
# local dispatch already raises one directly. Callers that only
|
|
85
|
+
# need "the domain said no" (not which specific rule) are
|
|
86
|
+
# unaffected; callers pattern-matching a SPECIFIC refusal class
|
|
87
|
+
# against a Lambda-routed domain are the ones this gap would bite.
|
|
88
|
+
class RemoteRefusal < StandardError; end
|
|
89
|
+
|
|
90
|
+
# The domain saying NO — the errors a reaction may legitimately meet and
|
|
91
|
+
# record as an undelivered outcome. A policy whose target refuses is a fact
|
|
92
|
+
# about the domain ; the originating command still stands.
|
|
93
|
+
#
|
|
94
|
+
# Everything ELSE is a defect : a NoMethodError in an interpreter, a
|
|
95
|
+
# NameError from a missing constant, a TypeError from a bad assumption. A
|
|
96
|
+
# blanket `rescue StandardError` used to fold both into one line —
|
|
97
|
+
# `delivered: false, reason: "..."` — so a crash in the runtime was
|
|
98
|
+
# indistinguishable from a rule doing its job, and read as normal operation
|
|
99
|
+
# in the log.
|
|
100
|
+
#
|
|
101
|
+
# UnknownVerb IS one of these, and deliberately : a cross-domain policy
|
|
102
|
+
# (`across "Notifications"`) fires in deployments where that domain is not
|
|
103
|
+
# loaded, and recording the undelivered reaction rather than raising is the
|
|
104
|
+
# design — spec/policy_spec states it in so many words, "records a reaction
|
|
105
|
+
# it cannot deliver rather than swallowing it".
|
|
106
|
+
# InvariantViolation belongs here and was missing. A value object refusing
|
|
107
|
+
# its own rule is the domain saying no as plainly as a given is — but the
|
|
108
|
+
# class is declared over in value.rb and never made the list, so the policy
|
|
109
|
+
# and saga interpreters, which rescue exactly these, would let it propagate
|
|
110
|
+
# as though the RUNTIME had broken. A reaction whose target violates an
|
|
111
|
+
# invariant is declined, not crashed. Found by spec/domain_refusal_spec on
|
|
112
|
+
# its first run : every corpus refusal must be a class named here, and 23
|
|
113
|
+
# of banking's were InvariantViolation.
|
|
114
|
+
# THE NAMES COME FROM THE LANGUAGE, the classes from this module.
|
|
115
|
+
# `DomainRefusal` declares WHICH refusals are the domain's own —
|
|
116
|
+
# a rule the caller broke — as against a runtime fault. Resolving
|
|
117
|
+
# each name here means a refusal declared but never defined fails
|
|
118
|
+
# at load with a NameError, rather than being quietly absent from
|
|
119
|
+
# a list nothing re-checks.
|
|
120
|
+
DOMAIN_REFUSALS = Hecks::Vocabulary.fetch("DomainRefusal").map { |name| const_get(name) }.freeze
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Runtime
|
|
5
|
+
# `correlation` is NOT on the wire — `to_h` below deliberately omits it,
|
|
6
|
+
# the same as `bin/run`'s own event projection does. It is runtime
|
|
7
|
+
# bookkeeping stamped by `Dispatcher#dispatch` when a saga leg's own
|
|
8
|
+
# dispatch causes this event (see `SagaInterpreter#deliver_saga_dispatch`
|
|
9
|
+
# and `#saga_correlation`) : a Hash of `correlation_head` -> correlation
|
|
10
|
+
# value, so an event caused by one saga cannot be misread by an unrelated
|
|
11
|
+
# one correlating on a different field. Absent for any event no saga
|
|
12
|
+
# dispatch caused, which is most of them.
|
|
13
|
+
Event = Struct.new(:name, :aggregate, :id, :payload, :occurred_at, :correlation, keyword_init: true) do
|
|
14
|
+
# AN EMITTED EVENT IS A RECORD OF SOMETHING THAT HAPPENED, and a
|
|
15
|
+
# mutable audit trail is not one. The PAYLOAD — the domain fact the
|
|
16
|
+
# event carries — is frozen THROUGH on emission: freezing the Hash
|
|
17
|
+
# alone would leave every value in it editable in place, which is
|
|
18
|
+
# the shape all four previous freezing bugs had.
|
|
19
|
+
#
|
|
20
|
+
# THE WHOLE EVENT, not just its payload. Correlation used to be
|
|
21
|
+
# merged onto already-emitted events by `Dispatcher#dispatch`, which
|
|
22
|
+
# is what kept an event writable after it had happened; it is set at
|
|
23
|
+
# construction now, because it is part of the transaction and known
|
|
24
|
+
# from `dispatch`'s own argument before anything is emitted.
|
|
25
|
+
#
|
|
26
|
+
# The LOG stays appendable: new events are still recorded. It is
|
|
27
|
+
# each event that stops changing once it exists.
|
|
28
|
+
def emit!
|
|
29
|
+
Freezer.deep(payload)
|
|
30
|
+
Freezer.deep(correlation)
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
{
|
|
36
|
+
name: name,
|
|
37
|
+
aggregate: aggregate,
|
|
38
|
+
id: id,
|
|
39
|
+
payload: payload,
|
|
40
|
+
occurred_at: occurred_at
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_s
|
|
45
|
+
"#{name}(#{aggregate}##{id}) #{payload.inspect}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def inspect = "#<Event #{self}>"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
require_relative "../naming"
|
|
2
|
+
require_relative "value"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Runtime
|
|
6
|
+
# THE SCALAR AN IDENTITY PATH NAMES.
|
|
7
|
+
#
|
|
8
|
+
# An identity is DECLARED as a path — `identified_by :number` — and
|
|
9
|
+
# this is the one place that reads one. It follows the path and nothing else.
|
|
10
|
+
#
|
|
11
|
+
# What it replaced was `Value.identifier`, which opened a one-field value
|
|
12
|
+
# object and took whatever was inside : that let `identified_by :number` pass
|
|
13
|
+
# for an identity, with the runtime guessing which field had been meant. The
|
|
14
|
+
# guess is gone. A declaration that names no field is now refused when the
|
|
15
|
+
# bluebook loads (`an aggregate that is identified names a field`, `an entity
|
|
16
|
+
# is known by a field`), so by the time anything is dispatched there is
|
|
17
|
+
# always a path here to follow.
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
#
|
|
21
|
+
# Identity.scalar("number.value", account_number_value_object) # => "acct-1"
|
|
22
|
+
#
|
|
23
|
+
module Identity
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# A hash read that decides which spelling of a key answers by
|
|
27
|
+
# PRESENCE, never by `||` — a bare `||` treats a genuinely-held
|
|
28
|
+
# `false` the same as an absent key and falls through to the other
|
|
29
|
+
# spelling, landing on `nil` instead of the real, stored answer.
|
|
30
|
+
def hash_lookup(hash, key)
|
|
31
|
+
sym = key.to_sym
|
|
32
|
+
hash.key?(sym) ? hash[sym] : hash[key]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The head names the ATTRIBUTE and is consumed by whoever looked the value
|
|
36
|
+
# up; what is left is the walk down into it. A path with no fields to walk
|
|
37
|
+
# — an aggregate that declares no identity and falls back to `id` — hands
|
|
38
|
+
# back what it was given, because there is nothing declared to dig for.
|
|
39
|
+
def scalar(path, held)
|
|
40
|
+
_head, *fields = path.to_s.split(".")
|
|
41
|
+
return held if fields.empty?
|
|
42
|
+
|
|
43
|
+
fields.reduce(Value.materialize(held)) do |dug, field|
|
|
44
|
+
dug.is_a?(Hash) ? hash_lookup(dug, field) : nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# THE IDENTITY IS THE JOIN OF ITS PARTS, in declaration order. Shared by
|
|
49
|
+
# `CommandInterpreter` (an aggregate acting on itself) and
|
|
50
|
+
# `EntityInterpreter` (a piece addressed through its aggregate) — a piece
|
|
51
|
+
# declares an identity the same shape a head does, so it derives one the
|
|
52
|
+
# same way. `construct` answers `identity_paths` / `identity_heads` /
|
|
53
|
+
# `attribute` (an Aggregate or an Entity, either one) ; `value_owner`
|
|
54
|
+
# answers for coercion (`Value.for_attribute`'s first argument), which for
|
|
55
|
+
# an entity is its OWNING aggregate — an entity's value objects resolve
|
|
56
|
+
# through the aggregate's namespace, not its own.
|
|
57
|
+
#
|
|
58
|
+
# A part the payload does not carry makes the WHOLE identity unresolvable,
|
|
59
|
+
# rather than half of one. Half an identity names nothing, and joining what
|
|
60
|
+
# did arrive would silently name a different record on every dispatch — the
|
|
61
|
+
# precise failure that minting an id caused, arrived at by another road.
|
|
62
|
+
def of(construct, args, value_owner: construct)
|
|
63
|
+
paths = construct.identity_paths
|
|
64
|
+
return nil if paths.empty?
|
|
65
|
+
|
|
66
|
+
parts = paths.map { |path| from(construct, args, path, value_owner: value_owner) }
|
|
67
|
+
# A BLANK PART NAMES NOTHING, the same as an ABSENT one — AN ID IS A
|
|
68
|
+
# SCALAR, and "" is not a fact about anything. This used to check only
|
|
69
|
+
# `nil?`, so a canonical text extracted as "" (an expression whose
|
|
70
|
+
# source did not survive extraction) resolved to a REAL, empty-string
|
|
71
|
+
# identity — a record addressable by an id no caller could have meant.
|
|
72
|
+
return nil if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
|
|
73
|
+
|
|
74
|
+
Naming.identity(parts)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A path digs into the value object that carries the identity, so what is
|
|
78
|
+
# stored is the SCALAR inside it rather than the object serialised whole.
|
|
79
|
+
def from(construct, args, key, value_owner: construct)
|
|
80
|
+
return nil unless key
|
|
81
|
+
|
|
82
|
+
head, *rest = key.to_s.split(".")
|
|
83
|
+
head = head.to_sym
|
|
84
|
+
return nil unless args.key?(head)
|
|
85
|
+
|
|
86
|
+
unless rest.empty?
|
|
87
|
+
held = args[head]
|
|
88
|
+
held = held.to_h if held.respond_to?(:to_h)
|
|
89
|
+
# AN ID IS ALWAYS A SCALAR. The path says WHICH FIELD carries it, so a
|
|
90
|
+
# caller may hand that field's value straight over — a string or a
|
|
91
|
+
# number, never a serialised object. Only a value object that actually
|
|
92
|
+
# arrived whole has to be opened.
|
|
93
|
+
return held.to_s unless held.is_a?(Hash)
|
|
94
|
+
|
|
95
|
+
return rest.reduce(held) { |h, f| h.is_a?(Hash) ? hash_lookup(h, f) : nil }&.to_s
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Coerced against the identity ATTRIBUTE only when the caller actually
|
|
99
|
+
# named it. A saga addresses an aggregate by its correlation key, and
|
|
100
|
+
# that key carries the id ALREADY RESOLVED — coercing "w1" against a
|
|
101
|
+
# WireReference asked the caller to pass fields for a value object they
|
|
102
|
+
# never mentioned.
|
|
103
|
+
attribute = construct.identity_heads.include?(head) ? construct.attribute(head) : nil
|
|
104
|
+
raw = args[head]
|
|
105
|
+
return raw unless attribute
|
|
106
|
+
|
|
107
|
+
# AN ID IS ALWAYS A SCALAR — same contract the dotted branch above
|
|
108
|
+
# already keeps, just reached a different way here: a BARE
|
|
109
|
+
# (undotted) identity path names one of THIS construct's own
|
|
110
|
+
# declared attributes directly, and when that attribute's type is
|
|
111
|
+
# a value object (Translation's own compound `identified_by
|
|
112
|
+
# :domain, :from, :to`, each typed `TranslationDomainName`/
|
|
113
|
+
# `TranslationEraName`), `Value.for_attribute` coerces it into a
|
|
114
|
+
# real single-field Value wrapper — never unwrapped before this,
|
|
115
|
+
# so `Naming.identity`'s own plain `Array#join` (`Naming.identity`'s
|
|
116
|
+
# own header: parts must already be scalars) fell through to
|
|
117
|
+
# Ruby's default `Object#to_s`, leaking a raw, run-to-run-random
|
|
118
|
+
# memory address (`#<Hecks::Runtime::Value:0x...>`) into
|
|
119
|
+
# every refusal quoting this identity — found live via bin/fuzz on
|
|
120
|
+
# the self-hosted "translation" domain (replay_is_deterministic:
|
|
121
|
+
# the SAME address never repeats, so two replays of the
|
|
122
|
+
# identical steps produced different histories the moment a
|
|
123
|
+
# Translation went missing). `materialize_unwrapped` is the
|
|
124
|
+
# SAME single-field-VO-recurses-to-its-bare-scalar helper
|
|
125
|
+
# `read_model_interpreter.rb` already uses for exactly this
|
|
126
|
+
# unwrap; passthrough for anything that isn't a Value at all.
|
|
127
|
+
Value.materialize_unwrapped(Value.for_attribute(value_owner, attribute, raw)).to_s
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# How an identity READS when the runtime has to name it in a refusal — the
|
|
131
|
+
# paths as they were declared, so the message quotes the bluebook back.
|
|
132
|
+
def reading(construct)
|
|
133
|
+
construct.identity_paths.join(", ")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# BEST-EFFORT, FOR A LOCK KEY ONLY — `Runtime::AggregateLock`'s own
|
|
137
|
+
# per-record striping needs SOME id to key on before dispatch has run
|
|
138
|
+
# far enough to hydrate for real, so this walks the identical chain
|
|
139
|
+
# `CommandInterpreter#hydrate_existing`/`#hydrate_prior_or_initial`
|
|
140
|
+
# and `EntityInterpreter#parent` already use to locate the real
|
|
141
|
+
# record — but wrapped to never raise. Choosing which Mutex to hold
|
|
142
|
+
# must never itself become a crash. `nil` means "could not resolve
|
|
143
|
+
# from the raw, pre-normalized payload this runs against" — the
|
|
144
|
+
# caller locks by aggregate type alone in that case (coarser, still
|
|
145
|
+
# correct, just less concurrent).
|
|
146
|
+
def best_effort(construct, args, route = nil, reference_key: nil)
|
|
147
|
+
route&.aggregate ||
|
|
148
|
+
of(construct, args) ||
|
|
149
|
+
from(construct, args, :id) ||
|
|
150
|
+
(reference_key && from(construct, args, reference_key))
|
|
151
|
+
rescue StandardError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|