hecks 2.8.0 → 2.9.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/exe/hecks +4 -6
- data/lib/hecks/adapters/driven/claude_code.rb +4 -104
- data/lib/hecks/adapters/driven/d1.rb +44 -237
- data/lib/hecks/adapters/driven/folder.rb +17 -76
- data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
- data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
- data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
- data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
- data/lib/hecks/adapters/driven/heki.rb +13 -42
- data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
- data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
- data/lib/hecks/adapters/driven/lambda.rb +10 -84
- data/lib/hecks/adapters/driven/local_storage.rb +5 -40
- data/lib/hecks/adapters/driven/memory.rb +17 -98
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
- data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
- data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
- data/lib/hecks/adapters/driven/postgres.rb +145 -134
- data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
- data/lib/hecks/adapters/driven/prism.rb +7 -30
- data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
- data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
- data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
- data/lib/hecks/adapters/driven/sqlite.rb +128 -65
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +5 -48
- data/lib/hecks/adapters/driven.rb +5 -36
- data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
- data/lib/hecks/behaviors/dsl.rb +7 -25
- data/lib/hecks/behaviors/expectations.rb +31 -141
- data/lib/hecks/behaviors/ir.rb +7 -20
- data/lib/hecks/behaviors/rspec.rb +6 -17
- data/lib/hecks/behaviors/runner.rb +10 -34
- data/lib/hecks/behaviors.rb +4 -8
- data/lib/hecks/bluebook/aggregate.rb +9 -75
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
- data/lib/hecks/bluebook/assembly/build.rb +4 -15
- data/lib/hecks/bluebook/assembly/contract.rb +15 -136
- data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
- data/lib/hecks/bluebook/assembly/marks.rb +31 -223
- data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
- data/lib/hecks/bluebook/assembly.rb +6 -45
- data/lib/hecks/bluebook/attribute.rb +12 -61
- data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
- data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
- data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
- data/lib/hecks/bluebook/behaviour/command.rb +14 -66
- data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
- data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
- data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
- data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
- data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
- data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
- data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
- data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
- data/lib/hecks/bluebook/capabilities.rb +17 -49
- data/lib/hecks/bluebook/chapter.rb +10 -48
- data/lib/hecks/bluebook/command.rb +23 -81
- data/lib/hecks/bluebook/domain_port.rb +14 -63
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
- data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
- data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -194
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
- data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
- data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
- data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
- data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
- data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
- data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
- data/lib/hecks/bluebook/dsl/world_builder.rb +19 -111
- data/lib/hecks/bluebook/entity.rb +13 -71
- data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
- data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
- data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
- data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
- data/lib/hecks/bluebook/expression/resolver.rb +37 -660
- data/lib/hecks/bluebook/hexagon.rb +5 -28
- data/lib/hecks/bluebook/lifecycle.rb +5 -16
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
- data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
- data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
- data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +43 -305
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
- data/lib/hecks/bluebook/meta_validator.rb +79 -349
- data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
- data/lib/hecks/bluebook/model_check.rb +100 -597
- data/lib/hecks/bluebook/pattern_subset.rb +11 -69
- data/lib/hecks/bluebook/process_manager.rb +13 -83
- data/lib/hecks/bluebook/project_loader.rb +3 -39
- data/lib/hecks/bluebook/project_register.rb +12 -60
- data/lib/hecks/bluebook/query.rb +9 -48
- data/lib/hecks/bluebook/read_model.rb +57 -81
- data/lib/hecks/bluebook/reference.rb +7 -41
- data/lib/hecks/bluebook/smoke_test.rb +8 -98
- data/lib/hecks/bluebook/synthesizer.rb +13 -62
- data/lib/hecks/bluebook/translation.rb +26 -62
- data/lib/hecks/bluebook/value_object.rb +11 -36
- data/lib/hecks/bluebook.rb +5 -22
- data/lib/hecks/cache_dir.rb +4 -23
- data/lib/hecks/cli/document.rb +3 -21
- data/lib/hecks/cli/ir.rb +3 -5
- data/lib/hecks/cli/mcp.rb +2 -3
- data/lib/hecks/cli/mcp_door.rb +19 -114
- data/lib/hecks/cli/model_check.rb +18 -85
- data/lib/hecks/cli/project_cli.rb +7 -40
- data/lib/hecks/cli/project_diagrams.rb +0 -7
- data/lib/hecks/cli/run.rb +12 -58
- data/lib/hecks/cli/smoke_test.rb +2 -16
- data/lib/hecks/cli/stores.rb +1 -2
- data/lib/hecks/cli.rb +18 -24
- data/lib/hecks/construct.rb +8 -34
- data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
- data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
- data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
- data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
- data/lib/hecks/embryonaut_bluebook.rb +4 -71
- data/lib/hecks/facade/cli_door.rb +24 -106
- data/lib/hecks/facade/cli_runner.rb +28 -172
- data/lib/hecks/facade/command_request.rb +13 -33
- data/lib/hecks/facade/handle.rb +34 -121
- data/lib/hecks/facade/json_door.rb +31 -146
- data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
- data/lib/hecks/facade/surface/chapter.rb +10 -54
- data/lib/hecks/facade/surface.rb +8 -23
- data/lib/hecks/facade.rb +10 -25
- data/lib/hecks/forms/app.rb +15 -70
- data/lib/hecks/forms/command_form_renderer.rb +10 -29
- data/lib/hecks/forms/field_renderer.rb +17 -156
- data/lib/hecks/forms/field_shape.rb +30 -222
- data/lib/hecks/forms/html.rb +12 -47
- data/lib/hecks/forms/index_renderer.rb +1 -4
- data/lib/hecks/forms/page.rb +3 -9
- data/lib/hecks/forms/params.rb +14 -60
- data/lib/hecks/forms/port_argument.rb +2 -19
- data/lib/hecks/forms/query_form_renderer.rb +4 -79
- data/lib/hecks/forms/record_renderer.rb +8 -75
- data/lib/hecks/forms/record_table.rb +7 -50
- data/lib/hecks/forms/reference_options.rb +3 -30
- data/lib/hecks/forms/value_object_shape.rb +8 -35
- data/lib/hecks/forms.rb +5 -33
- data/lib/hecks/fqn.rb +5 -12
- data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
- data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
- data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
- data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
- data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
- data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
- data/lib/hecks/framework.rb +11 -80
- data/lib/hecks/freezer.rb +5 -39
- data/lib/hecks/grammar/expression.bluebook +15 -40
- data/lib/hecks/grammar/translation.bluebook +3 -6
- data/lib/hecks/grammar.rb +16 -41
- data/lib/hecks/ir.rb +14 -83
- data/lib/hecks/language/adapter.bluebook +2 -7
- data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
- data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
- data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
- data/lib/hecks/language/bluebook/command.bluebook +27 -144
- data/lib/hecks/language/bluebook/entity.bluebook +24 -97
- data/lib/hecks/language/bluebook/policy.bluebook +16 -67
- data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
- data/lib/hecks/language/bluebook/projection.bluebook +126 -60
- data/lib/hecks/language/bluebook/query.bluebook +12 -43
- data/lib/hecks/language/bluebook/shape.bluebook +23 -73
- data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
- data/lib/hecks/language/bluebook/vocabulary.bluebook +82 -197
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
- data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
- data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
- data/lib/hecks/language/oidc.json +35 -0
- data/lib/hecks/language/port.bluebook +4 -17
- data/lib/hecks/language/translation/translation.bluebook +3 -9
- data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
- data/lib/hecks/language/world/wiring.bluebook +1 -3
- data/lib/hecks/language/world/world.bluebook +5 -7
- data/lib/hecks/literal.rb +8 -50
- data/lib/hecks/mcp_door_scope.rb +9 -90
- data/lib/hecks/mcp_stdio_guard.rb +4 -41
- data/lib/hecks/naming.rb +29 -137
- data/lib/hecks/ports/access_control.rb +7 -23
- data/lib/hecks/ports/agent/answers.rb +5 -23
- data/lib/hecks/ports/agent.rb +47 -172
- data/lib/hecks/ports/authentication.rb +12 -39
- data/lib/hecks/ports/authorization.rb +16 -43
- data/lib/hecks/ports/clock.rb +5 -45
- data/lib/hecks/ports/extraction.rb +7 -12
- data/lib/hecks/ports/identity_assignment.rb +8 -29
- data/lib/hecks/ports/identity_generation.rb +6 -29
- data/lib/hecks/ports/identity_resolution.rb +2 -10
- data/lib/hecks/ports/key_vault.rb +9 -45
- data/lib/hecks/ports/persistence/append_only.rb +61 -64
- data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
- data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
- data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
- data/lib/hecks/ports/persistence/plugin.rb +13 -42
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
- data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
- data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
- data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
- data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
- data/lib/hecks/ports/persistence/state_codec.rb +23 -103
- data/lib/hecks/ports/persistence.rb +7 -14
- data/lib/hecks/ports/projection.rb +42 -84
- data/lib/hecks/ports/query/in_memory.rb +10 -58
- data/lib/hecks/ports/query/ordering.rb +11 -32
- data/lib/hecks/ports/query.rb +14 -37
- data/lib/hecks/ports.rb +2 -8
- data/lib/hecks/projections/bootstrap_table.rb +11 -43
- data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
- data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
- data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
- data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
- data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
- data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
- data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
- data/lib/hecks/projections/deploy/fargate.rb +35 -202
- data/lib/hecks/projections/deploy/lambda.rb +150 -678
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
- data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
- data/lib/hecks/projections/deploy/preview/script.rb +2 -14
- data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
- data/lib/hecks/projections/deploy/preview/template.rb +2 -58
- data/lib/hecks/projections/deploy/preview.rb +2 -67
- data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
- data/lib/hecks/projections/deploy/scripts.rb +4 -57
- data/lib/hecks/projections/deploy/shared.rb +47 -146
- data/lib/hecks/projections/deploy/smoke.rb +4 -61
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
- data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
- data/lib/hecks/projections/deploy/template_diff.rb +5 -57
- data/lib/hecks/projections/diagrams.rb +20 -582
- data/lib/hecks/projections/glossary/html.rb +14 -130
- data/lib/hecks/projections/glossary/markdown.rb +8 -69
- data/lib/hecks/projections/glossary/mermaid.rb +9 -66
- data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
- data/lib/hecks/projections/glossary/sentences.rb +14 -159
- data/lib/hecks/projections/glossary.rb +30 -222
- data/lib/hecks/projections/ir.rb +3 -10
- data/lib/hecks/projections/model/deviations.rb +15 -42
- data/lib/hecks/projections/model.rb +12 -51
- data/lib/hecks/projections/oidc.rb +12 -56
- data/lib/hecks/projections/parser_table.rb +9 -28
- data/lib/hecks/projections/reference.rb +2 -15
- data/lib/hecks/projections/rust_vocabulary.rb +12 -250
- data/lib/hecks/projections/shape.rb +3 -29
- data/lib/hecks/projections/statements.rb +7 -86
- data/lib/hecks/projections/vocabulary.rb +6 -22
- data/lib/hecks/projections.rb +2 -16
- data/lib/hecks/projector/cli_projector.rb +60 -375
- data/lib/hecks/projector/docs_projector.rb +10 -201
- data/lib/hecks/projector/exporter.rb +42 -134
- data/lib/hecks/projector/ir_projector.rb +2 -9
- data/lib/hecks/projector/narrate_projector.rb +15 -189
- data/lib/hecks/projector/target.rb +13 -76
- data/lib/hecks/projector.rb +30 -121
- data/lib/hecks/query_specification/common/comparators.rb +5 -26
- data/lib/hecks/query_specification/common/comparison.rb +35 -143
- data/lib/hecks/query_specification/common/dsl.rb +8 -24
- data/lib/hecks/query_specification/common/null_policy.rb +20 -70
- data/lib/hecks/query_specification/common/options.rb +2 -6
- data/lib/hecks/query_specification/common/where_clause.rb +2 -10
- data/lib/hecks/query_specification/field_path.rb +37 -77
- data/lib/hecks/query_specification/hop_path.rb +12 -76
- data/lib/hecks/release/gem_pin.rb +2 -26
- data/lib/hecks/release/runner/ci_publisher.rb +3 -6
- data/lib/hecks/release/runner/commands.rb +2 -4
- data/lib/hecks/release/runner/gem_publisher.rb +2 -3
- data/lib/hecks/release/runner/npm_publisher.rb +6 -20
- data/lib/hecks/release/runner/preflight.rb +4 -19
- data/lib/hecks/release/runner/tagger.rb +2 -9
- data/lib/hecks/release/runner.rb +3 -32
- data/lib/hecks/rendering.rb +4 -26
- data/lib/hecks/router/namespace_installer.rb +12 -36
- data/lib/hecks/router.rb +4 -8
- data/lib/hecks/runtime/aggregate_lock.rb +8 -33
- data/lib/hecks/runtime/boot_gates.rb +3 -18
- data/lib/hecks/runtime/caller.rb +19 -59
- data/lib/hecks/runtime/capability_graph.rb +5 -21
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
- data/lib/hecks/runtime/command_interpreter.rb +124 -306
- data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
- data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
- data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
- data/lib/hecks/runtime/command_rules/emission.rb +8 -21
- data/lib/hecks/runtime/command_rules/references.rb +23 -202
- data/lib/hecks/runtime/dependency_planning.rb +30 -102
- data/lib/hecks/runtime/dispatcher.rb +82 -241
- data/lib/hecks/runtime/entity_element.rb +40 -462
- data/lib/hecks/runtime/entity_interpreter.rb +64 -219
- data/lib/hecks/runtime/errors.rb +32 -115
- data/lib/hecks/runtime/event.rb +3 -25
- data/lib/hecks/runtime/identity.rb +18 -154
- data/lib/hecks/runtime/instance.rb +18 -150
- data/lib/hecks/runtime/interpreting.rb +11 -70
- data/lib/hecks/runtime/invocation.rb +28 -164
- data/lib/hecks/runtime/loader.rb +48 -186
- data/lib/hecks/runtime/outbox.rb +38 -261
- data/lib/hecks/runtime/policy_interpreter.rb +38 -200
- data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
- data/lib/hecks/runtime/query_interpreter.rb +45 -187
- data/lib/hecks/runtime/reaction_invocation.rb +36 -138
- data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
- data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
- data/lib/hecks/runtime/reference_hop.rb +8 -81
- data/lib/hecks/runtime/refusal_wording.rb +8 -89
- data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
- data/lib/hecks/runtime/registry/verification.rb +52 -225
- data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
- data/lib/hecks/runtime/registry.rb +99 -244
- data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
- data/lib/hecks/runtime/routing.rb +4 -15
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
- data/lib/hecks/runtime/saga_interpreter.rb +79 -263
- data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
- data/lib/hecks/runtime/tenant_check.rb +6 -68
- data/lib/hecks/runtime/tenant_scope.rb +3 -28
- data/lib/hecks/runtime/value/admission.rb +16 -67
- data/lib/hecks/runtime/value/coercion.rb +155 -641
- data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
- data/lib/hecks/runtime/value.rb +12 -84
- data/lib/hecks/runtime.rb +17 -65
- data/lib/hecks/storehouse.rb +141 -568
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
- data/lib/hecks/vendoring/git_environment.rb +2 -8
- data/lib/hecks/vendoring/git_source.rb +4 -10
- data/lib/hecks/vendoring.rb +7 -46
- data/lib/hecks/version.rb +3 -14
- data/lib/hecks.rb +26 -81
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
- metadata +3 -2
|
@@ -8,53 +8,23 @@ module Hecks
|
|
|
8
8
|
module Expression
|
|
9
9
|
class EvaluationError < StandardError; end
|
|
10
10
|
|
|
11
|
-
# The dotted/arithmetic leaf grammar a predicate's `Resolve` node
|
|
12
|
-
#
|
|
13
|
-
# growing set of vendored method-call suffixes (`.match?`,
|
|
14
|
-
# `.present?`/`.blank?`, `.set?`/`.unset?`, `.split`, `.first`/
|
|
15
|
-
# `.last`, `.all?`/`.any?`/`.none?`/`.find`, ...) this file and its
|
|
16
|
-
# `resolver/block_predicates.rb` sibling parse and interpret. Each
|
|
17
|
-
# leaf's `parse` is a pure function of its own string, cached at the
|
|
18
|
-
# `Evaluator` layer; `interpret` reads state/attrs fresh each call.
|
|
11
|
+
# The dotted/arithmetic leaf grammar a predicate's `Resolve` node bottoms out in.
|
|
12
|
+
# `parse` is a pure function of its string; `interpret` reads state/attrs fresh.
|
|
19
13
|
module Resolver
|
|
20
14
|
SIGN_TESTS = Hecks::Vocabulary.fetch("SignTest")
|
|
21
15
|
|
|
22
|
-
#
|
|
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.
|
|
16
|
+
# Comparison operator each sign test stands for, against the literal 0.
|
|
26
17
|
SIGN_TEST_OPERATORS = Hecks::Vocabulary.rows("SignTest")
|
|
27
18
|
.to_h { |row| [row["name"], row["compares_via"]] }
|
|
28
19
|
.freeze
|
|
29
20
|
|
|
30
|
-
#
|
|
31
|
-
# Which node a string produces is a pure function of the string, so
|
|
32
|
-
# parse() runs once per distinct leaf (folded into Evaluator's own
|
|
33
|
-
# ast_cache — see evaluator.rb) and interpret() reads state/attrs
|
|
34
|
-
# fresh on every call. Only Lookup — the true leaf — ever touches
|
|
35
|
-
# state/attrs.
|
|
21
|
+
# Leaf nodes of the grammar. Only Lookup touches state/attrs.
|
|
36
22
|
IntegerLiteral = Struct.new(:value, keyword_init: true)
|
|
37
23
|
FloatLiteral = Struct.new(:value, keyword_init: true)
|
|
38
24
|
StringLiteral = Struct.new(:value, keyword_init: true)
|
|
39
25
|
BoolLiteral = Struct.new(:value, keyword_init: true)
|
|
40
|
-
# `["active", "suspended"]` — a literal set, the haystack half of
|
|
41
|
-
# an `.include?`. Vocabulary::IncludeHaystack has always admitted
|
|
42
|
-
# Array (and Evaluator#includes? has always had a `when Array`
|
|
43
|
-
# arm), but nothing could produce one: there was no array-literal
|
|
44
|
-
# node, so `["a", "b"].include?(x)` fell through to Lookup and
|
|
45
|
-
# refused with `cannot resolve "[\"a\", \"b\"]"`. A declared
|
|
46
|
-
# capability with no way to spell it.
|
|
47
26
|
ArrayLiteral = Struct.new(:elements, keyword_init: true)
|
|
48
|
-
# A plain class
|
|
49
|
-
# sibling leaf node here carries at least one field, but this
|
|
50
|
-
# one carries none by nature (a nil literal has no data to
|
|
51
|
-
# hold), and `Struct.new` with zero member names ahead of
|
|
52
|
-
# `keyword_init:` is real, live Ruby-version-dependent
|
|
53
|
-
# behavior: works on 3.3, raises "wrong number of arguments
|
|
54
|
-
# (given 0, expected 1+)" on 3.2 (caught deploying to Lambda's
|
|
55
|
-
# own ruby3.2 runtime). `.new`/`case ... when NilLiteral` below
|
|
56
|
-
# are the only two things this type is ever used for, and a
|
|
57
|
-
# bare class answers both identically.
|
|
27
|
+
# A plain class: `Struct.new(keyword_init: true)` with no members raises on Ruby 3.2.
|
|
58
28
|
NilLiteral = Class.new
|
|
59
29
|
Addition = Struct.new(:left, :right, keyword_init: true)
|
|
60
30
|
SignTest = Struct.new(:operator, :test, :receiver, keyword_init: true)
|
|
@@ -64,186 +34,39 @@ module Hecks
|
|
|
64
34
|
Size = Struct.new(:receiver, keyword_init: true)
|
|
65
35
|
Lookup = Struct.new(:path, keyword_init: true)
|
|
66
36
|
|
|
67
|
-
# Update 2026-08-27: every "vendored addition, not (yet) upstream
|
|
68
|
-
# hecks" comment on this file's own MatchesRegex/Presence/Split/
|
|
69
|
-
# First/Last/StartsWith/EndsWith below (plus ArrayLiteral above)
|
|
70
|
-
# described a real gap, found the hard way, in the history each
|
|
71
|
-
# comment tells — and that history stays exactly as written,
|
|
72
|
-
# on purpose. What changed is the present tense claim "not (yet)
|
|
73
|
-
# upstream": a review of this exact migration found these eight
|
|
74
|
-
# symbols had working Ruby parse/interpret arms but had never
|
|
75
|
-
# gone through Propose -> Render -> Admit
|
|
76
|
-
# (lib/hecks/grammar/expression_operators.json) the way every
|
|
77
|
-
# other operator here has — a closed-vocabulary guard
|
|
78
|
-
# (spec/operator_conformance_spec.rb) built entirely over tables
|
|
79
|
-
# structurally could not see hand-coded Struct/parse/interpret
|
|
80
|
-
# additions, so eight operators ran in Ruby, admitted nowhere,
|
|
81
|
-
# invisible to the one guard whose whole job was "reads in every
|
|
82
|
-
# target." All eight are now ledger-admitted for real, with full
|
|
83
|
-
# Rust kernel parity (rust/src/kernel/expression_operators/
|
|
84
|
-
# {pattern_match,presence,text,positional}.rs) — the two-tier
|
|
85
|
-
# gap is closed, not merely tracked.
|
|
86
|
-
#
|
|
87
|
-
# `receiver.match?(/pattern/)` -- vendored addition, not (yet)
|
|
88
|
-
# upstream hecks (migration plan task 8): confirmed the
|
|
89
|
-
# single most impactful corpus-wide dispatch-time gap of the
|
|
90
|
-
# whole migration -- `.match?(regex)` appears in nearly every
|
|
91
|
-
# value_object's format-validation rule across every corpus
|
|
92
|
-
# this migration touched (email/phone/ISO-8601-timestamp/zip
|
|
93
|
-
# patterns, dozens of files), and had no parse support at all:
|
|
94
|
-
# it fell all the way through to the `Lookup` catch-all below,
|
|
95
|
-
# which tried to split the entire ".match?(/\A\d{5}\z/)" text
|
|
96
|
-
# on "." as if it were a dotted attribute path, and crashed with
|
|
97
|
-
# an opaque "no implicit conversion of Symbol into Integer"
|
|
98
|
-
# somewhere downstream -- confirmed live via a real dispatch,
|
|
99
|
-
# not validate (validate never evaluates a predicate body).
|
|
100
|
-
# `receiver` still needs its own parse (it may itself be a
|
|
101
|
-
# dotted lookup, e.g. `some_field.match?(...)`), the pattern
|
|
102
|
-
# text is taken as-is between the slashes (a Ruby Regexp literal,
|
|
103
|
-
# not the evaluator's own mini-grammar -- there is nothing to
|
|
104
|
-
# recurse into).
|
|
105
37
|
MatchesRegex = Struct.new(:receiver, :pattern, :flags, keyword_init: true)
|
|
106
38
|
|
|
107
|
-
# `.present?`/`.blank?` -- vendored addition, not (yet) upstream
|
|
108
|
-
# hecks (migration plan task 8): the second-most pervasive
|
|
109
|
-
# gap this pass found, right behind `.match?` -- confirmed real
|
|
110
|
-
# via the same "no such attribute" Lookup-fallback crash, since
|
|
111
|
-
# neither suffix existed in this leaf grammar at all. Rails-
|
|
112
|
-
# standard semantics (blank = nil, or responds_to?(:empty?) &&
|
|
113
|
-
# empty? ; present = !blank), not just `!nil?` -- matches what
|
|
114
|
-
# every corpus author actually means by `.present?` on a string
|
|
115
|
-
# field that could legitimately be "" rather than absent.
|
|
116
39
|
Presence = Struct.new(:receiver, :negated, keyword_init: true)
|
|
117
40
|
|
|
118
|
-
# `.set?`/`.unset?`
|
|
119
|
-
# above, added for a deliberately narrower question, spelled out in
|
|
120
|
-
# the name so choosing between the two pairs is a choice, not a
|
|
121
|
-
# trap: `.present?` means "not EMPTY" (Rails-standard -- nil/false,
|
|
122
|
-
# and an empty String/Array/Hash, are blank; nothing else is),
|
|
123
|
-
# which quietly answers "was this ever assigned" and "does the
|
|
124
|
-
# assigned value happen to be empty" as the same question. For an
|
|
125
|
-
# optional field whose only legitimate unset state is nil, that
|
|
126
|
-
# conflation is a real trap -- confirmed live: a corpus author
|
|
127
|
-
# reaching for `superseded_by.blank?` to guard an optional
|
|
128
|
-
# reference burned real time before landing on "just don't guard
|
|
129
|
-
# it," because `.blank?` cannot be asked to mean ONLY "was this
|
|
130
|
-
# never assigned." `.set?`/`.unset?` ask exactly that, and nothing
|
|
131
|
-
# else: `!receiver.nil?`, full stop -- an assigned-but-empty
|
|
132
|
-
# String/Array/Hash (or a VO wrapping one) is `.set?`, not
|
|
133
|
-
# `.unset?`, unlike `.blank?`'s own reading of the identical value.
|
|
41
|
+
# `.set?`/`.unset?` ask only `!nil?`; unlike `.blank?`, an assigned empty value is set.
|
|
134
42
|
Assignment = Struct.new(:receiver, :negated, keyword_init: true)
|
|
135
43
|
|
|
136
|
-
# `receiver.split("SEP")` -- vendored addition, not (yet) upstream
|
|
137
|
-
# hecks (migration plan task 9): confirmed the single
|
|
138
|
-
# highest-value new finding of the real-dispatch smoke sweep --
|
|
139
|
-
# the bus's own core `Phrase` value object (dispatch/lexicon/
|
|
140
|
-
# query/command_bus.bluebook, all four storehouse-kernel files,
|
|
141
|
-
# byte-identical text) validates its four-segment shape with
|
|
142
|
-
# `value.split("::").length == 4 && value.split("::").all? { |s|
|
|
143
|
-
# s.length > 0 }` -- `.split(` matched none of this grammar's
|
|
144
|
-
# known suffixes, so it fell through to the `Lookup` catch-all,
|
|
145
|
-
# which split the raw expression text on "." (not the runtime
|
|
146
|
-
# value) and crashed with `TypeError: no implicit conversion of
|
|
147
|
-
# Symbol into Integer` the moment `String#[]` was handed a
|
|
148
|
-
# Symbol segment -- meaning every command taking a Phrase was
|
|
149
|
-
# entirely undispatchable, confirmed live via `Lexicon::Lexicon.
|
|
150
|
-
# Lookup`/`Query::Query.Run`, not inferred. Produces a real
|
|
151
|
-
# Array, deliberately not a scalar -- `.length`/`.all?` compose
|
|
152
|
-
# with it below the same way they compose with any other
|
|
153
|
-
# receiver, because `parse` recurses on the receiver text and
|
|
154
|
-
# the existing `Size` node (`.length`/`.size`) already treats
|
|
155
|
-
# "whatever `interpret` returns" as its receiver's value, not a
|
|
156
|
-
# fixed type. Separator is taken literally between the quotes,
|
|
157
|
-
# same "no sub-grammar to recurse into" precedent `MatchesRegex`
|
|
158
|
-
# already set for its own `/pattern/` text above.
|
|
159
44
|
Split = Struct.new(:receiver, :separator, keyword_init: true)
|
|
160
45
|
|
|
161
|
-
# `receiver.last` -- vendored addition, not (yet) upstream
|
|
162
|
-
# hecks (migration plan task 9), same pass as `Split` above
|
|
163
|
-
# and built for the same `Phrase`/`Path` value objects --
|
|
164
|
-
# `Query::Phrase`'s own invariant chains `.split("::").last.
|
|
165
|
-
# match?(...)` to check the fourth segment's casing. Same shape
|
|
166
|
-
# as `.length`/`.size` (Size) and `.empty?` (Empty) -- a plain
|
|
167
|
-
# receiver-in, scalar-out accessor, no sub-grammar of its own.
|
|
168
46
|
Last = Struct.new(:receiver, keyword_init: true)
|
|
169
47
|
|
|
170
|
-
# `receiver.first` -- sibling addition to `Last` immediately
|
|
171
|
-
# above, same shape, same vendored-not-upstream status. Added
|
|
172
|
-
# for the shipping domain's `legs.first` (an itinerary's
|
|
173
|
-
# departure leg), the exact mirror of `legs.last` (its arrival
|
|
174
|
-
# leg) that domain already leaned on -- `Last`'s own duck-typed-
|
|
175
|
-
# on-`respond_to?` reasoning applies unchanged, so `first_of`
|
|
176
|
-
# below is `last_of` with the one method swapped, not a new
|
|
177
|
-
# design.
|
|
178
48
|
First = Struct.new(:receiver, keyword_init: true)
|
|
179
49
|
|
|
180
|
-
# `receiver.start_with?("prefix")` / `receiver.end_with?("suffix")`
|
|
181
|
-
# -- vendored addition, not (yet) upstream hecks (migration
|
|
182
|
-
# plan task 9), the sibling gap the `Split`/`Last`/`BlockPredicate`
|
|
183
|
-
# pass above flagged and deliberately left unfixed --
|
|
184
|
-
# `Query::Query.Run`/`Dispatch::Dispatch.Route`/`CommandBus::
|
|
185
|
-
# CommandBus.Route`'s shared `Params` value object validates its
|
|
186
|
-
# JSON-object shape with `value.start_with?("{") && value.
|
|
187
|
-
# end_with?("}")` (dispatch/query/command_bus.bluebook, all three
|
|
188
|
-
# storehouse-kernel files, byte-identical text) -- `.start_with?(`/
|
|
189
|
-
# `.end_with?(` matched none of this grammar's known suffixes, so
|
|
190
|
-
# both fell through to the `Lookup` catch-all and crashed with the
|
|
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`/
|
|
195
|
-
# `SignTest` precedent) -- `start_with?`/`end_with?` aren't two
|
|
196
|
-
# spellings of the same test the way `all?`/`any?`/`none?` are (one
|
|
197
|
-
# Array-aggregation family) or `positive?`/`negative?`/`zero?` are
|
|
198
|
-
# (one comparison-against-0 family) ; they check different ends of
|
|
199
|
-
# the same string and share nothing but their
|
|
200
|
-
# receiver-plus-literal-argument shape. `substring` is taken
|
|
201
|
-
# literally between the quotes, same "no sub-grammar to recurse
|
|
202
|
-
# into" precedent `Split`'s `separator`/`MatchesRegex`'s `pattern`
|
|
203
|
-
# already set.
|
|
204
50
|
StartsWith = Struct.new(:receiver, :substring, keyword_init: true)
|
|
205
51
|
EndsWith = Struct.new(:receiver, :substring, keyword_init: true)
|
|
206
52
|
|
|
207
53
|
module_function
|
|
208
54
|
|
|
209
|
-
# Parses and interprets `expr` in one step,
|
|
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.
|
|
55
|
+
# Parses and interprets `expr` in one step, bypassing `Evaluator`'s boolean grammar.
|
|
213
56
|
#
|
|
214
|
-
# @param expr [String] the dotted/arithmetic leaf expression
|
|
215
|
-
#
|
|
216
|
-
# @param
|
|
217
|
-
#
|
|
218
|
-
# @
|
|
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
|
|
57
|
+
# @param expr [String] the dotted/arithmetic leaf expression
|
|
58
|
+
# @param state [Hash{Symbol => Object}] stored attribute values
|
|
59
|
+
# @param attrs [Hash{Symbol => Object}] call arguments, checked before `state`
|
|
60
|
+
# @return [Object] the resolved Integer, Float, String, boolean, nil, or Array
|
|
61
|
+
# @raise [EvaluationError] on an unknown name or a wrongly typed operand
|
|
226
62
|
def resolve(expr, state, attrs)
|
|
227
63
|
interpret(parse(expr), state, attrs)
|
|
228
64
|
end
|
|
229
65
|
|
|
230
|
-
#
|
|
231
|
-
# expr =~ /pattern/` per leaf production, in a fixed precedence
|
|
232
|
-
# order (`.length` before the generic suffixes, sign tests before
|
|
233
|
-
# the rest, etc. — see this file's own history comments on why
|
|
234
|
-
# several of these were added in exactly this position). Each
|
|
235
|
-
# branch is already one line; splitting the table into smaller
|
|
236
|
-
# methods would not shrink any single check, only hide the
|
|
237
|
-
# precedence order this method's own line-by-line sequence is.
|
|
66
|
+
# Branch order is the precedence order (`.length` first, sign tests before the rest).
|
|
238
67
|
#
|
|
239
|
-
# @param expr [String] the leaf expression text
|
|
240
|
-
# @return [Object] the parsed leaf node
|
|
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
|
|
68
|
+
# @param expr [String] the leaf expression text
|
|
69
|
+
# @return [Object] the parsed leaf node, or `Lookup` when nothing else matches
|
|
247
70
|
# rubocop:disable-next Metrics/AbcSize
|
|
248
71
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
249
72
|
# rubocop:disable-next Metrics/MethodLength
|
|
@@ -313,13 +136,6 @@ module Hecks
|
|
|
313
136
|
Lookup.new(path: expr)
|
|
314
137
|
end
|
|
315
138
|
|
|
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
|
|
323
139
|
def sign_test_node(parts)
|
|
324
140
|
receiver, test = parts
|
|
325
141
|
symbol = SIGN_TEST_OPERATORS.fetch(test)
|
|
@@ -327,28 +143,10 @@ module Hecks
|
|
|
327
143
|
SignTest.new(operator: operator, test: test, receiver: parse(receiver))
|
|
328
144
|
end
|
|
329
145
|
|
|
330
|
-
# `parse`
|
|
331
|
-
# type it can produce, each already delegating to a small named
|
|
332
|
-
# helper (add, apply_sign_test, emptiness_of, ...) — the case
|
|
333
|
-
# itself is the closed, declared set this method exists to
|
|
334
|
-
# exhaust; the `else` backstop's own comment explains why a
|
|
335
|
-
# missing arm is a bug this method is built to make loud, not
|
|
336
|
-
# quiet.
|
|
146
|
+
# Evaluates a node `parse` produced against `state` and `attrs`.
|
|
337
147
|
#
|
|
338
|
-
# @
|
|
339
|
-
#
|
|
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
|
|
148
|
+
# @return [Object] the value the node resolves to
|
|
149
|
+
# @raise [EvaluationError] if the node type is unhandled or an operand is refused
|
|
352
150
|
# rubocop:disable-next Metrics/AbcSize
|
|
353
151
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
354
152
|
# rubocop:disable-next Metrics/MethodLength
|
|
@@ -394,42 +192,16 @@ module Hecks
|
|
|
394
192
|
when Lookup
|
|
395
193
|
lookup(node.path, state, attrs)
|
|
396
194
|
else
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
# type (this file's own history: MatchesRegex/Presence/
|
|
400
|
-
# Split/Last/First/Find/StartsWith/EndsWith/BlockPredicate
|
|
401
|
-
# were each added exactly this way, and each one — before it had an
|
|
402
|
-
# `interpret` arm — fell all the way through to the
|
|
403
|
-
# `Lookup` catch-all in `parse` and crashed downstream with
|
|
404
|
-
# an opaque type error, never here). A missing arm here
|
|
405
|
-
# would instead return bare `nil` silently, the one wrong-
|
|
406
|
-
# answer shape this leaf grammar has otherwise never
|
|
407
|
-
# allowed.
|
|
195
|
+
# A missing arm must raise; silently returning nil is the one wrong answer
|
|
196
|
+
# this grammar never allows.
|
|
408
197
|
raise EvaluationError, "no interpreter handles #{node.class} — add a case before parse can produce it"
|
|
409
198
|
end
|
|
410
199
|
end
|
|
411
200
|
|
|
412
|
-
# `.present?`/`.blank?` -- vendored addition, see the
|
|
413
|
-
# `Presence` struct's own comment above. `nil` and `false` are
|
|
414
|
-
# blank ; a String/Array/Hash is blank when empty, not merely
|
|
415
|
-
# falsy -- a VO-wrapped field that is assigned (`{value: "x"}`,
|
|
416
|
-
# `Value#to_h`'d first) is present regardless of what its own
|
|
417
|
-
# inner value holds, matching how every VO-typed field in this
|
|
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
|
|
424
201
|
def blank?(value)
|
|
425
202
|
return true if value.nil? || value == false
|
|
426
203
|
|
|
427
|
-
# Duck-typed
|
|
428
|
-
# is `Bluebook::Expression`, a different namespace tree from
|
|
429
|
-
# `Runtime::Value` entirely, and reaching across for a single
|
|
430
|
-
# class check is the exact cross-module coupling that already
|
|
431
|
-
# broke `Modulo`'s own `match_call` reference elsewhere in
|
|
432
|
-
# this file (found live while building this fix, not assumed).
|
|
204
|
+
# Duck-typed: reaching across to Runtime::Value would couple the namespaces.
|
|
433
205
|
value = value.to_h if value.respond_to?(:to_h) && !value.is_a?(Hash)
|
|
434
206
|
case value
|
|
435
207
|
when String, Array, Hash then value.empty?
|
|
@@ -437,26 +209,6 @@ module Hecks
|
|
|
437
209
|
end
|
|
438
210
|
end
|
|
439
211
|
|
|
440
|
-
# `receiver.match?(/pattern/)` -- vendored addition, see the
|
|
441
|
-
# `MatchesRegex` struct's own comment above. `receiver_value` is
|
|
442
|
-
# coerced to a plain String first -- inlined here rather than
|
|
443
|
-
# calling `Evaluator#string_of` (a different module_function
|
|
444
|
-
# module; not actually in scope from inside Resolver despite
|
|
445
|
-
# `Modulo`'s own parse rule above calling a same-named
|
|
446
|
-
# `match_call` that has the identical cross-module problem --
|
|
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
|
|
460
212
|
def matches_regex?(receiver_value, pattern, flags)
|
|
461
213
|
text = case receiver_value
|
|
462
214
|
when String, Symbol, Integer, Float then receiver_value.to_s
|
|
@@ -472,25 +224,10 @@ module Hecks
|
|
|
472
224
|
|
|
473
225
|
Regexp.new(pattern, options).match?(text)
|
|
474
226
|
rescue RegexpError => e
|
|
475
|
-
#
|
|
476
|
-
# character class, say) is a defect in the expression text
|
|
477
|
-
# itself, exactly the same category of author mistake an
|
|
478
|
-
# unresolvable attribute name already refuses for — `Regexp.new`
|
|
479
|
-
# raising a raw `RegexpError` crossed this sublanguage's own
|
|
480
|
-
# refusal boundary the same way the `ZeroDivisionError`/
|
|
481
|
-
# `TypeError` cases elsewhere in this file did.
|
|
227
|
+
# A malformed pattern is an author mistake; refuse it as EvaluationError.
|
|
482
228
|
raise EvaluationError, "match? given an invalid pattern #{pattern.inspect} — #{e.message}"
|
|
483
229
|
end
|
|
484
230
|
|
|
485
|
-
# The elements of a bracketed literal, or nil if this isn't one.
|
|
486
|
-
# Splits on top-level commas only — quote-aware and depth-aware,
|
|
487
|
-
# the same discipline `split_addition` already applies, so a
|
|
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
|
|
494
231
|
def array_elements(expr)
|
|
495
232
|
return nil unless expr.start_with?("[") && expr.end_with?("]")
|
|
496
233
|
|
|
@@ -523,24 +260,8 @@ module Hecks
|
|
|
523
260
|
elements.reject(&:empty?)
|
|
524
261
|
end
|
|
525
262
|
|
|
526
|
-
# Braces count toward depth
|
|
527
|
-
#
|
|
528
|
-
# own addition. `parse` tries addition before `parse_block_opener`,
|
|
529
|
-
# so a paren-only depth count split
|
|
530
|
-
# kings.any? { |k| k.square.file == to.file + 1 && ... }
|
|
531
|
-
# at that inner `+`, turning the whole expression into a nonsense
|
|
532
|
-
# Addition whose left operand then walked "any? { |k| k" into an
|
|
533
|
-
# Array as an attribute path — "TypeError: no implicit conversion
|
|
534
|
-
# of Symbol into Integer", the same signature every unsupported
|
|
535
|
-
# construct raises, which is what let this hide. Found live in a
|
|
536
|
-
# downstream chess domain's castling given; the evaluator's own
|
|
537
|
-
# top_level_index has counted braces since its own version of this
|
|
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
|
|
263
|
+
# Braces and brackets count toward depth like parens, so a `+` inside a block body
|
|
264
|
+
# or an array element is not this expression's own addition.
|
|
544
265
|
def split_addition(expr)
|
|
545
266
|
depth = 0
|
|
546
267
|
quote = nil
|
|
@@ -550,17 +271,7 @@ module Hecks
|
|
|
550
271
|
quote = nil if char == quote
|
|
551
272
|
elsif ['"', "'"].include?(char)
|
|
552
273
|
quote = char
|
|
553
|
-
# `[`/`]`
|
|
554
|
-
# comment already names, a third time (found live via the
|
|
555
|
-
# type-directed bounded-exhaustive expression generator,
|
|
556
|
-
# Phase 7 of the equivalence-gap plan): `ArrayLiteral` can
|
|
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.
|
|
274
|
+
# `[`/`]` count too: an array element may hold its own top-level `+`.
|
|
564
275
|
elsif ["(", "{", "["].include?(char)
|
|
565
276
|
depth += 1
|
|
566
277
|
elsif [")", "}", "]"].include?(char)
|
|
@@ -572,23 +283,9 @@ module Hecks
|
|
|
572
283
|
nil
|
|
573
284
|
end
|
|
574
285
|
|
|
575
|
-
# Integer is signed 64-bit (C3.3,
|
|
576
|
-
# — Ruby's own Integer would promote past ±2^63-1 silently; the
|
|
577
|
-
# language does not, so a sum outside that range is an evaluation
|
|
578
|
-
# fault, worded exactly as the Rust kernel's `checked_add` words
|
|
579
|
-
# it. A Float sum that is not finite is the same fault (C3.4).
|
|
286
|
+
# Integer is signed 64-bit; a sum outside the range is an evaluation fault (C3.3, C3.4).
|
|
580
287
|
INT64_RANGE = (-(2**63))..((2**63) - 1)
|
|
581
288
|
|
|
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
|
|
592
289
|
def add(left, right)
|
|
593
290
|
lhs = require_number(left, "addition")
|
|
594
291
|
rhs = require_number(right, "addition")
|
|
@@ -603,12 +300,6 @@ module Hecks
|
|
|
603
300
|
raise EvaluationError, "addition overflowed: #{lhs} + #{rhs} is not a finite number"
|
|
604
301
|
end
|
|
605
302
|
|
|
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
|
|
612
303
|
def quoted?(expr)
|
|
613
304
|
return false if expr.length < 2
|
|
614
305
|
|
|
@@ -616,135 +307,54 @@ module Hecks
|
|
|
616
307
|
(expr.start_with?("'") && expr.end_with?("'"))
|
|
617
308
|
end
|
|
618
309
|
|
|
619
|
-
#
|
|
620
|
-
# (language/bluebook/vocabulary.bluebook) — spec/vocabulary_conformance_spec
|
|
621
|
-
# holds this equal to the language. Shared by .size and .empty?,
|
|
622
|
-
# which admit the same set for the same reason.
|
|
310
|
+
# Held equal to Vocabulary::SizedType by spec/vocabulary_conformance_spec.
|
|
623
311
|
SIZED_TYPES = Hecks::Vocabulary.fetch("SizedType")
|
|
624
312
|
|
|
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
|
|
633
313
|
def size_of(value)
|
|
634
314
|
return value.size if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
|
|
635
315
|
|
|
636
316
|
raise EvaluationError, "size expects a list or string, got #{describe(value)}"
|
|
637
317
|
end
|
|
638
318
|
|
|
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
|
|
647
319
|
def emptiness_of(value)
|
|
648
320
|
return value.empty? if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
|
|
649
321
|
|
|
650
322
|
raise EvaluationError, "empty? expects a list or string, got #{describe(value)}"
|
|
651
323
|
end
|
|
652
324
|
|
|
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
|
|
666
325
|
def split_value(value, separator)
|
|
667
326
|
raise EvaluationError, "split expects a string, got #{describe(value)}" unless value.is_a?(String)
|
|
668
327
|
|
|
669
328
|
value.split(separator)
|
|
670
329
|
end
|
|
671
330
|
|
|
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`
|
|
685
331
|
def last_of(value)
|
|
686
332
|
return value.last if value.respond_to?(:last)
|
|
687
333
|
|
|
688
334
|
raise EvaluationError, "last expects a list, got #{describe(value)}"
|
|
689
335
|
end
|
|
690
336
|
|
|
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`
|
|
698
337
|
def first_of(value)
|
|
699
338
|
return value.first if value.respond_to?(:first)
|
|
700
339
|
|
|
701
340
|
raise EvaluationError, "first expects a list, got #{describe(value)}"
|
|
702
341
|
end
|
|
703
342
|
|
|
704
|
-
# Reports whether `value` starts with `substring`, for
|
|
705
|
-
# `.start_with?("prefix")` -- vendored addition, see the
|
|
706
|
-
# `StartsWith` struct's own comment above. String-only, same
|
|
707
|
-
# reasoning as `.split` above -- every corpus usage found this
|
|
708
|
-
# pass (`Params`'s own JSON-object-shape invariant) receives a
|
|
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
|
|
715
343
|
def starts_with?(value, substring)
|
|
716
344
|
raise EvaluationError, "start_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
|
|
717
345
|
|
|
718
346
|
value.start_with?(substring)
|
|
719
347
|
end
|
|
720
348
|
|
|
721
|
-
# Reports whether `value` ends with `substring`, for
|
|
722
|
-
# `.end_with?("suffix")` -- vendored addition, see the `EndsWith`
|
|
723
|
-
# struct's own comment above. Same String-only reasoning as
|
|
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
|
|
730
349
|
def ends_with?(value, substring)
|
|
731
350
|
raise EvaluationError, "end_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
|
|
732
351
|
|
|
733
352
|
value.end_with?(substring)
|
|
734
353
|
end
|
|
735
354
|
|
|
736
|
-
#
|
|
737
|
-
# (language/bluebook/vocabulary.bluebook) — spec/vocabulary_conformance_spec
|
|
738
|
-
# holds this equal to the language.
|
|
355
|
+
# Held equal to Vocabulary::ToStringType by spec/vocabulary_conformance_spec.
|
|
739
356
|
TO_STRING_TYPES = Hecks::Vocabulary.fetch("ToStringType")
|
|
740
357
|
|
|
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
|
|
748
358
|
def string_of(value)
|
|
749
359
|
case value
|
|
750
360
|
when String then value
|
|
@@ -755,16 +365,6 @@ module Hecks
|
|
|
755
365
|
end
|
|
756
366
|
end
|
|
757
367
|
|
|
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
|
|
768
368
|
def match_suffix(expr, suffixes)
|
|
769
369
|
suffixes.each do |suffix|
|
|
770
370
|
marker = ".#{suffix}"
|
|
@@ -773,14 +373,6 @@ module Hecks
|
|
|
773
373
|
nil
|
|
774
374
|
end
|
|
775
375
|
|
|
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
|
|
784
376
|
def apply_sign_test(node, value)
|
|
785
377
|
number = numeric(value)
|
|
786
378
|
raise EvaluationError, "#{node.test} expects a number, got #{describe(value)}" unless number
|
|
@@ -788,58 +380,8 @@ module Hecks
|
|
|
788
380
|
Evaluator.apply(node.operator, number, 0)
|
|
789
381
|
end
|
|
790
382
|
|
|
791
|
-
#
|
|
792
|
-
#
|
|
793
|
-
# bounded_exhaustive_expression_spec.rb): `.modulo(`'s own
|
|
794
|
-
# argument position accepts any numeric sub-expression, including
|
|
795
|
-
# another `.modulo(...)` call — `0.modulo(num_b.modulo(-1))` is
|
|
796
|
-
# perfectly well-typed — but `expr.rindex(marker)` finds 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
|
|
800
|
-
# split there, producing a receiver of `"0.modulo(num_b"` and a
|
|
801
|
-
# divisor of `"-1)"` — both garbage, both re-parsed as bogus
|
|
802
|
-
# `Lookup` paths, both then refusing with "cannot resolve" — a
|
|
803
|
-
# silent misparse that happened to fail safe into a real
|
|
804
|
-
# `EvaluationError` rather than a raw crash, which is exactly why
|
|
805
|
-
# this had gone unnoticed: nothing before this generator existed
|
|
806
|
-
# ever fed `.modulo` a nested `.modulo` call, random fuzzing
|
|
807
|
-
# essentially never manufactures that specific shape by chance,
|
|
808
|
-
# and the resulting refusal looks like an ordinary, correct one
|
|
809
|
-
# unless you already know every name this generator's own
|
|
810
|
-
# synthetic state declares (real corpus authors would see this as
|
|
811
|
-
# a mysterious "cannot resolve" on text they never wrote).
|
|
812
|
-
#
|
|
813
|
-
# Fixed the same way `split_addition`/`Evaluator.top_level_index`
|
|
814
|
-
# already handle nested `(`/`{` elsewhere in this exact file:
|
|
815
|
-
# find the first (leftmost, outermost) occurrence of the marker,
|
|
816
|
-
# then track paren/quote depth from there to find its own
|
|
817
|
-
# matching close — not just strip the string's own trailing `)`
|
|
818
|
-
# and hope it belongs to this call.
|
|
819
|
-
# Not just the first occurrence, either — `.modulo` also chains
|
|
820
|
-
# (`x.modulo(a).modulo(b)`, the receiver of the outer call itself
|
|
821
|
-
# ending in a `.modulo(...)` call), a second real shape the
|
|
822
|
-
# leftmost-occurrence-only version of this fix still mis-parsed:
|
|
823
|
-
# the first `.modulo(`'s own matching close paren lands mid-
|
|
824
|
-
# string (right after `a)`, before the second `.modulo(b)`), so
|
|
825
|
-
# it correctly fails the "reaches the end" check below and must
|
|
826
|
-
# be tried again at the next occurrence rather than giving up.
|
|
827
|
-
# Trying occurrences strictly left to right and taking the first
|
|
828
|
-
# one whose matching close reaches the string's last character
|
|
829
|
-
# handles both shapes with the same rule: for nesting
|
|
830
|
-
# (`.modulo(x.modulo(y))`), the leftmost (outer) occurrence's own
|
|
831
|
-
# paren-depth tracking already walks straight through the inner
|
|
832
|
-
# call to the true final `)`; for chaining, the leftmost
|
|
833
|
-
# occurrence's close lands short and is rejected, so the next
|
|
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
|
|
383
|
+
# Finds the outermost `marker` call whose matching `)` ends `expr`. `rindex` would split
|
|
384
|
+
# nested `.modulo(` calls at the inner one, and a first match alone mis-parses chains.
|
|
843
385
|
def match_call(expr, marker)
|
|
844
386
|
start = 0
|
|
845
387
|
while (index = expr.index(marker, start))
|
|
@@ -851,17 +393,6 @@ module Hecks
|
|
|
851
393
|
nil
|
|
852
394
|
end
|
|
853
395
|
|
|
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
|
|
865
396
|
def matching_paren(expr, start)
|
|
866
397
|
depth = 1
|
|
867
398
|
quote = nil
|
|
@@ -883,32 +414,7 @@ module Hecks
|
|
|
883
414
|
nil
|
|
884
415
|
end
|
|
885
416
|
|
|
886
|
-
#
|
|
887
|
-
# zero-check, and the check reads the coerced divisor — not the
|
|
888
|
-
# raw `divisor_value` (which might not even respond to `.zero?`,
|
|
889
|
-
# a String for instance) and not a `.to_i`-truncated stand-in for
|
|
890
|
-
# it either. The old order checked a truncated `divisor.to_i`
|
|
891
|
-
# after already validating the untruncated value wasn't zero, so
|
|
892
|
-
# a divisor merely small (`0.3`, truncating to `0`) sailed past
|
|
893
|
-
# the guard and then blew up `Integer#%` with a raw
|
|
894
|
-
# `ZeroDivisionError` the moment it reached zero anyway.
|
|
895
|
-
#
|
|
896
|
-
# The modulo itself is plain `%` on the coerced values, matching
|
|
897
|
-
# `add`'s own no-truncation precedent just above — Ruby's native
|
|
898
|
-
# `%` already handles every Integer/Float combination correctly
|
|
899
|
-
# (promoting to Float when either side is one), so rounding both
|
|
900
|
-
# operands down to Integer first was pure data loss with no
|
|
901
|
-
# purpose: `7.5.modulo(2.5)` silently became `7 % 2` (`1`)
|
|
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
|
|
417
|
+
# The zero-check reads the coerced divisor, not the raw value or a `to_i` truncation.
|
|
912
418
|
def apply_modulo(receiver_value, divisor_value)
|
|
913
419
|
receiver = require_number(receiver_value, "modulo")
|
|
914
420
|
divisor = require_number(divisor_value, "modulo")
|
|
@@ -917,20 +423,6 @@ module Hecks
|
|
|
917
423
|
receiver % divisor
|
|
918
424
|
end
|
|
919
425
|
|
|
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
|
|
934
426
|
def lookup(expr, state, attrs)
|
|
935
427
|
return unwrap_scalar(fetch(expr, state, attrs)) unless expr.include?(".")
|
|
936
428
|
|
|
@@ -938,25 +430,8 @@ module Hecks
|
|
|
938
430
|
unwrap_scalar(walk_path(fetch(head, state, attrs), rest))
|
|
939
431
|
end
|
|
940
432
|
|
|
941
|
-
# Walks a
|
|
942
|
-
#
|
|
943
|
-
# `found_of` (the `Find` node's own path projection) can walk a
|
|
944
|
-
# `.find { ... }`-produced element the identical way `lookup`
|
|
945
|
-
# walks a plain attribute path, rather than duplicating the
|
|
946
|
-
# symbol-or-string key step twice in this file. `key?` decides
|
|
947
|
-
# which spelling answers — a bare `||` between the two would
|
|
948
|
-
# treat a genuinely-held `false` the same as an absent key and
|
|
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)
|
|
433
|
+
# Walks dotted segments through a Hash-like value. `key?` picks the symbol or string
|
|
434
|
+
# spelling so a held `false` is not mistaken for an absent key.
|
|
960
435
|
def walk_path(value, segments)
|
|
961
436
|
segments.reduce(value) do |current, segment|
|
|
962
437
|
break nil unless current.respond_to?(:[])
|
|
@@ -968,15 +443,7 @@ module Hecks
|
|
|
968
443
|
begin
|
|
969
444
|
current[segment]
|
|
970
445
|
rescue TypeError
|
|
971
|
-
#
|
|
972
|
-
# path can walk onto an Array (e.g. the result of `.split`,
|
|
973
|
-
# or a `list_of` attribute) — Array#[] demands an
|
|
974
|
-
# Integer/Range and raises a raw TypeError for a String
|
|
975
|
-
# segment ("no implicit conversion of String into
|
|
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.
|
|
446
|
+
# `Array#[]` raises a raw TypeError for a String segment; refuse it instead.
|
|
980
447
|
raise EvaluationError,
|
|
981
448
|
"cannot read #{segment.inspect} from #{describe(current)}"
|
|
982
449
|
end
|
|
@@ -984,64 +451,9 @@ module Hecks
|
|
|
984
451
|
end
|
|
985
452
|
end
|
|
986
453
|
|
|
987
|
-
# Unwraps a single-field value object to its
|
|
988
|
-
#
|
|
989
|
-
#
|
|
990
|
-
# dispatch-time gap this pass found, same family as `.match?`/
|
|
991
|
-
# `.present?` above -- a lookup of a single-field
|
|
992
|
-
# scalar-convenience value object (the exact shape `Value.
|
|
993
|
-
# from_identifier`/`Value::Coercion#fields_for`'s own single-
|
|
994
|
-
# field auto-unwrap already treats as "this VO IS its scalar"
|
|
995
|
-
# everywhere else in this runtime) came back as the `Value`
|
|
996
|
-
# wrapper itself, never unwrapped for reading -- so `Value#==`
|
|
997
|
-
# (which only ever equals another `Value` instance) silently
|
|
998
|
-
# refused every `guarantees "..." do status == "active" end` /
|
|
999
|
-
# `expects "..." do trash_day.present? end`-shaped bare
|
|
1000
|
-
# comparison against a raw literal. Confirmed corpus-wide, not
|
|
1001
|
-
# one file's mistake: bin-buddy alone has this exact `field ==
|
|
1002
|
-
# "literal"` shape in plan.bluebook, service_task.bluebook,
|
|
1003
|
-
# route.bluebook, and subscription.bluebook, all equally silent
|
|
1004
|
-
# until a real dispatch (never validate) exercised the
|
|
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
|
|
454
|
+
# Unwraps a single-field value object to its scalar so `field == "literal"` works.
|
|
455
|
+
# Gated on the field count, not its name. Mirrors `impl Fielded for Json` in
|
|
456
|
+
# rust/src/kernel/json.rs; change both together.
|
|
1045
457
|
def unwrap_scalar(value)
|
|
1046
458
|
return value unless value.respond_to?(:to_h) && !value.is_a?(Hash) && !value.is_a?(Array)
|
|
1047
459
|
|
|
@@ -1054,17 +466,6 @@ module Hecks
|
|
|
1054
466
|
hash.size == 1 && hash.key?(:value) ? hash[:value] : value
|
|
1055
467
|
end
|
|
1056
468
|
|
|
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`
|
|
1068
469
|
def fetch(name, state, attrs)
|
|
1069
470
|
key = name.to_sym
|
|
1070
471
|
return attrs[key] if attrs.key?(key)
|
|
@@ -1073,45 +474,21 @@ module Hecks
|
|
|
1073
474
|
raise EvaluationError, "cannot resolve #{name.inspect} — no such attribute or argument"
|
|
1074
475
|
end
|
|
1075
476
|
|
|
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
|
|
1084
477
|
def known?(state, key)
|
|
1085
478
|
return state.key?(key) if state.respond_to?(:key?)
|
|
1086
479
|
|
|
1087
480
|
!state[key].nil?
|
|
1088
481
|
end
|
|
1089
482
|
|
|
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`
|
|
1095
483
|
def numeric(value)
|
|
1096
484
|
value if value.is_a?(Integer) || value.is_a?(Float)
|
|
1097
485
|
end
|
|
1098
486
|
|
|
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
|
|
1106
487
|
def require_number(value, operation)
|
|
1107
488
|
numeric(value) ||
|
|
1108
489
|
raise(EvaluationError, "#{operation} expects a number, got #{describe(value)}")
|
|
1109
490
|
end
|
|
1110
491
|
|
|
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)`
|
|
1115
492
|
def describe(value) = Rendering.describe(value)
|
|
1116
493
|
end
|
|
1117
494
|
end
|