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.
Files changed (202) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/d1.rb +38 -14
  3. data/lib/hecks/adapters/driven/governance_authorization.rb +41 -2
  4. data/lib/hecks/adapters/driven/heki/journal.rb +7 -2
  5. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  6. data/lib/hecks/adapters/driven/heki/snapshot.rb +31 -4
  7. data/lib/hecks/adapters/driven/heki.rb +40 -9
  8. data/lib/hecks/adapters/driven/lambda.rb +16 -2
  9. data/lib/hecks/adapters/driven/postgres/codec.rb +8 -0
  10. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +45 -6
  11. data/lib/hecks/adapters/driven/postgres.rb +76 -29
  12. data/lib/hecks/adapters/driven/postgres_era.adapter +11 -0
  13. data/lib/hecks/adapters/driven/sql_query_builder.rb +1 -1
  14. data/lib/hecks/adapters/driven/sqlite/codec.rb +17 -0
  15. data/lib/hecks/adapters/driven/sqlite/projection.rb +76 -9
  16. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +17 -5
  17. data/lib/hecks/adapters/driven/sqlite.rb +30 -9
  18. data/lib/hecks/adapters/driven.rb +19 -1
  19. data/lib/hecks/behaviors/dsl.rb +29 -0
  20. data/lib/hecks/behaviors/expectations.rb +62 -2
  21. data/lib/hecks/bluebook/assembly/contracts.rb +36 -7
  22. data/lib/hecks/bluebook/assembly/marks.rb +4 -3
  23. data/lib/hecks/bluebook/assembly.rb +14 -1
  24. data/lib/hecks/bluebook/behaviour/lifecycle.rb +18 -1
  25. data/lib/hecks/bluebook/behaviour/process_manager.rb +14 -1
  26. data/lib/hecks/bluebook/behaviour/value_object.rb +13 -7
  27. data/lib/hecks/bluebook/chapter.rb +21 -11
  28. data/lib/hecks/bluebook/command.rb +1 -1
  29. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +117 -5
  30. data/lib/hecks/bluebook/dsl/attribute_collector.rb +21 -0
  31. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +71 -2
  32. data/lib/hecks/bluebook/dsl/command_builder.rb +144 -3
  33. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +49 -7
  34. data/lib/hecks/bluebook/dsl/entity_builder.rb +129 -4
  35. data/lib/hecks/bluebook/dsl/policy_builder.rb +18 -3
  36. data/lib/hecks/bluebook/dsl/port_builder.rb +12 -3
  37. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +109 -10
  38. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -0
  39. data/lib/hecks/bluebook/dsl/word_gate.rb +9 -2
  40. data/lib/hecks/bluebook/dsl/world_builder.rb +44 -4
  41. data/lib/hecks/bluebook/expression/ast_json.rb +149 -0
  42. data/lib/hecks/bluebook/expression/canonical_form.rb +71 -3
  43. data/lib/hecks/bluebook/expression/evaluator.rb +50 -7
  44. data/lib/hecks/bluebook/expression/projection.json +48 -0
  45. data/lib/hecks/bluebook/expression/resolver.rb +161 -10
  46. data/lib/hecks/bluebook/expression.rb +1 -0
  47. data/lib/hecks/bluebook/hexagon.rb +1 -1
  48. data/lib/hecks/bluebook/meta_validator/judge.rb +78 -16
  49. data/lib/hecks/bluebook/meta_validator/port_judge.rb +4 -0
  50. data/lib/hecks/bluebook/meta_validator/readings.rb +14 -4
  51. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +42 -3
  52. data/lib/hecks/bluebook/meta_validator/shapes.rb +30 -10
  53. data/lib/hecks/bluebook/meta_validator.rb +103 -13
  54. data/lib/hecks/bluebook/model_check.rb +147 -6
  55. data/lib/hecks/bluebook/pattern_subset.rb +66 -2
  56. data/lib/hecks/bluebook/process_manager.rb +53 -11
  57. data/lib/hecks/bluebook/project_register.rb +25 -0
  58. data/lib/hecks/bluebook/value_object.rb +19 -2
  59. data/lib/hecks/doc/reference.rb +22 -1
  60. data/lib/hecks/facade/cli_door.rb +6 -3
  61. data/lib/hecks/facade/json_door.rb +16 -4
  62. data/lib/hecks/forms/app.rb +47 -6
  63. data/lib/hecks/forms/command_form_renderer.rb +1 -1
  64. data/lib/hecks/forms/field_renderer.rb +11 -4
  65. data/lib/hecks/forms/html.rb +31 -0
  66. data/lib/hecks/forms/params.rb +30 -1
  67. data/lib/hecks/forms/port_argument.rb +46 -0
  68. data/lib/hecks/forms/record_renderer.rb +6 -2
  69. data/lib/hecks/forms/record_table.rb +6 -1
  70. data/lib/hecks/framework/bluebook/console_settings.bluebook +19 -19
  71. data/lib/hecks/framework/bluebook/governance.bluebook +26 -11
  72. data/lib/hecks/framework/bluebook/identity.bluebook +2 -2
  73. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  74. data/lib/hecks/fuzzing/invalid_value_generator.rb +3 -2
  75. data/lib/hecks/fuzzing/isolated_boot.rb +212 -18
  76. data/lib/hecks/fuzzing/properties.rb +52 -6
  77. data/lib/hecks/fuzzing/replay.rb +51 -18
  78. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +28 -2
  79. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +71 -6
  80. data/lib/hecks/fuzzing/sequence_generator.rb +5 -4
  81. data/lib/hecks/fuzzing/value_generator.rb +25 -2
  82. data/lib/hecks/grammar/evolve.rb +33 -0
  83. data/lib/hecks/grammar/expression.bluebook +8 -5
  84. data/lib/hecks/grammar/expression_operators.json +497 -1
  85. data/lib/hecks/language/bluebook/aggregate.bluebook +12 -12
  86. data/lib/hecks/language/bluebook/bluebook.bluebook +3 -3
  87. data/lib/hecks/language/bluebook/command.bluebook +42 -8
  88. data/lib/hecks/language/bluebook/entity.bluebook +86 -10
  89. data/lib/hecks/language/bluebook/policy.bluebook +21 -4
  90. data/lib/hecks/language/bluebook/process_manager.bluebook +135 -18
  91. data/lib/hecks/language/bluebook/projection.bluebook +6 -6
  92. data/lib/hecks/language/bluebook/query.bluebook +4 -4
  93. data/lib/hecks/language/bluebook/shape.bluebook +6 -6
  94. data/lib/hecks/language/bluebook/syntax.bluebook +12 -11
  95. data/lib/hecks/language/bluebook/vocabulary.bluebook +22 -7
  96. data/lib/hecks/language/hecksagon/domain_port.bluebook +25 -1
  97. data/lib/hecks/language/oidc.json +20 -0
  98. data/lib/hecks/language/port.bluebook +30 -2
  99. data/lib/hecks/naming.rb +54 -1
  100. data/lib/hecks/ports/access_control.port +7 -2
  101. data/lib/hecks/ports/access_control.rb +1 -1
  102. data/lib/hecks/ports/agent.port +6 -2
  103. data/lib/hecks/ports/agent.rb +1 -1
  104. data/lib/hecks/ports/authentication.port +4 -2
  105. data/lib/hecks/ports/authentication.rb +1 -1
  106. data/lib/hecks/ports/authorization.port +5 -2
  107. data/lib/hecks/ports/authorization.rb +14 -11
  108. data/lib/hecks/ports/clock.port +3 -2
  109. data/lib/hecks/ports/clock.rb +1 -1
  110. data/lib/hecks/ports/extraction.port +3 -2
  111. data/lib/hecks/ports/extraction.rb +1 -1
  112. data/lib/hecks/ports/identity_assignment.port +3 -2
  113. data/lib/hecks/ports/identity_assignment.rb +1 -1
  114. data/lib/hecks/ports/identity_generation.port +3 -2
  115. data/lib/hecks/ports/identity_generation.rb +1 -1
  116. data/lib/hecks/ports/identity_resolution.port +3 -2
  117. data/lib/hecks/ports/identity_resolution.rb +1 -1
  118. data/lib/hecks/ports/persistence/append_only.rb +40 -4
  119. data/lib/hecks/ports/persistence/execution.rb +6 -1
  120. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  121. data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_check.rb +41 -8
  122. data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard.rb +24 -56
  123. data/lib/hecks/ports/persistence/{lineage.rb → plugins/era/lineage.rb} +31 -4
  124. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/era_store.rb +3 -3
  125. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/head_compiler.rb +59 -10
  126. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/mint_transaction.rb +2 -2
  127. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/provisioning.rb +29 -1
  128. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/tail_merge.rb +11 -4
  129. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/transform_installer.rb +20 -0
  130. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage.rb +1 -1
  131. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/coverage_check.rb +5 -5
  132. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/era_resolver.rb +5 -2
  133. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/merge_coordinator.rb +2 -2
  134. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/minter.rb +4 -4
  135. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager.rb +2 -2
  136. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era.rb +116 -29
  137. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/approval_digest.rb +1 -1
  138. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_one.rb +14 -5
  139. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_two.rb +31 -6
  140. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/unfed_report.rb +5 -1
  141. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/reattest.rb +3 -3
  142. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/differ.rb +1 -1
  143. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  144. data/lib/hecks/ports/persistence.rb +1 -1
  145. data/lib/hecks/ports/projection.rb +20 -0
  146. data/lib/hecks/projections/diagrams.rb +230 -1
  147. data/lib/hecks/projections/shape.rb +15 -2
  148. data/lib/hecks/projector/cli_projector.rb +10 -1
  149. data/lib/hecks/projector/exporter.rb +19 -2
  150. data/lib/hecks/query_ir.rb +19 -0
  151. data/lib/hecks/query_specification/common/null_policy.rb +13 -1
  152. data/lib/hecks/query_specification/field_path.rb +20 -2
  153. data/lib/hecks/query_specification/hop_path.rb +7 -5
  154. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  155. data/lib/hecks/runtime/boot_gates.rb +41 -0
  156. data/lib/hecks/runtime/caller.rb +19 -3
  157. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -2
  158. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -0
  159. data/lib/hecks/runtime/command_interpreter.rb +165 -34
  160. data/lib/hecks/runtime/command_rules/admissibility.rb +101 -16
  161. data/lib/hecks/runtime/command_rules/arithmetic.rb +40 -4
  162. data/lib/hecks/runtime/command_rules/authorization.rb +2 -1
  163. data/lib/hecks/runtime/command_rules/references.rb +27 -19
  164. data/lib/hecks/runtime/dependency_planning.rb +66 -5
  165. data/lib/hecks/runtime/dispatcher.rb +19 -4
  166. data/lib/hecks/runtime/entity_interpreter.rb +92 -14
  167. data/lib/hecks/runtime/errors.rb +22 -0
  168. data/lib/hecks/runtime/identity.rb +30 -2
  169. data/lib/hecks/runtime/instance.rb +59 -4
  170. data/lib/hecks/runtime/interpreting.rb +21 -0
  171. data/lib/hecks/runtime/loader.rb +59 -18
  172. data/lib/hecks/runtime/query_interpreter.rb +36 -4
  173. data/lib/hecks/runtime/reaction_invocation.rb +9 -1
  174. data/lib/hecks/runtime/read_model_interpreter.rb +89 -5
  175. data/lib/hecks/runtime/refusal_wording.rb +2 -0
  176. data/lib/hecks/runtime/registry/saga_persistence.rb +75 -3
  177. data/lib/hecks/runtime/registry/verification.rb +88 -0
  178. data/lib/hecks/runtime/registry.rb +69 -8
  179. data/lib/hecks/runtime/saga_interpreter.rb +215 -13
  180. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  181. data/lib/hecks/runtime/value/admission.rb +19 -1
  182. data/lib/hecks/runtime/value/coercion.rb +75 -10
  183. data/lib/hecks/runtime.rb +17 -5
  184. data/lib/hecks/storehouse.rb +632 -0
  185. data/lib/hecks/version.rb +1 -1
  186. data/lib/hecks/vocabulary.rb +6 -1
  187. data/lib/hecks.rb +28 -3
  188. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  189. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  190. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  191. metadata +49 -37
  192. /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard/shape_diff.rb +0 -0
  193. /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_tamper.rb +0 -0
  194. /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/field_cache.rb +0 -0
  195. /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/resumable_backfill.rb +0 -0
  196. /data/lib/hecks/{runtime → ports/persistence/plugins/era}/storage_shape.rb +0 -0
  197. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit.rb +0 -0
  198. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/rule_compiler.rb +0 -0
  199. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/renderer.rb +0 -0
  200. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/writer.rb +0 -0
  201. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold.rb +0 -0
  202. /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, PortVerb
11
- attribute :signal, PortSignal
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
- return word[0..-2] if word.length > 1 && word.end_with?("s")
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
@@ -1,4 +1,9 @@
1
1
  Hecks.port "access_control" do
2
- verb "granted_by"
3
- signal :reply
2
+ verb "granted_by"
3
+ signal :reply
4
+ answers :session_for_identity
5
+ answers :provision
6
+ answers :available_roles
7
+ answers :grant
8
+ answers :all_people
4
9
  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 Adapters.const_get(implementations.first.name)
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"
@@ -1,4 +1,8 @@
1
1
  Hecks.port "agent" do
2
- verb "asked_by"
3
- signal :reply
2
+ verb "asked_by"
3
+ signal :reply
4
+ answers :ask
5
+ answers :interpret
6
+ answers :critique
7
+ answers :suggest_name
4
8
  end
@@ -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 Adapters.const_get(implementations.first.name)
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"
@@ -1,4 +1,6 @@
1
1
  Hecks.port "authentication" do
2
- verb "authenticated_by"
3
- signal :reply
2
+ verb "authenticated_by"
3
+ signal :reply
4
+ answers :authorization_url
5
+ answers :verify
4
6
  end
@@ -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 Adapters.const_get(implementations.first.name)
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"
@@ -1,4 +1,7 @@
1
1
  Hecks.port "authorization" do
2
- verb "authorized_by"
3
- signal :reply
2
+ verb "authorized_by"
3
+ signal :reply
4
+ answers :holds_role?
5
+ answers :authorized_as?
6
+ answers :live_role_for
4
7
  end
@@ -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`, and nothing here is consulted by
19
- # `CommandRules::Authorization`: that rule still only compares an
20
- # ALREADY-BOUND caller's role against a command's, the same as it
21
- # always has. `spec/act_as_spec.rb` remains the precedent for the
22
- # OTHER shape two separate registries, queried directly by name —
23
- # for whenever Governance is not in the same boot as the caller;
24
- # this port is the same two questions asked through one adapter
25
- # when it is.
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 Adapters.const_get(implementations.first.name)
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"
@@ -1,4 +1,5 @@
1
1
  Hecks.port "clock" do
2
- verb "timed_by"
3
- signal :reply
2
+ verb "timed_by"
3
+ signal :reply
4
+ answers :now
4
5
  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 Adapters.const_get(implementations.first.name)
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"
@@ -1,4 +1,5 @@
1
1
  Hecks.port "extraction" do
2
- verb "extracted_by"
3
- signal :reply
2
+ verb "extracted_by"
3
+ signal :reply
4
+ answers :canonical
4
5
  end
@@ -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 Adapters.const_get(implementations.first.name)
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 " \
@@ -1,4 +1,5 @@
1
1
  Hecks.port "identity_assignment" do
2
- verb "assigned_by"
3
- signal :reply
2
+ verb "assigned_by"
3
+ signal :reply
4
+ answers :assign
4
5
  end
@@ -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 Adapters.const_get(implementations.first.name)
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"
@@ -1,4 +1,5 @@
1
1
  Hecks.port "identity_generation" do
2
- verb "generated_by"
3
- signal :reply
2
+ verb "generated_by"
3
+ signal :reply
4
+ answers :uuid
4
5
  end
@@ -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 Adapters.const_get(implementations.first.name)
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"
@@ -1,4 +1,5 @@
1
1
  Hecks.port "identity_resolution" do
2
- verb "resolved_by"
3
- signal :reply
2
+ verb "resolved_by"
3
+ signal :reply
4
+ answers :resolve
4
5
  end
@@ -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 Adapters.const_get(implementations.first.name)
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
- def save(instance)
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
- project(entry)
71
- instance
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(event) = @adapter.record_event(event) if @adapter.respond_to?(: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
- STATUSES = %i[inserted replaced updated conflicted missing saved].freeze
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 "../ports/persistence"
2
- require_relative "../ports/persistence/binding_policy"
3
- require_relative "../ports/persistence/lineage"
4
- require_relative "../naming"
5
- require_relative "../framework"
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