hecks 0.2.0 → 0.3.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 +403 -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 +62 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +148 -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 +131 -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 +80 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +391 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
- data/lib/hecks/adapters/driven/postgres_era.rb +671 -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 +63 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +285 -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 +36 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +96 -0
- data/lib/hecks/behaviors/expectations.rb +280 -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 +409 -0
- data/lib/hecks/bluebook/assembly/marks.rb +227 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +78 -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 +51 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -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 +68 -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 +906 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +626 -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 +305 -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 +120 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -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 +172 -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 +221 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +77 -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 +55 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
- data/lib/hecks/bluebook/expression/projection.json +170 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +630 -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 +559 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -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 +452 -0
- data/lib/hecks/bluebook/model_check.rb +318 -0
- data/lib/hecks/bluebook/pattern_subset.rb +120 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +86 -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 +58 -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 +389 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +116 -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 +154 -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 +300 -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 +170 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +53 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +132 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +115 -0
- data/lib/hecks/forms/record_table.rb +63 -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 +130 -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/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
- data/lib/hecks/fuzzing/properties.rb +1146 -0
- data/lib/hecks/fuzzing/replay.rb +635 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
- data/lib/hecks/fuzzing/value_generator.rb +177 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +291 -0
- data/lib/hecks/grammar/expression.bluebook +417 -0
- data/lib/hecks/grammar/expression_operators.json +1152 -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 +437 -0
- data/lib/hecks/language/bluebook/entity.bluebook +316 -0
- data/lib/hecks/language/bluebook/policy.bluebook +172 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +264 -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 +447 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -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 +313 -0
- data/lib/hecks/language/port.bluebook +92 -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 +121 -0
- data/lib/hecks/ports/access_control.port +4 -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 +4 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +4 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +4 -0
- data/lib/hecks/ports/authorization.rb +59 -0
- data/lib/hecks/ports/clock.port +4 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +4 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +4 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +4 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +4 -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 +102 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +18 -0
- data/lib/hecks/ports/persistence/lineage.rb +277 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -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 +76 -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 +413 -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 +31 -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 +437 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +141 -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 +392 -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 +80 -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 +89 -0
- data/lib/hecks/query_specification/hop_path.rb +130 -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/caller.rb +50 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
- data/lib/hecks/runtime/command_interpreter.rb +413 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +181 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +231 -0
- data/lib/hecks/runtime/dispatcher.rb +286 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +228 -0
- data/lib/hecks/runtime/era_check.rb +136 -0
- data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/runtime/era_guard.rb +193 -0
- data/lib/hecks/runtime/era_tamper.rb +61 -0
- data/lib/hecks/runtime/errors.rb +100 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +128 -0
- data/lib/hecks/runtime/instance.rb +120 -0
- data/lib/hecks/runtime/interpreting.rb +69 -0
- data/lib/hecks/runtime/loader.rb +143 -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 +254 -0
- data/lib/hecks/runtime/reaction_invocation.rb +245 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +266 -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 +124 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
- data/lib/hecks/runtime/registry/verification.rb +158 -0
- data/lib/hecks/runtime/registry.rb +218 -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 +301 -0
- data/lib/hecks/runtime/storage_shape.rb +120 -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 +112 -0
- data/lib/hecks/runtime/value/coercion.rb +500 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +95 -0
- data/lib/hecks/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/translation/audit/layer_one.rb +37 -0
- data/lib/hecks/translation/audit/layer_two.rb +77 -0
- data/lib/hecks/translation/audit/unfed_report.rb +42 -0
- data/lib/hecks/translation/audit.rb +70 -0
- data/lib/hecks/translation/reattest.rb +72 -0
- data/lib/hecks/translation/rule_compiler.rb +120 -0
- data/lib/hecks/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/translation/scaffold.rb +30 -0
- data/lib/hecks/translation.rb +11 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +213 -0
- data/lib/hecks.rb +118 -6
- metadata +401 -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,307 @@
|
|
|
1
|
+
require_relative "../../bluebook/expression/evaluator"
|
|
2
|
+
require_relative "../../rendering"
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
require_relative "../refusal_wording"
|
|
5
|
+
require_relative "../value"
|
|
6
|
+
require_relative "../instance"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Runtime
|
|
10
|
+
class CommandRules
|
|
11
|
+
# Whether a command may run at all: its declared givens, and the
|
|
12
|
+
# lifecycle transition it asks for.
|
|
13
|
+
module Admissibility
|
|
14
|
+
# WRAPS `subject` so a guard can read a DECLARED-but-storage-absent
|
|
15
|
+
# optional attribute as nil, not "cannot resolve." `Instance
|
|
16
|
+
# #hydrate_with_defaults` deliberately leaves one absent rather
|
|
17
|
+
# than nil-filled — "an attribute with no default stays absent,
|
|
18
|
+
# exactly as stored," that file's own comment — which is right
|
|
19
|
+
# for storage fidelity but wrong for a `given`/`ensures` reading
|
|
20
|
+
# an optional field a record predates (measured, not assumed: a
|
|
21
|
+
# real Item.Promote crashed on `!promoted` against a record from
|
|
22
|
+
# before `promoted` existed). Defensive on purpose — a `subject`
|
|
23
|
+
# with no `.aggregate` (an entity's own view/settled wrapper,
|
|
24
|
+
# entity_interpreter.rb's own callers) just degrades to today's
|
|
25
|
+
# exact behavior, zero risk to a path this bug was never measured
|
|
26
|
+
# against.
|
|
27
|
+
#
|
|
28
|
+
# NIL IS NOT THE ONLY OUTCOME, though. The Item.Promote fix above
|
|
29
|
+
# covered an OPTIONAL field — the honest case, where absence is
|
|
30
|
+
# exactly what optional means. It also, as a side effect, let a
|
|
31
|
+
# NON-optional field predating a record read nil the same way,
|
|
32
|
+
# which is the `ne:`/array-`in:` bug class applied to rule
|
|
33
|
+
# evaluation: a predicate silently answers against a value nobody
|
|
34
|
+
# ever wrote (ADR 0025, "Added attributes and absence"). `[]`
|
|
35
|
+
# below narrows the nil-read back to what it was built for —
|
|
36
|
+
# optional stays nil, non-optional raises a named refusal
|
|
37
|
+
# identifying the field, so a rule that reads it fails loud
|
|
38
|
+
# instead of quietly wrong.
|
|
39
|
+
class GuardState
|
|
40
|
+
def initialize(instance)
|
|
41
|
+
@instance = instance
|
|
42
|
+
@declared = instance.respond_to?(:aggregate) ? instance.aggregate.attributes.to_h { |a| [a.name, a] } : {}
|
|
43
|
+
# S12, ADR 0025 — a SEPARATE index, the same reason
|
|
44
|
+
# `Aggregate#projected_fields` is a separate IR collection
|
|
45
|
+
# rather than folded into `attributes` (see that field's
|
|
46
|
+
# own comment): a projected field's absence means
|
|
47
|
+
# something different from an ordinary attribute's, so it
|
|
48
|
+
# needs its own refusal below, not `AttributeAbsent`'s.
|
|
49
|
+
# `projects` is AGGREGATE-scoped only — an entity's own
|
|
50
|
+
# `instance.aggregate` answers the ENTITY construct here
|
|
51
|
+
# (EntityInterpreter's own subject), which declares no
|
|
52
|
+
# `projected_fields` of its own, hence the extra guard
|
|
53
|
+
# `@declared` above does not need.
|
|
54
|
+
owner = instance.aggregate if instance.respond_to?(:aggregate)
|
|
55
|
+
@projected = owner.respond_to?(:projected_fields) ? owner.projected_fields.to_h { |f| [f.name, f] } : {}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def key?(name) = @declared.key?(name.to_sym) || @projected.key?(name.to_sym) || @instance.key?(name)
|
|
59
|
+
|
|
60
|
+
def [](name)
|
|
61
|
+
return @instance[name] if @instance.key?(name)
|
|
62
|
+
|
|
63
|
+
projected = @projected[name.to_sym]
|
|
64
|
+
return raise_projection_absent(projected) if projected
|
|
65
|
+
|
|
66
|
+
attribute = @declared[name.to_sym]
|
|
67
|
+
return nil if attribute.nil? || attribute.optional?
|
|
68
|
+
|
|
69
|
+
raise AttributeAbsent,
|
|
70
|
+
RefusalWording.render("AttributeAbsent", "absent_read",
|
|
71
|
+
aggregate: @instance.aggregate.hecks_name, field: name)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def raise_projection_absent(projected)
|
|
77
|
+
raise ProjectionAbsent,
|
|
78
|
+
RefusalWording.render("ProjectionAbsent", "absent_read",
|
|
79
|
+
aggregate: @instance.aggregate.hecks_name, field: projected.name,
|
|
80
|
+
reference: projected.reference, remote_field: projected.remote_field)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
private_constant :GuardState
|
|
84
|
+
|
|
85
|
+
# `domain:` is only needed to dereference a reference-typed field
|
|
86
|
+
# (`customer.status`) — see References#dereference. `owner` is the
|
|
87
|
+
# declaring aggregate/entity when `subject` carries one (an
|
|
88
|
+
# entity's pre-mutation `view` does, same as an aggregate's
|
|
89
|
+
# `Instance`); a `subject` with no `.aggregate` just hydrates
|
|
90
|
+
# nothing from state, same as GuardState degrades above.
|
|
91
|
+
#
|
|
92
|
+
# MERGE ORDER MATTERS, and it is NOT "args always win": an
|
|
93
|
+
# unaliased command-level reference dereferences under a
|
|
94
|
+
# different name than the argument holds (`account_id` the arg,
|
|
95
|
+
# `account` the hydrated key — no collision, order is moot). An
|
|
96
|
+
# ALIASED one (`reference_to Customer, as: :customer`) hydrates
|
|
97
|
+
# under the SAME name the argument itself holds — `customer` is
|
|
98
|
+
# both the raw id an arg puts there and the key `customer.status`
|
|
99
|
+
# expects to dig into. If `args` merged last, the raw id (a
|
|
100
|
+
# String) would win and `.status` on a String is where a fuzzer
|
|
101
|
+
# found this — TypeError, not a refusal. Command-level
|
|
102
|
+
# dereferencing is the one thing that is SUPPOSED to override
|
|
103
|
+
# its own source argument for exactly this reason; args still
|
|
104
|
+
# wins over stored OWNER state (unchanged from before this fix).
|
|
105
|
+
# `parent:` is an entity command's OWN parent aggregate record
|
|
106
|
+
# (EntityInterpreter's `ctx.instance` — "the PARENT aggregate
|
|
107
|
+
# record", its own doc comment) — the entity's containment, not a
|
|
108
|
+
# declared reference attribute, so it doesn't come from
|
|
109
|
+
# `dereference`'s attribute scan the way `owner`'s do. Hydrated
|
|
110
|
+
# the same shape regardless: the parent's own state, MERGED so
|
|
111
|
+
# the dereferenced hash wins over the raw reference it replaces
|
|
112
|
+
# (ADR 0025 dropped the `_id` suffix that used to keep the two
|
|
113
|
+
# apart by name, so `parent.state.merge(dereference(...))` is
|
|
114
|
+
# now load-bearing, not redundant) — plus ITS OWN references
|
|
115
|
+
# dereferenced one level in, so `parent.customer.status` (a
|
|
116
|
+
# parent aggregate reaching ITS OWN customer) resolves the same
|
|
117
|
+
# way `account.customer.status` does for a command-level reference.
|
|
118
|
+
# nil for an aggregate command — CommandInterpreter never passes it.
|
|
119
|
+
def enforce_givens(subject, command, args, domain:, declaring: nil, parent: nil)
|
|
120
|
+
state = GuardState.new(subject)
|
|
121
|
+
owner = subject.aggregate if subject.respond_to?(:aggregate)
|
|
122
|
+
attrs = dereference(domain, owner, subject).merge(args).merge(dereference(domain, command, args))
|
|
123
|
+
attrs = attrs.merge(parent: parent.state.merge(dereference(domain, parent.aggregate, parent.state))) if parent
|
|
124
|
+
command.givens.each do |given|
|
|
125
|
+
next if Bluebook::Expression::Evaluator.call(given.canonical, state, attrs)
|
|
126
|
+
|
|
127
|
+
raise GivenNotMet, "#{command.hecks_name} refused — #{given.description}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
enforce_lifecycle_guard(declaring, command, subject) if declaring
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# LIFECYCLE STATE AS A COMMAND GUARD (S10, ADR 0025) — `command
|
|
134
|
+
# "Debit", from: "open"` checked here, folded into the SAME
|
|
135
|
+
# dispatch step `given` already runs at (both are preconditions,
|
|
136
|
+
# evaluated before any mutation) rather than earning its own
|
|
137
|
+
# DISPATCH_ORDER entry. A GUARD, never a transition: it names no
|
|
138
|
+
# target state and `step_advance_lifecycle` never sees it — see
|
|
139
|
+
# `admissible_transition`, right below, for the transition this
|
|
140
|
+
# is deliberately NOT reusing (its own `StateTransition#target`
|
|
141
|
+
# is required, and a guard-only command has none to give it).
|
|
142
|
+
def enforce_lifecycle_guard(declaring, command, subject)
|
|
143
|
+
return unless command.from
|
|
144
|
+
|
|
145
|
+
lifecycle = declaring.lifecycle
|
|
146
|
+
current = Value.scalar(subject[lifecycle.field]).to_s
|
|
147
|
+
return if Array(command.from).include?(current)
|
|
148
|
+
|
|
149
|
+
# ROUTED THROUGH RefusalWording's OWN "transition_blocked"
|
|
150
|
+
# TEMPLATE — the same one #admissible_transition, right below,
|
|
151
|
+
# already raises LifecycleRefused through for the same
|
|
152
|
+
# refusal class. This used to hand-roll its own wording
|
|
153
|
+
# inline ("...only runs from..." vs. the template's "...moves
|
|
154
|
+
# it only from...") — two shapes for one refusal kind, so
|
|
155
|
+
# anything string-matching a LifecycleRefused message (a
|
|
156
|
+
# property, a spec, a caller) had to know both existed rather
|
|
157
|
+
# than one.
|
|
158
|
+
raise LifecycleRefused,
|
|
159
|
+
RefusalWording.render("LifecycleRefused", "transition_blocked",
|
|
160
|
+
command: command.hecks_name, field: lifecycle.field,
|
|
161
|
+
current: Rendering.describe(current),
|
|
162
|
+
allowed: Array(command.from).map(&:inspect).join(" or "))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The far side of the contract: evaluated against the SETTLED record
|
|
166
|
+
# — after mutations and the lifecycle move, before anything persists
|
|
167
|
+
# — with `old` carrying the state as the givens saw it. Injected into
|
|
168
|
+
# the attrs at evaluation time only; the payload gate never sees it.
|
|
169
|
+
#
|
|
170
|
+
# `old` — and every dispatch ARGUMENT — wins over a same-named STATE
|
|
171
|
+
# field in expression scope (Resolver#fetch checks attrs first). An
|
|
172
|
+
# ensures naming a field the command also takes as an argument (or,
|
|
173
|
+
# on an entity, a field that doubles as the addressing argument
|
|
174
|
+
# element_of reads) will read the ARGUMENT, not the settled value.
|
|
175
|
+
# Not new to ensures — `given` lives under the same rule — but an
|
|
176
|
+
# ensures is more likely to collide, since it typically re-reads a
|
|
177
|
+
# field the command just took in to mutate it.
|
|
178
|
+
def enforce_ensures(subject, command, args, old:, domain:, parent: nil)
|
|
179
|
+
state = GuardState.new(subject)
|
|
180
|
+
owner = subject.aggregate if subject.respond_to?(:aggregate)
|
|
181
|
+
# Same merge-order reasoning as enforce_givens above: an
|
|
182
|
+
# aliased command-level reference must override its own raw
|
|
183
|
+
# id argument, not the other way round. `old` still wins over
|
|
184
|
+
# everything, unchanged.
|
|
185
|
+
attrs = dereference(domain, owner, subject).merge(args).merge(dereference(domain, command, args))
|
|
186
|
+
attrs = attrs.merge(parent: parent.state.merge(dereference(domain, parent.aggregate, parent.state))) if parent
|
|
187
|
+
attrs = attrs.merge(old: old)
|
|
188
|
+
command.ensures.each do |rule|
|
|
189
|
+
next if Bluebook::Expression::Evaluator.call(rule.canonical, state, attrs)
|
|
190
|
+
|
|
191
|
+
raise EnsuresNotMet, "#{command.hecks_name} refused — #{rule.description}"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# THE AGGREGATE BOUNDARY, checked after every command, before
|
|
196
|
+
# save (S10, ADR 0025 — "Rules") — the same point `enforce_
|
|
197
|
+
# ensures` already checks at, and for the same reason: an
|
|
198
|
+
# invariant is a claim about the SETTLED record, not the
|
|
199
|
+
# command that produced it, so it reads no `args`/`old` at all,
|
|
200
|
+
# only the record's own (dereferenced) state. `subject` here is
|
|
201
|
+
# always the AGGREGATE's own instance — `CommandInterpreter`
|
|
202
|
+
# passes its own `ctx.instance`, and `EntityInterpreter` passes
|
|
203
|
+
# the PARENT record (`ctx.instance`, not the element), since an
|
|
204
|
+
# entity mutation changes data inside the SAME aggregate
|
|
205
|
+
# boundary the invariant guards; there is no separate "entity
|
|
206
|
+
# invariant" to check the piece's own view against.
|
|
207
|
+
def enforce_invariants(subject, aggregate, domain:)
|
|
208
|
+
state = GuardState.new(subject)
|
|
209
|
+
attrs = dereference(domain, aggregate, subject)
|
|
210
|
+
aggregate.invariants.each do |invariant|
|
|
211
|
+
next if Bluebook::Expression::Evaluator.call(invariant.canonical, state, attrs)
|
|
212
|
+
|
|
213
|
+
raise InvariantViolation, "#{aggregate.hecks_name} refused — #{invariant.description}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
check_entity_invariants(aggregate, subject, domain: domain)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# A PIECE'S OWN SHAPE RULE, checked against EVERY INSTANCE the
|
|
220
|
+
# aggregate holds — not a separate boundary from the aggregate's
|
|
221
|
+
# own invariants just above (same two checkpoints: after every
|
|
222
|
+
# mutation, before save), just a WIDER one: the aggregate's own
|
|
223
|
+
# consistency includes each of its pieces individually looking
|
|
224
|
+
# right, the same way `ValueObject#invariants` already checks
|
|
225
|
+
# each of ITS OWN instances one construct up. RECURSES into
|
|
226
|
+
# nested pieces (S17, ADR 0026 — Dispatch inside Handler) the
|
|
227
|
+
# same way `check_entity_invariants`'s own caller recurses
|
|
228
|
+
# nowhere else needs to, since a piece's `entities` are already
|
|
229
|
+
# exactly as reachable as an aggregate's.
|
|
230
|
+
#
|
|
231
|
+
# `list_attr` reuses the EXACT lookup `EntityInterpreter#
|
|
232
|
+
# element_of` already makes to locate a SINGLE addressed
|
|
233
|
+
# element by identity — this reads every element instead, but
|
|
234
|
+
# the "which field on the owner holds this piece's own
|
|
235
|
+
# instances" question is the identical one. A piece declaring
|
|
236
|
+
# invariants that nothing on its owner actually holds (no
|
|
237
|
+
# matching list attribute) is a static-analysis gap for a
|
|
238
|
+
# future gate, not a runtime concern here — `next` past it
|
|
239
|
+
# rather than raising mid-enforcement for an unrelated command.
|
|
240
|
+
def check_entity_invariants(owner_construct, owner_instance, domain:)
|
|
241
|
+
owner_construct.entities.each do |entity|
|
|
242
|
+
next if entity.invariants.empty?
|
|
243
|
+
|
|
244
|
+
list_attr = owner_construct.attributes.find { |a| a.list? && a.type.to_s == entity.hecks_name }
|
|
245
|
+
next unless list_attr
|
|
246
|
+
|
|
247
|
+
Array(owner_instance[list_attr.name]).each do |element|
|
|
248
|
+
wrapped = Instance.new(aggregate: entity, id: nil, state: element)
|
|
249
|
+
element_state = GuardState.new(wrapped)
|
|
250
|
+
attrs = dereference(domain, entity, wrapped)
|
|
251
|
+
.merge(parent: owner_instance.state.merge(dereference(domain, owner_construct, owner_instance)))
|
|
252
|
+
|
|
253
|
+
entity.invariants.each do |invariant|
|
|
254
|
+
next if Bluebook::Expression::Evaluator.call(invariant.canonical, element_state, attrs)
|
|
255
|
+
|
|
256
|
+
raise InvariantViolation, "#{entity.hecks_name} refused — #{invariant.description}"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
check_entity_invariants(entity, wrapped, domain: domain)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def admissible_transition(declaring, command, subject)
|
|
265
|
+
lifecycle = declaring.lifecycle
|
|
266
|
+
return nil unless lifecycle
|
|
267
|
+
|
|
268
|
+
candidates = lifecycle.transitions_for(command.hecks_name)
|
|
269
|
+
return nil if candidates.empty?
|
|
270
|
+
|
|
271
|
+
# `Value.scalar` unwrap -- vendored addition, not (yet)
|
|
272
|
+
# upstream hecks (migration plan task 9): a VO-typed
|
|
273
|
+
# lifecycle field (the norm, not the exception, per this
|
|
274
|
+
# corpus's own no-primitive-envy convention) holds a real
|
|
275
|
+
# `Runtime::Value` here, and a bare `.to_s` on that hit Ruby's
|
|
276
|
+
# default `Object#to_s` instead of unwrapping the inner
|
|
277
|
+
# scalar first -- `current` came back as a raw object-pointer
|
|
278
|
+
# string (`"#<Hecks::Runtime::Value:0x...>"`) that could
|
|
279
|
+
# never match any declared `from` state, so EVERY transition
|
|
280
|
+
# on a VO-typed lifecycle field refused unconditionally, and
|
|
281
|
+
# when it refused the message leaked the pointer too.
|
|
282
|
+
# Confirmed live via `Plan::Task.Complete` (status defaults to
|
|
283
|
+
# `TaskStatus`, a single-field VO), not inferred. Reuses
|
|
284
|
+
# `Value.scalar` -- this file's own third candidate for "how
|
|
285
|
+
# to unwrap a Value/Hash-shaped field," already built and
|
|
286
|
+
# already documented for exactly this job ("rendering a value
|
|
287
|
+
# object into a column or a message, where there is no path
|
|
288
|
+
# to consult," `value/coercion.rb`'s own comment) -- rather
|
|
289
|
+
# than inventing a second unwrap helper beside `Resolver#
|
|
290
|
+
# unwrap_scalar`'s bare-comparison one. Duck-typed the same
|
|
291
|
+
# way : a bare, non-VO lifecycle field passes through
|
|
292
|
+
# unchanged (`Value.scalar` only opens a `Value` instance).
|
|
293
|
+
current = Value.scalar(subject[lifecycle.field]).to_s
|
|
294
|
+
admitted = candidates.find { |t| !t.constrained? || Array(t.from).include?(current) }
|
|
295
|
+
return admitted if admitted
|
|
296
|
+
|
|
297
|
+
allowed = candidates.flat_map { |t| Array(t.from) }.uniq
|
|
298
|
+
raise LifecycleRefused,
|
|
299
|
+
RefusalWording.render("LifecycleRefused", "transition_blocked",
|
|
300
|
+
command: command.hecks_name, field: lifecycle.field,
|
|
301
|
+
current: Rendering.describe(current),
|
|
302
|
+
allowed: allowed.map(&:inspect).join(" or "))
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
require_relative "../../rendering"
|
|
2
|
+
require_relative "../errors"
|
|
3
|
+
require_relative "../refusal_wording"
|
|
4
|
+
require_relative "../value"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Runtime
|
|
8
|
+
class CommandRules
|
|
9
|
+
# The arithmetic half of mutation: what a source resolves to, and how
|
|
10
|
+
# increment/decrement land on an Integer or a one-numeric-field value
|
|
11
|
+
# object.
|
|
12
|
+
module Arithmetic
|
|
13
|
+
# The ops Runtime::CommandInterpreter applies. Declared the same way in
|
|
14
|
+
# Vocabulary::MutationOp (language/bluebook/vocabulary.bluebook) —
|
|
15
|
+
# spec/vocabulary_conformance_spec holds the two tables equal, so
|
|
16
|
+
# increment/decrement's sign cannot drift from what the language says
|
|
17
|
+
# each op means. set/append carry no sign — they do no arithmetic.
|
|
18
|
+
MutationOp = Struct.new(:name, :sign, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
MUTATION_OPS = [
|
|
21
|
+
MutationOp.new(name: "set", sign: nil),
|
|
22
|
+
MutationOp.new(name: "append", sign: nil),
|
|
23
|
+
MutationOp.new(name: "increment", sign: 1),
|
|
24
|
+
MutationOp.new(name: "decrement", sign: -1),
|
|
25
|
+
# Vendored addition, not (yet) upstream hecks (migration
|
|
26
|
+
# plan task 4, i106): multiply/clamp carry no sign -- like
|
|
27
|
+
# set/append, they do no add-or-subtract arithmetic (multiply
|
|
28
|
+
# scales, clamp bounds). See #multiply/#clamp below.
|
|
29
|
+
MutationOp.new(name: "multiply", sign: nil),
|
|
30
|
+
MutationOp.new(name: "clamp", sign: nil),
|
|
31
|
+
# Vendored addition, not (yet) upstream hecks (migration
|
|
32
|
+
# plan task 4): remove -- carries no sign, like set/append; it
|
|
33
|
+
# matches a list element by value rather than doing arithmetic.
|
|
34
|
+
# Declared here so this table stays exactly what
|
|
35
|
+
# Vocabulary::MutationOp declares (spec/vocabulary_conformance_spec
|
|
36
|
+
# holds the two equal) -- MutationApplier's own `when :remove`
|
|
37
|
+
# branch (mutation_applier.rb) never calls #sign_of, so this was
|
|
38
|
+
# a declared-vocabulary gap, not a behaviour gap.
|
|
39
|
+
MutationOp.new(name: "remove", sign: nil),
|
|
40
|
+
# Vendored addition, not (yet) upstream hecks —
|
|
41
|
+
# CommandBuilder#delegates_to's own comment gives the full
|
|
42
|
+
# reasoning; carries no sign, like set/append/remove — it does
|
|
43
|
+
# no arithmetic, only a synchronous handoff into one nested
|
|
44
|
+
# entity command. Declared here so this table stays exactly
|
|
45
|
+
# what Vocabulary::MutationOp declares — MutationApplier's own
|
|
46
|
+
# `when :delegate` branch (mutation_applier.rb) never calls
|
|
47
|
+
# #sign_of either, same as `remove`'s own note above.
|
|
48
|
+
MutationOp.new(name: "delegate", sign: nil)
|
|
49
|
+
].freeze
|
|
50
|
+
|
|
51
|
+
# A mutation's source is either the NAME OF AN ARGUMENT or a LITERAL, and
|
|
52
|
+
# the two are told apart by type : a Symbol is always a name, a String or a
|
|
53
|
+
# number is always a value. Checked across all eight chapters — `to: :name`
|
|
54
|
+
# and `to: "sold"`, never a Symbol meant as a value.
|
|
55
|
+
#
|
|
56
|
+
# `&& args.key?(source)` used to guard the lookup, and that guard is what
|
|
57
|
+
# made an ABSENT argument fall through to `source` and return THE SYMBOL
|
|
58
|
+
# ITSELF as the value. `Customer.Register` without its `name` set name to
|
|
59
|
+
# the literal `:name`, coercion met a Symbol where a PersonName belonged,
|
|
60
|
+
# and the refusal read "name is a PersonName — pass its fields as an
|
|
61
|
+
# object, not :name" — a message describing a mistake the caller had not
|
|
62
|
+
# made. The real mistake, an absent argument, was never the one refused,
|
|
63
|
+
# which is what fuzz surfaced.
|
|
64
|
+
#
|
|
65
|
+
# Absent now resolves to nil. Whether it should be REFUSED instead
|
|
66
|
+
# is a separate question — the language cannot yet say which arguments are
|
|
67
|
+
# optional, and the meta-domain has plenty that are.
|
|
68
|
+
def resolve_source(source, args)
|
|
69
|
+
return args[source] if source.is_a?(Symbol)
|
|
70
|
+
|
|
71
|
+
source
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def arithmetic(current, amount, target, sign)
|
|
75
|
+
op = sign.positive? ? "increment" : "decrement"
|
|
76
|
+
current ||= 0
|
|
77
|
+
|
|
78
|
+
if current.is_a?(Value) && amount.is_a?(Value)
|
|
79
|
+
return arithmetic_value_object(current, amount, target, sign, op)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# `current` genuinely absent (no declared default, never set) and
|
|
83
|
+
# `amount` arrives VO-wrapped — a real command argument typed the
|
|
84
|
+
# same as the attribute, but with nothing to combine field-by-
|
|
85
|
+
# field against yet (that is what `arithmetic_value_object`,
|
|
86
|
+
# above, is for once BOTH sides carry real fields). Before this,
|
|
87
|
+
# falling straight to `unless amount.is_a?(Numeric)` below
|
|
88
|
+
# refused with "increment needs an Integer, got 500" — true of
|
|
89
|
+
# nothing: 500 is exactly the Integer it asked for, just still
|
|
90
|
+
# wearing the Money wrapper the command's own declared attribute
|
|
91
|
+
# type put it in. Unwrapped here, the same shape #clamp already
|
|
92
|
+
# falls through to for an absent VO-typed attribute
|
|
93
|
+
# (`current ||= 0`, then a raw scalar) — the mutation applier
|
|
94
|
+
# re-wraps the raw result into the declared VO type on write,
|
|
95
|
+
# the same way it already does for clamp's own result.
|
|
96
|
+
amount = unwrap_single_numeric_field(amount) if amount.is_a?(Value)
|
|
97
|
+
|
|
98
|
+
# Widened from Integer to Numeric (migration plan task 4, i106):
|
|
99
|
+
# miette's organ math increments a Float (`increment: 0.02`) --
|
|
100
|
+
# the raw, non-value-object path only ever mattered for Integer
|
|
101
|
+
# counters before this corpus existed. Integer stays the common
|
|
102
|
+
# case; Float is now accepted the same way.
|
|
103
|
+
unless amount.is_a?(Numeric)
|
|
104
|
+
raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_amount",
|
|
105
|
+
op: op, target: target, offered: Rendering.describe(amount))
|
|
106
|
+
end
|
|
107
|
+
unless current.is_a?(Numeric)
|
|
108
|
+
raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current",
|
|
109
|
+
op: op, target: target, offered: Rendering.describe(current))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
current + (sign * amount)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def arithmetic_value_object(current, amount, target, sign, op)
|
|
116
|
+
current_fields = current.to_h
|
|
117
|
+
amount_fields = amount.to_h
|
|
118
|
+
# Widened from Integer to Numeric -- see #arithmetic's own
|
|
119
|
+
# comment. A synthesised value-object wrapper around a bare
|
|
120
|
+
# Float attribute (miette's Synapse#strength, auto-wrapped per
|
|
121
|
+
# Part 3a's "bare primitives forbidden" finding) lands here as
|
|
122
|
+
# a one-Float-field Value exactly the way a one-Integer-field
|
|
123
|
+
# Value already did.
|
|
124
|
+
shared_numeric = current_fields.keys.select do |field|
|
|
125
|
+
current_fields[field].is_a?(Numeric) && amount_fields[field].is_a?(Numeric)
|
|
126
|
+
end
|
|
127
|
+
unless shared_numeric.size == 1
|
|
128
|
+
raise TypeMismatch,
|
|
129
|
+
RefusalWording.render("TypeMismatch", "arithmetic_shared_field", op: op, target: target)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
field = shared_numeric.first
|
|
133
|
+
current.with(field, current[field] + (sign * amount[field]))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Not a bare `.find(...)&.sign || -1` — that silently answered
|
|
137
|
+
# DECREMENT'S sign for BOTH an op this table has never heard of
|
|
138
|
+
# AND a declared, real op that simply carries no sign at all
|
|
139
|
+
# (set/append/multiply/clamp/remove — see MUTATION_OPS above).
|
|
140
|
+
# Callers today only ever reach this for :increment/:decrement
|
|
141
|
+
# (both MutationApplier#apply and EntityInterpreter#
|
|
142
|
+
# apply_to_element gate every other op through their own `case`
|
|
143
|
+
# first, each with its own loud WiringError backstop), so this
|
|
144
|
+
# raise is not a real runtime path yet — it is the same
|
|
145
|
+
# backstop one level down, in case a future caller reaches
|
|
146
|
+
# #sign_of directly for an op that was never meant to have one.
|
|
147
|
+
def sign_of(op)
|
|
148
|
+
MUTATION_OPS.find { |candidate| candidate.name == op.to_s }&.sign ||
|
|
149
|
+
raise(WiringError, "no sign declared for mutation op #{op.inspect} — add one before calling #sign_of")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Vendored addition, not (yet) upstream hecks (migration plan
|
|
153
|
+
# task 4, i106): `current * amount` -- the scaling counterpart to
|
|
154
|
+
# increment/decrement's add/subtract. Same raw-vs-value-object
|
|
155
|
+
# branch shape as #arithmetic/#arithmetic_value_object, reused
|
|
156
|
+
# rather than duplicated verb-for-verb (a `Proc` picks the actual
|
|
157
|
+
# arithmetic; everything else -- the Value unwrap/rewrap, the
|
|
158
|
+
# TypeMismatch refusals -- is identical to the additive pair).
|
|
159
|
+
def multiply(current, amount, target)
|
|
160
|
+
current ||= 0
|
|
161
|
+
|
|
162
|
+
if current.is_a?(Value) && amount.is_a?(Value)
|
|
163
|
+
return combine_value_object(current, amount, target, "multiply") { |c, a| c * a }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Same absent-`current`, VO-wrapped-`amount` gap as `#arithmetic`
|
|
167
|
+
# — see that method's own comment.
|
|
168
|
+
amount = unwrap_single_numeric_field(amount) if amount.is_a?(Value)
|
|
169
|
+
|
|
170
|
+
unless amount.is_a?(Numeric) && current.is_a?(Numeric)
|
|
171
|
+
raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_amount",
|
|
172
|
+
op: "multiply", target: target,
|
|
173
|
+
offered: Rendering.describe(current.is_a?(Numeric) ? amount : current))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
current * amount
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Vendored addition, not (yet) upstream hecks (migration plan
|
|
180
|
+
# task 4, i106): bound the CURRENT value into `[min, max]` -- no
|
|
181
|
+
# "amount" to combine, so it does not go through
|
|
182
|
+
# #arithmetic/#multiply's shared-numeric-field matching at all;
|
|
183
|
+
# it clamps whichever single numeric field the wrapping value
|
|
184
|
+
# object carries (a synthesised wrapper always carries exactly
|
|
185
|
+
# one, per Part 3a's auto-synthesis).
|
|
186
|
+
def clamp(current, bounds, target)
|
|
187
|
+
min, max = bounds
|
|
188
|
+
# THE SAME `current ||= 0` #arithmetic/#multiply both give a
|
|
189
|
+
# PHANTOM (never-set) numeric field, one line up from each —
|
|
190
|
+
# this was the one arithmetic op that didn't, so a VO-typed
|
|
191
|
+
# attribute with no declared `default:` (genuinely absent,
|
|
192
|
+
# `Instance.defaults`/`#default_for`) hit TypeMismatch on the
|
|
193
|
+
# FIRST clamp. (#arithmetic/#multiply's OWN absent-current gap
|
|
194
|
+
# was a real, separate bug this comment used to describe wrong —
|
|
195
|
+
# they did not "silently treat the same absent field as zero";
|
|
196
|
+
# they raised too, blaming a perfectly valid `amount` for not
|
|
197
|
+
# being an Integer when it was one, just still Money-wrapped.
|
|
198
|
+
# Fixed alongside this one — see #unwrap_single_numeric_field.)
|
|
199
|
+
current ||= 0
|
|
200
|
+
if current.is_a?(Value)
|
|
201
|
+
fields = current.to_h
|
|
202
|
+
field = fields.keys.find { |f| fields[f].is_a?(Numeric) } or
|
|
203
|
+
raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current",
|
|
204
|
+
op: "clamp", target: target, offered: Rendering.describe(current))
|
|
205
|
+
return current.with(field, fields[field].clamp(min, max))
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
unless current.is_a?(Numeric)
|
|
209
|
+
raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current",
|
|
210
|
+
op: "clamp", target: target, offered: Rendering.describe(current))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
current.clamp(min, max)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
# `amount` arrives VO-wrapped whenever the command's own declared
|
|
219
|
+
# attribute type says so (a real `Money`, not a bare Integer) —
|
|
220
|
+
# true whether or not `current` has ever been set. Only meaningful
|
|
221
|
+
# to call once `current` is known NOT to be a Value itself (the
|
|
222
|
+
# `current.is_a?(Value) && amount.is_a?(Value)` branch, above in
|
|
223
|
+
# both callers, already owns the case where both sides carry real
|
|
224
|
+
# fields to combine). Refuses rather than guesses when more than
|
|
225
|
+
# one field is numeric — genuinely ambiguous which one an absent
|
|
226
|
+
# `current` should be treated as zero for, the same reasoning
|
|
227
|
+
# `combine_value_object`'s own `shared_numeric.size == 1` check
|
|
228
|
+
# already holds to when both sides ARE present.
|
|
229
|
+
def unwrap_single_numeric_field(value)
|
|
230
|
+
fields = value.to_h
|
|
231
|
+
numeric_fields = fields.keys.select { |field| fields[field].is_a?(Numeric) }
|
|
232
|
+
return value unless numeric_fields.size == 1
|
|
233
|
+
|
|
234
|
+
fields[numeric_fields.first]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def combine_value_object(current, amount, target, op)
|
|
238
|
+
current_fields = current.to_h
|
|
239
|
+
amount_fields = amount.to_h
|
|
240
|
+
shared_numeric = current_fields.keys.select do |field|
|
|
241
|
+
current_fields[field].is_a?(Numeric) && amount_fields[field].is_a?(Numeric)
|
|
242
|
+
end
|
|
243
|
+
unless shared_numeric.size == 1
|
|
244
|
+
raise TypeMismatch,
|
|
245
|
+
RefusalWording.render("TypeMismatch", "arithmetic_shared_field", op: op, target: target)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
field = shared_numeric.first
|
|
249
|
+
current.with(field, yield(current[field], amount[field]))
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
else
|
|
42
|
+
caller.role == command.role
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
return if authorized
|
|
46
|
+
|
|
47
|
+
raise Unauthorized, RefusalWording.render("Unauthorized", "role_mismatch",
|
|
48
|
+
command: command.hecks_name, role: command.role,
|
|
49
|
+
caller_role: caller.role)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# SELF-EXEMPT, like `HecksagonBuilder#refuse_ungoverned_roles!` —
|
|
55
|
+
# Governance IS the thing a `holds_role?` lookup runs against, so
|
|
56
|
+
# its own commands are always checked by the string fallback, not
|
|
57
|
+
# a lookup against itself.
|
|
58
|
+
def governance_attached?(domain)
|
|
59
|
+
domain.to_s == "Governance" || registry.hecksagon(domain)&.framework_members&.include?("Governance") || false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
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
|