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,65 @@
|
|
|
1
|
+
require_relative "../caller"
|
|
2
|
+
require_relative "../errors"
|
|
3
|
+
require_relative "../refusal_wording"
|
|
4
|
+
require_relative "../../ports/authorization"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Runtime
|
|
8
|
+
class CommandRules
|
|
9
|
+
# Whether the caller may run this command at all — a `role` mismatch,
|
|
10
|
+
# the one check that runs before any domain-state work, alongside the
|
|
11
|
+
# argument gate rather than after it.
|
|
12
|
+
#
|
|
13
|
+
# TWO CHECKS, NOT A REPLACEMENT — ADR 0025 §9's own caution against
|
|
14
|
+
# "silently downgrading `role` to documentation" cuts both ways: a
|
|
15
|
+
# caller who never named WHO they are (every caller before this) is
|
|
16
|
+
# checked exactly the way it always has been, string equality
|
|
17
|
+
# against the command's own `role`. Only a caller that ALSO binds an
|
|
18
|
+
# `actor_id` (`Hecks.as_caller(role:, actor_id:)`) reaches the
|
|
19
|
+
# real check — a live Governance::RoleAssignment lookup through
|
|
20
|
+
# `Ports::Authorization`, once the command's domain declares
|
|
21
|
+
# `uses_framework "Governance"` (`HecksagonBuilder` refuses any
|
|
22
|
+
# domain that declares a `role` and does not, at build time — see
|
|
23
|
+
# its own `refuse_ungoverned_roles!`). An identified caller is never
|
|
24
|
+
# let back through the string fallback: a real identity that holds
|
|
25
|
+
# no matching grant is refused, not waved through because it also
|
|
26
|
+
# happens to type the right word.
|
|
27
|
+
module Authorization
|
|
28
|
+
# OPT-IN, on BOTH sides. No caller bound: unchecked, exactly as
|
|
29
|
+
# today. No role declared: unchecked too — `role` is genuinely
|
|
30
|
+
# optional in this language (roughly a third of banking's own
|
|
31
|
+
# commands declare none), so a command that never named a role has
|
|
32
|
+
# nothing to check a caller against.
|
|
33
|
+
def refuse_role_mismatch(command, domain)
|
|
34
|
+
caller = Caller.current
|
|
35
|
+
return unless caller
|
|
36
|
+
return if command.role.to_s.empty?
|
|
37
|
+
|
|
38
|
+
authorized =
|
|
39
|
+
if caller.actor_id && governance_attached?(domain)
|
|
40
|
+
Ports::Authorization.holds_role?(registry, actor_id: caller.actor_id, role: command.role,
|
|
41
|
+
as_of: caller.as_of, scope: caller.scope)
|
|
42
|
+
else
|
|
43
|
+
caller.role == command.role
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
return if authorized
|
|
47
|
+
|
|
48
|
+
raise Unauthorized, RefusalWording.render("Unauthorized", "role_mismatch",
|
|
49
|
+
command: command.hecks_name, role: command.role,
|
|
50
|
+
caller_role: caller.role)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# SELF-EXEMPT, like `HecksagonBuilder#refuse_ungoverned_roles!` —
|
|
56
|
+
# Governance IS the thing a `holds_role?` lookup runs against, so
|
|
57
|
+
# its own commands are always checked by the string fallback, not
|
|
58
|
+
# a lookup against itself.
|
|
59
|
+
def governance_attached?(domain)
|
|
60
|
+
domain.to_s == "Governance" || registry.hecksagon(domain)&.framework_members&.include?("Governance") || false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require_relative "../event"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Runtime
|
|
5
|
+
class CommandRules
|
|
6
|
+
# What a command's `emits` becomes: an Event in the registry's log,
|
|
7
|
+
# and — where the store can hold one — a recorded event beside the
|
|
8
|
+
# data it describes.
|
|
9
|
+
module Emission
|
|
10
|
+
# `correlation` arrives HERE rather than being merged onto the
|
|
11
|
+
# event afterwards. It is part of the transaction — known from
|
|
12
|
+
# `dispatch`'s own argument before anything is emitted — and an
|
|
13
|
+
# event that is still being written to is not yet a record of
|
|
14
|
+
# what happened. Setting it at construction is what lets the
|
|
15
|
+
# event be frozen the moment it exists.
|
|
16
|
+
def emit(command, domain, aggregate, instance, args, repository, correlation = nil)
|
|
17
|
+
command.emits.map do |event_name|
|
|
18
|
+
event = Event.new(
|
|
19
|
+
name: event_name,
|
|
20
|
+
aggregate: "#{domain}::#{aggregate.hecks_name}",
|
|
21
|
+
id: instance.id,
|
|
22
|
+
payload: args,
|
|
23
|
+
occurred_at: Time.now.utc.iso8601,
|
|
24
|
+
correlation: correlation
|
|
25
|
+
)
|
|
26
|
+
@registry.event_log << event.emit!
|
|
27
|
+
repository.record_event(event) if repository.respond_to?(:record_event)
|
|
28
|
+
event
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
require_relative "../errors"
|
|
2
|
+
require_relative "../refusal_wording"
|
|
3
|
+
require_relative "../value"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Runtime
|
|
7
|
+
class CommandRules
|
|
8
|
+
# A reference must point at something that EXISTS.
|
|
9
|
+
#
|
|
10
|
+
# `reference_to Customer` is the one guarantee an aggregate reference is
|
|
11
|
+
# for, and it was declared 14 times across banking and enforced nowhere :
|
|
12
|
+
# an Account could belong to a customer who was never registered, and
|
|
13
|
+
# every gate stayed green because
|
|
14
|
+
# no corpus step ever passed a dangling reference.
|
|
15
|
+
module References
|
|
16
|
+
# Resolved here rather than in coercion because coercion is pure — it
|
|
17
|
+
# holds no repository. A reference INTO ANOTHER DOMAIN is left alone : a
|
|
18
|
+
# cross-domain target may legitimately not be loaded, which is the same
|
|
19
|
+
# reading `across` policies already get.
|
|
20
|
+
#
|
|
21
|
+
# Shared by CommandInterpreter and EntityInterpreter — an entity command
|
|
22
|
+
# can declare a reference-typed attribute the same way an aggregate
|
|
23
|
+
# command can, even though nothing in the real corpus does yet.
|
|
24
|
+
def resolve_references(domain, command, args)
|
|
25
|
+
command.attributes.each do |attribute|
|
|
26
|
+
next unless attribute.reference?
|
|
27
|
+
next unless args.key?(attribute.name)
|
|
28
|
+
|
|
29
|
+
held = args[attribute.name]
|
|
30
|
+
next if held.nil?
|
|
31
|
+
|
|
32
|
+
target = referenced_aggregate(attribute)
|
|
33
|
+
next unless target
|
|
34
|
+
|
|
35
|
+
validate_reference_values(domain, target, held, list: attribute.list?)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Structural references are checked again against the settled state.
|
|
40
|
+
# This is what makes a `has_many` declared on an aggregate honest even
|
|
41
|
+
# when a command supplies its list through an ordinary typed argument.
|
|
42
|
+
def resolve_state_references(domain, construct, state)
|
|
43
|
+
construct.attributes.each do |attribute|
|
|
44
|
+
next unless attribute.reference?
|
|
45
|
+
|
|
46
|
+
held = state[attribute.name]
|
|
47
|
+
validate_relationship_cardinality(construct, attribute, held)
|
|
48
|
+
next if held.nil?
|
|
49
|
+
|
|
50
|
+
target = referenced_aggregate(attribute)
|
|
51
|
+
next unless target
|
|
52
|
+
|
|
53
|
+
validate_reference_values(domain, target, held, list: attribute.list?)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
Array(construct.entities).each do |entity|
|
|
57
|
+
field = construct.attribute(Naming.snake(entity.hecks_name).to_sym) ||
|
|
58
|
+
construct.attributes.find { |attribute| attribute.type.to_s == entity.hecks_name.to_s }
|
|
59
|
+
next unless field
|
|
60
|
+
|
|
61
|
+
Array(state[field.name]).each { |row| resolve_state_references(domain, entity, row) }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# `has_one` and `belongs_to` mean exactly one target unless the
|
|
66
|
+
# declaration explicitly says optional. State validation owns this
|
|
67
|
+
# boundary because a command may leave an aggregate field untouched;
|
|
68
|
+
# checking only command arguments would let a required relationship be
|
|
69
|
+
# persisted as nil. `has_many` admits zero members, so its empty list is
|
|
70
|
+
# already a valid cardinality and needs no presence refusal.
|
|
71
|
+
def validate_relationship_cardinality(construct, attribute, held)
|
|
72
|
+
return if attribute.relationship.nil? || attribute.list?
|
|
73
|
+
return unless held.nil? && !attribute.optional?
|
|
74
|
+
|
|
75
|
+
raise TypeMismatch,
|
|
76
|
+
"#{construct.hecks_name}.#{attribute.name} is a required " \
|
|
77
|
+
"#{attribute.relationship} relationship — expected one " \
|
|
78
|
+
"#{attribute.type.target_name} identity, got nil"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The reference RESOLVES itself — through the chapter's own IR, so the
|
|
82
|
+
# bluebook's declared heads are the index. This used to regex the target's
|
|
83
|
+
# name out of "Reference<Customer>" and then search
|
|
84
|
+
# `registry.bluebook(domain).aggregates` for it — and later reached the
|
|
85
|
+
# target through Ruby's constant tree, a class thrown away for its `.ir`
|
|
86
|
+
# the moment it was found.
|
|
87
|
+
def referenced_aggregate(attribute)
|
|
88
|
+
attribute.type.resolve
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def validate_reference_values(domain, target, held, list:)
|
|
92
|
+
values = list ? Array(held) : [held]
|
|
93
|
+
values.each do |value|
|
|
94
|
+
key = reference_key(value)
|
|
95
|
+
next if key.empty?
|
|
96
|
+
next if @registry.repository(domain, target).find(key)
|
|
97
|
+
|
|
98
|
+
raise NotFound,
|
|
99
|
+
RefusalWording.render("NotFound", "reference_target_missing",
|
|
100
|
+
target: target.name, heads: target.identity_heads.join(", "),
|
|
101
|
+
key: key.inspect)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# `value` is the referenced record's own id, EXACTLY as `Identity.of`
|
|
106
|
+
# would build it for that record — a bare scalar for a single-field
|
|
107
|
+
# identity (the overwhelming common case; Banking's own plain
|
|
108
|
+
# `reference_to Customer` holds one already, so `Value.
|
|
109
|
+
# materialize_unwrapped` is a no-op passthrough here), or a
|
|
110
|
+
# `Naming.identity`-joined string for a compound one
|
|
111
|
+
# (`belongs_to Translation, as: :translation_ref` — Translation's
|
|
112
|
+
# own `identified_by :domain, :from, :to`, THREE fields). Before
|
|
113
|
+
# this, plain `value.to_s` on that compound case's own coerced
|
|
114
|
+
# Value hit Ruby's default `Object#to_s` (a raw, run-to-run-random
|
|
115
|
+
# memory address) instead of joining the record's real id — found
|
|
116
|
+
# live via bin/fuzz on the self-hosted "translation" domain
|
|
117
|
+
# (replay_is_deterministic), the SAME class of gap `Identity.from`
|
|
118
|
+
# already had for a compound `identified_by`'s own bare (undotted)
|
|
119
|
+
# attribute paths. `materialize_unwrapped` recurses a multi-
|
|
120
|
+
# attribute value object to a plain Hash keyed by attribute name,
|
|
121
|
+
# in declaration order — `Naming.identity` on `.values` reproduces
|
|
122
|
+
# the identical join `Identity.of` itself would produce for the
|
|
123
|
+
# SAME fields.
|
|
124
|
+
def reference_key(value)
|
|
125
|
+
unwrapped = Value.materialize_unwrapped(value)
|
|
126
|
+
return Naming.identity(unwrapped.values).to_s if unwrapped.is_a?(Hash)
|
|
127
|
+
|
|
128
|
+
unwrapped.to_s
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# A COMMAND ARGUMENT's own related record, reachable by name from
|
|
132
|
+
# `given`/`ensures` — `disputed_by.status`, say, `CardPayment
|
|
133
|
+
# .Dispute`'s own fresh `Reference<Customer>` argument — without
|
|
134
|
+
# teaching the pure expression evaluator anything about
|
|
135
|
+
# repositories. The lookup happens HERE, once, before evaluation;
|
|
136
|
+
# `Resolver#lookup` just digs into a plain Hash exactly as it
|
|
137
|
+
# always has.
|
|
138
|
+
#
|
|
139
|
+
# `owner` NARROWED TO `command` ONLY (S12, ADR 0025 — "rules
|
|
140
|
+
# confined to their own aggregate boundary"): dereferencing the
|
|
141
|
+
# DECLARING aggregate/entity's own STORED `reference_to` used to
|
|
142
|
+
# be the other half of this method's job — a live query against
|
|
143
|
+
# another aggregate's own repository, every time a `given`/
|
|
144
|
+
# `ensures`/`invariant` ran. That half is gone; a cross-aggregate
|
|
145
|
+
# fact a rule needs now has to be a `projects`-maintained LOCAL
|
|
146
|
+
# field (`AggregateBuilder#projects_impl`'s own comment), already
|
|
147
|
+
# present in `subject`'s own state, no hydration needed. A
|
|
148
|
+
# reference-typed COMMAND ARGUMENT stays in bounds, though — the
|
|
149
|
+
# ADR's own boundary list names "its command arguments" as
|
|
150
|
+
# readable, and nothing is stored yet for a fresh argument to
|
|
151
|
+
# project from; resolving it once here, synchronous with THIS
|
|
152
|
+
# command's own admission, is a different shape from a live query
|
|
153
|
+
# against an ALREADY-PERSISTED reference. `enforce_givens`/
|
|
154
|
+
# `enforce_ensures` are this method's only two remaining callers,
|
|
155
|
+
# both passing `command`/`args`, never a `subject`'s own
|
|
156
|
+
# aggregate — verified before this comment was written, not
|
|
157
|
+
# assumed.
|
|
158
|
+
#
|
|
159
|
+
# RECURSES into what it finds, so a chain deeper than one hop
|
|
160
|
+
# still resolves in one pass. Depth-bounded rather than cycle-
|
|
161
|
+
# detected — nothing in this corpus dots more than two hops on a
|
|
162
|
+
# fresh argument, and a bound is simpler than tracking visited
|
|
163
|
+
# (type, id) pairs for a cycle nothing here declares.
|
|
164
|
+
DEREFERENCE_DEPTH = 4
|
|
165
|
+
private_constant :DEREFERENCE_DEPTH
|
|
166
|
+
|
|
167
|
+
def dereference(domain, owner, source, depth: DEREFERENCE_DEPTH)
|
|
168
|
+
return {} if depth <= 0 || owner.nil?
|
|
169
|
+
|
|
170
|
+
owner.attributes.each_with_object({}) do |attribute, hydrated|
|
|
171
|
+
next unless attribute.reference?
|
|
172
|
+
|
|
173
|
+
id = source[attribute.name]
|
|
174
|
+
next if id.nil?
|
|
175
|
+
|
|
176
|
+
target = referenced_aggregate(attribute)
|
|
177
|
+
next unless target
|
|
178
|
+
|
|
179
|
+
record = @registry.repository(domain, target).find(id.to_s)
|
|
180
|
+
next unless record
|
|
181
|
+
|
|
182
|
+
name = attribute.name.to_s.sub(/_id\z/, "").to_sym
|
|
183
|
+
hydrated[name] = record.state.merge(dereference(domain, target, record.state, depth: depth - 1))
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require_relative "command_rules/admissibility"
|
|
2
|
+
require_relative "command_rules/references"
|
|
3
|
+
require_relative "command_rules/arithmetic"
|
|
4
|
+
require_relative "command_rules/emission"
|
|
5
|
+
require_relative "command_rules/authorization"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Runtime
|
|
9
|
+
# The rules both interpreters consult, one concern per file beside
|
|
10
|
+
# this one: whether a command may run (admissibility), whether its
|
|
11
|
+
# references point at anything (references), what its arithmetic
|
|
12
|
+
# mutations mean (arithmetic), what its emits become (emission), and
|
|
13
|
+
# whether the caller may run it at all (authorization).
|
|
14
|
+
class CommandRules
|
|
15
|
+
include Admissibility
|
|
16
|
+
include References
|
|
17
|
+
include Arithmetic
|
|
18
|
+
include Emission
|
|
19
|
+
include Authorization
|
|
20
|
+
|
|
21
|
+
attr_reader :registry
|
|
22
|
+
|
|
23
|
+
def initialize(registry)
|
|
24
|
+
@registry = registry
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
require_relative "../bluebook/expression"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Runtime
|
|
5
|
+
# Persistence-neutral facts derived from a command's existing semantic IR.
|
|
6
|
+
# This module deliberately does not execute a plan or choose a repository.
|
|
7
|
+
module DependencyPlanning
|
|
8
|
+
ATOMIC_PUT = :atomic_put
|
|
9
|
+
TRANSACTIONAL_FALLBACK = :load_apply_validate_store
|
|
10
|
+
|
|
11
|
+
Plan = Struct.new(
|
|
12
|
+
:read_set,
|
|
13
|
+
:write_set,
|
|
14
|
+
:payload_read_set,
|
|
15
|
+
:complete_state,
|
|
16
|
+
:state_independent,
|
|
17
|
+
:unresolved_dependencies,
|
|
18
|
+
keyword_init: true
|
|
19
|
+
) do
|
|
20
|
+
def complete_state? = complete_state
|
|
21
|
+
def state_independent? = state_independent
|
|
22
|
+
|
|
23
|
+
# Capability negotiation is correctness-first: an optimization is
|
|
24
|
+
# selected only when both the semantic proof and adapter capability
|
|
25
|
+
# are present. This is planning data only; no runtime path calls it yet.
|
|
26
|
+
def strategy_for(capabilities: [])
|
|
27
|
+
return TRANSACTIONAL_FALLBACK unless complete_state? && state_independent?
|
|
28
|
+
return TRANSACTIONAL_FALLBACK unless capabilities.map(&:to_sym).include?(ATOMIC_PUT)
|
|
29
|
+
|
|
30
|
+
ATOMIC_PUT
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module ExpressionReads
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
# Read the same parsed canonical-expression nodes the evaluator uses.
|
|
38
|
+
# A generic Struct walk keeps this additive when the expression grammar
|
|
39
|
+
# gains a composed node; only Lookup nodes carry domain dependencies.
|
|
40
|
+
def paths(canonical)
|
|
41
|
+
collect(Bluebook::Expression::Evaluator.parse(canonical), Set.new)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def collect(node, bound_names)
|
|
45
|
+
case node
|
|
46
|
+
when Bluebook::Expression::Resolver::Lookup
|
|
47
|
+
root = node.path.to_s.split(".", 2).first
|
|
48
|
+
bound_names.include?(root) ? [] : [node.path.to_s]
|
|
49
|
+
when Bluebook::Expression::Resolver::BlockPredicate
|
|
50
|
+
collect(node.receiver, bound_names) +
|
|
51
|
+
collect(node.predicate, bound_names | [node.param.to_s])
|
|
52
|
+
when Struct
|
|
53
|
+
node.each_pair.flat_map { |_name, value| collect(value, bound_names) }
|
|
54
|
+
when Array
|
|
55
|
+
node.flat_map { |value| collect(value, bound_names) }
|
|
56
|
+
else
|
|
57
|
+
[]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class Analyzer
|
|
63
|
+
STATEFUL_MUTATIONS = %i[append increment decrement multiply clamp remove].freeze
|
|
64
|
+
|
|
65
|
+
def self.call(aggregate:, command:) = new(aggregate, command).call
|
|
66
|
+
|
|
67
|
+
def initialize(aggregate, command)
|
|
68
|
+
@aggregate = aggregate
|
|
69
|
+
@command = command
|
|
70
|
+
@owner_fields = aggregate.attributes.to_set(&:name)
|
|
71
|
+
@owner_fields << aggregate.lifecycle.field.to_sym if aggregate.lifecycle
|
|
72
|
+
# `projects` FIELDS (S12, ADR 0025) ARE OWNER STATE TOO — a
|
|
73
|
+
# `given`/`ensures` reading one (e.g. `customer_status ==
|
|
74
|
+
# "active"`) is reading this record's own stored field, same
|
|
75
|
+
# as any attribute, even though nothing here WRITES it via a
|
|
76
|
+
# declared mutation (`CommandInterpreter#seed_projected_fields`
|
|
77
|
+
# populates it outside this analysis entirely). Left out of
|
|
78
|
+
# `known_writes` deliberately: `add_preservation_reads` then
|
|
79
|
+
# correctly treats it as a prior-state read that must survive
|
|
80
|
+
# a partial mutation, which is exactly right — a projected
|
|
81
|
+
# field's freshness comes from the interpreter reseeding it on
|
|
82
|
+
# save, not from anything a caller-supplied write set carries.
|
|
83
|
+
if aggregate.respond_to?(:projected_fields)
|
|
84
|
+
aggregate.projected_fields.each { |field| @owner_fields << field.name }
|
|
85
|
+
end
|
|
86
|
+
@payload_fields = command.attributes.to_set(&:name)
|
|
87
|
+
@state_reads = Set.new
|
|
88
|
+
@payload_reads = Set.new
|
|
89
|
+
@writes = Set.new
|
|
90
|
+
@known_writes = Set.new
|
|
91
|
+
@unresolved = Set.new
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def call
|
|
95
|
+
analyze_initial_state
|
|
96
|
+
analyze_mutations
|
|
97
|
+
analyze_lifecycle
|
|
98
|
+
analyze_rules(command.givens, phase: :before)
|
|
99
|
+
analyze_rules(command.ensures, phase: :after)
|
|
100
|
+
analyze_rules(aggregate.invariants, phase: :after)
|
|
101
|
+
add_preservation_reads
|
|
102
|
+
|
|
103
|
+
complete = unresolved.empty? && owner_fields.subset?(known_writes)
|
|
104
|
+
independent = complete && state_reads.empty?
|
|
105
|
+
|
|
106
|
+
Plan.new(
|
|
107
|
+
read_set: sorted(state_reads),
|
|
108
|
+
write_set: sorted(writes),
|
|
109
|
+
payload_read_set: sorted(payload_reads),
|
|
110
|
+
complete_state: complete,
|
|
111
|
+
state_independent: independent,
|
|
112
|
+
unresolved_dependencies: unresolved.to_a.sort.freeze
|
|
113
|
+
).freeze
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
attr_reader :aggregate, :command, :owner_fields, :payload_fields,
|
|
119
|
+
:state_reads, :payload_reads, :writes, :known_writes, :unresolved
|
|
120
|
+
|
|
121
|
+
# A fresh Instance supplies these values without reading a stored
|
|
122
|
+
# record. Keep this aligned with Instance.defaults/default_for. They
|
|
123
|
+
# establish completeness but are not command mutations, so they do not
|
|
124
|
+
# appear in write_set.
|
|
125
|
+
def analyze_initial_state
|
|
126
|
+
aggregate.attributes.each do |attribute|
|
|
127
|
+
known_writes << attribute.name if deterministic_initial_value?(attribute)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
known_writes << aggregate.lifecycle.field.to_sym if aggregate.lifecycle
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def deterministic_initial_value?(attribute)
|
|
134
|
+
return true if attribute.list? || attribute.optional? || !attribute.default.nil?
|
|
135
|
+
return false unless aggregate.respond_to?(:value_object)
|
|
136
|
+
|
|
137
|
+
value_object = aggregate.value_object(attribute.type)
|
|
138
|
+
value_object&.attributes&.all? { |field| !field.default.nil? }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def analyze_mutations
|
|
142
|
+
command.mutations.each do |mutation|
|
|
143
|
+
target = mutation.target.to_sym
|
|
144
|
+
writes << target
|
|
145
|
+
|
|
146
|
+
unless owner_fields.include?(target)
|
|
147
|
+
unresolved << "mutation target #{target} is not an aggregate field"
|
|
148
|
+
next
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if mutation.op == :set
|
|
152
|
+
known_writes << target if analyze_source?(mutation.source)
|
|
153
|
+
elsif STATEFUL_MUTATIONS.include?(mutation.op)
|
|
154
|
+
state_reads << target
|
|
155
|
+
analyze_source?(mutation.source)
|
|
156
|
+
else
|
|
157
|
+
unresolved << "mutation operation #{mutation.op} has no dependency rule"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Returns true only when the source is known without prior aggregate
|
|
163
|
+
# state. Hash sources are the canonical append binding shape.
|
|
164
|
+
def analyze_source?(source)
|
|
165
|
+
case source
|
|
166
|
+
when Symbol
|
|
167
|
+
if payload_fields.include?(source)
|
|
168
|
+
payload_reads << source
|
|
169
|
+
return true
|
|
170
|
+
end
|
|
171
|
+
if owner_fields.include?(source)
|
|
172
|
+
state_reads << source
|
|
173
|
+
return false
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
unresolved << "mutation source #{source} has no payload or aggregate field"
|
|
177
|
+
false
|
|
178
|
+
when Hash
|
|
179
|
+
source.values.map { |value| analyze_source?(value) }.all?
|
|
180
|
+
else
|
|
181
|
+
true
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def analyze_lifecycle
|
|
186
|
+
lifecycle = aggregate.lifecycle
|
|
187
|
+
return unless lifecycle
|
|
188
|
+
|
|
189
|
+
if command.from
|
|
190
|
+
state_reads << lifecycle.field
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
return if lifecycle.transitions_for(command.hecks_name).empty?
|
|
194
|
+
|
|
195
|
+
writes << lifecycle.field
|
|
196
|
+
known_writes << lifecycle.field
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def analyze_rules(rules, phase:)
|
|
200
|
+
rules.each do |rule|
|
|
201
|
+
ExpressionReads.paths(rule.canonical).each { |path| classify_path(path, phase) }
|
|
202
|
+
rescue ArgumentError => error
|
|
203
|
+
unresolved << "expression #{rule.canonical.inspect} could not be analyzed: #{error.message}"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def classify_path(path, phase)
|
|
208
|
+
head, nested = path.split(".", 2)
|
|
209
|
+
name = head.to_sym
|
|
210
|
+
|
|
211
|
+
if name == :parent
|
|
212
|
+
parent_field = nested.to_s.split(".", 2).first
|
|
213
|
+
if parent_field.empty? || !owner_fields.include?(parent_field.to_sym)
|
|
214
|
+
unresolved << "#{path} does not name parent aggregate state"
|
|
215
|
+
else
|
|
216
|
+
state_reads << parent_field.to_sym
|
|
217
|
+
end
|
|
218
|
+
elsif name == :old
|
|
219
|
+
old_field = nested.to_s.split(".", 2).first
|
|
220
|
+
if old_field.empty? || !owner_fields.include?(old_field.to_sym)
|
|
221
|
+
unresolved << "#{path} does not name prior aggregate state"
|
|
222
|
+
else
|
|
223
|
+
state_reads << old_field.to_sym
|
|
224
|
+
end
|
|
225
|
+
elsif payload_fields.include?(name)
|
|
226
|
+
payload_reads << name
|
|
227
|
+
elsif owner_fields.include?(name)
|
|
228
|
+
state_reads << name if phase == :before || !known_writes.include?(name)
|
|
229
|
+
else
|
|
230
|
+
unresolved << "expression path #{path} has no payload or aggregate field"
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# A partial mutation must preserve every untouched field on the
|
|
235
|
+
# correctness path. Those prior values are real reads even when no rule
|
|
236
|
+
# names them. A deterministic write needs no preservation read.
|
|
237
|
+
def add_preservation_reads
|
|
238
|
+
state_reads.merge(owner_fields - known_writes)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def sorted(values) = values.to_a.sort.freeze
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|