hecks 1.0.2 → 1.2.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/claude_code.rb +3 -3
- data/lib/hecks/adapters/driven/d1.rb +48 -28
- data/lib/hecks/adapters/driven/folder.rb +10 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +3 -5
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
- data/lib/hecks/adapters/driven/lambda.rb +2 -2
- data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
- data/lib/hecks/adapters/driven/local_storage.rb +120 -0
- data/lib/hecks/adapters/driven/memory.rb +48 -3
- data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
- data/lib/hecks/adapters/driven/postgres/outbox.rb +100 -0
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +57 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +22 -2
- data/lib/hecks/adapters/driven/postgres.rb +45 -31
- data/lib/hecks/adapters/driven/prism.rb +11 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
- data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +22 -3
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
- data/lib/hecks/adapters/driven/sqlite.rb +87 -8
- data/lib/hecks/adapters/driven.rb +4 -0
- data/lib/hecks/adapters.rb +4 -0
- data/lib/hecks/behaviors/dsl.rb +7 -0
- data/lib/hecks/behaviors/expectations.rb +32 -4
- data/lib/hecks/behaviors/rspec.rb +2 -0
- data/lib/hecks/behaviors/runner.rb +4 -0
- data/lib/hecks/bluebook/aggregate.rb +6 -5
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
- data/lib/hecks/bluebook/assembly/build.rb +6 -3
- data/lib/hecks/bluebook/assembly/contract.rb +5 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
- data/lib/hecks/bluebook/assembly/marks.rb +15 -4
- data/lib/hecks/bluebook/assembly.rb +1 -3
- data/lib/hecks/bluebook/attribute.rb +6 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +53 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
- data/lib/hecks/bluebook/command.rb +6 -5
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +29 -477
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
- data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
- data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
- data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
- data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
- data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +142 -19
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
- data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
- data/lib/hecks/bluebook/entity.rb +5 -3
- data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
- data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
- data/lib/hecks/bluebook/expression/projection.json +12 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
- data/lib/hecks/bluebook/expression/resolver.rb +83 -11
- data/lib/hecks/bluebook/expression.rb +1 -0
- data/lib/hecks/bluebook/hexagon.rb +10 -0
- data/lib/hecks/bluebook/lifecycle.rb +9 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +5 -5
- data/lib/hecks/bluebook/model_check.rb +119 -82
- data/lib/hecks/bluebook/pattern_subset.rb +9 -0
- data/lib/hecks/bluebook/policy.rb +2 -1
- data/lib/hecks/bluebook/process_manager.rb +6 -0
- data/lib/hecks/bluebook/project_register.rb +45 -25
- data/lib/hecks/bluebook/query.rb +4 -0
- data/lib/hecks/bluebook/read_model.rb +2 -2
- data/lib/hecks/bluebook/smoke_test.rb +5 -0
- data/lib/hecks/bluebook/synthesizer.rb +2 -1
- data/lib/hecks/bluebook/translation.rb +3 -0
- data/lib/hecks/bluebook/value_object.rb +2 -2
- data/lib/hecks/codemod.rb +13 -0
- data/lib/hecks/doc/reference.rb +17 -11
- data/lib/hecks/facade/cli_runner.rb +2 -2
- data/lib/hecks/facade/command_request.rb +11 -11
- data/lib/hecks/facade/handle.rb +3 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
- data/lib/hecks/facade.rb +4 -0
- data/lib/hecks/forms/app.rb +10 -6
- data/lib/hecks/forms/command_form_renderer.rb +3 -3
- data/lib/hecks/forms/field_renderer.rb +14 -9
- data/lib/hecks/forms/field_shape.rb +7 -2
- data/lib/hecks/forms/html.rb +5 -2
- data/lib/hecks/forms/index_renderer.rb +1 -1
- data/lib/hecks/forms/page.rb +7 -3
- data/lib/hecks/forms/query_form_renderer.rb +3 -2
- data/lib/hecks/forms/record_renderer.rb +22 -16
- data/lib/hecks/forms.rb +4 -0
- data/lib/hecks/fqn.rb +21 -9
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
- data/lib/hecks/fuzzing/properties/guards.rb +251 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
- data/lib/hecks/fuzzing/properties/querying.rb +206 -0
- data/lib/hecks/fuzzing/properties.rb +41 -1065
- data/lib/hecks/fuzzing/replay.rb +60 -16
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
- data/lib/hecks/fuzzing/value_generator.rb +2 -2
- data/lib/hecks/grammar/evolve.rb +34 -10
- data/lib/hecks/grammar/expression_operators.json +124 -0
- data/lib/hecks/grammar.rb +15 -5
- data/lib/hecks/ir.rb +9 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
- data/lib/hecks/language/bluebook/command.bluebook +2 -1
- data/lib/hecks/language/bluebook/entity.bluebook +2 -1
- data/lib/hecks/language/bluebook/projection.bluebook +2 -1
- data/lib/hecks/language/bluebook/query.bluebook +4 -2
- data/lib/hecks/language/bluebook/shape.bluebook +2 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
- data/lib/hecks/literal.rb +18 -8
- data/lib/hecks/naming.rb +6 -3
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.rb +5 -3
- data/lib/hecks/ports/authentication.rb +3 -2
- data/lib/hecks/ports/authorization.rb +1 -1
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.rb +5 -1
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/loading.rb +4 -1
- data/lib/hecks/ports/persistence/append_only.rb +51 -4
- data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
- data/lib/hecks/ports/persistence/plugin.rb +3 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +77 -18
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
- data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
- data/lib/hecks/ports/persistence.rb +7 -3
- data/lib/hecks/ports/projection.rb +13 -5
- data/lib/hecks/ports/query/in_memory.rb +8 -2
- data/lib/hecks/ports/query.rb +1 -3
- data/lib/hecks/ports.rb +2 -0
- data/lib/hecks/projections/diagrams.rb +11 -3
- data/lib/hecks/projections/glossary.rb +374 -0
- data/lib/hecks/projections/ir.rb +2 -0
- data/lib/hecks/projections/model/deviations.rb +2 -1
- data/lib/hecks/projections/model.rb +15 -10
- data/lib/hecks/projections/vocabulary.rb +1 -3
- data/lib/hecks/projections.rb +1 -0
- data/lib/hecks/projector/cli_projector.rb +78 -42
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +34 -1
- data/lib/hecks/projector/narrate_projector.rb +83 -25
- data/lib/hecks/projector.rb +2 -2
- data/lib/hecks/query_ir.rb +60 -36
- data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
- data/lib/hecks/query_specification/common/comparators.rb +4 -0
- data/lib/hecks/query_specification/common/comparison.rb +8 -1
- data/lib/hecks/query_specification/common/dsl.rb +5 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -2
- data/lib/hecks/query_specification/common/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- data/lib/hecks/query_specification/common/order_by.rb +9 -2
- data/lib/hecks/query_specification/common/where_clause.rb +15 -2
- data/lib/hecks/query_specification/field_path.rb +4 -4
- data/lib/hecks/query_specification/read_model/specification.rb +5 -2
- data/lib/hecks/router/namespace_installer.rb +22 -10
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
- data/lib/hecks/runtime/command_interpreter.rb +104 -65
- data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
- data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
- data/lib/hecks/runtime/dependency_planning.rb +33 -21
- data/lib/hecks/runtime/dispatcher.rb +41 -7
- data/lib/hecks/runtime/entity_element.rb +25 -6
- data/lib/hecks/runtime/entity_interpreter.rb +28 -15
- data/lib/hecks/runtime/errors.rb +28 -2
- data/lib/hecks/runtime/instance.rb +9 -3
- data/lib/hecks/runtime/interpreting.rb +45 -9
- data/lib/hecks/runtime/loader.rb +21 -0
- data/lib/hecks/runtime/outbox.rb +365 -0
- data/lib/hecks/runtime/policy_interpreter.rb +32 -17
- data/lib/hecks/runtime/query_interpreter.rb +36 -16
- data/lib/hecks/runtime/reaction_invocation.rb +35 -22
- data/lib/hecks/runtime/read_model_interpreter.rb +31 -7
- data/lib/hecks/runtime/reference_hop.rb +1 -1
- data/lib/hecks/runtime/refusal_wording.rb +55 -49
- data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
- data/lib/hecks/runtime/registry/verification.rb +39 -2
- data/lib/hecks/runtime/registry.rb +21 -11
- data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
- data/lib/hecks/runtime/routing.rb +52 -39
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
- data/lib/hecks/runtime/saga_interpreter.rb +119 -90
- data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
- data/lib/hecks/runtime/tenant_scope.rb +7 -0
- data/lib/hecks/runtime/value/coercion.rb +271 -49
- data/lib/hecks/runtime/value.rb +1 -1
- data/lib/hecks/runtime.rb +7 -2
- data/lib/hecks/storehouse.rb +15 -8
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -0
- data/lib/hecks.rb +17 -13
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
- metadata +16 -2
data/lib/hecks/adapters.rb
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
module Hecks
|
|
2
|
+
# Namespace for every driven-side adapter (Memory, Folder, SQLite,
|
|
3
|
+
# Postgres, Heki, ...) a `persisted_by`/port binding can resolve to;
|
|
4
|
+
# concrete adapters are required in below from adapters/driven and
|
|
5
|
+
# register themselves into this namespace as they load.
|
|
2
6
|
module Adapters
|
|
3
7
|
end
|
|
4
8
|
end
|
data/lib/hecks/behaviors/dsl.rb
CHANGED
|
@@ -11,6 +11,10 @@ module Hecks
|
|
|
11
11
|
# inside a boot, over a suite that was never going to be scoped.
|
|
12
12
|
class Malformed < StandardError; end
|
|
13
13
|
|
|
14
|
+
# The `test "description" do ... end` block's own receiver — collects
|
|
15
|
+
# `tests`/`setup`/`input`/`expect` calls and builds a `TestCase` (ir.rb).
|
|
16
|
+
# `validate_expect!` (private, below) is where a malformed or empty
|
|
17
|
+
# `expect` is refused at build time rather than silently passing later.
|
|
14
18
|
class TestCaseBuilder
|
|
15
19
|
def initialize(description)
|
|
16
20
|
@description = description
|
|
@@ -76,6 +80,9 @@ module Hecks
|
|
|
76
80
|
end
|
|
77
81
|
end
|
|
78
82
|
|
|
83
|
+
# The top-level `Hecks.behaviors "Name" do ... end` receiver — collects
|
|
84
|
+
# `vision`/`loads`/`test` calls and builds a `BehaviorsSuite` (ir.rb),
|
|
85
|
+
# refusing to build one missing either `vision` or `loads` (`#build`).
|
|
79
86
|
class BehaviorsBuilder
|
|
80
87
|
def initialize(name, source_path:)
|
|
81
88
|
@name = name
|
|
@@ -38,6 +38,8 @@ require_relative "../ports/persistence/binding_policy"
|
|
|
38
38
|
# always on and `emits:` is expected to see them.
|
|
39
39
|
module Hecks
|
|
40
40
|
module Behaviors
|
|
41
|
+
# See this file's own header above for what this module does and the
|
|
42
|
+
# two deliberate deviations from the prior port it's built from.
|
|
41
43
|
module Expectations
|
|
42
44
|
module_function
|
|
43
45
|
|
|
@@ -75,6 +77,10 @@ module Hecks
|
|
|
75
77
|
# loop or a long rspec session actually relies on. `runtime:` lets
|
|
76
78
|
# a caller that already holds a booted runtime (a spec, a REPL)
|
|
77
79
|
# hand it in; it is reset the same way.
|
|
80
|
+
# NOT frozen — a real cache, mutated below (`RUNTIMES[key] ||=
|
|
81
|
+
# boot_and_guard(files)`) and by #reset!. False positive for
|
|
82
|
+
# Style/MutableConstant.
|
|
83
|
+
# rubocop:disable-next Style/MutableConstant
|
|
78
84
|
RUNTIMES = {}
|
|
79
85
|
RUNTIMES_LOCK = Mutex.new
|
|
80
86
|
private_constant :RUNTIMES, :RUNTIMES_LOCK
|
|
@@ -137,11 +143,17 @@ module Hecks
|
|
|
137
143
|
before = runtime.registry.event_log.length
|
|
138
144
|
result = dispatch_command(runtime, verb, test.input)
|
|
139
145
|
|
|
140
|
-
|
|
146
|
+
if test.expect.key?(:refused)
|
|
147
|
+
return fail_result(test,
|
|
148
|
+
"expected refused: #{test.expect[:refused].inspect} but dispatch succeeded")
|
|
149
|
+
end
|
|
141
150
|
|
|
142
151
|
if (expected_emits = test.expect[:emits])
|
|
143
152
|
actual = runtime.registry.event_log[before..].map(&:name)
|
|
144
|
-
|
|
153
|
+
unless actual == expected_emits
|
|
154
|
+
return fail_result(test,
|
|
155
|
+
"expected emits: #{expected_emits.inspect}, got #{actual.inspect}")
|
|
156
|
+
end
|
|
145
157
|
end
|
|
146
158
|
|
|
147
159
|
check_ok(test) || check_fields(test, settled_state(runtime, verb, result)) || pass_result(test)
|
|
@@ -205,7 +217,10 @@ module Hecks
|
|
|
205
217
|
def run_query(test, runtime, verb)
|
|
206
218
|
rows = runtime.query(verb, **test.input)
|
|
207
219
|
|
|
208
|
-
|
|
220
|
+
if test.expect.key?(:refused)
|
|
221
|
+
return fail_result(test,
|
|
222
|
+
"expected refused: #{test.expect[:refused].inspect} but the query succeeded")
|
|
223
|
+
end
|
|
209
224
|
|
|
210
225
|
if (expected = test.expect[:count])
|
|
211
226
|
count = if rows.is_a?(Array)
|
|
@@ -307,6 +322,14 @@ module Hecks
|
|
|
307
322
|
def qualify(command, on_aggregate, bluebooks, kind:)
|
|
308
323
|
return command.to_s if command.to_s.include?(".")
|
|
309
324
|
|
|
325
|
+
candidates = qualify_candidates(command, on_aggregate, bluebooks, kind)
|
|
326
|
+
disambiguate_qualified_name(candidates, command, kind, bluebooks)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# THE SEARCH — every (bluebook, aggregate) pair that declares a
|
|
330
|
+
# command/query named `command`, narrowed to `on_aggregate` by name
|
|
331
|
+
# when given.
|
|
332
|
+
def qualify_candidates(command, on_aggregate, bluebooks, kind)
|
|
310
333
|
members = kind == :query ? :queries : :commands
|
|
311
334
|
pairs =
|
|
312
335
|
if on_aggregate
|
|
@@ -314,8 +337,13 @@ module Hecks
|
|
|
314
337
|
else
|
|
315
338
|
bluebooks.flat_map { |bb| bb.aggregates.map { |agg| [bb, agg] } }
|
|
316
339
|
end
|
|
317
|
-
|
|
340
|
+
pairs.select { |_, agg| agg.public_send(members).any? { |m| m.hecks_name == command.to_s } }
|
|
341
|
+
end
|
|
318
342
|
|
|
343
|
+
# THE REPORT — zero candidates and more-than-one candidates both
|
|
344
|
+
# refuse (with a different message); exactly one resolves to its
|
|
345
|
+
# dotted FQN.
|
|
346
|
+
def disambiguate_qualified_name(candidates, command, kind, bluebooks)
|
|
319
347
|
case candidates.size
|
|
320
348
|
when 0
|
|
321
349
|
raise ArgumentError, "no aggregate among #{bluebooks.map(&:name).inspect} declares a #{kind} " \
|
|
@@ -6,6 +6,10 @@ require_relative "expectations"
|
|
|
6
6
|
# that actually touches a live runtime; this file finds `.behaviors`
|
|
7
7
|
# files, loads them, and maps their tests through it.
|
|
8
8
|
module Hecks
|
|
9
|
+
# See this file's own header above for `.run`/`.run_all`/`.parse` — the
|
|
10
|
+
# discovery-and-aggregation half of the `.behaviors` authoring surface;
|
|
11
|
+
# `Expectations` (expectations.rb) is the impure edge that actually runs
|
|
12
|
+
# one test against a live runtime.
|
|
9
13
|
module Behaviors
|
|
10
14
|
FileResult = Struct.new(:path, :parse_error, :runs, keyword_init: true)
|
|
11
15
|
SweepResult = Struct.new(:root, :files_swept, :files, :summary, keyword_init: true)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "behaviour/aggregate"
|
|
2
|
+
require_relative "expression/ast_json"
|
|
2
3
|
|
|
3
4
|
module Hecks
|
|
4
5
|
module Bluebook
|
|
@@ -40,14 +41,14 @@ module Hecks
|
|
|
40
41
|
attributes: many(:attributes),
|
|
41
42
|
value_objects: many(:value_objects),
|
|
42
43
|
commands: many(:commands),
|
|
43
|
-
invariants: -> { invariants.map { |rule|
|
|
44
|
+
invariants: -> { invariants.map { |rule| Expression::AstJson.rule_row(rule) } },
|
|
44
45
|
# A PRECONDITION SHARED ACROSS COMMANDS, DECLARED ONCE (S10, ADR
|
|
45
46
|
# 0025) — the aggregate's OWN named `given`s, the declaration a
|
|
46
47
|
# referencing command's own (already-resolved) `givens` entry
|
|
47
48
|
# came from. Both sides of "declared once, referenced many"
|
|
48
49
|
# are real IR, the same shape a value object's TYPE and an
|
|
49
50
|
# attribute's own reference to it both are.
|
|
50
|
-
preconditions: -> { preconditions.map { |rule|
|
|
51
|
+
preconditions: -> { preconditions.map { |rule| Expression::AstJson.rule_row(rule) } },
|
|
51
52
|
# S12, ADR 0025 — deliberately NOT folded into `attributes`:
|
|
52
53
|
# `EraGuard::ShapeDiff` only ever walks `attributes` to decide
|
|
53
54
|
# whether a NEW field leaves an existing record with something
|
|
@@ -55,10 +56,10 @@ module Hecks
|
|
|
55
56
|
# is different — a record predating the `projects` declaration
|
|
56
57
|
# is expected to be missing it until the rebuild sweep runs,
|
|
57
58
|
# not a shape drift a translation needs to explain.
|
|
58
|
-
projected_fields:
|
|
59
|
-
projected_fields.map
|
|
59
|
+
projected_fields: lambda {
|
|
60
|
+
projected_fields.map do |field|
|
|
60
61
|
{ name: field.name.to_s, reference: field.reference.to_s, remote_field: field.remote_field.to_s }
|
|
61
|
-
|
|
62
|
+
end
|
|
62
63
|
},
|
|
63
64
|
lifecycle: one(:lifecycle),
|
|
64
65
|
entities: many(:entities),
|
|
@@ -77,11 +77,11 @@ module Hecks
|
|
|
77
77
|
# and resolve. Deliberately across every list that can carry one — a
|
|
78
78
|
# reference the walk misses resolves to nil, and a nil target is SKIPPED
|
|
79
79
|
# rather than refused, so the guarantee would go quiet instead of red.
|
|
80
|
-
def stamp_references(
|
|
81
|
-
lists = [
|
|
82
|
-
entity_reference_lists(
|
|
80
|
+
def stamp_references(aggregate)
|
|
81
|
+
lists = [aggregate.attributes, *aggregate.commands.map(&:attributes), *aggregate.queries.map(&:attributes)]
|
|
82
|
+
entity_reference_lists(aggregate.entities, lists)
|
|
83
83
|
|
|
84
|
-
lists.flatten.select(&:reference?).each { |field| field.type.declared_in =
|
|
84
|
+
lists.flatten.select(&:reference?).each { |field| field.type.declared_in = aggregate }
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
# S17, ADR 0026 — walks NESTED entities too (Dispatch, inside
|
|
@@ -20,7 +20,7 @@ module Hecks
|
|
|
20
20
|
contract = Assembly.contract(category)
|
|
21
21
|
keywords = contract.fields.to_h { |keyword, (key, reader)| [keyword, read(reader, row[key])] }
|
|
22
22
|
|
|
23
|
-
holder(contract).public_send(contract.make, **keywords
|
|
23
|
+
holder(contract).public_send(contract.make, **keywords, **extra)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def holder(contract)
|
|
@@ -37,8 +37,11 @@ module Hecks
|
|
|
37
37
|
when Array
|
|
38
38
|
# [:each, reader] maps a list ; [:option, name] reads one named option,
|
|
39
39
|
# which needs its name as well as its value.
|
|
40
|
-
reader.first == :each
|
|
41
|
-
|
|
40
|
+
if reader.first == :each
|
|
41
|
+
Array(value).map { |held| Marks.public_send(reader.last, held) }
|
|
42
|
+
else
|
|
43
|
+
Marks.option(reader.last, value)
|
|
44
|
+
end
|
|
42
45
|
else Marks.public_send(reader, value)
|
|
43
46
|
end
|
|
44
47
|
end
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Bluebook
|
|
3
|
+
# The rulebook translating between the language's own declarations and the
|
|
4
|
+
# IR objects `Build`/`Reconstruction` produce from them — the `Contract`
|
|
5
|
+
# struct format itself (this file) plus, reopened in contracts.rb, the
|
|
6
|
+
# `CONTRACTS` table of one `Contract` per construct category.
|
|
3
7
|
class Assembly
|
|
4
8
|
# WHAT A CONSTRUCT NEEDS THAT THE LANGUAGE CANNOT SAY, and how a claim that a
|
|
5
9
|
# field needs no assembling is CHECKED.
|
|
@@ -78,7 +82,7 @@ module Hecks
|
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
def answers?(method)
|
|
81
|
-
make == :declare ? holder.respond_to?(method) : holder.
|
|
85
|
+
make == :declare ? holder.respond_to?(method) : holder.method_defined?(method)
|
|
82
86
|
end
|
|
83
87
|
|
|
84
88
|
def accepts?(keyword)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Bluebook
|
|
3
|
+
# Reopened here for `CONTRACTS`/`.contract(category)` — the one table of
|
|
4
|
+
# every construct category's own `Contract` (Bluebook, Aggregate,
|
|
5
|
+
# Command, ValueObject, Query, ...); see `Contract`'s own header
|
|
6
|
+
# (contract.rb) for the struct format each entry below fills in.
|
|
3
7
|
class Assembly
|
|
4
8
|
# ONE TABLE, WHERE THERE WERE FIVE HAND-WRITTEN MIRRORS OF IT.
|
|
5
9
|
#
|
|
@@ -129,7 +129,8 @@ module Hecks
|
|
|
129
129
|
# `:delegate`/`:corrects` (CommandBuilder#delegates_to's and
|
|
130
130
|
# #corrects_impl's own comments) ride the SAME multi-binding
|
|
131
131
|
# shape `:append` does.
|
|
132
|
-
return Mutation.new(target: target, op: op, source: appended(change[:fields])) if [:append, :delegate,
|
|
132
|
+
return Mutation.new(target: target, op: op, source: appended(change[:fields])) if [:append, :delegate,
|
|
133
|
+
:corrects].include?(op)
|
|
133
134
|
|
|
134
135
|
Mutation.new(target: target, op: op, source: classified(change[:source]))
|
|
135
136
|
end
|
|
@@ -169,9 +170,19 @@ module Hecks
|
|
|
169
170
|
# a saga with nothing to do.
|
|
170
171
|
def read(value) = Literal.read(value)
|
|
171
172
|
|
|
173
|
+
# `target:` (ADR 0055) — read straight off the wire, unconverted:
|
|
174
|
+
# it's already the bare aggregate-name STRING `WhereClause#to_h`/
|
|
175
|
+
# `OrderBy#to_h`/`LimitSpec#to_h` wrote (`resolve_target`'s own
|
|
176
|
+
# `Naming.demodulise` already ran once, at DSL-build time; this is
|
|
177
|
+
# the REPLAY path every real boot actually goes through, reading
|
|
178
|
+
# that same wire shape back — see this class's own header). Absent
|
|
179
|
+
# from `clause`/`declared` entirely on older wire data that never
|
|
180
|
+
# declared `on:` — `clause[:target]`/`declared[:target]` reads
|
|
181
|
+
# `nil` for a missing key exactly like an explicit `nil` would,
|
|
182
|
+
# so this is additive, not a migration.
|
|
172
183
|
def where_clause(clause)
|
|
173
184
|
QuerySpecification::Common::WhereClause.new(
|
|
174
|
-
field: clause[:field], op: clause[:op].to_sym, value: read(clause[:value])
|
|
185
|
+
field: clause[:field], op: clause[:op].to_sym, value: read(clause[:value]), target: clause[:target]
|
|
175
186
|
)
|
|
176
187
|
end
|
|
177
188
|
|
|
@@ -179,14 +190,14 @@ module Hecks
|
|
|
179
190
|
return nil unless declared
|
|
180
191
|
|
|
181
192
|
QuerySpecification::Common::OrderBy.new(
|
|
182
|
-
field: declared[:field], direction: declared[:direction].to_sym
|
|
193
|
+
field: declared[:field], direction: declared[:direction].to_sym, target: declared[:target]
|
|
183
194
|
)
|
|
184
195
|
end
|
|
185
196
|
|
|
186
197
|
def limit(declared)
|
|
187
198
|
return nil unless declared
|
|
188
199
|
|
|
189
|
-
QuerySpecification::Common::LimitSpec.new(value: read(declared[:value]))
|
|
200
|
+
QuerySpecification::Common::LimitSpec.new(value: read(declared[:value]), target: declared[:target])
|
|
190
201
|
end
|
|
191
202
|
|
|
192
203
|
# EVERY OTHER SPECIFICATION OPTION, from one table.
|
|
@@ -38,15 +38,13 @@ module Hecks
|
|
|
38
38
|
aggregates = Array(@declaration[:aggregates]).map { |row| AggregateAssembly.new(row).aggregate }
|
|
39
39
|
models = Array(@declaration[:read_models]).map { |row| Build.call("ReadModel", row) }
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Build.call(
|
|
42
42
|
"Bluebook", @declaration,
|
|
43
43
|
aggregates: aggregates,
|
|
44
44
|
read_models: models,
|
|
45
45
|
policies: reactions(aggregates),
|
|
46
46
|
process_managers: Array(@declaration[:process_managers]).map { |row| process_manager(row) }
|
|
47
47
|
)
|
|
48
|
-
|
|
49
|
-
chapter
|
|
50
48
|
end
|
|
51
49
|
|
|
52
50
|
private
|
|
@@ -5,6 +5,12 @@ require_relative "../naming"
|
|
|
5
5
|
|
|
6
6
|
module Hecks
|
|
7
7
|
module Bluebook
|
|
8
|
+
# One declared field on a construct — name, type (a primitive, a
|
|
9
|
+
# Reference, or another construct's name, always spelled via `#spell`),
|
|
10
|
+
# whether it's a list, its default/optional/pattern, and the closed set
|
|
11
|
+
# it `admits`. What `attribute :x, Type` (or `identified_by`,
|
|
12
|
+
# `reference_to`, etc.) actually mints, on an Aggregate, Entity, Command,
|
|
13
|
+
# ValueObject, Query or PortOperation alike.
|
|
8
14
|
class Attribute
|
|
9
15
|
include Hecks::IR
|
|
10
16
|
include Behaviour::Attribute
|
|
@@ -24,6 +24,24 @@ module Hecks
|
|
|
24
24
|
# event's own payload.
|
|
25
25
|
def guarded? = !@where.to_s.empty?
|
|
26
26
|
|
|
27
|
+
# THE STRUCTURED FORM OF `where`, derived once — the same tree
|
|
28
|
+
# `AstJson.emit_predicate` spells for every rule row, memoized
|
|
29
|
+
# here because a policy is consulted once per event, not once per
|
|
30
|
+
# boot. Nil when there is no `where`, exactly as the wire carries
|
|
31
|
+
# it.
|
|
32
|
+
def where_ast
|
|
33
|
+
if defined?(@where_ast)
|
|
34
|
+
@where_ast
|
|
35
|
+
else
|
|
36
|
+
(@where_ast = guarded? ? Expression::AstJson.emit_predicate(@where) : nil)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The rule-shaped reading of the guard, for `Evaluator.call_rule`
|
|
41
|
+
# — a policy's `where` has no description (nothing refuses with
|
|
42
|
+
# it; an unmet where is a silent skip).
|
|
43
|
+
def where_rule = @where_rule ||= Given.new(description: nil, canonical: @where, ast: where_ast)
|
|
44
|
+
|
|
27
45
|
# THE FAN-OUT QUERY'S ROUTE, split the way the runtime runs it:
|
|
28
46
|
# `[query_domain, aggregate_name, query_name]`. The query runs
|
|
29
47
|
# against the triggering event's OWN domain unless `for_each`
|
|
@@ -7,7 +7,23 @@ module Hecks
|
|
|
7
7
|
module ProcessManager
|
|
8
8
|
def hecks_name = @name
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
# THE LEG THAT ANSWERS — selected by (event, current state), C10.3
|
|
11
|
+
# (docs/semantics/bluebook-semantics.md). Two legs may answer the
|
|
12
|
+
# same event from different states; which one runs is decided by
|
|
13
|
+
# the state the instance is in, never by declaration order. With
|
|
14
|
+
# no `state` the reading is declarative — "does any leg answer this
|
|
15
|
+
# event, and what is it" — for callers that ask about the
|
|
16
|
+
# procedure rather than an instance (`saga`, the outbox's consumer
|
|
17
|
+
# selection). Build refuses two legs on one (event, state) pair
|
|
18
|
+
# (`ProcessManagerBuilder#validate!`), so a stated lookup is
|
|
19
|
+
# unambiguous by construction.
|
|
20
|
+
def handler_for(event, state = nil)
|
|
21
|
+
@handlers.find { |h| h.event_type == event.to_s && (state.nil? || h.from_state == state.to_s) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def handlers_for(event) = @handlers.select { |h| h.event_type == event.to_s }
|
|
25
|
+
|
|
26
|
+
def handles?(event) = @handlers.any? { |h| h.event_type == event.to_s }
|
|
11
27
|
|
|
12
28
|
# WHETHER A STATE IS ONE THIS PROCEDURE DECLARES — asked of a value
|
|
13
29
|
# a real run left a saga instance holding (its live or rehydrated
|
|
@@ -14,14 +14,59 @@ module Hecks
|
|
|
14
14
|
|
|
15
15
|
def query_name = Naming.snake(@name)
|
|
16
16
|
|
|
17
|
-
# WHICH GATHERED
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
# WHICH GATHERED HEADS THE FILTERING APPLIES TO (ADR 0055) — plural,
|
|
18
|
+
# since `where`/`order_by`/`limit`/`offset` can now each independently
|
|
19
|
+
# name a many-side head via `on:` once there's more than one. A read
|
|
20
|
+
# model with a single many-side head keeps the old reading: every
|
|
21
|
+
# UNTARGETED option (plus `group_by`/`count`/`median`, still
|
|
22
|
+
# single-head-only — ADR 0055) applies to it, same as before `on:`
|
|
23
|
+
# existed. With several many-side heads, only the ones actually named
|
|
24
|
+
# by a targeted option are eligible.
|
|
25
|
+
def filtered_head_names
|
|
26
|
+
many = @aggregate_heads.select { |head| head[:many] }
|
|
27
|
+
return [] if many.empty?
|
|
28
|
+
|
|
29
|
+
return single_filtered_head_name(many) if many.one?
|
|
30
|
+
|
|
31
|
+
targets = (wheres.map(&:target) + [order_by&.target, limit&.target, offset&.target]).compact.uniq
|
|
32
|
+
targets.filter_map { |target| many.find { |head| head[:aggregate] == target.to_s } }.map { |head| head[:as] }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The pre-`on:` reading (ADR 0055), unchanged: with exactly one
|
|
36
|
+
# many-side head, every UNTARGETED option (plus `group_by`/`count`/
|
|
37
|
+
# `median`, still single-head-only) applies to it — split out only
|
|
38
|
+
# to keep `filtered_head_names` itself under this file's own
|
|
39
|
+
# complexity budget, not because the two questions differ in kind.
|
|
40
|
+
def single_filtered_head_name(many)
|
|
41
|
+
declared = wheres.any? || order_by || limit || offset || authorization&.tenant ||
|
|
42
|
+
@group_by.any? || count? || @median_field
|
|
43
|
+
declared ? [many.first[:as]] : []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# THE where/order_by/limit/offset THAT APPLY TO ONE ELIGIBLE HEAD
|
|
47
|
+
# (ADR 0055) — a small view `Ports::Query::InMemory.execute` reads
|
|
48
|
+
# exactly the way it already reads a whole `Query`/`ReadModel`
|
|
49
|
+
# (`.wheres`/`.order_by`/`.limit`/`.offset`/`.null_semantics`), scoped
|
|
50
|
+
# to `head_as`'s own aggregate: an UNTARGETED option applies when
|
|
51
|
+
# `head_as` is the read model's ONE many-side head (the pre-`on:`
|
|
52
|
+
# reading, unchanged) ; a TARGETED one applies when its `target`
|
|
53
|
+
# resolves to `head_as`'s own aggregate.
|
|
54
|
+
FilteredOptions = Struct.new(:wheres, :order_by, :limit, :offset, :null_semantics)
|
|
55
|
+
|
|
56
|
+
def options_for(head_as)
|
|
57
|
+
many = @aggregate_heads.select { |head| head[:many] }
|
|
58
|
+
aggregate_name = @aggregate_heads.find { |head| head[:as] == head_as }&.fetch(:aggregate)
|
|
59
|
+
applies = lambda do |target|
|
|
60
|
+
target.nil? ? many.one? : target.to_s == aggregate_name
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
FilteredOptions.new(
|
|
64
|
+
wheres.select { |where| applies.call(where.target) },
|
|
65
|
+
order_by && applies.call(order_by.target) ? order_by : nil,
|
|
66
|
+
limit && applies.call(limit.target) ? limit : nil,
|
|
67
|
+
offset && applies.call(offset.target) ? offset : nil,
|
|
68
|
+
null_semantics
|
|
69
|
+
)
|
|
25
70
|
end
|
|
26
71
|
end
|
|
27
72
|
end
|
|
@@ -29,7 +29,7 @@ module Hecks
|
|
|
29
29
|
# readers that need all of them ask for `identity_heads`.
|
|
30
30
|
module Identified
|
|
31
31
|
def derive_identity
|
|
32
|
-
@identity_paths = Array(@identified_by).map
|
|
32
|
+
@identity_paths = Array(@identified_by).map(&:to_s).reject(&:empty?)
|
|
33
33
|
@identity_heads = @identity_paths.map { |path| path.split(".").first.to_sym }.uniq
|
|
34
34
|
@identified_by = @identity_heads.size == 1 ? @identity_heads.first : nil
|
|
35
35
|
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
require_relative "behaviour/command"
|
|
2
2
|
require_relative "../vocabulary"
|
|
3
|
+
require_relative "expression/ast_json"
|
|
3
4
|
|
|
4
5
|
module Hecks
|
|
5
6
|
module Bluebook
|
|
6
|
-
Given = Struct.new(:description, :canonical, :predicate, keyword_init: true)
|
|
7
|
+
Given = Struct.new(:description, :canonical, :predicate, :ast, keyword_init: true)
|
|
7
8
|
|
|
8
9
|
Mutation = Struct.new(:target, :op, :source, keyword_init: true) do
|
|
9
10
|
include Hecks::IR
|
|
@@ -24,8 +25,8 @@ module Hecks
|
|
|
24
25
|
# real layering inversion (runtime depends on bluebook, not the
|
|
25
26
|
# reverse). "" (not nil) for ops with no sign, matching every other
|
|
26
27
|
# optional IR text field's own absent-is-empty-string convention.
|
|
27
|
-
def self.sign_for(
|
|
28
|
-
Hecks::Vocabulary.rows("MutationOp").find { |row| row["name"] ==
|
|
28
|
+
def self.sign_for(oper)
|
|
29
|
+
Hecks::Vocabulary.rows("MutationOp").find { |row| row["name"] == oper.to_s }&.fetch("sign", "") || ""
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
emits_ir(target: :target, op: :op, sign: -> { Mutation.sign_for(op) })
|
|
@@ -74,8 +75,8 @@ module Hecks
|
|
|
74
75
|
goal: :goal,
|
|
75
76
|
references: :references,
|
|
76
77
|
attributes: many(:attributes),
|
|
77
|
-
givens: -> { givens.map { |rule|
|
|
78
|
-
ensures: -> { ensures.map { |rule|
|
|
78
|
+
givens: -> { givens.map { |rule| Expression::AstJson.rule_row(rule) } },
|
|
79
|
+
ensures: -> { ensures.map { |rule| Expression::AstJson.rule_row(rule) } },
|
|
79
80
|
mutations: many(:mutations),
|
|
80
81
|
emits: :emits,
|
|
81
82
|
# THE LIFECYCLE STATE THIS COMMAND IS ADMISSIBLE FROM (S10, ADR
|
|
@@ -2,8 +2,11 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# The `Hecks.adapter "Name" do port ...; field ...; secret ... end`
|
|
6
|
+
# receiver — collects the port it implements plus its own settings
|
|
7
|
+
# fields and secrets, then builds and judges an `Adapter` construct.
|
|
5
8
|
class AdapterBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "Adapter"
|
|
9
|
+
GRAMMAR_CONTEXT = "Adapter".freeze
|
|
7
10
|
|
|
8
11
|
include WordGate
|
|
9
12
|
|