hecks 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/d1.rb +38 -14
- data/lib/hecks/adapters/driven/governance_authorization.rb +41 -2
- data/lib/hecks/adapters/driven/heki/journal.rb +7 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +31 -4
- data/lib/hecks/adapters/driven/heki.rb +40 -9
- data/lib/hecks/adapters/driven/lambda.rb +16 -2
- data/lib/hecks/adapters/driven/postgres/codec.rb +8 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +45 -6
- data/lib/hecks/adapters/driven/postgres.rb +76 -29
- data/lib/hecks/adapters/driven/postgres_era.adapter +11 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +17 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +76 -9
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +17 -5
- data/lib/hecks/adapters/driven/sqlite.rb +30 -9
- data/lib/hecks/adapters/driven.rb +19 -1
- data/lib/hecks/behaviors/dsl.rb +29 -0
- data/lib/hecks/behaviors/expectations.rb +62 -2
- data/lib/hecks/bluebook/assembly/contracts.rb +36 -7
- data/lib/hecks/bluebook/assembly/marks.rb +4 -3
- data/lib/hecks/bluebook/assembly.rb +14 -1
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +18 -1
- data/lib/hecks/bluebook/behaviour/process_manager.rb +14 -1
- data/lib/hecks/bluebook/chapter.rb +21 -11
- data/lib/hecks/bluebook/command.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +117 -5
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +21 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +71 -2
- data/lib/hecks/bluebook/dsl/command_builder.rb +91 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +129 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +18 -3
- data/lib/hecks/bluebook/dsl/port_builder.rb +12 -3
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +109 -10
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +9 -2
- data/lib/hecks/bluebook/dsl/world_builder.rb +44 -4
- data/lib/hecks/bluebook/expression/canonical_form.rb +71 -3
- data/lib/hecks/bluebook/expression/evaluator.rb +50 -7
- data/lib/hecks/bluebook/expression/projection.json +48 -0
- data/lib/hecks/bluebook/expression/resolver.rb +161 -10
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +78 -16
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +14 -4
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/shapes.rb +30 -10
- data/lib/hecks/bluebook/meta_validator.rb +103 -13
- data/lib/hecks/bluebook/model_check.rb +132 -5
- data/lib/hecks/bluebook/pattern_subset.rb +66 -2
- data/lib/hecks/bluebook/process_manager.rb +53 -11
- data/lib/hecks/bluebook/value_object.rb +9 -1
- data/lib/hecks/doc/reference.rb +22 -1
- data/lib/hecks/facade/cli_door.rb +6 -3
- data/lib/hecks/facade/json_door.rb +16 -4
- data/lib/hecks/forms/app.rb +47 -6
- data/lib/hecks/forms/command_form_renderer.rb +1 -1
- data/lib/hecks/forms/field_renderer.rb +11 -4
- data/lib/hecks/forms/html.rb +31 -0
- data/lib/hecks/forms/params.rb +30 -1
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/record_renderer.rb +6 -2
- data/lib/hecks/forms/record_table.rb +6 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +19 -19
- data/lib/hecks/framework/bluebook/governance.bluebook +26 -11
- data/lib/hecks/framework/bluebook/identity.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +212 -18
- data/lib/hecks/fuzzing/properties.rb +52 -6
- data/lib/hecks/fuzzing/replay.rb +51 -18
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +28 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +71 -6
- data/lib/hecks/fuzzing/sequence_generator.rb +5 -4
- data/lib/hecks/fuzzing/value_generator.rb +25 -2
- data/lib/hecks/grammar/evolve.rb +33 -0
- data/lib/hecks/grammar/expression.bluebook +8 -5
- data/lib/hecks/grammar/expression_operators.json +497 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +12 -12
- data/lib/hecks/language/bluebook/bluebook.bluebook +3 -3
- data/lib/hecks/language/bluebook/command.bluebook +42 -8
- data/lib/hecks/language/bluebook/entity.bluebook +86 -10
- data/lib/hecks/language/bluebook/policy.bluebook +21 -4
- data/lib/hecks/language/bluebook/process_manager.bluebook +135 -18
- data/lib/hecks/language/bluebook/projection.bluebook +6 -6
- data/lib/hecks/language/bluebook/query.bluebook +4 -4
- data/lib/hecks/language/bluebook/shape.bluebook +6 -6
- data/lib/hecks/language/bluebook/syntax.bluebook +12 -11
- data/lib/hecks/language/bluebook/vocabulary.bluebook +22 -7
- data/lib/hecks/language/oidc.json +20 -0
- data/lib/hecks/language/port.bluebook +30 -2
- data/lib/hecks/naming.rb +54 -1
- data/lib/hecks/ports/access_control.port +7 -2
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.port +6 -2
- data/lib/hecks/ports/agent.rb +1 -1
- data/lib/hecks/ports/authentication.port +4 -2
- data/lib/hecks/ports/authentication.rb +1 -1
- data/lib/hecks/ports/authorization.port +5 -2
- data/lib/hecks/ports/authorization.rb +14 -11
- data/lib/hecks/ports/clock.port +3 -2
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.port +3 -2
- data/lib/hecks/ports/extraction.rb +1 -1
- data/lib/hecks/ports/identity_assignment.port +3 -2
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.port +3 -2
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.port +3 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/execution.rb +6 -1
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_check.rb +41 -8
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard.rb +24 -56
- data/lib/hecks/ports/persistence/{lineage.rb → plugins/era/lineage.rb} +31 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/era_store.rb +3 -3
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/head_compiler.rb +59 -10
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/mint_transaction.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/provisioning.rb +29 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/tail_merge.rb +11 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/transform_installer.rb +20 -0
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage.rb +1 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/coverage_check.rb +5 -5
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/era_resolver.rb +5 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/merge_coordinator.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/minter.rb +4 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era.rb +116 -29
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/approval_digest.rb +1 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_one.rb +14 -5
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_two.rb +31 -6
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/unfed_report.rb +5 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/reattest.rb +3 -3
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/differ.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence.rb +1 -1
- data/lib/hecks/ports/projection.rb +20 -0
- data/lib/hecks/projections/diagrams.rb +230 -1
- data/lib/hecks/projections/shape.rb +15 -2
- data/lib/hecks/projector/cli_projector.rb +10 -1
- data/lib/hecks/projector/exporter.rb +19 -2
- data/lib/hecks/query_ir.rb +19 -0
- data/lib/hecks/query_specification/common/null_policy.rb +13 -1
- data/lib/hecks/query_specification/field_path.rb +20 -2
- data/lib/hecks/query_specification/hop_path.rb +7 -5
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +19 -3
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -0
- data/lib/hecks/runtime/command_interpreter.rb +97 -13
- data/lib/hecks/runtime/command_rules/admissibility.rb +64 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +7 -1
- data/lib/hecks/runtime/command_rules/authorization.rb +2 -1
- data/lib/hecks/runtime/command_rules/references.rb +27 -19
- data/lib/hecks/runtime/dependency_planning.rb +14 -0
- data/lib/hecks/runtime/dispatcher.rb +19 -4
- data/lib/hecks/runtime/entity_interpreter.rb +85 -14
- data/lib/hecks/runtime/errors.rb +22 -0
- data/lib/hecks/runtime/identity.rb +30 -2
- data/lib/hecks/runtime/instance.rb +59 -4
- data/lib/hecks/runtime/interpreting.rb +21 -0
- data/lib/hecks/runtime/loader.rb +59 -18
- data/lib/hecks/runtime/query_interpreter.rb +36 -4
- data/lib/hecks/runtime/reaction_invocation.rb +9 -1
- data/lib/hecks/runtime/read_model_interpreter.rb +76 -1
- data/lib/hecks/runtime/refusal_wording.rb +2 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +75 -3
- data/lib/hecks/runtime/registry/verification.rb +88 -0
- data/lib/hecks/runtime/registry.rb +69 -8
- data/lib/hecks/runtime/saga_interpreter.rb +215 -13
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/value/admission.rb +19 -1
- data/lib/hecks/runtime/value/coercion.rb +75 -10
- data/lib/hecks/runtime.rb +17 -5
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +6 -1
- data/lib/hecks.rb +7 -2
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +48 -37
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard/shape_diff.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_tamper.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/field_cache.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/resumable_backfill.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/storage_shape.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/rule_compiler.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/renderer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/writer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold.rb +0 -0
- /data/lib/hecks/{translation.rb → ports/persistence/plugins/era/translation.rb} +0 -0
|
@@ -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
|
|
|
@@ -37,7 +37,7 @@ module Hecks
|
|
|
37
37
|
|
|
38
38
|
def step(text, rule)
|
|
39
39
|
case rule.strategy
|
|
40
|
-
when "collapse_whitespace" then text.gsub(/\s+/, " ")
|
|
40
|
+
when "collapse_whitespace" then map_outside_strings(text) { |segment| segment.gsub(/\s+/, " ") }
|
|
41
41
|
when "replace" then replace(text, rule)
|
|
42
42
|
else
|
|
43
43
|
raise ArgumentError, "#{rule.strategy.inspect} is not a linked normalisation strategy"
|
|
@@ -45,9 +45,77 @@ module Hecks
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def replace(text, rule)
|
|
48
|
-
|
|
48
|
+
map_outside_strings(text) do |segment|
|
|
49
|
+
if rule.boundary == "none"
|
|
50
|
+
segment.gsub(rule.source_token, rule.replacement)
|
|
51
|
+
else
|
|
52
|
+
segment.gsub(/#{Regexp.escape(rule.source_token)}(?![[:alnum:]_])/, rule.replacement)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Applies a normalisation rule to the text OUTSIDE quoted string
|
|
58
|
+
# literals only, copying every quoted run through byte-for-byte.
|
|
59
|
+
# Every rule here (collapse_whitespace, the `.length`→`.size` fold)
|
|
60
|
+
# used to run quote-blind — `"a b"` collapsed to `"a b"` and
|
|
61
|
+
# `"a.length"` folded to `"a.size"` just as readily as the real
|
|
62
|
+
# source outside the quotes, silently rewriting what a predicate
|
|
63
|
+
# compares a string attribute against, not merely how the
|
|
64
|
+
# predicate itself is spelled. A canonical string literal's
|
|
65
|
+
# CONTENTS are data, never syntax to normalise.
|
|
66
|
+
#
|
|
67
|
+
# Handles both `"` and `'` delimiters (this grammar's own
|
|
68
|
+
# `Resolver.quoted?` admits either), quote-aware exactly the way
|
|
69
|
+
# `Evaluator.top_level_index`/`Resolver.array_elements` already are
|
|
70
|
+
# elsewhere in this sublanguage. An unterminated quote (malformed
|
|
71
|
+
# input) is passed through raw rather than risk mangling it further.
|
|
72
|
+
def map_outside_strings(text)
|
|
73
|
+
result = +""
|
|
74
|
+
buffer = +""
|
|
75
|
+
quote = nil
|
|
76
|
+
|
|
77
|
+
text.each_char do |char|
|
|
78
|
+
if quote
|
|
79
|
+
buffer << char
|
|
80
|
+
if char == quote
|
|
81
|
+
result << buffer
|
|
82
|
+
buffer = +""
|
|
83
|
+
quote = nil
|
|
84
|
+
end
|
|
85
|
+
elsif ['"', "'"].include?(char)
|
|
86
|
+
result << yield(buffer)
|
|
87
|
+
# `char.dup`, not `char.to_s` (a no-op on a String — always
|
|
88
|
+
# returns self, never a copy) and not `+char` either
|
|
89
|
+
# (`String#+@` only dups a FROZEN receiver; `each_char`'s
|
|
90
|
+
# yielded strings aren't frozen, so `+char` is just as
|
|
91
|
+
# much a no-op here). Without a REAL copy, `buffer` and
|
|
92
|
+
# `quote` alias the same mutable object: the very next
|
|
93
|
+
# `buffer << char` grows `quote` right along with it, so
|
|
94
|
+
# `char == quote` can only ever compare a single character
|
|
95
|
+
# against an ever-lengthening string and never closes the
|
|
96
|
+
# literal — everything after a predicate's first quoted
|
|
97
|
+
# string silently skipped normalisation for the rest of
|
|
98
|
+
# the text, undetected because passing text through
|
|
99
|
+
# unnormalised is silent. MASKED by every existing spec
|
|
100
|
+
# here, which only checks that quoted CONTENTS survive
|
|
101
|
+
# untouched (the M7 fix this method exists for) — that
|
|
102
|
+
# still holds by accident once the bug makes the "outside"
|
|
103
|
+
# branch unreachable. Found live: a multi-line `given`/
|
|
104
|
+
# `ensures` block whose ONLY quoted literal closes before a
|
|
105
|
+
# later line — the newline and that later line's own
|
|
106
|
+
# indentation went uncollapsed, diverging from
|
|
107
|
+
# `hecks-parse`'s own (correct) single-space join.
|
|
108
|
+
# `spec/parser_parity_spec.rb`, roster's "a front-row seat
|
|
109
|
+
# takes a member of age".
|
|
110
|
+
buffer = char.dup
|
|
111
|
+
quote = char.dup
|
|
112
|
+
else
|
|
113
|
+
buffer << char
|
|
114
|
+
end
|
|
115
|
+
end
|
|
49
116
|
|
|
50
|
-
|
|
117
|
+
result << (quote ? buffer : yield(buffer))
|
|
118
|
+
result
|
|
51
119
|
end
|
|
52
120
|
end
|
|
53
121
|
end
|
|
@@ -67,6 +67,23 @@ module Hecks
|
|
|
67
67
|
left, right = split_top_level(expr, "&&")
|
|
68
68
|
return And.new(left: parse(left), right: parse(right)) if left
|
|
69
69
|
|
|
70
|
+
# Tried BEFORE `.include?`/comparisons, not after — `!` negates
|
|
71
|
+
# the WHOLE boolean expression that follows it (`!names.include?(x)`
|
|
72
|
+
# means `!(names.include?(x))`, never "call .include? on the negated
|
|
73
|
+
# receiver"), so the leading marker has to be stripped and the
|
|
74
|
+
# remainder re-parsed before anything downstream gets a chance to
|
|
75
|
+
# mis-scan across it. It used to sit after `match_include`, whose
|
|
76
|
+
# naive `rindex(".include?(")` has no concept of a leading `!` —
|
|
77
|
+
# for `!names.include?(x)` it swallowed the `!` straight into the
|
|
78
|
+
# haystack text ("!names"), which `Resolver.parse` cannot resolve,
|
|
79
|
+
# so every spelling of negated membership raised instead of
|
|
80
|
+
# evaluating. Moving the check here fixes both the bare prefix
|
|
81
|
+
# (`!names.include?(x)`) and the parenthesized form
|
|
82
|
+
# (`!(names.include?(x))`) — the recursive `parse` call sees the
|
|
83
|
+
# clean remainder and correctly finds the `.include?` (or `&&`/`||`)
|
|
84
|
+
# inside it.
|
|
85
|
+
return Not.new(node: parse(Regexp.last_match(1))) if expr =~ /\A!(.+)\z/
|
|
86
|
+
|
|
70
87
|
membership = match_include(expr)
|
|
71
88
|
return Include.new(haystack: Resolver.parse(membership[0]), needle: Resolver.parse(membership[1])) if membership
|
|
72
89
|
|
|
@@ -75,8 +92,6 @@ module Hecks
|
|
|
75
92
|
return Compare.new(operator: op, left: Resolver.parse(left), right: Resolver.parse(right)) if left
|
|
76
93
|
end
|
|
77
94
|
|
|
78
|
-
return Not.new(node: parse(Regexp.last_match(1))) if expr =~ /\A!(.+)\z/
|
|
79
|
-
|
|
80
95
|
Resolve.new(expr: Resolver.parse(expr))
|
|
81
96
|
end
|
|
82
97
|
|
|
@@ -145,11 +160,28 @@ module Hecks
|
|
|
145
160
|
value.nil? ? "nil" : value.class.name
|
|
146
161
|
end
|
|
147
162
|
|
|
163
|
+
# THE SAME MIS-SPLIT `Resolver.match_call` had (its own comment
|
|
164
|
+
# has the full story), found here too by the same generator: a
|
|
165
|
+
# `.include?` needle can itself be — or contain — ANOTHER
|
|
166
|
+
# `.include?` call (`"".include?(arr.all? { |el| "".include?("")
|
|
167
|
+
# }.to_s)`, a String built via `.to_s` off a block predicate
|
|
168
|
+
# whose own body happens to include one) — `rindex` finds the
|
|
169
|
+
# INNERMOST occurrence, not the outermost this split actually
|
|
170
|
+
# needs. Fixed identically: try each occurrence left to right,
|
|
171
|
+
# keep the first whose own balanced-paren match reaches the
|
|
172
|
+
# string's last character — `Resolver.matching_paren` is reused
|
|
173
|
+
# directly rather than duplicated, the same depth-tracking rule
|
|
174
|
+
# either grammar layer needs here.
|
|
148
175
|
def match_include(expr)
|
|
149
|
-
|
|
150
|
-
|
|
176
|
+
start = 0
|
|
177
|
+
marker = ".include?("
|
|
178
|
+
while (index = expr.index(marker, start))
|
|
179
|
+
close = Resolver.matching_paren(expr, index + marker.length)
|
|
180
|
+
return [expr[0...index], expr[(index + marker.length)...close]] if close == expr.length - 1
|
|
151
181
|
|
|
152
|
-
|
|
182
|
+
start = index + 1
|
|
183
|
+
end
|
|
184
|
+
nil
|
|
153
185
|
end
|
|
154
186
|
|
|
155
187
|
# Declared the same way in Vocabulary::IncludeHaystack
|
|
@@ -243,9 +275,20 @@ module Hecks
|
|
|
243
275
|
# built to close. `{`/`}` cannot legitimately appear inside
|
|
244
276
|
# a quoted literal either, so this sits beside the existing
|
|
245
277
|
# paren-depth branch, not instead of it.
|
|
246
|
-
|
|
278
|
+
#
|
|
279
|
+
# `[`/`]` -- the identical lesson a THIRD time (found live via
|
|
280
|
+
# the type-directed bounded-exhaustive expression generator,
|
|
281
|
+
# Phase 7 of the equivalence-gap plan): `Resolver::ArrayLiteral`
|
|
282
|
+
# (`[a, b]`) can appear as a general sub-expression, not only
|
|
283
|
+
# as `.include?`'s own haystack, the moment an array-typed
|
|
284
|
+
# attribute or a synthesized literal is embedded anywhere else
|
|
285
|
+
# -- and an element containing a top-level `+`/comparison of
|
|
286
|
+
# its own (`[0, 0 + 0]`) used to read as a split point for
|
|
287
|
+
# THIS expression's own boolean/comparison grammar, exactly
|
|
288
|
+
# the way an un-tracked `{`/`}` once did for block predicates.
|
|
289
|
+
elsif char == "(" || char == "{" || char == "["
|
|
247
290
|
depth += 1
|
|
248
|
-
elsif char == ")" || char == "}"
|
|
291
|
+
elsif char == ")" || char == "}" || char == "]"
|
|
249
292
|
depth -= 1
|
|
250
293
|
elsif depth.zero? && expr[index, operator.length] == operator
|
|
251
294
|
return index if !block_given? || yield(index)
|
|
@@ -149,6 +149,54 @@
|
|
|
149
149
|
"category": "enumeration",
|
|
150
150
|
"precedence": 10,
|
|
151
151
|
"arity": 2
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"symbol": ".match?",
|
|
155
|
+
"category": "pattern_match",
|
|
156
|
+
"precedence": 7,
|
|
157
|
+
"arity": 2
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"symbol": ".present?",
|
|
161
|
+
"category": "presence",
|
|
162
|
+
"precedence": 7,
|
|
163
|
+
"arity": 1
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"symbol": ".blank?",
|
|
167
|
+
"category": "presence",
|
|
168
|
+
"precedence": 7,
|
|
169
|
+
"arity": 1
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"symbol": ".split",
|
|
173
|
+
"category": "text",
|
|
174
|
+
"precedence": 7,
|
|
175
|
+
"arity": 2
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"symbol": ".start_with?",
|
|
179
|
+
"category": "text",
|
|
180
|
+
"precedence": 7,
|
|
181
|
+
"arity": 2
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"symbol": ".end_with?",
|
|
185
|
+
"category": "text",
|
|
186
|
+
"precedence": 7,
|
|
187
|
+
"arity": 2
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"symbol": ".first",
|
|
191
|
+
"category": "positional",
|
|
192
|
+
"precedence": 7,
|
|
193
|
+
"arity": 1
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"symbol": ".last",
|
|
197
|
+
"category": "positional",
|
|
198
|
+
"precedence": 7,
|
|
199
|
+
"arity": 1
|
|
152
200
|
}
|
|
153
201
|
],
|
|
154
202
|
"normalisations": [
|