hecks 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +51 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +301 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -20
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +124 -0
- data/lib/hecks/fuzzing/form_census.rb +199 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
- data/lib/hecks/fuzzing/properties/guards.rb +129 -26
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
- data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +84 -39
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +224 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +676 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +39 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
- data/lib/hecks/fuzzing/value_generator.rb +66 -14
- data/lib/hecks/fuzzing.rb +11 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +89 -21
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +250 -0
- data/lib/hecks/projections/glossary/markdown.rb +105 -0
- data/lib/hecks/projections/glossary/mermaid.rb +110 -0
- data/lib/hecks/projections/glossary/page.css +271 -0
- data/lib/hecks/projections/glossary/page.js +72 -0
- data/lib/hecks/projections/glossary/sections.rb +17 -0
- data/lib/hecks/projections/glossary/sentences.rb +205 -0
- data/lib/hecks/projections/glossary.rb +214 -286
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +15 -22
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +132 -24
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +94 -19
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +282 -46
- data/lib/hecks/runtime/entity_interpreter.rb +160 -69
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +111 -56
- data/lib/hecks/runtime/reaction_invocation.rb +76 -9
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -112
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +56 -27
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -88
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +93 -56
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +379 -226
- data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
- data/lib/hecks/runtime/value.rb +28 -23
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +207 -5
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +43 -2
|
@@ -12,7 +12,7 @@ module Hecks
|
|
|
12
12
|
# typed Value. Extended into Value, so every method here reads as
|
|
13
13
|
# `Value.for`, `Value.build`, … — `self` is the Value class.
|
|
14
14
|
module Coercion
|
|
15
|
-
#
|
|
15
|
+
# The four SHAPES an attribute's value can take — named here because
|
|
16
16
|
# `for_attribute` immediately below is the one place that actually
|
|
17
17
|
# branches on all four, and nowhere else in the language collects
|
|
18
18
|
# them into a single closed list. `Attribute#list?`/`#optional?`
|
|
@@ -23,7 +23,7 @@ module Hecks
|
|
|
23
23
|
# below — but the branch is exactly as real, so it gets a name here
|
|
24
24
|
# too rather than staying anonymous.
|
|
25
25
|
#
|
|
26
|
-
# A
|
|
26
|
+
# A second runtime's kernel ports this method by hand (rust/src/
|
|
27
27
|
# kernel/attribute_shapes/*.rs — one file per name in this array,
|
|
28
28
|
# generated into a Rust enum by bin/project_kernel_capabilities so
|
|
29
29
|
# every match over it is compiler-checked exhaustive). If a fifth
|
|
@@ -49,19 +49,28 @@ module Hecks
|
|
|
49
49
|
# recursively via `build`) with `scalar` as what's left once
|
|
50
50
|
# neither of those applies (the raw value, passed through
|
|
51
51
|
# unchanged).
|
|
52
|
-
# `boundary: false` is the
|
|
52
|
+
# `boundary: false` is the query door (`QueryInterpreter#normalize_args`):
|
|
53
53
|
# a query attribute's declared type names the argument for callers and
|
|
54
54
|
# generators, never a runtime shape — comparison unwraps both sides
|
|
55
55
|
# itself, so a `reference: {value: ...}` offered against a `String`
|
|
56
56
|
# query field is the documented allowance (see banking's own
|
|
57
57
|
# `Account.OpenForCustomer`), not a C3.8 mismatch.
|
|
58
|
-
# `argument: true` is the
|
|
59
|
-
#
|
|
60
|
-
# for a non-optional attribute is
|
|
61
|
-
# argument empty (C3.7)
|
|
58
|
+
# `argument: true` is the command/entity/port argument door only
|
|
59
|
+
# (`Interpreting#normalize_args`, every command/entity/port
|
|
60
|
+
# dispatch): the one place a nil for a non-optional attribute is
|
|
61
|
+
# the caller leaving a required argument empty (C3.7), absorbed
|
|
62
|
+
# via the type's own field defaults when every field has one
|
|
63
|
+
# (`nil_argument` below). Every other caller — `sets` copying an
|
|
62
64
|
# optional argument into state, hydration, entity elements,
|
|
63
|
-
# identity, defaults — is state assembly, where nil is a
|
|
64
|
-
# "absent is not empty" value the aggregate's own
|
|
65
|
+
# identity, defaults — is state assembly, where nil is a
|
|
66
|
+
# legitimate "absent is not empty" value the aggregate's own
|
|
67
|
+
# attribute may hold. `QueryInterpreter#normalize_args` never
|
|
68
|
+
# passes `argument: true` — a null required value-object-typed
|
|
69
|
+
# query argument is checked, and refused, entirely on its own
|
|
70
|
+
# side (`null_vo_argument!`, query_interpreter.rb) precisely so
|
|
71
|
+
# it does not reach this default-absorbing fallback (QualityControl
|
|
72
|
+
# BUG#36 — a query's own null VO argument must refuse regardless
|
|
73
|
+
# of any default, unlike a command's).
|
|
65
74
|
def for_attribute(aggregate, attribute, value, boundary: true, argument: false)
|
|
66
75
|
return nil_or_missing(aggregate, attribute, value, argument) if attribute.nil? || value.nil?
|
|
67
76
|
return reference_list(attribute, value) if attribute.list? && attribute.reference?
|
|
@@ -69,12 +78,12 @@ module Hecks
|
|
|
69
78
|
return hydrate_entity_list(aggregate, attribute, value) if attribute.list? # :list
|
|
70
79
|
return value unless aggregate.respond_to?(:value_object)
|
|
71
80
|
|
|
72
|
-
#
|
|
81
|
+
# The set the attribute names is checked where the attribute is known.
|
|
73
82
|
# `build` below sees only the value object, never which attribute asked
|
|
74
83
|
# for it, so a command argument's `admits:` has to be read here — this
|
|
75
84
|
# is the door every argument and every head field comes through.
|
|
76
85
|
#
|
|
77
|
-
#
|
|
86
|
+
# After coercion, not before: a scalar arrives wrapped in whatever holder
|
|
78
87
|
# its type names (`{value: "append"}` for an OpName), and checking the
|
|
79
88
|
# raw payload would be checking the envelope.
|
|
80
89
|
value_object = value_object_for(aggregate, attribute.type)
|
|
@@ -90,12 +99,12 @@ module Hecks
|
|
|
90
99
|
coerced
|
|
91
100
|
end
|
|
92
101
|
|
|
93
|
-
# NIL
|
|
102
|
+
# NIL is not a value for a non-optional argument (C3.7/C3.8). An
|
|
94
103
|
# `optional:` attribute and a load from the store pass nil through
|
|
95
104
|
# as they always did; a command argument offered as null for a
|
|
96
105
|
# required attribute is
|
|
97
106
|
# refused as the field the caller left empty — a value object is
|
|
98
|
-
#
|
|
107
|
+
# built from no fields, so its first required field refuses with
|
|
99
108
|
# exactly the wording the Rust side's `from_json` gives it
|
|
100
109
|
# ("Money.cents expects Integer, got nil"), and a bare scalar
|
|
101
110
|
# refuses through `check_bare_primitive`'s own wording. Lists and
|
|
@@ -103,7 +112,7 @@ module Hecks
|
|
|
103
112
|
# relationship, `validate_relationship_cardinality`'s business).
|
|
104
113
|
# The `attribute.nil?`/`value.nil?` branch of `for_attribute`,
|
|
105
114
|
# pulled out on its own — an unknown attribute has no shape left
|
|
106
|
-
# to branch on, and a nil
|
|
115
|
+
# to branch on, and a nil value is either an ordinary absence
|
|
107
116
|
# (state assembly, hydration, a query ask) or, at the argument
|
|
108
117
|
# door only, `nil_argument`'s own C3.7 refusal.
|
|
109
118
|
private def nil_or_missing(aggregate, attribute, value, argument)
|
|
@@ -120,12 +129,12 @@ module Hecks
|
|
|
120
129
|
return build(value_object, {}, aggregate) if value_object
|
|
121
130
|
|
|
122
131
|
raise TypeMismatch,
|
|
123
|
-
RefusalWording.
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
RefusalWording.render_site("TypeMismatch", "numeric_field",
|
|
133
|
+
type: aggregate.hecks_name, field: attribute.name,
|
|
134
|
+
expected: attribute.type, offered: "nil")
|
|
126
135
|
end
|
|
127
136
|
|
|
128
|
-
# A
|
|
137
|
+
# A bare primitive is type-checked at the boundary too (C3.8,
|
|
129
138
|
# docs/semantics/bluebook-semantics.md) — the same two predicates a
|
|
130
139
|
# value object's own fields get, so wrong-typed caller input is a
|
|
131
140
|
# TypeMismatch refusal here, never an evaluation fault later. Its
|
|
@@ -171,11 +180,51 @@ module Hecks
|
|
|
171
180
|
|
|
172
181
|
direct_head = direct_identity_head(value, target)
|
|
173
182
|
parts = paths.map { |path| identity_part(materialized, path, direct_head) }
|
|
174
|
-
|
|
183
|
+
if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
|
|
184
|
+
return sole_scalar_identity(value, paths) || value
|
|
185
|
+
end
|
|
175
186
|
|
|
176
187
|
Naming.identity(parts)
|
|
177
188
|
end
|
|
178
189
|
|
|
190
|
+
# A command may redeclare a `reference_to` field under its own,
|
|
191
|
+
# differently-named single-attribute value object (`attribute
|
|
192
|
+
# :venue, VenueHandle; sets :venue`, not `reference_to Venue` —
|
|
193
|
+
# QualityControl BUG#121, `qa/stress_domains/generated_revalued_
|
|
194
|
+
# shape`'s own `Hangar.Repoint`) instead of naming the target's
|
|
195
|
+
# own identity field(s) directly. `identity_part`'s path walk
|
|
196
|
+
# above only ever matches an incoming shape that already uses the
|
|
197
|
+
# target's own field names (or is the target's own identity value
|
|
198
|
+
# object, `direct_identity_head`) — an ad hoc wrapper around a
|
|
199
|
+
# bare scalar under some other field name (`VenueHandle`'s own
|
|
200
|
+
# `value`, not `Venue`'s own `code`) fails every path lookup, and
|
|
201
|
+
# used to fall through to the unresolved `return value` above,
|
|
202
|
+
# storing the wrapped Value. That leaves one reference field on
|
|
203
|
+
# one aggregate holding two different shapes depending on which
|
|
204
|
+
# command last wrote it — `Open`'s own bare `reference_to Venue`
|
|
205
|
+
# argument was never wrapped in the first place (the top guard
|
|
206
|
+
# clause passes a bare scalar straight through), so it always
|
|
207
|
+
# stored the canonical bare identity this class's own header
|
|
208
|
+
# comment promises ("canonical target identities, not Ruby Value
|
|
209
|
+
# wrappers") — while `Repoint` silently kept the wrapper instead.
|
|
210
|
+
#
|
|
211
|
+
# Unambiguous only when both sides admit exactly one scalar: the
|
|
212
|
+
# target names exactly one identity path (`paths.one?` — a
|
|
213
|
+
# compound identity has no single field either side could stand
|
|
214
|
+
# in for) and the offered value is itself a single-attribute
|
|
215
|
+
# value object (`sole_attribute` — a multi-field VO has no one
|
|
216
|
+
# scalar to unwrap either). `materialize_unwrapped` recurses
|
|
217
|
+
# through any further single-field wrapping the same way it
|
|
218
|
+
# already does for `Value.materialize_unwrapped`'s other callers,
|
|
219
|
+
# landing on the bare scalar `Open`'s own path already produces
|
|
220
|
+
# for the identical target field.
|
|
221
|
+
def sole_scalar_identity(value, paths)
|
|
222
|
+
return nil unless value.is_a?(self) && paths.one?
|
|
223
|
+
return nil unless value.value_object.sole_attribute
|
|
224
|
+
|
|
225
|
+
materialize_unwrapped(value)
|
|
226
|
+
end
|
|
227
|
+
|
|
179
228
|
# Whether `value` is itself the target's own (single) identity
|
|
180
229
|
# value object — pure, self-contained: reads only `value` and
|
|
181
230
|
# `target`, decides nothing about any particular path.
|
|
@@ -226,14 +275,14 @@ module Hecks
|
|
|
226
275
|
|
|
227
276
|
# Vendored addition, not (yet) upstream hecks (migration
|
|
228
277
|
# plan task 5): a bare scalar auto-wraps into a single-field
|
|
229
|
-
# value object's sole attribute -- the
|
|
278
|
+
# value object's sole attribute -- the same shape
|
|
230
279
|
# #from_identifier already establishes for identity coercion
|
|
231
280
|
# (`build(value_object, { fields.first.name => identifier }) if
|
|
232
|
-
# fields.size == 1`), made consistent here for
|
|
281
|
+
# fields.size == 1`), made consistent here for mutation
|
|
233
282
|
# coercion too. Real, corpus-wide gap: a synthesised single-
|
|
234
283
|
# field wrapper (Part 3a's bare-primitive auto-synthesis, the
|
|
235
284
|
# norm for a VO-typed aggregate field) is exactly the shape
|
|
236
|
-
# #rewrap_arithmetic_result hands back a raw scalar
|
|
285
|
+
# #rewrap_arithmetic_result hands back a raw scalar result to
|
|
237
286
|
# -- without this, every phantom-field increment/multiply on a
|
|
238
287
|
# single-field-wrapped attribute refused with "pass its fields
|
|
239
288
|
# as an object, not <scalar>" the instant it tried to re-wrap
|
|
@@ -244,27 +293,27 @@ module Hecks
|
|
|
244
293
|
return { value_object.attributes.first.name => value } if value_object.attributes.size == 1
|
|
245
294
|
|
|
246
295
|
raise TypeMismatch,
|
|
247
|
-
RefusalWording.
|
|
248
|
-
|
|
249
|
-
|
|
296
|
+
RefusalWording.render_site("TypeMismatch", "value_object_shape",
|
|
297
|
+
name: name, type: value_object.hecks_name,
|
|
298
|
+
offered: Rendering.describe(value))
|
|
250
299
|
end
|
|
251
300
|
|
|
252
301
|
# `build`'s own recursive twin of `for_attribute`'s single-level
|
|
253
|
-
# normalization — a value object's
|
|
302
|
+
# normalization — a value object's own composite-typed fields
|
|
254
303
|
# (`Pizza.price_cents`, a `Price`) never otherwise pass back
|
|
255
304
|
# through `fields_for`, so a bare scalar or partial Hash for one
|
|
256
|
-
# of
|
|
305
|
+
# of those sails past the outer VO's own shape check (`Pizza`
|
|
257
306
|
# itself has two fields, so nothing unwraps there) and lands
|
|
258
307
|
# stored one field down exactly as handed in — found live: once
|
|
259
308
|
# the fuzzer actually generated the bare-scalar shape
|
|
260
|
-
# `fields_for` has accepted at the
|
|
309
|
+
# `fields_for` has accepted at the top level since 86727afd, a
|
|
261
310
|
# nested `Price` stored as a raw Integer broke every later
|
|
262
311
|
# dotted-path read (`pizza.price_cents.cents`) expecting one
|
|
263
312
|
# more level of Hash.
|
|
264
313
|
#
|
|
265
314
|
# Stays a plain Hash, never a nested `Value` — `Value#with`'s own
|
|
266
315
|
# header and `materialize_unwrapped`'s comment already depend on
|
|
267
|
-
# a value-object-typed field of
|
|
316
|
+
# a value-object-typed field of another value object staying a
|
|
268
317
|
# plain Hash once stored, and this does not change that; it only
|
|
269
318
|
# makes sure that Hash has the shape its own type declares.
|
|
270
319
|
# `aggregate` is the one thing `build` didn't used to need — a
|
|
@@ -272,12 +321,12 @@ module Hecks
|
|
|
272
321
|
# value_object(name)`, so callers with no aggregate in reach
|
|
273
322
|
# (`Value#with`, always re-setting an already-scalar arithmetic
|
|
274
323
|
# field) simply skip this and keep their prior behavior.
|
|
275
|
-
#
|
|
324
|
+
# Recurses into each nested field's own validation too, not only its
|
|
276
325
|
# shape — found live alongside the shape bug this method's header
|
|
277
326
|
# already describes: a nested `Price`/`Size` (a value-object-typed
|
|
278
|
-
# field of
|
|
327
|
+
# field of another value object, e.g. `Pizza.price_cents`,
|
|
279
328
|
# `Pizza.size`) had its Hash shape normalized here but never ran
|
|
280
|
-
# `validate!` — `build`, below, only ever validated the
|
|
329
|
+
# `validate!` — `build`, below, only ever validated the outer value
|
|
281
330
|
# object's own direct fields, so a negative `price_cents.cents` or an
|
|
282
331
|
# out-of-`one_of` `size.value` sailed through a `Pizza`-typed command
|
|
283
332
|
# argument untouched, while the exact same nested type declared as a
|
|
@@ -290,7 +339,23 @@ module Hecks
|
|
|
290
339
|
return fields unless aggregate.respond_to?(:value_object)
|
|
291
340
|
|
|
292
341
|
value_object.attributes.each do |attribute|
|
|
293
|
-
next
|
|
342
|
+
next unless fields.key?(attribute.name)
|
|
343
|
+
|
|
344
|
+
# A list member read back from the store hydrates like a top-level
|
|
345
|
+
# list — `list_of(Entity)` elements get their fields coerced,
|
|
346
|
+
# `list_of(ValueObject)` elements become Values — so a value object
|
|
347
|
+
# holding a list loads into the same shape the live dispatch that
|
|
348
|
+
# wrote it held. Found by PR A3 (every adapter through the state
|
|
349
|
+
# codec): chess-style `sets :positions, append: { pieces:
|
|
350
|
+
# state(:pieces) }` snapshots read back from Heki/Sqlite/Postgres
|
|
351
|
+
# (and now Memory's codec copy) with raw element hashes, so a
|
|
352
|
+
# `given` comparing a snapshot piece's `id` Value to a live one
|
|
353
|
+
# never matched after a restart. Load door only: an input list
|
|
354
|
+
# member is left exactly as before, refusals unchanged.
|
|
355
|
+
if attribute.list?
|
|
356
|
+
fields[attribute.name] = for_attribute(aggregate, attribute, fields[attribute.name]) if trusting_stored_state?
|
|
357
|
+
next
|
|
358
|
+
end
|
|
294
359
|
|
|
295
360
|
raw = fields[attribute.name]
|
|
296
361
|
next if raw.nil? || raw.is_a?(self)
|
|
@@ -313,20 +378,21 @@ module Hecks
|
|
|
313
378
|
end
|
|
314
379
|
end
|
|
315
380
|
|
|
316
|
-
#
|
|
381
|
+
# The full door a value object's own fields pass through — shared by
|
|
317
382
|
# `build` (the outer value object) and `normalize_composite_fields`
|
|
318
383
|
# (every nested one), so a nested `Price`/`Size` is refused exactly
|
|
319
384
|
# the same way, with exactly the same wording, as the identical type
|
|
320
385
|
# declared directly on a command.
|
|
321
386
|
def validate!(value_object, fields)
|
|
322
387
|
# C6.3 (docs/semantics/bluebook-semantics.md) — a value object is
|
|
323
|
-
# validated on
|
|
388
|
+
# validated on construction from input only; state read back from
|
|
324
389
|
# the store is trusted as it was written, so tightening an
|
|
325
390
|
# invariant never makes an old record unreadable (migration is
|
|
326
391
|
# the era system's job). `hydrate` — the one load door — sets
|
|
327
392
|
# the flag; every input door leaves it unset.
|
|
328
393
|
return if trusting_stored_state?
|
|
329
394
|
|
|
395
|
+
check_unknown_fields(value_object, fields)
|
|
330
396
|
check_required_fields(value_object, fields)
|
|
331
397
|
admit_member(value_object, fields)
|
|
332
398
|
check_admitted(value_object, fields)
|
|
@@ -337,9 +403,9 @@ module Hecks
|
|
|
337
403
|
next if Bluebook::Expression::Evaluator.call_rule(invariant, fields)
|
|
338
404
|
|
|
339
405
|
raise InvariantViolation,
|
|
340
|
-
RefusalWording.
|
|
341
|
-
|
|
342
|
-
|
|
406
|
+
RefusalWording.render_site("InvariantViolation", "value_object_invariant",
|
|
407
|
+
name: value_object.hecks_name, description: invariant.description,
|
|
408
|
+
offered: canonical_fields(fields))
|
|
343
409
|
end
|
|
344
410
|
end
|
|
345
411
|
|
|
@@ -350,10 +416,26 @@ module Hecks
|
|
|
350
416
|
new(value_object, fields)
|
|
351
417
|
end
|
|
352
418
|
|
|
419
|
+
# State arrives decoded or not at all (Phase 2, Track A, PR A4).
|
|
420
|
+
# Every persistence adapter reads through `Ports::Persistence::
|
|
421
|
+
# StateCodec.decode` (A3), which symbolizes every top-level key, and
|
|
422
|
+
# the runtime's own callers (entity elements, the remote dispatcher's
|
|
423
|
+
# `symbolize_names:` parse, Era's audit) build symbol-keyed state
|
|
424
|
+
# themselves. A String key here is an adapter or caller that skipped
|
|
425
|
+
# the codec, so it is refused by name rather than respelled: the
|
|
426
|
+
# silent `to_sym` that used to sit here is exactly what hid such a
|
|
427
|
+
# bypass. Always on, because it costs one `is_a?` per key, the same
|
|
428
|
+
# as the `to_sym` it replaced.
|
|
353
429
|
def hydrate(aggregate, state)
|
|
430
|
+
undecoded = state.keys.grep_v(Symbol)
|
|
431
|
+
unless undecoded.empty?
|
|
432
|
+
raise WiringError,
|
|
433
|
+
"#{aggregate.name} state reached hydration with non-Symbol keys #{undecoded.inspect} — " \
|
|
434
|
+
"decode stored state through Hecks::Ports::Persistence::StateCodec.decode first"
|
|
435
|
+
end
|
|
436
|
+
|
|
354
437
|
trusting_stored_state do
|
|
355
|
-
state.each_with_object({}) do |(
|
|
356
|
-
key = name.to_sym
|
|
438
|
+
state.each_with_object({}) do |(key, value), hydrated|
|
|
357
439
|
attribute = aggregate.attribute(key)
|
|
358
440
|
hydrated[key] = attribute ? for_attribute(aggregate, attribute, value) : value
|
|
359
441
|
end
|
|
@@ -372,111 +454,59 @@ module Hecks
|
|
|
372
454
|
|
|
373
455
|
def trusting_stored_state? = Thread.current[TRUSTED_LOAD_KEY] == true
|
|
374
456
|
|
|
375
|
-
#
|
|
376
|
-
#
|
|
377
|
-
#
|
|
378
|
-
#
|
|
379
|
-
#
|
|
380
|
-
#
|
|
381
|
-
#
|
|
382
|
-
#
|
|
383
|
-
#
|
|
384
|
-
#
|
|
385
|
-
# `
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
#
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
#
|
|
400
|
-
#
|
|
401
|
-
#
|
|
402
|
-
#
|
|
403
|
-
#
|
|
404
|
-
#
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
# `
|
|
408
|
-
#
|
|
409
|
-
#
|
|
410
|
-
#
|
|
411
|
-
#
|
|
412
|
-
#
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
next element unless element.is_a?(Hash)
|
|
422
|
-
|
|
423
|
-
element.each_with_object({}) do |(name, field_value), acc|
|
|
424
|
-
key = name.to_sym
|
|
425
|
-
field = entity.attribute(key)
|
|
426
|
-
acc[key] = field ? for_attribute(aggregate, field, field_value) : field_value
|
|
427
|
-
end
|
|
428
|
-
end
|
|
429
|
-
Freezer.deep(hydrated)
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
# The value-object sibling of the entity branch above: an element
|
|
433
|
-
# already shaped like the target `Value` (or a `Hash`/scalar that
|
|
434
|
-
# `fields_for` can still open) is rebuilt through the SAME `build`
|
|
435
|
-
# a scalar composite attribute already uses (`for_attribute`'s own
|
|
436
|
-
# `coerced = ... build(value_object, fields_for(...), aggregate)`
|
|
437
|
-
# line) — same defaults, same `pattern:`/`admits:`/invariant
|
|
438
|
-
# checks, same `trusting_stored_state?` bypass on a trusted load.
|
|
439
|
-
# `attribute.type` naming neither an entity nor a value object
|
|
440
|
-
# (a `list_of(String)`, say) has no shape to rebuild into, so the
|
|
441
|
-
# element passes through unchanged, exactly as the entity branch's
|
|
442
|
-
# own non-Hash elements do.
|
|
443
|
-
#
|
|
444
|
-
# NOT `Array(value).map` (unlike the entity branch above) — `value`
|
|
445
|
-
# here is not always genuinely list-shaped. `MutationApplier#
|
|
446
|
-
# removed`'s own `Value.for_attribute(aggregate, attribute, value)`
|
|
447
|
-
# call (`attribute` = the LIST attribute, `mutation.target`; `value`
|
|
448
|
-
# = the single REMOVE-target argument, already a real `Value` by
|
|
449
|
-
# the time it gets here) reuses this exact branch — for `remove:`,
|
|
450
|
-
# not for a whole-list `sets`. `Array(a_real_Value)` alone would be
|
|
451
|
-
# harmless (`Value` defines neither `to_a` nor `to_ary`, so Kernel
|
|
452
|
-
# wraps it `[value]`), but `Array(a_Hash)` is NOT harmless: Ruby's
|
|
453
|
-
# `Array()` opens a bare Hash into its own `[[k, v], ...]` pairs,
|
|
454
|
-
# not `[hash]` — silently shredding a single-element Hash-shaped
|
|
455
|
-
# target into garbage instead of hydrating it. Branching on
|
|
456
|
-
# `value.is_a?(Array)` up front (true only for a genuine whole-list
|
|
457
|
-
# `sets`/hydrate load) keeps the single-target shape a single
|
|
458
|
-
# target, hydrated the same way, never listified.
|
|
459
|
-
def hydrate_value_object_list(aggregate, attribute, value)
|
|
460
|
-
return value unless aggregate.respond_to?(:value_object)
|
|
461
|
-
|
|
462
|
-
value_object = value_object_for(aggregate, attribute.type)
|
|
463
|
-
return value unless value_object
|
|
464
|
-
|
|
465
|
-
return hydrate_value_object_element(aggregate, attribute, value_object, value) unless value.is_a?(Array)
|
|
466
|
-
|
|
467
|
-
hydrated = value.map { |element| hydrate_value_object_element(aggregate, attribute, value_object, element) }
|
|
468
|
-
Freezer.deep(hydrated)
|
|
457
|
+
# QualityControl BUG#125 — the one narrow door `check_scalar_shapes`
|
|
458
|
+
# keeps open, now that a non-string scalar is otherwise refused for a
|
|
459
|
+
# String-typed field. `MetaValidator::Judge#send_to` — the single
|
|
460
|
+
# choke point every one of the language's own self-hosted dispatches
|
|
461
|
+
# goes through while walking a bluebook's declarations into the
|
|
462
|
+
# "Bluebook" meta-domain — wraps itself in this, and nothing else
|
|
463
|
+
# does. `Judge#appends`' generic `POSITION` handling
|
|
464
|
+
# (judge.rb#appends) keys purely off a field being named "position",
|
|
465
|
+
# the convention every other append list actually uses it for
|
|
466
|
+
# (ValueObject::Member, ProcessManager::Handler, ... — all really
|
|
467
|
+
# `Position`/Integer-typed); `Normalise`'s own `NormalisationRule`
|
|
468
|
+
# happens to also name its own domain field "position"
|
|
469
|
+
# (bluebook.bluebook), but declares it `RuleText` (String) — so the
|
|
470
|
+
# same walk-index substitution (`Judge#v(index)`) hands it a raw
|
|
471
|
+
# Integer too, on every domain's very first boot (the language
|
|
472
|
+
# self-judges its own grammar via `MetaValidator.fresh_runtime`'s
|
|
473
|
+
# fixpoint). Confirmed (QualityControl BUG#125 investigation): the
|
|
474
|
+
# resulting value is never read back — `normalisations` is an
|
|
475
|
+
# `ELSEWHERE`/`derived` field spliced straight from
|
|
476
|
+
# `Expression::CanonicalForm.table` (assembly/contracts.rb), so the
|
|
477
|
+
# judged record holding the Integer is discarded whole — this is a
|
|
478
|
+
# walk-index/domain-field name collision inside `Judge#appends`, not
|
|
479
|
+
# a genuine semantic need for `position` to arrive numeric. Fixing
|
|
480
|
+
# that collision at its own root is a separate, larger change to
|
|
481
|
+
# self-hosted bootstrap mechanics that every domain's boot depends
|
|
482
|
+
# on; this flag only ever loosens scalar-shape checking for the
|
|
483
|
+
# meta-grammar's own value objects (RuleText, BluebookName, Position,
|
|
484
|
+
# …) that Judge itself constructs while walking a bluebook's
|
|
485
|
+
# declarations — never for a real domain's own declared value
|
|
486
|
+
# objects (PieceId, Money, …), which Judge never dispatches commands
|
|
487
|
+
# against. `offer` (judge.rb) already converts a `TypeMismatch` here
|
|
488
|
+
# into a recorded refusal rather than letting it propagate, but
|
|
489
|
+
# `MetaValidator.call` raises the instant `refusals` is non-empty
|
|
490
|
+
# (meta_validator.rb) — so, unexempted, this would fail every
|
|
491
|
+
# domain's boot, not just the language's own bootstrap. Composite
|
|
492
|
+
# shapes (Array/Hash) stay refused unconditionally, bootstrap or not
|
|
493
|
+
# — nothing Judge does ever legitimately needs those for a scalar
|
|
494
|
+
# field.
|
|
495
|
+
BOOTSTRAP_KEY = :hecks_judge_bootstrapping
|
|
496
|
+
|
|
497
|
+
def judge_bootstrapping
|
|
498
|
+
previous = Thread.current[BOOTSTRAP_KEY]
|
|
499
|
+
Thread.current[BOOTSTRAP_KEY] = true
|
|
500
|
+
yield
|
|
501
|
+
ensure
|
|
502
|
+
Thread.current[BOOTSTRAP_KEY] = previous
|
|
469
503
|
end
|
|
470
504
|
|
|
471
|
-
def
|
|
472
|
-
return element if element.is_a?(self) && element.type_name == value_object.hecks_name
|
|
473
|
-
|
|
474
|
-
build(value_object, fields_for(value_object, attribute.name, element), aggregate)
|
|
475
|
-
end
|
|
505
|
+
def judge_bootstrapping? = Thread.current[BOOTSTRAP_KEY] == true
|
|
476
506
|
|
|
477
507
|
# `Value.identifier` used to live here: hand it a one-field value object
|
|
478
508
|
# and it opened it, so `identified_by :number` could pass for an identity
|
|
479
|
-
# and the runtime would guess which field was meant.
|
|
509
|
+
# and the runtime would guess which field was meant. That guess is gone.
|
|
480
510
|
# An identity names its field — `identified_by :number` — and the
|
|
481
511
|
# path is what reaches the scalar. A declaration that names no field is
|
|
482
512
|
# refused when the bluebook loads, so nothing has to be unwrapped later.
|
|
@@ -485,12 +515,12 @@ module Hecks
|
|
|
485
515
|
# into a column or a message, where there is no path to consult.
|
|
486
516
|
|
|
487
517
|
# `Value.reference_id` lived here, opening a reference to find the id
|
|
488
|
-
# inside it. A reference
|
|
518
|
+
# inside it. A reference is the id now — refused at the payload gate if it
|
|
489
519
|
# arrives as anything else — so there is nothing left to open. The comment
|
|
490
|
-
# it carried said retiring it meant changing how references are
|
|
520
|
+
# it carried said retiring it meant changing how references are stored ;
|
|
491
521
|
# that is what happened.
|
|
492
522
|
|
|
493
|
-
# A
|
|
523
|
+
# A reference is an ID, so anything else is not one.
|
|
494
524
|
#
|
|
495
525
|
# Nothing coerces a reference — `for_attribute` misses on
|
|
496
526
|
# "Reference<Account>", which is no value object's name, and hands the
|
|
@@ -499,22 +529,77 @@ module Hecks
|
|
|
499
529
|
# refused, so whatever the first caller wrote became the shape.
|
|
500
530
|
#
|
|
501
531
|
# This is that place. It sits at the payload gate rather than inside
|
|
502
|
-
# coercion because the sentence names the
|
|
532
|
+
# coercion because the sentence names the command, and `for_attribute`
|
|
503
533
|
# never learns which command it is serving.
|
|
504
534
|
#
|
|
505
|
-
#
|
|
506
|
-
#
|
|
507
|
-
#
|
|
535
|
+
# Widened past the object shape by BUG#27 (QualityControl ledger,
|
|
536
|
+
# found live on `qa/stress_domains/referral_chain`'s `Member.Join`/
|
|
537
|
+
# `Referral.Issue`). A bare Boolean, Array, or `null` used to sail
|
|
538
|
+
# through here untouched — nothing but Hash/Value ever refused —
|
|
539
|
+
# then get `.to_s`'d into a lookup key by `CommandRules::
|
|
540
|
+
# References#reference_key` ("true", "false", "[8, 8]") and answer
|
|
541
|
+
# NotFound, or, for `null`, skip the lookup outright
|
|
542
|
+
# (`next if held.nil?`, command_rules/references.rb) and let the
|
|
543
|
+
# command run on to whatever its own `given` happened to say — a
|
|
544
|
+
# shape error misreading as a missing record, or as an unrelated
|
|
545
|
+
# domain refusal. Rust's generated `from_json` requires a JSON
|
|
546
|
+
# string for a required reference field before anything else runs
|
|
547
|
+
# (`JoinArgs.sponsor: expected String`); this closes the same gate
|
|
548
|
+
# at the same DISPATCH_ORDER step Ruby already runs it at
|
|
549
|
+
# (`normalize_args`, `Vocabulary::AggregateDispatchOrder`/
|
|
550
|
+
# `EntityDispatchOrder`), strictly before `resolve_references` ever
|
|
551
|
+
# receives a value to look up — so the two engines now agree on
|
|
552
|
+
# both kind and order, not just kind.
|
|
553
|
+
#
|
|
554
|
+
# `nil` stays legitimate for a `reference_to ..., optional: true`
|
|
555
|
+
# argument (`Improvement.Open`'s own `reference_to Angle, optional:
|
|
556
|
+
# true` — `qa/bluebook/quality_control.bluebook`): the caller
|
|
557
|
+
# genuinely may have nothing to name yet, and `nil_argument`
|
|
558
|
+
# (interpreting.rb) already passes an optional reference's `nil`
|
|
559
|
+
# through untouched. A required reference offered as `null` is a
|
|
560
|
+
# caller leaving a required argument empty in every other sense
|
|
561
|
+
# this runtime already refuses (C3.7) — refusing it here, rather
|
|
562
|
+
# than falling through to `resolve_references`' own nil-skip and
|
|
563
|
+
# then whatever the command's `given` happens to say, is what
|
|
564
|
+
# actually names the empty argument instead of something else.
|
|
565
|
+
#
|
|
566
|
+
# A `has_many` reference's own Array shape is still never refused
|
|
567
|
+
# by its wrapper (`Array(value).find { ... }` only inspects the
|
|
568
|
+
# list's elements) — a reference is never a scalar list-of-lists
|
|
569
|
+
# today, and inventing a rule for a shape the language cannot
|
|
570
|
+
# declare is how decoration gets written. `reference_list` (below)
|
|
571
|
+
# already owns "not an Array at all" for that case.
|
|
508
572
|
def refuse_object_reference(command, attribute, value)
|
|
509
573
|
return unless attribute.reference?
|
|
510
574
|
|
|
511
|
-
|
|
512
|
-
|
|
575
|
+
if attribute.list?
|
|
576
|
+
offered = Array(value).find { |item| item.is_a?(Hash) || item.is_a?(self) }
|
|
577
|
+
return unless offered
|
|
578
|
+
else
|
|
579
|
+
return if value.nil? && attribute.optional?
|
|
580
|
+
return if value.is_a?(String)
|
|
581
|
+
|
|
582
|
+
offered = value
|
|
583
|
+
end
|
|
513
584
|
|
|
514
585
|
raise TypeMismatch,
|
|
515
|
-
RefusalWording.
|
|
516
|
-
|
|
517
|
-
|
|
586
|
+
RefusalWording.render_site("TypeMismatch", "reference_wrong_shape",
|
|
587
|
+
command: command.hecks_name, attribute: attribute.name,
|
|
588
|
+
offered: reference_shape_description(offered),
|
|
589
|
+
known_by: known_by(attribute))
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# "an object" for the Hash/Value shape — the original wording this
|
|
593
|
+
# method always gave, pinned byte for byte by
|
|
594
|
+
# `spec/runtime/reference_shape_spec.rb`, kept unchanged by BUG#27's
|
|
595
|
+
# widening. `Rendering.describe` for everything else: `true`,
|
|
596
|
+
# `false`, `nil`, `[8, 8]` — the same rendering every other
|
|
597
|
+
# TypeMismatch in this file already uses for "here is what you
|
|
598
|
+
# actually sent."
|
|
599
|
+
def reference_shape_description(value)
|
|
600
|
+
return "an object" if value.is_a?(Hash) || value.is_a?(self)
|
|
601
|
+
|
|
602
|
+
Rendering.describe(value)
|
|
518
603
|
end
|
|
519
604
|
|
|
520
605
|
# "(Account is known by number)" — what to send instead. No article, on
|
|
@@ -523,8 +608,8 @@ module Hecks
|
|
|
523
608
|
# article-choosing rule. Silent when the target is another chapter's,
|
|
524
609
|
# where this runtime cannot see what it is known by.
|
|
525
610
|
#
|
|
526
|
-
#
|
|
527
|
-
# `identified_by`, which is the
|
|
611
|
+
# Every head, because a caller has to pass every one. This read
|
|
612
|
+
# `identified_by`, which is the single head and is nil the moment an
|
|
528
613
|
# identity has two parts — so a composite target fell through the guard
|
|
529
614
|
# and the refusal went silent exactly where it had the most to say. A
|
|
530
615
|
# single-path target reads as it always did.
|
|
@@ -541,7 +626,7 @@ module Hecks
|
|
|
541
626
|
fields = value.to_h
|
|
542
627
|
return fields.values.first if fields.size == 1
|
|
543
628
|
|
|
544
|
-
raise TypeMismatch, RefusalWording.
|
|
629
|
+
raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "multi_field_scalar", type: value.type_name)
|
|
545
630
|
end
|
|
546
631
|
|
|
547
632
|
def from_identifier(aggregate, attribute, identifier)
|
|
@@ -554,29 +639,29 @@ module Hecks
|
|
|
554
639
|
return build(value_object, { field.name => coerce_identifier(field, identifier) })
|
|
555
640
|
end
|
|
556
641
|
|
|
557
|
-
raise TypeMismatch, RefusalWording.
|
|
642
|
+
raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "composite_identity", type: value_object.hecks_name)
|
|
558
643
|
end
|
|
559
644
|
|
|
560
645
|
# Vendored fix, not (yet) upstream hecks (migration plan
|
|
561
|
-
# task 9): `identifier` here is always the
|
|
562
|
-
#
|
|
646
|
+
# task 9): `identifier` here is always the derived identity
|
|
647
|
+
# string -- `Identity.of`/`Identity.from` intentionally return
|
|
563
648
|
# one (correct for naming a repository key), and
|
|
564
649
|
# `Runtime::Instance#materialize_identity!` calls `from_identifier`
|
|
565
650
|
# with exactly that string on every fresh hydration -- but when
|
|
566
|
-
# the identity field's
|
|
651
|
+
# the identity field's own declared type is Integer/Float (not
|
|
567
652
|
# the overwhelmingly common String), seeding it straight from
|
|
568
653
|
# that string round-trips a correctly-derived identity back in
|
|
569
|
-
# as the
|
|
654
|
+
# as the wrong Ruby type -- and #build's own
|
|
570
655
|
# `check_numeric_fields` (added specifically to catch a genuine
|
|
571
|
-
#
|
|
656
|
+
# caller mismatch) then refused the runtime's own internal
|
|
572
657
|
# identity seed instead, on every dispatch, valid input or not.
|
|
573
658
|
#
|
|
574
|
-
# Reuses
|
|
659
|
+
# Reuses this same file's own `NUMERIC` table (declared-type ->
|
|
575
660
|
# expected-Ruby-class, already read by `check_numeric_fields`)
|
|
576
|
-
# to decide
|
|
661
|
+
# to decide which declared types need converting, and
|
|
577
662
|
# Kernel#Integer/#Float to do the converting. A genuinely
|
|
578
663
|
# malformed identifier (should never happen, since an identity
|
|
579
|
-
# is always derived
|
|
664
|
+
# is always derived from a correctly-typed field in the first
|
|
580
665
|
# place, but this stays defensive rather than assume it) passes
|
|
581
666
|
# back unconverted, and `check_numeric_fields` refuses it
|
|
582
667
|
# exactly as it always has -- preserving its real job of
|
|
@@ -598,24 +683,27 @@ module Hecks
|
|
|
598
683
|
JSON.generate(fields.sort_by { |name, _| name.to_s }.to_h)
|
|
599
684
|
end
|
|
600
685
|
|
|
601
|
-
# A field declared Integer or Float must
|
|
686
|
+
# A field declared Integer or Float must arrive as one.
|
|
602
687
|
#
|
|
603
688
|
# Without this a String sails into a numeric field and the failure surfaces
|
|
604
689
|
# later, inside a predicate, as `positive? expects a number, got "three"` —
|
|
605
|
-
# an EvaluationError, which is
|
|
690
|
+
# an EvaluationError, which is not a domain refusal. So the runtime broke
|
|
606
691
|
# where the domain should have said no, and the run contract recorded the
|
|
607
692
|
# crash beside genuine refusals as though the domain had judged it.
|
|
608
693
|
#
|
|
609
694
|
# C3.8 — the boundary check for an attribute whose type is a bare
|
|
610
695
|
# primitive rather than a value object: `Integer`/`Float` by exact
|
|
611
|
-
# numeric class (`NUMERIC`), `String`/booleans by rejecting a
|
|
612
|
-
# composite shape (`COMPOSITE_SHAPES`) —
|
|
613
|
-
# `
|
|
614
|
-
#
|
|
615
|
-
#
|
|
616
|
-
# other scalar
|
|
617
|
-
#
|
|
618
|
-
#
|
|
696
|
+
# numeric class (`NUMERIC`), `String`/booleans by rejecting only a
|
|
697
|
+
# composite shape (`COMPOSITE_SHAPES`) — not the same as
|
|
698
|
+
# `check_scalar_shapes` holds a value object's own `String` field to
|
|
699
|
+
# any more (QualityControl BUG#125 tightened that one to also refuse
|
|
700
|
+
# a non-string scalar; a bare `String` argument here still admits
|
|
701
|
+
# any other scalar, left exactly as it was — a bare-primitive
|
|
702
|
+
# attribute was never part of BUG#125's own investigation or fix,
|
|
703
|
+
# and whether it needs the same tightening, and against what real
|
|
704
|
+
# Judge dependency if any, is still open); worded by the same
|
|
705
|
+
# template with the owning construct as `type`. The Rust boundary is
|
|
706
|
+
# stricter there too, recorded in the clause.
|
|
619
707
|
private def check_bare_primitive(owner, attribute, value)
|
|
620
708
|
type = attribute.type.to_s
|
|
621
709
|
expected = NUMERIC[type]
|
|
@@ -628,9 +716,9 @@ module Hecks
|
|
|
628
716
|
end
|
|
629
717
|
if mistyped
|
|
630
718
|
raise TypeMismatch,
|
|
631
|
-
RefusalWording.
|
|
632
|
-
|
|
633
|
-
|
|
719
|
+
RefusalWording.render_site("TypeMismatch", "numeric_field",
|
|
720
|
+
type: owner.hecks_name, field: attribute.name,
|
|
721
|
+
expected: type, offered: Rendering.describe(value))
|
|
634
722
|
end
|
|
635
723
|
|
|
636
724
|
check_numeric_bounds(owner.hecks_name, attribute.name, value)
|
|
@@ -645,40 +733,90 @@ module Hecks
|
|
|
645
733
|
private def check_numeric_bounds(type_name, field_name, given)
|
|
646
734
|
if given.is_a?(Integer) && !INT64_RANGE.cover?(given)
|
|
647
735
|
raise TypeMismatch,
|
|
648
|
-
RefusalWording.
|
|
649
|
-
|
|
736
|
+
RefusalWording.render_site("TypeMismatch", "integer_range",
|
|
737
|
+
type: type_name, field: field_name, offered: Rendering.describe(given))
|
|
650
738
|
end
|
|
651
739
|
return unless given.is_a?(Float) && !given.finite?
|
|
652
740
|
|
|
653
741
|
raise TypeMismatch,
|
|
654
|
-
RefusalWording.
|
|
655
|
-
|
|
656
|
-
end
|
|
657
|
-
|
|
658
|
-
#
|
|
742
|
+
RefusalWording.render_site("TypeMismatch", "non_finite_field",
|
|
743
|
+
type: type_name, field: field_name, offered: Rendering.describe(given))
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
# QualityControl BUG#41 — a value object refuses a key it does not
|
|
747
|
+
# declare, the same way a command's own payload does
|
|
748
|
+
# (`CommandInterpreter::ArgumentGate#refuse_unknown_arguments`,
|
|
749
|
+
# argument_gate.rb) — reusing that method's exact refusal wording
|
|
750
|
+
# (`UnknownArgument unknown_args`, refusal_wording.rb:
|
|
751
|
+
# "{command} does not declare {unknown} — it takes {declared}")
|
|
752
|
+
# rather than inventing a new template, because every generated
|
|
753
|
+
# Rust value-object `from_json` already renders this refusal
|
|
754
|
+
# through that identical site: `rust/project/json_codec.rb`'s
|
|
755
|
+
# `emit_unknown_argument_check` (mirrored byte-for-byte in
|
|
756
|
+
# `rust/codegen/src/json_codec.rs`) emits `v.unknown_keys(&[...])`
|
|
757
|
+
# and the same "{name} does not declare {unknown} — it takes
|
|
758
|
+
# {declared}" format string for every value object's own
|
|
759
|
+
# `from_json` — `GameLabel::from_json`
|
|
760
|
+
# (rust/src/generated/chess/game.rs) is simply the first case
|
|
761
|
+
# this gap was reproduced against.
|
|
762
|
+
#
|
|
763
|
+
# Ruby never had an equivalent check anywhere in this `validate!`
|
|
764
|
+
# door before now — `fields[attribute.name]` reads only the
|
|
765
|
+
# declared attributes, so any other key a caller's Hash carried
|
|
766
|
+
# was silently ignored. `fields` here only ever holds what a
|
|
767
|
+
# caller (or `for_attribute`'s own recursive coercion) offered
|
|
768
|
+
# for this value object — built by `fields_for`'s plain
|
|
769
|
+
# key-symbolizing (never a Hash the runtime pads with bookkeeping
|
|
770
|
+
# keys of its own; confirmed by reading every call site that
|
|
771
|
+
# reaches `validate!`) — so there is nothing legitimate here to
|
|
772
|
+
# exempt.
|
|
773
|
+
#
|
|
774
|
+
# Checked first, before `check_required_fields` and everything
|
|
775
|
+
# after it — matching Rust's own `from_json`, which checks
|
|
776
|
+
# `unknown_keys` before reading a single declared field. So a
|
|
777
|
+
# Hash offering both an unrecognized key and a missing required
|
|
778
|
+
# one (BUG#41's own second demonstration case: `label: {extra:
|
|
779
|
+
# "bogus"}` — unknown and missing `value`) refuses the same
|
|
780
|
+
# UnknownArgument on both engines, not two different refusal
|
|
781
|
+
# kinds for one malformed call.
|
|
782
|
+
private def check_unknown_fields(value_object, fields)
|
|
783
|
+
known = value_object.attributes.map { |attribute| attribute.name.to_sym }
|
|
784
|
+
unknown = (fields.keys.map(&:to_sym) - known).sort
|
|
785
|
+
return if unknown.empty?
|
|
786
|
+
|
|
787
|
+
declared = value_object.attributes.map(&:name)
|
|
788
|
+
raise UnknownArgument,
|
|
789
|
+
RefusalWording.render_site("UnknownArgument", "unknown_args",
|
|
790
|
+
command: value_object.hecks_name, unknown: unknown,
|
|
791
|
+
declared: declared)
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# C3.7 — a value object is a typed field product: every non-optional
|
|
659
795
|
# field arrives, or construction refuses. A missing field and a null
|
|
660
796
|
# one are the same absence (`fields[name]` reads nil for both), worded
|
|
661
797
|
# as the type mismatch it is — "{type}.{field} expects {expected}, got
|
|
662
798
|
# nil" — the identical string the Rust side's generated `from_json`
|
|
663
799
|
# gives the same input, so the corpus can pin it on both. Checked
|
|
664
|
-
#
|
|
665
|
-
#
|
|
666
|
-
#
|
|
667
|
-
#
|
|
668
|
-
#
|
|
800
|
+
# first among the field-content checks (after `check_unknown_fields`'s
|
|
801
|
+
# own structural gate above, BUG#41): an invariant reading a field
|
|
802
|
+
# that never arrived is exactly the thing that used to answer
|
|
803
|
+
# "invariant violated" (or nothing at all — `ToppingName`'s
|
|
804
|
+
# `{value: null}` used to be accepted and stored). A `default:` has
|
|
805
|
+
# already been filled in by `apply_defaults`; a list field's absence
|
|
806
|
+
# is an empty list, never a refusal.
|
|
669
807
|
private def check_required_fields(value_object, fields)
|
|
670
808
|
value_object.attributes.each do |attribute|
|
|
671
809
|
next if attribute.optional? || attribute.list?
|
|
672
810
|
next unless fields[attribute.name].nil?
|
|
673
811
|
|
|
674
812
|
raise TypeMismatch,
|
|
675
|
-
RefusalWording.
|
|
676
|
-
|
|
677
|
-
|
|
813
|
+
RefusalWording.render_site("TypeMismatch", "numeric_field",
|
|
814
|
+
type: value_object.hecks_name, field: attribute.name,
|
|
815
|
+
expected: attribute.type, offered: "nil")
|
|
678
816
|
end
|
|
679
817
|
end
|
|
680
818
|
|
|
681
|
-
# Checked
|
|
819
|
+
# Checked before invariants, because an invariant reading a mistyped field
|
|
682
820
|
# is exactly the thing that used to explode.
|
|
683
821
|
NUMERIC = { "Integer" => Integer, "Float" => Numeric }.freeze
|
|
684
822
|
private def check_numeric_fields(value_object, fields)
|
|
@@ -691,9 +829,9 @@ module Hecks
|
|
|
691
829
|
|
|
692
830
|
unless given.is_a?(expected)
|
|
693
831
|
raise TypeMismatch,
|
|
694
|
-
RefusalWording.
|
|
695
|
-
|
|
696
|
-
|
|
832
|
+
RefusalWording.render_site("TypeMismatch", "numeric_field",
|
|
833
|
+
type: value_object.hecks_name, field: attribute.name,
|
|
834
|
+
expected: attribute.type, offered: Rendering.describe(given))
|
|
697
835
|
end
|
|
698
836
|
|
|
699
837
|
# PRD 05 (numeric-boundary-coverage) — `given.is_a?(expected)`
|
|
@@ -711,8 +849,8 @@ module Hecks
|
|
|
711
849
|
# storage, where `JSON.generate`/`#to_json` raises
|
|
712
850
|
# `JSON::GeneratorError: NaN/Infinity not allowed in JSON` the
|
|
713
851
|
# moment anything tries to persist or replay it — again a raw
|
|
714
|
-
# crash, not a refusal. `-0.0` is deliberately
|
|
715
|
-
# here: it
|
|
852
|
+
# crash, not a refusal. `-0.0` is deliberately not refused
|
|
853
|
+
# here: it is finite, round-trips through JSON as `-0.0`
|
|
716
854
|
# cleanly (confirmed empirically), and is a legitimate,
|
|
717
855
|
# meaningful float value (a signed zero), not a corruption
|
|
718
856
|
# risk — only NaN and +/-Infinity are.
|
|
@@ -721,45 +859,60 @@ module Hecks
|
|
|
721
859
|
end
|
|
722
860
|
|
|
723
861
|
# A field declared `String` (or a boolean) must not arrive as a
|
|
724
|
-
#
|
|
725
|
-
# what has to be a leaf scalar.
|
|
862
|
+
# composite — an Array or a Hash (or a nested Value) standing in for
|
|
863
|
+
# what has to be a leaf scalar. A `String` field, further, must not
|
|
864
|
+
# arrive as any other non-composite scalar either (Integer, Float,
|
|
865
|
+
# true/false) — QualityControl BUG#125, matching Rust's generated
|
|
866
|
+
# `from_json`, which requires a JSON string node for a String-typed
|
|
867
|
+
# field unconditionally and refuses anything else, including a JSON
|
|
868
|
+
# number or boolean. Ruby used to tolerate exactly that (found live:
|
|
869
|
+
# `Chess::Piece.Capture`'s `PieceId`, String-typed, offered a bignum
|
|
870
|
+
# `id` — Ruby let it pass and failed later on an unrelated field,
|
|
871
|
+
# Rust refused on `id` itself, immediately) — no longer, except
|
|
872
|
+
# inside `judge_bootstrapping?` (above), the one caller genuinely
|
|
873
|
+
# relying on the old leniency; see that flag's own comment for why.
|
|
726
874
|
#
|
|
727
|
-
#
|
|
728
|
-
#
|
|
729
|
-
#
|
|
730
|
-
#
|
|
731
|
-
# a
|
|
732
|
-
#
|
|
733
|
-
#
|
|
734
|
-
#
|
|
735
|
-
#
|
|
736
|
-
#
|
|
737
|
-
#
|
|
738
|
-
#
|
|
739
|
-
#
|
|
740
|
-
#
|
|
741
|
-
#
|
|
742
|
-
#
|
|
875
|
+
# `TrueClass`/`FalseClass` stay laxer than `check_numeric_fields`
|
|
876
|
+
# above: for those two, this still only enforces that the shape
|
|
877
|
+
# isn't a collection, not the exact Ruby class — narrower than the
|
|
878
|
+
# `String` case above because BUG#125 investigated and fixed String
|
|
879
|
+
# specifically; a boolean field's own scalar-shape tolerance is a
|
|
880
|
+
# separate, uninvestigated question left exactly as it was. No
|
|
881
|
+
# scalar field, of any declared type, can ever legitimately be
|
|
882
|
+
# handed an Array or a Hash — that shape is always wrong, and always
|
|
883
|
+
# was: `InvalidValueGenerator#array_for_scalar`'s own corruption is
|
|
884
|
+
# deliberately built to be refused (see that file's header), and
|
|
885
|
+
# until this check existed it sailed straight through for a
|
|
886
|
+
# String/boolean field the way it never could for an Integer/Float
|
|
887
|
+
# one (`check_numeric_fields` above already catches an Array offered
|
|
888
|
+
# for those). Found live via bin/fuzz, seed 17 on the fixtures
|
|
889
|
+
# domain : an Array standing in for a single-field identity's
|
|
890
|
+
# declared `String`, `.to_s`'d into a record id downstream.
|
|
743
891
|
COMPOSITE_SHAPES = [Array, ::Hash].freeze
|
|
744
892
|
NON_NUMERIC_SCALARS = %w[String TrueClass FalseClass].freeze
|
|
745
893
|
private def check_scalar_shapes(value_object, fields)
|
|
746
894
|
value_object.attributes.each do |attribute|
|
|
747
|
-
|
|
895
|
+
type = attribute.type.to_s
|
|
896
|
+
next unless NON_NUMERIC_SCALARS.include?(type)
|
|
748
897
|
|
|
749
898
|
given = fields[attribute.name]
|
|
750
|
-
next if given.nil?
|
|
899
|
+
next if given.nil?
|
|
900
|
+
|
|
901
|
+
composite = COMPOSITE_SHAPES.any? { |shape| given.is_a?(shape) }
|
|
902
|
+
non_string_scalar = type == "String" && !composite && !given.is_a?(String) && !judge_bootstrapping?
|
|
903
|
+
next unless composite || non_string_scalar
|
|
751
904
|
|
|
752
905
|
raise TypeMismatch,
|
|
753
|
-
RefusalWording.
|
|
754
|
-
|
|
755
|
-
|
|
906
|
+
RefusalWording.render_site("TypeMismatch", "numeric_field",
|
|
907
|
+
type: value_object.hecks_name, field: attribute.name,
|
|
908
|
+
expected: attribute.type, offered: Rendering.describe(given))
|
|
756
909
|
end
|
|
757
910
|
end
|
|
758
911
|
|
|
759
|
-
# A field declared with a
|
|
912
|
+
# A field declared with a pattern must match it.
|
|
760
913
|
#
|
|
761
914
|
# Beside check_numeric_fields and for the same reason : a value that does
|
|
762
|
-
# not look like what it claims to be is the
|
|
915
|
+
# not look like what it claims to be is the domain saying no, and it should
|
|
763
916
|
# say so here rather than let the wrong shape travel on and surface as a
|
|
764
917
|
# broken predicate later.
|
|
765
918
|
#
|
|
@@ -776,9 +929,9 @@ module Hecks
|
|
|
776
929
|
next if given.is_a?(String) && Regexp.new(pattern).match?(given)
|
|
777
930
|
|
|
778
931
|
raise TypeMismatch,
|
|
779
|
-
RefusalWording.
|
|
780
|
-
|
|
781
|
-
|
|
932
|
+
RefusalWording.render_site("TypeMismatch", "pattern_mismatch",
|
|
933
|
+
type: value_object.hecks_name, field: attribute.name,
|
|
934
|
+
pattern: pattern, offered: Rendering.describe(given))
|
|
782
935
|
end
|
|
783
936
|
end
|
|
784
937
|
end
|