hecks 0.3.0 → 1.0.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/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/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/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 +91 -3
- 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/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/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 +132 -5
- data/lib/hecks/bluebook/pattern_subset.rb +66 -2
- data/lib/hecks/bluebook/process_manager.rb +53 -11
- data/lib/hecks/bluebook/value_object.rb +9 -1
- 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/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/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 +97 -13
- data/lib/hecks/runtime/command_rules/admissibility.rb +64 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +7 -1
- 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 +14 -0
- data/lib/hecks/runtime/dispatcher.rb +19 -4
- data/lib/hecks/runtime/entity_interpreter.rb +85 -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 +76 -1
- 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 +7 -2
- 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 +48 -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
|
@@ -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
|
|
@@ -53,6 +53,14 @@ module Hecks
|
|
|
53
53
|
# argument became indistinguishable from one carrying a literal string.
|
|
54
54
|
def with_spec_rows(node) = pair_rows(node.to_h[:with_spec])
|
|
55
55
|
|
|
56
|
+
# THE SAME READ, ONE LEVEL IN — `compensates` folds `DispatchSpec`
|
|
57
|
+
# into the language's own `compensates_command_name`/
|
|
58
|
+
# `compensates_with_spec` (`Assembly::Contracts`' own comment on
|
|
59
|
+
# "Dispatch"), so its OWN with_spec pairs live nested one hash
|
|
60
|
+
# down from where `with_spec_rows` looks. `&.dig(...)` — no
|
|
61
|
+
# compensation at all is not an error, it is `pair_rows(nil)`, empty.
|
|
62
|
+
def compensates_with_spec_rows(node) = pair_rows(node.to_h[:compensates]&.dig(:with_spec))
|
|
63
|
+
|
|
56
64
|
# A read model carries the same options an ask does, plus its filters — see
|
|
57
65
|
# option_rows.
|
|
58
66
|
def read_model_option_rows(node) = option_rows(node, filters: true)
|
|
@@ -155,10 +163,12 @@ module Hecks
|
|
|
155
163
|
# was being handed a blank and could never refuse.
|
|
156
164
|
def mutation_rows(node)
|
|
157
165
|
Array(node.mutations).flat_map do |mutation|
|
|
158
|
-
# `:delegate` (CommandBuilder#delegates_to's
|
|
159
|
-
# the SAME multi-binding
|
|
160
|
-
#
|
|
161
|
-
|
|
166
|
+
# `:delegate`/`:corrects` (CommandBuilder#delegates_to's and
|
|
167
|
+
# #corrects_impl's own comments) ride the SAME multi-binding
|
|
168
|
+
# shape `:append` does — `with: {...}`/the assembled
|
|
169
|
+
# `as:`/`reason:`/`reverses:` hash is a field map, same as
|
|
170
|
+
# append's own `fields:`.
|
|
171
|
+
next set_row(mutation) unless [:append, :delegate, :corrects].include?(mutation.op)
|
|
162
172
|
|
|
163
173
|
mutation.source.map do |field, argument|
|
|
164
174
|
# Spelled the way Mutation#appended_fields spells it, because
|
|
@@ -207,9 +207,22 @@ module Hecks
|
|
|
207
207
|
# at a time.
|
|
208
208
|
def members_row(row) = members_of(row)
|
|
209
209
|
|
|
210
|
+
# THE KEY IS STRINGIFIED, NEVER THE VALUE — the same split
|
|
211
|
+
# `Bluebook::ValueObject#to_h`'s own `members:` emission makes
|
|
212
|
+
# (lib/hecks/bluebook/value_object.rb, L7 docs/audits/
|
|
213
|
+
# 2026-08-11-bug-triage.md). `Pair.value` is declared `String`
|
|
214
|
+
# (shape.bluebook), but a real pair can hold whatever native Ruby
|
|
215
|
+
# type the source member line actually wrote (`StatementFrequency`'s
|
|
216
|
+
# own `retention_months: 84`, examples/banking/bluebook/
|
|
217
|
+
# statements.bluebook) — `text` already unwraps the stored Value to
|
|
218
|
+
# that real type, so calling `.to_s` here erased it a second time,
|
|
219
|
+
# on the side spec/round_trip_spec.rb compares straight against a
|
|
220
|
+
# fresh raw load (`Reconstruction.of` directly, no Assembly in
|
|
221
|
+
# between): `declared 84, read back "84"` the moment `to_h` stopped
|
|
222
|
+
# erasing it on the OTHER side and this one kept erasing it alone.
|
|
210
223
|
def members_of(value_object_row)
|
|
211
224
|
Array(value_object_row[:members]).map do |member|
|
|
212
|
-
Array(member[:pairs]).map { |pair| [text(pair[:key]).to_s, text(pair[:value])
|
|
225
|
+
Array(member[:pairs]).map { |pair| [text(pair[:key]).to_s, text(pair[:value])] }
|
|
213
226
|
end
|
|
214
227
|
end
|
|
215
228
|
|
|
@@ -238,7 +251,12 @@ module Hecks
|
|
|
238
251
|
# Symbol there — the IR was not uniform about it, and only a round trip
|
|
239
252
|
# ever said so.
|
|
240
253
|
identified_by: identity_paths(row),
|
|
241
|
-
|
|
254
|
+
# `text(row[:aggregate])` — the OWNING aggregate, the same one
|
|
255
|
+
# `Judge#owning_aggregate_ref` resolved this piece's own
|
|
256
|
+
# attribute types against on the way in, so reconstruction reads
|
|
257
|
+
# a value-object-typed attribute back the identical way an
|
|
258
|
+
# aggregate's own is (`Shapes#shape_field`'s own comment).
|
|
259
|
+
attributes: Array(row[:attributes]).map { |field| shape_field(field, text(row[:aggregate])) },
|
|
242
260
|
# ADR 0028 — the SAME shape `aggregate(row)`'s own
|
|
243
261
|
# `preconditions:` reads two hand-typed methods up, read by
|
|
244
262
|
# hand for the identical reason: `entity(row)` is hand-typed
|
|
@@ -301,7 +319,28 @@ module Hecks
|
|
|
301
319
|
dispatches: Array(row[:dispatches]).map { |leg| dispatch(leg) })
|
|
302
320
|
end
|
|
303
321
|
|
|
304
|
-
|
|
322
|
+
# `compensates` — TWO FLAT FIELDS on this same row
|
|
323
|
+
# (`process_manager.bluebook`'s own comment on `Dispatch` for
|
|
324
|
+
# why), assembled BY HAND into the shape its own field actually
|
|
325
|
+
# is — `declaration()`'s generic per-field hash-build has no
|
|
326
|
+
# way to turn two cells into a second object, so this reads
|
|
327
|
+
# them directly and passes the result through `extra:`, the
|
|
328
|
+
# same seam `handler`/`process_manager` already use for a
|
|
329
|
+
# `:children` shape a flat Contract cannot describe.
|
|
330
|
+
# `compensates_command_name` absent means no compensation at
|
|
331
|
+
# all — a plain dispatch with nothing to undo. A PLAIN HASH, the
|
|
332
|
+
# SAME declaration shape `to_h` spells for everything else in
|
|
333
|
+
# this file (this file's own top comment) — never a real
|
|
334
|
+
# `DispatchSpec` here; `Assembly#dispatch` is the one place a
|
|
335
|
+
# declaration hash becomes the real object, and building it
|
|
336
|
+
# twice, in two different shapes, is exactly the kind of drift
|
|
337
|
+
# this whole arc exists to remove.
|
|
338
|
+
def dispatch(row)
|
|
339
|
+
name = text(row[:compensates_command_name])
|
|
340
|
+
compensates = name && { command_name: name, with_spec: pairs(row[:compensates_with_spec]) }
|
|
341
|
+
|
|
342
|
+
declaration("Dispatch", row, compensates: compensates)
|
|
343
|
+
end
|
|
305
344
|
|
|
306
345
|
def read_model(row)
|
|
307
346
|
declaration("ReadModel", row).merge(query_name: text(row[:query_name])).merge(options_of(row))
|
|
@@ -52,14 +52,33 @@ module Hecks
|
|
|
52
52
|
def identity_path(part) = text(part[:value]).to_s
|
|
53
53
|
|
|
54
54
|
# An argument, a parameter, a piece's attribute — the three places an
|
|
55
|
-
# attribute is written that carry no owner id to strip
|
|
55
|
+
# attribute is written that carry no owner id to strip... except a
|
|
56
|
+
# piece's own attribute now does : an entity is its own root (repeating
|
|
57
|
+
# the aggregate's whole shape one level down), so ITS attributes name a
|
|
58
|
+
# value object exactly the way an aggregate's own do (`Judge#cell`'s own
|
|
59
|
+
# comment), and reconstruction has to undo the same owned-vs-reference
|
|
60
|
+
# split `attribute`, below, already undoes for an aggregate — not the
|
|
61
|
+
# cruder "any qualified name is a Reference" guess this used before,
|
|
62
|
+
# which mistook a piece's own value-object type for a cross-aggregate
|
|
63
|
+
# head every time.
|
|
56
64
|
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
|
|
65
|
+
# `aggregate_id`, passed only for a piece's own attribute (an
|
|
66
|
+
# argument or a parameter carries no value-object type at all — see
|
|
67
|
+
# entity.bluebook's/command.bluebook's own comments on why — so the
|
|
68
|
+
# generic reader path calls this with one argument and gets the
|
|
69
|
+
# OLD reading unchanged), is the piece's OWNING aggregate — the same
|
|
70
|
+
# one its own attribute types were resolved against on the way in
|
|
71
|
+
# (`Judge#owning_aggregate_ref`).
|
|
72
|
+
#
|
|
73
|
+
# A REFERENCE among the other two came in as the head's ID, because
|
|
74
|
+
# that is what Command/Query.Reference offer, so it goes back out as
|
|
75
|
+
# the encoding the IR spells. A qualified name is the tell : an
|
|
76
|
+
# ordinary type names something declared beside it (`Money`,
|
|
77
|
+
# `AccountNumber`) and never carries a chapter, while a head's id
|
|
78
|
+
# always does.
|
|
79
|
+
def shape_field(field, aggregate_id = nil)
|
|
80
|
+
return attribute(field, aggregate_id) if aggregate_id
|
|
81
|
+
|
|
63
82
|
type = text(field[:type]).to_s
|
|
64
83
|
|
|
65
84
|
{
|
|
@@ -221,9 +240,10 @@ module Hecks
|
|
|
221
240
|
# language does not hold is a named gap, not a byte-for-byte one).
|
|
222
241
|
def mutation(target, op, bindings)
|
|
223
242
|
base = { target: target.to_sym, op: op.to_sym, sign: Hecks::Bluebook::Mutation.sign_for(op) }
|
|
224
|
-
# `:delegate` (CommandBuilder#delegates_to's
|
|
225
|
-
# the SAME multi-binding
|
|
226
|
-
|
|
243
|
+
# `:delegate`/`:corrects` (CommandBuilder#delegates_to's and
|
|
244
|
+
# #corrects_impl's own comments) ride the SAME multi-binding
|
|
245
|
+
# shape `:append` does.
|
|
246
|
+
return base.merge(fields: appended(bindings)) if ["append", "delegate", "corrects"].include?(op)
|
|
227
247
|
|
|
228
248
|
base.merge(source: classified(bindings.first))
|
|
229
249
|
end
|