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,500 @@
|
|
|
1
|
+
require_relative "../../bluebook/expression/evaluator"
|
|
2
|
+
require_relative "../../naming"
|
|
3
|
+
require_relative "../../rendering"
|
|
4
|
+
require_relative "../errors"
|
|
5
|
+
require_relative "../refusal_wording"
|
|
6
|
+
require_relative "invariant_violation"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Runtime
|
|
10
|
+
class Value
|
|
11
|
+
# The class-side engine: how a raw argument or stored field becomes a
|
|
12
|
+
# typed Value. Extended into Value, so every method here reads as
|
|
13
|
+
# `Value.for`, `Value.build`, … — `self` is the Value class.
|
|
14
|
+
module Coercion
|
|
15
|
+
# THE FOUR SHAPES AN ATTRIBUTE'S VALUE CAN TAKE — named here because
|
|
16
|
+
# `for_attribute` immediately below is the one place that actually
|
|
17
|
+
# branches on all four, and nowhere else in the language collects
|
|
18
|
+
# them into a single closed list. `Attribute#list?`/`#optional?`
|
|
19
|
+
# are real predicates on the IR node itself (bluebook/attribute.rb);
|
|
20
|
+
# `:scalar` and `:composite` are not named predicates there — they
|
|
21
|
+
# fall out of whether `aggregate.value_object(attribute.type)`
|
|
22
|
+
# resolves to something, read directly in the `coerced =` line
|
|
23
|
+
# below — but the branch is exactly as real, so it gets a name here
|
|
24
|
+
# too rather than staying anonymous.
|
|
25
|
+
#
|
|
26
|
+
# A SECOND RUNTIME'S KERNEL PORTS THIS METHOD BY HAND (rust/src/
|
|
27
|
+
# kernel/attribute_shapes/*.rs — one file per name in this array,
|
|
28
|
+
# generated into a Rust enum by bin/project_kernel_capabilities so
|
|
29
|
+
# every match over it is compiler-checked exhaustive). If a fifth
|
|
30
|
+
# branch is ever added to `for_attribute`, add its name here in the
|
|
31
|
+
# same breath — this list is that port's only source of truth for
|
|
32
|
+
# "which shapes exist," and a shape missing from it is a shape the
|
|
33
|
+
# generated Rust enum, and therefore the kernel, can never learn
|
|
34
|
+
# about no matter how correct the Ruby below is.
|
|
35
|
+
SHAPES = %i[scalar list optional composite].freeze
|
|
36
|
+
|
|
37
|
+
def for(aggregate, name, value)
|
|
38
|
+
attribute = aggregate.attribute(name)
|
|
39
|
+
return value unless attribute
|
|
40
|
+
|
|
41
|
+
for_attribute(aggregate, attribute, value)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The four `SHAPES` above, in the order this method actually checks
|
|
45
|
+
# them: `optional`/nil-passthrough first (a value that isn't there
|
|
46
|
+
# has no shape left to branch on), `list` second (a list of
|
|
47
|
+
# elements, hydrated as entities), then — inside `coerced =` —
|
|
48
|
+
# `composite` (the type names a declared value object, rebuilt
|
|
49
|
+
# recursively via `build`) with `scalar` as what's left once
|
|
50
|
+
# neither of those applies (the raw value, passed through
|
|
51
|
+
# unchanged).
|
|
52
|
+
def for_attribute(aggregate, attribute, value)
|
|
53
|
+
return value if attribute.nil? || value.nil? # :optional
|
|
54
|
+
return reference_list(attribute, value) if attribute.list? && attribute.reference?
|
|
55
|
+
return reference_identity(attribute, value) if attribute.reference?
|
|
56
|
+
return hydrate_entity_list(aggregate, attribute, value) if attribute.list? # :list
|
|
57
|
+
return value unless aggregate.respond_to?(:value_object)
|
|
58
|
+
|
|
59
|
+
# THE SET THE ATTRIBUTE NAMES IS CHECKED WHERE THE ATTRIBUTE IS KNOWN.
|
|
60
|
+
# `build` below sees only the value object, never which attribute asked
|
|
61
|
+
# for it, so a command argument's `admits:` has to be read here — this
|
|
62
|
+
# is the door every argument and every head field comes through.
|
|
63
|
+
#
|
|
64
|
+
# AFTER coercion, not before: a scalar arrives wrapped in whatever holder
|
|
65
|
+
# its type names (`{value: "append"}` for an OpName), and checking the
|
|
66
|
+
# raw payload would be checking the envelope.
|
|
67
|
+
coerced = value_object_for(aggregate, attribute.type)
|
|
68
|
+
.then do |value_object|
|
|
69
|
+
if value.is_a?(self) && value.type_name == value_object&.hecks_name
|
|
70
|
+
value
|
|
71
|
+
elsif value_object
|
|
72
|
+
build(value_object, fields_for(value_object, attribute.name, value), aggregate)
|
|
73
|
+
else
|
|
74
|
+
value
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
admit_declared_set(aggregate, attribute, coerced)
|
|
79
|
+
coerced
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Aggregate-local value objects remain authoritative, which permits
|
|
83
|
+
# intentional duplication. An ordinary fact may also name an identity
|
|
84
|
+
# value object declared on another aggregate; that shape is borrowed
|
|
85
|
+
# only when every chapter declaration with the name agrees.
|
|
86
|
+
def value_object_for(aggregate, type)
|
|
87
|
+
local = aggregate.value_object(type)
|
|
88
|
+
return local if local
|
|
89
|
+
|
|
90
|
+
chapter = aggregate.respond_to?(:hecks_owner) ? aggregate.hecks_owner : nil
|
|
91
|
+
return nil unless chapter.respond_to?(:aggregates)
|
|
92
|
+
|
|
93
|
+
matches = chapter.aggregates.filter_map { |candidate| candidate.value_object(type) }
|
|
94
|
+
shapes = matches.group_by do |shape|
|
|
95
|
+
shape.attributes.map { |field| [field.name, field.type.to_s, field.list?, field.optional?] }
|
|
96
|
+
end
|
|
97
|
+
shapes.size == 1 ? matches.first : nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Retained relationships store canonical target identities, not Ruby
|
|
101
|
+
# Value wrappers. Raw scalar IDs remain a compatibility input. A named
|
|
102
|
+
# identity VO omits its minted aggregate field at the command boundary;
|
|
103
|
+
# a bespoke compound VO may instead name the target heads directly.
|
|
104
|
+
# Neither form requires reverse-splitting a canonical ID.
|
|
105
|
+
def reference_identity(attribute, value)
|
|
106
|
+
return value unless value.is_a?(self) || value.is_a?(Hash)
|
|
107
|
+
|
|
108
|
+
target = attribute.type.resolve
|
|
109
|
+
return value unless target
|
|
110
|
+
|
|
111
|
+
materialized = materialize(value)
|
|
112
|
+
paths = target.identity_paths
|
|
113
|
+
return value if paths.empty?
|
|
114
|
+
|
|
115
|
+
direct_head = if value.is_a?(self) && target.identity_heads.one?
|
|
116
|
+
head = target.identity_heads.first
|
|
117
|
+
target.attribute(head)&.type.to_s == value.type_name ? head.to_s : nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
parts = paths.map do |path|
|
|
121
|
+
segments = path.to_s.split(".")
|
|
122
|
+
segments.shift if direct_head && segments.first == direct_head
|
|
123
|
+
segments.reduce(materialized) do |held, segment|
|
|
124
|
+
held.is_a?(Hash) ? (held[segment.to_sym] || held[segment]) : nil
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
return value if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
|
|
128
|
+
|
|
129
|
+
Naming.identity(parts)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def reference_list(attribute, value)
|
|
133
|
+
unless value.is_a?(Array)
|
|
134
|
+
raise TypeMismatch,
|
|
135
|
+
"#{attribute.name} is a has_many relationship — pass a list of identities"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
Freezer.deep(value.dup)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def fields_for(value_object, name, value)
|
|
142
|
+
return value.transform_keys(&:to_sym) if value.is_a?(Hash)
|
|
143
|
+
# Mutations may legitimately carry a value object into a differently
|
|
144
|
+
# named value-object slot with the same declared fields (for example,
|
|
145
|
+
# PositiveMoney into an Account's Money balance). Rebuild the target
|
|
146
|
+
# type from its state; callers at the public boundary still have to
|
|
147
|
+
# supply an object rather than a scalar.
|
|
148
|
+
return value.to_h if value.is_a?(self)
|
|
149
|
+
|
|
150
|
+
# Vendored addition, not (yet) upstream hecks (migration
|
|
151
|
+
# plan task 5): a bare scalar auto-wraps into a single-field
|
|
152
|
+
# value object's sole attribute -- the SAME shape
|
|
153
|
+
# #from_identifier already establishes for identity coercion
|
|
154
|
+
# (`build(value_object, { fields.first.name => identifier }) if
|
|
155
|
+
# fields.size == 1`), made consistent here for MUTATION
|
|
156
|
+
# coercion too. Real, corpus-wide gap: a synthesised single-
|
|
157
|
+
# field wrapper (Part 3a's bare-primitive auto-synthesis, the
|
|
158
|
+
# norm for a VO-typed aggregate field) is exactly the shape
|
|
159
|
+
# #rewrap_arithmetic_result hands back a raw scalar RESULT to
|
|
160
|
+
# -- without this, every phantom-field increment/multiply on a
|
|
161
|
+
# single-field-wrapped attribute refused with "pass its fields
|
|
162
|
+
# as an object, not <scalar>" the instant it tried to re-wrap
|
|
163
|
+
# its own correctly-computed result. Multi-field VOs still
|
|
164
|
+
# refuse below, unchanged -- only the genuinely unambiguous
|
|
165
|
+
# single-field case auto-wraps, matching from_identifier's own
|
|
166
|
+
# precedent exactly.
|
|
167
|
+
if value_object.attributes.size == 1
|
|
168
|
+
return { value_object.attributes.first.name => value }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
raise TypeMismatch,
|
|
172
|
+
RefusalWording.render("TypeMismatch", "value_object_shape",
|
|
173
|
+
name: name, type: value_object.hecks_name,
|
|
174
|
+
offered: Rendering.describe(value))
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# `build`'s own recursive twin of `for_attribute`'s single-level
|
|
178
|
+
# normalization — a value object's OWN composite-typed fields
|
|
179
|
+
# (`Pizza.price_cents`, a `Price`) never otherwise pass back
|
|
180
|
+
# through `fields_for`, so a bare scalar or partial Hash for one
|
|
181
|
+
# of THOSE sails past the outer VO's own shape check (`Pizza`
|
|
182
|
+
# itself has two fields, so nothing unwraps there) and lands
|
|
183
|
+
# stored one field down exactly as handed in — found live: once
|
|
184
|
+
# the fuzzer actually generated the bare-scalar shape
|
|
185
|
+
# `fields_for` has accepted at the TOP level since 86727afd, a
|
|
186
|
+
# nested `Price` stored as a raw Integer broke every later
|
|
187
|
+
# dotted-path read (`pizza.price_cents.cents`) expecting one
|
|
188
|
+
# more level of Hash.
|
|
189
|
+
#
|
|
190
|
+
# Stays a plain Hash, never a nested `Value` — `Value#with`'s own
|
|
191
|
+
# header and `materialize_unwrapped`'s comment already depend on
|
|
192
|
+
# a value-object-typed field of ANOTHER value object staying a
|
|
193
|
+
# plain Hash once stored, and this does not change that; it only
|
|
194
|
+
# makes sure that Hash has the shape its own type declares.
|
|
195
|
+
# `aggregate` is the one thing `build` didn't used to need — a
|
|
196
|
+
# nested type can only be resolved through `aggregate.
|
|
197
|
+
# value_object(name)`, so callers with no aggregate in reach
|
|
198
|
+
# (`Value#with`, always re-setting an already-scalar arithmetic
|
|
199
|
+
# field) simply skip this and keep their prior behavior.
|
|
200
|
+
def normalize_composite_fields(aggregate, value_object, fields)
|
|
201
|
+
return fields unless aggregate&.respond_to?(:value_object)
|
|
202
|
+
|
|
203
|
+
value_object.attributes.each do |attribute|
|
|
204
|
+
next if attribute.list? || !fields.key?(attribute.name)
|
|
205
|
+
|
|
206
|
+
raw = fields[attribute.name]
|
|
207
|
+
next if raw.nil? || raw.is_a?(self)
|
|
208
|
+
|
|
209
|
+
nested = value_object_for(aggregate, attribute.type)
|
|
210
|
+
next unless nested
|
|
211
|
+
|
|
212
|
+
fields[attribute.name] = normalize_composite_fields(aggregate, nested, fields_for(nested, attribute.name, raw))
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
fields
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def build(value_object, fields, aggregate = nil)
|
|
219
|
+
fields = value_object.attributes.each_with_object(fields.transform_keys(&:to_sym)) do |attribute, completed|
|
|
220
|
+
completed[attribute.name] = attribute.default unless completed.key?(attribute.name) || attribute.default.nil?
|
|
221
|
+
end
|
|
222
|
+
fields = normalize_composite_fields(aggregate, value_object, fields)
|
|
223
|
+
admit_member(value_object, fields)
|
|
224
|
+
check_admitted(value_object, fields)
|
|
225
|
+
check_numeric_fields(value_object, fields)
|
|
226
|
+
check_scalar_shapes(value_object, fields)
|
|
227
|
+
check_patterns(value_object, fields)
|
|
228
|
+
value_object.invariants.each do |invariant|
|
|
229
|
+
next if Bluebook::Expression::Evaluator.call(invariant.canonical, fields)
|
|
230
|
+
|
|
231
|
+
raise InvariantViolation,
|
|
232
|
+
RefusalWording.render("InvariantViolation", "value_object_invariant",
|
|
233
|
+
name: value_object.hecks_name, description: invariant.description,
|
|
234
|
+
offered: canonical_fields(fields))
|
|
235
|
+
end
|
|
236
|
+
new(value_object, fields)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def hydrate(aggregate, state)
|
|
240
|
+
state.each_with_object({}) do |(name, value), hydrated|
|
|
241
|
+
key = name.to_sym
|
|
242
|
+
attribute = aggregate.attribute(key)
|
|
243
|
+
hydrated[key] = attribute ? for_attribute(aggregate, attribute, value) : value
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# S17, ADR 0026 — SEARCHES THE WHOLE ENTITY TREE, not only the
|
|
248
|
+
# root's own direct children. `aggregate` here is always the
|
|
249
|
+
# ROOT aggregate — `for_attribute`'s own `aggregate` argument is
|
|
250
|
+
# never reassigned as hydration recurses into a nested element,
|
|
251
|
+
# because coercion has to resolve value objects, and only the
|
|
252
|
+
# root answers `.value_object` at all (Entity's own header
|
|
253
|
+
# comment: an entity must NOT answer to it, or `Value.
|
|
254
|
+
# for_attribute` could no longer tell a piece from a head). So
|
|
255
|
+
# a NESTED entity — Dispatch, inside Handler — is not a direct
|
|
256
|
+
# child of the root the way Handler itself is, and a plain
|
|
257
|
+
# `aggregate.entities.find` stops one level short of it.
|
|
258
|
+
def find_entity(construct, name)
|
|
259
|
+
construct.entities.each do |candidate|
|
|
260
|
+
return candidate if candidate.hecks_name == name
|
|
261
|
+
|
|
262
|
+
found = find_entity(candidate, name)
|
|
263
|
+
return found if found
|
|
264
|
+
end
|
|
265
|
+
nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Frozen through: a list read back out of the store is an answer,
|
|
269
|
+
# not a handle on what is stored.
|
|
270
|
+
def hydrate_entity_list(aggregate, attribute, value)
|
|
271
|
+
entity = find_entity(aggregate, attribute.type.to_s)
|
|
272
|
+
return value unless entity
|
|
273
|
+
|
|
274
|
+
Array(value).map do |element|
|
|
275
|
+
next element unless element.is_a?(Hash)
|
|
276
|
+
|
|
277
|
+
element.each_with_object({}) do |(name, field_value), hydrated|
|
|
278
|
+
key = name.to_sym
|
|
279
|
+
field = entity.attribute(key)
|
|
280
|
+
hydrated[key] = field ? for_attribute(aggregate, field, field_value) : field_value
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
.then { |hydrated| Freezer.deep(hydrated) }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# `Value.identifier` used to live here: hand it a one-field value object
|
|
287
|
+
# and it opened it, so `identified_by :number` could pass for an identity
|
|
288
|
+
# and the runtime would guess which field was meant. THAT GUESS IS GONE.
|
|
289
|
+
# An identity names its field — `identified_by :number` — and the
|
|
290
|
+
# path is what reaches the scalar. A declaration that names no field is
|
|
291
|
+
# refused when the bluebook loads, so nothing has to be unwrapped later.
|
|
292
|
+
#
|
|
293
|
+
# `scalar` below is a different job and stays: rendering a value object
|
|
294
|
+
# into a column or a message, where there is no path to consult.
|
|
295
|
+
|
|
296
|
+
# `Value.reference_id` lived here, opening a reference to find the id
|
|
297
|
+
# inside it. A reference IS the id now — refused at the payload gate if it
|
|
298
|
+
# arrives as anything else — so there is nothing left to open. The comment
|
|
299
|
+
# it carried said retiring it meant changing how references are STORED ;
|
|
300
|
+
# that is what happened.
|
|
301
|
+
|
|
302
|
+
# A REFERENCE IS AN ID, SO AN OBJECT IS NOT ONE.
|
|
303
|
+
#
|
|
304
|
+
# Nothing coerces a reference — `for_attribute` misses on
|
|
305
|
+
# "Reference<Account>", which is no value object's name, and hands the
|
|
306
|
+
# argument straight through. That is why the wrapped form went in
|
|
307
|
+
# unnoticed for as long as it did: there was no place it could be
|
|
308
|
+
# refused, so whatever the first caller wrote became the shape.
|
|
309
|
+
#
|
|
310
|
+
# This is that place. It sits at the payload gate rather than inside
|
|
311
|
+
# coercion because the sentence names the COMMAND, and `for_attribute`
|
|
312
|
+
# never learns which command it is serving.
|
|
313
|
+
#
|
|
314
|
+
# An Array is deliberately not refused here. A reference is never a list
|
|
315
|
+
# today, and inventing a rule for a shape the language cannot declare is
|
|
316
|
+
# how decoration gets written.
|
|
317
|
+
def refuse_object_reference(command, attribute, value)
|
|
318
|
+
return unless attribute.reference?
|
|
319
|
+
|
|
320
|
+
offered = attribute.list? ? Array(value).find { |item| item.is_a?(Hash) || item.is_a?(self) } : value
|
|
321
|
+
return unless offered.is_a?(Hash) || offered.is_a?(self)
|
|
322
|
+
|
|
323
|
+
raise TypeMismatch,
|
|
324
|
+
RefusalWording.render("TypeMismatch", "reference_as_object",
|
|
325
|
+
command: command.hecks_name, attribute: attribute.name,
|
|
326
|
+
known_by: known_by(attribute))
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# "(Account is known by number)" — what to send instead. No article, on
|
|
330
|
+
# purpose: "an Account" and "a Customer" differ by the target's first
|
|
331
|
+
# letter, and a refusal pinned byte-for-byte should not hinge on an
|
|
332
|
+
# article-choosing rule. Silent when the target is another chapter's,
|
|
333
|
+
# where this runtime cannot see what it is known by.
|
|
334
|
+
#
|
|
335
|
+
# EVERY HEAD, because a caller has to pass every one. This read
|
|
336
|
+
# `identified_by`, which is the SINGLE head and is nil the moment an
|
|
337
|
+
# identity has two parts — so a composite target fell through the guard
|
|
338
|
+
# and the refusal went silent exactly where it had the most to say. A
|
|
339
|
+
# single-path target reads as it always did.
|
|
340
|
+
def known_by(attribute)
|
|
341
|
+
heads = Array(attribute.type.resolve&.identity_heads)
|
|
342
|
+
return "" if heads.empty?
|
|
343
|
+
|
|
344
|
+
" (#{attribute.type.target_name} is known by #{heads.join(', ')})"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def scalar(value)
|
|
348
|
+
return value unless value.is_a?(self)
|
|
349
|
+
|
|
350
|
+
fields = value.to_h
|
|
351
|
+
return fields.values.first if fields.size == 1
|
|
352
|
+
|
|
353
|
+
raise TypeMismatch, RefusalWording.render("TypeMismatch", "multi_field_scalar", type: value.type_name)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def from_identifier(aggregate, attribute, identifier)
|
|
357
|
+
value_object = value_object_for(aggregate, attribute.type)
|
|
358
|
+
return identifier unless value_object
|
|
359
|
+
|
|
360
|
+
fields = value_object.attributes
|
|
361
|
+
if fields.size == 1
|
|
362
|
+
field = fields.first
|
|
363
|
+
return build(value_object, { field.name => coerce_identifier(field, identifier) })
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
raise TypeMismatch, RefusalWording.render("TypeMismatch", "composite_identity", type: value_object.hecks_name)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Vendored fix, not (yet) upstream hecks (migration plan
|
|
370
|
+
# task 9): `identifier` here is always the DERIVED IDENTITY
|
|
371
|
+
# STRING -- `Identity.of`/`Identity.from` intentionally return
|
|
372
|
+
# one (correct for naming a repository key), and
|
|
373
|
+
# `Runtime::Instance#materialize_identity!` calls `from_identifier`
|
|
374
|
+
# with exactly that string on every fresh hydration -- but when
|
|
375
|
+
# the identity field's OWN declared type is Integer/Float (not
|
|
376
|
+
# the overwhelmingly common String), seeding it straight from
|
|
377
|
+
# that string round-trips a correctly-derived identity back in
|
|
378
|
+
# as the WRONG Ruby type -- and #build's own
|
|
379
|
+
# `check_numeric_fields` (added specifically to catch a genuine
|
|
380
|
+
# CALLER mismatch) then refused the runtime's OWN internal
|
|
381
|
+
# identity seed instead, on every dispatch, valid input or not.
|
|
382
|
+
#
|
|
383
|
+
# Reuses THIS SAME FILE's own `NUMERIC` table (declared-type ->
|
|
384
|
+
# expected-Ruby-class, already read by `check_numeric_fields`)
|
|
385
|
+
# to decide WHICH declared types need converting, and
|
|
386
|
+
# Kernel#Integer/#Float to do the converting. A genuinely
|
|
387
|
+
# malformed identifier (should never happen, since an identity
|
|
388
|
+
# is always derived FROM a correctly-typed field in the first
|
|
389
|
+
# place, but this stays defensive rather than assume it) passes
|
|
390
|
+
# back unconverted, and `check_numeric_fields` refuses it
|
|
391
|
+
# exactly as it always has -- preserving its real job of
|
|
392
|
+
# catching a genuine caller mismatch, not just this migration's
|
|
393
|
+
# own runtime-internal one.
|
|
394
|
+
private def coerce_identifier(field, identifier)
|
|
395
|
+
return identifier unless identifier.is_a?(String) && NUMERIC.key?(field.type.to_s)
|
|
396
|
+
|
|
397
|
+
case field.type.to_s
|
|
398
|
+
when "Integer" then Integer(identifier)
|
|
399
|
+
when "Float" then Float(identifier)
|
|
400
|
+
else identifier
|
|
401
|
+
end
|
|
402
|
+
rescue ArgumentError
|
|
403
|
+
identifier
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def canonical_fields(fields)
|
|
407
|
+
JSON.generate(fields.sort_by { |name, _| name.to_s }.to_h)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# A field declared Integer or Float must ARRIVE as one.
|
|
411
|
+
#
|
|
412
|
+
# Without this a String sails into a numeric field and the failure surfaces
|
|
413
|
+
# later, inside a predicate, as `positive? expects a number, got "three"` —
|
|
414
|
+
# an EvaluationError, which is NOT a domain refusal. So the runtime broke
|
|
415
|
+
# where the domain should have said no, and the run contract recorded the
|
|
416
|
+
# crash beside genuine refusals as though the domain had judged it.
|
|
417
|
+
#
|
|
418
|
+
# Checked BEFORE invariants, because an invariant reading a mistyped field
|
|
419
|
+
# is exactly the thing that used to explode.
|
|
420
|
+
NUMERIC = { "Integer" => Integer, "Float" => Numeric }.freeze
|
|
421
|
+
private def check_numeric_fields(value_object, fields)
|
|
422
|
+
value_object.attributes.each do |attribute|
|
|
423
|
+
expected = NUMERIC[attribute.type.to_s]
|
|
424
|
+
next unless expected
|
|
425
|
+
|
|
426
|
+
given = fields[attribute.name]
|
|
427
|
+
next if given.nil? || given.is_a?(expected)
|
|
428
|
+
|
|
429
|
+
raise TypeMismatch,
|
|
430
|
+
RefusalWording.render("TypeMismatch", "numeric_field",
|
|
431
|
+
type: value_object.hecks_name, field: attribute.name,
|
|
432
|
+
expected: attribute.type, offered: Rendering.describe(given))
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# A field declared `String` (or a boolean) must not arrive as a
|
|
437
|
+
# COMPOSITE — an Array or a Hash (or a nested Value) standing in for
|
|
438
|
+
# what has to be a leaf scalar.
|
|
439
|
+
#
|
|
440
|
+
# Deliberately laxer than `check_numeric_fields` above : it does not
|
|
441
|
+
# enforce the exact Ruby class, only that the shape isn't a collection.
|
|
442
|
+
# `Judge#v` — the language's own self-hosted grammar validation —
|
|
443
|
+
# hands a String-typed field (`Normalise`'s `position`, a `RuleText`)
|
|
444
|
+
# a raw Integer walk-index on purpose, on every boot, and that has
|
|
445
|
+
# always been tolerated ; a full String-vs-Integer check here would
|
|
446
|
+
# refuse the runtime's own bootstrap. But no scalar field, of any
|
|
447
|
+
# declared type, can ever legitimately be handed an Array or a Hash —
|
|
448
|
+
# that shape is always wrong, and always was: `InvalidValueGenerator#
|
|
449
|
+
# array_for_scalar`'s own corruption is deliberately built to be
|
|
450
|
+
# REFUSED (see that file's header), and until this check existed it
|
|
451
|
+
# sailed straight through for a String/boolean field the way it never
|
|
452
|
+
# could for an Integer/Float one (`check_numeric_fields` above already
|
|
453
|
+
# catches an Array offered for those). Found live via bin/fuzz, seed
|
|
454
|
+
# 17 on the fixtures domain : an Array standing in for a single-field
|
|
455
|
+
# identity's declared `String`, `.to_s`'d into a record id downstream.
|
|
456
|
+
COMPOSITE_SHAPES = [Array, ::Hash].freeze
|
|
457
|
+
NON_NUMERIC_SCALARS = %w[String TrueClass FalseClass].freeze
|
|
458
|
+
private def check_scalar_shapes(value_object, fields)
|
|
459
|
+
value_object.attributes.each do |attribute|
|
|
460
|
+
next unless NON_NUMERIC_SCALARS.include?(attribute.type.to_s)
|
|
461
|
+
|
|
462
|
+
given = fields[attribute.name]
|
|
463
|
+
next if given.nil? || COMPOSITE_SHAPES.none? { |shape| given.is_a?(shape) }
|
|
464
|
+
|
|
465
|
+
raise TypeMismatch,
|
|
466
|
+
RefusalWording.render("TypeMismatch", "numeric_field",
|
|
467
|
+
type: value_object.hecks_name, field: attribute.name,
|
|
468
|
+
expected: attribute.type, offered: Rendering.describe(given))
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# A field declared with a PATTERN must match it.
|
|
473
|
+
#
|
|
474
|
+
# Beside check_numeric_fields and for the same reason : a value that does
|
|
475
|
+
# not look like what it claims to be is the DOMAIN saying no, and it should
|
|
476
|
+
# say so here rather than let the wrong shape travel on and surface as a
|
|
477
|
+
# broken predicate later.
|
|
478
|
+
#
|
|
479
|
+
# Which regexes may be written at all is PatternSubset's job, enforced when
|
|
480
|
+
# the bluebook is declared — so by the time a value arrives here the pattern
|
|
481
|
+
# is already a vetted, unambiguous one, and this is a plain match.
|
|
482
|
+
private def check_patterns(value_object, fields)
|
|
483
|
+
value_object.attributes.each do |attribute|
|
|
484
|
+
pattern = attribute.pattern
|
|
485
|
+
next unless pattern
|
|
486
|
+
|
|
487
|
+
given = fields[attribute.name]
|
|
488
|
+
next if given.nil?
|
|
489
|
+
next if given.is_a?(String) && Regexp.new(pattern).match?(given)
|
|
490
|
+
|
|
491
|
+
raise TypeMismatch,
|
|
492
|
+
RefusalWording.render("TypeMismatch", "pattern_mismatch",
|
|
493
|
+
type: value_object.hecks_name, field: attribute.name,
|
|
494
|
+
pattern: pattern, offered: Rendering.describe(given))
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "../rendering"
|
|
3
|
+
require_relative "value/invariant_violation"
|
|
4
|
+
require_relative "value/coercion"
|
|
5
|
+
require_relative "value/admission"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Runtime
|
|
9
|
+
# A typed value object in hand: frozen fields, read by name. How one is
|
|
10
|
+
# MADE — coerced from a raw argument, checked against its declared
|
|
11
|
+
# numeric types, patterns and closed sets — is the class-side engine in
|
|
12
|
+
# value/coercion.rb and value/admission.rb, extended here so the door
|
|
13
|
+
# stays where it always was: `Value.for`, `Value.build`.
|
|
14
|
+
class Value
|
|
15
|
+
extend Coercion
|
|
16
|
+
extend Admission
|
|
17
|
+
|
|
18
|
+
attr_reader :value_object
|
|
19
|
+
|
|
20
|
+
# FROZEN THROUGH, not just on top.
|
|
21
|
+
#
|
|
22
|
+
# `@fields.freeze` alone stops a key being added or removed and
|
|
23
|
+
# nothing else: the String, Array or Hash a field HOLDS stays
|
|
24
|
+
# mutable, so `vo[:value] << "!"` edits a value object in place —
|
|
25
|
+
# demonstrated on a real dispatch before this was written, not
|
|
26
|
+
# supposed. Same shape as the three freezing bugs already fixed
|
|
27
|
+
# (list attributes, the event log, query rows), where the container
|
|
28
|
+
# was frozen and the contents were not.
|
|
29
|
+
#
|
|
30
|
+
# A value object is the one thing in the domain that has no
|
|
31
|
+
# identity to change over — `with` already answers a NEW one rather
|
|
32
|
+
# than mutating — so freezing it through is what it always claimed
|
|
33
|
+
# to be.
|
|
34
|
+
def initialize(value_object, fields)
|
|
35
|
+
@value_object = value_object
|
|
36
|
+
@fields = Freezer.deep(fields.transform_keys(&:to_sym))
|
|
37
|
+
freeze
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def type_name = @value_object.hecks_name
|
|
41
|
+
def [](field) = @fields[field.to_sym]
|
|
42
|
+
def key?(field) = @fields.key?(field.to_sym)
|
|
43
|
+
def to_h = @fields.transform_values { |value| self.class.materialize(value) }
|
|
44
|
+
def to_json(*) = JSON.generate(to_h)
|
|
45
|
+
|
|
46
|
+
def ==(other)
|
|
47
|
+
other.is_a?(self.class) && other.type_name == type_name && other.to_h == to_h
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def with(field, value)
|
|
51
|
+
self.class.build(@value_object, @fields.merge(field.to_sym => value))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.materialize(value)
|
|
55
|
+
case value
|
|
56
|
+
when self then value.to_h
|
|
57
|
+
when Array then value.map { |item| materialize(item) }
|
|
58
|
+
when Hash then value.transform_values { |item| materialize(item) }
|
|
59
|
+
else value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# `materialize`, but a single-attribute value object (`sole_attribute`
|
|
64
|
+
# — [[feedback_name_the_scalar_field]]) recurses into its own bare
|
|
65
|
+
# field instead of building `{field: ...}` — a Board's own `label`
|
|
66
|
+
# unwraps to `"Kanban"`, not `{value: "Kanban"}`. NOT a replacement
|
|
67
|
+
# for `materialize` itself: every existing report/query/command
|
|
68
|
+
# caller keeps the wrapped shape it already depends on (Banking's
|
|
69
|
+
# own `CustomerPortfolio` reads `payment[:amount][:cents]`, and
|
|
70
|
+
# changing that out from under it would be a real breaking change,
|
|
71
|
+
# not a bug fix). This is read_model_interpreter.rb's own opt-in,
|
|
72
|
+
# used ONLY for a `group_by`-declared head's own rows — grouping
|
|
73
|
+
# needs a real scalar to key by regardless, so a report already
|
|
74
|
+
# asking for that gets the unwrap for free.
|
|
75
|
+
def self.materialize_unwrapped(value)
|
|
76
|
+
case value
|
|
77
|
+
when self
|
|
78
|
+
sole = value.value_object.sole_attribute
|
|
79
|
+
return materialize_unwrapped(value[sole.name]) if sole
|
|
80
|
+
|
|
81
|
+
# NOT `value.to_h` — `Value#to_h` materializes each field through
|
|
82
|
+
# plain `materialize`, so a VO nested inside a multi-attribute VO
|
|
83
|
+
# would already be a plain Hash by the time this method ever saw
|
|
84
|
+
# it, and never reach the `when self` branch above. Read each
|
|
85
|
+
# field straight off `value` instead, so recursion actually
|
|
86
|
+
# happens through THIS method the whole way down.
|
|
87
|
+
value.value_object.attributes.to_h { |attr| [attr.name, materialize_unwrapped(value[attr.name])] }
|
|
88
|
+
when Array then value.map { |item| materialize_unwrapped(item) }
|
|
89
|
+
when Hash then value.transform_values { |item| materialize_unwrapped(item) }
|
|
90
|
+
else value
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# REDUCES AN APPEND-ONLY SUB-LOG TO ITS CURRENT STATE — the same
|
|
95
|
+
# "a later fact supersedes an earlier one" reduction this runtime
|
|
96
|
+
# already performs replaying an AGGREGATE's own command history
|
|
97
|
+
# into its current attributes, applied here to a single `list_of`
|
|
98
|
+
# FIELD acting as its own miniature append-only log (a placement
|
|
99
|
+
# history, a tombstone-style soft-delete list, a versioned
|
|
100
|
+
# setting). `rows` is what a `list_of` attribute hands back — an
|
|
101
|
+
# Array of `Value`, in append order — and `key` names the field
|
|
102
|
+
# that identifies "the same logical thing" across entries.
|
|
103
|
+
#
|
|
104
|
+
# GROUPING ONLY, NEVER INTERPRETATION. What counts as "removed,"
|
|
105
|
+
# how to order what survives — that meaning belongs to whichever
|
|
106
|
+
# domain declared the field, never here: a generic reduction that
|
|
107
|
+
# started guessing domain semantics would need to keep guessing
|
|
108
|
+
# forever, once per shape of "gone" any caller ever invents. The
|
|
109
|
+
# caller filters and sorts the result; this only groups it.
|
|
110
|
+
def self.latest_by(rows, key)
|
|
111
|
+
rows.each_with_object({}) { |row, latest| latest[row.public_send(key)] = row }.values
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def method_missing(name, *args)
|
|
115
|
+
return @fields[name] if @fields.key?(name)
|
|
116
|
+
|
|
117
|
+
super
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def respond_to_missing?(name, include_private = false)
|
|
121
|
+
@fields.key?(name) || super
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|