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
|
@@ -7,8 +7,9 @@ Hecks.bluebook "Port" do
|
|
|
7
7
|
|
|
8
8
|
identified_by PortName, as: :name
|
|
9
9
|
|
|
10
|
-
attribute :verb,
|
|
11
|
-
attribute :signal,
|
|
10
|
+
attribute :verb, PortVerb
|
|
11
|
+
attribute :signal, PortSignal
|
|
12
|
+
attribute :answers, list_of(PortAnswer)
|
|
12
13
|
|
|
13
14
|
value_object "PortName" do
|
|
14
15
|
attribute :value, String
|
|
@@ -24,6 +25,19 @@ Hecks.bluebook "Port" do
|
|
|
24
25
|
attribute :value, String, one_of: ["reply", "effect"]
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
# THE METHOD CONTRACT — what an adapter must actually `respond_to?`
|
|
29
|
+
# for a dispatch through this door to reach it, as opposed to `verb`
|
|
30
|
+
# and `signal`, which only ever describe the door itself. Optional
|
|
31
|
+
# the same way `verb` is (a port declared with none is exactly
|
|
32
|
+
# today's pre-existing behavior, unchecked past `verify!`'s existing
|
|
33
|
+
# gates), and a plain list of method names rather than a richer
|
|
34
|
+
# shape — the same restraint `AdapterField` already takes for
|
|
35
|
+
# `field`/`secret`.
|
|
36
|
+
value_object "PortAnswer" do
|
|
37
|
+
attribute :value, String
|
|
38
|
+
invariant("an answer names a real method") { !value.to_s.empty? }
|
|
39
|
+
end
|
|
40
|
+
|
|
27
41
|
command "Declare" do
|
|
28
42
|
role "Deployment"
|
|
29
43
|
goal "Open a resource door a domain's own aggregates may call by verb"
|
|
@@ -45,6 +59,18 @@ Hecks.bluebook "Port" do
|
|
|
45
59
|
emits "PortDeclared"
|
|
46
60
|
end
|
|
47
61
|
|
|
62
|
+
command "AddAnswer" do
|
|
63
|
+
role "Deployment"
|
|
64
|
+
goal "Name one method a real adapter for this port must respond to"
|
|
65
|
+
|
|
66
|
+
reference_to Port
|
|
67
|
+
attribute :value, PortAnswer
|
|
68
|
+
|
|
69
|
+
sets :answers, append: { value: :value }
|
|
70
|
+
|
|
71
|
+
emits "PortAnswerAdded"
|
|
72
|
+
end
|
|
73
|
+
|
|
48
74
|
# HOW PORT IS SPELLED. These rows live with the concept
|
|
49
75
|
# they describe; SyntaxBoot discovers every aggregate-local seed table.
|
|
50
76
|
# Contexts represented here: File, Port.
|
|
@@ -63,6 +89,7 @@ Hecks.bluebook "Port" do
|
|
|
63
89
|
member word: "port", context: "File", body: "keywords", inner: "Port", opens: "Port", fills: ""
|
|
64
90
|
member word: "verb", context: "Port", body: "none", inner: "", opens: "", fills: "verb"
|
|
65
91
|
member word: "signal", context: "Port", body: "none", inner: "", opens: "", fills: "signal"
|
|
92
|
+
member word: "answers", context: "Port", body: "none", inner: "", opens: "", fills: "answers"
|
|
66
93
|
|
|
67
94
|
end
|
|
68
95
|
|
|
@@ -86,6 +113,7 @@ Hecks.bluebook "Port" do
|
|
|
86
113
|
member keyword: "port", context: "File", at: "1", named: "", kind: "text", required: "true", fills: "name"
|
|
87
114
|
member keyword: "verb", context: "Port", at: "1", named: "", kind: "text", required: "true", fills: "verb"
|
|
88
115
|
member keyword: "signal", context: "Port", at: "1", named: "", kind: "symbol", required: "true", fills: "signal"
|
|
116
|
+
member keyword: "answers", context: "Port", at: "1", named: "", kind: "symbol", required: "true", fills: "answers"
|
|
89
117
|
|
|
90
118
|
end
|
|
91
119
|
end
|
data/lib/hecks/naming.rb
CHANGED
|
@@ -61,7 +61,21 @@ module Hecks
|
|
|
61
61
|
def singularize(text)
|
|
62
62
|
word = text.to_s
|
|
63
63
|
return "#{word[0..-4]}y" if word.length > 3 && word.end_with?("ies")
|
|
64
|
-
|
|
64
|
+
|
|
65
|
+
# `plural`'s OWN second rule adds "es" (not bare "s") after
|
|
66
|
+
# s/x/z/ch/sh — undone here the same way, or a word `plural`
|
|
67
|
+
# itself would have suffixed with "es" comes back missing its
|
|
68
|
+
# own trailing letter ("Boxes" -> "Boxe", not "Box") once this
|
|
69
|
+
# only ever knew how to drop a bare "s". Checked BEFORE the
|
|
70
|
+
# bare-"s" rule below: stripping "es" first and confirming what
|
|
71
|
+
# is left actually ends in one of those five shapes is what
|
|
72
|
+
# keeps an ordinary "-es" word (e.g. "Invoices" -> "Invoice")
|
|
73
|
+
# from also losing a letter it never doubled.
|
|
74
|
+
if word.length > 3 && word.end_with?("es") && word[0..-3].match?(/(s|x|z|ch|sh)\z/)
|
|
75
|
+
return word[0..-3]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
return word[0..-2] if word.length > 1 && word.end_with?("s")
|
|
65
79
|
|
|
66
80
|
word
|
|
67
81
|
end
|
|
@@ -117,5 +131,44 @@ module Hecks
|
|
|
117
131
|
path, _, command = text.rpartition("::")
|
|
118
132
|
path.empty? ? text : "#{path}.#{command}"
|
|
119
133
|
end
|
|
134
|
+
|
|
135
|
+
# `emits Account::AccountFrozen` / `on Account::AccountFrozen` — the
|
|
136
|
+
# event-side twin of `command_ref`, above (ADR 0025, S6 — "events
|
|
137
|
+
# first-class"). Identical transform (a bare `ScopedConstant`'s last
|
|
138
|
+
# `::` becomes `.`, a String passes through unchanged for legacy
|
|
139
|
+
# `shadow_parse` text and for corpus sites this pass didn't migrate —
|
|
140
|
+
# see that method's own comment for why both rules exist), given its
|
|
141
|
+
# own name because the two references mean different things even
|
|
142
|
+
# though the rewrite is byte-identical: an event name is not a
|
|
143
|
+
# command name that happens to share a format.
|
|
144
|
+
def event_ref(value) = command_ref(value)
|
|
145
|
+
|
|
146
|
+
# `transition Account::AccountDebited => "state"` / `starts_on
|
|
147
|
+
# Transfer::TransferRequested` / `ends_on Transfer::TransferSettled`
|
|
148
|
+
# — a process manager's OWN event references (ADR 0025, S6),
|
|
149
|
+
# DELIBERATELY NOT `event_ref` — found live, not assumed, wiring a
|
|
150
|
+
# real migrated corpus site into `bin/model_check` for the first
|
|
151
|
+
# time (2026-08-28): `SagaInterpreter#begin_saga`/`#advance_saga`
|
|
152
|
+
# match `pm.starts_on`/`pm.handler_for` against `event.name`, which
|
|
153
|
+
# `CommandRules::Emission#emit` stamps BARE — a command's own
|
|
154
|
+
# `emits AccountDebited` never carries its aggregate's name at all
|
|
155
|
+
# (unlike a policy's cross-aggregate `on`, matched instead by
|
|
156
|
+
# `Naming.demodulise(event.aggregate)` split apart from the bare
|
|
157
|
+
# name — `PolicyInterpreter#policies_for`). Handing a saga's own
|
|
158
|
+
# matcher the DOTTED `event_ref` form ("Account.AccountDebited")
|
|
159
|
+
# would silently name an event no command in the domain ever
|
|
160
|
+
# actually emits — caught by `bin/model_check`'s own `deaf_handler`/
|
|
161
|
+
# `deaf_trigger` findings the moment a real qualified corpus site
|
|
162
|
+
# existed to trip them, not by any unit test in isolation.
|
|
163
|
+
#
|
|
164
|
+
# A qualifier is still worth WRITING (`Account::`) — the same
|
|
165
|
+
# provenance a reader gets from `trigger Account::Debit` — it is
|
|
166
|
+
# only not worth KEEPING: `demodulise` drops everything but the
|
|
167
|
+
# final segment, so `Account::AccountDebited` and a bare
|
|
168
|
+
# `AccountDebited` resolve to the identical stored string. A String
|
|
169
|
+
# passes through unchanged either way, exactly like `command_ref`'s
|
|
170
|
+
# own legacy branch — this corpus never spelled one dotted to begin
|
|
171
|
+
# with, so there is nothing here to strip.
|
|
172
|
+
def event_name_ref(value) = demodulise(value)
|
|
120
173
|
end
|
|
121
174
|
end
|
|
@@ -47,7 +47,7 @@ module Hecks
|
|
|
47
47
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
48
48
|
|
|
49
49
|
case implementations.size
|
|
50
|
-
when 1 then
|
|
50
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
51
51
|
when 0
|
|
52
52
|
raise Runtime::WiringError,
|
|
53
53
|
"no adapter implements the #{NAME} port — nothing can answer who's allowed in"
|
data/lib/hecks/ports/agent.port
CHANGED
data/lib/hecks/ports/agent.rb
CHANGED
|
@@ -150,7 +150,7 @@ module Hecks
|
|
|
150
150
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
151
151
|
|
|
152
152
|
case implementations.size
|
|
153
|
-
when 1 then
|
|
153
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
154
154
|
when 0
|
|
155
155
|
raise Runtime::WiringError,
|
|
156
156
|
"no adapter implements the #{NAME} port — nothing can conduct an interview"
|
|
@@ -35,7 +35,7 @@ module Hecks
|
|
|
35
35
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
36
36
|
|
|
37
37
|
case implementations.size
|
|
38
|
-
when 1 then
|
|
38
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
39
39
|
when 0
|
|
40
40
|
raise Runtime::WiringError,
|
|
41
41
|
"no adapter implements the #{NAME} port — nothing can authenticate a sign-in"
|
|
@@ -15,21 +15,24 @@ module Hecks
|
|
|
15
15
|
# A caller decides what to DO with the answer — dispatch under that
|
|
16
16
|
# role, refuse, log, prefer it over some other fallback value — this
|
|
17
17
|
# only answers the question asked. Nothing here binds a
|
|
18
|
-
# `Runtime::Caller
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
18
|
+
# `Runtime::Caller`.
|
|
19
|
+
#
|
|
20
|
+
# STALE AS OF THE `as_of`/`scope` split below, this used to also say
|
|
21
|
+
# nothing here is consulted by `CommandRules::Authorization` — it now
|
|
22
|
+
# is: `refuse_role_mismatch` calls `holds_role?` directly, once a
|
|
23
|
+
# caller binds an `actor_id` and the command's domain has Governance
|
|
24
|
+
# attached (see that rule's own header). `spec/act_as_spec.rb`
|
|
25
|
+
# remains the precedent for the OTHER shape — two separate
|
|
26
|
+
# registries, queried directly by name — for whenever Governance is
|
|
27
|
+
# not in the same boot as the caller; this port is the same
|
|
28
|
+
# questions asked through one adapter when it is.
|
|
26
29
|
module Authorization
|
|
27
30
|
NAME = "authorization"
|
|
28
31
|
|
|
29
32
|
module_function
|
|
30
33
|
|
|
31
|
-
def holds_role?(registry, actor_id:, role:)
|
|
32
|
-
adapter(registry).holds_role?(registry, actor_id: actor_id, role: role)
|
|
34
|
+
def holds_role?(registry, actor_id:, role:, as_of: nil, scope: nil)
|
|
35
|
+
adapter(registry).holds_role?(registry, actor_id: actor_id, role: role, as_of: as_of, scope: scope)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def authorized_as?(registry, from_role:, to_role:)
|
|
@@ -44,7 +47,7 @@ module Hecks
|
|
|
44
47
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
45
48
|
|
|
46
49
|
case implementations.size
|
|
47
|
-
when 1 then
|
|
50
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
48
51
|
when 0
|
|
49
52
|
raise Runtime::WiringError,
|
|
50
53
|
"no adapter implements the #{NAME} port — nothing can answer a role check"
|
data/lib/hecks/ports/clock.port
CHANGED
data/lib/hecks/ports/clock.rb
CHANGED
|
@@ -47,7 +47,7 @@ module Hecks
|
|
|
47
47
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
48
48
|
|
|
49
49
|
case implementations.size
|
|
50
|
-
when 1 then
|
|
50
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
51
51
|
when 0
|
|
52
52
|
raise Runtime::WiringError,
|
|
53
53
|
"no adapter implements the #{NAME} port — nothing can say what time it is"
|
|
@@ -20,7 +20,7 @@ module Hecks
|
|
|
20
20
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
21
21
|
|
|
22
22
|
case implementations.size
|
|
23
|
-
when 1 then
|
|
23
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
24
24
|
when 0
|
|
25
25
|
raise Runtime::WiringError,
|
|
26
26
|
"no adapter implements the #{NAME} port — nothing can recover a " \
|
|
@@ -30,7 +30,7 @@ module Hecks
|
|
|
30
30
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
31
31
|
|
|
32
32
|
case implementations.size
|
|
33
|
-
when 1 then
|
|
33
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
34
34
|
when 0
|
|
35
35
|
raise Runtime::WiringError,
|
|
36
36
|
"no adapter implements the #{NAME} port — nothing can assign an identity"
|
|
@@ -34,7 +34,7 @@ module Hecks
|
|
|
34
34
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
35
35
|
|
|
36
36
|
case implementations.size
|
|
37
|
-
when 1 then
|
|
37
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
38
38
|
when 0
|
|
39
39
|
raise Runtime::WiringError,
|
|
40
40
|
"no adapter implements the #{NAME} port — nothing can mint an identity"
|
|
@@ -25,7 +25,7 @@ module Hecks
|
|
|
25
25
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
26
26
|
|
|
27
27
|
case implementations.size
|
|
28
|
-
when 1 then
|
|
28
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
29
29
|
when 0
|
|
30
30
|
raise Runtime::WiringError,
|
|
31
31
|
"no adapter implements the #{NAME} port — nothing can resolve an identity"
|
|
@@ -64,11 +64,32 @@ module Hecks
|
|
|
64
64
|
def append(entry) = @adapter.append(entry)
|
|
65
65
|
def project(entry) = @adapter.project(entry)
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
# Returns an `Outcome`, not a bare `Instance` — every call site
|
|
68
|
+
# (`CommandInterpreter`/`EntityInterpreter`'s own `step_save`,
|
|
69
|
+
# `RebuildSweep#refresh`) reads it that way.
|
|
70
|
+
#
|
|
71
|
+
# `expected_version:` requests optimistic-concurrency CAS — commit
|
|
72
|
+
# only if the stored record's version still matches what THIS
|
|
73
|
+
# instance was read at. It is `nil` both when a caller explicitly
|
|
74
|
+
# doesn't want CAS (`RebuildSweep#refresh`'s own projection-field
|
|
75
|
+
# touch-up, which has no `given` to protect) and when the instance
|
|
76
|
+
# is brand new (never read from storage, so `instance.version` is
|
|
77
|
+
# nil) — both cases fall through to the plain, unconditional
|
|
78
|
+
# `project(entry)` below, byte-for-byte today's behavior. Only an
|
|
79
|
+
# adapter that both receives a non-nil `expected_version` AND
|
|
80
|
+
# declares `:optimistic_concurrency` gets CAS treatment; every
|
|
81
|
+
# other adapter/call site is unaffected.
|
|
82
|
+
def save(instance, expected_version: nil)
|
|
68
83
|
entry = Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
|
|
69
84
|
append(entry)
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
if expected_version && capabilities.include?(:optimistic_concurrency)
|
|
86
|
+
saved = @adapter.project(entry, expected_version: expected_version)
|
|
87
|
+
return Outcome.new(status: :stale, instance: instance) if saved.nil?
|
|
88
|
+
|
|
89
|
+
return Outcome.new(status: :saved, instance: saved)
|
|
90
|
+
end
|
|
91
|
+
saved = @adapter.project(entry)
|
|
92
|
+
Outcome.new(status: :saved, instance: saved || instance)
|
|
72
93
|
end
|
|
73
94
|
|
|
74
95
|
def atomic_put(instance, insert_only: false)
|
|
@@ -90,7 +111,22 @@ module Hecks
|
|
|
90
111
|
true
|
|
91
112
|
end
|
|
92
113
|
|
|
93
|
-
def record_event
|
|
114
|
+
# NOT an endless `def record_event = ... if ...` — same gotcha as
|
|
115
|
+
# `events` above, and it bit for real here: this guard evaluated
|
|
116
|
+
# against `@adapter` at class-body time (nil, always false), so
|
|
117
|
+
# `record_event` was never defined at all. `emission.rb`'s own
|
|
118
|
+
# `repository.record_event(event) if repository.respond_to?(:record_event)`
|
|
119
|
+
# therefore never fired for any adapter, ever — every declared
|
|
120
|
+
# `emits` was computed and reported in `registry.event_log` (an
|
|
121
|
+
# in-process array, gone at exit) but never durably recorded.
|
|
122
|
+
# Caught because a live tail of a domain's own persisted events
|
|
123
|
+
# found nothing to tail. `sqlite_spec.rb`/`postgres_spec.rb`/
|
|
124
|
+
# `postgres_era_spec.rb` all call `adapter.record_event` directly,
|
|
125
|
+
# bypassing this wrapper — which is exactly why no spec noticed.
|
|
126
|
+
def record_event(event)
|
|
127
|
+
@adapter.record_event(event) if @adapter.respond_to?(:record_event)
|
|
128
|
+
end
|
|
129
|
+
|
|
94
130
|
def query_read_model(domain, model, args, bluebook = nil)
|
|
95
131
|
return unless @adapter.respond_to?(:query_read_model)
|
|
96
132
|
|
|
@@ -3,7 +3,12 @@ module Hecks
|
|
|
3
3
|
module Persistence
|
|
4
4
|
# Adapter outcomes are runtime facts, never command lifecycle.
|
|
5
5
|
Outcome = Struct.new(:status, :instance, keyword_init: true) do
|
|
6
|
-
|
|
6
|
+
# `:stale` is an optimistic-concurrency conflict on a plain `save`
|
|
7
|
+
# (someone else committed since this instance was read) — never a
|
|
8
|
+
# domain refusal, distinct from `:conflicted` (a `creates?`
|
|
9
|
+
# command's identity already exists, `AlreadyExists`). See
|
|
10
|
+
# `Runtime::StaleWrite` (runtime/errors.rb) and `AppendOnly#save`.
|
|
11
|
+
STATUSES = %i[inserted replaced updated conflicted missing saved stale].freeze
|
|
7
12
|
|
|
8
13
|
def initialize(status:, instance: nil)
|
|
9
14
|
normalized = status.to_sym
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Ports
|
|
3
|
+
module Persistence
|
|
4
|
+
# A tiny, real seam — ADR 0033. Core never names a persistence plugin;
|
|
5
|
+
# a plugin makes itself known as a side effect of being `require`d, the
|
|
6
|
+
# same way an adapter's own `.rb` file already registers itself
|
|
7
|
+
# (`Runtime::Registry#add_adapter`) rather than core listing adapter
|
|
8
|
+
# names anywhere. `register_plugin` is the ENTIRE contract a plugin
|
|
9
|
+
# must satisfy: anything responding to `contribute_boot_gates(registry,
|
|
10
|
+
# gates)` (a no-op is a valid implementation).
|
|
11
|
+
#
|
|
12
|
+
# This is deliberately NOT the same registry `Hecks::Projector` uses
|
|
13
|
+
# (ADR 0027) or `Runtime::BootGates` (ADR 0031) — each of those is
|
|
14
|
+
# scoped to its own seam (IR-in/artifact-out; per-boot phased gates).
|
|
15
|
+
# A shared base is deferred until a third registry actually wants one.
|
|
16
|
+
#
|
|
17
|
+
# NAME-KEYED, PROCESS-WIDE, NOT PER-BOOT — unlike `BootGates` (one
|
|
18
|
+
# instance per `Loader.boot` call, because gate REGISTRATION is
|
|
19
|
+
# capability-conditional per registry), a persistence plugin is either
|
|
20
|
+
# `require`d into this process or it isn't; there is no "this boot's
|
|
21
|
+
# registry doesn't need it" case to isolate against, so a plain
|
|
22
|
+
# module-level hash is the right shape here, not an instantiable class.
|
|
23
|
+
module Plugin
|
|
24
|
+
@plugins = {}
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def register(name, plugin)
|
|
28
|
+
@plugins[name.to_sym] = plugin
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def registered?(name)
|
|
32
|
+
@plugins.key?(name.to_sym)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def each(&)
|
|
36
|
+
@plugins.each_value(&)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def any? = !@plugins.empty?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# `Ports::Persistence.plugin?(:era)` / `.each_plugin` — the ordinary
|
|
44
|
+
# call surface; `Plugin.register`/`.registered?`/`.each` stay reachable
|
|
45
|
+
# directly for a plugin's own file to call at require-time.
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
def register_plugin(name, plugin) = Plugin.register(name, plugin)
|
|
49
|
+
def plugin?(name) = Plugin.registered?(name)
|
|
50
|
+
def each_plugin(&) = Plugin.each(&)
|
|
51
|
+
def plugins_loaded? = Plugin.any?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "
|
|
3
|
-
require_relative "
|
|
4
|
-
require_relative "
|
|
5
|
-
require_relative "
|
|
6
|
-
require_relative "registry"
|
|
1
|
+
require_relative "../../../../ports/persistence"
|
|
2
|
+
require_relative "../../../../ports/persistence/binding_policy"
|
|
3
|
+
require_relative "lineage"
|
|
4
|
+
require_relative "../../../../naming"
|
|
5
|
+
require_relative "../../../../framework"
|
|
6
|
+
require_relative "../../../../runtime/registry"
|
|
7
7
|
|
|
8
8
|
module Hecks
|
|
9
9
|
module Runtime
|
|
@@ -44,11 +44,46 @@ module Hecks
|
|
|
44
44
|
# the first refuses toward a scaffold that was never the real
|
|
45
45
|
# drift.
|
|
46
46
|
def check!(registry, directory)
|
|
47
|
+
check_compute_rules_for_registry!(registry)
|
|
48
|
+
check_lineage!(registry, directory)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The domain-agnostic half, split out for ADR 0031's boot-gate
|
|
52
|
+
# registry: a compute rule requires Postgres whatever adapter is
|
|
53
|
+
# actually bound, so this must run for EVERY registry, the same way
|
|
54
|
+
# `registry.verify!` does — it is not conditional on any adapter
|
|
55
|
+
# being lineage-capable, and must never be skipped by
|
|
56
|
+
# `check_lineage!`'s own capability gate below.
|
|
57
|
+
def check_compute_rules_for_registry!(registry)
|
|
58
|
+
registry.bluebooks.each_value { |bluebook| check_compute_rules!(registry, bluebook) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The capability-gated half — ADR 0031's registered `:era_check`
|
|
62
|
+
# gate. Registration is conditional on `lineage_capable_registry?`;
|
|
63
|
+
# `check_bluebook!` below still carries its own per-bluebook
|
|
64
|
+
# `lineage_capable?` return-early, unchanged, for a registry with a
|
|
65
|
+
# mix of lineage-capable and plain-adapter bluebooks.
|
|
66
|
+
def check_lineage!(registry, directory)
|
|
47
67
|
registry.bluebooks.each_value do |bluebook|
|
|
48
68
|
check_bluebook!(registry, bluebook, source_text_for(bluebook, directory), directory: directory)
|
|
49
69
|
end
|
|
50
70
|
end
|
|
51
71
|
|
|
72
|
+
# The `:era_check` gate's own registration predicate: true iff at
|
|
73
|
+
# least one bluebook's own anchor (first) aggregate resolves to a
|
|
74
|
+
# lineage-capable adapter — mirrors `check_bluebook!`'s existing
|
|
75
|
+
# per-bluebook anchor check, just asked once, up front, of the
|
|
76
|
+
# whole registry, so a registry with nothing lineage-capable bound
|
|
77
|
+
# anywhere never registers the gate at all.
|
|
78
|
+
def lineage_capable_registry?(registry)
|
|
79
|
+
registry.bluebooks.each_value.any? do |bluebook|
|
|
80
|
+
first = bluebook.aggregates.first
|
|
81
|
+
next false unless first
|
|
82
|
+
|
|
83
|
+
lineage_capable?(registry, adapter_for(registry, bluebook.name, first))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
52
87
|
# The domain's own directory first, matched by name — a real app's
|
|
53
88
|
# directory may hold more than one file once `uses_framework`
|
|
54
89
|
# exists, so ".first" alone can no longer be trusted, the exact
|
|
@@ -80,8 +115,6 @@ module Hecks
|
|
|
80
115
|
end
|
|
81
116
|
|
|
82
117
|
def check_bluebook!(registry, bluebook, current_text, directory: nil)
|
|
83
|
-
check_compute_rules!(registry, bluebook)
|
|
84
|
-
|
|
85
118
|
first = bluebook.aggregates.first
|
|
86
119
|
return unless first
|
|
87
120
|
|