hecks 1.4.0 → 1.5.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 (275) 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.bluebook +105 -0
  112. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  113. data/lib/hecks/deploy/oidc.json +5 -0
  114. data/lib/hecks/doc/reference.rb +185 -16
  115. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  116. data/lib/hecks/facade/handle.rb +76 -3
  117. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  118. data/lib/hecks/forms/field_shape.rb +3 -0
  119. data/lib/hecks/forms/page.rb +14 -0
  120. data/lib/hecks/forms/port_argument.rb +12 -0
  121. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  122. data/lib/hecks/forms/record_renderer.rb +58 -0
  123. data/lib/hecks/forms/record_table.rb +27 -0
  124. data/lib/hecks/forms/reference_options.rb +24 -0
  125. data/lib/hecks/forms/value_object_shape.rb +10 -0
  126. data/lib/hecks/fqn.rb +58 -0
  127. data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
  128. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  129. data/lib/hecks/framework/oidc.json +15 -0
  130. data/lib/hecks/framework.rb +43 -20
  131. data/lib/hecks/freezer.rb +17 -1
  132. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  133. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  134. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  135. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  136. data/lib/hecks/fuzzing/differential.rb +34 -0
  137. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  138. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  139. data/lib/hecks/fuzzing/form_census.rb +86 -0
  140. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  141. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  142. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  143. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  144. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  145. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  146. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  147. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  148. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  149. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  150. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  151. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  152. data/lib/hecks/fuzzing/properties.rb +24 -15
  153. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  154. data/lib/hecks/fuzzing/replay.rb +137 -29
  155. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  156. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  157. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  158. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  159. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  160. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  161. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  162. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  163. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  164. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  165. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  166. data/lib/hecks/grammar/evolve.rb +178 -2
  167. data/lib/hecks/grammar.rb +46 -0
  168. data/lib/hecks/ir.rb +38 -7
  169. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  170. data/lib/hecks/literal.rb +32 -0
  171. data/lib/hecks/naming.rb +88 -7
  172. data/lib/hecks/ports/access_control.rb +5 -10
  173. data/lib/hecks/ports/authorization.rb +3 -6
  174. data/lib/hecks/ports/identity_assignment.rb +1 -2
  175. data/lib/hecks/ports/identity_resolution.rb +1 -2
  176. data/lib/hecks/ports/key_vault.port +6 -0
  177. data/lib/hecks/ports/key_vault.rb +85 -0
  178. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  179. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  180. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  182. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  183. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  184. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  185. data/lib/hecks/ports/query/in_memory.rb +39 -6
  186. data/lib/hecks/ports/query/ordering.rb +15 -0
  187. data/lib/hecks/ports.rb +1 -0
  188. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  189. data/lib/hecks/projections/deploy/fargate.rb +666 -0
  190. data/lib/hecks/projections/deploy/lambda.rb +2423 -0
  191. data/lib/hecks/projections/deploy/shared.rb +624 -0
  192. data/lib/hecks/projections/diagrams.rb +243 -7
  193. data/lib/hecks/projections/glossary/html.rb +88 -0
  194. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  195. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  196. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  197. data/lib/hecks/projections/glossary.rb +161 -19
  198. data/lib/hecks/projections/model/deviations.rb +44 -0
  199. data/lib/hecks/projections/model.rb +51 -1
  200. data/lib/hecks/projections/oidc.rb +40 -3
  201. data/lib/hecks/projections/parser_table.rb +49 -8
  202. data/lib/hecks/projections/reference.rb +12 -0
  203. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  204. data/lib/hecks/projections/shape.rb +8 -0
  205. data/lib/hecks/projections/statements.rb +63 -16
  206. data/lib/hecks/projections/vocabulary.rb +17 -0
  207. data/lib/hecks/projections.rb +3 -0
  208. data/lib/hecks/projector/cli_projector.rb +218 -10
  209. data/lib/hecks/projector/docs_projector.rb +145 -19
  210. data/lib/hecks/projector/exporter.rb +65 -11
  211. data/lib/hecks/projector/ir_projector.rb +6 -0
  212. data/lib/hecks/projector/narrate_projector.rb +136 -15
  213. data/lib/hecks/projector/target.rb +47 -10
  214. data/lib/hecks/projector.rb +101 -19
  215. data/lib/hecks/query_ir.rb +47 -0
  216. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  217. data/lib/hecks/rendering.rb +6 -0
  218. data/lib/hecks/router/namespace_installer.rb +13 -0
  219. data/lib/hecks/router.rb +55 -0
  220. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  221. data/lib/hecks/runtime/boot_gates.rb +18 -0
  222. data/lib/hecks/runtime/caller.rb +32 -0
  223. data/lib/hecks/runtime/capability_graph.rb +11 -0
  224. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  225. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  226. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  227. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  228. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  229. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  230. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  231. data/lib/hecks/runtime/dispatcher.rb +28 -50
  232. data/lib/hecks/runtime/entity_element.rb +161 -8
  233. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  234. data/lib/hecks/runtime/errors.rb +18 -4
  235. data/lib/hecks/runtime/event.rb +10 -5
  236. data/lib/hecks/runtime/identity.rb +71 -3
  237. data/lib/hecks/runtime/instance.rb +67 -7
  238. data/lib/hecks/runtime/interpreting.rb +13 -5
  239. data/lib/hecks/runtime/invocation.rb +118 -36
  240. data/lib/hecks/runtime/loader.rb +94 -8
  241. data/lib/hecks/runtime/outbox.rb +145 -7
  242. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  243. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  244. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  245. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  246. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  247. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  248. data/lib/hecks/runtime/reference_hop.rb +42 -0
  249. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  250. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  251. data/lib/hecks/runtime/registry/verification.rb +119 -4
  252. data/lib/hecks/runtime/registry.rb +157 -4
  253. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  254. data/lib/hecks/runtime/routing.rb +27 -2
  255. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  256. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  257. data/lib/hecks/runtime/tenant_check.rb +26 -6
  258. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  259. data/lib/hecks/runtime/value/coercion.rb +255 -33
  260. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  261. data/lib/hecks/runtime/value.rb +50 -0
  262. data/lib/hecks/runtime.rb +32 -0
  263. data/lib/hecks/storehouse.rb +305 -9
  264. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  265. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  266. data/lib/hecks/version.rb +1 -1
  267. data/lib/hecks.rb +79 -1
  268. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  269. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  270. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  271. metadata +14 -5
  272. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  273. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  274. data/lib/hecks/deprecation.rb +0 -95
  275. data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
@@ -253,19 +253,19 @@ module Hecks
253
253
 
254
254
  private
255
255
 
256
- # THE LANGUAGE'S OWN BOOTSTRAP SAVES ITS SELF-DESCRIPTION QUADRATICALLY
257
- # OTHERWISE. `MetaValidator::Judge` dispatches every declaration in the
256
+ # The language's own bootstrap saves its self-description quadratically
257
+ # otherwise. `MetaValidator::Judge` dispatches every declaration in the
258
258
  # self-hosted grammar into a fresh, private, never-durable `Memory`
259
259
  # store (meta_validator.rb's own header: "each bluebook is judged in a
260
260
  # fresh in-memory store") — and every nested-entity dispatch (a
261
261
  # `ValueObject::Member`, then one `ValueObject::Member::Pair` per
262
- # key/value pair) re-saves the WHOLE parent aggregate, because entities
262
+ # key/value pair) re-saves the whole parent aggregate, because entities
263
263
  # have no storage of their own (S17, ADR 0026). A table of N member
264
- # rows costs O(N) dispatches, each PAYING TWICE for the aggregate's own
264
+ # rows costs O(N) dispatches, each paying twice for the aggregate's own
265
265
  # size-N state: once in `StateCodec.copy` (`append`/`project`'s own
266
266
  # encode-then-decode round trip) and again in `Instance#initialize`'s
267
267
  # `hydrate_with_defaults`, which re-walks and re-validates every
268
- # element of an entity list on EVERY save regardless of how many of
268
+ # element of an entity list on every save regardless of how many of
269
269
  # them were already valid as of the previous one
270
270
  # (`Value::EntityListCoercion#hydrate_entity_list` has no "already
271
271
  # hydrated" shortcut for entity elements — only a value-object list
@@ -277,7 +277,7 @@ module Hecks
277
277
  #
278
278
  # `Runtime::Value.judge_bootstrapping?` (judge.rb's own `send_to`,
279
279
  # wrapping every dispatch the judge makes) is already the flag that
280
- # marks exactly this window and NOTHING else — "never for a REAL
280
+ # marks exactly this window and nothing else — "never for a real
281
281
  # domain's own declared value objects... which Judge never dispatches
282
282
  # commands against" (coercion.rb's own comment on the same flag). It
283
283
  # is reused here rather than a new toggle for the same reason: one
@@ -286,13 +286,13 @@ module Hecks
286
286
  # both round trips here) is simpler to reason about than two flags
287
287
  # that would always be true or false together.
288
288
  #
289
- # WHY SKIPPING BOTH IS SAFE HERE, AND ONLY HERE: `entry.state` a save
289
+ # Why skipping both is safe here, and only here: `entry.state` a save
290
290
  # ever hands this adapter is always `instance.state.dup`
291
- # (`AppendOnly#save`) — a shallow copy of an ALREADY-hydrated,
292
- # ALREADY-validated live `Instance`'s own state, built by the very
291
+ # (`AppendOnly#save`) — a shallow copy of an already-hydrated,
292
+ # already-validated live `Instance`'s own state, built by the very
293
293
  # same `Value.for_attribute`/`hydrate_with_defaults` machinery
294
294
  # `StateCodec.copy` and `Instance.new`'s default (`hydrate: true`)
295
- # path would otherwise redo. Every VALUE inside it — a `Runtime::
295
+ # path would otherwise redo. Every value inside it — a `Runtime::
296
296
  # Value` (frozen through, see value.rb's own header) or a `list_of`
297
297
  # attribute's own array (`Freezer.deep`d the moment it was built,
298
298
  # instance.rb's own header on `Instance#dup`) — is already immutable,
@@ -303,13 +303,13 @@ module Hecks
303
303
  # pure, avoidable cost for this one caller — never a correctness
304
304
  # requirement.
305
305
  #
306
- # `bootstrap_fast_path?` is the guard, and it is deliberately CHEAP —
306
+ # `bootstrap_fast_path?` is the guard, and it is deliberately cheap —
307
307
  # O(this aggregate's own declared attribute count), never O(N) — so
308
308
  # it cannot reintroduce the very cost it exists to avoid: `StateCodec.
309
309
  # decoded?` (the obvious-looking alternative) recurses into every
310
- # `list_of` element to check IT, which is exactly the O(N) walk this
310
+ # `list_of` element to check it, which is exactly the O(N) walk this
311
311
  # whole change removes. A `Hash` with every top-level key already a
312
- # `Symbol` is what `Instance#state` ALWAYS looks like (`Value.hydrate`
312
+ # `Symbol` is what `Instance#state` always looks like (`Value.hydrate`
313
313
  # refuses anything else, coercion.rb's own header), so it is checked
314
314
  # here instead — true for the actual shape every real save has,
315
315
  # false (falling back to the always-correct slow path) for anything
@@ -17,8 +17,28 @@ module Hecks
17
17
  # webhook against it (a smoke test, CI) has something real to key
18
18
  # off of — the real shape production has, minus the network hop.
19
19
  class MockStripeAdapter
20
+ # Accepts and discards the shared adapter constructor arguments; this adapter holds no
21
+ # state of its own.
22
+ #
23
+ # @param aggregate [Bluebook::Aggregate, nil] accepted for the shared adapter
24
+ # constructor shape and ignored
25
+ # @param settings [Hash] accepted for the shared adapter constructor shape and ignored
26
+ # @param root [String, nil] accepted for the shared adapter constructor shape and ignored
20
27
  def initialize(aggregate: nil, settings: {}, root: nil); end
21
28
 
29
+ # Builds a fake checkout URL carrying `registration_id`, standing in for a real
30
+ # Checkout-Session call.
31
+ #
32
+ # @param event [Object] the record data a real adapter would read the price and name
33
+ # from; adapter-defined shape (whatever the declaring bluebook's `checkout` port
34
+ # operation supplies as `event`) and never read here
35
+ # @param registration_id [String] the registration id to key a signed webhook off of;
36
+ # embedded in the returned URL, never read otherwise
37
+ # @param success_url [String] the base URL to redirect a successful payer to
38
+ # @param cancel_url [String] accepted for the shared checkout-adapter call shape and
39
+ # never read
40
+ # @return [String] `success_url` with `mock_checkout=1` and `mock_registration_id`
41
+ # query parameters appended
22
42
  def create_session(event:, registration_id:, success_url:, cancel_url:)
23
43
  separator = success_url.include?("?") ? "&" : "?"
24
44
  "#{success_url}#{separator}mock_checkout=1&mock_registration_id=#{registration_id}"
@@ -21,11 +21,22 @@ module Hecks
21
21
 
22
22
  module_function
23
23
 
24
+ # Recovers a block's body as canonical source text.
25
+ #
26
+ # @param block [Proc] a block written in a bluebook file, such as a `given` predicate
27
+ # or an `identified_by` path
28
+ # @return [String, nil] the block body's source, normalised by
29
+ # `Bluebook::Expression::CanonicalForm`; nil if `body_source` finds none
24
30
  def canonical(block)
25
31
  body = body_source(block)
26
32
  body && canonicalise(body)
27
33
  end
28
34
 
35
+ # Reads a block's own body source text back out of the file it was defined in.
36
+ #
37
+ # @param block [Proc] the block to locate, via its own `source_location`
38
+ # @return [String, nil] the block body's raw source; nil if the file cannot be read,
39
+ # or no block node starts on the block's own line
29
40
  def body_source(block)
30
41
  file, line = block.source_location
31
42
  return nil unless file && File.readable?(file)
@@ -34,6 +45,12 @@ module Hecks
34
45
  node&.body&.slice
35
46
  end
36
47
 
48
+ # Finds the parsed block node starting on a given line of a file.
49
+ #
50
+ # @param file [String] the file path, parsed (and cached) through `tree_for`
51
+ # @param line [Integer] the 1-based source line the block starts on
52
+ # @return [Prism::BlockNode, nil] the first block node found starting on `line`, or nil
53
+ # if none does
37
54
  def block_node_at(file, line)
38
55
  found = nil
39
56
  walk(tree_for(file)) do |node|
@@ -45,12 +62,16 @@ module Hecks
45
62
  found
46
63
  end
47
64
 
65
+ # Parses a file with `prism`, caching the result for the life of the process.
66
+ #
67
+ # @param file [String] the file path to parse and cache under
68
+ # @return [Prism::ProgramNode] the parsed syntax tree's root node
48
69
  def tree_for(file)
49
70
  # ::Prism.parse_file(file) reads the file itself, at the C
50
71
  # extension level — bypassing Ruby's own File/IO layer entirely.
51
72
  # That's invisible to anything that virtualizes the filesystem at
52
73
  # the Ruby level instead of the OS level (e.g. tebako's memfs,
53
- # used to press a hecks-based app into a single executable —
74
+ # which presses a hecks-based app into a single executable —
54
75
  # see domain/README.md's "Deploying" section in lifeadelics for
55
76
  # why that matters). ::Prism.parse(File.read(file)) parses the
56
77
  # exact same bytes, just read through Ruby's File.read first,
@@ -66,16 +87,29 @@ module Hecks
66
87
  # tree reports a `given`/`ensures` block at its old line number,
67
88
  # which no longer matches the freshly re-executed file's own
68
89
  # `block.source_location` — surfacing as "did not survive
69
- # extraction" on a perfectly valid file. Found for real building
70
- # `Hecks::Codemod` (lib/hecks/codemod.rb), which used to
71
- # reach into `TREES.clear` directly — a private implementation
72
- # detail poked from outside. `forget`/`forget_all` are the real
73
- # API so nothing else that reloads an edited file in-process has
74
- # to know `TREES` exists at all.
90
+ # extraction" on a perfectly valid file. Built for real building
91
+ # `Hecks::Codemod` (lib/hecks/codemod.rb), which needs exactly this
92
+ # invalidation rather than reaching into `TREES.clear` directly — a
93
+ # private implementation detail poked from outside. `forget`/
94
+ # `forget_all` are the real API so nothing else that reloads an
95
+ # edited file in-process has to know `TREES` exists at all.
96
+ #
97
+ # @param file [String] the file path to drop from the cache
98
+ # @return [Prism::ProgramNode, nil] the cached tree that was removed, or nil if
99
+ # nothing was cached for `file`
75
100
  def forget(file) = TREES.delete(file)
76
101
 
102
+ # Drops every cached parse tree.
103
+ #
104
+ # @return [Hash] the now-empty cache
77
105
  def forget_all = TREES.clear
78
106
 
107
+ # Visits `node` and every descendant depth-first, calling `visit` on each.
108
+ #
109
+ # @param node [Prism::Node, Object] the node to walk; anything that is not a
110
+ # `Prism::Node` is a silent no-op
111
+ # @yieldparam node [Prism::Node] each node visited, `node` itself first
112
+ # @return [void]
79
113
  def walk(node, &visit)
80
114
  return unless node.is_a?(::Prism::Node)
81
115
 
@@ -83,6 +117,11 @@ module Hecks
83
117
  node.compact_child_nodes.each { |child| walk(child, &visit) }
84
118
  end
85
119
 
120
+ # Normalises raw source text into the framework's own canonical form.
121
+ #
122
+ # @param source [String] the raw source text to normalise
123
+ # @return [String] the normalised text (whitespace collapsed, linked replacements
124
+ # applied)
86
125
  def canonicalise(source)
87
126
  Bluebook::Expression::CanonicalForm.apply(source)
88
127
  end
@@ -8,6 +8,9 @@ module Hecks
8
8
  module SecureRandomIdentity
9
9
  module_function
10
10
 
11
+ # Mints a fresh random identity value.
12
+ #
13
+ # @return [String] a newly minted, random UUID
11
14
  def uuid = SecureRandom.uuid
12
15
  end
13
16
  end
@@ -8,6 +8,9 @@ module Hecks
8
8
  module SystemClock
9
9
  module_function
10
10
 
11
+ # Reads the machine's own current time.
12
+ #
13
+ # @return [Integer] the current time, in Unix epoch seconds
11
14
  def now = Time.now.to_i
12
15
  end
13
16
  end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "TenantProvisioner" do
2
+ port "TenantProvisioning"
3
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ # THE IMPERATIVE HALF of provisioning a tenant, pulled out of
8
+ # `bin/project_tenant` and behind a real driven port
9
+ # (`Deploy::Tenant.port "TenantProvisioning"`) instead — the same
10
+ # hexagonal reasoning `GithubChecks` already gives for its own
11
+ # port: real, impure, side-effecting work (writing the overlay
12
+ # file) belongs in an adapter, not in a bare script or a command
13
+ # handler. `Deploy::Tenant`'s own `asks "Provision"` operation calls
14
+ # `#provision` and turns whatever it returns into `TenantProvisioned`,
15
+ # or whatever it raises into `ProvisioningRefused` — see
16
+ # `PortOperationInterpreter#ask`'s own "every failure is an answer"
17
+ # comment for why nothing here needs its own rescue.
18
+ #
19
+ # DELIBERATELY DOES NOT BOOT THE TARGET DOMAIN — that step (proving
20
+ # it boots for real, running the tenant_capable? gate) stays in
21
+ # `bin/project_tenant` itself, called AFTER this operation answers,
22
+ # not nested inside it. Booting a whole domain from inside an
23
+ # adapter method that is itself running mid-dispatch (this port
24
+ # operation) is a real, separate concern from writing one file, and
25
+ # keeping the two apart avoids a nested `Hecks.boot` call ever
26
+ # running inside another boot's own dispatch call stack.
27
+ #
28
+ # RETURNS A HASH SHAPED LIKE `Tenancy::Tenant.Register`'S OWN
29
+ # ARGUMENTS, on purpose — `tenancy.hecksagon`'s own `translates`
30
+ # reaction forwards an answered event's payload verbatim, so this
31
+ # adapter's own return value IS the translation from Deploy's
32
+ # vocabulary into Tenancy's, decided here rather than in a mapping
33
+ # layer neither side could see into.
34
+ class TenantProvisioner
35
+ def initialize(aggregate: nil, settings: {}, root: nil); end
36
+
37
+ # `directory:` IS REQUIRED, NOT DERIVED FROM `domain` — a real,
38
+ # confirmed bug found only under CI's own real-Postgres suite: a
39
+ # domain's own DECLARED NAME (`domain`, e.g. "Scratch") is not a
40
+ # filesystem path, and File.expand_path(domain, Dir.pwd) silently
41
+ # resolved to the wrong directory (relative to whatever the
42
+ # RUNNING PROCESS' own cwd happened to be) whenever the CLI's own
43
+ # `--domain=` value didn't happen to equal its own directory
44
+ # argument by coincidence. `bin/project_tenant` passes its own
45
+ # real `domain_directory` local straight through.
46
+
47
+ def provision(slug:, domain:, realm:, schema:, database:, adapter:, directory:)
48
+ domain_directory = File.expand_path(directory[:value] || directory)
49
+
50
+ overlay_path = File.join(domain_directory, "environments", "#{slug[:value] || slug}.world")
51
+ FileUtils.mkdir_p(File.dirname(overlay_path))
52
+ File.write(overlay_path, <<~WORLD)
53
+ Hecks.world "#{domain[:value] || domain}" do
54
+ realm "#{realm[:value] || realm}"
55
+ persisted_by("#{adapter[:value] || adapter}") do
56
+ database "#{database[:value] || database}"
57
+ schema "#{schema[:value] || schema}"
58
+ end
59
+ end
60
+ WORLD
61
+
62
+ { slug: slug, domain: domain, realm: realm, schema: schema }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -40,7 +40,9 @@ require_relative "driven/prism"
40
40
  require_relative "driven/folder"
41
41
  require_relative "driven/d1"
42
42
  require_relative "driven/mock_stripe_adapter"
43
+ require_relative "driven/tenant_provisioner"
43
44
  require_relative "driven/secure_random_identity"
45
+ require_relative "driven/in_process_key_vault"
44
46
  require_relative "driven/system_clock"
45
47
  # `SequentialIdentity` — the deterministic identity_generation test
46
48
  # double — is not required here on purpose. It lives at
@@ -26,6 +26,8 @@ module Hecks
26
26
  # in `qa/adapters/github_ci_webhook.rb`, the subclass of this file
27
27
  # that actually knows what a `QualityControl::Clearance` is.
28
28
  #
29
+ # ## A plain rack app
30
+ #
29
31
  # A plain rack app (`#call(env)`) — no Sinatra, no Rails — the same
30
32
  # shape `Hecks::Forms::App` (lib/hecks/forms/app.rb) already
31
33
  # established for the one other HTTP-facing surface this library
@@ -37,7 +39,9 @@ module Hecks
37
39
  # `rack` installed, the same "opt in by requiring the file at all"
38
40
  # contract `hecks/forms.rb` already has for `Forms::App`.
39
41
  #
40
- # **Subclass responsibility**: implement `#handle_event(event, action,
42
+ # ## Subclass responsibility
43
+ #
44
+ # Implement `#handle_event(event, action,
41
45
  # payload)`, returning `[http_status, response_body_hash]`. Called
42
46
  # only after the signature has verified and the body has parsed as
43
47
  # JSON — a subclass never has to re-check either. `event` is
@@ -79,12 +83,21 @@ module Hecks
79
83
  # or wherever it keeps one — rather than this class reaching into
80
84
  # the environment itself and hiding that requirement inside a
81
85
  # default.
86
+ # @param secret [String] the shared webhook secret (e.g. `ENV.fetch
87
+ # ("GITHUB_WEBHOOK_SECRET")`), checked against each request's `X-Hub-Signature-256`
88
+ # @raise [ArgumentError] if `secret` is nil or empty
82
89
  def initialize(secret:)
83
90
  raise ArgumentError, "no webhook secret configured" if secret.to_s.empty?
84
91
 
85
92
  @secret = secret
86
93
  end
87
94
 
95
+ # The rack entry point: verifies, parses, and routes one webhook request.
96
+ #
97
+ # @param env [Hash] the rack environment
98
+ # @return [Array(Integer, Hash, Array<String>)] the rack response triple — status,
99
+ # headers, and a one-element body Array holding the JSON-encoded response
100
+ # @raise [NotImplementedError] if the subclass has not implemented `#handle_event`
88
101
  def call(env)
89
102
  request = Rack::Request.new(env)
90
103
  return respond(405, error: "MethodNotAllowed", message: "POST only") unless request.post?
@@ -16,6 +16,7 @@ module Hecks
16
16
  # `validate_expect!` (private, below) is where a malformed or empty
17
17
  # `expect` is refused at build time rather than silently passing later.
18
18
  class TestCaseBuilder
19
+ # @param description [String] the `test "description" do ... end` text
19
20
  def initialize(description)
20
21
  @description = description
21
22
  @tests_command = nil
@@ -26,19 +27,46 @@ module Hecks
26
27
  @expect = {}
27
28
  end
28
29
 
30
+ # Records which command or query this test exercises.
31
+ #
32
+ # @param command [String, Symbol] the command or query verb, bare or dotted FQN
33
+ # @param on [String, Symbol, nil] the aggregate `command` is dispatched on, when
34
+ # `command` is not already a dotted FQN
35
+ # @param kind [Symbol] `:command` or `:query`
36
+ # @return [void]
29
37
  def tests(command, on: nil, kind: :command)
30
38
  @tests_command = command
31
39
  @on_aggregate = on
32
40
  @kind = kind
33
41
  end
34
42
 
43
+ # Records one setup command to dispatch before the test itself runs.
44
+ #
45
+ # @param command [String, Symbol] the setup command's verb
46
+ # @param kwargs [Hash{Symbol => Object}] the setup command's own arguments
47
+ # @return [void]
35
48
  def setup(command, **kwargs)
36
49
  @setups << TestSetup.new(command: command, args: kwargs)
37
50
  end
38
51
 
52
+ # Merges fields into the arguments the tested command or query is called with.
53
+ #
54
+ # @param kwargs [Hash{Symbol => Object}] argument names and values to merge in
55
+ # @return [Hash{Symbol => Object}] the input Hash so far
39
56
  def input(**kwargs) = @input.merge!(kwargs)
57
+
58
+ # Merges fields into what this test expects of the tested dispatch.
59
+ #
60
+ # @param kwargs [Hash{Symbol => Object}] expectation keys (`ok:`, `refused:`,
61
+ # `emits:`, `count:`, or a field name) and their expected values
62
+ # @return [Hash{Symbol => Object}] the expectation Hash so far
40
63
  def expect(**kwargs) = @expect.merge!(kwargs)
41
64
 
65
+ # Builds the `TestCase` this builder collected.
66
+ #
67
+ # @return [TestCase] the built test case
68
+ # @raise [Malformed] if `tests` was never called, `expect` is empty, or
69
+ # `expect` declares `count:` on a command or `emits:` on a query
42
70
  def build
43
71
  unless @tests_command
44
72
  raise Malformed, "test #{@description.inspect} never calls `tests` — " \
@@ -84,6 +112,8 @@ module Hecks
84
112
  # `vision`/`loads`/`test` calls and builds a `BehaviorsSuite` (ir.rb),
85
113
  # refusing to build one missing either `vision` or `loads` (`#build`).
86
114
  class BehaviorsBuilder
115
+ # @param name [String] the suite's declared name
116
+ # @param source_path [String] the `.behaviors` file's own path
87
117
  def initialize(name, source_path:)
88
118
  @name = name
89
119
  @source_path = source_path
@@ -93,21 +123,40 @@ module Hecks
93
123
  @tests = []
94
124
  end
95
125
 
126
+ # Records the suite's one-line description of what it is examples of.
127
+ #
128
+ # @param text [String] the vision text
129
+ # @return [String] `text`, unchanged
96
130
  def vision(text) = @vision = text
97
131
 
98
132
  # Relative to this `.behaviors` file, never to the filesystem's cwd
99
133
  # or a same-stem convention — scope is a fact this file declares,
100
134
  # not one a runner infers.
135
+ #
136
+ # @param paths [Array<String>] paths to the files this suite's domain boots
137
+ # from, relative to the `.behaviors` file
138
+ # @return [void]
101
139
  def loads(*paths)
102
140
  @loads = paths.map { |path| File.expand_path(path, @source_dir) }
103
141
  end
104
142
 
143
+ # Builds and records one test case.
144
+ #
145
+ # @param description [String] the test's own description
146
+ # @yield the test's body, evaluated against a `TestCaseBuilder`
147
+ # @return [void]
148
+ # @raise [Malformed] if the test never calls `tests`, has no `expect`, or
149
+ # declares `count:` on a command or `emits:` on a query
105
150
  def test(description, &block)
106
151
  builder = TestCaseBuilder.new(description)
107
152
  builder.instance_eval(&block) if block
108
153
  @tests << builder.build
109
154
  end
110
155
 
156
+ # Builds the `BehaviorsSuite` this builder collected.
157
+ #
158
+ # @return [BehaviorsSuite] the built suite
159
+ # @raise [Malformed] if the suite declares no `vision` or no `loads`
111
160
  def build
112
161
  unless @vision
113
162
  raise Malformed, "#{@source_path}: no `vision \"...\"` — say in one line " \
@@ -122,6 +171,15 @@ module Hecks
122
171
  tests: @tests, path: @source_path)
123
172
  end
124
173
 
174
+ # Builds a suite in one call: constructs a builder, evaluates `block`
175
+ # against it, and builds the result.
176
+ #
177
+ # @param name [String] the suite's declared name
178
+ # @param source_path [String] the `.behaviors` file's own path
179
+ # @yield the suite's body, evaluated against the new builder
180
+ # @return [BehaviorsSuite] the built suite
181
+ # @raise [Malformed] if the suite declares no `vision` or `loads`, or any of
182
+ # its `test` blocks is malformed
125
183
  def self.build(name, source_path:, &block)
126
184
  builder = new(name, source_path: source_path)
127
185
  builder.instance_eval(&block) if block