hecks 1.3.0 → 1.5.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 +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -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/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- 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/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -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 +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- 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 +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- 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 +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- 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 +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- 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 +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -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 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- 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 +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -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 +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- 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 +118 -66
- 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 +34 -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 +33 -9
- 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 +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- 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 +48 -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 +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -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 +23 -3
- 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 +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- 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 +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- 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 +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
|
@@ -20,14 +20,12 @@ module Hecks
|
|
|
20
20
|
SIGN_TESTS = Hecks::Vocabulary.fetch("SignTest")
|
|
21
21
|
|
|
22
22
|
# Which Comparison operator each sign test is sugar for, against the
|
|
23
|
-
# literal 0 —
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
SIGN_TEST_OPERATORS =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"zero?" => "=="
|
|
30
|
-
}.freeze
|
|
23
|
+
# literal 0 — Vocabulary::SignTest's own compares_via
|
|
24
|
+
# (language/bluebook/vocabulary.bluebook), read off the generated
|
|
25
|
+
# table rather than typed a second time.
|
|
26
|
+
SIGN_TEST_OPERATORS = Hecks::Vocabulary.rows("SignTest")
|
|
27
|
+
.to_h { |row| [row["name"], row["compares_via"]] }
|
|
28
|
+
.freeze
|
|
31
29
|
|
|
32
30
|
# The leaf grammar an expression's dotted/arithmetic side parses into.
|
|
33
31
|
# Which node a string produces is a pure function of the string, so
|
|
@@ -40,7 +38,7 @@ module Hecks
|
|
|
40
38
|
StringLiteral = Struct.new(:value, keyword_init: true)
|
|
41
39
|
BoolLiteral = Struct.new(:value, keyword_init: true)
|
|
42
40
|
# `["active", "suspended"]` — a literal set, the haystack half of
|
|
43
|
-
# an `.include?`. Vocabulary::IncludeHaystack has always
|
|
41
|
+
# an `.include?`. Vocabulary::IncludeHaystack has always admitted
|
|
44
42
|
# Array (and Evaluator#includes? has always had a `when Array`
|
|
45
43
|
# arm), but nothing could produce one: there was no array-literal
|
|
46
44
|
# node, so `["a", "b"].include?(x)` fell through to Lookup and
|
|
@@ -66,18 +64,18 @@ module Hecks
|
|
|
66
64
|
Size = Struct.new(:receiver, keyword_init: true)
|
|
67
65
|
Lookup = Struct.new(:path, keyword_init: true)
|
|
68
66
|
|
|
69
|
-
#
|
|
67
|
+
# Update 2026-08-27: every "vendored addition, not (yet) upstream
|
|
70
68
|
# hecks" comment on this file's own MatchesRegex/Presence/Split/
|
|
71
69
|
# First/Last/StartsWith/EndsWith below (plus ArrayLiteral above)
|
|
72
|
-
# described a
|
|
70
|
+
# described a real gap, found the hard way, in the history each
|
|
73
71
|
# comment tells — and that history stays exactly as written,
|
|
74
|
-
# on purpose. What changed is the
|
|
72
|
+
# on purpose. What changed is the present tense claim "not (yet)
|
|
75
73
|
# upstream": a review of this exact migration found these eight
|
|
76
|
-
# symbols had working Ruby parse/interpret arms but had
|
|
74
|
+
# symbols had working Ruby parse/interpret arms but had never
|
|
77
75
|
# gone through Propose -> Render -> Admit
|
|
78
76
|
# (lib/hecks/grammar/expression_operators.json) the way every
|
|
79
77
|
# other operator here has — a closed-vocabulary guard
|
|
80
|
-
# (spec/operator_conformance_spec.rb) built entirely over
|
|
78
|
+
# (spec/operator_conformance_spec.rb) built entirely over tables
|
|
81
79
|
# structurally could not see hand-coded Struct/parse/interpret
|
|
82
80
|
# additions, so eight operators ran in Ruby, admitted nowhere,
|
|
83
81
|
# invisible to the one guard whose whole job was "reads in every
|
|
@@ -88,13 +86,13 @@ module Hecks
|
|
|
88
86
|
#
|
|
89
87
|
# `receiver.match?(/pattern/)` -- vendored addition, not (yet)
|
|
90
88
|
# upstream hecks (migration plan task 8): confirmed the
|
|
91
|
-
#
|
|
89
|
+
# single most impactful corpus-wide dispatch-time gap of the
|
|
92
90
|
# whole migration -- `.match?(regex)` appears in nearly every
|
|
93
91
|
# value_object's format-validation rule across every corpus
|
|
94
92
|
# this migration touched (email/phone/ISO-8601-timestamp/zip
|
|
95
|
-
# patterns, dozens of files), and had
|
|
93
|
+
# patterns, dozens of files), and had no parse support at all:
|
|
96
94
|
# it fell all the way through to the `Lookup` catch-all below,
|
|
97
|
-
# which tried to split the
|
|
95
|
+
# which tried to split the entire ".match?(/\A\d{5}\z/)" text
|
|
98
96
|
# on "." as if it were a dotted attribute path, and crashed with
|
|
99
97
|
# an opaque "no implicit conversion of Symbol into Integer"
|
|
100
98
|
# somewhere downstream -- confirmed live via a real dispatch,
|
|
@@ -118,13 +116,13 @@ module Hecks
|
|
|
118
116
|
Presence = Struct.new(:receiver, :negated, keyword_init: true)
|
|
119
117
|
|
|
120
118
|
# `.set?`/`.unset?` -- sibling of `.present?`/`.blank?` immediately
|
|
121
|
-
# above, added for a
|
|
119
|
+
# above, added for a deliberately narrower question, spelled out in
|
|
122
120
|
# the name so choosing between the two pairs is a choice, not a
|
|
123
121
|
# trap: `.present?` means "not EMPTY" (Rails-standard -- nil/false,
|
|
124
|
-
# and an
|
|
122
|
+
# and an empty String/Array/Hash, are blank; nothing else is),
|
|
125
123
|
# which quietly answers "was this ever assigned" and "does the
|
|
126
|
-
# assigned value happen to be empty" as the
|
|
127
|
-
# optional field whose only legitimate unset state
|
|
124
|
+
# assigned value happen to be empty" as the same question. For an
|
|
125
|
+
# optional field whose only legitimate unset state is nil, that
|
|
128
126
|
# conflation is a real trap -- confirmed live: a corpus author
|
|
129
127
|
# reaching for `superseded_by.blank?` to guard an optional
|
|
130
128
|
# reference burned real time before landing on "just don't guard
|
|
@@ -144,7 +142,7 @@ module Hecks
|
|
|
144
142
|
# `value.split("::").length == 4 && value.split("::").all? { |s|
|
|
145
143
|
# s.length > 0 }` -- `.split(` matched none of this grammar's
|
|
146
144
|
# known suffixes, so it fell through to the `Lookup` catch-all,
|
|
147
|
-
# which split the
|
|
145
|
+
# which split the raw expression text on "." (not the runtime
|
|
148
146
|
# value) and crashed with `TypeError: no implicit conversion of
|
|
149
147
|
# Symbol into Integer` the moment `String#[]` was handed a
|
|
150
148
|
# Symbol segment -- meaning every command taking a Phrase was
|
|
@@ -190,10 +188,10 @@ module Hecks
|
|
|
190
188
|
# storehouse-kernel files, byte-identical text) -- `.start_with?(`/
|
|
191
189
|
# `.end_with?(` matched none of this grammar's known suffixes, so
|
|
192
190
|
# both fell through to the `Lookup` catch-all and crashed with the
|
|
193
|
-
#
|
|
194
|
-
# Integer` shape
|
|
195
|
-
# real dispatch (not validate), not inferred. Two
|
|
196
|
-
# types rather than one `mode:`-keyed struct (the `BlockPredicate`/
|
|
191
|
+
# same `TypeError: no implicit conversion of Symbol into
|
|
192
|
+
# Integer` shape any unsupported suffix produces here, confirmed
|
|
193
|
+
# live via a real dispatch (not validate), not inferred. Two
|
|
194
|
+
# separate node types rather than one `mode:`-keyed struct (the `BlockPredicate`/
|
|
197
195
|
# `SignTest` precedent) -- `start_with?`/`end_with?` aren't two
|
|
198
196
|
# spellings of the same test the way `all?`/`any?`/`none?` are (one
|
|
199
197
|
# Array-aggregation family) or `positive?`/`negative?`/`zero?` are
|
|
@@ -208,18 +206,44 @@ module Hecks
|
|
|
208
206
|
|
|
209
207
|
module_function
|
|
210
208
|
|
|
209
|
+
# Parses and interprets `expr` in one step, without going through
|
|
210
|
+
# `Evaluator`'s own boolean/comparison grammar or its `ast_cache` —
|
|
211
|
+
# for a caller that wants this leaf grammar's own raw value (an
|
|
212
|
+
# Integer, a String, an Array, ...), not a boolean.
|
|
213
|
+
#
|
|
214
|
+
# @param expr [String] the dotted/arithmetic leaf expression to
|
|
215
|
+
# resolve
|
|
216
|
+
# @param state [Hash{Symbol => Object}] the stored attribute values
|
|
217
|
+
# an unqualified name may resolve against
|
|
218
|
+
# @param attrs [Hash{Symbol => Object}] the call's own argument
|
|
219
|
+
# values, checked before `state`
|
|
220
|
+
# @return [Object] whatever `expr` resolves to: an Integer, Float,
|
|
221
|
+
# String, `true`/`false`, `nil`, or Array, depending on `expr`'s
|
|
222
|
+
# own leaf grammar
|
|
223
|
+
# @raise [EvaluationError] if `expr` names an attribute or argument
|
|
224
|
+
# neither `attrs` nor `state` has, or if an operation in `expr`
|
|
225
|
+
# is applied to a value of the wrong type
|
|
211
226
|
def resolve(expr, state, attrs)
|
|
212
227
|
interpret(parse(expr), state, attrs)
|
|
213
228
|
end
|
|
214
229
|
|
|
215
230
|
# A grammar's own dispatch table — one `return Node.new(...) if
|
|
216
|
-
# expr =~ /pattern/` per leaf production, in a
|
|
231
|
+
# expr =~ /pattern/` per leaf production, in a fixed precedence
|
|
217
232
|
# order (`.length` before the generic suffixes, sign tests before
|
|
218
233
|
# the rest, etc. — see this file's own history comments on why
|
|
219
234
|
# several of these were added in exactly this position). Each
|
|
220
235
|
# branch is already one line; splitting the table into smaller
|
|
221
236
|
# methods would not shrink any single check, only hide the
|
|
222
|
-
# precedence order this method's own line-by-line sequence
|
|
237
|
+
# precedence order this method's own line-by-line sequence is.
|
|
238
|
+
#
|
|
239
|
+
# @param expr [String] the leaf expression text to parse
|
|
240
|
+
# @return [Object] the parsed leaf node — one of this module's own
|
|
241
|
+
# Struct types (`IntegerLiteral`, `FloatLiteral`, `StringLiteral`,
|
|
242
|
+
# `BoolLiteral`, `NilLiteral`, `ArrayLiteral`, `Addition`,
|
|
243
|
+
# `SignTest`, `Empty`, `ToS`, `Modulo`, `Size`, `MatchesRegex`,
|
|
244
|
+
# `Presence`, `Assignment`, `Split`, `First`, `Last`,
|
|
245
|
+
# `StartsWith`, `EndsWith`, `BlockPredicate`, `Find`), chosen by
|
|
246
|
+
# `expr`'s own shape, or `Lookup` when nothing else matches
|
|
223
247
|
# rubocop:disable-next Metrics/AbcSize
|
|
224
248
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
225
249
|
# rubocop:disable-next Metrics/MethodLength
|
|
@@ -289,6 +313,13 @@ module Hecks
|
|
|
289
313
|
Lookup.new(path: expr)
|
|
290
314
|
end
|
|
291
315
|
|
|
316
|
+
# Builds the `SignTest` node for a matched `.positive?`/
|
|
317
|
+
# `.negative?`/`.zero?`-style suffix.
|
|
318
|
+
#
|
|
319
|
+
# @param parts [Array(String, String)] the `[receiver_text,
|
|
320
|
+
# test_name]` pair `match_suffix` returns
|
|
321
|
+
# @return [SignTest] the parsed sign-test node, its `receiver`
|
|
322
|
+
# itself parsed
|
|
292
323
|
def sign_test_node(parts)
|
|
293
324
|
receiver, test = parts
|
|
294
325
|
symbol = SIGN_TEST_OPERATORS.fetch(test)
|
|
@@ -299,10 +330,25 @@ module Hecks
|
|
|
299
330
|
# `parse`'s own dispatch table, mirrored: one `when` per leaf node
|
|
300
331
|
# type it can produce, each already delegating to a small named
|
|
301
332
|
# helper (add, apply_sign_test, emptiness_of, ...) — the case
|
|
302
|
-
# itself
|
|
333
|
+
# itself is the closed, declared set this method exists to
|
|
303
334
|
# exhaust; the `else` backstop's own comment explains why a
|
|
304
335
|
# missing arm is a bug this method is built to make loud, not
|
|
305
336
|
# quiet.
|
|
337
|
+
#
|
|
338
|
+
# @param node [Object] a node `parse` produced (one of this
|
|
339
|
+
# module's own Struct types) or built directly by a caller
|
|
340
|
+
# @param state [Hash{Symbol => Object}] the stored attribute values
|
|
341
|
+
# a `Lookup` node may resolve against
|
|
342
|
+
# @param attrs [Hash{Symbol => Object}] the call's own argument
|
|
343
|
+
# values, checked before `state`
|
|
344
|
+
# @return [Object] the value `node` resolves to: an Integer, Float,
|
|
345
|
+
# String, `true`/`false`, `nil`, or Array, depending on `node`'s
|
|
346
|
+
# own type
|
|
347
|
+
# @raise [EvaluationError] if `node` is not one of the handled
|
|
348
|
+
# types, or if a helper it dispatches to (`add`, `apply_sign_test`,
|
|
349
|
+
# `matches_regex?`, `split_value`, `last_of`, `first_of`,
|
|
350
|
+
# `starts_with?`, `ends_with?`, `apply_modulo`, `size_of`,
|
|
351
|
+
# `emptiness_of`, `lookup`) refuses its operand
|
|
306
352
|
# rubocop:disable-next Metrics/AbcSize
|
|
307
353
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
308
354
|
# rubocop:disable-next Metrics/MethodLength
|
|
@@ -365,11 +411,16 @@ module Hecks
|
|
|
365
411
|
|
|
366
412
|
# `.present?`/`.blank?` -- vendored addition, see the
|
|
367
413
|
# `Presence` struct's own comment above. `nil` and `false` are
|
|
368
|
-
# blank ; a String/Array/Hash is blank when
|
|
369
|
-
# falsy -- a VO-wrapped field that
|
|
414
|
+
# blank ; a String/Array/Hash is blank when empty, not merely
|
|
415
|
+
# falsy -- a VO-wrapped field that is assigned (`{value: "x"}`,
|
|
370
416
|
# `Value#to_h`'d first) is present regardless of what its own
|
|
371
417
|
# inner value holds, matching how every VO-typed field in this
|
|
372
418
|
# corpus is actually shaped once set at all.
|
|
419
|
+
#
|
|
420
|
+
# @param value [Object] the already-interpreted receiver value
|
|
421
|
+
# @return [Boolean] `true` for `nil`, `false`, or an empty
|
|
422
|
+
# String/Array/Hash (a to_h-able value is converted first);
|
|
423
|
+
# `false` for anything else
|
|
373
424
|
def blank?(value)
|
|
374
425
|
return true if value.nil? || value == false
|
|
375
426
|
|
|
@@ -389,11 +440,23 @@ module Hecks
|
|
|
389
440
|
# `receiver.match?(/pattern/)` -- vendored addition, see the
|
|
390
441
|
# `MatchesRegex` struct's own comment above. `receiver_value` is
|
|
391
442
|
# coerced to a plain String first -- inlined here rather than
|
|
392
|
-
# calling `Evaluator#string_of` (a
|
|
443
|
+
# calling `Evaluator#string_of` (a different module_function
|
|
393
444
|
# module; not actually in scope from inside Resolver despite
|
|
394
445
|
# `Modulo`'s own parse rule above calling a same-named
|
|
395
446
|
# `match_call` that has the identical cross-module problem --
|
|
396
447
|
# found live while building this, not assumed).
|
|
448
|
+
#
|
|
449
|
+
# @param receiver_value [String, Symbol, Integer, Float, nil] the
|
|
450
|
+
# already-interpreted receiver value
|
|
451
|
+
# @param pattern [String] the regex source, exactly as it appeared
|
|
452
|
+
# between the `/.../` delimiters
|
|
453
|
+
# @param flags [String] the suffix flag letters after the closing
|
|
454
|
+
# `/`; `"i"` enables `Regexp::IGNORECASE`, `"m"` `MULTILINE`,
|
|
455
|
+
# `"x"` `EXTENDED`
|
|
456
|
+
# @return [Boolean] whether `pattern` matches `receiver_value`'s
|
|
457
|
+
# string form
|
|
458
|
+
# @raise [EvaluationError] if `receiver_value` is not a scalar, or
|
|
459
|
+
# `pattern` is not a valid regex
|
|
397
460
|
def matches_regex?(receiver_value, pattern, flags)
|
|
398
461
|
text = case receiver_value
|
|
399
462
|
when String, Symbol, Integer, Float then receiver_value.to_s
|
|
@@ -410,7 +473,7 @@ module Hecks
|
|
|
410
473
|
Regexp.new(pattern, options).match?(text)
|
|
411
474
|
rescue RegexpError => e
|
|
412
475
|
# M9: a malformed pattern between the slashes (an unclosed
|
|
413
|
-
# character class, say) is a defect in the
|
|
476
|
+
# character class, say) is a defect in the expression text
|
|
414
477
|
# itself, exactly the same category of author mistake an
|
|
415
478
|
# unresolvable attribute name already refuses for — `Regexp.new`
|
|
416
479
|
# raising a raw `RegexpError` crossed this sublanguage's own
|
|
@@ -420,9 +483,14 @@ module Hecks
|
|
|
420
483
|
end
|
|
421
484
|
|
|
422
485
|
# The elements of a bracketed literal, or nil if this isn't one.
|
|
423
|
-
# Splits on
|
|
486
|
+
# Splits on top-level commas only — quote-aware and depth-aware,
|
|
424
487
|
# the same discipline `split_addition` already applies, so a
|
|
425
488
|
# nested array or a comma inside a string element stays whole.
|
|
489
|
+
#
|
|
490
|
+
# @param expr [String] the leaf expression text
|
|
491
|
+
# @return [Array<String>, nil] the unparsed source text of each
|
|
492
|
+
# element, in order (`[]` for `"[]"`), or `nil` when `expr` is
|
|
493
|
+
# not bracketed
|
|
426
494
|
def array_elements(expr)
|
|
427
495
|
return nil unless expr.start_with?("[") && expr.end_with?("]")
|
|
428
496
|
|
|
@@ -455,9 +523,9 @@ module Hecks
|
|
|
455
523
|
elements.reject(&:empty?)
|
|
456
524
|
end
|
|
457
525
|
|
|
458
|
-
#
|
|
526
|
+
# Braces count toward depth, exactly as parens do — a `+` inside a
|
|
459
527
|
# block predicate's own `{ |x| ... }` body is not this expression's
|
|
460
|
-
# own addition. `parse` tries addition
|
|
528
|
+
# own addition. `parse` tries addition before `parse_block_opener`,
|
|
461
529
|
# so a paren-only depth count split
|
|
462
530
|
# kings.any? { |k| k.square.file == to.file + 1 && ... }
|
|
463
531
|
# at that inner `+`, turning the whole expression into a nonsense
|
|
@@ -468,6 +536,11 @@ module Hecks
|
|
|
468
536
|
# downstream chess domain's castling given; the evaluator's own
|
|
469
537
|
# top_level_index has counted braces since its own version of this
|
|
470
538
|
# exact lesson.
|
|
539
|
+
#
|
|
540
|
+
# @param expr [String] the leaf expression text
|
|
541
|
+
# @return [Array(String, String), nil] the `[left, right]` operand
|
|
542
|
+
# text around the first top-level `+`, or `nil` when `expr` has
|
|
543
|
+
# none
|
|
471
544
|
def split_addition(expr)
|
|
472
545
|
depth = 0
|
|
473
546
|
quote = nil
|
|
@@ -481,12 +554,13 @@ module Hecks
|
|
|
481
554
|
# comment already names, a third time (found live via the
|
|
482
555
|
# type-directed bounded-exhaustive expression generator,
|
|
483
556
|
# Phase 7 of the equivalence-gap plan): `ArrayLiteral` can
|
|
484
|
-
# appear as a general sub-expression
|
|
485
|
-
# `.include?`'s own haystack, so
|
|
486
|
-
#
|
|
487
|
-
#
|
|
488
|
-
#
|
|
489
|
-
#
|
|
557
|
+
# appear as a general sub-expression, not only as
|
|
558
|
+
# `.include?`'s own haystack, so without counting `[`/`]`
|
|
559
|
+
# toward depth here too, an array element containing its own
|
|
560
|
+
# top-level `+` (`[0, 0 + 0]`) reads as this expression's own
|
|
561
|
+
# addition split point -- tearing the whole receiver before
|
|
562
|
+
# `.all?`/`.any?`/etc. in half before `parse_block_opener`
|
|
563
|
+
# ever sees it as one atomic leaf.
|
|
490
564
|
elsif ["(", "{", "["].include?(char)
|
|
491
565
|
depth += 1
|
|
492
566
|
elsif [")", "}", "]"].include?(char)
|
|
@@ -505,6 +579,16 @@ module Hecks
|
|
|
505
579
|
# it. A Float sum that is not finite is the same fault (C3.4).
|
|
506
580
|
INT64_RANGE = (-(2**63))..((2**63) - 1)
|
|
507
581
|
|
|
582
|
+
# Adds two already-interpreted numeric operands, refusing an
|
|
583
|
+
# out-of-range result rather than letting it wrap or promote
|
|
584
|
+
# silently.
|
|
585
|
+
#
|
|
586
|
+
# @param left [Integer, Float] the left operand
|
|
587
|
+
# @param right [Integer, Float] the right operand
|
|
588
|
+
# @return [Integer, Float] `left + right`
|
|
589
|
+
# @raise [EvaluationError] if either operand is not numeric, or the
|
|
590
|
+
# Integer sum falls outside signed 64-bit range, or the Float sum
|
|
591
|
+
# is not finite
|
|
508
592
|
def add(left, right)
|
|
509
593
|
lhs = require_number(left, "addition")
|
|
510
594
|
rhs = require_number(right, "addition")
|
|
@@ -519,6 +603,12 @@ module Hecks
|
|
|
519
603
|
raise EvaluationError, "addition overflowed: #{lhs} + #{rhs} is not a finite number"
|
|
520
604
|
end
|
|
521
605
|
|
|
606
|
+
# Reports whether `expr` is a whole string literal — `"` or `'`
|
|
607
|
+
# delimited, both ends.
|
|
608
|
+
#
|
|
609
|
+
# @param expr [String] the leaf expression text
|
|
610
|
+
# @return [Boolean] whether `expr` opens and closes with matching
|
|
611
|
+
# quote characters
|
|
522
612
|
def quoted?(expr)
|
|
523
613
|
return false if expr.length < 2
|
|
524
614
|
|
|
@@ -532,67 +622,111 @@ module Hecks
|
|
|
532
622
|
# which admit the same set for the same reason.
|
|
533
623
|
SIZED_TYPES = Hecks::Vocabulary.fetch("SizedType")
|
|
534
624
|
|
|
625
|
+
# Counts `value`'s elements or characters, for `.length`/`.size`.
|
|
626
|
+
#
|
|
627
|
+
# @param value [Array, String, Hash] the already-interpreted
|
|
628
|
+
# receiver value
|
|
629
|
+
# @return [Integer] the number of elements (Array/Hash) or
|
|
630
|
+
# characters (String) `value` holds
|
|
631
|
+
# @raise [EvaluationError] if `value` is not an Array, String, or
|
|
632
|
+
# Hash
|
|
535
633
|
def size_of(value)
|
|
536
634
|
return value.size if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
|
|
537
635
|
|
|
538
636
|
raise EvaluationError, "size expects a list or string, got #{describe(value)}"
|
|
539
637
|
end
|
|
540
638
|
|
|
639
|
+
# Reports whether `value` holds no elements or characters, for
|
|
640
|
+
# `.empty?`.
|
|
641
|
+
#
|
|
642
|
+
# @param value [Array, String, Hash] the already-interpreted
|
|
643
|
+
# receiver value
|
|
644
|
+
# @return [Boolean] whether `value` holds no elements or characters
|
|
645
|
+
# @raise [EvaluationError] if `value` is not an Array, String, or
|
|
646
|
+
# Hash
|
|
541
647
|
def emptiness_of(value)
|
|
542
648
|
return value.empty? if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
|
|
543
649
|
|
|
544
650
|
raise EvaluationError, "empty? expects a list or string, got #{describe(value)}"
|
|
545
651
|
end
|
|
546
652
|
|
|
547
|
-
# `.split("SEP")` -- vendored
|
|
548
|
-
# own comment above. Only a
|
|
549
|
-
# split -- unlike `.length`/`.size
|
|
550
|
-
# meaningful over Array/Hash too,
|
|
551
|
-
# method in the corpus's own usage
|
|
552
|
-
# pass splits a Phrase's own string
|
|
653
|
+
# Splits `value` on `separator`, for `.split("SEP")` -- vendored
|
|
654
|
+
# addition, see the `Split` struct's own comment above. Only a
|
|
655
|
+
# String receiver makes sense to split -- unlike `.length`/`.size`/
|
|
656
|
+
# `.empty?`, which are already meaningful over Array/Hash too,
|
|
657
|
+
# `.split` is a String-only method in the corpus's own usage
|
|
658
|
+
# (every occurrence found this pass splits a Phrase's own string
|
|
659
|
+
# value).
|
|
660
|
+
#
|
|
661
|
+
# @param value [String] the already-interpreted receiver value
|
|
662
|
+
# @param separator [String] the literal separator text
|
|
663
|
+
# @return [Array<String>] `value` split on `separator`, Ruby's own
|
|
664
|
+
# `String#split` semantics
|
|
665
|
+
# @raise [EvaluationError] if `value` is not a String
|
|
553
666
|
def split_value(value, separator)
|
|
554
667
|
raise EvaluationError, "split expects a string, got #{describe(value)}" unless value.is_a?(String)
|
|
555
668
|
|
|
556
669
|
value.split(separator)
|
|
557
670
|
end
|
|
558
671
|
|
|
559
|
-
#
|
|
560
|
-
# comment above. Duck-typed
|
|
561
|
-
# hard-coding Array -- the one
|
|
562
|
-
# (`Query::Phrase`'s
|
|
563
|
-
# `Split`-produced Array,
|
|
564
|
-
# Array-specific, and this
|
|
565
|
-
# duck-typed-over-a-known-set
|
|
566
|
-
# narrower rule than the method
|
|
672
|
+
# Returns `value`'s last element, for `.last` -- vendored
|
|
673
|
+
# addition, see the `Last` struct's own comment above. Duck-typed
|
|
674
|
+
# on `respond_to?(:last)` rather than hard-coding Array -- the one
|
|
675
|
+
# corpus usage found this pass (`Query::Phrase`'s
|
|
676
|
+
# `.split("::").last`) always receives a `Split`-produced Array,
|
|
677
|
+
# but nothing about `.last` itself is Array-specific, and this
|
|
678
|
+
# matches `Empty`/`Size`'s own duck-typed-over-a-known-set
|
|
679
|
+
# precedent without inventing a narrower rule than the method
|
|
680
|
+
# needs.
|
|
681
|
+
#
|
|
682
|
+
# @param value [#last] the already-interpreted receiver value
|
|
683
|
+
# @return [Object] `value.last`
|
|
684
|
+
# @raise [EvaluationError] if `value` does not respond to `:last`
|
|
567
685
|
def last_of(value)
|
|
568
686
|
return value.last if value.respond_to?(:last)
|
|
569
687
|
|
|
570
688
|
raise EvaluationError, "last expects a list, got #{describe(value)}"
|
|
571
689
|
end
|
|
572
690
|
|
|
573
|
-
# `.first` -- see the
|
|
574
|
-
# `last_of` with the one
|
|
575
|
-
# reasoning.
|
|
691
|
+
# Returns `value`'s first element, for `.first` -- see the
|
|
692
|
+
# `First` struct's own comment above. `last_of` with the one
|
|
693
|
+
# method swapped, same duck-typed reasoning.
|
|
694
|
+
#
|
|
695
|
+
# @param value [#first] the already-interpreted receiver value
|
|
696
|
+
# @return [Object] `value.first`
|
|
697
|
+
# @raise [EvaluationError] if `value` does not respond to `:first`
|
|
576
698
|
def first_of(value)
|
|
577
699
|
return value.first if value.respond_to?(:first)
|
|
578
700
|
|
|
579
701
|
raise EvaluationError, "first expects a list, got #{describe(value)}"
|
|
580
702
|
end
|
|
581
703
|
|
|
704
|
+
# Reports whether `value` starts with `substring`, for
|
|
582
705
|
# `.start_with?("prefix")` -- vendored addition, see the
|
|
583
706
|
# `StartsWith` struct's own comment above. String-only, same
|
|
584
707
|
# reasoning as `.split` above -- every corpus usage found this
|
|
585
708
|
# pass (`Params`'s own JSON-object-shape invariant) receives a
|
|
586
709
|
# plain String field.
|
|
710
|
+
#
|
|
711
|
+
# @param value [String] the already-interpreted receiver value
|
|
712
|
+
# @param substring [String] the literal prefix text
|
|
713
|
+
# @return [Boolean] whether `value` starts with `substring`
|
|
714
|
+
# @raise [EvaluationError] if `value` is not a String
|
|
587
715
|
def starts_with?(value, substring)
|
|
588
716
|
raise EvaluationError, "start_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
|
|
589
717
|
|
|
590
718
|
value.start_with?(substring)
|
|
591
719
|
end
|
|
592
720
|
|
|
721
|
+
# Reports whether `value` ends with `substring`, for
|
|
593
722
|
# `.end_with?("suffix")` -- vendored addition, see the `EndsWith`
|
|
594
723
|
# struct's own comment above. Same String-only reasoning as
|
|
595
724
|
# `start_with?` immediately above.
|
|
725
|
+
#
|
|
726
|
+
# @param value [String] the already-interpreted receiver value
|
|
727
|
+
# @param substring [String] the literal suffix text
|
|
728
|
+
# @return [Boolean] whether `value` ends with `substring`
|
|
729
|
+
# @raise [EvaluationError] if `value` is not a String
|
|
596
730
|
def ends_with?(value, substring)
|
|
597
731
|
raise EvaluationError, "end_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
|
|
598
732
|
|
|
@@ -604,6 +738,13 @@ module Hecks
|
|
|
604
738
|
# holds this equal to the language.
|
|
605
739
|
TO_STRING_TYPES = Hecks::Vocabulary.fetch("ToStringType")
|
|
606
740
|
|
|
741
|
+
# Coerces `value` to its string form, for `.to_s`.
|
|
742
|
+
#
|
|
743
|
+
# @param value [String, Integer, Float, Boolean, nil] the
|
|
744
|
+
# already-interpreted receiver value
|
|
745
|
+
# @return [String] `value` unchanged if already a String,
|
|
746
|
+
# `value.to_s` for a numeric or boolean, or `""` for `nil`
|
|
747
|
+
# @raise [EvaluationError] if `value` is not one of the above
|
|
607
748
|
def string_of(value)
|
|
608
749
|
case value
|
|
609
750
|
when String then value
|
|
@@ -614,6 +755,16 @@ module Hecks
|
|
|
614
755
|
end
|
|
615
756
|
end
|
|
616
757
|
|
|
758
|
+
# Splits `expr` at the first of `suffixes` it ends with — the
|
|
759
|
+
# shared lookup behind every `.positive?`/`.negative?`/`.zero?`-
|
|
760
|
+
# style sign test.
|
|
761
|
+
#
|
|
762
|
+
# @param expr [String] the leaf expression text
|
|
763
|
+
# @param suffixes [Array<String>] the admitted suffix names
|
|
764
|
+
# (without the leading `.`), tried in order
|
|
765
|
+
# @return [Array(String, String), nil] the `[receiver_text,
|
|
766
|
+
# matched_suffix]` pair for the first matching suffix, or `nil`
|
|
767
|
+
# if none matches
|
|
617
768
|
def match_suffix(expr, suffixes)
|
|
618
769
|
suffixes.each do |suffix|
|
|
619
770
|
marker = ".#{suffix}"
|
|
@@ -622,6 +773,14 @@ module Hecks
|
|
|
622
773
|
nil
|
|
623
774
|
end
|
|
624
775
|
|
|
776
|
+
# Applies `node`'s comparison (`positive?`/`negative?`/`zero?`)
|
|
777
|
+
# against the literal `0`, reusing `Evaluator.apply`'s own
|
|
778
|
+
# comparison primitives rather than re-deriving them by hand.
|
|
779
|
+
#
|
|
780
|
+
# @param node [SignTest] the sign-test node being evaluated
|
|
781
|
+
# @param value [Object] the already-interpreted receiver value
|
|
782
|
+
# @return [Boolean] whether `value` passes `node`'s own sign test
|
|
783
|
+
# @raise [EvaluationError] if `value` is not numeric
|
|
625
784
|
def apply_sign_test(node, value)
|
|
626
785
|
number = numeric(value)
|
|
627
786
|
raise EvaluationError, "#{node.test} expects a number, got #{describe(value)}" unless number
|
|
@@ -629,50 +788,58 @@ module Hecks
|
|
|
629
788
|
Evaluator.apply(node.operator, number, 0)
|
|
630
789
|
end
|
|
631
790
|
|
|
632
|
-
#
|
|
791
|
+
# Found live via the type-directed bounded-exhaustive expression
|
|
633
792
|
# generator (Phase 7, equivalence-gap plan — spec/
|
|
634
793
|
# bounded_exhaustive_expression_spec.rb): `.modulo(`'s own
|
|
635
794
|
# argument position accepts any numeric sub-expression, including
|
|
636
|
-
#
|
|
795
|
+
# another `.modulo(...)` call — `0.modulo(num_b.modulo(-1))` is
|
|
637
796
|
# perfectly well-typed — but `expr.rindex(marker)` finds the
|
|
638
|
-
#
|
|
639
|
-
#
|
|
640
|
-
# it found the
|
|
797
|
+
# rightmost (innermost) `.modulo(` in the whole string, not the
|
|
798
|
+
# outermost one a nested call needs split at. For that expression
|
|
799
|
+
# it found the inner `.modulo(` (inside `num_b.modulo(-1)`) and
|
|
641
800
|
# split there, producing a receiver of `"0.modulo(num_b"` and a
|
|
642
801
|
# divisor of `"-1)"` — both garbage, both re-parsed as bogus
|
|
643
802
|
# `Lookup` paths, both then refusing with "cannot resolve" — a
|
|
644
|
-
#
|
|
803
|
+
# silent misparse that happened to fail safe into a real
|
|
645
804
|
# `EvaluationError` rather than a raw crash, which is exactly why
|
|
646
805
|
# this had gone unnoticed: nothing before this generator existed
|
|
647
806
|
# ever fed `.modulo` a nested `.modulo` call, random fuzzing
|
|
648
807
|
# essentially never manufactures that specific shape by chance,
|
|
649
|
-
# and the resulting refusal
|
|
808
|
+
# and the resulting refusal looks like an ordinary, correct one
|
|
650
809
|
# unless you already know every name this generator's own
|
|
651
810
|
# synthetic state declares (real corpus authors would see this as
|
|
652
811
|
# a mysterious "cannot resolve" on text they never wrote).
|
|
653
812
|
#
|
|
654
813
|
# Fixed the same way `split_addition`/`Evaluator.top_level_index`
|
|
655
814
|
# already handle nested `(`/`{` elsewhere in this exact file:
|
|
656
|
-
# find the
|
|
657
|
-
# then track paren/quote depth from there to find
|
|
815
|
+
# find the first (leftmost, outermost) occurrence of the marker,
|
|
816
|
+
# then track paren/quote depth from there to find its own
|
|
658
817
|
# matching close — not just strip the string's own trailing `)`
|
|
659
818
|
# and hope it belongs to this call.
|
|
660
|
-
# Not just the
|
|
661
|
-
# (`x.modulo(a).modulo(b)`, the receiver of the
|
|
819
|
+
# Not just the first occurrence, either — `.modulo` also chains
|
|
820
|
+
# (`x.modulo(a).modulo(b)`, the receiver of the outer call itself
|
|
662
821
|
# ending in a `.modulo(...)` call), a second real shape the
|
|
663
822
|
# leftmost-occurrence-only version of this fix still mis-parsed:
|
|
664
823
|
# the first `.modulo(`'s own matching close paren lands mid-
|
|
665
824
|
# string (right after `a)`, before the second `.modulo(b)`), so
|
|
666
825
|
# it correctly fails the "reaches the end" check below and must
|
|
667
|
-
# be tried again at the
|
|
668
|
-
# Trying occurrences strictly left to right and taking the
|
|
826
|
+
# be tried again at the next occurrence rather than giving up.
|
|
827
|
+
# Trying occurrences strictly left to right and taking the first
|
|
669
828
|
# one whose matching close reaches the string's last character
|
|
670
|
-
# handles both shapes with the same rule: for
|
|
829
|
+
# handles both shapes with the same rule: for nesting
|
|
671
830
|
# (`.modulo(x.modulo(y))`), the leftmost (outer) occurrence's own
|
|
672
831
|
# paren-depth tracking already walks straight through the inner
|
|
673
|
-
# call to the true final `)`; for
|
|
832
|
+
# call to the true final `)`; for chaining, the leftmost
|
|
674
833
|
# occurrence's close lands short and is rejected, so the next
|
|
675
834
|
# occurrence (the true outermost call) is tried instead.
|
|
835
|
+
#
|
|
836
|
+
# @param expr [String] the leaf expression text
|
|
837
|
+
# @param marker [String] the call-opening text to search for, such
|
|
838
|
+
# as `".modulo("`
|
|
839
|
+
# @return [Array(String, String), nil] the `[receiver_text,
|
|
840
|
+
# argument_text]` pair for the outermost occurrence of `marker`
|
|
841
|
+
# whose matching close paren reaches `expr`'s last character, or
|
|
842
|
+
# `nil` if none does
|
|
676
843
|
def match_call(expr, marker)
|
|
677
844
|
start = 0
|
|
678
845
|
while (index = expr.index(marker, start))
|
|
@@ -684,10 +851,17 @@ module Hecks
|
|
|
684
851
|
nil
|
|
685
852
|
end
|
|
686
853
|
|
|
687
|
-
# `
|
|
688
|
-
#
|
|
689
|
-
#
|
|
690
|
-
#
|
|
854
|
+
# Finds the index of the `)` that closes the `(` already consumed
|
|
855
|
+
# by the caller. `matching_brace` (resolver/block_predicates.rb)'s
|
|
856
|
+
# own twin, one bracket pair over: `start` is the index just past
|
|
857
|
+
# the opening `(` already consumed by the caller (depth starts at
|
|
858
|
+
# 1, not 0, for the same reason).
|
|
859
|
+
#
|
|
860
|
+
# @param expr [String] the source text to scan
|
|
861
|
+
# @param start [Integer] the index just past the already-consumed
|
|
862
|
+
# opening `(`
|
|
863
|
+
# @return [Integer, nil] the index of the matching `)`, or `nil` if
|
|
864
|
+
# `expr` has no balanced close from `start` onward
|
|
691
865
|
def matching_paren(expr, start)
|
|
692
866
|
depth = 1
|
|
693
867
|
quote = nil
|
|
@@ -709,12 +883,12 @@ module Hecks
|
|
|
709
883
|
nil
|
|
710
884
|
end
|
|
711
885
|
|
|
712
|
-
# Both operands are coerced to a real Integer/Float
|
|
713
|
-
# zero-check, and the check reads the
|
|
886
|
+
# Both operands are coerced to a real Integer/Float before the
|
|
887
|
+
# zero-check, and the check reads the coerced divisor — not the
|
|
714
888
|
# raw `divisor_value` (which might not even respond to `.zero?`,
|
|
715
889
|
# a String for instance) and not a `.to_i`-truncated stand-in for
|
|
716
890
|
# it either. The old order checked a truncated `divisor.to_i`
|
|
717
|
-
#
|
|
891
|
+
# after already validating the untruncated value wasn't zero, so
|
|
718
892
|
# a divisor merely small (`0.3`, truncating to `0`) sailed past
|
|
719
893
|
# the guard and then blew up `Integer#%` with a raw
|
|
720
894
|
# `ZeroDivisionError` the moment it reached zero anyway.
|
|
@@ -726,6 +900,15 @@ module Hecks
|
|
|
726
900
|
# operands down to Integer first was pure data loss with no
|
|
727
901
|
# purpose: `7.5.modulo(2.5)` silently became `7 % 2` (`1`)
|
|
728
902
|
# instead of the real `0.0`.
|
|
903
|
+
#
|
|
904
|
+
# @param receiver_value [Object] the already-interpreted receiver
|
|
905
|
+
# value
|
|
906
|
+
# @param divisor_value [Object] the already-interpreted divisor
|
|
907
|
+
# value
|
|
908
|
+
# @return [Integer, Float] `receiver % divisor`, on the coerced
|
|
909
|
+
# numeric operands
|
|
910
|
+
# @raise [EvaluationError] if either operand is not numeric, or the
|
|
911
|
+
# coerced divisor is zero
|
|
729
912
|
def apply_modulo(receiver_value, divisor_value)
|
|
730
913
|
receiver = require_number(receiver_value, "modulo")
|
|
731
914
|
divisor = require_number(divisor_value, "modulo")
|
|
@@ -734,6 +917,20 @@ module Hecks
|
|
|
734
917
|
receiver % divisor
|
|
735
918
|
end
|
|
736
919
|
|
|
920
|
+
# Resolves a `Lookup` node's dotted path against `attrs` (checked
|
|
921
|
+
# first) and `state`, unwrapping a single-field value object's
|
|
922
|
+
# result to its own scalar.
|
|
923
|
+
#
|
|
924
|
+
# @param expr [String] the dotted (or bare) attribute path, such as
|
|
925
|
+
# `"customer.status"`
|
|
926
|
+
# @param state [Hash{Symbol => Object}] the stored attribute values
|
|
927
|
+
# the path's root name may resolve against
|
|
928
|
+
# @param attrs [Hash{Symbol => Object}] the call's own argument
|
|
929
|
+
# values, checked before `state`
|
|
930
|
+
# @return [Object] the resolved value, unwrapped by `unwrap_scalar`
|
|
931
|
+
# @raise [EvaluationError] if the path's root name is in neither
|
|
932
|
+
# `attrs` nor `state`, or a later segment cannot be read from the
|
|
933
|
+
# value it navigates onto
|
|
737
934
|
def lookup(expr, state, attrs)
|
|
738
935
|
return unwrap_scalar(fetch(expr, state, attrs)) unless expr.include?(".")
|
|
739
936
|
|
|
@@ -750,6 +947,16 @@ module Hecks
|
|
|
750
947
|
# which spelling answers — a bare `||` between the two would
|
|
751
948
|
# treat a genuinely-held `false` the same as an absent key and
|
|
752
949
|
# fall through to the other spelling, landing on `nil`.
|
|
950
|
+
#
|
|
951
|
+
# @param value [Object] the already-resolved starting value; `nil`
|
|
952
|
+
# once any segment lands on a value that does not respond to
|
|
953
|
+
# `#[]`
|
|
954
|
+
# @param segments [Array<String>] the dotted path's remaining
|
|
955
|
+
# segments, in order
|
|
956
|
+
# @return [Object, nil] the value reached by walking every segment,
|
|
957
|
+
# or `nil` if a segment lands on a value with no `#[]`
|
|
958
|
+
# @raise [EvaluationError] if a segment cannot be read from an
|
|
959
|
+
# Array-like value (a non-Integer/Range index)
|
|
753
960
|
def walk_path(value, segments)
|
|
754
961
|
segments.reduce(value) do |current, segment|
|
|
755
962
|
break nil unless current.respond_to?(:[])
|
|
@@ -766,10 +973,10 @@ module Hecks
|
|
|
766
973
|
# or a `list_of` attribute) — Array#[] demands an
|
|
767
974
|
# Integer/Range and raises a raw TypeError for a String
|
|
768
975
|
# segment ("no implicit conversion of String into
|
|
769
|
-
# Integer")
|
|
770
|
-
# sublanguage's own refusal
|
|
771
|
-
#
|
|
772
|
-
#
|
|
976
|
+
# Integer"). This rescue catches it and re-raises as this
|
|
977
|
+
# sublanguage's own refusal ("this predicate doesn't apply
|
|
978
|
+
# here"), rather than letting the raw TypeError cross this
|
|
979
|
+
# sublanguage's own refusal boundary and crash the runtime.
|
|
773
980
|
raise EvaluationError,
|
|
774
981
|
"cannot read #{segment.inspect} from #{describe(current)}"
|
|
775
982
|
end
|
|
@@ -777,6 +984,7 @@ module Hecks
|
|
|
777
984
|
end
|
|
778
985
|
end
|
|
779
986
|
|
|
987
|
+
# Unwraps a single-field value object to its own scalar, so
|
|
780
988
|
# `field == "literal"` -- vendored addition, not (yet) upstream
|
|
781
989
|
# hecks (migration plan task 8): the third-most pervasive
|
|
782
990
|
# dispatch-time gap this pass found, same family as `.match?`/
|
|
@@ -785,7 +993,7 @@ module Hecks
|
|
|
785
993
|
# from_identifier`/`Value::Coercion#fields_for`'s own single-
|
|
786
994
|
# field auto-unwrap already treats as "this VO IS its scalar"
|
|
787
995
|
# everywhere else in this runtime) came back as the `Value`
|
|
788
|
-
# wrapper itself, never unwrapped for
|
|
996
|
+
# wrapper itself, never unwrapped for reading -- so `Value#==`
|
|
789
997
|
# (which only ever equals another `Value` instance) silently
|
|
790
998
|
# refused every `guarantees "..." do status == "active" end` /
|
|
791
999
|
# `expects "..." do trash_day.present? end`-shaped bare
|
|
@@ -794,48 +1002,46 @@ module Hecks
|
|
|
794
1002
|
# "literal"` shape in plan.bluebook, service_task.bluebook,
|
|
795
1003
|
# route.bluebook, and subscription.bluebook, all equally silent
|
|
796
1004
|
# until a real dispatch (never validate) exercised the
|
|
797
|
-
# predicate.
|
|
798
|
-
#
|
|
799
|
-
#
|
|
800
|
-
#
|
|
801
|
-
#
|
|
802
|
-
#
|
|
803
|
-
#
|
|
804
|
-
#
|
|
805
|
-
#
|
|
806
|
-
#
|
|
807
|
-
#
|
|
808
|
-
#
|
|
809
|
-
#
|
|
810
|
-
#
|
|
811
|
-
#
|
|
812
|
-
#
|
|
813
|
-
#
|
|
814
|
-
#
|
|
815
|
-
#
|
|
816
|
-
#
|
|
817
|
-
#
|
|
818
|
-
#
|
|
819
|
-
#
|
|
820
|
-
#
|
|
821
|
-
#
|
|
822
|
-
#
|
|
823
|
-
#
|
|
824
|
-
#
|
|
825
|
-
#
|
|
826
|
-
#
|
|
827
|
-
#
|
|
828
|
-
#
|
|
829
|
-
#
|
|
830
|
-
#
|
|
831
|
-
#
|
|
832
|
-
#
|
|
833
|
-
#
|
|
834
|
-
#
|
|
835
|
-
#
|
|
836
|
-
#
|
|
837
|
-
# Rust side — change them in lockstep or rust_conformance
|
|
838
|
-
# diverges.
|
|
1005
|
+
# predicate.
|
|
1006
|
+
#
|
|
1007
|
+
# Unwraps both the bare (undotted) case and a dotted lookup's own
|
|
1008
|
+
# terminal value: a dotted lookup that navigates through an
|
|
1009
|
+
# entity/list element to a nested field (`leg.voyage`, where
|
|
1010
|
+
# `voyage` is itself a single-field VO) lands on the same
|
|
1011
|
+
# unwrapped-`Value` shape a bare lookup does, and hits the
|
|
1012
|
+
# identical silent `Value#==` failure without the same
|
|
1013
|
+
# "this VO IS its scalar" treatment — only the intermediate hops
|
|
1014
|
+
# need raw `#[]` addressing to keep navigating. `unwrap_scalar` is
|
|
1015
|
+
# idempotent on an already-raw scalar (a String/Integer doesn't
|
|
1016
|
+
# respond to `#to_h`), so this is safe for `field.value`-shaped
|
|
1017
|
+
# dotted lookups too — they already return a raw scalar and stay
|
|
1018
|
+
# unaffected.
|
|
1019
|
+
#
|
|
1020
|
+
# Gated on the sole attribute's *count*, never its *name*: gating
|
|
1021
|
+
# on a literal `:value` key would be a lie of omission for
|
|
1022
|
+
# `Money{amount}` and every other single-field value object whose
|
|
1023
|
+
# author picked a domain name for the field — the same
|
|
1024
|
+
# "this VO IS its scalar" reading
|
|
1025
|
+
# ([[feedback_name_the_scalar_field]],
|
|
1026
|
+
# `Behaviour::ValueObject#sole_attribute`) applies regardless of
|
|
1027
|
+
# what the sole field happens to be called, and a name gate would
|
|
1028
|
+
# make a bare `balance > 0` work for a `Balance{value}` while
|
|
1029
|
+
# silently comparing a whole VO for a `Balance{amount}`. A
|
|
1030
|
+
# declared `Runtime::Value` reads its own `sole_attribute` (the
|
|
1031
|
+
# declaration's answer, not the stored hash's); any other
|
|
1032
|
+
# to_h-able (a Struct, a bespoke wrapper with no declaration to
|
|
1033
|
+
# consult) keeps the `{value: X}`-only unwrap, so nothing that
|
|
1034
|
+
# never was a value object gains a surprise unwrapping.
|
|
1035
|
+
# `rust/src/kernel/json.rs`'s `impl Fielded for Json` mirrors the
|
|
1036
|
+
# count-only reading on the Rust side — change them in lockstep
|
|
1037
|
+
# or rust_conformance diverges.
|
|
1038
|
+
#
|
|
1039
|
+
# @param value [Object] a resolved lookup or path-walk result
|
|
1040
|
+
# @return [Object] `value` itself if it is a Hash, Array, or has no
|
|
1041
|
+
# `#to_h`; a declared `Runtime::Value`'s sole attribute's own
|
|
1042
|
+
# value, read via `sole.name`, if it has exactly one attribute;
|
|
1043
|
+
# `value[:value]` for any other to_h-able with exactly one key
|
|
1044
|
+
# named `:value`; `value` unchanged otherwise
|
|
839
1045
|
def unwrap_scalar(value)
|
|
840
1046
|
return value unless value.respond_to?(:to_h) && !value.is_a?(Hash) && !value.is_a?(Array)
|
|
841
1047
|
|
|
@@ -848,6 +1054,17 @@ module Hecks
|
|
|
848
1054
|
hash.size == 1 && hash.key?(:value) ? hash[:value] : value
|
|
849
1055
|
end
|
|
850
1056
|
|
|
1057
|
+
# Resolves a single unqualified name — `attrs` first, `state`
|
|
1058
|
+
# second.
|
|
1059
|
+
#
|
|
1060
|
+
# @param name [String, Symbol] the attribute or argument name
|
|
1061
|
+
# @param state [Hash{Symbol => Object}, #[]] the stored attribute
|
|
1062
|
+
# values
|
|
1063
|
+
# @param attrs [Hash{Symbol => Object}] the call's own argument
|
|
1064
|
+
# values, checked first
|
|
1065
|
+
# @return [Object] the value held under `name`
|
|
1066
|
+
# @raise [EvaluationError] if `name` is in neither `attrs` nor
|
|
1067
|
+
# `state`
|
|
851
1068
|
def fetch(name, state, attrs)
|
|
852
1069
|
key = name.to_sym
|
|
853
1070
|
return attrs[key] if attrs.key?(key)
|
|
@@ -856,21 +1073,45 @@ module Hecks
|
|
|
856
1073
|
raise EvaluationError, "cannot resolve #{name.inspect} — no such attribute or argument"
|
|
857
1074
|
end
|
|
858
1075
|
|
|
1076
|
+
# Reports whether `state` holds `key`, duck-typed for a `state`
|
|
1077
|
+
# that does not respond to `:key?`.
|
|
1078
|
+
#
|
|
1079
|
+
# @param state [Hash{Symbol => Object}, #[]] the stored attribute
|
|
1080
|
+
# values
|
|
1081
|
+
# @param key [Symbol] the attribute name to look for
|
|
1082
|
+
# @return [Boolean] `state.key?(key)` when `state` responds to
|
|
1083
|
+
# `:key?`; otherwise whether `state[key]` is non-nil
|
|
859
1084
|
def known?(state, key)
|
|
860
1085
|
return state.key?(key) if state.respond_to?(:key?)
|
|
861
1086
|
|
|
862
1087
|
!state[key].nil?
|
|
863
1088
|
end
|
|
864
1089
|
|
|
1090
|
+
# Returns `value` if it is already numeric, or `nil` if it is not.
|
|
1091
|
+
#
|
|
1092
|
+
# @param value [Object] the value to check
|
|
1093
|
+
# @return [Integer, Float, nil] `value` unchanged if it is an
|
|
1094
|
+
# Integer or Float, otherwise `nil`
|
|
865
1095
|
def numeric(value)
|
|
866
1096
|
value if value.is_a?(Integer) || value.is_a?(Float)
|
|
867
1097
|
end
|
|
868
1098
|
|
|
1099
|
+
# Coerces `value` to a number or refuses it.
|
|
1100
|
+
#
|
|
1101
|
+
# @param value [Object] the value to check
|
|
1102
|
+
# @param operation [String] the operation name, for the refusal
|
|
1103
|
+
# message
|
|
1104
|
+
# @return [Integer, Float] `value` unchanged
|
|
1105
|
+
# @raise [EvaluationError] if `value` is not an Integer or Float
|
|
869
1106
|
def require_number(value, operation)
|
|
870
1107
|
numeric(value) ||
|
|
871
1108
|
raise(EvaluationError, "#{operation} expects a number, got #{describe(value)}")
|
|
872
1109
|
end
|
|
873
1110
|
|
|
1111
|
+
# Renders `value` the way it should read inside a refusal message.
|
|
1112
|
+
#
|
|
1113
|
+
# @param value [Object] the value to render
|
|
1114
|
+
# @return [String] `Rendering.describe(value)`
|
|
874
1115
|
def describe(value) = Rendering.describe(value)
|
|
875
1116
|
end
|
|
876
1117
|
end
|