hecks 1.4.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 +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- 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 +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- 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 +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- 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 +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +29 -5
- data/lib/hecks/projector.rb +74 -6
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +11 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
|
@@ -53,14 +53,31 @@ module Hecks
|
|
|
53
53
|
|
|
54
54
|
module_function
|
|
55
55
|
|
|
56
|
-
#
|
|
56
|
+
# Memoizes each distinct predicate string's own parsed AST, keyed by
|
|
57
|
+
# the exact string `call` receives. Canonical text is already
|
|
57
58
|
# normalised at DSL-build time, so the same given/invariant's text is
|
|
58
59
|
# byte-identical across every dispatch that evaluates it — parsed once
|
|
59
60
|
# here, interpreted fresh against each call's own state/attrs. Matches
|
|
60
61
|
# MetaValidator.verdicts' unsynchronized `||= {}` idiom : redundant
|
|
61
62
|
# parse work under real parallelism, never corruption.
|
|
63
|
+
#
|
|
64
|
+
# @return [Hash{String => Object}] the process-wide parse cache,
|
|
65
|
+
# keyed by predicate string; each value is one of `Or`, `And`,
|
|
66
|
+
# `Not`, `Compare`, `Include`, or `Resolve`
|
|
62
67
|
def ast_cache = @ast_cache ||= {}
|
|
63
68
|
|
|
69
|
+
# Parses `expr` (cached per distinct string) and interprets it
|
|
70
|
+
# against `state`/`attrs`.
|
|
71
|
+
#
|
|
72
|
+
# @param expr [String] the canonical predicate text to evaluate
|
|
73
|
+
# @param state [Hash{Symbol => Object}] the stored attribute values
|
|
74
|
+
# a `Resolve`/`Compare` leaf may resolve against
|
|
75
|
+
# @param attrs [Hash{Symbol => Object}] the call's own argument
|
|
76
|
+
# values, checked before `state`
|
|
77
|
+
# @return [Boolean] whether `expr` holds
|
|
78
|
+
# @raise [EvaluationError] if `expr` resolves an unknown attribute
|
|
79
|
+
# or argument, or applies an operation to a value of the wrong
|
|
80
|
+
# type
|
|
64
81
|
def call(expr, state, attrs = {})
|
|
65
82
|
interpret(ast_cache[expr] ||= parse(expr), state, attrs)
|
|
66
83
|
end
|
|
@@ -77,31 +94,60 @@ module Hecks
|
|
|
77
94
|
#
|
|
78
95
|
# HECKS_EVAL=string reverts to the text path wholesale, kept for
|
|
79
96
|
# one release as the escape hatch while the ast path beds in.
|
|
97
|
+
#
|
|
98
|
+
# @param rule [Bluebook::Given, Bluebook::Invariant] the rule to
|
|
99
|
+
# evaluate
|
|
100
|
+
# @param state [Hash{Symbol => Object}] the stored attribute values
|
|
101
|
+
# a `Resolve`/`Compare` leaf may resolve against
|
|
102
|
+
# @param attrs [Hash{Symbol => Object}] the call's own argument
|
|
103
|
+
# values, checked before `state`
|
|
104
|
+
# @return [Boolean] whether `rule` holds
|
|
105
|
+
# @raise [EvaluationError] if `rule` resolves an unknown attribute
|
|
106
|
+
# or argument, or applies an operation to a value of the wrong
|
|
107
|
+
# type
|
|
80
108
|
def call_rule(rule, state, attrs = {})
|
|
81
109
|
return call(rule.canonical, state, attrs) if ENV["HECKS_EVAL"] == "string"
|
|
82
110
|
|
|
83
111
|
interpret(ast_cache[rule.canonical] ||= nodes_for(rule), state, attrs)
|
|
84
112
|
end
|
|
85
113
|
|
|
114
|
+
# Returns `rule`'s own AST, read back from its `ast` field when
|
|
115
|
+
# present, otherwise parsed fresh from `canonical`.
|
|
116
|
+
#
|
|
117
|
+
# @param rule [Bluebook::Given, Bluebook::Invariant] the rule to
|
|
118
|
+
# read
|
|
119
|
+
# @return [Object] one of `Or`, `And`, `Not`, `Compare`, `Include`,
|
|
120
|
+
# or `Resolve`
|
|
86
121
|
def nodes_for(rule)
|
|
87
122
|
rule.ast ? AstReader.read_predicate(rule.ast) : parse(rule.canonical)
|
|
88
123
|
end
|
|
89
124
|
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
125
|
+
# Renders `expr`'s own two operands, for a refusal message to show
|
|
126
|
+
# alongside a failed `given`/`ensures`/`invariant`. A refused rule
|
|
127
|
+
# names its own description ("not already superseded") but, on its
|
|
128
|
+
# own, not what the block actually evaluated to — the difference
|
|
129
|
+
# between "the rule is right and my data is wrong" and "the rule
|
|
130
|
+
# is subtly wrong" is often just seeing the two operands. Scoped
|
|
131
|
+
# to the single shape that has one honest answer: `expr`'s own
|
|
132
|
+
# top-level node is a bare `Compare` — not `Or`/`And`/`Not` (which
|
|
133
|
+
# of several sub-comparisons would even be "the" one at fault is
|
|
134
|
+
# genuinely ambiguous), `Include` (no left/right to show), or
|
|
135
|
+
# `Resolve` (a bare boolean read, nothing to compare against).
|
|
136
|
+
# Values are rendered with `Rendering.describe`, the same house
|
|
137
|
+
# style every other refusal already prints a value through.
|
|
138
|
+
# Returns `nil` — not raised — on anything else, including an
|
|
139
|
+
# operand that itself fails to resolve (`EvaluationError`): a
|
|
103
140
|
# missing diagnostic is a worse debugging experience than none, a
|
|
104
141
|
# crash while building one is worse still.
|
|
142
|
+
#
|
|
143
|
+
# @param expr [String] the canonical predicate text that was just
|
|
144
|
+
# refused
|
|
145
|
+
# @param state [Hash{Symbol => Object}] the stored attribute values
|
|
146
|
+
# the operands may resolve against
|
|
147
|
+
# @param attrs [Hash{Symbol => Object}] the call's own argument
|
|
148
|
+
# values, checked before `state`
|
|
149
|
+
# @return [String, nil] `"left: X, right: Y"` when `expr` is a bare
|
|
150
|
+
# comparison whose operands both resolve; `nil` otherwise
|
|
105
151
|
def comparison_detail(expr, state, attrs = {})
|
|
106
152
|
node = ast_cache[expr] ||= parse(expr)
|
|
107
153
|
return nil unless node.is_a?(Compare)
|
|
@@ -113,6 +159,12 @@ module Hecks
|
|
|
113
159
|
nil
|
|
114
160
|
end
|
|
115
161
|
|
|
162
|
+
# Parses `expr`'s boolean/comparison grammar into an AST, recursing
|
|
163
|
+
# into `Resolver.parse` for each leaf.
|
|
164
|
+
#
|
|
165
|
+
# @param expr [String] the canonical predicate text to parse
|
|
166
|
+
# @return [Object] one of `Or`, `And`, `Not`, `Compare`, `Include`,
|
|
167
|
+
# or `Resolve`, chosen by `expr`'s own shape
|
|
116
168
|
def parse(expr)
|
|
117
169
|
expr = strip_parens(expr.to_s.strip)
|
|
118
170
|
|
|
@@ -127,13 +179,13 @@ module Hecks
|
|
|
127
179
|
# means `!(names.include?(x))`, never "call .include? on the negated
|
|
128
180
|
# receiver"), so the leading marker has to be stripped and the
|
|
129
181
|
# remainder re-parsed before anything downstream gets a chance to
|
|
130
|
-
# mis-scan across it.
|
|
182
|
+
# mis-scan across it. Tried after `match_include` instead, its
|
|
131
183
|
# naive `rindex(".include?(")` has no concept of a leading `!` —
|
|
132
|
-
# for `!names.include?(x)` it
|
|
133
|
-
# haystack text ("!names"), which `Resolver.parse` cannot
|
|
134
|
-
# so every spelling of negated membership
|
|
135
|
-
# evaluating.
|
|
136
|
-
# (`!names.include?(x)`) and the parenthesized form
|
|
184
|
+
# for `!names.include?(x)` it would swallow the `!` straight into
|
|
185
|
+
# the haystack text ("!names"), which `Resolver.parse` cannot
|
|
186
|
+
# resolve, so every spelling of negated membership would raise
|
|
187
|
+
# instead of evaluating. Checking here first fixes both the bare
|
|
188
|
+
# prefix (`!names.include?(x)`) and the parenthesized form
|
|
137
189
|
# (`!(names.include?(x))`) — the recursive `parse` call sees the
|
|
138
190
|
# clean remainder and correctly finds the `.include?` (or `&&`/`||`)
|
|
139
191
|
# inside it.
|
|
@@ -150,6 +202,19 @@ module Hecks
|
|
|
150
202
|
Resolve.new(expr: Resolver.parse(expr))
|
|
151
203
|
end
|
|
152
204
|
|
|
205
|
+
# Interprets a parsed boolean/comparison node against `state`/
|
|
206
|
+
# `attrs`.
|
|
207
|
+
#
|
|
208
|
+
# @param node [Object] a node `parse` produced (`Or`, `And`, `Not`,
|
|
209
|
+
# `Compare`, `Include`, or `Resolve`)
|
|
210
|
+
# @param state [Hash{Symbol => Object}] the stored attribute values
|
|
211
|
+
# a leaf may resolve against
|
|
212
|
+
# @param attrs [Hash{Symbol => Object}] the call's own argument
|
|
213
|
+
# values, checked before `state`
|
|
214
|
+
# @return [Boolean] whether `node` holds
|
|
215
|
+
# @raise [EvaluationError] if `node` is not one of the handled
|
|
216
|
+
# types, or a leaf it delegates to (`compare`, `includes?`,
|
|
217
|
+
# `Resolver.interpret`) refuses its operand
|
|
153
218
|
def interpret(node, state, attrs)
|
|
154
219
|
case node
|
|
155
220
|
when Or then interpret(node.left, state, attrs) || interpret(node.right, state, attrs)
|
|
@@ -162,16 +227,30 @@ module Hecks
|
|
|
162
227
|
# Every node `parse` can produce has a `when` above — a
|
|
163
228
|
# backstop against the day this grammar grows a new node
|
|
164
229
|
# type and `interpret` doesn't grow to match it. A missing
|
|
165
|
-
# arm here
|
|
166
|
-
# that straight into the boolean algebra as ordinary
|
|
167
|
-
# — reading exactly like "the rule legitimately does
|
|
168
|
-
# hold" rather than "the runtime cannot evaluate this
|
|
169
|
-
# at all", the one silent no-op this language otherwise
|
|
230
|
+
# arm here would instead return bare `nil`, and `Or`/`And`
|
|
231
|
+
# fold that straight into the boolean algebra as ordinary
|
|
232
|
+
# falsy — reading exactly like "the rule legitimately does
|
|
233
|
+
# not hold" rather than "the runtime cannot evaluate this
|
|
234
|
+
# rule at all", the one silent no-op this language otherwise
|
|
170
235
|
# refuses.
|
|
171
236
|
raise EvaluationError, "no interpreter handles #{node.class} — add a case before parse can produce it"
|
|
172
237
|
end
|
|
173
238
|
end
|
|
174
239
|
|
|
240
|
+
# Resolves `left`/`right` and applies `comparator` to the results.
|
|
241
|
+
#
|
|
242
|
+
# @param comparator [Operator] the comparison operator to apply
|
|
243
|
+
# @param left [Object] a `Resolver` leaf node (`Resolver.parse`'s
|
|
244
|
+
# own return) for the left operand
|
|
245
|
+
# @param right [Object] a `Resolver` leaf node (`Resolver.parse`'s
|
|
246
|
+
# own return) for the right operand
|
|
247
|
+
# @param state [Hash{Symbol => Object}] the stored attribute values
|
|
248
|
+
# the operands may resolve against
|
|
249
|
+
# @param attrs [Hash{Symbol => Object}] the call's own argument
|
|
250
|
+
# values, checked before `state`
|
|
251
|
+
# @return [Boolean] the comparison's result
|
|
252
|
+
# @raise [EvaluationError] if an operand does not resolve, or
|
|
253
|
+
# `less_than`/`equal?` cannot compare the resolved values
|
|
175
254
|
def compare(comparator, left, right, state, attrs)
|
|
176
255
|
lhs = Resolver.interpret(left, state, attrs)
|
|
177
256
|
rhs = Resolver.interpret(right, state, attrs)
|
|
@@ -183,12 +262,27 @@ module Hecks
|
|
|
183
262
|
# test (SignTest#compares_via names an Operator symbol) can apply the
|
|
184
263
|
# same primitives compare() uses against the literal 0, rather than
|
|
185
264
|
# re-deriving positive?/negative?/zero? by hand a second time.
|
|
265
|
+
#
|
|
266
|
+
# @param comparator [Operator] the comparison operator to apply
|
|
267
|
+
# @param lhs [Object] the already-resolved left operand
|
|
268
|
+
# @param rhs [Object] the already-resolved right operand
|
|
269
|
+
# @return [Boolean] `comparator`'s result over `lhs`/`rhs`
|
|
270
|
+
# @raise [EvaluationError] if `comparator` tests less-than and
|
|
271
|
+
# `lhs`/`rhs` are not both numeric or both String
|
|
186
272
|
def apply(comparator, lhs, rhs)
|
|
187
273
|
result = (comparator.compares_less_than && less_than(lhs, rhs)) ||
|
|
188
274
|
(comparator.compares_equal && equal?(lhs, rhs))
|
|
189
275
|
comparator.negated ? !result : result
|
|
190
276
|
end
|
|
191
277
|
|
|
278
|
+
# Compares two already-resolved operands for the `<` primitive.
|
|
279
|
+
#
|
|
280
|
+
# @param lhs [Object] the already-resolved left operand
|
|
281
|
+
# @param rhs [Object] the already-resolved right operand
|
|
282
|
+
# @return [Boolean] whether `lhs` is less than `rhs`, comparing
|
|
283
|
+
# numerically if both are numeric, lexically if both are String
|
|
284
|
+
# @raise [EvaluationError] if `lhs`/`rhs` are not both numeric or
|
|
285
|
+
# both String
|
|
192
286
|
def less_than(lhs, rhs)
|
|
193
287
|
left = Resolver.numeric(lhs)
|
|
194
288
|
right = Resolver.numeric(rhs)
|
|
@@ -199,6 +293,12 @@ module Hecks
|
|
|
199
293
|
"comparison of #{class_of(lhs)} with #{Resolver.describe(rhs)} failed"
|
|
200
294
|
end
|
|
201
295
|
|
|
296
|
+
# Compares two already-resolved operands for the `equal` primitive.
|
|
297
|
+
#
|
|
298
|
+
# @param lhs [Object] the already-resolved left operand
|
|
299
|
+
# @param rhs [Object] the already-resolved right operand
|
|
300
|
+
# @return [Boolean] whether `lhs` equals `rhs`, comparing
|
|
301
|
+
# numerically if both are numeric, `==` otherwise
|
|
202
302
|
def equal?(lhs, rhs)
|
|
203
303
|
left = Resolver.numeric(lhs)
|
|
204
304
|
right = Resolver.numeric(rhs)
|
|
@@ -207,10 +307,19 @@ module Hecks
|
|
|
207
307
|
lhs == rhs
|
|
208
308
|
end
|
|
209
309
|
|
|
310
|
+
# Reports whether `value` is truthy, for a bare `Resolve` node.
|
|
311
|
+
#
|
|
312
|
+
# @param value [Object] the value to test
|
|
313
|
+
# @return [Boolean] Ruby's own truthiness: `false` for `nil` and
|
|
314
|
+
# `false`, `true` for everything else
|
|
210
315
|
def truthy?(value)
|
|
211
316
|
!value.nil? && value != false
|
|
212
317
|
end
|
|
213
318
|
|
|
319
|
+
# Names `value`'s class, for a refusal message.
|
|
320
|
+
#
|
|
321
|
+
# @param value [Object] the value to name
|
|
322
|
+
# @return [String] `"nil"` for `nil`, otherwise `value.class.name`
|
|
214
323
|
def class_of(value)
|
|
215
324
|
value.nil? ? "nil" : value.class.name
|
|
216
325
|
end
|
|
@@ -227,6 +336,13 @@ module Hecks
|
|
|
227
336
|
# string's last character — `Resolver.matching_paren` is reused
|
|
228
337
|
# directly rather than duplicated, the same depth-tracking rule
|
|
229
338
|
# either grammar layer needs here.
|
|
339
|
+
# Splits `expr` at its outermost `.include?(...)` call, if any.
|
|
340
|
+
#
|
|
341
|
+
# @param expr [String] the boolean-position expression text
|
|
342
|
+
# @return [Array(String, String), nil] the `[haystack_text,
|
|
343
|
+
# needle_text]` pair for the outermost `.include?(` occurrence
|
|
344
|
+
# whose matching close paren reaches `expr`'s last character, or
|
|
345
|
+
# `nil` if none does
|
|
230
346
|
def match_include(expr)
|
|
231
347
|
start = 0
|
|
232
348
|
marker = ".include?("
|
|
@@ -246,6 +362,19 @@ module Hecks
|
|
|
246
362
|
# does.
|
|
247
363
|
INCLUDE_HAYSTACKS = Hecks::Vocabulary.fetch("IncludeHaystack")
|
|
248
364
|
|
|
365
|
+
# Resolves and evaluates an `Include` node's own `.include?` test.
|
|
366
|
+
#
|
|
367
|
+
# @param parts [Array(Object, Object)] the `[haystack, needle]`
|
|
368
|
+
# pair of `Resolver` leaf nodes from `Include#haystack`/`#needle`
|
|
369
|
+
# @param state [Hash{Symbol => Object}] the stored attribute values
|
|
370
|
+
# the operands may resolve against
|
|
371
|
+
# @param attrs [Hash{Symbol => Object}] the call's own argument
|
|
372
|
+
# values, checked before `state`
|
|
373
|
+
# @return [Boolean] whether the resolved haystack includes the
|
|
374
|
+
# resolved needle (`equal?`-compared for an Array, `#include?`
|
|
375
|
+
# for a String); `false` for any other resolved haystack type
|
|
376
|
+
# @raise [EvaluationError] if the haystack resolves to a String and
|
|
377
|
+
# the needle does not
|
|
249
378
|
def includes?(parts, state, attrs)
|
|
250
379
|
haystack, needle = parts
|
|
251
380
|
wanted = Resolver.interpret(needle, state, attrs)
|
|
@@ -260,6 +389,11 @@ module Hecks
|
|
|
260
389
|
end
|
|
261
390
|
end
|
|
262
391
|
|
|
392
|
+
# Strips a redundant outer pair of parens, recursively.
|
|
393
|
+
#
|
|
394
|
+
# @param expr [String] the expression text
|
|
395
|
+
# @return [String] `expr` with every redundant outer `(...)` pair
|
|
396
|
+
# removed; `expr` unchanged if it is not wholly parenthesized
|
|
263
397
|
def strip_parens(expr)
|
|
264
398
|
return expr unless expr.start_with?("(") && expr.end_with?(")")
|
|
265
399
|
|
|
@@ -272,6 +406,14 @@ module Hecks
|
|
|
272
406
|
strip_parens(expr[1..-2].strip)
|
|
273
407
|
end
|
|
274
408
|
|
|
409
|
+
# Splits `expr` at its first top-level occurrence of `operator`.
|
|
410
|
+
#
|
|
411
|
+
# @param expr [String] the expression text
|
|
412
|
+
# @param operator [String] the operator text to split on, such as
|
|
413
|
+
# `"||"` or `"&&"`
|
|
414
|
+
# @return [Array(String, String), nil] the `[left, right]` operand
|
|
415
|
+
# text around the split, or `nil` if `expr` has no top-level
|
|
416
|
+
# occurrence
|
|
275
417
|
def split_top_level(expr, operator)
|
|
276
418
|
index = top_level_index(expr, operator)
|
|
277
419
|
return nil unless index
|
|
@@ -279,6 +421,17 @@ module Hecks
|
|
|
279
421
|
[expr[0...index].strip, expr[(index + operator.length)..].strip]
|
|
280
422
|
end
|
|
281
423
|
|
|
424
|
+
# Splits `expr` at its first top-level occurrence of `operator`
|
|
425
|
+
# that is not part of a longer operator's own spelling (`==` is
|
|
426
|
+
# not mistaken for the middle of `===`, `<` is not mistaken for
|
|
427
|
+
# the leading `<` of `<=`).
|
|
428
|
+
#
|
|
429
|
+
# @param expr [String] the expression text
|
|
430
|
+
# @param operator [String] the comparison operator text to split
|
|
431
|
+
# on, such as `"=="` or `"<"`
|
|
432
|
+
# @return [Array(String, String), nil] the `[left, right]` operand
|
|
433
|
+
# text around the split, or `nil` if `expr` has no matching
|
|
434
|
+
# top-level occurrence
|
|
282
435
|
def split_comparison(expr, operator)
|
|
283
436
|
index = top_level_index(expr, operator) { |at| !part_of_longer?(expr, at, operator) }
|
|
284
437
|
return nil unless index
|
|
@@ -286,6 +439,15 @@ module Hecks
|
|
|
286
439
|
[expr[0...index].strip, expr[(index + operator.length)..].strip]
|
|
287
440
|
end
|
|
288
441
|
|
|
442
|
+
# Reports whether the occurrence of `operator` at `index` is
|
|
443
|
+
# actually the middle of a longer operator's own spelling (`==`
|
|
444
|
+
# inside `===`, or `<`/`>`/`!`/`=` immediately before a bare `=`).
|
|
445
|
+
#
|
|
446
|
+
# @param expr [String] the expression text
|
|
447
|
+
# @param index [Integer] the index of the candidate occurrence
|
|
448
|
+
# @param operator [String] the operator text being tried
|
|
449
|
+
# @return [Boolean] whether this occurrence belongs to a longer
|
|
450
|
+
# operator and should be skipped
|
|
289
451
|
def part_of_longer?(expr, index, operator)
|
|
290
452
|
after = expr[index + operator.length]
|
|
291
453
|
before = index.positive? ? expr[index - 1] : nil
|
|
@@ -296,6 +458,21 @@ module Hecks
|
|
|
296
458
|
false
|
|
297
459
|
end
|
|
298
460
|
|
|
461
|
+
# A grammar's own depth-aware scanner, shared by `split_top_level`/
|
|
462
|
+
# `split_comparison` — one pass finds the first top-level
|
|
463
|
+
# occurrence of `operator`, tracking quotes and every bracket
|
|
464
|
+
# kind (`(`/`)`, `{`/`}`, `[`/`]`) so an operator inside a nested
|
|
465
|
+
# call, block predicate, or array literal is never mistaken for a
|
|
466
|
+
# split point at this level.
|
|
467
|
+
#
|
|
468
|
+
# @param expr [String] the expression text
|
|
469
|
+
# @param operator [String] the operator text to search for
|
|
470
|
+
# @yieldparam index [Integer] a candidate top-level occurrence's
|
|
471
|
+
# index, offered so the caller can reject it (`split_comparison`
|
|
472
|
+
# uses this to skip a longer operator's own spelling)
|
|
473
|
+
# @yieldreturn [Boolean] whether to accept this occurrence
|
|
474
|
+
# @return [Integer, nil] the accepted occurrence's index, or `nil`
|
|
475
|
+
# if `operator` has no top-level occurrence the block accepts
|
|
299
476
|
def top_level_index(expr, operator)
|
|
300
477
|
depth = 0
|
|
301
478
|
quote = nil
|
|
@@ -335,10 +512,11 @@ module Hecks
|
|
|
335
512
|
# (`[a, b]`) can appear as a general sub-expression, not only
|
|
336
513
|
# as `.include?`'s own haystack, the moment an array-typed
|
|
337
514
|
# attribute or a synthesized literal is embedded anywhere else
|
|
338
|
-
# --
|
|
339
|
-
#
|
|
340
|
-
# this expression's
|
|
341
|
-
#
|
|
515
|
+
# -- so without counting `[`/`]` toward depth here too, an
|
|
516
|
+
# element containing a top-level `+`/comparison of its own
|
|
517
|
+
# (`[0, 0 + 0]`) reads as a split point for this expression's
|
|
518
|
+
# own boolean/comparison grammar, exactly the way an
|
|
519
|
+
# un-tracked `{`/`}` does for block predicates.
|
|
342
520
|
elsif ["(", "{", "["].include?(char)
|
|
343
521
|
depth += 1
|
|
344
522
|
elsif [")", "}", "]"].include?(char)
|
|
@@ -119,6 +119,10 @@ module Hecks
|
|
|
119
119
|
# is captured from whatever follows the closing brace ; every
|
|
120
120
|
# other suffix instead requires nothing follow it at all (the
|
|
121
121
|
# `BlockPredicate` shape, unchanged from before this rewrite).
|
|
122
|
+
#
|
|
123
|
+
# @param expr [String] the leaf expression text to parse
|
|
124
|
+
# @return [Find, BlockPredicate, nil] the parsed node, or nil when `expr` does
|
|
125
|
+
# not open a `.all?`/`.any?`/`.none?`/`.find` block, or its brace never closes
|
|
122
126
|
def parse_block_opener(expr)
|
|
123
127
|
pattern = /\A(.+?)\.(#{BLOCK_OPENER_SUFFIXES.map { |suffix| Regexp.escape(suffix) }.join('|')})\s*\{\s*\|(\w+)\|\s*/m
|
|
124
128
|
header = expr.match(pattern)
|
|
@@ -155,6 +159,11 @@ module Hecks
|
|
|
155
159
|
# quoted substring (`.start_with?("}")`) never miscounts, the
|
|
156
160
|
# same discipline `split_addition`/`array_elements` already
|
|
157
161
|
# apply for their own depth tracking.
|
|
162
|
+
#
|
|
163
|
+
# @param expr [String] the text to scan
|
|
164
|
+
# @param start [Integer] the index just after the opening `{`, where depth is 1
|
|
165
|
+
# @return [Integer, nil] the index of the matching `}`, or nil if `expr` runs
|
|
166
|
+
# out before depth returns to 0
|
|
158
167
|
def matching_brace(expr, start)
|
|
159
168
|
depth = 1
|
|
160
169
|
quote = nil
|
|
@@ -186,6 +195,13 @@ module Hecks
|
|
|
186
195
|
# variable concept added anywhere else in Resolver's state model,
|
|
187
196
|
# just `attrs` extended with the bound name for the span of that
|
|
188
197
|
# one predicate evaluation, discarded immediately after.
|
|
198
|
+
#
|
|
199
|
+
# @param node [BlockPredicate] the parsed `.all?`/`.any?`/`.none?` node
|
|
200
|
+
# @param collection [Object] the interpreted receiver, expected to be an Array
|
|
201
|
+
# @param state [Hash{Symbol => Object}] the record's own current state
|
|
202
|
+
# @param attrs [Hash{Symbol => Object}] the command's own bound arguments
|
|
203
|
+
# @return [Boolean] whether the collection satisfies `node.mode`
|
|
204
|
+
# @raise [EvaluationError] if `collection` is not an Array
|
|
189
205
|
def evaluate_block_predicate(node, collection, state, attrs)
|
|
190
206
|
raise EvaluationError, "#{node.mode}? expects a list, got #{describe(collection)}" unless collection.is_a?(Array)
|
|
191
207
|
|
|
@@ -202,6 +218,18 @@ module Hecks
|
|
|
202
218
|
# evaluation -- `attrs` wins over `state` in `fetch` (see below),
|
|
203
219
|
# so the bound name shadows any same-named state/attrs field for
|
|
204
220
|
# the span of this one call only ; nothing persists past it.
|
|
221
|
+
#
|
|
222
|
+
# @param node [BlockPredicate, Find] the node whose `param`/
|
|
223
|
+
# `predicate` to bind and interpret
|
|
224
|
+
# @param element [Object] the one collection element to bind
|
|
225
|
+
# `node.param` to
|
|
226
|
+
# @param state [Hash{Symbol => Object}] the record's own current
|
|
227
|
+
# state
|
|
228
|
+
# @param attrs [Hash{Symbol => Object}] the command's own bound
|
|
229
|
+
# arguments
|
|
230
|
+
# @return [Object] `node.predicate` interpreted with `node.param`
|
|
231
|
+
# bound to `element`
|
|
232
|
+
# @raise [EvaluationError] if `node.predicate` refuses to evaluate
|
|
205
233
|
def interpret_with_element(node, element, state, attrs)
|
|
206
234
|
Evaluator.interpret(node.predicate, state, attrs.merge(node.param.to_sym => element))
|
|
207
235
|
end
|
|
@@ -219,6 +247,14 @@ module Hecks
|
|
|
219
247
|
# value case in this grammar already has, and the one a re-
|
|
220
248
|
# routing check like "is there a leg after this one" needs :
|
|
221
249
|
# not finding one is a normal outcome, not an error.
|
|
250
|
+
#
|
|
251
|
+
# @param node [Find] the parsed `.find` node
|
|
252
|
+
# @param collection [Object] the interpreted receiver, expected to be an Array
|
|
253
|
+
# @param state [Hash{Symbol => Object}] the record's own current state
|
|
254
|
+
# @param attrs [Hash{Symbol => Object}] the command's own bound arguments
|
|
255
|
+
# @return [Object, nil] the found element (or `node.path` projected through it),
|
|
256
|
+
# or nil when no element matches or a `path` segment does not resolve
|
|
257
|
+
# @raise [EvaluationError] if `collection` is not an Array
|
|
222
258
|
def found_of(node, collection, state, attrs)
|
|
223
259
|
raise EvaluationError, "find expects a list, got #{describe(collection)}" unless collection.is_a?(Array)
|
|
224
260
|
|