hecks 1.4.0 → 1.5.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 (267) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  112. data/lib/hecks/doc/reference.rb +185 -16
  113. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  114. data/lib/hecks/facade/handle.rb +76 -3
  115. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  116. data/lib/hecks/forms/field_shape.rb +3 -0
  117. data/lib/hecks/forms/page.rb +14 -0
  118. data/lib/hecks/forms/port_argument.rb +12 -0
  119. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  120. data/lib/hecks/forms/record_renderer.rb +58 -0
  121. data/lib/hecks/forms/record_table.rb +27 -0
  122. data/lib/hecks/forms/reference_options.rb +24 -0
  123. data/lib/hecks/forms/value_object_shape.rb +10 -0
  124. data/lib/hecks/fqn.rb +58 -0
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  126. data/lib/hecks/framework/oidc.json +15 -0
  127. data/lib/hecks/framework.rb +43 -20
  128. data/lib/hecks/freezer.rb +17 -1
  129. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  130. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  131. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  132. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  133. data/lib/hecks/fuzzing/differential.rb +34 -0
  134. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  135. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  136. data/lib/hecks/fuzzing/form_census.rb +86 -0
  137. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  138. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  139. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  140. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  141. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  142. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  143. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  144. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  145. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  146. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  147. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  148. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  149. data/lib/hecks/fuzzing/properties.rb +24 -15
  150. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  151. data/lib/hecks/fuzzing/replay.rb +137 -29
  152. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  153. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  154. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  155. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  156. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  157. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  158. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  159. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  160. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  161. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  162. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  163. data/lib/hecks/grammar/evolve.rb +178 -2
  164. data/lib/hecks/grammar.rb +46 -0
  165. data/lib/hecks/ir.rb +38 -7
  166. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  167. data/lib/hecks/literal.rb +32 -0
  168. data/lib/hecks/naming.rb +88 -7
  169. data/lib/hecks/ports/access_control.rb +5 -10
  170. data/lib/hecks/ports/authorization.rb +3 -6
  171. data/lib/hecks/ports/identity_assignment.rb +1 -2
  172. data/lib/hecks/ports/identity_resolution.rb +1 -2
  173. data/lib/hecks/ports/key_vault.port +6 -0
  174. data/lib/hecks/ports/key_vault.rb +58 -0
  175. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  176. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  177. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  178. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  179. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  180. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  182. data/lib/hecks/ports/query/in_memory.rb +39 -6
  183. data/lib/hecks/ports/query/ordering.rb +15 -0
  184. data/lib/hecks/ports.rb +1 -0
  185. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  186. data/lib/hecks/projections/diagrams.rb +243 -7
  187. data/lib/hecks/projections/glossary/html.rb +88 -0
  188. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  189. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  190. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  191. data/lib/hecks/projections/glossary.rb +161 -19
  192. data/lib/hecks/projections/model/deviations.rb +44 -0
  193. data/lib/hecks/projections/model.rb +51 -1
  194. data/lib/hecks/projections/oidc.rb +40 -3
  195. data/lib/hecks/projections/parser_table.rb +49 -8
  196. data/lib/hecks/projections/reference.rb +12 -0
  197. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  198. data/lib/hecks/projections/shape.rb +8 -0
  199. data/lib/hecks/projections/statements.rb +63 -16
  200. data/lib/hecks/projections/vocabulary.rb +17 -0
  201. data/lib/hecks/projector/cli_projector.rb +218 -10
  202. data/lib/hecks/projector/docs_projector.rb +145 -19
  203. data/lib/hecks/projector/exporter.rb +65 -11
  204. data/lib/hecks/projector/ir_projector.rb +6 -0
  205. data/lib/hecks/projector/narrate_projector.rb +136 -15
  206. data/lib/hecks/projector/target.rb +29 -5
  207. data/lib/hecks/projector.rb +74 -6
  208. data/lib/hecks/query_ir.rb +47 -0
  209. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  210. data/lib/hecks/rendering.rb +6 -0
  211. data/lib/hecks/router/namespace_installer.rb +13 -0
  212. data/lib/hecks/router.rb +55 -0
  213. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  214. data/lib/hecks/runtime/boot_gates.rb +18 -0
  215. data/lib/hecks/runtime/caller.rb +32 -0
  216. data/lib/hecks/runtime/capability_graph.rb +11 -0
  217. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  218. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  219. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  220. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  221. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  222. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  223. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  224. data/lib/hecks/runtime/dispatcher.rb +28 -50
  225. data/lib/hecks/runtime/entity_element.rb +161 -8
  226. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  227. data/lib/hecks/runtime/errors.rb +18 -4
  228. data/lib/hecks/runtime/event.rb +10 -5
  229. data/lib/hecks/runtime/identity.rb +71 -3
  230. data/lib/hecks/runtime/instance.rb +67 -7
  231. data/lib/hecks/runtime/interpreting.rb +13 -5
  232. data/lib/hecks/runtime/invocation.rb +118 -36
  233. data/lib/hecks/runtime/loader.rb +94 -8
  234. data/lib/hecks/runtime/outbox.rb +145 -7
  235. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  236. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  237. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  238. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  239. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  240. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  241. data/lib/hecks/runtime/reference_hop.rb +42 -0
  242. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  243. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  244. data/lib/hecks/runtime/registry/verification.rb +119 -4
  245. data/lib/hecks/runtime/registry.rb +157 -4
  246. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  247. data/lib/hecks/runtime/routing.rb +27 -2
  248. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  249. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  250. data/lib/hecks/runtime/tenant_check.rb +26 -6
  251. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  252. data/lib/hecks/runtime/value/coercion.rb +255 -33
  253. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  254. data/lib/hecks/runtime/value.rb +50 -0
  255. data/lib/hecks/runtime.rb +32 -0
  256. data/lib/hecks/storehouse.rb +305 -9
  257. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  258. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  259. data/lib/hecks/version.rb +1 -1
  260. data/lib/hecks.rb +79 -1
  261. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  262. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  263. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  264. metadata +11 -5
  265. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  266. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  267. data/lib/hecks/deprecation.rb +0 -95
@@ -61,10 +61,10 @@ module Hecks
61
61
  # One tightly ordered loop over steps, with several "oracle" snapshots
62
62
  # (reaction_mark, fan_out_snapshot, guard_check, mutation_trace) that
63
63
  # must be taken at very specific points relative to dispatch — see
64
- # fan_out_snapshot's own comment above for the real, previously-
65
- # shipped bug this exact before/after ordering fixes. Splitting this
66
- # into smaller methods would mean threading five-plus oracle-state
67
- # locals across method boundaries as parameters/return values, and
64
+ # fan_out_snapshot's own comment above for the real bug this exact
65
+ # before/after ordering fixes. Splitting this into smaller methods
66
+ # would mean threading five-plus oracle-state locals across method
67
+ # boundaries as parameters/return values, and
68
68
  # would let a future editor silently reorder a snapshot relative to
69
69
  # `dispatch` without any single method looking wrong — the ordering
70
70
  # invariant is only visible with the whole sequence in one place.
@@ -91,6 +91,22 @@ module Hecks
91
91
  # identity rather than a shared default the way `:postgres` does.
92
92
  # Forwarded straight through, unchanged, exactly like `adapter:`
93
93
  # itself already was.
94
+ #
95
+ # @param domain_path [String] filesystem path to the domain directory to replay
96
+ # @param steps [Array<Hash>] the step list to dispatch, any-keyed (normalized to
97
+ # String keys internally)
98
+ # @param adapter [Symbol] persistence adapter to boot the copy with
99
+ # @param database [String, nil] PostgresEra database name; required, only meaningful
100
+ # when `adapter: :postgres_era`
101
+ # @param schema [String, nil] PostgresEra schema name; required, only meaningful
102
+ # when `adapter: :postgres_era`
103
+ # @param self_consistency [Boolean] whether to run `SelfConsistency.check` against
104
+ # this replay's own runtime before returning
105
+ # @return [Hash] the replay history: `:instances`, `:events`, `:refusals`,
106
+ # `:reactions`, `:sagas`, `:saga_instances`, `:queries`, `:dry_runs`,
107
+ # `:dry_run_traces`, `:fan_outs`, `:guard_checks`, `:mutation_traces`,
108
+ # `:outbox_traces`, `:saga_dispatches`, `:policy_dispatches`, `:bluebook`,
109
+ # `:bluebooks`, and (when `self_consistency:` is true) `:self_consistency`
94
110
  def call(domain_path, steps, adapter: :memory, database: nil, schema: nil, self_consistency: false)
95
111
  # See isolated_boot.rb's own header: resets data/ and rebinds
96
112
  # persistence to the chosen adapter (Memory by default), since a
@@ -457,9 +473,16 @@ module Hecks
457
473
  end
458
474
  end
459
475
 
476
+ # Runs the block with this step's `role:`/`actor_id:` bound as the
477
+ # ambient caller, if it declares one.
478
+ #
460
479
  # A step with no `role:` dispatches exactly as every corpus step
461
480
  # always has — bare, no caller bound at all (`Caller.current` nil,
462
481
  # so `refuse_role_mismatch` returns before checking anything).
482
+ #
483
+ # @param step [Hash] a String-keyed step hash, read for `"role"`/`"actor_id"`
484
+ # @yield the step's own dispatch or dry-run call
485
+ # @return [Object] whatever the block returns
463
486
  def as_step_caller(step, &)
464
487
  return yield unless step["role"]
465
488
 
@@ -491,15 +514,14 @@ module Hecks
491
514
  # reference) become the step's own real dispatch outcome — this
492
515
  # is a separate, best-effort read, not part of the step's own
493
516
  # control flow.
494
- # The same shape `call`'s own end-of-replay block used to build
495
- # inline — every persisted record, keyed the way `query_eligible_rows`/
496
- # `#eligible_rows` (properties.rb) already expect. Now also called
497
- # once per query step (see `call`, above), not only once at the very
498
- # end: a query asked at step 1 of a script whose later steps go on
499
- # to create more records was being checked, by every property that
500
- # independently recomputes "the eligible rows," against the final
501
- # snapshot — the records that existed after the whole replay, not
502
- # the ones that existed when the query actually ran. Found live:
517
+ # Every persisted record, keyed the way `query_eligible_rows`/
518
+ # `#eligible_rows` (properties.rb) already expect. Called once per
519
+ # query step (see `call`, above), not only once at the very end,
520
+ # because a query asked at step 1 of a script whose later steps go on
521
+ # to create more records needs to be checked, by every property that
522
+ # independently recomputes "the eligible rows," against the state as
523
+ # of that query — not the final snapshot after the whole replay.
524
+ # Found live:
503
525
  # `Banking.accounts_by_kind`, asked as literally the first step of a
504
526
  # 3-step script, correctly answered against zero accounts (none
505
527
  # existed yet) while `group_by_matches_recompute`'s own independent
@@ -508,6 +530,11 @@ module Hecks
508
530
  # its own `instances_at:` snapshot, taken at the moment it ran, so
509
531
  # every property that recomputes against "the eligible rows" reads
510
532
  # the state as that query actually saw it, not a shared final one.
533
+ #
534
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
535
+ # to read every persisted record from
536
+ # @return [Hash{String => Hash}] every persisted record's state, keyed by
537
+ # `"Domain::Aggregate#id"`
511
538
  def snapshot_instances(runtime)
512
539
  instances = {}
513
540
  runtime.registry.bluebooks.each do |domain_name, bluebook|
@@ -530,6 +557,15 @@ module Hecks
530
557
  # rubocop:disable-next Metrics/AbcSize
531
558
  # rubocop:disable-next Metrics/CyclomaticComplexity
532
559
  # rubocop:disable-next Metrics/PerceivedComplexity
560
+ #
561
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
562
+ # to resolve the step's own verb and record against
563
+ # @param verb [String] the step's own command verb
564
+ # @param args [Hash{Symbol => Object}] the step's own args, symbol-keyed
565
+ # @return [Hash, nil] `{verb:, domain:, aggregate:, command:, id:, recomputed_refused:,
566
+ # recomputed_kind:}` if there was a record and a guard to check; nil for anything
567
+ # out of scope (an entity/port verb, a creating command, an unresolvable id, or a
568
+ # command with nothing to check)
533
569
  def build_guard_check(runtime, verb, args)
534
570
  domain_name, aggregate_name, command_name = Naming.split_verb(verb)
535
571
  return nil unless command_name && !command_name.include?(".")
@@ -618,6 +654,14 @@ module Hecks
618
654
  # rubocop:disable-next Metrics/AbcSize
619
655
  # rubocop:disable-next Metrics/CyclomaticComplexity
620
656
  # rubocop:disable-next Metrics/PerceivedComplexity
657
+ #
658
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
659
+ # to resolve the step's own verb and record against
660
+ # @param verb [String] the step's own command verb
661
+ # @param args [Hash{Symbol => Object}] the step's own args, symbol-keyed
662
+ # @return [Hash, nil] `{verb:, domain:, aggregate:, command:, parent_id:, list_attr:,
663
+ # element_wants:, before:, args:}` if this is an entity-dispatched command with
664
+ # mutations acting on a resolvable element; nil for anything out of scope
621
665
  def build_mutation_trace(runtime, verb, args)
622
666
  domain_name, aggregate_name, command_name = Naming.split_verb(verb)
623
667
  return nil unless command_name&.include?(".")
@@ -660,12 +704,22 @@ module Hecks
660
704
  nil
661
705
  end
662
706
 
707
+ # Re-reads the same entity element, after dispatch, for comparison against
708
+ # `trace[:before]`.
709
+ #
663
710
  # The same element, re-located, after dispatch — by identity, not
664
711
  # position (an append could have changed the array's own length
665
712
  # or order relative to it). `nil` if it somehow vanished (not
666
713
  # expected for any op this fixture declares — none of them
667
714
  # remove the acted-on element itself — but a property comparing
668
715
  # against `nil` fails loudly rather than crashing this replay).
716
+ #
717
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
718
+ # dispatch just ran against
719
+ # @param trace [Hash] the mutation trace `build_mutation_trace` built for this step,
720
+ # read for `:domain`, `:aggregate`, `:parent_id`, `:list_attr`, `:element_wants`
721
+ # @return [Hash, nil] the element's materialized state after dispatch, or nil if
722
+ # the parent record, its aggregate, or the element itself can no longer be found
669
723
  def read_mutation_after(runtime, trace)
670
724
  aggregate = runtime.registry.bluebook(trace[:domain])&.aggregate(trace[:aggregate])
671
725
  return nil unless aggregate
@@ -696,6 +750,15 @@ module Hecks
696
750
  # apart. Recomputed once per event, not once per policy-and-event,
697
751
  # because a `Chapter` de-duplicates on nothing this loop cannot
698
752
  # cheaply repeat.
753
+ #
754
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
755
+ # @param snapshot [Hash{Array(String, String) => Hash}] the pre-dispatch `for_each`
756
+ # query snapshot, keyed by `[query_domain, aggregate_name]`
757
+ # @param announced [Array<Runtime::Event>] the events this step's own dispatch produced
758
+ # @param reactions_since [Array<Hash>] the reaction-log rows this step's own
759
+ # dispatch appended
760
+ # @return [Array<Hash>] one `{policy:, on:, expected_row_ids:, actual_row_ids:}`
761
+ # finding per (event, fanning-out policy) pair
699
762
  def fan_out_findings(runtime, snapshot, announced, reactions_since)
700
763
  announced.each_with_object([]) do |event, findings|
701
764
  # `event.aggregate` is domain-qualified ("Banking::Account" —
@@ -718,6 +781,19 @@ module Hecks
718
781
  end
719
782
  end
720
783
 
784
+ # Builds one fan-out finding, comparing one `for_each` policy's independently
785
+ # recomputed expected rows against what actually reacted for one event.
786
+ #
787
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
788
+ # @param snapshot [Hash{Array(String, String) => Hash}] the pre-dispatch `for_each`
789
+ # query snapshot, keyed by `[query_domain, aggregate_name]`
790
+ # @param policy [Bluebook::Policy] the fanning-out policy this event could trigger
791
+ # @param event [Runtime::Event] the announced event being checked
792
+ # @param domain [String] the emitting event's own domain name
793
+ # @param reactions_since [Array<Hash>] the reaction-log rows this step's own
794
+ # dispatch appended
795
+ # @return [Hash{Symbol => Object}] `{policy: String, on: String,
796
+ # expected_row_ids: Array<String>, nil, actual_row_ids: Array}`
721
797
  def fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
722
798
  payload = event.payload.transform_keys(&:to_sym)
723
799
  held = policy.where.to_s.empty? ||
@@ -743,6 +819,19 @@ module Hecks
743
819
  # the triggering event's own payload (the same binding
744
820
  # `OpenForCustomer`'s `customer_id: :customer_id` relies on); a
745
821
  # literal is compared as declared.
822
+ #
823
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
824
+ # to resolve the target query against
825
+ # @param snapshot [Hash{Array(String, String) => Hash}] the pre-dispatch `for_each`
826
+ # query snapshot, keyed by `[query_domain, aggregate_name]`
827
+ # @param policy [Bluebook::Policy] the fanning-out policy whose `for_each` route
828
+ # names the target query
829
+ # @param domain [String] the emitting event's own domain name, resolved through
830
+ # `policy.for_each_route`
831
+ # @param payload [Hash] the triggering event's own payload, symbol-keyed; binds any
832
+ # `Symbol` where-value
833
+ # @return [Array<String>] matching row ids, as strings, sorted; `[]` if the target
834
+ # query cannot be resolved
746
835
  def expected_fan_out_rows(runtime, snapshot, policy, domain, payload)
747
836
  query_domain, aggregate_name, query_name = policy.for_each_route(domain)
748
837
  aggregate = runtime.registry.bluebook(query_domain)&.aggregate(aggregate_name)
@@ -760,6 +849,21 @@ module Hecks
760
849
  matched.keys.map(&:to_s).sort
761
850
  end
762
851
 
852
+ # Names the outcome class a recorded refusal row should carry.
853
+ #
854
+ # The outcome class a recorded refusal row names (C8.2/C8.3,
855
+ # docs/semantics/bluebook-semantics.md): a domain refusal is its own
856
+ # class; an evaluation fault — the language refusing to interpret a
857
+ # broken rule or input — is `"Fault"`, the same word the Rust kernel
858
+ # emits (`Refusal::Fault`), never a refusal class and never a raw
859
+ # Ruby exception name.
860
+ #
861
+ # @param error [StandardError] the raised error a step's refusal was rescued as
862
+ # @return [String] `"Fault"` for an evaluation fault, otherwise `error`'s own class name
863
+ def refusal_kind(error)
864
+ error.is_a?(Bluebook::Expression::EvaluationError) ? "Fault" : error.class.name
865
+ end
866
+
763
867
  # Answers one ad hoc filter step for real — the mirror image of
764
868
  # kernel/cli.rs's own `run_filter`, deliberately calling the exact
765
869
  # same production module that method's Rust port stands in for
@@ -775,16 +879,15 @@ module Hecks
775
879
  # Sorted by id ascending regardless — `Ports::Query::Ordering`'s own
776
880
  # header explains why an ask with no declared order still needs
777
881
  # this tier ("the identity tier is what makes an ask total").
778
- # The outcome class a recorded refusal row names (C8.2/C8.3,
779
- # docs/semantics/bluebook-semantics.md): a domain refusal is its own
780
- # class; an evaluation fault — the language refusing to interpret a
781
- # broken rule or input — is `"Fault"`, the same word the Rust kernel
782
- # emits (`Refusal::Fault`), never a refusal class and never a raw
783
- # Ruby exception name.
784
- def refusal_kind(error)
785
- error.is_a?(Bluebook::Expression::EvaluationError) ? "Fault" : error.class.name
786
- end
787
-
882
+ #
883
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
884
+ # to filter against
885
+ # @param filter [Hash] the ad hoc filter step's own Hash value: String-keyed
886
+ # `"aggregate"`, `"field"`, `"op"`, `"value"`
887
+ # @return [Array<Hash>] matching records, sorted by id ascending, each `{id:}`
888
+ # merged with the record's own state
889
+ # @raise [Bluebook::Expression::EvaluationError] if `op` names no known comparator,
890
+ # or `"aggregate"` names no loaded aggregate
788
891
  def run_filter(runtime, filter)
789
892
  aggregate_ref = filter["aggregate"].to_s
790
893
  field = filter["field"].to_s
@@ -810,12 +913,17 @@ module Hecks
810
913
  matched.sort_by { |record| record.id.to_s }.map { |record| { id: record.id }.merge(record.state) }
811
914
  end
812
915
 
813
- # The `refusals` entry's own "verb" column for a refused ad hoc
814
- # filter — there is no real verb to report (a filter step carries
815
- # none), so this builds the same descriptive label kernel/cli.rs's
816
- # own `filter_label` builds from the same three raw fields, tolerant
817
- # of any of them being missing (Ruby's own nil-to-"" interpolation)
818
- # the same way that Rust port is.
916
+ # Builds the `refusals` entry's own "verb" column for a refused ad hoc filter.
917
+ #
918
+ # There is no real verb to report (a filter step carries none), so this
919
+ # builds the same descriptive label kernel/cli.rs's own `filter_label`
920
+ # builds from the same three raw fields, tolerant of any of them being
921
+ # missing (Ruby's own nil-to-"" interpolation) the same way that Rust
922
+ # port is.
923
+ #
924
+ # @param filter [Hash] the ad hoc filter step's own Hash value, read for
925
+ # `"aggregate"`, `"field"`, `"op"`
926
+ # @return [String] a descriptive label such as `"filter Banking::Account.status eq"`
819
927
  def filter_label(filter) = "filter #{filter['aggregate']}.#{filter['field']} #{filter['op']}"
820
928
  end
821
929
  end
@@ -10,28 +10,30 @@ module Hecks
10
10
  # view; this module is where the blend actually happens, against the
11
11
  # rows that query already returns.
12
12
  #
13
- # **Pure, deliberately**. Every method here is a function of its own
14
- # arguments only — no clock read, no query dispatched, nothing
15
- # random — so this is unit-testable with plain hashes and no ledger
16
- # boot at all, and reruns identically given the same rotation, the
17
- # same `now` and the same dials. `now` arrives as an argument for
18
- # the exact reason `Target.Claim`'s own comment already gives: a
19
- # pure function cannot ask the time, so whoever calls this supplies
20
- # it, exactly as it supplies an id.
13
+ # ## Pure, deliberately
21
14
  #
22
- # Where this actually matters, and where it does not — checked
23
- # against `bin/qa_sweep`'s own code, not assumed. `bin/qa_sweep
24
- # --all` sweeps every currently-waiting target in one pass,
25
- # regardless of order (`run_all_mode`'s own `waiting.map { spawn_
26
- # sweep_child }` — every element the query returned, never
27
- # `.first`), so nothing about this module's own ordering changes
28
- # what `--all` does; it already sweeps everything the ledger has to
29
- # offer. What it changes is `bin/qa_sweep` invoked with no target
30
- # argument, which today picks exactly one target via `Target.
31
- # Rotation.first` — wiring `.pick` in there instead is this module's
32
- # one real caller, and (per the same investigation) its only one:
33
- # there is no separate scheduling or concurrency-limit mechanism
34
- # elsewhere in this repository for it to feed instead.
15
+ # Every method here is a function of its own arguments only — no clock
16
+ # read, no query dispatched, nothing random — so this is unit-testable
17
+ # with plain hashes and no ledger boot at all, and reruns identically
18
+ # given the same rotation, the same `now` and the same dials. `now`
19
+ # arrives as an argument for the exact reason `Target.Claim`'s own
20
+ # comment already gives: a pure function cannot ask the time, so
21
+ # whoever calls this supplies it, exactly as it supplies an id.
22
+ #
23
+ # ## Where this actually matters, and where it does not
24
+ #
25
+ # Checked against `bin/qa_sweep`'s own code, not assumed. `bin/qa_sweep
26
+ # --all` sweeps every currently-waiting target in one pass, regardless
27
+ # of order (`run_all_mode`'s own `waiting.map { spawn_sweep_child }` —
28
+ # every element the query returned, never `.first`), so nothing about
29
+ # this module's own ordering changes what `--all` does; it already
30
+ # sweeps everything the ledger has to offer. What it changes is
31
+ # `bin/qa_sweep` invoked with no target argument, which today picks
32
+ # exactly one target via `Target.Rotation.first` — wiring `.pick` in
33
+ # there instead is this module's one real caller, and (per the same
34
+ # investigation) its only one: there is no separate scheduling or
35
+ # concurrency-limit mechanism elsewhere in this repository for it to
36
+ # feed instead.
35
37
  module RotationPriority
36
38
  module_function
37
39
 
@@ -46,6 +48,14 @@ module Hecks
46
48
  # that does not care still gets the practice's own current answer
47
49
  # to "how much of this is 'recent'" rather than a second, silently
48
50
  # drifting copy of the same number.
51
+ #
52
+ # @param old_score [Integer] the target's `Target.yield_score` before this period
53
+ # @param surprises_this_period [Integer] how many Surprised checks this period found
54
+ # @param decay_percent [Integer] percent of `old_score` that survives into the new
55
+ # score; defaults to `QualityControlDials::YIELD_DECAY_PERCENT`
56
+ # @return [Integer] the next `Target.yield_score` to store
57
+ # @raise [ArgumentError] if `old_score` is negative
58
+ # @raise [ArgumentError] if `surprises_this_period` is negative
49
59
  def next_yield_score(old_score:, surprises_this_period:,
50
60
  decay_percent: QualityControlDials::YIELD_DECAY_PERCENT)
51
61
  raise ArgumentError, "old_score must not be negative" if old_score.negative?
@@ -77,6 +87,16 @@ module Hecks
77
87
  # "how many seconds of extra staleness one point of yield is
78
88
  # worth" rather than an opaque multiplier nobody could sanity
79
89
  # check by eye.
90
+ #
91
+ # @param rows [Array<Hash>] rows from `Target.Rotation`, each carrying at least
92
+ # `:last_swept` and `:yield_score` as `{ value: Integer }`
93
+ # @param now [Integer] the current time, Unix epoch seconds
94
+ # @param weight_seconds [Integer] seconds of extra staleness one point of yield is
95
+ # worth; defaults to `QualityControlDials::YIELD_WEIGHT_SECONDS`
96
+ # @param floor_seconds [Integer] staleness, in seconds, past which a row is picked
97
+ # ahead of every row not yet that stale; defaults to
98
+ # `QualityControlDials::ROTATION_STALE_FLOOR_SECONDS`
99
+ # @return [Hash, nil] the picked row, or `nil` if `rows` is empty
80
100
  def pick(rows, now:, weight_seconds: QualityControlDials::YIELD_WEIGHT_SECONDS,
81
101
  floor_seconds: QualityControlDials::ROTATION_STALE_FLOOR_SECONDS)
82
102
  return nil if rows.empty?
@@ -7,28 +7,32 @@ module Hecks
7
7
  # `manifest.json` files `rust/project/domain_generator.rb` writes beside
8
8
  # every generated module, never inferred from Rust's refusal wording.
9
9
  #
10
- # The differential fuzzer (`Differential.diff`) used to drop any refusal
11
- # whose Rust error contained "is not generated for this domain". That
12
- # tolerated whatever Rust happened to say, including a codegen regression
13
- # that started refusing a construct it used to generate. Now a query or
14
- # read-model verb is dropped from the comparison if and only if the
15
- # generator's own manifest recorded it as `generated: false`, with the
16
- # `gap_class` and `construct` that explain why. A refusal the manifest
17
- # does not account for stays in the comparison and fails it.
10
+ # The differential fuzzer (`Differential.diff`) drops a query or
11
+ # read-model verb from the comparison if and only if the generator's own
12
+ # manifest recorded it as `generated: false`, with the `gap_class` and
13
+ # `construct` that explain why — never by pattern-matching Rust's
14
+ # refusal wording ("is not generated for this domain"), which would
15
+ # tolerate whatever Rust happened to say, including a codegen
16
+ # regression that silently started refusing a construct it once
17
+ # generated. A refusal the manifest does not account for stays in the
18
+ # comparison and fails it.
18
19
  #
19
- # **Which manifests describe a binary**. `build_and_pin` (spec/support/
20
- # rust_conformance_helpers.rb) and `bin/qa_generated_domains` both pin a
21
- # feature's binary at `<rust_dir>/target/debug/rust-<feature>`. That
22
- # binary compiles `src/generated/<feature>/` plus every shared framework
23
- # chapter (a generated directory with no `merged.rs` of its own —
24
- # `governance`, `identity`; see the generated `mod.rs` header). Ids in
25
- # those manifests are domain-qualified, so a chapter a domain never
26
- # attaches contributes entries no sequence for that domain can name.
20
+ # ## Which manifests describe a binary
27
21
  #
28
- # **A missing manifest tolerates nothing**. A hand-written fixture crate, or
29
- # a tree generated before manifests existed, has no declaration to
30
- # honour, so every refusal it produces is compared as-is. That is the
31
- # fail-closed direction.
22
+ # `build_and_pin` (spec/support/rust_conformance_helpers.rb) and
23
+ # `bin/qa_generated_domains` both pin a feature's binary at
24
+ # `<rust_dir>/target/debug/rust-<feature>`. That binary compiles
25
+ # `src/generated/<feature>/` plus every shared framework chapter (a
26
+ # generated directory with no `merged.rs` of its own — `governance`,
27
+ # `identity`; see the generated `mod.rs` header). Ids in those manifests
28
+ # are domain-qualified, so a chapter a domain never attaches contributes
29
+ # entries no sequence for that domain can name.
30
+ #
31
+ # ## A missing manifest tolerates nothing
32
+ #
33
+ # A hand-written fixture crate, or a tree generated before manifests
34
+ # existed, has no declaration to honour, so every refusal it produces
35
+ # is compared as-is. That is the fail-closed direction.
32
36
  class RustGapManifest
33
37
  # The only kinds the kernel answers as a query step. A not-generated
34
38
  # command changes state, so dropping its refusal would not make the
@@ -38,6 +42,11 @@ module Hecks
38
42
 
39
43
  attr_reader :rust_dir, :feature, :entries
40
44
 
45
+ # Builds the manifest reader for the pinned conformance binary at `binary`.
46
+ #
47
+ # @param binary [String] path to a pinned binary, `<rust_dir>/target/debug/rust-<feature>`
48
+ # @return [Fuzzing::RustGapManifest] the manifest reader for that binary's feature
49
+ # @raise [ArgumentError] if `binary` does not match the pinned-binary path shape
41
50
  def self.for_binary(binary)
42
51
  match = PINNED_BINARY.match(File.expand_path(binary.to_s))
43
52
  unless match
@@ -51,6 +60,10 @@ module Hecks
51
60
  # Every committed manifest entry under `rust_dir`, each tagged with the
52
61
  # generated module it came from — what the boundary ratchet and
53
62
  # bin/rust_coverage's allowlist staleness check read.
63
+ #
64
+ # @param rust_dir [String] path to the Rust project root (holds `src/generated/*/`)
65
+ # @return [Array<Hash>] every `src/generated/*/manifest.json` entry, each a
66
+ # String-keyed manifest Hash plus `"module" => String` naming the directory it came from
54
67
  def self.all_entries(rust_dir)
55
68
  Dir.glob(File.join(rust_dir, "src/generated/*/manifest.json")).flat_map do |path|
56
69
  module_name = File.basename(File.dirname(path))
@@ -58,6 +71,8 @@ module Hecks
58
71
  end
59
72
  end
60
73
 
74
+ # @param rust_dir [String] path to the Rust project root (holds `src/generated/*/`)
75
+ # @param feature [String] the domain feature this manifest reader describes
61
76
  def initialize(rust_dir:, feature:)
62
77
  @rust_dir = rust_dir
63
78
  @feature = feature
@@ -68,14 +83,25 @@ module Hecks
68
83
  # The manifest entry that declares `verb` not generated, or nil. `verb`
69
84
  # is the wire spelling a sequence step uses; an ad hoc filter (a Hash)
70
85
  # is never declared and answers nil.
86
+ #
87
+ # @param verb [String, Object] the wire-spelled query or read-model verb to look up
88
+ # @return [Hash, nil] the String-keyed manifest entry declaring `verb` not generated,
89
+ # or nil if `verb` is not a String or no entry declares it
71
90
  def not_generated(verb)
72
91
  return nil unless verb.is_a?(String)
73
92
 
74
93
  @not_generated[verb]
75
94
  end
76
95
 
96
+ # Answers whether `verb` is declared not generated by this manifest.
97
+ #
98
+ # @param verb [String, Object] the wire-spelled query or read-model verb to look up
99
+ # @return [Boolean] whether the manifest declares `verb` not generated
77
100
  def not_generated?(verb) = !not_generated(verb).nil?
78
101
 
102
+ # Lists every verb this manifest declares not generated.
103
+ #
104
+ # @return [Set<String>] every verb the manifest declares not generated
79
105
  def not_generated_verbs = @not_generated.keys.to_set
80
106
 
81
107
  private