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
|
@@ -30,17 +30,27 @@ module Hecks
|
|
|
30
30
|
IR_VERSION = 1
|
|
31
31
|
|
|
32
32
|
emits_ir(
|
|
33
|
-
ir_version:
|
|
34
|
-
name:
|
|
35
|
-
version:
|
|
36
|
-
vision:
|
|
37
|
-
classification:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
ir_version: -> { IR_VERSION },
|
|
34
|
+
name: :name,
|
|
35
|
+
version: :version,
|
|
36
|
+
vision: :vision,
|
|
37
|
+
classification: :classification,
|
|
38
|
+
# M10 — a domain rename (`formerly_known_as "OldName"`) drives a
|
|
39
|
+
# real Postgres schema rename at boot (EraResolver reads
|
|
40
|
+
# `bluebook.formerly_known_as` off the live Ruby object) and the
|
|
41
|
+
# meta-validator's cache key is `SHA256(JSON(bluebook.to_h))` — so
|
|
42
|
+
# a fact this method didn't spell was a fact two chapters
|
|
43
|
+
# differing ONLY by their old name could hash identically on,
|
|
44
|
+
# same as read-model filters before them. Spelled here for the
|
|
45
|
+
# same reason `version`/`vision` are: a plain field, present
|
|
46
|
+
# (possibly nil) rather than silently absent.
|
|
47
|
+
formerly_known_as: :formerly_known_as,
|
|
48
|
+
aggregates: many(:aggregates),
|
|
49
|
+
read_models: many(:read_models),
|
|
50
|
+
policies: many(:policies),
|
|
51
|
+
process_managers: many(:process_managers),
|
|
52
|
+
attaches_to: :attaches_to,
|
|
53
|
+
canonical_form: -> { Expression::CanonicalForm.table }
|
|
44
54
|
)
|
|
45
55
|
|
|
46
56
|
attr_reader :name, :version, :vision, :aggregates, :policies, :process_managers,
|
|
@@ -39,7 +39,7 @@ module Hecks
|
|
|
39
39
|
# the tail, which is why a construct with a variable shape needs no
|
|
40
40
|
# new mixin API.
|
|
41
41
|
def to_h
|
|
42
|
-
return super.merge(fields: appended_fields) if [:append, :delegate].include?(op)
|
|
42
|
+
return super.merge(fields: appended_fields) if [:append, :delegate, :corrects].include?(op)
|
|
43
43
|
|
|
44
44
|
super.merge(source: classified_source)
|
|
45
45
|
end
|
|
@@ -10,7 +10,8 @@ module Hecks
|
|
|
10
10
|
include RuleReference
|
|
11
11
|
include WordGate
|
|
12
12
|
|
|
13
|
-
def initialize(name, chapter_named_givens: {}, chapter_pending_givens: []
|
|
13
|
+
def initialize(name, chapter_named_givens: {}, chapter_pending_givens: [],
|
|
14
|
+
chapter_entity_named_givens: {}, chapter_entity_pending_givens: [])
|
|
14
15
|
@name = name
|
|
15
16
|
@value_objects = []
|
|
16
17
|
@commands = []
|
|
@@ -37,6 +38,15 @@ module Hecks
|
|
|
37
38
|
# here and `BluebookBuilder#resolve_pending_chapter_givens!`
|
|
38
39
|
# for where it drains.
|
|
39
40
|
@chapter_pending_givens = chapter_pending_givens
|
|
41
|
+
# ONE LEVEL WIDER STILL, PAST THE CHAPTER'S OWN AGGREGATE-LEVEL
|
|
42
|
+
# POOL — the chapter's own entity-scoped pool, threaded from
|
|
43
|
+
# `BluebookBuilder#aggregate_impl` the same way
|
|
44
|
+
# `@chapter_named_givens` is, and passed straight through
|
|
45
|
+
# (unchanged) to every top-level piece this aggregate builds
|
|
46
|
+
# (`#drain_pending!`). See `EntityBuilder#given_impl`'s own
|
|
47
|
+
# comment for what this closes.
|
|
48
|
+
@chapter_entity_named_givens = chapter_entity_named_givens
|
|
49
|
+
@chapter_entity_pending_givens = chapter_entity_pending_givens
|
|
40
50
|
# DEFERRED CONSTRUCTION — `entity`/`command`/`query` push a
|
|
41
51
|
# pending descriptor here instead of building immediately; see
|
|
42
52
|
# `#drain_pending!`'s own comment for why.
|
|
@@ -419,6 +429,7 @@ module Hecks
|
|
|
419
429
|
seal_defaults
|
|
420
430
|
seal_lifecycle_guards
|
|
421
431
|
seal_projected_fields
|
|
432
|
+
seal_correction_targets
|
|
422
433
|
|
|
423
434
|
ir = Aggregate.new(
|
|
424
435
|
name: @name,
|
|
@@ -445,8 +456,11 @@ module Hecks
|
|
|
445
456
|
ir
|
|
446
457
|
end
|
|
447
458
|
|
|
448
|
-
def self.build(name, chapter_named_givens: {}, chapter_pending_givens: [],
|
|
449
|
-
|
|
459
|
+
def self.build(name, chapter_named_givens: {}, chapter_pending_givens: [],
|
|
460
|
+
chapter_entity_named_givens: {}, chapter_entity_pending_givens: [], &block)
|
|
461
|
+
builder = new(name, chapter_named_givens: chapter_named_givens, chapter_pending_givens: chapter_pending_givens,
|
|
462
|
+
chapter_entity_named_givens: chapter_entity_named_givens,
|
|
463
|
+
chapter_entity_pending_givens: chapter_entity_pending_givens)
|
|
450
464
|
builder.instance_eval(&block) if block
|
|
451
465
|
builder.build
|
|
452
466
|
end
|
|
@@ -496,6 +510,9 @@ module Hecks
|
|
|
496
510
|
owner_named_givens: @entity_named_givens,
|
|
497
511
|
identity_name_prefix: "#{Naming.demodulise(@name)}#{Naming.demodulise(name)}",
|
|
498
512
|
identity_value_object_installer: ->(value_object) { @value_objects << value_object },
|
|
513
|
+
aggregate_name: @name,
|
|
514
|
+
chapter_entity_named_givens: @chapter_entity_named_givens,
|
|
515
|
+
chapter_entity_pending_givens: @chapter_entity_pending_givens,
|
|
499
516
|
&block)
|
|
500
517
|
end
|
|
501
518
|
|
|
@@ -609,7 +626,17 @@ module Hecks
|
|
|
609
626
|
# keys: a default that is a Hash is left to `Value.for_attribute`, which
|
|
610
627
|
# is where a wrong FIELD belongs.
|
|
611
628
|
def seal_defaults
|
|
612
|
-
|
|
629
|
+
# `closed_sets` TOO, not only `@value_objects` — the exact gap
|
|
630
|
+
# this method's own comment names: an inline `one_of(...)`
|
|
631
|
+
# synthesises its value object through `closed_sets`
|
|
632
|
+
# (AttributeCollector#synthesise_closed_set), never installed
|
|
633
|
+
# into `@value_objects` until `#build` merges them (see
|
|
634
|
+
# `#build`'s own `@value_objects + closed_sets`, and
|
|
635
|
+
# `declared_value_object`'s identical merge). Checking
|
|
636
|
+
# `@value_objects` alone made this exact attribute — a bare
|
|
637
|
+
# default on an inline closed set — invisible to the one
|
|
638
|
+
# check meant to catch it.
|
|
639
|
+
shapes = (@value_objects + closed_sets).map { |shape| shape.hecks_name.to_s }
|
|
613
640
|
|
|
614
641
|
attributes.each do |attribute|
|
|
615
642
|
next if attribute.default.nil? || attribute.default.is_a?(Hash)
|
|
@@ -683,7 +710,10 @@ module Hecks
|
|
|
683
710
|
# #step_delegate_to_entity`, which refuses a real one that
|
|
684
711
|
# names no such entity or command). Sealing THIS check
|
|
685
712
|
# against it would refuse every delegating command outright.
|
|
686
|
-
|
|
713
|
+
# `:corrects` — CommandBuilder#corrects_impl's own comment —
|
|
714
|
+
# targets an EVENT name, not a field either; checked instead
|
|
715
|
+
# by `seal_correction_targets`, below.
|
|
716
|
+
next if [:delegate, :corrects].include?(mutation.op)
|
|
687
717
|
next if known.include?(mutation.target.to_sym)
|
|
688
718
|
|
|
689
719
|
raise Malformed,
|
|
@@ -694,6 +724,88 @@ module Hecks
|
|
|
694
724
|
end
|
|
695
725
|
end
|
|
696
726
|
|
|
727
|
+
# `corrects` — CommandBuilder#corrects_impl's own comment. Runs
|
|
728
|
+
# once every command in the aggregate is known (the same reason
|
|
729
|
+
# this is a `seal_*` step rather than living in `corrects_impl`
|
|
730
|
+
# itself — a command cannot see its own siblings' `emits` while
|
|
731
|
+
# it is still being built). Two things are checked:
|
|
732
|
+
#
|
|
733
|
+
# 1. The named event must be something a SIBLING command here
|
|
734
|
+
# actually `emits` — naming an event nothing in this aggregate
|
|
735
|
+
# ever announces is a build-time authoring error. (Whether
|
|
736
|
+
# THIS record has actually emitted it YET is the dispatch-time
|
|
737
|
+
# half — CommandRules::Admissibility#enforce_correction_target.)
|
|
738
|
+
#
|
|
739
|
+
# 2. `reverses: true` derives the corrective `sets` from the
|
|
740
|
+
# ORIGINAL command's own mutations, rather than the author
|
|
741
|
+
# writing them — but only when every one of those mutations is
|
|
742
|
+
# STRUCTURALLY invertible with no runtime data: increment/
|
|
743
|
+
# decrement, same argument, opposite verb (`sign_for`'s own
|
|
744
|
+
# +1/-1 pair — CommandRules::Arithmetic applies `current +
|
|
745
|
+
# sign * amount`, so the SAME source with the OPPOSITE sign
|
|
746
|
+
# undoes it exactly). Nothing else qualifies today: `set` has
|
|
747
|
+
# no such rule at all — inverting it needs the SPECIFIC prior
|
|
748
|
+
# value at the moment the original fired, which is per-
|
|
749
|
+
# instance runtime data no build-time derivation can have;
|
|
750
|
+
# `multiply`/`clamp` are lossy by design (a clamped value's
|
|
751
|
+
# own pre-clamp magnitude is not recoverable from the mutation
|
|
752
|
+
# at all); `append`/`remove` LOOK symmetric but are not
|
|
753
|
+
# reliably so — `append`'s source is a per-field binding hash
|
|
754
|
+
# (`append: { name: :name, amount: :amount }`), `remove`'s is
|
|
755
|
+
# a single resolved value to match by equality
|
|
756
|
+
# (MutationApplier#removed), and collapsing one shape into the
|
|
757
|
+
# other correctly needs the target list's own value-object
|
|
758
|
+
# field names, not just the mutation's own recorded shape — a
|
|
759
|
+
# real gap, left for a follow-on round rather than guessed at
|
|
760
|
+
# here. Refuses rather than silently deriving something wrong
|
|
761
|
+
# — see docs/decisions/ for the ADR that draws this exact
|
|
762
|
+
# line.
|
|
763
|
+
def seal_correction_targets
|
|
764
|
+
inverse_op = { increment: :decrement, decrement: :increment }
|
|
765
|
+
emitted_by = Hash.new { |hash, key| hash[key] = [] }
|
|
766
|
+
@commands.each { |command| command.emits.each { |event_name| emitted_by[event_name] << command } }
|
|
767
|
+
|
|
768
|
+
@commands.each do |command|
|
|
769
|
+
correction = command.mutations.find { |mutation| mutation.op == :corrects }
|
|
770
|
+
next unless correction
|
|
771
|
+
|
|
772
|
+
event = correction.target
|
|
773
|
+
sources = emitted_by[event]
|
|
774
|
+
if sources.empty?
|
|
775
|
+
raise Malformed,
|
|
776
|
+
"#{@name}.#{command.hecks_name} corrects #{event.inspect}, but nothing " \
|
|
777
|
+
"declared on #{@name} ever emits it — corrects names a fact this " \
|
|
778
|
+
"aggregate actually announces, not an aspiration"
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
next unless correction.source[:reverses]
|
|
782
|
+
|
|
783
|
+
own_mutations = command.mutations.reject { |mutation| mutation.op == :corrects }
|
|
784
|
+
if own_mutations.any?
|
|
785
|
+
raise Malformed,
|
|
786
|
+
"#{@name}.#{command.hecks_name} declares both corrects #{event.inspect}, " \
|
|
787
|
+
"reverses: true AND its own sets — reverses: true means the correction " \
|
|
788
|
+
"is DERIVED; write one or the other, never both"
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
derived = sources.flat_map(&:mutations).reject { |mutation| mutation.op == :corrects }
|
|
792
|
+
unsupported = derived.reject { |mutation| inverse_op.key?(mutation.op) }
|
|
793
|
+
if unsupported.any?
|
|
794
|
+
raise Malformed,
|
|
795
|
+
"#{@name}.#{command.hecks_name} corrects #{event.inspect}, reverses: " \
|
|
796
|
+
"true, but the command(s) that emit it use " \
|
|
797
|
+
"#{unsupported.map(&:op).uniq.join(', ')} — not statically invertible " \
|
|
798
|
+
"(set needs the specific prior value, multiply/clamp are lossy) — " \
|
|
799
|
+
"declare the corrective sets by hand instead"
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
derived.each do |mutation|
|
|
803
|
+
command.mutations << Mutation.new(target: mutation.target, op: inverse_op.fetch(mutation.op),
|
|
804
|
+
source: mutation.source)
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
|
|
697
809
|
# A query must ask about a field the aggregate actually HAS — the same
|
|
698
810
|
# seal `then_set` gets, closing the same silence: a where over a field
|
|
699
811
|
# nothing declares matches nothing and refuses nothing, forever, on
|
|
@@ -62,6 +62,8 @@ module Hecks
|
|
|
62
62
|
admits: nil, one_of: nil)
|
|
63
63
|
# moved to the language: FieldName invariant, on Root.Attribute
|
|
64
64
|
|
|
65
|
+
refuse_duplicate_attribute!(name)
|
|
66
|
+
|
|
65
67
|
if type.equal?(UNSET)
|
|
66
68
|
raise Malformed, "#{name} declares no type — attribute :#{name}, SomeType is required, " \
|
|
67
69
|
"there is no default"
|
|
@@ -156,6 +158,7 @@ module Hecks
|
|
|
156
158
|
private
|
|
157
159
|
|
|
158
160
|
def relationship_attribute(target, kind, name, optional: false, list: false)
|
|
161
|
+
refuse_duplicate_attribute!(name)
|
|
159
162
|
attributes << Attribute.new(
|
|
160
163
|
name: name,
|
|
161
164
|
type: Reference.new(target),
|
|
@@ -183,6 +186,24 @@ module Hecks
|
|
|
183
186
|
"with the type-position one_of(...) instead"
|
|
184
187
|
end
|
|
185
188
|
|
|
189
|
+
# A NAME DECLARED TWICE ON THE SAME OWNER IS TWO ATTRIBUTES SHARING
|
|
190
|
+
# ONE NAME, and nothing downstream disambiguates them — every
|
|
191
|
+
# reader that walks `attributes` looking for one by name
|
|
192
|
+
# (`seal_mutation_targets`, `seal_query_field`, `projects`'s own
|
|
193
|
+
# local check, `Instance#[]`, ...) uses `Array#find`/`any?`, which
|
|
194
|
+
# silently answers whichever declaration happens to come first and
|
|
195
|
+
# discards the second. Used to boot clean and stay that way : both
|
|
196
|
+
# declarations survived into the IR, one of them permanently
|
|
197
|
+
# unreachable by name. Refused HERE, at the one place every owner
|
|
198
|
+
# (Aggregate/Entity/Command/Query/PortOperation/ValueObject, each
|
|
199
|
+
# `include AttributeCollector`) mints an attribute through, rather
|
|
200
|
+
# than taught to each of those readers individually.
|
|
201
|
+
def refuse_duplicate_attribute!(name)
|
|
202
|
+
return unless attributes.any? { |attribute| attribute.name == name }
|
|
203
|
+
|
|
204
|
+
raise Malformed, "#{name} is declared twice — an attribute name is declared once, not twice"
|
|
205
|
+
end
|
|
206
|
+
|
|
186
207
|
# A pattern is refused AT DECLARATION, not when a value first meets it :
|
|
187
208
|
# a regex whose meaning depends on which engine reads it is a defect in
|
|
188
209
|
# the bluebook, and a bluebook that loads is one whose patterns carry
|
|
@@ -28,6 +28,13 @@ module Hecks
|
|
|
28
28
|
# what queues here and `#resolve_pending_chapter_givens!`,
|
|
29
29
|
# below, for where it drains.
|
|
30
30
|
@chapter_pending_givens = []
|
|
31
|
+
# ONE LEVEL WIDER STILL — the CHAPTER-WIDE, ENTITY-SCOPED pool
|
|
32
|
+
# (the piece analogue of `@chapter_named_givens`, above). See
|
|
33
|
+
# `EntityBuilder#given_impl`'s own comment for what this
|
|
34
|
+
# closes; `docs/implemented/resolution-rules/
|
|
35
|
+
# chapter-entity-given.md` for the full algorithm.
|
|
36
|
+
@chapter_entity_named_givens = {}
|
|
37
|
+
@chapter_entity_pending_givens = []
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
# Chapter metadata belongs to the composed folder, not whichever file
|
|
@@ -85,8 +92,10 @@ module Hecks
|
|
|
85
92
|
# chapter's own top-level shape is written with it), so also
|
|
86
93
|
# named in GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.
|
|
87
94
|
def aggregate_impl(name, &block)
|
|
88
|
-
@aggregates << AggregateBuilder.build(name, chapter_named_givens:
|
|
89
|
-
chapter_pending_givens:
|
|
95
|
+
@aggregates << AggregateBuilder.build(name, chapter_named_givens: @chapter_named_givens,
|
|
96
|
+
chapter_pending_givens: @chapter_pending_givens,
|
|
97
|
+
chapter_entity_named_givens: @chapter_entity_named_givens,
|
|
98
|
+
chapter_entity_pending_givens: @chapter_entity_pending_givens, &block)
|
|
90
99
|
end
|
|
91
100
|
|
|
92
101
|
# `read_model` is the word (ADR 0025 reverts `report` — the IR
|
|
@@ -143,6 +152,7 @@ module Hecks
|
|
|
143
152
|
# `validate_assembled!` there — nothing downstream should ever
|
|
144
153
|
# read an unresolved placeholder's fields.
|
|
145
154
|
resolve_pending_chapter_givens! unless MetaValidator.deferring?
|
|
155
|
+
resolve_pending_chapter_entity_givens! unless MetaValidator.deferring?
|
|
146
156
|
|
|
147
157
|
# A CHAPTER MAY BE SPLIT ACROSS FILES (see `self.build`'s own
|
|
148
158
|
# comment). Every check below needs the WHOLE chapter present —
|
|
@@ -225,6 +235,47 @@ module Hecks
|
|
|
225
235
|
end
|
|
226
236
|
private :resolve_pending_chapter_given
|
|
227
237
|
|
|
238
|
+
# THE ENTITY-SCOPED ANALOGUE, one level down — see
|
|
239
|
+
# `#resolve_pending_chapter_givens!`'s own comment; identical
|
|
240
|
+
# shape, resolved against `@chapter_entity_named_givens` instead.
|
|
241
|
+
def resolve_pending_chapter_entity_givens!
|
|
242
|
+
@chapter_entity_pending_givens.each do |entry|
|
|
243
|
+
resolved = resolve_pending_chapter_entity_given(entry)
|
|
244
|
+
entry[:placeholder].description = resolved.description
|
|
245
|
+
entry[:placeholder].canonical = resolved.canonical
|
|
246
|
+
entry[:placeholder].predicate = resolved.predicate
|
|
247
|
+
end
|
|
248
|
+
@chapter_entity_pending_givens.clear
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def resolve_pending_chapter_entity_given(entry)
|
|
252
|
+
description = entry[:description]
|
|
253
|
+
candidates = RuleReference.resolve_owner_keyed(@chapter_entity_named_givens, description)
|
|
254
|
+
|
|
255
|
+
if entry[:declared_by]
|
|
256
|
+
candidates[entry[:declared_by]] ||
|
|
257
|
+
raise(Malformed,
|
|
258
|
+
"#{entry[:entity]}'s given #{description.inspect} names no precondition " \
|
|
259
|
+
"#{entry[:declared_by]} declares in this chapter — #{entry[:declared_by]} " \
|
|
260
|
+
"either hasn't declared #{description.inspect}, or declared_by: named the " \
|
|
261
|
+
"wrong piece")
|
|
262
|
+
elsif candidates.size == 1
|
|
263
|
+
candidates.values.first
|
|
264
|
+
elsif candidates.empty?
|
|
265
|
+
raise(Malformed,
|
|
266
|
+
"#{entry[:entity]}'s given #{description.inspect} names no precondition " \
|
|
267
|
+
"any piece in this chapter ever declares — declare it once with a block " \
|
|
268
|
+
"(some piece's own given(#{description.inspect}) { ... })")
|
|
269
|
+
else
|
|
270
|
+
raise(Malformed,
|
|
271
|
+
"#{entry[:entity]}'s given #{description.inspect} is ambiguous across the " \
|
|
272
|
+
"chapter's own pieces — #{candidates.keys.join(', ')} each declare a DIFFERENT " \
|
|
273
|
+
"predicate under this same description; name which one with declared_by: (e.g. " \
|
|
274
|
+
"given(#{description.inspect}, declared_by: #{candidates.keys.first.inspect}))")
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
private :resolve_pending_chapter_entity_given
|
|
278
|
+
|
|
228
279
|
# EVERY WHOLE-CHAPTER CHECK, IN ONE PLACE — the battery `#build`
|
|
229
280
|
# used to run inline, now a pure function of an assembled
|
|
230
281
|
# `Bluebook::Chapter` so `MetaValidator.judge_deferred!` can run
|
|
@@ -862,6 +913,24 @@ module Hecks
|
|
|
862
913
|
# nothing further to check once it matches by name.
|
|
863
914
|
return if remote_attribute.nil? && target.lifecycle&.field.to_s == plan.tail
|
|
864
915
|
|
|
916
|
+
# A PROJECTION MAY CHAIN THROUGH ANOTHER PROJECTION (S12, ADR
|
|
917
|
+
# 0025's own boundary rule, followed through) — `target`'s
|
|
918
|
+
# OWN projected fields live in `projected_fields`, a
|
|
919
|
+
# separate list from `attributes`, so a match there is
|
|
920
|
+
# invisible to the check above even though it names a real,
|
|
921
|
+
# always-current, stored field. `Transfer.projects
|
|
922
|
+
# :source_customer_status, from: :"source.customer_status"`
|
|
923
|
+
# reads Account's own already-projected `customer_status`
|
|
924
|
+
# this way — Account is one hop from Customer, Transfer is
|
|
925
|
+
# one hop from Account, and neither aggregate needs to know
|
|
926
|
+
# about the other's target two hops away. A projected
|
|
927
|
+
# field's remote value is always a scalar by construction
|
|
928
|
+
# (`RebuildSweep.remote_value` never copies a reference, a
|
|
929
|
+
# value object, or a list), so nothing further to check once
|
|
930
|
+
# it matches by name — same reasoning the lifecycle
|
|
931
|
+
# fallback just above already applies.
|
|
932
|
+
return if remote_attribute.nil? && target.projected_fields.any? { |f| f.name.to_s == plan.tail }
|
|
933
|
+
|
|
865
934
|
unless remote_attribute
|
|
866
935
|
raise Malformed,
|
|
867
936
|
"#{aggregate.hecks_name}.projects :#{field.name} reads #{target.hecks_name}'s own " \
|
|
@@ -297,7 +297,7 @@ module Hecks
|
|
|
297
297
|
end
|
|
298
298
|
|
|
299
299
|
op, source = named.first
|
|
300
|
-
@mutations << Mutation.new(target: target.to_sym, op: op, source: source)
|
|
300
|
+
@mutations << Mutation.new(target: target.to_sym, op: op, source: normalize_append_source(op, source))
|
|
301
301
|
end
|
|
302
302
|
|
|
303
303
|
# LEGACY UNDER SHADOW-PARSING (S0a's own bridge) — frozen era text
|
|
@@ -319,8 +319,23 @@ module Hecks
|
|
|
319
319
|
# enforces it. This is the first rule to move ACROSS rather than be
|
|
320
320
|
# duplicated : delete the declaration and an unnamed event is accepted,
|
|
321
321
|
# which is what makes the meta-domain load-bearing rather than decorative.
|
|
322
|
+
#
|
|
323
|
+
# BARE CONSTANT ACCEPTED (ADR 0025, S6 — "events first-class"),
|
|
324
|
+
# `emits Account::AccountFrozen`, resolved through `ConstShim` the
|
|
325
|
+
# same way `trigger`/`dispatch` already resolve a command
|
|
326
|
+
# reference (`Naming.event_ref`, that method's own header). NOT
|
|
327
|
+
# yet a REQUIRED spelling, deliberately, unlike `trigger`/
|
|
328
|
+
# `dispatch`'s own quoted-text refusal: those were safe to refuse
|
|
329
|
+
# only because command references are already 100% migrated
|
|
330
|
+
# across the live corpus (verified 2026-08-27) — `emits`/`on`
|
|
331
|
+
# are not, so refusing the quoted form here would break every
|
|
332
|
+
# live `.bluebook` site this pass didn't touch, not just frozen
|
|
333
|
+
# era text `shadow_parse` exists to keep readable. Both forms
|
|
334
|
+
# are accepted in live source until a full corpus migration
|
|
335
|
+
# lands and the same refusal this file's `reference_to`/
|
|
336
|
+
# `trigger_impl` already carry can be added here safely.
|
|
322
337
|
def emits(event_name)
|
|
323
|
-
@emits << event_name
|
|
338
|
+
@emits << Naming.event_ref(event_name)
|
|
324
339
|
end
|
|
325
340
|
|
|
326
341
|
# THE RECORD'S OWN VALUE AS A MUTATION SOURCE — `sets :positions,
|
|
@@ -412,6 +427,57 @@ module Hecks
|
|
|
412
427
|
@mutations << Mutation.new(target: target.to_s, op: :delegate, source: with)
|
|
413
428
|
end
|
|
414
429
|
|
|
430
|
+
# A COMMAND DECLARING WHAT PAST FACT IT AMENDS — the append-only
|
|
431
|
+
# answer to "what if this record's history turns out to have been
|
|
432
|
+
# wrong": never rewrite the original event (the log stays exactly
|
|
433
|
+
# what it was), always append a NEW fact on top. `event` names the
|
|
434
|
+
# event this command corrects; `as:` optionally binds the located
|
|
435
|
+
# instance for a `given`/`ensures` to reference, the same shape
|
|
436
|
+
# `ensures`'s own `old` binding already has; `reason:` is not
|
|
437
|
+
# descriptive-only the way `goal` is — it is carried as data, the
|
|
438
|
+
# one thing an audit trail actually needs ("we corrected this, and
|
|
439
|
+
# here is why"), refused when blank the same way a `given`'s own
|
|
440
|
+
# description is required to say something.
|
|
441
|
+
#
|
|
442
|
+
# STORED AS A MUTATION, not a new Command field — see the
|
|
443
|
+
# KeywordSeed row's own comment (command.bluebook) for why: this
|
|
444
|
+
# is the exact same choice `delegates_to` already made, for the
|
|
445
|
+
# exact same reason. Rides the SAME multi-binding wire shape
|
|
446
|
+
# `append`/`delegate` use — `as:`/`reason:`/`reverses:` assembled
|
|
447
|
+
# by hand into one `source` hash, the way `sets_impl` assembles up
|
|
448
|
+
# to seven kwargs into one `named` hash above.
|
|
449
|
+
#
|
|
450
|
+
# `reverses: true` NAMES an intent to auto-derive the corrective
|
|
451
|
+
# `sets` from the original event's own mutations, rather than the
|
|
452
|
+
# author writing it — see `AggregateBuilder#seal_correction_targets`,
|
|
453
|
+
# where that derivation actually happens (it needs every sibling
|
|
454
|
+
# command in the aggregate already known, which this builder alone
|
|
455
|
+
# cannot see). MUTUALLY EXCLUSIVE with an explicit `sets` on the
|
|
456
|
+
# same command — two ways of saying the same thing is exactly the
|
|
457
|
+
# redundancy `sets`'s own omittable-`to:` rule refuses elsewhere.
|
|
458
|
+
#
|
|
459
|
+
# `as:` IS ALWAYS STORED AS TEXT, never left a bare Symbol —
|
|
460
|
+
# `Mutation#classified_source`/`#appended_fields` (Behaviour::
|
|
461
|
+
# Mutation) classify any bare Symbol field as `kind: "argument"`,
|
|
462
|
+
# meaning "resolve this against one of THIS command's own declared
|
|
463
|
+
# attributes at dispatch time" (append/delegate's own meaning for a
|
|
464
|
+
# Symbol). `as:` names no such thing — it is a plain label, not yet
|
|
465
|
+
# wired into the expression evaluator (a future round's work, once
|
|
466
|
+
# a real runtime consumer exists) — so coercing it to a String here
|
|
467
|
+
# keeps it out of that machinery entirely rather than silently
|
|
468
|
+
# miscategorised as an unresolvable argument reference.
|
|
469
|
+
def corrects_impl(event, as: nil, reason: nil, reverses: false)
|
|
470
|
+
if reason.to_s.strip.empty?
|
|
471
|
+
raise Malformed,
|
|
472
|
+
"#{@name}'s corrects #{event.inspect} names no reason — a correction " \
|
|
473
|
+
"is carried as data (an audit trail needs to say WHY), the same way a " \
|
|
474
|
+
"given's own description must say something"
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
@mutations << Mutation.new(target: event.to_s, op: :corrects,
|
|
478
|
+
source: { as: as&.to_s, reason: reason.to_s, reverses: reverses })
|
|
479
|
+
end
|
|
480
|
+
|
|
415
481
|
def build
|
|
416
482
|
resolve_implicit_attributes!
|
|
417
483
|
|
|
@@ -488,6 +554,59 @@ module Hecks
|
|
|
488
554
|
end
|
|
489
555
|
refuse_unknown_state_sources!(mutation)
|
|
490
556
|
end
|
|
557
|
+
# A SECOND, SEPARATE pass — not folded into the loop above — so
|
|
558
|
+
# every mutation's own self-referential import (resolve_bare_set!)
|
|
559
|
+
# has already landed before any mutation's SOURCE is checked
|
|
560
|
+
# against the final `attributes` list. `sets :a, to: :b` declared
|
|
561
|
+
# before `sets :b` (bare, importing :b from the owner) is real
|
|
562
|
+
# and legal; checking inline, mutation by mutation, would refuse
|
|
563
|
+
# it for an ordering accident this language has never required
|
|
564
|
+
# authors to avoid. rubocop's Style/CombinableLoops can't see
|
|
565
|
+
# that dependency — it only sees two `@mutations.each` calls
|
|
566
|
+
# back to back — so it's disabled here, not satisfied.
|
|
567
|
+
# rubocop:disable-next Style/CombinableLoops
|
|
568
|
+
@mutations.each { |mutation| refuse_unknown_argument_sources!(mutation) }
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# THE ONES `resolve_source` (CommandRules::Arithmetic) ONLY EVER
|
|
572
|
+
# READS FROM `args` — never a fallback to the record's own current
|
|
573
|
+
# state the way `append`'s own per-field resolution legitimately
|
|
574
|
+
# can (`MutationApplier#resolve_append_source`'s own `instance
|
|
575
|
+
# [source]` fallback, a real, intentional second meaning this
|
|
576
|
+
# check must not foreclose). For exactly these five ops, a bare
|
|
577
|
+
# Symbol source has exactly one legitimate reading — the name of
|
|
578
|
+
# a declared argument — so anything else is unreachable at
|
|
579
|
+
# runtime, not merely optional: no caller can ever supply a value
|
|
580
|
+
# under a name the command never declared (ArgumentGate's own
|
|
581
|
+
# `refuse_unknown_arguments` already refuses that), so the source
|
|
582
|
+
# resolves to nil FOREVER, indistinguishable from a legitimately
|
|
583
|
+
# absent OPTIONAL argument until this check existed to tell them
|
|
584
|
+
# apart. Mirrors `AggregateBuilder#seal_query_argument`'s
|
|
585
|
+
# identical shape for a query's own where-clause argument —
|
|
586
|
+
# same mistake, one construct over.
|
|
587
|
+
CHECKED_SYMBOL_SOURCE_OPS = %i[set increment decrement multiply remove].freeze
|
|
588
|
+
private_constant :CHECKED_SYMBOL_SOURCE_OPS
|
|
589
|
+
|
|
590
|
+
def refuse_unknown_argument_sources!(mutation)
|
|
591
|
+
return unless CHECKED_SYMBOL_SOURCE_OPS.include?(mutation.op)
|
|
592
|
+
return unless mutation.source.is_a?(Symbol)
|
|
593
|
+
# The bare self-referential shape (`sets :field` alone, `source
|
|
594
|
+
# == target`) is `resolve_bare_set!`'s own territory, not this
|
|
595
|
+
# check's — when neither the command nor the owner declares
|
|
596
|
+
# that name, the MORE SPECIFIC, pre-existing refusal one level
|
|
597
|
+
# up (`AggregateBuilder#seal_mutation_targets`, checking the
|
|
598
|
+
# mutation's TARGET against the aggregate's own fields) is the
|
|
599
|
+
# one that should fire, naming the field as a target problem,
|
|
600
|
+
# not — confusingly — as a source problem this check would
|
|
601
|
+
# otherwise misreport it as.
|
|
602
|
+
return if mutation.source.to_s == mutation.target.to_s
|
|
603
|
+
return if attributes.any? { |attr| attr.name == mutation.source }
|
|
604
|
+
|
|
605
|
+
raise Malformed,
|
|
606
|
+
"#{@name}'s sets :#{mutation.target} resolves :#{mutation.source} from its " \
|
|
607
|
+
"arguments, but #{@name} declares no #{mutation.source} attribute — an " \
|
|
608
|
+
"argument that does not exist resolves to nil, always, never what the " \
|
|
609
|
+
"caller actually sent"
|
|
491
610
|
end
|
|
492
611
|
|
|
493
612
|
# `state(:name)` names one of the OWNER'S OWN fields — a snapshot
|
|
@@ -618,7 +737,29 @@ module Hecks
|
|
|
618
737
|
end
|
|
619
738
|
|
|
620
739
|
op, source = named.first
|
|
621
|
-
@mutations << Mutation.new(target: target.to_sym, op: op, source: source)
|
|
740
|
+
@mutations << Mutation.new(target: target.to_sym, op: op, source: normalize_append_source(op, source))
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
# `append:` NORMALLY binds several fields at once (`append: {
|
|
744
|
+
# name: :name, amount: :amount }`) — `Mutation#appended_fields`/
|
|
745
|
+
# `MutationApplier#appended`/the meta-validator Judge's own
|
|
746
|
+
# `mutation_rows` all read `mutation.source` as a Hash
|
|
747
|
+
# unconditionally. A BARE value (`append: :single_field`, or any
|
|
748
|
+
# non-Hash literal) is the one-field shorthand: exactly what an
|
|
749
|
+
# explicit `append: { value: :single_field }` would have meant,
|
|
750
|
+
# named the same way a single-field value object's own implicit
|
|
751
|
+
# member already is (`MutationApplier#appended`'s own `:value`
|
|
752
|
+
# scalar-unwrap). Without this, that shorthand built a Mutation
|
|
753
|
+
# whose `source` was a bare Symbol, which crashed with a raw
|
|
754
|
+
# `NoMethodError` on `#transform_values` the moment anything
|
|
755
|
+
# downstream read it — at dispatch (`MutationApplier#appended`),
|
|
756
|
+
# at IR emission (`Mutation#appended_fields`), and in the
|
|
757
|
+
# meta-validator's own Judge (`Readings#mutation_rows`). #138.
|
|
758
|
+
def normalize_append_source(op, source)
|
|
759
|
+
return source unless op == :append
|
|
760
|
+
return source if source.is_a?(::Hash)
|
|
761
|
+
|
|
762
|
+
{ value: source }
|
|
622
763
|
end
|
|
623
764
|
end
|
|
624
765
|
end
|
|
@@ -7,10 +7,28 @@ module Hecks
|
|
|
7
7
|
|
|
8
8
|
include WordGate
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
# `legacy_bare_port:` — ONLY `Hecks.port`'s own top-level method
|
|
11
|
+
# (lib/hecks.rb) passes `true`. `PortBuilder#build` never refused
|
|
12
|
+
# an empty build (no verb, no signal, nothing) — `Port.new(verb:
|
|
13
|
+
# nil, signal: :reply)` is a real, allowed shape dsl_spec.rb's own
|
|
14
|
+
# "a port" tests rely on (`signal`-only, no `verb` at all). The
|
|
15
|
+
# AGGREGATE-scoped (`BindingProxy#port`) and hecksagon-ROOT
|
|
16
|
+
# (`HecksagonBuilder#port_impl`) callers both reach this SAME
|
|
17
|
+
# class with `owner: nil` too when they're building the bare-verb
|
|
18
|
+
# shape (`port_impl`'s own root-level port can be EITHER shape,
|
|
19
|
+
# decided only after `build` returns) — so `owner.nil?` cannot be
|
|
20
|
+
# the discriminator between "old Hecks.port semantics" and "real
|
|
21
|
+
# DomainPort semantics"; those two callers correctly want the
|
|
22
|
+
# stricter "declares no verb and no operations" refusal `build`
|
|
23
|
+
# already raises below, unchanged. Only the literal top-level
|
|
24
|
+
# `.port` file caller wants the older, looser rule.
|
|
25
|
+
def initialize(name, owner: nil, legacy_bare_port: false)
|
|
26
|
+
@name = name
|
|
27
|
+
@owner = owner
|
|
28
|
+
@operations = []
|
|
29
|
+
@signal = :reply
|
|
30
|
+
@answers = []
|
|
31
|
+
@legacy_bare_port = legacy_bare_port
|
|
14
32
|
end
|
|
15
33
|
|
|
16
34
|
# WHAT THE OUTSIDE TELLS US — an external fact arriving, translated
|
|
@@ -58,18 +76,42 @@ module Hecks
|
|
|
58
76
|
# coerce-and-assign with nothing else, now executed by
|
|
59
77
|
# `GenericDispatch`.
|
|
60
78
|
|
|
79
|
+
# `Hecks.port "x" do verb "y"; signal :effect end`'s own two words,
|
|
80
|
+
# reachable here too — a bare-verb `DomainPortBuilder.build` falls
|
|
81
|
+
# back to the SAME `Port` object `PortBuilder` produces (`build`,
|
|
82
|
+
# below), so any `.port` file can migrate to being parsed by this
|
|
83
|
+
# builder with zero change to its own text, or to any caller that
|
|
84
|
+
# reads `.verb`/`.signal` off the `Port` it gets back. Ordinary
|
|
85
|
+
# `def`s, exactly like `PortBuilder`'s own — `WordGate`'s own
|
|
86
|
+
# header is explicit that a word answered this way never reaches
|
|
87
|
+
# its `method_missing`, so no new self-hosted grammar row is
|
|
88
|
+
# needed for either word under this context.
|
|
89
|
+
def verb(value) = @verb = value.to_s
|
|
90
|
+
def signal(value) = @signal = value.to_sym
|
|
91
|
+
|
|
92
|
+
# THE METHOD CONTRACT — `PortBuilder#answers`'s own twin, added
|
|
93
|
+
# here after the fact: a `.port` file migrated to parse through
|
|
94
|
+
# this builder (the repoint `lib/hecks.rb#port`'s own comment
|
|
95
|
+
# describes) can still declare one (`extraction.port`'s own
|
|
96
|
+
# `answers :canonical`, real, live corpus text) — this builder's
|
|
97
|
+
# bare-verb fallback needs to carry it through to the same `Port`
|
|
98
|
+
# object `PortBuilder` itself would have built, or the migration
|
|
99
|
+
# would silently drop a method-contract check for any `.port`
|
|
100
|
+
# file that uses this word.
|
|
101
|
+
def answers(name) = @answers << name.to_sym
|
|
102
|
+
|
|
61
103
|
def build
|
|
62
104
|
raise Malformed, "#{@name} declares both a verb and operations — a port is one or the other, not both" if @verb && !@operations.empty?
|
|
63
105
|
|
|
64
|
-
return Port.new(name: @name, verb: @verb, signal: :
|
|
106
|
+
return MetaValidator.call_port(Port.new(name: @name, verb: @verb, signal: @signal, answers: @answers)) if @verb || (@legacy_bare_port && @operations.empty?)
|
|
65
107
|
|
|
66
108
|
raise Malformed, "#{@name} declares no verb and no operations" if @operations.empty?
|
|
67
109
|
|
|
68
110
|
DomainPort.new(name: @name, operations: @operations)
|
|
69
111
|
end
|
|
70
112
|
|
|
71
|
-
def self.build(name, owner: nil, &block)
|
|
72
|
-
builder = new(name, owner: owner)
|
|
113
|
+
def self.build(name, owner: nil, legacy_bare_port: false, &block)
|
|
114
|
+
builder = new(name, owner: owner, legacy_bare_port: legacy_bare_port)
|
|
73
115
|
builder.instance_eval(&block) if block
|
|
74
116
|
builder.build
|
|
75
117
|
end
|