hecks 0.3.0 → 1.0.1
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/d1.rb +38 -14
- data/lib/hecks/adapters/driven/governance_authorization.rb +41 -2
- data/lib/hecks/adapters/driven/heki/journal.rb +7 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +31 -4
- data/lib/hecks/adapters/driven/heki.rb +40 -9
- data/lib/hecks/adapters/driven/lambda.rb +16 -2
- data/lib/hecks/adapters/driven/postgres/codec.rb +8 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +45 -6
- data/lib/hecks/adapters/driven/postgres.rb +76 -29
- data/lib/hecks/adapters/driven/postgres_era.adapter +11 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +1 -1
- data/lib/hecks/adapters/driven/sqlite/codec.rb +17 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +76 -9
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +17 -5
- data/lib/hecks/adapters/driven/sqlite.rb +30 -9
- data/lib/hecks/adapters/driven.rb +19 -1
- data/lib/hecks/behaviors/dsl.rb +29 -0
- data/lib/hecks/behaviors/expectations.rb +62 -2
- data/lib/hecks/bluebook/assembly/contracts.rb +36 -7
- data/lib/hecks/bluebook/assembly/marks.rb +4 -3
- data/lib/hecks/bluebook/assembly.rb +14 -1
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +18 -1
- data/lib/hecks/bluebook/behaviour/process_manager.rb +14 -1
- data/lib/hecks/bluebook/behaviour/value_object.rb +13 -7
- data/lib/hecks/bluebook/chapter.rb +21 -11
- data/lib/hecks/bluebook/command.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +117 -5
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +21 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +71 -2
- data/lib/hecks/bluebook/dsl/command_builder.rb +144 -3
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +49 -7
- data/lib/hecks/bluebook/dsl/entity_builder.rb +129 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +18 -3
- data/lib/hecks/bluebook/dsl/port_builder.rb +12 -3
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +109 -10
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +9 -2
- data/lib/hecks/bluebook/dsl/world_builder.rb +44 -4
- data/lib/hecks/bluebook/expression/ast_json.rb +149 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +71 -3
- data/lib/hecks/bluebook/expression/evaluator.rb +50 -7
- data/lib/hecks/bluebook/expression/projection.json +48 -0
- data/lib/hecks/bluebook/expression/resolver.rb +161 -10
- data/lib/hecks/bluebook/expression.rb +1 -0
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +78 -16
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +14 -4
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/shapes.rb +30 -10
- data/lib/hecks/bluebook/meta_validator.rb +103 -13
- data/lib/hecks/bluebook/model_check.rb +147 -6
- data/lib/hecks/bluebook/pattern_subset.rb +66 -2
- data/lib/hecks/bluebook/process_manager.rb +53 -11
- data/lib/hecks/bluebook/project_register.rb +25 -0
- data/lib/hecks/bluebook/value_object.rb +19 -2
- data/lib/hecks/doc/reference.rb +22 -1
- data/lib/hecks/facade/cli_door.rb +6 -3
- data/lib/hecks/facade/json_door.rb +16 -4
- data/lib/hecks/forms/app.rb +47 -6
- data/lib/hecks/forms/command_form_renderer.rb +1 -1
- data/lib/hecks/forms/field_renderer.rb +11 -4
- data/lib/hecks/forms/html.rb +31 -0
- data/lib/hecks/forms/params.rb +30 -1
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/record_renderer.rb +6 -2
- data/lib/hecks/forms/record_table.rb +6 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +19 -19
- data/lib/hecks/framework/bluebook/governance.bluebook +26 -11
- data/lib/hecks/framework/bluebook/identity.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +3 -2
- data/lib/hecks/fuzzing/isolated_boot.rb +212 -18
- data/lib/hecks/fuzzing/properties.rb +52 -6
- data/lib/hecks/fuzzing/replay.rb +51 -18
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +28 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +71 -6
- data/lib/hecks/fuzzing/sequence_generator.rb +5 -4
- data/lib/hecks/fuzzing/value_generator.rb +25 -2
- data/lib/hecks/grammar/evolve.rb +33 -0
- data/lib/hecks/grammar/expression.bluebook +8 -5
- data/lib/hecks/grammar/expression_operators.json +497 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +12 -12
- data/lib/hecks/language/bluebook/bluebook.bluebook +3 -3
- data/lib/hecks/language/bluebook/command.bluebook +42 -8
- data/lib/hecks/language/bluebook/entity.bluebook +86 -10
- data/lib/hecks/language/bluebook/policy.bluebook +21 -4
- data/lib/hecks/language/bluebook/process_manager.bluebook +135 -18
- data/lib/hecks/language/bluebook/projection.bluebook +6 -6
- data/lib/hecks/language/bluebook/query.bluebook +4 -4
- data/lib/hecks/language/bluebook/shape.bluebook +6 -6
- data/lib/hecks/language/bluebook/syntax.bluebook +12 -11
- data/lib/hecks/language/bluebook/vocabulary.bluebook +22 -7
- data/lib/hecks/language/hecksagon/domain_port.bluebook +25 -1
- data/lib/hecks/language/oidc.json +20 -0
- data/lib/hecks/language/port.bluebook +30 -2
- data/lib/hecks/naming.rb +54 -1
- data/lib/hecks/ports/access_control.port +7 -2
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.port +6 -2
- data/lib/hecks/ports/agent.rb +1 -1
- data/lib/hecks/ports/authentication.port +4 -2
- data/lib/hecks/ports/authentication.rb +1 -1
- data/lib/hecks/ports/authorization.port +5 -2
- data/lib/hecks/ports/authorization.rb +14 -11
- data/lib/hecks/ports/clock.port +3 -2
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.port +3 -2
- data/lib/hecks/ports/extraction.rb +1 -1
- data/lib/hecks/ports/identity_assignment.port +3 -2
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.port +3 -2
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.port +3 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/execution.rb +6 -1
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_check.rb +41 -8
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard.rb +24 -56
- data/lib/hecks/ports/persistence/{lineage.rb → plugins/era/lineage.rb} +31 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/era_store.rb +3 -3
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/head_compiler.rb +59 -10
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/mint_transaction.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/provisioning.rb +29 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/tail_merge.rb +11 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/transform_installer.rb +20 -0
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage.rb +1 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/coverage_check.rb +5 -5
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/era_resolver.rb +5 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/merge_coordinator.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/minter.rb +4 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era.rb +116 -29
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/approval_digest.rb +1 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_one.rb +14 -5
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_two.rb +31 -6
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/unfed_report.rb +5 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/reattest.rb +3 -3
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/differ.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence.rb +1 -1
- data/lib/hecks/ports/projection.rb +20 -0
- data/lib/hecks/projections/diagrams.rb +230 -1
- data/lib/hecks/projections/shape.rb +15 -2
- data/lib/hecks/projector/cli_projector.rb +10 -1
- data/lib/hecks/projector/exporter.rb +19 -2
- data/lib/hecks/query_ir.rb +19 -0
- data/lib/hecks/query_specification/common/null_policy.rb +13 -1
- data/lib/hecks/query_specification/field_path.rb +20 -2
- data/lib/hecks/query_specification/hop_path.rb +7 -5
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +19 -3
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -0
- data/lib/hecks/runtime/command_interpreter.rb +165 -34
- data/lib/hecks/runtime/command_rules/admissibility.rb +101 -16
- data/lib/hecks/runtime/command_rules/arithmetic.rb +40 -4
- data/lib/hecks/runtime/command_rules/authorization.rb +2 -1
- data/lib/hecks/runtime/command_rules/references.rb +27 -19
- data/lib/hecks/runtime/dependency_planning.rb +66 -5
- data/lib/hecks/runtime/dispatcher.rb +19 -4
- data/lib/hecks/runtime/entity_interpreter.rb +92 -14
- data/lib/hecks/runtime/errors.rb +22 -0
- data/lib/hecks/runtime/identity.rb +30 -2
- data/lib/hecks/runtime/instance.rb +59 -4
- data/lib/hecks/runtime/interpreting.rb +21 -0
- data/lib/hecks/runtime/loader.rb +59 -18
- data/lib/hecks/runtime/query_interpreter.rb +36 -4
- data/lib/hecks/runtime/reaction_invocation.rb +9 -1
- data/lib/hecks/runtime/read_model_interpreter.rb +89 -5
- data/lib/hecks/runtime/refusal_wording.rb +2 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +75 -3
- data/lib/hecks/runtime/registry/verification.rb +88 -0
- data/lib/hecks/runtime/registry.rb +69 -8
- data/lib/hecks/runtime/saga_interpreter.rb +215 -13
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/value/admission.rb +19 -1
- data/lib/hecks/runtime/value/coercion.rb +75 -10
- data/lib/hecks/runtime.rb +17 -5
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +6 -1
- data/lib/hecks.rb +28 -3
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +49 -37
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard/shape_diff.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_tamper.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/field_cache.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/resumable_backfill.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/storage_shape.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/rule_compiler.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/renderer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/writer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold.rb +0 -0
- /data/lib/hecks/{translation.rb → ports/persistence/plugins/era/translation.rb} +0 -0
|
@@ -37,7 +37,7 @@ module Hecks
|
|
|
37
37
|
|
|
38
38
|
def step(text, rule)
|
|
39
39
|
case rule.strategy
|
|
40
|
-
when "collapse_whitespace" then text.gsub(/\s+/, " ")
|
|
40
|
+
when "collapse_whitespace" then map_outside_strings(text) { |segment| segment.gsub(/\s+/, " ") }
|
|
41
41
|
when "replace" then replace(text, rule)
|
|
42
42
|
else
|
|
43
43
|
raise ArgumentError, "#{rule.strategy.inspect} is not a linked normalisation strategy"
|
|
@@ -45,9 +45,77 @@ module Hecks
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def replace(text, rule)
|
|
48
|
-
|
|
48
|
+
map_outside_strings(text) do |segment|
|
|
49
|
+
if rule.boundary == "none"
|
|
50
|
+
segment.gsub(rule.source_token, rule.replacement)
|
|
51
|
+
else
|
|
52
|
+
segment.gsub(/#{Regexp.escape(rule.source_token)}(?![[:alnum:]_])/, rule.replacement)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Applies a normalisation rule to the text OUTSIDE quoted string
|
|
58
|
+
# literals only, copying every quoted run through byte-for-byte.
|
|
59
|
+
# Every rule here (collapse_whitespace, the `.length`→`.size` fold)
|
|
60
|
+
# used to run quote-blind — `"a b"` collapsed to `"a b"` and
|
|
61
|
+
# `"a.length"` folded to `"a.size"` just as readily as the real
|
|
62
|
+
# source outside the quotes, silently rewriting what a predicate
|
|
63
|
+
# compares a string attribute against, not merely how the
|
|
64
|
+
# predicate itself is spelled. A canonical string literal's
|
|
65
|
+
# CONTENTS are data, never syntax to normalise.
|
|
66
|
+
#
|
|
67
|
+
# Handles both `"` and `'` delimiters (this grammar's own
|
|
68
|
+
# `Resolver.quoted?` admits either), quote-aware exactly the way
|
|
69
|
+
# `Evaluator.top_level_index`/`Resolver.array_elements` already are
|
|
70
|
+
# elsewhere in this sublanguage. An unterminated quote (malformed
|
|
71
|
+
# input) is passed through raw rather than risk mangling it further.
|
|
72
|
+
def map_outside_strings(text)
|
|
73
|
+
result = +""
|
|
74
|
+
buffer = +""
|
|
75
|
+
quote = nil
|
|
76
|
+
|
|
77
|
+
text.each_char do |char|
|
|
78
|
+
if quote
|
|
79
|
+
buffer << char
|
|
80
|
+
if char == quote
|
|
81
|
+
result << buffer
|
|
82
|
+
buffer = +""
|
|
83
|
+
quote = nil
|
|
84
|
+
end
|
|
85
|
+
elsif ['"', "'"].include?(char)
|
|
86
|
+
result << yield(buffer)
|
|
87
|
+
# `char.dup`, not `char.to_s` (a no-op on a String — always
|
|
88
|
+
# returns self, never a copy) and not `+char` either
|
|
89
|
+
# (`String#+@` only dups a FROZEN receiver; `each_char`'s
|
|
90
|
+
# yielded strings aren't frozen, so `+char` is just as
|
|
91
|
+
# much a no-op here). Without a REAL copy, `buffer` and
|
|
92
|
+
# `quote` alias the same mutable object: the very next
|
|
93
|
+
# `buffer << char` grows `quote` right along with it, so
|
|
94
|
+
# `char == quote` can only ever compare a single character
|
|
95
|
+
# against an ever-lengthening string and never closes the
|
|
96
|
+
# literal — everything after a predicate's first quoted
|
|
97
|
+
# string silently skipped normalisation for the rest of
|
|
98
|
+
# the text, undetected because passing text through
|
|
99
|
+
# unnormalised is silent. MASKED by every existing spec
|
|
100
|
+
# here, which only checks that quoted CONTENTS survive
|
|
101
|
+
# untouched (the M7 fix this method exists for) — that
|
|
102
|
+
# still holds by accident once the bug makes the "outside"
|
|
103
|
+
# branch unreachable. Found live: a multi-line `given`/
|
|
104
|
+
# `ensures` block whose ONLY quoted literal closes before a
|
|
105
|
+
# later line — the newline and that later line's own
|
|
106
|
+
# indentation went uncollapsed, diverging from
|
|
107
|
+
# `hecks-parse`'s own (correct) single-space join.
|
|
108
|
+
# `spec/parser_parity_spec.rb`, roster's "a front-row seat
|
|
109
|
+
# takes a member of age".
|
|
110
|
+
buffer = char.dup
|
|
111
|
+
quote = char.dup
|
|
112
|
+
else
|
|
113
|
+
buffer << char
|
|
114
|
+
end
|
|
115
|
+
end
|
|
49
116
|
|
|
50
|
-
|
|
117
|
+
result << (quote ? buffer : yield(buffer))
|
|
118
|
+
result
|
|
51
119
|
end
|
|
52
120
|
end
|
|
53
121
|
end
|
|
@@ -67,6 +67,23 @@ module Hecks
|
|
|
67
67
|
left, right = split_top_level(expr, "&&")
|
|
68
68
|
return And.new(left: parse(left), right: parse(right)) if left
|
|
69
69
|
|
|
70
|
+
# Tried BEFORE `.include?`/comparisons, not after — `!` negates
|
|
71
|
+
# the WHOLE boolean expression that follows it (`!names.include?(x)`
|
|
72
|
+
# means `!(names.include?(x))`, never "call .include? on the negated
|
|
73
|
+
# receiver"), so the leading marker has to be stripped and the
|
|
74
|
+
# remainder re-parsed before anything downstream gets a chance to
|
|
75
|
+
# mis-scan across it. It used to sit after `match_include`, whose
|
|
76
|
+
# naive `rindex(".include?(")` has no concept of a leading `!` —
|
|
77
|
+
# for `!names.include?(x)` it swallowed the `!` straight into the
|
|
78
|
+
# haystack text ("!names"), which `Resolver.parse` cannot resolve,
|
|
79
|
+
# so every spelling of negated membership raised instead of
|
|
80
|
+
# evaluating. Moving the check here fixes both the bare prefix
|
|
81
|
+
# (`!names.include?(x)`) and the parenthesized form
|
|
82
|
+
# (`!(names.include?(x))`) — the recursive `parse` call sees the
|
|
83
|
+
# clean remainder and correctly finds the `.include?` (or `&&`/`||`)
|
|
84
|
+
# inside it.
|
|
85
|
+
return Not.new(node: parse(Regexp.last_match(1))) if expr =~ /\A!(.+)\z/
|
|
86
|
+
|
|
70
87
|
membership = match_include(expr)
|
|
71
88
|
return Include.new(haystack: Resolver.parse(membership[0]), needle: Resolver.parse(membership[1])) if membership
|
|
72
89
|
|
|
@@ -75,8 +92,6 @@ module Hecks
|
|
|
75
92
|
return Compare.new(operator: op, left: Resolver.parse(left), right: Resolver.parse(right)) if left
|
|
76
93
|
end
|
|
77
94
|
|
|
78
|
-
return Not.new(node: parse(Regexp.last_match(1))) if expr =~ /\A!(.+)\z/
|
|
79
|
-
|
|
80
95
|
Resolve.new(expr: Resolver.parse(expr))
|
|
81
96
|
end
|
|
82
97
|
|
|
@@ -145,11 +160,28 @@ module Hecks
|
|
|
145
160
|
value.nil? ? "nil" : value.class.name
|
|
146
161
|
end
|
|
147
162
|
|
|
163
|
+
# THE SAME MIS-SPLIT `Resolver.match_call` had (its own comment
|
|
164
|
+
# has the full story), found here too by the same generator: a
|
|
165
|
+
# `.include?` needle can itself be — or contain — ANOTHER
|
|
166
|
+
# `.include?` call (`"".include?(arr.all? { |el| "".include?("")
|
|
167
|
+
# }.to_s)`, a String built via `.to_s` off a block predicate
|
|
168
|
+
# whose own body happens to include one) — `rindex` finds the
|
|
169
|
+
# INNERMOST occurrence, not the outermost this split actually
|
|
170
|
+
# needs. Fixed identically: try each occurrence left to right,
|
|
171
|
+
# keep the first whose own balanced-paren match reaches the
|
|
172
|
+
# string's last character — `Resolver.matching_paren` is reused
|
|
173
|
+
# directly rather than duplicated, the same depth-tracking rule
|
|
174
|
+
# either grammar layer needs here.
|
|
148
175
|
def match_include(expr)
|
|
149
|
-
|
|
150
|
-
|
|
176
|
+
start = 0
|
|
177
|
+
marker = ".include?("
|
|
178
|
+
while (index = expr.index(marker, start))
|
|
179
|
+
close = Resolver.matching_paren(expr, index + marker.length)
|
|
180
|
+
return [expr[0...index], expr[(index + marker.length)...close]] if close == expr.length - 1
|
|
151
181
|
|
|
152
|
-
|
|
182
|
+
start = index + 1
|
|
183
|
+
end
|
|
184
|
+
nil
|
|
153
185
|
end
|
|
154
186
|
|
|
155
187
|
# Declared the same way in Vocabulary::IncludeHaystack
|
|
@@ -243,9 +275,20 @@ module Hecks
|
|
|
243
275
|
# built to close. `{`/`}` cannot legitimately appear inside
|
|
244
276
|
# a quoted literal either, so this sits beside the existing
|
|
245
277
|
# paren-depth branch, not instead of it.
|
|
246
|
-
|
|
278
|
+
#
|
|
279
|
+
# `[`/`]` -- the identical lesson a THIRD time (found live via
|
|
280
|
+
# the type-directed bounded-exhaustive expression generator,
|
|
281
|
+
# Phase 7 of the equivalence-gap plan): `Resolver::ArrayLiteral`
|
|
282
|
+
# (`[a, b]`) can appear as a general sub-expression, not only
|
|
283
|
+
# as `.include?`'s own haystack, the moment an array-typed
|
|
284
|
+
# attribute or a synthesized literal is embedded anywhere else
|
|
285
|
+
# -- and an element containing a top-level `+`/comparison of
|
|
286
|
+
# its own (`[0, 0 + 0]`) used to read as a split point for
|
|
287
|
+
# THIS expression's own boolean/comparison grammar, exactly
|
|
288
|
+
# the way an un-tracked `{`/`}` once did for block predicates.
|
|
289
|
+
elsif char == "(" || char == "{" || char == "["
|
|
247
290
|
depth += 1
|
|
248
|
-
elsif char == ")" || char == "}"
|
|
291
|
+
elsif char == ")" || char == "}" || char == "]"
|
|
249
292
|
depth -= 1
|
|
250
293
|
elsif depth.zero? && expr[index, operator.length] == operator
|
|
251
294
|
return index if !block_given? || yield(index)
|
|
@@ -149,6 +149,54 @@
|
|
|
149
149
|
"category": "enumeration",
|
|
150
150
|
"precedence": 10,
|
|
151
151
|
"arity": 2
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"symbol": ".match?",
|
|
155
|
+
"category": "pattern_match",
|
|
156
|
+
"precedence": 7,
|
|
157
|
+
"arity": 2
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"symbol": ".present?",
|
|
161
|
+
"category": "presence",
|
|
162
|
+
"precedence": 7,
|
|
163
|
+
"arity": 1
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"symbol": ".blank?",
|
|
167
|
+
"category": "presence",
|
|
168
|
+
"precedence": 7,
|
|
169
|
+
"arity": 1
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"symbol": ".split",
|
|
173
|
+
"category": "text",
|
|
174
|
+
"precedence": 7,
|
|
175
|
+
"arity": 2
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"symbol": ".start_with?",
|
|
179
|
+
"category": "text",
|
|
180
|
+
"precedence": 7,
|
|
181
|
+
"arity": 2
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"symbol": ".end_with?",
|
|
185
|
+
"category": "text",
|
|
186
|
+
"precedence": 7,
|
|
187
|
+
"arity": 2
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"symbol": ".first",
|
|
191
|
+
"category": "positional",
|
|
192
|
+
"precedence": 7,
|
|
193
|
+
"arity": 1
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"symbol": ".last",
|
|
197
|
+
"category": "positional",
|
|
198
|
+
"precedence": 7,
|
|
199
|
+
"arity": 1
|
|
152
200
|
}
|
|
153
201
|
],
|
|
154
202
|
"normalisations": [
|
|
@@ -58,6 +58,26 @@ module Hecks
|
|
|
58
58
|
Size = Struct.new(:receiver, keyword_init: true)
|
|
59
59
|
Lookup = Struct.new(:path, keyword_init: true)
|
|
60
60
|
|
|
61
|
+
# UPDATE 2026-08-27: every "vendored addition, not (yet) upstream
|
|
62
|
+
# hecks" comment on this file's own MatchesRegex/Presence/Split/
|
|
63
|
+
# First/Last/StartsWith/EndsWith below (plus ArrayLiteral above)
|
|
64
|
+
# described a REAL gap, found the hard way, in the history each
|
|
65
|
+
# comment tells — and that history stays exactly as written,
|
|
66
|
+
# on purpose. What changed is the PRESENT TENSE claim "not (yet)
|
|
67
|
+
# upstream": a review of this exact migration found these eight
|
|
68
|
+
# symbols had working Ruby parse/interpret arms but had NEVER
|
|
69
|
+
# gone through Propose -> Render -> Admit
|
|
70
|
+
# (lib/hecks/grammar/expression_operators.json) the way every
|
|
71
|
+
# other operator here has — a closed-vocabulary guard
|
|
72
|
+
# (spec/operator_conformance_spec.rb) built entirely over TABLES
|
|
73
|
+
# structurally could not see hand-coded Struct/parse/interpret
|
|
74
|
+
# additions, so eight operators ran in Ruby, admitted nowhere,
|
|
75
|
+
# invisible to the one guard whose whole job was "reads in every
|
|
76
|
+
# target." All eight are now ledger-admitted for real, with full
|
|
77
|
+
# Rust kernel parity (rust/src/kernel/expression_operators/
|
|
78
|
+
# {pattern_match,presence,text,positional}.rs) — the two-tier
|
|
79
|
+
# gap is closed, not merely tracked.
|
|
80
|
+
#
|
|
61
81
|
# `receiver.match?(/pattern/)` -- vendored addition, not (yet)
|
|
62
82
|
# upstream hecks (migration plan task 8): confirmed the
|
|
63
83
|
# SINGLE most impactful corpus-wide dispatch-time gap of the
|
|
@@ -332,6 +352,15 @@ module Hecks
|
|
|
332
352
|
options |= Regexp::EXTENDED if flags.include?("x")
|
|
333
353
|
|
|
334
354
|
Regexp.new(pattern, options).match?(text)
|
|
355
|
+
rescue RegexpError => e
|
|
356
|
+
# M9: a malformed pattern between the slashes (an unclosed
|
|
357
|
+
# character class, say) is a defect in the EXPRESSION TEXT
|
|
358
|
+
# itself, exactly the same category of author mistake an
|
|
359
|
+
# unresolvable attribute name already refuses for — `Regexp.new`
|
|
360
|
+
# raising a raw `RegexpError` crossed this sublanguage's own
|
|
361
|
+
# refusal boundary the same way the `ZeroDivisionError`/
|
|
362
|
+
# `TypeError` cases elsewhere in this file did.
|
|
363
|
+
raise EvaluationError, "match? given an invalid pattern #{pattern.inspect} — #{e.message}"
|
|
335
364
|
end
|
|
336
365
|
|
|
337
366
|
# The elements of a bracketed literal, or nil if this isn't one.
|
|
@@ -392,9 +421,19 @@ module Hecks
|
|
|
392
421
|
quote = nil if char == quote
|
|
393
422
|
elsif ['"', "'"].include?(char)
|
|
394
423
|
quote = char
|
|
395
|
-
|
|
424
|
+
# `[`/`]` -- the identical lesson this method's own `(`/`{`
|
|
425
|
+
# comment already names, a third time (found live via the
|
|
426
|
+
# type-directed bounded-exhaustive expression generator,
|
|
427
|
+
# Phase 7 of the equivalence-gap plan): `ArrayLiteral` can
|
|
428
|
+
# appear as a general sub-expression now, not only as
|
|
429
|
+
# `.include?`'s own haystack, so an array element containing
|
|
430
|
+
# its own top-level `+` (`[0, 0 + 0]`) used to read as THIS
|
|
431
|
+
# expression's own addition split point -- the whole
|
|
432
|
+
# receiver before `.all?`/`.any?`/etc. torn in half before
|
|
433
|
+
# `parse_block_opener` ever saw it as one atomic leaf.
|
|
434
|
+
elsif ["(", "{", "["].include?(char)
|
|
396
435
|
depth += 1
|
|
397
|
-
elsif [")", "}"].include?(char)
|
|
436
|
+
elsif [")", "}", "]"].include?(char)
|
|
398
437
|
depth -= 1
|
|
399
438
|
elsif char == "+" && depth.zero?
|
|
400
439
|
return [expr[0...index].strip, expr[(index + 1)..].strip]
|
|
@@ -518,18 +557,109 @@ module Hecks
|
|
|
518
557
|
Evaluator.apply(node.operator, number, 0)
|
|
519
558
|
end
|
|
520
559
|
|
|
560
|
+
# FOUND LIVE via the type-directed bounded-exhaustive expression
|
|
561
|
+
# generator (Phase 7, equivalence-gap plan — spec/
|
|
562
|
+
# bounded_exhaustive_expression_spec.rb): `.modulo(`'s own
|
|
563
|
+
# argument position accepts any numeric sub-expression, including
|
|
564
|
+
# ANOTHER `.modulo(...)` call — `0.modulo(num_b.modulo(-1))` is
|
|
565
|
+
# perfectly well-typed — but `expr.rindex(marker)` finds the
|
|
566
|
+
# RIGHTMOST (innermost) `.modulo(` in the whole string, not the
|
|
567
|
+
# OUTERMOST one a nested call needs split at. For that expression
|
|
568
|
+
# it found the INNER `.modulo(` (inside `num_b.modulo(-1)`) and
|
|
569
|
+
# split there, producing a receiver of `"0.modulo(num_b"` and a
|
|
570
|
+
# divisor of `"-1)"` — both garbage, both re-parsed as bogus
|
|
571
|
+
# `Lookup` paths, both then refusing with "cannot resolve" — a
|
|
572
|
+
# SILENT MISPARSE that happened to fail safe into a real
|
|
573
|
+
# `EvaluationError` rather than a raw crash, which is exactly why
|
|
574
|
+
# this had gone unnoticed: nothing before this generator existed
|
|
575
|
+
# ever fed `.modulo` a nested `.modulo` call, random fuzzing
|
|
576
|
+
# essentially never manufactures that specific shape by chance,
|
|
577
|
+
# and the resulting refusal LOOKS like an ordinary, correct one
|
|
578
|
+
# unless you already know every name this generator's own
|
|
579
|
+
# synthetic state declares (real corpus authors would see this as
|
|
580
|
+
# a mysterious "cannot resolve" on text they never wrote).
|
|
581
|
+
#
|
|
582
|
+
# Fixed the same way `split_addition`/`Evaluator.top_level_index`
|
|
583
|
+
# already handle nested `(`/`{` elsewhere in this exact file:
|
|
584
|
+
# find the FIRST (leftmost, outermost) occurrence of the marker,
|
|
585
|
+
# then track paren/quote depth from there to find ITS OWN
|
|
586
|
+
# matching close — not just strip the string's own trailing `)`
|
|
587
|
+
# and hope it belongs to this call.
|
|
588
|
+
# Not just the FIRST occurrence, either — `.modulo` also CHAINS
|
|
589
|
+
# (`x.modulo(a).modulo(b)`, the receiver of the OUTER call itself
|
|
590
|
+
# ending in a `.modulo(...)` call), a second real shape the
|
|
591
|
+
# leftmost-occurrence-only version of this fix still mis-parsed:
|
|
592
|
+
# the first `.modulo(`'s own matching close paren lands mid-
|
|
593
|
+
# string (right after `a)`, before the second `.modulo(b)`), so
|
|
594
|
+
# it correctly fails the "reaches the end" check below and must
|
|
595
|
+
# be tried again at the NEXT occurrence rather than giving up.
|
|
596
|
+
# Trying occurrences strictly left to right and taking the FIRST
|
|
597
|
+
# one whose matching close reaches the string's last character
|
|
598
|
+
# handles both shapes with the same rule: for NESTING
|
|
599
|
+
# (`.modulo(x.modulo(y))`), the leftmost (outer) occurrence's own
|
|
600
|
+
# paren-depth tracking already walks straight through the inner
|
|
601
|
+
# call to the true final `)`; for CHAINING, the leftmost
|
|
602
|
+
# occurrence's close lands short and is rejected, so the next
|
|
603
|
+
# occurrence (the true outermost call) is tried instead.
|
|
521
604
|
def match_call(expr, marker)
|
|
522
|
-
|
|
523
|
-
|
|
605
|
+
start = 0
|
|
606
|
+
while (index = expr.index(marker, start))
|
|
607
|
+
close = matching_paren(expr, index + marker.length)
|
|
608
|
+
return [expr[0...index], expr[(index + marker.length)...close]] if close == expr.length - 1
|
|
524
609
|
|
|
525
|
-
|
|
610
|
+
start = index + 1
|
|
611
|
+
end
|
|
612
|
+
nil
|
|
526
613
|
end
|
|
527
614
|
|
|
615
|
+
# `matching_brace` (resolver/block_predicates.rb)'s own twin, one
|
|
616
|
+
# bracket pair over: `start` is the index just past the OPENING
|
|
617
|
+
# `(` already consumed by the caller (depth starts at 1, not 0,
|
|
618
|
+
# for the same reason).
|
|
619
|
+
def matching_paren(expr, start)
|
|
620
|
+
depth = 1
|
|
621
|
+
quote = nil
|
|
622
|
+
index = start
|
|
623
|
+
while index < expr.length
|
|
624
|
+
char = expr[index]
|
|
625
|
+
if quote
|
|
626
|
+
quote = nil if char == quote
|
|
627
|
+
elsif ['"', "'"].include?(char)
|
|
628
|
+
quote = char
|
|
629
|
+
elsif char == "("
|
|
630
|
+
depth += 1
|
|
631
|
+
elsif char == ")"
|
|
632
|
+
depth -= 1
|
|
633
|
+
return index if depth.zero?
|
|
634
|
+
end
|
|
635
|
+
index += 1
|
|
636
|
+
end
|
|
637
|
+
nil
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
# Both operands are coerced to a real Integer/Float BEFORE the
|
|
641
|
+
# zero-check, and the check reads the COERCED divisor — not the
|
|
642
|
+
# raw `divisor_value` (which might not even respond to `.zero?`,
|
|
643
|
+
# a String for instance) and not a `.to_i`-truncated stand-in for
|
|
644
|
+
# it either. The old order checked a truncated `divisor.to_i`
|
|
645
|
+
# AFTER already validating the untruncated value wasn't zero, so
|
|
646
|
+
# a divisor merely small (`0.3`, truncating to `0`) sailed past
|
|
647
|
+
# the guard and then blew up `Integer#%` with a raw
|
|
648
|
+
# `ZeroDivisionError` the moment it reached zero anyway.
|
|
649
|
+
#
|
|
650
|
+
# The modulo itself is plain `%` on the coerced values, matching
|
|
651
|
+
# `add`'s own no-truncation precedent just above — Ruby's native
|
|
652
|
+
# `%` already handles every Integer/Float combination correctly
|
|
653
|
+
# (promoting to Float when either side is one), so rounding both
|
|
654
|
+
# operands down to Integer first was pure data loss with no
|
|
655
|
+
# purpose: `7.5.modulo(2.5)` silently became `7 % 2` (`1`)
|
|
656
|
+
# instead of the real `0.0`.
|
|
528
657
|
def apply_modulo(receiver_value, divisor_value)
|
|
529
|
-
|
|
658
|
+
receiver = require_number(receiver_value, "modulo")
|
|
659
|
+
divisor = require_number(divisor_value, "modulo")
|
|
530
660
|
raise EvaluationError, "divided by 0" if divisor.zero?
|
|
531
661
|
|
|
532
|
-
|
|
662
|
+
receiver % divisor
|
|
533
663
|
end
|
|
534
664
|
|
|
535
665
|
def lookup(expr, state, attrs)
|
|
@@ -544,13 +674,34 @@ module Hecks
|
|
|
544
674
|
# `found_of` (the `Find` node's own path projection) can walk a
|
|
545
675
|
# `.find { ... }`-produced element the identical way `lookup`
|
|
546
676
|
# walks a plain attribute path, rather than duplicating the
|
|
547
|
-
#
|
|
548
|
-
#
|
|
677
|
+
# symbol-or-string key step twice in this file. `key?` decides
|
|
678
|
+
# which spelling answers — a bare `||` between the two would
|
|
679
|
+
# treat a genuinely-held `false` the same as an absent key and
|
|
680
|
+
# fall through to the other spelling, landing on `nil`.
|
|
549
681
|
def walk_path(value, segments)
|
|
550
682
|
segments.reduce(value) do |current, segment|
|
|
551
683
|
break nil unless current.respond_to?(:[])
|
|
552
684
|
|
|
553
|
-
current
|
|
685
|
+
if current.is_a?(Hash)
|
|
686
|
+
sym = segment.to_sym
|
|
687
|
+
current.key?(sym) ? current[sym] : current[segment]
|
|
688
|
+
else
|
|
689
|
+
begin
|
|
690
|
+
current[segment]
|
|
691
|
+
rescue TypeError
|
|
692
|
+
# M9 (docs/audits/2026-08-10-main-bug-audit.md): a dotted
|
|
693
|
+
# path can walk onto an Array (e.g. the result of `.split`,
|
|
694
|
+
# or a `list_of` attribute) — Array#[] demands an
|
|
695
|
+
# Integer/Range and raises a raw TypeError for a String
|
|
696
|
+
# segment ("no implicit conversion of String into
|
|
697
|
+
# Integer"), which used to cross straight past this
|
|
698
|
+
# sublanguage's own refusal boundary and crash the
|
|
699
|
+
# runtime instead of reading as "this predicate doesn't
|
|
700
|
+
# apply here."
|
|
701
|
+
raise EvaluationError,
|
|
702
|
+
"cannot read #{segment.inspect} from #{describe(current)}"
|
|
703
|
+
end
|
|
704
|
+
end
|
|
554
705
|
end
|
|
555
706
|
end
|
|
556
707
|
|
|
@@ -3,7 +3,7 @@ require_relative "../ir"
|
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Bluebook
|
|
6
|
-
Port = Struct.new(:name, :verb, :signal, keyword_init: true) do
|
|
6
|
+
Port = Struct.new(:name, :verb, :signal, :answers, keyword_init: true) do
|
|
7
7
|
def reply? = signal == :reply
|
|
8
8
|
def effect? = signal == :effect
|
|
9
9
|
end
|
|
@@ -25,10 +25,23 @@ module Hecks
|
|
|
25
25
|
class Judge
|
|
26
26
|
include Readings
|
|
27
27
|
|
|
28
|
-
# Children offered BEFORE the parent's own lists
|
|
29
|
-
# offered as the id of the thing it names, so
|
|
30
|
-
#
|
|
31
|
-
|
|
28
|
+
# Children offered BEFORE the parent's own lists, IN THIS ORDER. An
|
|
29
|
+
# attribute's type is offered as the id of the thing it names, so the
|
|
30
|
+
# value objects have to exist before anything that can name one — an
|
|
31
|
+
# aggregate's own attributes, AND an entity's own (M13: an entity is its
|
|
32
|
+
# own root, repeating the aggregate's whole shape one level down, so its
|
|
33
|
+
# attributes resolve against the SAME value-object pool). ValueObject
|
|
34
|
+
# first, Entity second, so an entity's own attributes are never offered
|
|
35
|
+
# before the value objects they may reference exist — a self-hosting
|
|
36
|
+
# casualty found live: the meta-grammar's own Handler/Dispatch/Member/
|
|
37
|
+
# Keyword/Argument entities (S17, ADR 0026) failed reference resolution
|
|
38
|
+
# on their own plain value-object-typed attributes (`HandlerText`,
|
|
39
|
+
# `MemberPosition`, ...) the moment entity attributes started being
|
|
40
|
+
# checked at all, because `@plan.names`' own (incidental) declaration
|
|
41
|
+
# order happened to walk Entity first. The order is stated here, not
|
|
42
|
+
# left to whatever order the plan's own category table iterates in —
|
|
43
|
+
# see `detail_node`'s own use of this constant, below.
|
|
44
|
+
EAGER_CHILDREN = { "Aggregate" => %w[ValueObject Entity] }.freeze
|
|
32
45
|
|
|
33
46
|
# Categories an ENTITY declares as well as an aggregate. The IR reuses
|
|
34
47
|
# Command and Query for a piece's own commands and queries, so the
|
|
@@ -157,13 +170,32 @@ module Hecks
|
|
|
157
170
|
identity = extra.merge(node_identity(plan, category, node, index, parent_id))
|
|
158
171
|
receiver ||= { aggregate: id, entities: [] }
|
|
159
172
|
eager, later = children_of(category).partition { |child| eager?(category, child) }
|
|
173
|
+
# ORDERED AS `EAGER_CHILDREN` DECLARES, not as `children_of` happens to
|
|
174
|
+
# list them — `children_of` reads `@plan.names`, whose own order is an
|
|
175
|
+
# accident of which .bluebook file registered which category first,
|
|
176
|
+
# never a promise about which of two eager children exists before the
|
|
177
|
+
# other. `EAGER_CHILDREN`'s own array IS that promise (ValueObject
|
|
178
|
+
# before Entity), so the walk keeps only what this parent actually
|
|
179
|
+
# has, in the order the constant states — see that constant's own
|
|
180
|
+
# comment for the bug this exact reordering fixes.
|
|
181
|
+
eager = Array(EAGER_CHILDREN[category]) & eager
|
|
160
182
|
|
|
161
183
|
eager.each { |child| walk_all(child, node, id, entity_child_extra(child, identity), receiver: receiver) }
|
|
162
184
|
setters(plan, category, node, receiver)
|
|
163
|
-
appends
|
|
185
|
+
# BEFORE `appends`, not after — the same reason `EAGER_CHILDREN`
|
|
186
|
+
# walks an aggregate's OWN entities before its OWN attributes
|
|
187
|
+
# (M13): a piece nested inside a piece (Handler's own
|
|
188
|
+
# `dispatches, list_of(Dispatch)` — S17, ADR 0026) must exist
|
|
189
|
+
# before this piece's own attribute list can reference it as a
|
|
190
|
+
# HELD entity, the same way `Account#ledger` needs Account's own
|
|
191
|
+
# entities walked eagerly. `nest_entities` is a no-op for every
|
|
192
|
+
# category but "Entity" (its own early return), so reordering it
|
|
193
|
+
# ahead of `appends` costs nothing for anything else that walks
|
|
194
|
+
# through here.
|
|
195
|
+
nest_entities(category, node, id, parent_id)
|
|
196
|
+
appends(plan, category, node, receiver, parent_id)
|
|
164
197
|
later.each { |child| walk_all(child, node, id, entity_child_extra(child, identity), receiver: receiver) }
|
|
165
198
|
within_entity(category, node, id, parent_id)
|
|
166
|
-
nest_entities(category, node, id, parent_id)
|
|
167
199
|
sealers(plan, category, receiver)
|
|
168
200
|
end
|
|
169
201
|
|
|
@@ -353,8 +385,9 @@ module Hecks
|
|
|
353
385
|
end
|
|
354
386
|
end
|
|
355
387
|
|
|
356
|
-
def appends(plan, category, node, receiver)
|
|
388
|
+
def appends(plan, category, node, receiver, parent_id)
|
|
357
389
|
id = receiver[:entities].last || receiver[:aggregate]
|
|
390
|
+
owner_id = owning_aggregate_ref(category, id, parent_id)
|
|
358
391
|
plan.appends.each do |list_name, append|
|
|
359
392
|
rows_for(category, list_name, node).each_with_index do |row, index|
|
|
360
393
|
chosen = append_for(category, list_name, append, row, node)
|
|
@@ -367,7 +400,7 @@ module Hecks
|
|
|
367
400
|
v(index)
|
|
368
401
|
else
|
|
369
402
|
carried(@plan.category(category), chosen.verb, argument,
|
|
370
|
-
cell(category, list_name, row, field, id, chosen))
|
|
403
|
+
cell(category, list_name, row, field, id, chosen, owner_id))
|
|
371
404
|
end
|
|
372
405
|
[argument.to_sym, value]
|
|
373
406
|
end
|
|
@@ -378,21 +411,41 @@ module Hecks
|
|
|
378
411
|
end
|
|
379
412
|
end
|
|
380
413
|
|
|
414
|
+
# WHICH AGGREGATE OWNS THE VALUE OBJECTS an attribute's TYPE may
|
|
415
|
+
# resolve against. An aggregate owns its own — `id` already names
|
|
416
|
+
# it. An entity never has value objects of its own (Entity
|
|
417
|
+
# deliberately never answers `value_object` — see entity.rb's own
|
|
418
|
+
# comment on why); its attributes read the SAME pool its
|
|
419
|
+
# enclosing aggregate declares, one level up the construct tree
|
|
420
|
+
# no matter how many entities deep this attribute is nested —
|
|
421
|
+
# `parent_id` names it because `detail_node`/`nest_entities`
|
|
422
|
+
# thread the ROOT aggregate's id down as `parent_id` at every
|
|
423
|
+
# entity level, never the direct (possibly entity) parent.
|
|
424
|
+
def owning_aggregate_ref(category, id, parent_id)
|
|
425
|
+
category == "Entity" ? parent_id : id
|
|
426
|
+
end
|
|
427
|
+
|
|
381
428
|
def sealers(plan, category, receiver)
|
|
382
429
|
id = receiver[:entities].last || receiver[:aggregate]
|
|
383
430
|
plan.sealers.each { |verb| send_to("Bluebook::#{verb_for(plan, verb)}", id, to: receiver) }
|
|
384
431
|
end
|
|
385
432
|
|
|
386
|
-
# An aggregate's attribute names its value object by TYPE,
|
|
387
|
-
# models that as a reference — so the type is offered as
|
|
388
|
-
# own id. This is the rule "attributes must use
|
|
389
|
-
# enforced by reference resolution rather than by a
|
|
433
|
+
# An aggregate's or an entity's attribute names its value object by TYPE,
|
|
434
|
+
# and the language models that as a reference — so the type is offered as
|
|
435
|
+
# the value object's own id. This is the rule "attributes must use
|
|
436
|
+
# value-object types", enforced by reference resolution rather than by a
|
|
437
|
+
# predicate — for a HEAD's own attributes, aggregate or entity alike: an
|
|
438
|
+
# entity is its own root, repeating the aggregate's whole shape one level
|
|
439
|
+
# down (entity.rb's own words), and an undeclared type on an entity's
|
|
440
|
+
# attribute must fail the same reference resolution an aggregate's own
|
|
441
|
+
# does, not go unchecked because only "Aggregate.attributes" was ever
|
|
442
|
+
# asked.
|
|
390
443
|
# An attribute's type is offered as the ID OF THE THING IT NAMES, so the
|
|
391
444
|
# language resolves it as a reference and "the type is declared" costs no
|
|
392
445
|
# predicate. Three kinds, three ids: a value object and an entity both hang
|
|
393
446
|
# off this aggregate, so they share its prefix; another aggregate's head
|
|
394
447
|
# hangs off the chapter.
|
|
395
|
-
def cell(category, list_name, row, field, id, append)
|
|
448
|
+
def cell(category, list_name, row, field, id, append, aggregate_id)
|
|
396
449
|
value = row_value(row, field)
|
|
397
450
|
# A default keeps its TYPE by being written as a literal — 0.0 rather than
|
|
398
451
|
# "0.0" — because the language holds it as text and text alone forgets.
|
|
@@ -400,13 +453,22 @@ module Hecks
|
|
|
400
453
|
return value unless field == :type
|
|
401
454
|
# A REFERENCE names another head WHEREVER it is written — on a head, on
|
|
402
455
|
# a command, on a piece, on an ask — so it is offered as that head's
|
|
403
|
-
# id in all four. Only
|
|
456
|
+
# id in all four. Only a HEAD's own attributes additionally qualify
|
|
404
457
|
# an ordinary type into a value object's id ; a command argument's
|
|
405
458
|
# type is text and stays text.
|
|
406
459
|
return points_at(row, id) if append.verb == "Reference"
|
|
407
|
-
return value unless
|
|
460
|
+
return value unless attribute_list?(category, list_name)
|
|
461
|
+
|
|
462
|
+
Naming.identity([owning_aggregate_id(aggregate_id, value), value])
|
|
463
|
+
end
|
|
408
464
|
|
|
409
|
-
|
|
465
|
+
# A HEAD'S OWN ATTRIBUTES — an aggregate's, or an entity's (its own root,
|
|
466
|
+
# one level down). Every other "attributes" list belongs to something that
|
|
467
|
+
# is not a head at all (a command's arguments, a value object's own
|
|
468
|
+
# fields), and a type written there is a name, not a reference — the same
|
|
469
|
+
# distinction `cell`'s own comment draws.
|
|
470
|
+
def attribute_list?(category, list_name)
|
|
471
|
+
list_name.to_s == "attributes" && %w[Aggregate Entity].include?(category)
|
|
410
472
|
end
|
|
411
473
|
|
|
412
474
|
# `id` NAMES THE ATTRIBUTE'S OWN AGGREGATE, not necessarily the
|
|
@@ -40,6 +40,10 @@ module Hecks
|
|
|
40
40
|
def judge!
|
|
41
41
|
send_to("Port::Port.Declare", @port.name, name: v(@port.name),
|
|
42
42
|
verb: v(@port.verb), signal: v(@port.signal))
|
|
43
|
+
|
|
44
|
+
Array(@port.answers).each do |answer|
|
|
45
|
+
send_to("Port::Port.AddAnswer", @port.name, name: @port.name, value: v(answer))
|
|
46
|
+
end
|
|
43
47
|
end
|
|
44
48
|
end
|
|
45
49
|
end
|