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
@@ -7,6 +7,8 @@ module Hecks
7
7
  # A small, valid bluebook, written from a seed — so the QA loop can test
8
8
  # construct combinations nobody has hand-authored yet.
9
9
  #
10
+ # ## Why this exists
11
+ #
10
12
  # The ledger's most productive moments were a new stress domain's first
11
13
  # sweep: `corrections` found four bugs, `referral_chain` four,
12
14
  # `tenant_ledger` two. Every one of those domains was written by hand,
@@ -20,14 +22,17 @@ module Hecks
20
22
  # policy, a role), and renders the whole thing as ordinary bluebook
21
23
  # source any runtime boots.
22
24
  #
23
- # A blueprint, not source, is the unit. `generate` answers a plain,
24
- # JSON-shaped Hash (string keys) — a small IR of its own — and `render`
25
- # turns it into source. That split is what makes a finding shrinkable
26
- # at the domain level: `shrink_candidates` removes one element at a time
27
- # (a query, a command, an entity, an aggregate, a given…) and `prune`
28
- # drops whatever that removal left dangling, using the explicit
29
- # `requires` tokens every dependent element carries. Nothing here
30
- # parses Ruby back.
25
+ # ## Blueprint, not source, is the unit
26
+ #
27
+ # `generate` answers a plain, JSON-shaped Hash (string keys) — a small
28
+ # IR of its own — and `render` turns it into source. That split is what
29
+ # makes a finding shrinkable at the domain level: `shrink_candidates`
30
+ # removes one element at a time (a query, a command, an entity, an
31
+ # aggregate, a given…) and `prune` drops whatever that removal left
32
+ # dangling, using the explicit `requires` tokens every dependent
33
+ # element carries. Nothing here parses Ruby back.
34
+ #
35
+ # ## One fixed domain name
31
36
  #
32
37
  # Every generated domain is the same domain name — `QaGenerated`, in a
33
38
  # `qa_generated/bluebook/qa_generated.bluebook` directory — because the
@@ -60,6 +65,16 @@ module Hecks
60
65
 
61
66
  module_function
62
67
 
68
+ # Builds a random blueprint from a seed and prunes it to a fixpoint,
69
+ # ready for `write` to render into a fresh `QaGenerated` domain.
70
+ #
71
+ # @param seed [Integer] the random seed; the same seed and `forms` always
72
+ # produce the same blueprint
73
+ # @param forms [Array<String, Symbol>, nil] the `FormCensus::FORMS` names to
74
+ # force onto the primary aggregate; two random forms from `FORMS` when nil
75
+ # @return [Hash{String => Object}] the pruned blueprint, with `"seed"` and
76
+ # `"forms"` merged in
77
+ # @raise [ArgumentError] if `forms` names anything outside `FORMS`
63
78
  def generate(seed:, forms: nil)
64
79
  random = Random.new(seed)
65
80
  forms = Array(forms || FORMS.sample(2, random: random)).map(&:to_s)
@@ -77,6 +92,11 @@ module Hecks
77
92
  # A blueprint carrying `"source"` is bluebook text someone else wrote
78
93
  # (`bin/qa_mine_combinations`' agent): it is adopted, not rendered,
79
94
  # and its empty `aggregates`/`policies` leave nothing to shrink.
95
+ #
96
+ # @param blueprint [Hash{String => Object}] a blueprint as `generate` answers,
97
+ # or one carrying a `"source"` key instead of `"aggregates"`/`"policies"`
98
+ # @param root [String] the directory to write the domain and blueprint under
99
+ # @return [String] the written domain's directory (`<root>/qa_generated`)
80
100
  def write(blueprint, root)
81
101
  domain = File.join(root, DIRECTORY)
82
102
  FileUtils.rm_rf(domain)
@@ -89,12 +109,21 @@ module Hecks
89
109
 
90
110
  # Renames an outside bluebook's own chapter to `QaGenerated`, the one
91
111
  # name the scratch crate and child processes are built around.
112
+ #
113
+ # @param source [String] bluebook source text declaring one chapter
114
+ # @return [String] `source` with its `Hecks.bluebook "..."` chapter name
115
+ # replaced by `"QaGenerated"`
92
116
  def adopt(source)
93
117
  source.sub(/Hecks\.bluebook\s*\(?\s*(["'])[^"']+\1/) { "Hecks.bluebook #{DOMAIN_NAME.inspect}" }
94
118
  end
95
119
 
96
120
  # ── rendering ───────────────────────────────────────────────────
97
121
 
122
+ # Renders a whole blueprint into one bluebook source file.
123
+ #
124
+ # @param blueprint [Hash{String => Object}] a blueprint as `generate` answers
125
+ # @return [String] the complete `Hecks.bluebook "QaGenerated" do ... end` source,
126
+ # ending in a newline
98
127
  def render(blueprint)
99
128
  out = []
100
129
  out << "# GENERATED by Hecks::Fuzzing::DomainGenerator — seed #{blueprint['seed']}, forms " \
@@ -109,6 +138,11 @@ module Hecks
109
138
  "#{out.join("\n")}\n"
110
139
  end
111
140
 
141
+ # Renders one aggregate's whole `aggregate ... end` block, including
142
+ # every value object, entity, command and query it declares.
143
+ #
144
+ # @param aggregate [Hash{String => Object}] one aggregate's blueprint entry
145
+ # @return [Array<String>] source lines, unindented at the block's own `aggregate`/`end`
112
146
  def render_aggregate(aggregate)
113
147
  out = aggregate_header(aggregate)
114
148
  aggregate["attributes"].each { |attribute| out << " #{render_attribute(attribute)}" }
@@ -122,12 +156,22 @@ module Hecks
122
156
  out << " end"
123
157
  end
124
158
 
159
+ # Renders an aggregate block's opening lines, up through `identified_by`.
160
+ #
161
+ # @param aggregate [Hash{String => Object}] one aggregate's blueprint entry
162
+ # @return [Array<String>] the `aggregate ... do`, `description` and
163
+ # `identified_by` lines
125
164
  def aggregate_header(aggregate)
126
165
  ["", " aggregate #{aggregate['name'].inspect} do",
127
166
  " description #{"A generated #{aggregate['name'].downcase}.".inspect}", "",
128
167
  " identified_by #{aggregate['identity'].map { |part| ":#{part}" }.join(', ')}"]
129
168
  end
130
169
 
170
+ # Renders one entity's whole `entity ... end` block.
171
+ #
172
+ # @param entity [Hash{String => Object}] one entity's blueprint entry, as built
173
+ # by `Builder#entity`
174
+ # @return [Array<String>] source lines, unindented at the block's own `entity`/`end`
131
175
  def render_entity(entity)
132
176
  out = ["", " entity #{entity['name'].inspect} do",
133
177
  " description #{"A generated #{entity['name'].downcase}.".inspect}"]
@@ -138,6 +182,12 @@ module Hecks
138
182
  out << " end"
139
183
  end
140
184
 
185
+ # Renders one `attribute` declaration line.
186
+ #
187
+ # @param attribute [Hash{String => Object}] `"name"`, `"type"`, and optional
188
+ # `"list"`, `"optional"`, `"default"` keys
189
+ # @return [String] one `attribute :name, Type` line, with `list_of`, `optional:`
190
+ # and `default:` added as the Hash calls for
141
191
  def render_attribute(attribute)
142
192
  type = attribute["list"] ? "list_of(#{attribute['type']})" : attribute["type"]
143
193
  line = "attribute :#{attribute['name']}, #{type}"
@@ -146,6 +196,13 @@ module Hecks
146
196
  line
147
197
  end
148
198
 
199
+ # Renders one `value_object ... end` block.
200
+ #
201
+ # @param name [String] the value object's declared name
202
+ # @param value_object [Hash{String => Object}] `"kind"` (`"string"`, `"positive"`,
203
+ # `"integer"` or `"closed"`) and, for `"closed"`, a `"members"` array
204
+ # @param indent [String] leading whitespace for every rendered line
205
+ # @return [Array<String>] source lines, indented by `indent`
149
206
  def render_value_object(name, value_object, indent)
150
207
  body =
151
208
  case value_object["kind"]
@@ -162,6 +219,12 @@ module Hecks
162
219
  ["", "#{indent}value_object #{name.inspect} do", *body.map { |line| "#{indent} #{line}" }, "#{indent}end"]
163
220
  end
164
221
 
222
+ # Renders one `lifecycle ... end` block.
223
+ #
224
+ # @param lifecycle [Hash{String => Object}] `"field"`, `"default"` and
225
+ # `"transitions"` (each `"command"`, `"to"`, `"from"`)
226
+ # @param indent [String] leading whitespace for every rendered line
227
+ # @return [Array<String>] source lines, indented by `indent`
165
228
  def render_lifecycle(lifecycle, indent)
166
229
  out = ["", "#{indent}lifecycle :#{lifecycle['field']}, default: #{lifecycle['default'].inspect} do"]
167
230
  lifecycle["transitions"].each do |transition|
@@ -171,6 +234,15 @@ module Hecks
171
234
  out << "#{indent}end"
172
235
  end
173
236
 
237
+ # Renders one `command ... end` block.
238
+ #
239
+ # @param command [Hash{String => Object}] `"name"`, `"creates"`, `"role"`,
240
+ # `"references"`, `"args"`, `"givens"`, `"sets"` and `"emits"`
241
+ # @param self_name [String, nil] the owning aggregate's name, added as a
242
+ # `reference_to` on a non-creating entity command; nil for an
243
+ # aggregate-level command, which needs no reference to itself
244
+ # @param indent [String] leading whitespace for every rendered line
245
+ # @return [Array<String>] source lines, indented by `indent`
174
246
  def render_command(command, self_name, indent)
175
247
  out = ["", "#{indent}command #{command['name'].inspect} do"]
176
248
  out << "#{indent} role #{command['role'].inspect}" if command["role"]
@@ -184,6 +256,12 @@ module Hecks
184
256
  out << "#{indent}end"
185
257
  end
186
258
 
259
+ # Renders one `sets` line inside a command block.
260
+ #
261
+ # @param set [Hash{String => Object}] `"target"`, plus either `"to"` (rename) or
262
+ # `"append"` (a Hash of field => value token to append with); neither means a
263
+ # bare positional set
264
+ # @return [String] one `sets :target[, to: :value | append: { ... }]` line
187
265
  def render_set(set)
188
266
  return "sets :#{set['target']}, to: :#{set['to']}" if set["to"]
189
267
  return "sets :#{set['target']}, append: { #{set['append'].map { |k, v| "#{k}: :#{v}" }.join(', ')} }" if set["append"]
@@ -191,6 +269,11 @@ module Hecks
191
269
  "sets :#{set['target']}"
192
270
  end
193
271
 
272
+ # Renders one `query ... end` block.
273
+ #
274
+ # @param query [Hash{String => Object}] `"name"`, `"wheres"` (each `"field"`,
275
+ # `"value"`) and `"order_by"`
276
+ # @return [Array<String>] source lines, unindented at the block's own `query`/`end`
194
277
  def render_query(query)
195
278
  out = ["", " query #{query['name'].inspect} do", " description #{"Generated #{query['name']}.".inspect}"]
196
279
  query["wheres"].each do |where|
@@ -200,6 +283,10 @@ module Hecks
200
283
  out << " end"
201
284
  end
202
285
 
286
+ # Renders one `policy ... end` block.
287
+ #
288
+ # @param policy [Hash{String => Object}] `"name"`, `"on"` and `"trigger"`
289
+ # @return [Array<String>] source lines, unindented at the block's own `policy`/`end`
203
290
  def render_policy(policy)
204
291
  ["", " policy #{policy['name'].inspect} do", " on #{policy['on']}", " trigger #{policy['trigger']}", " end"]
205
292
  end
@@ -209,10 +296,19 @@ module Hecks
209
296
  # Every blueprint one removal smaller, each already pruned. Identity
210
297
  # attributes and creating commands are never offered: without them
211
298
  # there is no domain left to dispatch against.
299
+ #
300
+ # @param blueprint [Hash{String => Object}] a blueprint as `generate` answers
301
+ # @return [Array<Hash{String => Object}>] one pruned blueprint per candidate removal,
302
+ # deduplicated and never including `blueprint` itself
212
303
  def shrink_candidates(blueprint)
213
304
  removals(blueprint).map { |path| prune(remove_at(blueprint, path)) }.uniq.reject { |candidate| candidate == blueprint }
214
305
  end
215
306
 
307
+ # Every path into `blueprint` naming one element a shrink could remove.
308
+ #
309
+ # @param blueprint [Hash{String => Object}] a blueprint as `generate` answers
310
+ # @return [Array<Array<String, Integer>>] one path per removable element, each
311
+ # a sequence of Hash keys and Array indices `Hash#dig` can follow
216
312
  def removals(blueprint)
217
313
  paths = blueprint["policies"].each_index.map { |i| ["policies", i] }
218
314
  blueprint["aggregates"].each_with_index do |aggregate, a|
@@ -229,6 +325,12 @@ module Hecks
229
325
  paths
230
326
  end
231
327
 
328
+ # `removals`'s own entity half — one path per entity's lifecycle (if any) and
329
+ # per entity command.
330
+ #
331
+ # @param aggregate [Hash{String => Object}] one aggregate's blueprint entry
332
+ # @param at [Array<String, Integer>] the path prefix to `aggregate` itself
333
+ # @return [Array<Array<String, Integer>>] one path per removable entity element
232
334
  def entity_removals(aggregate, at)
233
335
  aggregate["entities"].each_with_index.flat_map do |entity, e|
234
336
  paths = entity["lifecycle"] ? [[*at, "entities", e, "lifecycle"]] : []
@@ -236,6 +338,13 @@ module Hecks
236
338
  end
237
339
  end
238
340
 
341
+ # `removals`'s own command half — one path for the whole command (unless it
342
+ # creates the aggregate), one per given, and one for its role or its last
343
+ # extra emit.
344
+ #
345
+ # @param command [Hash{String => Object}] one command's blueprint entry
346
+ # @param at [Array<String, Integer>] the path prefix to `command` itself
347
+ # @return [Array<Array<String, Integer>>] one path per removable command element
239
348
  def command_removals(command, at)
240
349
  paths = command["creates"] ? [] : [at]
241
350
  command["givens"].each_index { |g| paths << [*at, "givens", g] }
@@ -244,6 +353,13 @@ module Hecks
244
353
  paths
245
354
  end
246
355
 
356
+ # Deep-copies `blueprint` and deletes whatever `path` names.
357
+ #
358
+ # @param blueprint [Hash{String => Object}] a blueprint as `generate` answers
359
+ # @param path [Array<String, Integer>] a path as `removals` returns, its last
360
+ # element the Hash key or Array index to delete
361
+ # @return [Hash{String => Object}] a deep copy of `blueprint` with that one
362
+ # element removed
247
363
  def remove_at(blueprint, path)
248
364
  copy = JSON.parse(JSON.generate(blueprint))
249
365
  *parents, last = path
@@ -257,6 +373,11 @@ module Hecks
257
373
  # what must still exist (`attribute:Ticket.score`, `lifecycle:Desk`,
258
374
  # `reference:Ticket->Desk`, `command:Ticket.Close`, …) — so this is
259
375
  # set arithmetic over those tokens, never a reading of the source.
376
+ #
377
+ # @param blueprint [Hash{String => Object}] a blueprint as `generate` answers
378
+ # @return [Hash{String => Object}] a deep copy of `blueprint` with every element
379
+ # whose `requires` tokens are not all still present removed, repeated until
380
+ # nothing more can be dropped
260
381
  def prune(blueprint)
261
382
  current = JSON.parse(JSON.generate(blueprint))
262
383
  loop do
@@ -268,12 +389,28 @@ module Hecks
268
389
  current
269
390
  end
270
391
 
392
+ # One pass of `prune` — drops every policy, then every aggregate element,
393
+ # whose `requires` tokens are not all in `available`.
394
+ #
395
+ # @param blueprint [Hash{String => Object}] the blueprint being pruned, mutated
396
+ # in place
397
+ # @param available [Set<String>] every token `tokens` finds still declared
398
+ # @return [void]
271
399
  def prune_once!(blueprint, available)
272
400
  keep = ->(item) { Array(item["requires"]).all? { |token| available.include?(token) } }
273
401
  blueprint["policies"].select!(&keep)
274
402
  blueprint["aggregates"].each { |aggregate| prune_aggregate!(aggregate, available, keep) }
275
403
  end
276
404
 
405
+ # `prune_once!`'s own per-aggregate pass — drops attributes, invariants,
406
+ # entities, unsatisfied references and empty queries, then recurses into
407
+ # each remaining lifecycle and command.
408
+ #
409
+ # @param aggregate [Hash{String => Object}] one aggregate's blueprint entry,
410
+ # mutated in place
411
+ # @param available [Set<String>] every token `tokens` finds still declared
412
+ # @param keep [Proc] the `requires`-satisfied predicate `prune_once!` built
413
+ # @return [void]
277
414
  def prune_aggregate!(aggregate, available, keep)
278
415
  aggregate["references"].select! { |target| available.include?("aggregate:#{target}") }
279
416
  %w[attributes invariants entities].each { |key| aggregate[key].select!(&keep) }
@@ -285,6 +422,14 @@ module Hecks
285
422
  end
286
423
  end
287
424
 
425
+ # Drops transitions whose own command no longer exists, then whatever that
426
+ # leaves unreachable from the default state; clears `"lifecycle"` outright
427
+ # once no transition survives.
428
+ #
429
+ # @param owner [Hash{String => Object}] an aggregate or entity blueprint entry,
430
+ # mutated in place
431
+ # @param available [Set<String>] every token `tokens` finds still declared
432
+ # @return [void]
288
433
  def prune_lifecycle!(owner, available)
289
434
  return unless owner["lifecycle"]
290
435
 
@@ -297,9 +442,14 @@ module Hecks
297
442
 
298
443
  # Every state a path from the default reaches. A transition out of a
299
444
  # state nothing reaches can never fire, so a removal that orphans one
300
- # goes too: shrinking away `Close` used to leave `Reopen from closed`
301
- # behind — qa/stress_domains/generated_revalued_shape was promoted
302
- # that way, and bin/model_check reports it as a dead transition.
445
+ # goes too — without this, shrinking away `Close` leaves `Reopen from
446
+ # closed` behind, the shape `qa/stress_domains/generated_revalued_shape`
447
+ # was promoted with, which `bin/model_check` reports as a dead transition.
448
+ #
449
+ # @param default [String] the lifecycle's default state name
450
+ # @param transitions [Array<Hash{String => Object}>] the lifecycle's surviving
451
+ # transitions, each with `"from"` (an Array of state names) and `"to"`
452
+ # @return [Array<String>] every state reachable from `default`, `default` included
303
453
  def reachable_states(default, transitions)
304
454
  reachable = [default]
305
455
  loop do
@@ -310,12 +460,25 @@ module Hecks
310
460
  end
311
461
  end
312
462
 
463
+ # Drops a command's own unsatisfied references, args, givens and sets.
464
+ #
465
+ # @param command [Hash{String => Object}] one command's blueprint entry,
466
+ # mutated in place
467
+ # @param available [Set<String>] every token `tokens` finds still declared
468
+ # @return [void]
313
469
  def prune_command!(command, available)
314
470
  keep = ->(item) { Array(item["requires"]).all? { |token| available.include?(token) } }
315
471
  command["references"].select! { |target| available.include?("aggregate:#{target}") }
316
472
  %w[args givens sets].each { |key| command[key].select!(&keep) }
317
473
  end
318
474
 
475
+ # Every declaration token `blueprint` currently makes available — what a
476
+ # `requires` entry elsewhere in the blueprint can point at.
477
+ #
478
+ # @param blueprint [Hash{String => Object}] a blueprint as `generate` answers
479
+ # @return [Set<String>] tokens such as `"aggregate:Ticket"`,
480
+ # `"attribute:Ticket.score"`, `"lifecycle:Desk"`, `"reference:Ticket->Desk"`,
481
+ # `"command:Ticket.Close"` and `"event:Ticket.TicketOpened"`
319
482
  def tokens(blueprint)
320
483
  blueprint["aggregates"].each_with_object(Set.new) do |aggregate, set|
321
484
  name = aggregate["name"]
@@ -336,6 +499,11 @@ module Hecks
336
499
  end
337
500
  end
338
501
 
502
+ # Converts a rendered `CamelCase` aggregate/entity name to the `snake_case`
503
+ # form its own `reference_to`/`sets` field uses.
504
+ #
505
+ # @param name [String] a `CamelCase` name
506
+ # @return [String] `name` in `snake_case`
339
507
  def snake(name) = name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
340
508
 
341
509
  # ── building ────────────────────────────────────────────────────
@@ -343,6 +511,10 @@ module Hecks
343
511
  # One seeded pass: the aggregates the forms need, the forms forced
344
512
  # onto the first ("primary") aggregate, then extras everywhere.
345
513
  class Builder
514
+ # @param random [Random] the seeded generator every random choice this
515
+ # builder makes draws from
516
+ # @param forms [Array<String>] the `FormCensus::FORMS` names to force onto
517
+ # the primary aggregate
346
518
  def initialize(random, forms)
347
519
  @random = random
348
520
  @forms = forms
@@ -350,6 +522,11 @@ module Hecks
350
522
  @policies = []
351
523
  end
352
524
 
525
+ # Builds the aggregates the forced forms need, applies each forced form to
526
+ # the primary aggregate, then sprinkles extras onto every aggregate.
527
+ #
528
+ # @return [Hash{String => Object}] `"aggregates"` and `"policies"`, the two
529
+ # keys `DomainGenerator.generate` merges `"seed"`/`"forms"` into
353
530
  def build
354
531
  chain = @forms.intersect?(CHAIN_FORMS)
355
532
  count = if chain then 3
@@ -8,7 +8,9 @@ module Hecks
8
8
  # target's own real, already-configured `PostgresEra` ledger:
9
9
  # does any ancestor era still hold writes nobody has merged forward?
10
10
  #
11
- # **The bug this targets, named exactly**. Minting a new era (an attribute
11
+ # ## The bug this targets, named exactly
12
+ #
13
+ # Minting a new era (an attribute
12
14
  # or aggregate addition — `StorageShape.project`, lib/hecks/ports/
13
15
  # persistence/plugins/era/storage_shape.rb) advances the readable head
14
16
  # to a new partition; an old checkout, or a process that boots slower
@@ -26,6 +28,8 @@ module Hecks
26
28
  # line already answers by hand, run as an ordinary sweep Check instead
27
29
  # of only when a human remembers to ask.
28
30
  #
31
+ # ## Connection and safety
32
+ #
29
33
  # The target's real database, read-only, never a disposable one — every
30
34
  # other Postgres-touching mode here (`persistence_parity`,
31
35
  # `adapter_parity_sqlite`) owns a throwaway schema for the exact
@@ -44,19 +48,27 @@ module Hecks
44
48
  module EraBoundary
45
49
  module_function
46
50
 
51
+ # Reports whether any ancestor `PostgresEra` era still holds writes the
52
+ # head has never merged forward, for one target's real database.
53
+ #
47
54
  # `{ checked: true, diverged_total:, breakdown: [{ordinal:, diverged:}] }`
48
55
  # or `{ checked: false, kind:, reason: "..." }`. A `true` with
49
56
  # `diverged_total.positive?` is the finding this module exists to
50
57
  # surface: real post-cut writes an ancestor era is still holding.
51
58
  #
52
- # `kind:` splits the two things `checked: false` used to mean. Both
53
- # answered the same shape, and `bin/qa_sweep` logged that shape as a
54
- # held Check either way — so a refused connection or a `Lineage`
55
- # defect read, in the ledger, exactly like an audit that ran and
56
- # found nothing: counted toward `sweep.made` and the target's clean
59
+ # `kind:` distinguishes the two different reasons `checked: false` can
60
+ # occur — without it, `bin/qa_sweep` would log a refused connection
61
+ # and a `Lineage` defect exactly like an audit that ran and found
62
+ # nothing, counting either toward `sweep.made` and the target's clean
57
63
  # streak. `:not_applicable` is a target with no lineage to audit at
58
64
  # all; `:error` is the audit failing to run, which the caller
59
65
  # surfaces as a finding rather than holding.
66
+ #
67
+ # @param domain_path [String] filesystem path to the target domain's bluebook
68
+ # directory, or a directory containing one
69
+ # @return [Hash{Symbol => Object}] on success, `{checked: true, era_count: Integer,
70
+ # breakdown: Array<Hash{ordinal: Integer, diverged: Integer}>, diverged_total: Integer}`;
71
+ # otherwise `{checked: false, kind: Symbol, reason: String}`
60
72
  def diverged_ancestor_writes(domain_path)
61
73
  registry, directory = load_registry(domain_path)
62
74
  bluebook = registry.bluebooks.values.first
@@ -92,22 +104,40 @@ module Hecks
92
104
  end
93
105
  rescue StandardError => e
94
106
  # Could not audit is not the same as nothing to audit — a refused
95
- # connection, a `Lineage` defect, malformed `.world` settings. Every
96
- # one of these used to answer the benign shape and be held.
107
+ # connection, a `Lineage` defect, malformed `.world` settings. Each
108
+ # of these answers `:error`, not the benign `:not_applicable` shape,
109
+ # so `bin/qa_sweep` surfaces it as a finding rather than holding it
110
+ # as a clean audit.
97
111
  failed("#{e.class}: #{e.message}")
98
112
  end
99
113
 
100
- # Nothing to audit — this target holds no PostgresEra lineage, so no
101
- # ancestor era can carry post-cut writes. Not a finding, and not
102
- # evidence of anything either: the caller logs no Check at all.
114
+ # Builds the `checked: false` result for a target with no PostgresEra
115
+ # lineage to audit — no ancestor era can carry post-cut writes when
116
+ # there is no lineage at all. Not a finding, and not evidence of
117
+ # anything either: the caller logs no Check at all.
118
+ #
119
+ # @param reason [String] human-readable explanation of why no lineage applies
120
+ # @return [Hash{Symbol => Object}] `{checked: false, kind: :not_applicable, reason: String}`
103
121
  def not_applicable(reason) = { checked: false, kind: :not_applicable, reason: reason }
104
122
 
105
- # The audit itself could not run — reported as a finding, never held.
123
+ # Builds the `checked: false` result for an audit that could not run
124
+ # — reported as a finding, never held.
125
+ #
126
+ # @param reason [String] human-readable explanation of what failed, typically
127
+ # `"#{exception.class}: #{exception.message}"`
128
+ # @return [Hash{Symbol => Object}] `{checked: false, kind: :error, reason: String}`
106
129
  def failed(reason) = { checked: false, kind: :error, reason: reason }
107
130
 
108
- # The same load `bin/merge_tail` itself performs (that script's own
109
- # top half) — a fresh `Registry`, never the ledger's own (this asks
110
- # about the swept target's lineage, not QualityControl's own).
131
+ # Boots a fresh registry for the domain at `domain_path` — the same
132
+ # load `bin/merge_tail` itself performs (that script's own top half),
133
+ # never the ledger's own registry (this asks about the swept target's
134
+ # lineage, not QualityControl's own).
135
+ #
136
+ # @param domain_path [String] filesystem path to the target domain's bluebook
137
+ # directory, or a directory containing one
138
+ # @return [Array(Runtime::Registry, String)] the booted registry and the resolved
139
+ # bluebook directory path
140
+ # @raise [Errno::ENOENT] if `domain_path` names no existing domain directory
111
141
  def load_registry(domain_path)
112
142
  loading = Hecks::Ports::Loading.bootstrap
113
143
  directory = loading.bluebook_directory(domain_path)