hecks 1.0.2 → 1.1.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.
Files changed (239) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +3 -3
  3. data/lib/hecks/adapters/driven/d1.rb +48 -28
  4. data/lib/hecks/adapters/driven/folder.rb +10 -3
  5. data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
  6. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +3 -5
  8. data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +2 -2
  10. data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
  11. data/lib/hecks/adapters/driven/local_storage.rb +120 -0
  12. data/lib/hecks/adapters/driven/memory.rb +48 -3
  13. data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
  14. data/lib/hecks/adapters/driven/postgres/outbox.rb +97 -0
  15. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +3 -2
  16. data/lib/hecks/adapters/driven/postgres.rb +25 -14
  17. data/lib/hecks/adapters/driven/prism.rb +11 -0
  18. data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
  19. data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
  20. data/lib/hecks/adapters/driven/sqlite/projection.rb +17 -1
  21. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
  22. data/lib/hecks/adapters/driven/sqlite.rb +87 -8
  23. data/lib/hecks/adapters/driven.rb +4 -0
  24. data/lib/hecks/adapters.rb +4 -0
  25. data/lib/hecks/behaviors/dsl.rb +7 -0
  26. data/lib/hecks/behaviors/expectations.rb +32 -4
  27. data/lib/hecks/behaviors/rspec.rb +2 -0
  28. data/lib/hecks/behaviors/runner.rb +4 -0
  29. data/lib/hecks/bluebook/aggregate.rb +6 -5
  30. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
  31. data/lib/hecks/bluebook/assembly/build.rb +6 -3
  32. data/lib/hecks/bluebook/assembly/contract.rb +5 -1
  33. data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
  34. data/lib/hecks/bluebook/assembly/marks.rb +2 -1
  35. data/lib/hecks/bluebook/assembly.rb +1 -3
  36. data/lib/hecks/bluebook/attribute.rb +6 -0
  37. data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
  38. data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
  39. data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
  40. data/lib/hecks/bluebook/command.rb +6 -5
  41. data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
  42. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
  43. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +19 -472
  44. data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
  45. data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
  46. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
  47. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
  48. data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
  49. data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
  50. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
  51. data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
  52. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
  53. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
  54. data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
  55. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
  56. data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
  57. data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
  58. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
  59. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
  60. data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
  61. data/lib/hecks/bluebook/dsl/read_model_builder.rb +11 -3
  62. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
  63. data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
  64. data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
  65. data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
  66. data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
  67. data/lib/hecks/bluebook/entity.rb +5 -3
  68. data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
  69. data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
  70. data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
  71. data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
  72. data/lib/hecks/bluebook/expression/projection.json +12 -0
  73. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
  74. data/lib/hecks/bluebook/expression/resolver.rb +83 -11
  75. data/lib/hecks/bluebook/expression.rb +1 -0
  76. data/lib/hecks/bluebook/hexagon.rb +10 -0
  77. data/lib/hecks/bluebook/lifecycle.rb +9 -0
  78. data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
  79. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
  80. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
  81. data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
  82. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
  83. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
  84. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
  85. data/lib/hecks/bluebook/meta_validator.rb +5 -5
  86. data/lib/hecks/bluebook/model_check.rb +119 -82
  87. data/lib/hecks/bluebook/pattern_subset.rb +9 -0
  88. data/lib/hecks/bluebook/policy.rb +2 -1
  89. data/lib/hecks/bluebook/process_manager.rb +6 -0
  90. data/lib/hecks/bluebook/project_register.rb +45 -25
  91. data/lib/hecks/bluebook/query.rb +4 -0
  92. data/lib/hecks/bluebook/read_model.rb +2 -2
  93. data/lib/hecks/bluebook/smoke_test.rb +5 -0
  94. data/lib/hecks/bluebook/synthesizer.rb +2 -1
  95. data/lib/hecks/bluebook/translation.rb +3 -0
  96. data/lib/hecks/bluebook/value_object.rb +2 -2
  97. data/lib/hecks/codemod.rb +13 -0
  98. data/lib/hecks/doc/reference.rb +17 -11
  99. data/lib/hecks/facade/cli_runner.rb +2 -2
  100. data/lib/hecks/facade/command_request.rb +11 -11
  101. data/lib/hecks/facade/handle.rb +3 -3
  102. data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
  103. data/lib/hecks/facade.rb +4 -0
  104. data/lib/hecks/forms/app.rb +10 -6
  105. data/lib/hecks/forms/command_form_renderer.rb +3 -3
  106. data/lib/hecks/forms/field_renderer.rb +14 -9
  107. data/lib/hecks/forms/field_shape.rb +7 -2
  108. data/lib/hecks/forms/html.rb +5 -2
  109. data/lib/hecks/forms/index_renderer.rb +1 -1
  110. data/lib/hecks/forms/page.rb +7 -3
  111. data/lib/hecks/forms/query_form_renderer.rb +3 -2
  112. data/lib/hecks/forms/record_renderer.rb +22 -16
  113. data/lib/hecks/forms.rb +4 -0
  114. data/lib/hecks/fqn.rb +21 -9
  115. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
  116. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
  117. data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
  118. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
  119. data/lib/hecks/fuzzing/properties/guards.rb +251 -0
  120. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
  121. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
  122. data/lib/hecks/fuzzing/properties/querying.rb +206 -0
  123. data/lib/hecks/fuzzing/properties.rb +41 -1065
  124. data/lib/hecks/fuzzing/replay.rb +60 -16
  125. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
  126. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
  127. data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
  128. data/lib/hecks/fuzzing/value_generator.rb +2 -2
  129. data/lib/hecks/grammar/evolve.rb +34 -10
  130. data/lib/hecks/grammar/expression_operators.json +124 -0
  131. data/lib/hecks/grammar.rb +15 -5
  132. data/lib/hecks/ir.rb +9 -0
  133. data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
  134. data/lib/hecks/language/bluebook/command.bluebook +2 -1
  135. data/lib/hecks/language/bluebook/entity.bluebook +2 -1
  136. data/lib/hecks/language/bluebook/projection.bluebook +2 -1
  137. data/lib/hecks/language/bluebook/query.bluebook +4 -2
  138. data/lib/hecks/language/bluebook/shape.bluebook +2 -1
  139. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
  140. data/lib/hecks/literal.rb +18 -8
  141. data/lib/hecks/naming.rb +6 -3
  142. data/lib/hecks/ports/access_control.rb +1 -1
  143. data/lib/hecks/ports/agent.rb +5 -3
  144. data/lib/hecks/ports/authentication.rb +3 -2
  145. data/lib/hecks/ports/authorization.rb +1 -1
  146. data/lib/hecks/ports/clock.rb +1 -1
  147. data/lib/hecks/ports/extraction.rb +5 -1
  148. data/lib/hecks/ports/identity_assignment.rb +1 -1
  149. data/lib/hecks/ports/identity_generation.rb +1 -1
  150. data/lib/hecks/ports/identity_resolution.rb +1 -1
  151. data/lib/hecks/ports/loading.rb +4 -1
  152. data/lib/hecks/ports/persistence/append_only.rb +40 -4
  153. data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
  154. data/lib/hecks/ports/persistence/plugin.rb +3 -0
  155. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
  156. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
  157. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
  158. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
  159. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
  160. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
  161. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
  162. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
  163. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
  164. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
  165. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
  166. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
  167. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
  168. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +23 -13
  169. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
  170. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
  171. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
  172. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
  173. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
  174. data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
  175. data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
  176. data/lib/hecks/ports/persistence.rb +7 -3
  177. data/lib/hecks/ports/projection.rb +13 -5
  178. data/lib/hecks/ports/query/in_memory.rb +8 -2
  179. data/lib/hecks/ports/query.rb +1 -3
  180. data/lib/hecks/ports.rb +2 -0
  181. data/lib/hecks/projections/diagrams.rb +11 -3
  182. data/lib/hecks/projections/ir.rb +2 -0
  183. data/lib/hecks/projections/model/deviations.rb +2 -1
  184. data/lib/hecks/projections/model.rb +15 -10
  185. data/lib/hecks/projections/vocabulary.rb +1 -3
  186. data/lib/hecks/projector/cli_projector.rb +49 -29
  187. data/lib/hecks/projector/docs_projector.rb +21 -12
  188. data/lib/hecks/projector/exporter.rb +10 -1
  189. data/lib/hecks/projector/narrate_projector.rb +83 -25
  190. data/lib/hecks/projector.rb +2 -2
  191. data/lib/hecks/query_ir.rb +60 -36
  192. data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
  193. data/lib/hecks/query_specification/common/comparators.rb +4 -0
  194. data/lib/hecks/query_specification/common/comparison.rb +8 -1
  195. data/lib/hecks/query_specification/common/dsl.rb +5 -0
  196. data/lib/hecks/query_specification/common/null_policy.rb +6 -2
  197. data/lib/hecks/query_specification/common/options.rb +17 -5
  198. data/lib/hecks/query_specification/field_path.rb +4 -4
  199. data/lib/hecks/query_specification/read_model/specification.rb +5 -2
  200. data/lib/hecks/router/namespace_installer.rb +22 -10
  201. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
  202. data/lib/hecks/runtime/command_interpreter.rb +104 -65
  203. data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
  204. data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
  205. data/lib/hecks/runtime/dependency_planning.rb +33 -21
  206. data/lib/hecks/runtime/dispatcher.rb +41 -7
  207. data/lib/hecks/runtime/entity_element.rb +25 -6
  208. data/lib/hecks/runtime/entity_interpreter.rb +28 -15
  209. data/lib/hecks/runtime/errors.rb +28 -2
  210. data/lib/hecks/runtime/instance.rb +9 -3
  211. data/lib/hecks/runtime/interpreting.rb +29 -2
  212. data/lib/hecks/runtime/loader.rb +21 -0
  213. data/lib/hecks/runtime/outbox.rb +365 -0
  214. data/lib/hecks/runtime/policy_interpreter.rb +32 -17
  215. data/lib/hecks/runtime/query_interpreter.rb +36 -16
  216. data/lib/hecks/runtime/reaction_invocation.rb +35 -22
  217. data/lib/hecks/runtime/read_model_interpreter.rb +26 -3
  218. data/lib/hecks/runtime/reference_hop.rb +1 -1
  219. data/lib/hecks/runtime/refusal_wording.rb +55 -49
  220. data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
  221. data/lib/hecks/runtime/registry/verification.rb +39 -2
  222. data/lib/hecks/runtime/registry.rb +21 -11
  223. data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
  224. data/lib/hecks/runtime/routing.rb +52 -39
  225. data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
  226. data/lib/hecks/runtime/saga_interpreter.rb +119 -90
  227. data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
  228. data/lib/hecks/runtime/tenant_scope.rb +7 -0
  229. data/lib/hecks/runtime/value/coercion.rb +207 -48
  230. data/lib/hecks/runtime/value.rb +1 -1
  231. data/lib/hecks/runtime.rb +7 -2
  232. data/lib/hecks/storehouse.rb +15 -8
  233. data/lib/hecks/version.rb +1 -1
  234. data/lib/hecks/vocabulary.rb +1 -0
  235. data/lib/hecks.rb +17 -13
  236. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
  237. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
  238. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
  239. metadata +14 -2
@@ -57,6 +57,20 @@ module Hecks
57
57
  # DISPATCH_ORDER.
58
58
  GUARD_REFUSAL_CLASSES = [Runtime::GivenNotMet, Runtime::LifecycleRefused].freeze
59
59
 
60
+ # One tightly ordered loop over steps, with several "oracle" snapshots
61
+ # (reaction_mark, fan_out_snapshot, guard_check, mutation_trace) that
62
+ # must be taken at very specific points RELATIVE TO dispatch — see
63
+ # fan_out_snapshot's own comment above for the real, previously-
64
+ # shipped bug this exact before/after ordering fixes. Splitting this
65
+ # into smaller methods would mean threading five-plus oracle-state
66
+ # locals across method boundaries as parameters/return values, and
67
+ # would let a future editor silently reorder a snapshot relative to
68
+ # `dispatch` without any single method looking wrong — the ordering
69
+ # invariant is only visible with the whole sequence in one place.
70
+ # rubocop:disable-next Metrics/AbcSize
71
+ # rubocop:disable-next Metrics/CyclomaticComplexity
72
+ # rubocop:disable-next Metrics/MethodLength
73
+ # rubocop:disable-next Metrics/PerceivedComplexity
60
74
  def call(domain_path, steps, adapter: :memory)
61
75
  # See isolated_boot.rb's own header: resets data/ AND rebinds
62
76
  # persistence to the chosen adapter (Memory by default), since a
@@ -106,8 +120,8 @@ module Hecks
106
120
  begin
107
121
  rows = run_filter(runtime, question)
108
122
  queries << { query: question, rows: rows, instances_at: snapshot_instances(runtime) }
109
- rescue => e
110
- refusals << { verb: filter_label(question), error: e.message }
123
+ rescue StandardError => e
124
+ refusals << { verb: filter_label(question), error: e.message, kind: refusal_kind(e) }
111
125
  end
112
126
  next
113
127
  end
@@ -160,7 +174,7 @@ module Hecks
160
174
  end
161
175
  queries << entry
162
176
 
163
- refusals << { verb: question, error: native_error.message, kind: native_error.class.name } if native_error
177
+ refusals << { verb: question, error: native_error.message, kind: refusal_kind(native_error) } if native_error
164
178
  next
165
179
  end
166
180
 
@@ -256,9 +270,7 @@ module Hecks
256
270
  # so there is no "after" to compare (and #build_mutation_
257
271
  # trace already skipped anything with no mutations to
258
272
  # trace in the first place).
259
- if mutation_trace
260
- mutation_traces << mutation_trace.merge(after: read_mutation_after(runtime, mutation_trace))
261
- end
273
+ mutation_traces << mutation_trace.merge(after: read_mutation_after(runtime, mutation_trace)) if mutation_trace
262
274
  rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
263
275
  # `kind:` — the RAISED CLASS, not re-derived from the message.
264
276
  # `GivenNotMet`/`EnsuresNotMet` share their exact wording
@@ -269,7 +281,7 @@ module Hecks
269
281
  # would misread every one of those as a guard, or a guard as
270
282
  # one of those. The class is unambiguous where the string
271
283
  # is not.
272
- refusals << { verb: step["verb"], error: e.message, kind: e.class.name }
284
+ refusals << { verb: step["verb"], error: e.message, kind: refusal_kind(e) }
273
285
  # ONLY a refusal raised BY THE GUARD ITSELF counts here —
274
286
  # measured, not assumed: a step whose args were simply
275
287
  # incomplete (AbsentArgument, from normalize_args — which
@@ -287,16 +299,17 @@ module Hecks
287
299
  # two guard classes is left OUT of guard_checks entirely —
288
300
  # inconclusive, not a claimed pass.
289
301
  if guard_check && GUARD_REFUSAL_CLASSES.include?(e.class)
290
- guard_checks << guard_check.merge(actual_refused: true, actual_kind: e.class.name)
302
+ guard_checks << guard_check.merge(actual_refused: true,
303
+ actual_kind: e.class.name)
291
304
  end
292
305
  end
293
306
  end
294
307
 
295
308
  instances = snapshot_instances(runtime)
296
309
 
297
- events = runtime.events.map { |event|
310
+ events = runtime.events.map do |event|
298
311
  { name: event.name, aggregate: event.aggregate, id: event.id, payload: event.payload }
299
- }
312
+ end
300
313
 
301
314
  # THE LIVE PROCESS-MANAGER STORE, materialised to inert data —
302
315
  # `{ pm_name => { correlation => { state:, memory: } } }`, the
@@ -397,6 +410,16 @@ module Hecks
397
410
  instances
398
411
  end
399
412
 
413
+ # One early-return chain resolving "which record, if any" (see the
414
+ # long comment above), feeding a two-stage guard replay
415
+ # (enforce_givens then admissible_transition) that must run in that
416
+ # order to mirror the real DISPATCH_ORDER. Splitting the resolution
417
+ # out would still require passing domain_name/aggregate_name/
418
+ # aggregate/command/record back in, for no gain — every early
419
+ # return already reads as "nothing to check" at its own site.
420
+ # rubocop:disable-next Metrics/AbcSize
421
+ # rubocop:disable-next Metrics/CyclomaticComplexity
422
+ # rubocop:disable-next Metrics/PerceivedComplexity
400
423
  def build_guard_check(runtime, verb, args)
401
424
  domain_name, aggregate_name, command_name = Naming.split_verb(verb)
402
425
  return nil unless command_name && !command_name.include?(".")
@@ -412,7 +435,7 @@ module Hecks
412
435
  # counts as something to check now that `admissible_transition`
413
436
  # is reproduced below; skipping it here would just move the
414
437
  # exact gap that call was added to close one line earlier.
415
- has_transition = aggregate.lifecycle && aggregate.lifecycle.transitions_for(command.hecks_name).any?
438
+ has_transition = aggregate.lifecycle&.transitions_for(command.hecks_name)&.any?
416
439
  return nil if command.givens.empty? && !command.from && !has_transition
417
440
 
418
441
  reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
@@ -477,9 +500,17 @@ module Hecks
477
500
  # `nil` for anything out of scope: an aggregate-level command, an
478
501
  # entity command with no mutations at all, or one whose identity
479
502
  # args (parent OR element) don't resolve.
503
+ # Same shape as build_guard_check just above: one early-return chain
504
+ # resolving the entity/element this step's args address (see the
505
+ # comment above), each step depending on the previous one's
506
+ # resolved value. Threading those locals out to smaller methods
507
+ # would cost more than the cop gains.
508
+ # rubocop:disable-next Metrics/AbcSize
509
+ # rubocop:disable-next Metrics/CyclomaticComplexity
510
+ # rubocop:disable-next Metrics/PerceivedComplexity
480
511
  def build_mutation_trace(runtime, verb, args)
481
512
  domain_name, aggregate_name, command_name = Naming.split_verb(verb)
482
- return nil unless command_name && command_name.include?(".")
513
+ return nil unless command_name&.include?(".")
483
514
 
484
515
  aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
485
516
  return nil unless aggregate
@@ -487,7 +518,7 @@ module Hecks
487
518
  entity_name, entity_command_name = command_name.split(".", 2)
488
519
  entity = aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
489
520
  command = entity&.command(entity_command_name)
490
- return nil unless command && command.mutations.any?
521
+ return nil unless command&.mutations&.any?
491
522
 
492
523
  reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
493
524
  parent_id = Runtime::Identity.of(aggregate, args) ||
@@ -634,17 +665,30 @@ module Hecks
634
665
  # Sorted by id ascending regardless — `Ports::Query::Ordering`'s own
635
666
  # header explains why an ask with no declared order still needs
636
667
  # this tier ("the identity tier is what makes an ask total").
668
+ # THE OUTCOME CLASS a recorded refusal row names (C8.2/C8.3,
669
+ # docs/semantics/bluebook-semantics.md): a domain refusal is its own
670
+ # class; an evaluation FAULT — the language refusing to interpret a
671
+ # broken rule or input — is `"Fault"`, the same word the Rust kernel
672
+ # emits (`Refusal::Fault`), never a refusal class and never a raw
673
+ # Ruby exception name.
674
+ def refusal_kind(error)
675
+ error.is_a?(Bluebook::Expression::EvaluationError) ? "Fault" : error.class.name
676
+ end
677
+
637
678
  def run_filter(runtime, filter)
638
679
  aggregate_ref = filter["aggregate"].to_s
639
680
  field = filter["field"].to_s
640
681
  op = filter["op"].to_s
641
682
  value = filter["value"]
642
683
 
643
- raise "unknown query comparator #{op.inspect}" unless FILTER_COMPARATORS.include?(op)
684
+ # A malformed ad-hoc ask is a FAULT (C8.3), not a bare RuntimeError.
685
+ unless FILTER_COMPARATORS.include?(op)
686
+ raise Bluebook::Expression::EvaluationError, "unknown query comparator #{op.inspect}"
687
+ end
644
688
 
645
689
  domain_name, aggregate_name = aggregate_ref.split("::", 2)
646
690
  aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
647
- raise "unknown aggregate #{aggregate_ref.inspect}" unless aggregate
691
+ raise Bluebook::Expression::EvaluationError, "unknown aggregate #{aggregate_ref.inspect}" unless aggregate
648
692
 
649
693
  clause = QuerySpecification::Common::WhereClause.new(field: field, op: op, value: value)
650
694
  records = runtime.registry.repository(domain_name, aggregate).all
@@ -662,7 +706,7 @@ module Hecks
662
706
  # own `filter_label` builds from the same three raw fields, tolerant
663
707
  # of any of them being missing (Ruby's own nil-to-"" interpolation)
664
708
  # the same way that Rust port is.
665
- def filter_label(filter) = "filter #{filter["aggregate"]}.#{filter["field"]} #{filter["op"]}"
709
+ def filter_label(filter) = "filter #{filter['aggregate']}.#{filter['field']} #{filter['op']}"
666
710
  end
667
711
  end
668
712
  end
@@ -8,7 +8,11 @@ module Hecks
8
8
  private
9
9
 
10
10
  def build_catalog(runtime)
11
- creating, instance, entity_commands, queries, entity_queries = [], [], [], [], []
11
+ creating = []
12
+ instance = []
13
+ entity_commands = []
14
+ queries = []
15
+ entity_queries = []
12
16
  read_models = []
13
17
 
14
18
  runtime.registry.bluebooks.each do |domain_name, bluebook|
@@ -54,7 +58,7 @@ module Hecks
54
58
  populators: populators(runtime),
55
59
  # Which aggregates this corpus can actually make one of — the ones
56
60
  # `satisfiable?` is entitled to wait for.
57
- creatable: creating.map { |entry| entry[:aggregate].hecks_name }.to_set }
61
+ creatable: creating.to_set { |entry| entry[:aggregate].hecks_name } }
58
62
  end
59
63
 
60
64
  # Which command, on which aggregate, appends to which entity list — so a
@@ -175,7 +175,8 @@ module Hecks
175
175
  # `:id` the runtime itself never declared as an attribute
176
176
  # either), which still needs exactly the old minting behavior.
177
177
  unless composite_identity?(aggregate)
178
- args[parent_key] ||= identity_shaped(aggregate, aggregate.identified_by, ValueGenerator.random_id(@random), aggregate)
178
+ args[parent_key] ||= identity_shaped(aggregate, aggregate.identified_by, ValueGenerator.random_id(@random),
179
+ aggregate)
179
180
  end
180
181
  else
181
182
  scalar = pick_known(aggregate.hecks_name)
@@ -1,5 +1,4 @@
1
1
  require "fileutils"
2
- require "set"
3
2
  require "tmpdir"
4
3
  require_relative "isolated_boot"
5
4
  require_relative "invalid_value_generator"
@@ -128,9 +128,9 @@ module Hecks
128
128
  # admitted rows — deliberately, to exercise the refusal a `one_of`
129
129
  # exists to enforce, not just its happy path.
130
130
  def invalid_member(value_object, random:)
131
- value_object.attributes.to_h { |field|
131
+ value_object.attributes.to_h do |field|
132
132
  [field.name.to_s, primitive(field.type.to_s, random: random, name: field.name.to_s)]
133
- }
133
+ end
134
134
  end
135
135
 
136
136
  # THE ID ITSELF. This minted `{"value" => id}` back when a reference was
@@ -83,7 +83,12 @@ module Hecks
83
83
  end
84
84
 
85
85
  def propose(word:, context:, body: "none", inner: "", opens: "", fills: "", path: nil)
86
- raise Refusal, "#{context}.#{word} is already declared — one row per (word, context, form)" if keyword_rows(path).any? { |row| row[:word] == word && row[:context] == context }
86
+ if keyword_rows(path).any? do |row|
87
+ row[:word] == word && row[:context] == context
88
+ end
89
+ raise Refusal,
90
+ "#{context}.#{word} is already declared — one row per (word, context, form)"
91
+ end
87
92
 
88
93
  path = owner_path(context: context, word: word, opens: opens, paths: paths_for(path))
89
94
  source = File.read(path)
@@ -101,7 +106,8 @@ module Hecks
101
106
  end
102
107
 
103
108
  def set_status(word:, context:, to:, path: nil)
104
- raise Refusal, "#{to.inspect} is not a station a word's life admits" unless %w[proposed admitted deprecated retired].include?(to)
109
+ raise Refusal, "#{to.inspect} is not a station a word's life admits" unless %w[proposed admitted deprecated
110
+ retired].include?(to)
105
111
 
106
112
  path = path_holding_keyword(word, context, paths_for(path))
107
113
  source = File.read(path)
@@ -131,7 +137,12 @@ module Hecks
131
137
  row = keyword_rows(path).find { |r| r[:word] == word && r[:context] == context }
132
138
  raise Refusal, "#{context}.#{word} is not declared" unless row
133
139
  raise Refusal, "#{context}.#{word} was already #{row[:was]} — one rename hop, then eras" if row[:was]
134
- raise Refusal, "#{context}.#{to} is already declared — a rename cannot land on a living word" if keyword_rows(path).any? { |r| r[:word] == to && r[:context] == context }
140
+ if keyword_rows(path).any? do |r|
141
+ r[:word] == to && r[:context] == context
142
+ end
143
+ raise Refusal,
144
+ "#{context}.#{to} is already declared — a rename cannot land on a living word"
145
+ end
135
146
 
136
147
  paths = paths_for(path)
137
148
  path = path_holding_keyword(word, context, paths)
@@ -161,7 +172,11 @@ module Hecks
161
172
 
162
173
  def path_holding_argument(keyword, context, at, named, paths = syntax_paths)
163
174
  paths.find do |candidate|
164
- argument_blocks(File.read(candidate)).any? { |block| block.lines.any? { |line| argument_row?(line, keyword, context, at, named) } }
175
+ argument_blocks(File.read(candidate)).any? do |block|
176
+ block.lines.any? do |line|
177
+ argument_row?(line, keyword, context, at, named)
178
+ end
179
+ end
165
180
  end || raise(Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is not declared")
166
181
  end
167
182
 
@@ -251,7 +266,9 @@ module Hecks
251
266
 
252
267
  path = owner_path(context: context, word: keyword, paths: paths_for(path))
253
268
  source = File.read(path)
254
- block = argument_blocks(source).find { |candidate| candidate.include?(%(context: "#{context}")) } || argument_block(source)
269
+ block = argument_blocks(source).find do |candidate|
270
+ candidate.include?(%(context: "#{context}"))
271
+ end || argument_block(source)
255
272
  indent = block[/^(\s*)member /, 1] || " "
256
273
  shape = pairs_shape.to_s.empty? ? "" : %(pairs_shape: "#{pairs_shape}", )
257
274
  row = %(#{indent}member keyword: "#{keyword}", context: "#{context}", at: "#{at}", ) +
@@ -264,14 +281,21 @@ module Hecks
264
281
  end
265
282
 
266
283
  def set_argument_status(keyword:, context:, to:, at: "", named: "", path: nil)
267
- raise Refusal, "#{to.inspect} is not a station an argument's life admits" unless %w[proposed admitted deprecated retired].include?(to)
284
+ raise Refusal, "#{to.inspect} is not a station an argument's life admits" unless %w[proposed admitted deprecated
285
+ retired].include?(to)
268
286
 
269
287
  path = path_holding_argument(keyword, context, at, named, paths_for(path))
270
288
  source = File.read(path)
271
- block = argument_blocks(source).find { |candidate| candidate.lines.any? { |line| argument_row?(line, keyword, context, at, named) } }
272
- rows = block.lines.select { |line| argument_row?(line, keyword, context, at, named) }
273
- raise Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is not " \
274
- "declared" if rows.empty?
289
+ block = argument_blocks(source).find do |candidate|
290
+ candidate.lines.any? do |line|
291
+ argument_row?(line, keyword, context, at, named)
292
+ end
293
+ end
294
+ rows = block.lines.select { |line| argument_row?(line, keyword, context, at, named) }
295
+ if rows.empty?
296
+ raise Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is not " \
297
+ "declared"
298
+ end
275
299
 
276
300
  updated = block.lines.map do |line|
277
301
  next line unless argument_row?(line, keyword, context, at, named)
@@ -1643,6 +1643,130 @@
1643
1643
  "value": "length_to_size"
1644
1644
  }
1645
1645
  }
1646
+ },
1647
+ {
1648
+ "verb": "Expression::Operator.Propose",
1649
+ "args": {
1650
+ "symbol": {
1651
+ "value": ".set?"
1652
+ },
1653
+ "category": {
1654
+ "value": "presence"
1655
+ },
1656
+ "precedence": {
1657
+ "value": 7
1658
+ },
1659
+ "arity": {
1660
+ "value": 1
1661
+ },
1662
+ "grammar": {
1663
+ "value": "inner"
1664
+ },
1665
+ "strategy": {
1666
+ "value": "suffix_match"
1667
+ },
1668
+ "position": {
1669
+ "value": 21
1670
+ }
1671
+ }
1672
+ },
1673
+ {
1674
+ "verb": "Expression::Operator.Render",
1675
+ "args": {
1676
+ "symbol": {
1677
+ "value": ".set?"
1678
+ },
1679
+ "target": {
1680
+ "value": "ruby"
1681
+ },
1682
+ "form": {
1683
+ "value": "a.set?"
1684
+ }
1685
+ }
1686
+ },
1687
+ {
1688
+ "verb": "Expression::Operator.Render",
1689
+ "args": {
1690
+ "symbol": {
1691
+ "value": ".set?"
1692
+ },
1693
+ "target": {
1694
+ "value": "rust"
1695
+ },
1696
+ "form": {
1697
+ "value": "a.set? -> !nil?(a)"
1698
+ }
1699
+ }
1700
+ },
1701
+ {
1702
+ "verb": "Expression::Operator.Admit",
1703
+ "args": {
1704
+ "symbol": {
1705
+ "value": ".set?"
1706
+ }
1707
+ }
1708
+ },
1709
+ {
1710
+ "verb": "Expression::Operator.Propose",
1711
+ "args": {
1712
+ "symbol": {
1713
+ "value": ".unset?"
1714
+ },
1715
+ "category": {
1716
+ "value": "presence"
1717
+ },
1718
+ "precedence": {
1719
+ "value": 7
1720
+ },
1721
+ "arity": {
1722
+ "value": 1
1723
+ },
1724
+ "grammar": {
1725
+ "value": "inner"
1726
+ },
1727
+ "strategy": {
1728
+ "value": "suffix_match"
1729
+ },
1730
+ "position": {
1731
+ "value": 22
1732
+ }
1733
+ }
1734
+ },
1735
+ {
1736
+ "verb": "Expression::Operator.Render",
1737
+ "args": {
1738
+ "symbol": {
1739
+ "value": ".unset?"
1740
+ },
1741
+ "target": {
1742
+ "value": "ruby"
1743
+ },
1744
+ "form": {
1745
+ "value": "a.unset?"
1746
+ }
1747
+ }
1748
+ },
1749
+ {
1750
+ "verb": "Expression::Operator.Render",
1751
+ "args": {
1752
+ "symbol": {
1753
+ "value": ".unset?"
1754
+ },
1755
+ "target": {
1756
+ "value": "rust"
1757
+ },
1758
+ "form": {
1759
+ "value": "a.unset? -> nil?(a)"
1760
+ }
1761
+ }
1762
+ },
1763
+ {
1764
+ "verb": "Expression::Operator.Admit",
1765
+ "args": {
1766
+ "symbol": {
1767
+ "value": ".unset?"
1768
+ }
1769
+ }
1646
1770
  }
1647
1771
  ]
1648
1772
  }
data/lib/hecks/grammar.rb CHANGED
@@ -43,9 +43,9 @@ module Hecks
43
43
  args = symbolize(step.fetch("args"))
44
44
  begin
45
45
  dispatcher.dispatch(step.fetch("verb"), **args)
46
- rescue *Runtime::DOMAIN_REFUSALS => refusal
46
+ rescue *Runtime::DOMAIN_REFUSALS => e
47
47
  raise Runtime::WiringError,
48
- "the admission ledger refused at #{step['verb']} #{step['args']} — #{refusal.message}"
48
+ "the admission ledger refused at #{step['verb']} #{step['args']} — #{e.message}"
49
49
  end
50
50
  end
51
51
 
@@ -119,7 +119,7 @@ module Hecks
119
119
  # Every grammar/*.bluebook chapter, booted the same way the corpus
120
120
  # boots them — each alone, in a scratch registry.
121
121
  def grammar_chapters
122
- Dir[File.join(DIR, "*.bluebook")].sort.map do |chapter|
122
+ Dir[File.join(DIR, "*.bluebook")].map do |chapter|
123
123
  registry = Runtime::Registry.new
124
124
  root = File.expand_path("../..", __dir__)
125
125
  Hecks.with_registry(registry) do
@@ -138,14 +138,24 @@ module Hecks
138
138
  # walked for the resolver's arithmetic.
139
139
  def operators_in(canonical)
140
140
  evaluator = Bluebook::Expression::Evaluator
141
- node = begin
142
- evaluator.parse(canonical)
141
+ begin
142
+ node = evaluator.parse(canonical)
143
143
  rescue StandardError
144
144
  return []
145
145
  end
146
146
  walk_operators(node, evaluator).uniq
147
147
  end
148
148
 
149
+ # A recursive descent over a CLOSED, declared set of AST node types
150
+ # (Evaluator's boolean/compare/include nodes, Resolver's arithmetic
151
+ # nodes, and the generic Struct fallback) — each branch does the
152
+ # same one thing (name the node's own operator, recurse into its
153
+ # children) and the branches don't interact, but the ABC score adds
154
+ # every branch's cost together. Splitting the case out per node type
155
+ # would trade one place that shows the whole operator vocabulary for
156
+ # several that each show a fragment, with no reduction in real
157
+ # complexity.
158
+ # rubocop:disable-next Metrics/AbcSize
149
159
  def walk_operators(node, evaluator)
150
160
  resolver = Bluebook::Expression::Resolver
151
161
  case node
data/lib/hecks/ir.rb CHANGED
@@ -73,6 +73,11 @@ module Hecks
73
73
  # crash on an undeclared lifecycle.
74
74
  One = Struct.new(:source)
75
75
 
76
+ # The declaration side: `emits_ir(**spec)` records a construct's field ->
77
+ # rule map, `many`/`one` wrap a source name so a field can recurse into a
78
+ # list or a single nested construct, and `ir_spec` reads the declaration
79
+ # back — walking the superclass chain so an anonymous `Class.new(base)`
80
+ # inherits its base's shape instead of redeclaring it.
76
81
  module Declares
77
82
  def emits_ir(**spec)
78
83
  @ir_spec = spec
@@ -92,6 +97,10 @@ module Hecks
92
97
  end
93
98
  end
94
99
 
100
+ # The emission side: `to_h` reads the declaring construct's `ir_spec` and
101
+ # applies each field's rule (`Many#to_h`, `One&.to_h`, a plain Symbol
102
+ # send, or an instance-`exec`'d Proc) in declaration order to build the
103
+ # actual Hash.
95
104
  module Emits
96
105
  def to_h
97
106
  spec = ir_spec_for(self)
@@ -83,7 +83,8 @@ Hecks.bluebook "Bluebook" do
83
83
  # Without this an aggregate attribute's default was simply dropped, and the
84
84
  # reconstruction hardcoded nil, which happened to match every corpus member
85
85
  # and would have lied about the first one that declared a default.
86
- attribute :default, String
86
+ # optional: a declaration with no default has none (C3.7 — a value object's fields arrive or it refuses)
87
+ attribute :default, String, optional: true
87
88
  # THE CLOSED SET THIS VALUE MUST BELONG TO, named where it was already
88
89
  # declared — `Vocabulary::QueryComparator`. Qualified by the aggregate that
89
90
  # holds it, because a closed set is a value object INSIDE an aggregate and
@@ -103,7 +104,8 @@ Hecks.bluebook "Bluebook" do
103
104
 
104
105
  value_object "Transition" do
105
106
  attribute :command, String
106
- attribute :from_state, String
107
+ # optional: a transition declared with no `from:` matches any state (C3.7)
108
+ attribute :from_state, String, optional: true
107
109
  attribute :to_state, String
108
110
  end
109
111
 
@@ -82,7 +82,8 @@ Hecks.bluebook "Bluebook" do
82
82
  attribute :list, String
83
83
  attribute :optional, String, optional: true
84
84
  attribute :pattern, String, optional: true
85
- attribute :default, String
85
+ # optional: a declaration with no default has none (C3.7 — a value object's fields arrive or it refuses)
86
+ attribute :default, String, optional: true
86
87
  # The closed set this value must belong to — see Aggregate::Field, which is
87
88
  # the same declaration, spelled for a command's arguments.
88
89
  attribute :admits, String, optional: true
@@ -61,7 +61,8 @@ Hecks.bluebook "Bluebook" do
61
61
  attribute :list, String
62
62
  attribute :optional, String, optional: true
63
63
  attribute :pattern, String, optional: true
64
- attribute :default, String
64
+ # optional: a declaration with no default has none (C3.7 — a value object's fields arrive or it refuses)
65
+ attribute :default, String, optional: true
65
66
  # The closed set this value must belong to — see Aggregate::Field.
66
67
  attribute :admits, String, optional: true
67
68
  attribute :relationship, String, optional: true
@@ -60,7 +60,8 @@ Hecks.bluebook "Bluebook" do
60
60
  attribute :option, String
61
61
  attribute :key, String
62
62
  attribute :value, String
63
- attribute :at, String
63
+ # optional: not every projection option carries an `at` (C3.7)
64
+ attribute :at, String, optional: true
64
65
  end
65
66
 
66
67
  value_object "GroupByField" do
@@ -59,7 +59,8 @@ Hecks.bluebook "Bluebook" do
59
59
  attribute :list, String
60
60
  attribute :optional, String, optional: true
61
61
  attribute :pattern, String, optional: true
62
- attribute :default, String
62
+ # optional: a declaration with no default has none (C3.7 — a value object's fields arrive or it refuses)
63
+ attribute :default, String, optional: true
63
64
  # The closed set this value must belong to — see Aggregate::Field.
64
65
  attribute :admits, String, optional: true
65
66
  attribute :relationship, String, optional: true
@@ -74,7 +75,8 @@ Hecks.bluebook "Bluebook" do
74
75
  attribute :option, String
75
76
  attribute :key, String
76
77
  attribute :value, String
77
- attribute :at, String
78
+ # optional: only a repeated option (an index hint) carries an `at` (C3.7)
79
+ attribute :at, String, optional: true
78
80
  end
79
81
 
80
82
  # The way back: everything DECLARED IN the aggregate above. The parent
@@ -34,7 +34,8 @@ Hecks.bluebook "Bluebook" do
34
34
  attribute :list, String, pattern: '[^ \t\n\r]'
35
35
  attribute :optional, String, optional: true
36
36
  attribute :pattern, String, optional: true
37
- attribute :default, String
37
+ # optional: a declaration with no default has none (C3.7 — a value object's fields arrive or it refuses)
38
+ attribute :default, String, optional: true
38
39
  # The closed set this value must belong to — see Aggregate::Field.
39
40
  attribute :admits, String, optional: true
40
41
  attribute :relationship, String, optional: true
@@ -294,6 +294,8 @@ Hecks.bluebook "Bluebook" do
294
294
  template: "{type}.{field} expects {expected}, got {offered}"
295
295
  member refusal: "TypeMismatch", site: "non_finite_field",
296
296
  template: "{type}.{field} must be a finite number, got {offered}"
297
+ member refusal: "TypeMismatch", site: "integer_range",
298
+ template: "{type}.{field} must fit in a 64-bit integer, got {offered}"
297
299
  member refusal: "TypeMismatch", site: "pattern_mismatch",
298
300
  template: "{type}.{field} must match {pattern}, got {offered}"
299
301
  member refusal: "TypeMismatch", site: "arithmetic_amount",