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
|
@@ -11,7 +11,8 @@ module Hecks
|
|
|
11
11
|
include WordGate
|
|
12
12
|
|
|
13
13
|
def initialize(name, owner_value_objects: [], owner_named_givens: {},
|
|
14
|
-
identity_name_prefix: nil, identity_value_object_installer: nil
|
|
14
|
+
identity_name_prefix: nil, identity_value_object_installer: nil,
|
|
15
|
+
aggregate_name: nil, chapter_entity_named_givens: {}, chapter_entity_pending_givens: [])
|
|
15
16
|
@name = name
|
|
16
17
|
@commands = []
|
|
17
18
|
@queries = []
|
|
@@ -30,6 +31,18 @@ module Hecks
|
|
|
30
31
|
# reference reads from it via `CommandBuilder#
|
|
31
32
|
# reference_named_given`.
|
|
32
33
|
@owner_named_givens = owner_named_givens
|
|
34
|
+
# ONE LEVEL WIDER STILL — the CHAPTER-WIDE, ENTITY-SCOPED pool
|
|
35
|
+
# (the piece analogue of `AggregateBuilder#@chapter_named_givens`,
|
|
36
|
+
# one level down). `@aggregate_name` names THIS piece's own
|
|
37
|
+
# root, so the write-through below can key itself
|
|
38
|
+
# "AggregateName.EntityName" — the same dotted addressing
|
|
39
|
+
# convention `declared_by:` already uses chapter-wide, one
|
|
40
|
+
# level up. See `#given_impl`'s own comment for what this
|
|
41
|
+
# closes and `docs/implemented/resolution-rules/
|
|
42
|
+
# chapter-entity-given.md` for the full algorithm.
|
|
43
|
+
@aggregate_name = aggregate_name || Naming.demodulise(name)
|
|
44
|
+
@chapter_entity_named_givens = chapter_entity_named_givens
|
|
45
|
+
@chapter_entity_pending_givens = chapter_entity_pending_givens
|
|
33
46
|
# DEFERRED CONSTRUCTION — see `AggregateBuilder#drain_pending!`'s
|
|
34
47
|
# own comment; the identical mechanism, one level down, so a
|
|
35
48
|
# nested piece's own commands (Dispatch inside Handler) see
|
|
@@ -150,7 +163,40 @@ module Hecks
|
|
|
150
163
|
# magic.
|
|
151
164
|
# RENAMED FROM `given` — item #13's full metaprogrammed dispatch
|
|
152
165
|
# (slice 4b), same reasoning as reference_to_impl above.
|
|
153
|
-
|
|
166
|
+
#
|
|
167
|
+
# BARE — NO BLOCK — REFERENCES ANOTHER PIECE'S OWN DECLARATION,
|
|
168
|
+
# ANYWHERE IN THE CHAPTER, not just a sibling under this same
|
|
169
|
+
# aggregate — one level wider than round 4's own cross-entity
|
|
170
|
+
# sharing, mirroring `AggregateBuilder#given_impl`'s own
|
|
171
|
+
# chapter-wide shape exactly one level down. Real, live corpus
|
|
172
|
+
# this closes: `Account::LedgerEntry` and `SafeDepositBox::Visit`
|
|
173
|
+
# — two pieces under two DIFFERENT aggregates — independently
|
|
174
|
+
# typed `given("customer is active") { parent.customer.status ==
|
|
175
|
+
# "active" }` byte for byte; neither the aggregate-level chapter
|
|
176
|
+
# pool (a DIFFERENT canonical — bare `customer.status`, the
|
|
177
|
+
# wrong scope for a piece's own command) nor the existing
|
|
178
|
+
# same-aggregate cross-entity pool (`@owner_named_givens`, scoped
|
|
179
|
+
# to ONE aggregate's own entity tree) could reach across the
|
|
180
|
+
# aggregate boundary. Resolved against `@chapter_entity_named_
|
|
181
|
+
# givens`, keyed "AggregateName.EntityName" — see
|
|
182
|
+
# `#reference_named_chapter_entity_given`'s own comment for the
|
|
183
|
+
# algorithm and `docs/implemented/resolution-rules/
|
|
184
|
+
# chapter-entity-given.md` for the full write-up.
|
|
185
|
+
#
|
|
186
|
+
# `declared_by:` is a PLAIN STRING ("Account.LedgerEntry"), not a
|
|
187
|
+
# constant — unlike `AggregateBuilder#given_impl`'s own
|
|
188
|
+
# `declared_by:`, which names a real aggregate constant. A piece
|
|
189
|
+
# has no first-class, independently-addressable reference
|
|
190
|
+
# anywhere in this language (only its owning aggregate does);
|
|
191
|
+
# inventing one to make this ONE argument spelling symmetrical
|
|
192
|
+
# with the aggregate-level word is a real, separate, unscoped
|
|
193
|
+
# feature this fix does not need — ships textual now, the same
|
|
194
|
+
# way `admits:` shipped textual before its own constant-bridge
|
|
195
|
+
# existed, revisited only if a genuine, separate need for
|
|
196
|
+
# constant-addressed pieces shows up later.
|
|
197
|
+
def given_impl(description, declared_by: nil, &predicate)
|
|
198
|
+
return reference_named_chapter_entity_given(description, declared_by: declared_by) unless predicate
|
|
199
|
+
|
|
154
200
|
named = build_rule(Given, description, predicate, owner_name: @name, word: "given",
|
|
155
201
|
extraction_failure: "its source could not be read, so no other runtime could ever evaluate it")
|
|
156
202
|
@named_givens[description] = named
|
|
@@ -163,6 +209,17 @@ module Hecks
|
|
|
163
209
|
# by coincidence, same as an aggregate-level given already
|
|
164
210
|
# tolerates today).
|
|
165
211
|
@owner_named_givens[description] ||= named
|
|
212
|
+
# WRITE-THROUGH, PER OWNER — the chapter-wide analogue of the
|
|
213
|
+
# line above, keyed by [description, this piece's own dotted
|
|
214
|
+
# "Aggregate.Entity" name] rather than description alone, the
|
|
215
|
+
# identical reasoning `AggregateBuilder#given_impl`'s own
|
|
216
|
+
# chapter write-through gives: two DIFFERENT pieces (anywhere
|
|
217
|
+
# in the chapter) independently declaring the SAME description
|
|
218
|
+
# are two DISTINCT candidates a later bare reference chooses
|
|
219
|
+
# between (via `declared_by:` once there is more than one),
|
|
220
|
+
# never silently merged into one slot.
|
|
221
|
+
@chapter_entity_named_givens[description] ||= {}
|
|
222
|
+
@chapter_entity_named_givens[description]["#{@aggregate_name}.#{@name}"] ||= named
|
|
166
223
|
end
|
|
167
224
|
|
|
168
225
|
# A PIECE'S OWN SHAPE RULE (S10, ADR 0025's own "Rules" shape,
|
|
@@ -203,16 +260,81 @@ module Hecks
|
|
|
203
260
|
end
|
|
204
261
|
|
|
205
262
|
def self.build(name, owner_value_objects: [], owner_named_givens: {},
|
|
206
|
-
identity_name_prefix: nil, identity_value_object_installer: nil,
|
|
263
|
+
identity_name_prefix: nil, identity_value_object_installer: nil,
|
|
264
|
+
aggregate_name: nil, chapter_entity_named_givens: {}, chapter_entity_pending_givens: [], &block)
|
|
207
265
|
builder = new(name, owner_value_objects: owner_value_objects, owner_named_givens: owner_named_givens,
|
|
208
266
|
identity_name_prefix: identity_name_prefix,
|
|
209
|
-
identity_value_object_installer: identity_value_object_installer
|
|
267
|
+
identity_value_object_installer: identity_value_object_installer,
|
|
268
|
+
aggregate_name: aggregate_name,
|
|
269
|
+
chapter_entity_named_givens: chapter_entity_named_givens,
|
|
270
|
+
chapter_entity_pending_givens: chapter_entity_pending_givens)
|
|
210
271
|
builder.instance_eval(&block) if block
|
|
211
272
|
builder.build
|
|
212
273
|
end
|
|
213
274
|
|
|
214
275
|
private
|
|
215
276
|
|
|
277
|
+
# PRIMITIVE 2 (RuleReference#resolve_owner_keyed) — the CHAPTER-
|
|
278
|
+
# WIDE, ENTITY-SCOPED analogue of `AggregateBuilder#
|
|
279
|
+
# reference_named_chapter_given`; the three branches below are
|
|
280
|
+
# this construct's OWN refusal wording, not shared, matching that
|
|
281
|
+
# method's own precedent (`declared_by:` only exists on `given`
|
|
282
|
+
# so far, at either scope). UNRESOLVED is deferred, not raised
|
|
283
|
+
# here — see `#pending_chapter_entity_given`, below.
|
|
284
|
+
#
|
|
285
|
+
# WRITES THROUGH TO `@owner_named_givens` TOO — not just
|
|
286
|
+
# `@named_givens` — or this piece resolving a description via the
|
|
287
|
+
# WIDER, chapter pool would leave the NARROWER, same-aggregate
|
|
288
|
+
# pool (`EntityBuilder#given_impl`'s own block-form write-through)
|
|
289
|
+
# never populated for this description, breaking any SIBLING
|
|
290
|
+
# piece's existing command-level bare reference
|
|
291
|
+
# (`CommandBuilder#reference_named_given`) that depends on it —
|
|
292
|
+
# real, live corpus: `SafeDepositBox::KeyIssuance.Return`'s own
|
|
293
|
+
# bare `given("customer is active")` resolves through
|
|
294
|
+
# `@owner_named_givens`, populated by `Visit`'s declaration
|
|
295
|
+
# whether `Visit` types the predicate itself OR (now) references
|
|
296
|
+
# `Account::LedgerEntry`'s instead — this write keeps that
|
|
297
|
+
# working unchanged either way, `||=` so nothing here overrides
|
|
298
|
+
# an actual local declaration if one is ever added later.
|
|
299
|
+
def reference_named_chapter_entity_given(description, declared_by:)
|
|
300
|
+
verify_resolves_via!("given", "Entity", "owner_keyed")
|
|
301
|
+
candidates = resolve_owner_keyed(@chapter_entity_named_givens, description)
|
|
302
|
+
|
|
303
|
+
named =
|
|
304
|
+
if declared_by
|
|
305
|
+
candidates[declared_by] || pending_chapter_entity_given(description, declared_by: declared_by)
|
|
306
|
+
elsif candidates.size == 1
|
|
307
|
+
candidates.values.first
|
|
308
|
+
elsif candidates.empty?
|
|
309
|
+
pending_chapter_entity_given(description, declared_by: nil)
|
|
310
|
+
else
|
|
311
|
+
raise(Malformed,
|
|
312
|
+
"#{@aggregate_name}::#{@name}'s given #{description.inspect} is ambiguous " \
|
|
313
|
+
"across the chapter's own pieces — #{candidates.keys.join(', ')} each declare " \
|
|
314
|
+
"a DIFFERENT predicate under this same description; name which one with " \
|
|
315
|
+
"declared_by: (e.g. given(#{description.inspect}, declared_by: " \
|
|
316
|
+
"#{candidates.keys.first.inspect}))")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
@named_givens[description] = named
|
|
320
|
+
@owner_named_givens[description] ||= named
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# A CHAPTER MAY BE SPLIT ACROSS FILES — the identical reason
|
|
324
|
+
# `AggregateBuilder#pending_chapter_given` defers rather than
|
|
325
|
+
# raising the moment a bare reference outruns what's loaded so
|
|
326
|
+
# far. Hands back a PLACEHOLDER `Given`, embedded by Ruby object
|
|
327
|
+
# reference in this piece's own `preconditions`, and queues the
|
|
328
|
+
# request in `@chapter_entity_pending_givens` —
|
|
329
|
+
# `BluebookBuilder#resolve_pending_chapter_entity_givens!`
|
|
330
|
+
# mutates it in place once every file in the chapter has loaded.
|
|
331
|
+
def pending_chapter_entity_given(description, declared_by:)
|
|
332
|
+
placeholder = Given.new(description: description, canonical: nil, predicate: nil)
|
|
333
|
+
@chapter_entity_pending_givens << { entity: "#{@aggregate_name}.#{@name}", description: description,
|
|
334
|
+
declared_by: declared_by, placeholder: placeholder }
|
|
335
|
+
placeholder
|
|
336
|
+
end
|
|
337
|
+
|
|
216
338
|
# A PIECE'S OWN `one_of` LANDS ON ITS AGGREGATE. A type-position
|
|
217
339
|
# `one_of("never_moved", "moved")` on an entity attribute
|
|
218
340
|
# synthesizes a closed-set value object — and until this, that
|
|
@@ -257,6 +379,9 @@ module Hecks
|
|
|
257
379
|
owner_named_givens: @owner_named_givens,
|
|
258
380
|
identity_name_prefix: "#{@identity_name_prefix}#{Naming.demodulise(name)}",
|
|
259
381
|
identity_value_object_installer: @identity_value_object_installer,
|
|
382
|
+
aggregate_name: @aggregate_name,
|
|
383
|
+
chapter_entity_named_givens: @chapter_entity_named_givens,
|
|
384
|
+
chapter_entity_pending_givens: @chapter_entity_pending_givens,
|
|
260
385
|
&block)
|
|
261
386
|
end
|
|
262
387
|
|
|
@@ -11,9 +11,24 @@ module Hecks
|
|
|
11
11
|
@name = name
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
# `on` —
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# `on Account::AccountFrozen` — A BARE CONSTANT ACCEPTED (ADR
|
|
15
|
+
# 0025, S6 — "events first-class"), resolved through `ConstShim`
|
|
16
|
+
# the same way `trigger`/`dispatch` already resolve a command
|
|
17
|
+
# reference (`Naming.event_ref`, that method's own header). NOT
|
|
18
|
+
# a required spelling yet, unlike `trigger`'s own quoted-text
|
|
19
|
+
# refusal — see `policy.bluebook`'s own KeywordSeed comment for
|
|
20
|
+
# why: event names aren't 100% migrated across the live corpus
|
|
21
|
+
# the way command references are, so both `on "Account.
|
|
22
|
+
# AccountFrozen"` (quoted) and `on Account::AccountFrozen`
|
|
23
|
+
# (bare) stay admitted until a full migration lands.
|
|
24
|
+
#
|
|
25
|
+
# RENAMED FROM the generic single-fill coercion — item #13's
|
|
26
|
+
# full metaprogrammed dispatch, slice 1 (whole-project
|
|
27
|
+
# table-unification survey), now overridden here the same way
|
|
28
|
+
# `trigger_impl` overrides its own generic default.
|
|
29
|
+
def on_impl(event_ref)
|
|
30
|
+
@on_event = Naming.event_ref(event_ref)
|
|
31
|
+
end
|
|
17
32
|
|
|
18
33
|
# `with:` — WHAT THE TRIGGER IS GIVEN, when the event's own shape
|
|
19
34
|
# is not it. Omitted, the whole event payload forwards verbatim,
|
|
@@ -8,15 +8,24 @@ module Hecks
|
|
|
8
8
|
include WordGate
|
|
9
9
|
|
|
10
10
|
def initialize(name)
|
|
11
|
-
@name
|
|
12
|
-
@signal
|
|
11
|
+
@name = name
|
|
12
|
+
@signal = :reply
|
|
13
|
+
@answers = []
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def verb(value) = @verb = value.to_s
|
|
16
17
|
def signal(value) = @signal = value.to_sym
|
|
17
18
|
|
|
19
|
+
# THE METHOD CONTRACT — the fact a `.port` file's `verb`/`signal`
|
|
20
|
+
# never carried: what an adapter must actually RESPOND TO for a
|
|
21
|
+
# dispatch to reach it without a bare `NoMethodError`. Declared the
|
|
22
|
+
# same repeatable way `AdapterBuilder#field`/`#secret` already are,
|
|
23
|
+
# so `verify!` can check it with `respond_to?` at boot instead of
|
|
24
|
+
# the runtime discovering it live.
|
|
25
|
+
def answers(name) = @answers << name.to_sym
|
|
26
|
+
|
|
18
27
|
def build
|
|
19
|
-
MetaValidator.call_port(Port.new(name: @name, verb: @verb, signal: @signal))
|
|
28
|
+
MetaValidator.call_port(Port.new(name: @name, verb: @verb, signal: @signal, answers: @answers))
|
|
20
29
|
end
|
|
21
30
|
|
|
22
31
|
def self.build(name, &block)
|
|
@@ -14,10 +14,38 @@ module Hecks
|
|
|
14
14
|
@handlers = []
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
# `
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
17
|
+
# `starts_on Transfer::TransferRequested` — BARE CONSTANT
|
|
18
|
+
# ACCEPTED (ADR 0025, S6 — "events first-class"), resolved
|
|
19
|
+
# through `ConstShim` the same way `on_impl`/`transition_impl`
|
|
20
|
+
# resolve an event reference — but through `Naming.
|
|
21
|
+
# event_name_ref`, NOT `Naming.event_ref` (that method's own
|
|
22
|
+
# header has the full account: `SagaInterpreter` matches
|
|
23
|
+
# `pm.starts_on`/`pm.ends_on` against a BARE `event.name`, never
|
|
24
|
+
# a "." qualified one). A plain String still passes through
|
|
25
|
+
# unchanged, both for `shadow_parse` and for any corpus site a
|
|
26
|
+
# future pass hasn't migrated yet.
|
|
27
|
+
def starts_on_impl(event_ref)
|
|
28
|
+
@starts_on = Naming.event_name_ref(event_ref)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# `ends_on` — same reasoning as `starts_on_impl`, above.
|
|
32
|
+
def ends_on_impl(event_ref)
|
|
33
|
+
@ends_on = Naming.event_name_ref(event_ref)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# `correlates_by` — item #13's full metaprogrammed dispatch,
|
|
37
|
+
# slice 1 (whole-project table-unification survey): a bare,
|
|
38
|
+
# kind-driven coerce-and-assign with nothing else, executed by
|
|
39
|
+
# `GenericDispatch`.
|
|
40
|
+
#
|
|
41
|
+
# `starts_on`/`ends_on` USED TO be table-driven the same way,
|
|
42
|
+
# until ADR 0025 S6 gave each a second, `kind: "constant"`
|
|
43
|
+
# ArgumentSeed row (`starts_on Transfer::TransferRequested`) —
|
|
44
|
+
# `GenericDispatch::COERCE_BY_KIND` only knows `"text"`/
|
|
45
|
+
# `"symbol"`, and `shape_for` refuses to pick a `single_fill`
|
|
46
|
+
# shape at all once two Argument rows share a `fills:` target
|
|
47
|
+
# (`arguments.size == 1` below), so both words are hand-written
|
|
48
|
+
# again, `calls:`-routed like `transition` already is.
|
|
21
49
|
|
|
22
50
|
# ONE STATE-MACHINE VOCABULARY (S7, ADR 0025 — "events and
|
|
23
51
|
# reactions"): the SAME word `Lifecycle#transition` already
|
|
@@ -85,8 +113,25 @@ module Hecks
|
|
|
85
113
|
handler.instance_eval(&block) if block
|
|
86
114
|
|
|
87
115
|
mapping.each do |event_type, target|
|
|
116
|
+
# BARE CONSTANT ACCEPTED (ADR 0025, S6 — "events first-
|
|
117
|
+
# class"), `transition Account::AccountDebited => "state"` —
|
|
118
|
+
# `Naming.event_name_ref`, NOT the DOTTED `Naming.event_ref`
|
|
119
|
+
# transform `PolicyBuilder#on_impl` uses (that method's own
|
|
120
|
+
# header has the full account, found live wiring a real
|
|
121
|
+
# migrated corpus site into `bin/model_check` for the first
|
|
122
|
+
# time: `SagaInterpreter#advance_saga` matches `handler.
|
|
123
|
+
# event_type` against a BARE `event.name`, never a "."
|
|
124
|
+
# qualified one — a policy's own cross-aggregate match
|
|
125
|
+
# works differently, splitting the qualifier apart from the
|
|
126
|
+
# name rather than comparing the whole string). Writing the
|
|
127
|
+
# qualifier is still worth it (the same provenance `trigger
|
|
128
|
+
# Account::Debit` gives a reader) — `event_name_ref` keeps
|
|
129
|
+
# only the final segment, so `Account::AccountDebited` and a
|
|
130
|
+
# bare `AccountDebited` store identically. A plain String
|
|
131
|
+
# still passes through unchanged, both for `shadow_parse`
|
|
132
|
+
# and for every corpus site this pass didn't migrate.
|
|
88
133
|
state_transition = StateTransition.new(target: target, from: from)
|
|
89
|
-
expand(event_type
|
|
134
|
+
expand(Naming.event_name_ref(event_type), state_transition, handler.dispatches).each { |row| @handlers << row }
|
|
90
135
|
end
|
|
91
136
|
end
|
|
92
137
|
|
|
@@ -185,19 +230,73 @@ module Hecks
|
|
|
185
230
|
#
|
|
186
231
|
# RENAMED FROM `dispatch` — item #13's full metaprogrammed
|
|
187
232
|
# dispatch (slice 4), same reasoning as trigger_impl above.
|
|
188
|
-
|
|
233
|
+
#
|
|
234
|
+
# AN OPTIONAL BLOCK OPENS `compensates` ON THIS DISPATCH
|
|
235
|
+
# SPECIFICALLY — per-dispatch saga compensation, replacing a
|
|
236
|
+
# hand-written list at the saga's own `on :refused` leg. Real,
|
|
237
|
+
# live bug this closes: `examples/banking/bluebook/transfers_
|
|
238
|
+
# and_payments.bluebook`'s own `Settlement` saga wrote a
|
|
239
|
+
# compensating reversal by hand that depended on an event only
|
|
240
|
+
# fired if someone dispatched it manually — "the reversal was
|
|
241
|
+
# written and never armed" (that file's own comment). The
|
|
242
|
+
# runtime now tracks which legs actually completed
|
|
243
|
+
# (`SagaInterpreter`'s own `completed_compensations`) and
|
|
244
|
+
# compensates only those, newest first, instead of trusting an
|
|
245
|
+
# author's static list to be complete and correctly ordered.
|
|
246
|
+
def dispatch_impl(command_ref, with: nil, &block)
|
|
189
247
|
if command_ref.is_a?(::String) && !MetaValidator.shadow_parsing?
|
|
190
248
|
raise InvalidProcessManager,
|
|
191
249
|
"dispatch #{command_ref.inspect} is quoted text — give the bare command constant " \
|
|
192
250
|
"instead, e.g. dispatch Account::Debit"
|
|
193
251
|
end
|
|
194
252
|
|
|
195
|
-
DispatchSpec.new(
|
|
253
|
+
spec = DispatchSpec.new(
|
|
196
254
|
command_name: Naming.command_ref(command_ref),
|
|
197
255
|
with_spec: (with || {}).to_a
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
|
|
256
|
+
)
|
|
257
|
+
spec.instance_variable_set(:@projection_declared, !with.nil?)
|
|
258
|
+
|
|
259
|
+
if block
|
|
260
|
+
builder = DispatchBuilder.new
|
|
261
|
+
builder.instance_eval(&block)
|
|
262
|
+
# `instance_variable_get`, not a public `compensates_spec`
|
|
263
|
+
# reader — a public one would be a method the grammar
|
|
264
|
+
# never declares, exactly what `spec/syntax_conformance_
|
|
265
|
+
# spec.rb`'s "answers only words the language declares"
|
|
266
|
+
# check exists to catch.
|
|
267
|
+
spec.compensates = builder.instance_variable_get(:@compensates_spec)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
@dispatches << spec
|
|
271
|
+
spec
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# THE NESTED SCOPE `dispatch ... do ... end` OPENS — one word
|
|
275
|
+
# only (`compensates`), the compensating half of the dispatch it
|
|
276
|
+
# sits inside. Its own `compensates_impl` builds a SECOND
|
|
277
|
+
# `DispatchSpec`, shape-identical to `HandlerBuilder#dispatch_
|
|
278
|
+
# impl`'s own — a compensation takes the exact same two
|
|
279
|
+
# arguments (a bare command constant, an optional `with:`)
|
|
280
|
+
# because it resolves through the identical scope (current
|
|
281
|
+
# event payload, opening event memory, correlation binding)
|
|
282
|
+
# any saga dispatch already does
|
|
283
|
+
# (`SagaInterpreter#dispatch_args`).
|
|
284
|
+
class DispatchBuilder
|
|
285
|
+
GRAMMAR_CONTEXT = "Dispatch"
|
|
286
|
+
|
|
287
|
+
include WordGate
|
|
288
|
+
|
|
289
|
+
def compensates_impl(command_ref, with: nil)
|
|
290
|
+
if command_ref.is_a?(::String) && !MetaValidator.shadow_parsing?
|
|
291
|
+
raise InvalidProcessManager,
|
|
292
|
+
"compensates #{command_ref.inspect} is quoted text — give the bare command constant " \
|
|
293
|
+
"instead, e.g. compensates Account::Credit"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
@compensates_spec = DispatchSpec.new(
|
|
297
|
+
command_name: Naming.command_ref(command_ref),
|
|
298
|
+
with_spec: (with || {}).to_a
|
|
299
|
+
)
|
|
201
300
|
end
|
|
202
301
|
end
|
|
203
302
|
end
|
|
@@ -129,6 +129,7 @@ module Hecks
|
|
|
129
129
|
# the real table, every time, no exception.
|
|
130
130
|
BOOTSTRAP_FALLBACK = {
|
|
131
131
|
%w[given Aggregate] => { resolves_via: "owner_keyed", disambiguator: "declared_by" },
|
|
132
|
+
%w[given Entity] => { resolves_via: "owner_keyed", disambiguator: "declared_by" },
|
|
132
133
|
%w[given Command] => { resolves_via: "hash_chain" },
|
|
133
134
|
%w[invariant ValueObject] => { resolves_via: "sibling_scan" }
|
|
134
135
|
}.freeze
|
|
@@ -110,8 +110,15 @@ module Hecks
|
|
|
110
110
|
# below checks them in, for the same reason: `list_of`/
|
|
111
111
|
# `one_of` used in an attribute's own type position never
|
|
112
112
|
# arrive with `self.class::GRAMMAR_CONTEXT == "Type"`.
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
own_key = [self.class::GRAMMAR_CONTEXT, word.to_s]
|
|
114
|
+
type_key = ["Type", word.to_s]
|
|
115
|
+
# key? first, never `||` — a target method name is never
|
|
116
|
+
# actually stored as `false`, but the fallback lookup itself
|
|
117
|
+
# must not silently prefer "Type" over this class's own
|
|
118
|
+
# context on the strength of a falsy-looking value.
|
|
119
|
+
found = fallback.key?(own_key) || fallback.key?(type_key)
|
|
120
|
+
target = fallback.key?(own_key) ? fallback[own_key] : fallback[type_key]
|
|
121
|
+
return send(target, *args, **kwargs, &block) if found
|
|
115
122
|
|
|
116
123
|
return super
|
|
117
124
|
end
|
|
@@ -14,6 +14,30 @@ module Hecks
|
|
|
14
14
|
def to_h = @values
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# THE AGGREGATE-QUALIFIED MIRROR (#143) — a `.world` file's own
|
|
18
|
+
# `Pizzas::Order.charged_by("Stripe") do ... end` visually mirrors the
|
|
19
|
+
# SAME bind line the sibling `.hecksagon` file already writes
|
|
20
|
+
# (`HecksagonBuilder`'s own `BindingProxy`), but `IR::World#for_verb`/
|
|
21
|
+
# `#for_binding` key purely by verb and adapter name — the aggregate
|
|
22
|
+
# qualifier is never read back out, it exists only for that visual
|
|
23
|
+
# mirroring. So unlike `BindingProxy`, nothing here needs to hold onto
|
|
24
|
+
# the resolved constant chain at all: every verb call, qualified or
|
|
25
|
+
# not, has to land in the exact same `@settings` write path
|
|
26
|
+
# (`WorldBuilder#record_binding`).
|
|
27
|
+
class WorldConstProxy
|
|
28
|
+
def self.namespace(builder)
|
|
29
|
+
Module.new do
|
|
30
|
+
define_singleton_method(:const_missing) { |_aggregate| WorldConstProxy.new(builder) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(builder) = @builder = builder
|
|
35
|
+
|
|
36
|
+
def method_missing(verb, *args, **kwargs, &block) = @builder.record_binding(verb, args, kwargs, block)
|
|
37
|
+
|
|
38
|
+
def respond_to_missing?(_name, _include_private = false) = true
|
|
39
|
+
end
|
|
40
|
+
|
|
17
41
|
class WorldBuilder
|
|
18
42
|
GRAMMAR_CONTEXT = "World"
|
|
19
43
|
|
|
@@ -44,6 +68,17 @@ module Hecks
|
|
|
44
68
|
result = word_gate_dispatch(verb, args, kwargs, block)
|
|
45
69
|
return result unless result.equal?(WordGate::NOT_ADMITTED)
|
|
46
70
|
|
|
71
|
+
record_binding(verb, args, kwargs, block)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def respond_to_missing?(_name, _include_private = false) = true
|
|
75
|
+
|
|
76
|
+
# EXTRACTED from the old `method_missing` body (#143) so
|
|
77
|
+
# `WorldConstProxy`'s own aggregate-qualified verb calls
|
|
78
|
+
# (`Pizzas::Order.charged_by(...)`) write into the exact same
|
|
79
|
+
# place the bare top-level spelling (`charged_by(...)`) already
|
|
80
|
+
# does — one write path, two spellings.
|
|
81
|
+
def record_binding(verb, args, kwargs, block)
|
|
47
82
|
collector = SettingsCollector.new
|
|
48
83
|
collector.instance_eval(&block) if block
|
|
49
84
|
value = { adapter: args.first.to_s }.merge(kwargs).merge(collector.to_h)
|
|
@@ -51,17 +86,22 @@ module Hecks
|
|
|
51
86
|
@settings["#{verb}:#{args.first.to_s.downcase}"] = value
|
|
52
87
|
end
|
|
53
88
|
|
|
54
|
-
def respond_to_missing?(_name, _include_private = false) = true
|
|
55
|
-
|
|
56
89
|
def build
|
|
57
90
|
MetaValidator.call_world(
|
|
58
91
|
World.new(domain: @domain, realm: @realm, latest: @latest, settings: @settings)
|
|
59
92
|
)
|
|
60
93
|
end
|
|
61
94
|
|
|
95
|
+
# `ConstShim`'s resolver, the same bridge `HecksagonBuilder`/
|
|
96
|
+
# `BluebookBuilder` already wrap their own `instance_eval` in
|
|
97
|
+
# (#143) — without it, `Pizzas::Order.charged_by(...)` raised
|
|
98
|
+
# `NameError: uninitialized constant Pizzas` for every `.world`
|
|
99
|
+
# file using the aggregate-qualified mirror form, since a bare
|
|
100
|
+
# `Pizzas` has no real constant to resolve to.
|
|
62
101
|
def self.build(domain, &block)
|
|
63
|
-
builder
|
|
64
|
-
|
|
102
|
+
builder = new(domain)
|
|
103
|
+
resolver = ->(_domain) { WorldConstProxy.namespace(builder) }
|
|
104
|
+
ConstShim.with(resolver) { builder.instance_eval(&block) } if block
|
|
65
105
|
builder.build
|
|
66
106
|
end
|
|
67
107
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
require_relative "evaluator"
|
|
2
|
+
require_relative "resolver"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Bluebook
|
|
6
|
+
module Expression
|
|
7
|
+
# ── AST → JSON — walks the REAL Evaluator/Resolver AST (the same
|
|
8
|
+
# objects a live dispatch parses `given`/`ensures`/invariant text
|
|
9
|
+
# into — see docs/implemented/guides/running-a-runtime.md's "The
|
|
10
|
+
# expression grammar") and emits plain, JSON-serializable Ruby
|
|
11
|
+
# Hashes, tagged by `"op"`.
|
|
12
|
+
#
|
|
13
|
+
# THE SAME TWO-METHOD WALK `rust/project/expr_emitter.rb`'s own
|
|
14
|
+
# `emit_bool`/`emit_resolver` already do, over the SAME AST — that
|
|
15
|
+
# file's own methods build RUST SOURCE-CODE STRINGS for `rust/
|
|
16
|
+
# project`'s codegen (`rust/src/kernel::expr::Expr` literals, baked
|
|
17
|
+
# into a generated domain's own compiled binary); this builds DATA
|
|
18
|
+
# instead, for a genuinely different consumer with a genuinely
|
|
19
|
+
# different constraint: `rust/host` can never link the `rust`
|
|
20
|
+
# (kernel) crate at all (a real, load-bearing build constraint —
|
|
21
|
+
# `reference_validate.rs`'s own header has the full reasoning: one
|
|
22
|
+
# path dependency would statically bake every domain's generated
|
|
23
|
+
# dispatch code into every Lambda binary), so a value object's own
|
|
24
|
+
# `invariant` predicate has to travel as something `rust/host` can
|
|
25
|
+
# deserialize and interpret itself, at RUNTIME, from `ir.json` — the
|
|
26
|
+
# exact same relationship `rust/project`'s own `Expr` literals
|
|
27
|
+
# already have to the compiled kernel, one layer further out.
|
|
28
|
+
#
|
|
29
|
+
# LIVES IN CORE `lib/hecks`, NOT `rust/project/` — `rust/project.rb`
|
|
30
|
+
# is a separate, downstream toolchain
|
|
31
|
+
# (`lib/hecks/projector.rb`'s own header: "a whole separate Ruby
|
|
32
|
+
# program"), never `require`d by core `lib/hecks/bluebook/*.rb`
|
|
33
|
+
# files (confirmed: no core file does). `value_object.rb`'s own
|
|
34
|
+
# `invariants:` IR emission needs this for EVERY domain's ordinary
|
|
35
|
+
# `to_h`/`ir.json` export — golden fixtures, `hecks-parse`'s parity
|
|
36
|
+
# comparisons, and any deploy artifact, not only a `bin/project_rust`
|
|
37
|
+
# run — so it belongs beside `Evaluator`/`Resolver` themselves, not
|
|
38
|
+
# bolted onto a tool that only sometimes runs.
|
|
39
|
+
#
|
|
40
|
+
# COMPLETE, not corpus-scoped: every node this grammar admits gets
|
|
41
|
+
# a real arm, the identical "raise, don't silently drop" discipline
|
|
42
|
+
# `expr_emitter.rb`'s own `emit_bool`/`emit_resolver` already hold
|
|
43
|
+
# to — even though, as of this writing, no real corpus VALUE OBJECT
|
|
44
|
+
# invariant exercises `Include`/`Modulo`/`BlockPredicate`/`Find`/
|
|
45
|
+
# `Array`/`MatchesRegex`/`Presence`/`Split`/`StartsWith`/`EndsWith`/
|
|
46
|
+
# `First`/`Last` (only `given`/`ensures` clauses do, elsewhere in
|
|
47
|
+
# the corpus — a different consumer of this same grammar).
|
|
48
|
+
# `rust/host/src/expr_json.rs`'s own header names exactly which of
|
|
49
|
+
# these its interpreter evaluates for real today versus refuses
|
|
50
|
+
# cleanly — a narrower, deliberate, documented boundary on the
|
|
51
|
+
# INTERPRETING side, not on this EMITTING side: an author is free
|
|
52
|
+
# to write ANY real expression in a value object's own `invariant`,
|
|
53
|
+
# and this always emits it faithfully; whether `rust/host` can yet
|
|
54
|
+
# CHECK it at mint time is that file's own question to answer, not
|
|
55
|
+
# this one's to pre-empt by refusing to even try.
|
|
56
|
+
module AstJson
|
|
57
|
+
module_function
|
|
58
|
+
|
|
59
|
+
def emit_predicate(canonical)
|
|
60
|
+
emit_bool(Evaluator.parse(canonical))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def emit_bool(node)
|
|
64
|
+
case node
|
|
65
|
+
when Evaluator::Or then { "op" => "or", "left" => emit_bool(node.left), "right" => emit_bool(node.right) }
|
|
66
|
+
when Evaluator::And then { "op" => "and", "left" => emit_bool(node.left), "right" => emit_bool(node.right) }
|
|
67
|
+
when Evaluator::Not then { "op" => "not", "expr" => emit_bool(node.node) }
|
|
68
|
+
when Evaluator::Compare
|
|
69
|
+
{ "op" => "compare", "cmp" => emit_comparison(node.operator),
|
|
70
|
+
"left" => emit_resolver(node.left), "right" => emit_resolver(node.right) }
|
|
71
|
+
when Evaluator::Include
|
|
72
|
+
emit_include(node)
|
|
73
|
+
when Evaluator::Resolve
|
|
74
|
+
emit_resolver(node.expr)
|
|
75
|
+
else
|
|
76
|
+
raise "unhandled evaluator node #{node.class} — no JSON rendering exists for it (lib/hecks/bluebook/expression/ast_json.rb#emit_bool)"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def emit_comparison(op)
|
|
81
|
+
{ "less_than" => op.compares_less_than, "equal" => op.compares_equal, "negated" => op.negated }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The JSON-target sibling of `expr_emitter.rb`'s own
|
|
85
|
+
# `emit_include` — see that method's own comment for the full
|
|
86
|
+
# reasoning (a LITERAL array haystack has no `Expr::Include`-
|
|
87
|
+
# representable shape on EITHER target, kernel or host, so both
|
|
88
|
+
# rewrite it identically into an OR-of-equalities at emission
|
|
89
|
+
# time rather than carrying a shape neither interpreter could
|
|
90
|
+
# evaluate). A non-literal haystack still emits `include`
|
|
91
|
+
# unchanged.
|
|
92
|
+
EQ = Evaluator::OPERATORS.find { |op| op.symbol == "==" }
|
|
93
|
+
private_constant :EQ
|
|
94
|
+
|
|
95
|
+
def emit_include(node)
|
|
96
|
+
return { "op" => "include", "haystack" => emit_resolver(node.haystack), "needle" => emit_resolver(node.needle) } \
|
|
97
|
+
unless node.haystack.is_a?(Resolver::ArrayLiteral)
|
|
98
|
+
|
|
99
|
+
return { "op" => "bool", "value" => false } if node.haystack.elements.empty?
|
|
100
|
+
|
|
101
|
+
equalities = node.haystack.elements.map do |element|
|
|
102
|
+
{ "op" => "compare", "cmp" => emit_comparison(EQ), "left" => emit_resolver(node.needle), "right" => emit_resolver(element) }
|
|
103
|
+
end
|
|
104
|
+
equalities.reduce { |left, right| { "op" => "or", "left" => left, "right" => right } }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def emit_resolver(node)
|
|
108
|
+
case node
|
|
109
|
+
when Resolver::IntegerLiteral then { "op" => "int", "value" => node.value }
|
|
110
|
+
when Resolver::FloatLiteral then { "op" => "float", "value" => node.value }
|
|
111
|
+
when Resolver::StringLiteral then { "op" => "str", "value" => node.value }
|
|
112
|
+
when Resolver::BoolLiteral then { "op" => "bool", "value" => node.value }
|
|
113
|
+
when Resolver::NilLiteral then { "op" => "nil" }
|
|
114
|
+
when Resolver::Lookup then { "op" => "lookup", "path" => node.path }
|
|
115
|
+
when Resolver::Addition then { "op" => "add", "left" => emit_resolver(node.left), "right" => emit_resolver(node.right) }
|
|
116
|
+
when Resolver::SignTest
|
|
117
|
+
{ "op" => "sign_test", "cmp" => emit_comparison(node.operator), "receiver" => emit_resolver(node.receiver) }
|
|
118
|
+
when Resolver::Empty then { "op" => "empty", "receiver" => emit_resolver(node.receiver) }
|
|
119
|
+
when Resolver::ToS then { "op" => "to_s", "receiver" => emit_resolver(node.receiver) }
|
|
120
|
+
when Resolver::Modulo then { "op" => "modulo", "receiver" => emit_resolver(node.receiver), "divisor" => emit_resolver(node.divisor) }
|
|
121
|
+
when Resolver::Size then { "op" => "size", "receiver" => emit_resolver(node.receiver) }
|
|
122
|
+
when Resolver::BlockPredicate
|
|
123
|
+
{ "op" => "block_predicate", "mode" => node.mode.to_s, "receiver" => emit_resolver(node.receiver),
|
|
124
|
+
"param" => node.param.to_s, "predicate" => emit_bool(node.predicate) }
|
|
125
|
+
when Resolver::Find
|
|
126
|
+
{ "op" => "find", "receiver" => emit_resolver(node.receiver), "param" => node.param.to_s,
|
|
127
|
+
"predicate" => emit_bool(node.predicate), "path" => node.path.map(&:to_s) }
|
|
128
|
+
when Resolver::ArrayLiteral
|
|
129
|
+
{ "op" => "array", "elements" => node.elements.map { |element| emit_resolver(element) } }
|
|
130
|
+
when Resolver::MatchesRegex
|
|
131
|
+
{ "op" => "matches_regex", "receiver" => emit_resolver(node.receiver), "pattern" => node.pattern, "flags" => node.flags }
|
|
132
|
+
when Resolver::Presence
|
|
133
|
+
{ "op" => "presence", "receiver" => emit_resolver(node.receiver), "negated" => node.negated }
|
|
134
|
+
when Resolver::Split
|
|
135
|
+
{ "op" => "split", "receiver" => emit_resolver(node.receiver), "separator" => node.separator }
|
|
136
|
+
when Resolver::StartsWith
|
|
137
|
+
{ "op" => "starts_with", "receiver" => emit_resolver(node.receiver), "substring" => node.substring }
|
|
138
|
+
when Resolver::EndsWith
|
|
139
|
+
{ "op" => "ends_with", "receiver" => emit_resolver(node.receiver), "substring" => node.substring }
|
|
140
|
+
when Resolver::First then { "op" => "first", "receiver" => emit_resolver(node.receiver) }
|
|
141
|
+
when Resolver::Last then { "op" => "last", "receiver" => emit_resolver(node.receiver) }
|
|
142
|
+
else
|
|
143
|
+
raise "unhandled resolver node #{node.class} — no JSON rendering exists for it (lib/hecks/bluebook/expression/ast_json.rb#emit_resolver)"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|