hecks 1.3.0 → 1.4.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 (355) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +5 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/prism.rb +4 -4
  23. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  24. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  25. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  26. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  27. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  28. data/lib/hecks/adapters/driven.rb +4 -4
  29. data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
  30. data/lib/hecks/behaviors/dsl.rb +2 -2
  31. data/lib/hecks/behaviors/expectations.rb +23 -23
  32. data/lib/hecks/behaviors/ir.rb +1 -1
  33. data/lib/hecks/behaviors/rspec.rb +1 -1
  34. data/lib/hecks/behaviors/runner.rb +2 -2
  35. data/lib/hecks/behaviors.rb +1 -1
  36. data/lib/hecks/bluebook/aggregate.rb +13 -13
  37. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  38. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  39. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  40. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  41. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  42. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  43. data/lib/hecks/bluebook/assembly.rb +9 -9
  44. data/lib/hecks/bluebook/attribute.rb +9 -9
  45. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  46. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  47. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  48. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  49. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
  50. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  51. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  52. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  53. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  54. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  55. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  56. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  57. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  58. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  59. data/lib/hecks/bluebook/capabilities.rb +27 -0
  60. data/lib/hecks/bluebook/chapter.rb +28 -9
  61. data/lib/hecks/bluebook/command.rb +12 -12
  62. data/lib/hecks/bluebook/domain_port.rb +9 -9
  63. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  64. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  65. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  66. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  67. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  68. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  69. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  70. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  72. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  73. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  74. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  75. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  76. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  77. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  78. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  79. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  80. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  81. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  82. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  83. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  84. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  85. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  86. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  87. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  88. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  89. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  90. data/lib/hecks/bluebook/entity.rb +11 -11
  91. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  92. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  93. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  94. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  95. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  97. data/lib/hecks/bluebook/hexagon.rb +1 -1
  98. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  99. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
  101. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  102. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  103. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  104. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  105. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  106. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  107. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  108. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  109. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  110. data/lib/hecks/bluebook/model_check.rb +170 -84
  111. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  112. data/lib/hecks/bluebook/policy.rb +15 -13
  113. data/lib/hecks/bluebook/process_manager.rb +14 -14
  114. data/lib/hecks/bluebook/project_register.rb +6 -6
  115. data/lib/hecks/bluebook/query.rb +4 -4
  116. data/lib/hecks/bluebook/read_model.rb +14 -14
  117. data/lib/hecks/bluebook/reference.rb +8 -8
  118. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  119. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  120. data/lib/hecks/bluebook/translation.rb +4 -4
  121. data/lib/hecks/bluebook/value_object.rb +6 -6
  122. data/lib/hecks/bluebook.rb +2 -2
  123. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  124. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  125. data/lib/hecks/codemod.rb +36 -35
  126. data/lib/hecks/construct.rb +6 -6
  127. data/lib/hecks/corpus.rb +317 -0
  128. data/lib/hecks/deprecation.rb +95 -0
  129. data/lib/hecks/doc/reference.rb +19 -19
  130. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  131. data/lib/hecks/facade/cli_door.rb +69 -10
  132. data/lib/hecks/facade/cli_runner.rb +105 -24
  133. data/lib/hecks/facade/command_request.rb +23 -0
  134. data/lib/hecks/facade/handle.rb +79 -32
  135. data/lib/hecks/facade/json_door.rb +106 -25
  136. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  137. data/lib/hecks/facade/surface/chapter.rb +26 -17
  138. data/lib/hecks/facade/surface.rb +16 -3
  139. data/lib/hecks/facade.rb +15 -4
  140. data/lib/hecks/forms/app.rb +46 -30
  141. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  142. data/lib/hecks/forms/field_renderer.rb +142 -6
  143. data/lib/hecks/forms/field_shape.rb +180 -19
  144. data/lib/hecks/forms/html.rb +51 -7
  145. data/lib/hecks/forms/index_renderer.rb +14 -2
  146. data/lib/hecks/forms/params.rb +120 -23
  147. data/lib/hecks/forms/port_argument.rb +2 -2
  148. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  149. data/lib/hecks/forms/record_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_table.rb +1 -1
  151. data/lib/hecks/forms/value_object_shape.rb +3 -3
  152. data/lib/hecks/forms.rb +24 -4
  153. data/lib/hecks/fqn.rb +1 -1
  154. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  155. data/lib/hecks/framework.rb +48 -17
  156. data/lib/hecks/freezer.rb +11 -11
  157. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  158. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  159. data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
  160. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  161. data/lib/hecks/fuzzing/differential.rb +158 -0
  162. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  163. data/lib/hecks/fuzzing/era_boundary.rb +37 -18
  164. data/lib/hecks/fuzzing/form_census.rb +35 -20
  165. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  166. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  167. data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
  168. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  169. data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
  170. data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
  171. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
  172. data/lib/hecks/fuzzing/properties/guards.rb +42 -42
  173. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  174. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
  175. data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
  176. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  177. data/lib/hecks/fuzzing/properties.rb +64 -40
  178. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  179. data/lib/hecks/fuzzing/replay.rb +114 -114
  180. data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
  181. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  182. data/lib/hecks/fuzzing/self_consistency.rb +141 -112
  183. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
  184. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  185. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  186. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  187. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
  188. data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
  189. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  190. data/lib/hecks/fuzzing/structural_skips.rb +23 -130
  191. data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
  192. data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
  193. data/lib/hecks/fuzzing/value_generator.rb +14 -14
  194. data/lib/hecks/fuzzing.rb +5 -0
  195. data/lib/hecks/grammar/evolve.rb +10 -10
  196. data/lib/hecks/grammar.rb +7 -7
  197. data/lib/hecks/ir.rb +13 -13
  198. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  199. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  200. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  201. data/lib/hecks/language/oidc.json +5 -0
  202. data/lib/hecks/literal.rb +9 -9
  203. data/lib/hecks/naming.rb +25 -25
  204. data/lib/hecks/ports/access_control.rb +58 -2
  205. data/lib/hecks/ports/agent/answers.rb +83 -6
  206. data/lib/hecks/ports/agent.rb +119 -35
  207. data/lib/hecks/ports/authentication.rb +44 -4
  208. data/lib/hecks/ports/authorization.rb +53 -11
  209. data/lib/hecks/ports/clock.rb +42 -23
  210. data/lib/hecks/ports/extraction.rb +16 -0
  211. data/lib/hecks/ports/identity_assignment.rb +24 -2
  212. data/lib/hecks/ports/identity_generation.rb +17 -3
  213. data/lib/hecks/ports/identity_resolution.rb +18 -1
  214. data/lib/hecks/ports/loading.rb +4 -0
  215. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  216. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  217. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  218. data/lib/hecks/ports/persistence/execution.rb +4 -0
  219. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  220. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  221. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  222. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  224. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  225. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  226. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  241. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  242. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  247. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  248. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  249. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  252. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  253. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  254. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  255. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  256. data/lib/hecks/ports/persistence.rb +36 -1
  257. data/lib/hecks/ports/projection.rb +61 -7
  258. data/lib/hecks/ports/query/in_memory.rb +3 -3
  259. data/lib/hecks/ports/query/ordering.rb +6 -6
  260. data/lib/hecks/ports/query.rb +35 -0
  261. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  262. data/lib/hecks/projections/diagrams.rb +75 -75
  263. data/lib/hecks/projections/glossary/html.rb +3 -3
  264. data/lib/hecks/projections/glossary/markdown.rb +3 -3
  265. data/lib/hecks/projections/glossary/mermaid.rb +1 -1
  266. data/lib/hecks/projections/glossary/sections.rb +1 -1
  267. data/lib/hecks/projections/glossary/sentences.rb +5 -5
  268. data/lib/hecks/projections/glossary.rb +15 -15
  269. data/lib/hecks/projections/ir.rb +1 -1
  270. data/lib/hecks/projections/model/deviations.rb +18 -17
  271. data/lib/hecks/projections/model.rb +25 -21
  272. data/lib/hecks/projections/oidc.rb +7 -7
  273. data/lib/hecks/projections/parser_table.rb +5 -5
  274. data/lib/hecks/projections/reference.rb +3 -3
  275. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  276. data/lib/hecks/projections/shape.rb +2 -2
  277. data/lib/hecks/projections/statements.rb +11 -11
  278. data/lib/hecks/projections/vocabulary.rb +9 -9
  279. data/lib/hecks/projections.rb +5 -3
  280. data/lib/hecks/projector/cli_projector.rb +29 -29
  281. data/lib/hecks/projector/docs_projector.rb +13 -13
  282. data/lib/hecks/projector/exporter.rb +42 -21
  283. data/lib/hecks/projector/ir_projector.rb +1 -1
  284. data/lib/hecks/projector/narrate_projector.rb +11 -11
  285. data/lib/hecks/projector/target.rb +13 -13
  286. data/lib/hecks/projector.rb +15 -15
  287. data/lib/hecks/query_ir.rb +47 -47
  288. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  289. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  290. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  291. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  292. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  293. data/lib/hecks/query_specification/common/options.rb +25 -0
  294. data/lib/hecks/query_specification/field_path.rb +69 -15
  295. data/lib/hecks/query_specification/hop_path.rb +57 -20
  296. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  297. data/lib/hecks/rendering.rb +3 -3
  298. data/lib/hecks/router/namespace_installer.rb +3 -3
  299. data/lib/hecks/router.rb +1 -1
  300. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  301. data/lib/hecks/runtime/boot_gates.rb +3 -3
  302. data/lib/hecks/runtime/caller.rb +8 -8
  303. data/lib/hecks/runtime/capability_graph.rb +2 -2
  304. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  305. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
  306. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  307. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  308. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  309. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  310. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  311. data/lib/hecks/runtime/command_rules/references.rb +35 -35
  312. data/lib/hecks/runtime/command_rules.rb +3 -0
  313. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  314. data/lib/hecks/runtime/dispatcher.rb +283 -90
  315. data/lib/hecks/runtime/entity_element.rb +151 -67
  316. data/lib/hecks/runtime/entity_interpreter.rb +108 -82
  317. data/lib/hecks/runtime/errors.rb +19 -19
  318. data/lib/hecks/runtime/event.rb +6 -6
  319. data/lib/hecks/runtime/identity.rb +22 -22
  320. data/lib/hecks/runtime/instance.rb +39 -14
  321. data/lib/hecks/runtime/interpreting.rb +12 -12
  322. data/lib/hecks/runtime/invocation.rb +276 -0
  323. data/lib/hecks/runtime/loader.rb +14 -14
  324. data/lib/hecks/runtime/outbox.rb +23 -23
  325. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  326. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  327. data/lib/hecks/runtime/query_interpreter.rb +55 -64
  328. data/lib/hecks/runtime/reaction_invocation.rb +20 -20
  329. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  330. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  331. data/lib/hecks/runtime/reference_hop.rb +6 -6
  332. data/lib/hecks/runtime/refusal_wording.rb +92 -115
  333. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  334. data/lib/hecks/runtime/registry/verification.rb +36 -26
  335. data/lib/hecks/runtime/registry.rb +45 -28
  336. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  337. data/lib/hecks/runtime/routing.rb +10 -153
  338. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  339. data/lib/hecks/runtime/saga_interpreter.rb +64 -64
  340. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  341. data/lib/hecks/runtime/tenant_check.rb +9 -9
  342. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  343. data/lib/hecks/runtime/value/admission.rb +75 -30
  344. data/lib/hecks/runtime/value/coercion.rb +318 -127
  345. data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
  346. data/lib/hecks/runtime/value.rb +21 -21
  347. data/lib/hecks/runtime.rb +7 -7
  348. data/lib/hecks/storehouse.rb +64 -64
  349. data/lib/hecks/version.rb +3 -3
  350. data/lib/hecks/vocabulary.rb +205 -4
  351. data/lib/hecks.rb +13 -11
  352. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  353. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  354. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  355. metadata +22 -2
@@ -76,6 +76,11 @@
76
76
  "verb": "Bluebook::Bluebook.Normalise",
77
77
  "role": "Language"
78
78
  },
79
+ {
80
+ "scope": "bluebook:bluebook.provide",
81
+ "verb": "Bluebook::Bluebook.Provide",
82
+ "role": "Language"
83
+ },
79
84
  {
80
85
  "scope": "bluebook:command.acts_on",
81
86
  "verb": "Bluebook::Command.ActsOn",
data/lib/hecks/literal.rb CHANGED
@@ -1,31 +1,31 @@
1
1
  module Hecks
2
- # THE ONE SPELLING FOR A CAPTURED RUBY LITERAL ON THE WIRE.
2
+ # The one spelling for a captured Ruby literal on the wire.
3
3
  #
4
4
  # Several `to_h` fields hold a value the author wrote in a bluebook —
5
5
  # `where(status: { eq: "open" })`, `then_set append: { direction: { value:
6
6
  # "credit" } }`, `dispatch ... with: { number: :source }`, an attribute's
7
- # `default:` — as TEXT, because the export has to stand on its own and a
7
+ # `default:` — as text, because the export has to stand on its own and a
8
8
  # String field cannot say "this one was a Symbol". Every such field is the
9
9
  # same question and now gets the same answer.
10
10
  #
11
11
  # It was three answers before, and they disagreed on both axes that matter.
12
- # `Mutation#appended_fields` spelled a Symbol BARE and everything else
13
- # `inspect`; `QuerySpecification.render_value` spelled a Symbol with a COLON
12
+ # `Mutation#appended_fields` spelled a Symbol bare and everything else
13
+ # `inspect`; `QuerySpecification.render_value` spelled a Symbol with a colon
14
14
  # and everything else `to_s`. So `:amount` crossed as "amount" in a mutation
15
15
  # and ":amount" in a saga binding — the same value, two spellings, and each
16
16
  # reader had to know which field it was looking at. Worse, `to_s`/`inspect`
17
- # on a Hash is Ruby's OWN rendering, which moved under us: 3.3 writes
17
+ # on a Hash is Ruby's own rendering, which moved under us: 3.3 writes
18
18
  # `{:value=>"credit"}` and 3.4 writes `{value: "credit"}` for the identical
19
19
  # Hash, so the wire format was silently pinned to an interpreter version.
20
20
  #
21
- # SELF-DESCRIBING IS THE RULE, stated once here rather than inherited from
21
+ # Self-describing is the rule, stated once here rather than inherited from
22
22
  # whatever `inspect` happens to do: a symbol wears its colon, a string wears
23
23
  # its quotes, a hash wears `{key: value}` braces, a list wears its brackets,
24
24
  # and a number, a boolean and nil are bare. `read` is the exact inverse, and
25
25
  # is the only thing that should ever take one of these strings apart.
26
- # A MUTATION SOURCE THAT READS THE RECORD'S OWN STATE — `sets :positions,
26
+ # A mutation source that reads the record's own state — `sets :positions,
27
27
  # append: { knights: state(:knights) }`. A bare Symbol in a mutation
28
- # source always names a command ARGUMENT (and imports it as one when
28
+ # source always names a command argument (and imports it as one when
29
29
  # the target's own field carries the same name — `CommandBuilder
30
30
  # #resolve_append_fields!`); before this there was no way to say "the
31
31
  # value this field already holds", so a command could not snapshot its
@@ -102,7 +102,7 @@ module Hecks
102
102
 
103
103
  def read_array(raw) = split_items(raw[1..-2]).map { |item| read(item) }
104
104
 
105
- # Split on the commas that are actually SEPARATORS — never one inside a
105
+ # Split on the commas that are actually separators — never one inside a
106
106
  # quoted string or a nested brace/bracket. Scanned rather than
107
107
  # `String#split(", ")`, which tore `"a, b"` in half and lost the second
108
108
  # field of anything nested.
data/lib/hecks/naming.rb CHANGED
@@ -5,14 +5,14 @@ module Hecks
5
5
  # two readers never invent two different spellings for the same
6
6
  # derivation.
7
7
  module Naming
8
- # WHAT SEPARATES THE PARTS OF A DERIVED IDENTITY.
8
+ # What separates the parts of a derived identity.
9
9
  #
10
- # An identity of several parts is their JOIN, and the join has to be spelled the
10
+ # An identity of several parts is their join, and the join has to be spelled the
11
11
  # same everywhere or two readers name two different records off one declaration.
12
12
  # It was spelled three ways at once here — "::" for an aggregate under a
13
13
  # chapter, "." for everything under an aggregate, "#" for the three that keyed
14
14
  # off position — and each was written where it happened to be needed. Once the
15
- # runtime derived the same identity, the runtime made a FOURTH, and a reference
15
+ # runtime derived the same identity, the runtime made a fourth, and a reference
16
16
  # that resolved by string comparison found nothing.
17
17
  IDENTITY_JOIN = ":".freeze
18
18
 
@@ -39,7 +39,7 @@ module Hecks
39
39
  .downcase
40
40
  end
41
41
 
42
- # AN IDENTIFIER AS A PERSON WOULD SAY IT — `ATMCard` -> "ATM card",
42
+ # An identifier as a person would say it — `ATMCard` -> "ATM card",
43
43
  # `AccrueInterest` -> "Accrue interest", `daily_limit` -> "Daily
44
44
  # limit", `Back office` -> "Back office". The same two word-boundary
45
45
  # splits `snake` uses, with a space instead of an underscore — plus
@@ -73,14 +73,14 @@ module Hecks
73
73
  end
74
74
  end
75
75
 
76
- # The vowel-LETTER heuristic — safe here for the same reason
76
+ # The vowel-letter heuristic — safe here for the same reason
77
77
  # `Projections::Statements#article` gives: a construct name is a
78
78
  # plain word, never "hour" or "university".
79
79
  def a_or_an(word)
80
80
  %w[a e i o u].include?(word.to_s[0].to_s.downcase) ? "an" : "a"
81
81
  end
82
82
 
83
- # The name a COLLECTION of something takes.
83
+ # The name a collection of something takes.
84
84
  #
85
85
  # There were two of these and one was wrong. A read model's gathered heads
86
86
  # derived their name with a bare `"#{snake(target)}s"`, so the meta-domain's
@@ -97,9 +97,9 @@ module Hecks
97
97
  "#{word}s"
98
98
  end
99
99
 
100
- # `has_many`'s undo — the plural WRITTEN, back to the singular the target
100
+ # `has_many`'s undo — the plural written, back to the singular the target
101
101
  # aggregate is actually named. Deliberately the crude half of a pair: `plural`
102
- # above earns its precision (three suffix rules) because getting a COLLECTION
102
+ # above earns its precision (three suffix rules) because getting a collection
103
103
  # name wrong reads as a typo forever ; this only ever recovers a name someone
104
104
  # already wrote as a real aggregate, so "ies -> y, trailing s dropped" is the
105
105
  # whole rule — enough for `has_many Invoices` to resolve to the aggregate
@@ -108,11 +108,11 @@ module Hecks
108
108
  word = text.to_s
109
109
  return "#{word[0..-4]}y" if word.length > 3 && word.end_with?("ies")
110
110
 
111
- # `plural`'s OWN second rule adds "es" (not bare "s") after
111
+ # `plural`'s own second rule adds "es" (not bare "s") after
112
112
  # s/x/z/ch/sh — undone here the same way, or a word `plural`
113
113
  # itself would have suffixed with "es" comes back missing its
114
114
  # own trailing letter ("Boxes" -> "Boxe", not "Box") once this
115
- # only ever knew how to drop a bare "s". Checked BEFORE the
115
+ # only ever knew how to drop a bare "s". Checked before the
116
116
  # bare-"s" rule below: stripping "es" first and confirming what
117
117
  # is left actually ends in one of those five shapes is what
118
118
  # keeps an ordinary "-es" word (e.g. "Invoices" -> "Invoice")
@@ -143,19 +143,19 @@ module Hecks
143
143
  text.include?(".") ? text.split(".", 2).last : text
144
144
  end
145
145
 
146
- # DOMAIN, AGGREGATE, then the REST dot-joined into one command path.
146
+ # Domain, aggregate, then the REST dot-joined into one command path.
147
147
  #
148
148
  # The `::` boundary between domain and aggregate is unambiguous by
149
149
  # construction — every caller here has already prefixed the domain
150
150
  # itself (`PolicyInterpreter#deliver`, `SagaInterpreter#qualified`,
151
151
  # `Router#dispatch`'s own rebuilt string) before this ever runs. A
152
- # THIRD `::` segment can still show up past that boundary: a bare
152
+ # third `::` segment can still show up past that boundary: a bare
153
153
  # `ScopedConstant` naming a port operation (`command_ref`'s own
154
154
  # comment — `Aggregate::Port::Operation`, three colon-joined
155
- # segments with no `.` of its own) only gets its LAST `::` rewritten
155
+ # segments with no `.` of its own) only gets its last `::` rewritten
156
156
  # to `.` there, at DSL-build time, because nothing at that point
157
157
  # knows yet whether the constant names a port operation or a
158
- # domain-qualified command (`Domain::Aggregate::Command`, the OTHER
158
+ # domain-qualified command (`Domain::Aggregate::Command`, the other
159
159
  # shape `command_ref` documents) — both are textually identical.
160
160
  # Here, past the already-resolved domain boundary, any leftover
161
161
  # `::` is unambiguous: it is that same rewrite artifact, and folding
@@ -179,19 +179,19 @@ module Hecks
179
179
  end
180
180
 
181
181
  # `trigger Account::Debit` / `dispatch Account::Debit` — a bare
182
- # CONSTANT reference (`ConstShim`'s own `ScopedConstant`, S0b), not
182
+ # constant reference (`ConstShim`'s own `ScopedConstant`, S0b), not
183
183
  # text (ADR 0025, "events and reactions" — command references become
184
- # first-class). Ruby's `::` joins EVERY segment the same way a
184
+ # first-class). Ruby's `::` joins every segment the same way a
185
185
  # constant path always does, but a command's own `hecks_fqn` joins
186
186
  # its aggregate with `.` (`Construct#hecks_separator`'s default,
187
- # only an AGGREGATE overrides it to `::`) — so only the LAST `::`
187
+ # only an aggregate overrides it to `::`) — so only the last `::`
188
188
  # becomes a `.`; everything before it (the chapter, when a domain is
189
189
  # spelled at all: `Banking::Account::Debit`) stays `::`-joined.
190
190
  #
191
- # A STRING PASSES THROUGH UNCHANGED, on purpose — legacy era text
191
+ # A string passes through unchanged, on purpose — legacy era text
192
192
  # (S0a's own shadow-parsed spelling) already mixes `::` (domain) and
193
193
  # `.` (command) correctly on its own, e.g. `"Banking::Account.Debit"`,
194
- # and re-splitting that by content rather than by TYPE would corrupt
194
+ # and re-splitting that by content rather than by type would corrupt
195
195
  # it (its own last `::` sits between the domain and the aggregate,
196
196
  # not the aggregate and the command). Only an actual constant object
197
197
  # — never seen holding a `.` of its own — needs the rewrite at all.
@@ -216,25 +216,25 @@ module Hecks
216
216
 
217
217
  # `transition Account::AccountDebited => "state"` / `starts_on
218
218
  # Transfer::TransferRequested` / `ends_on Transfer::TransferSettled`
219
- # — a process manager's OWN event references (ADR 0025, S6),
220
- # DELIBERATELY NOT `event_ref` — found live, not assumed, wiring a
219
+ # — a process manager's own event references (ADR 0025, S6),
220
+ # deliberately not `event_ref` — found live, not assumed, wiring a
221
221
  # real migrated corpus site into `bin/model_check` for the first
222
222
  # time (2026-08-28): `SagaInterpreter#begin_saga`/`#advance_saga`
223
223
  # match `pm.starts_on`/`pm.handler_for` against `event.name`, which
224
- # `CommandRules::Emission#emit` stamps BARE — a command's own
224
+ # `CommandRules::Emission#emit` stamps bare — a command's own
225
225
  # `emits AccountDebited` never carries its aggregate's name at all
226
226
  # (unlike a policy's cross-aggregate `on`, matched instead by
227
227
  # `Naming.demodulise(event.aggregate)` split apart from the bare
228
228
  # name — `PolicyInterpreter#policies_for`). Handing a saga's own
229
- # matcher the DOTTED `event_ref` form ("Account.AccountDebited")
229
+ # matcher the dotted `event_ref` form ("Account.AccountDebited")
230
230
  # would silently name an event no command in the domain ever
231
231
  # actually emits — caught by `bin/model_check`'s own `deaf_handler`/
232
232
  # `deaf_trigger` findings the moment a real qualified corpus site
233
233
  # existed to trip them, not by any unit test in isolation.
234
234
  #
235
- # A qualifier is still worth WRITING (`Account::`) — the same
235
+ # A qualifier is still worth writing (`Account::`) — the same
236
236
  # provenance a reader gets from `trigger Account::Debit` — it is
237
- # only not worth KEEPING: `demodulise` drops everything but the
237
+ # only not worth keeping: `demodulise` drops everything but the
238
238
  # final segment, so `Account::AccountDebited` and a bare
239
239
  # `AccountDebited` resolve to the identical stored string. A String
240
240
  # passes through unchanged either way, exactly like `command_ref`'s
@@ -2,7 +2,7 @@ require_relative "../runtime/registry"
2
2
 
3
3
  module Hecks
4
4
  module Ports
5
- # WHO CAN SIGN IN, AND WHAT ROLE DO THEY HOLD — the domain-specific
5
+ # Who can sign in, and what role do they hold — the domain-specific
6
6
  # half of sign-in `Ports::Authentication` deliberately stays out of
7
7
  # (see that port's own header: it only ever talks to the external
8
8
  # provider, never this registry's own data). Resolved the same way
@@ -13,7 +13,7 @@ module Hecks
13
13
  # Every domain shapes this differently — who its own "a person who
14
14
  # can sign in" aggregate is, what admits them, what grants them
15
15
  # access — so this port is pure delegation, same as every sibling
16
- # port. The one piece of this that IS generic (a live Governance
16
+ # port. The one piece of this that is generic (a live Governance
17
17
  # grant should win over whatever an aggregate's own role field
18
18
  # says) lives on `Ports::Authorization#live_role_for` instead —
19
19
  # a domain's adapter here calls that directly, rather than this
@@ -23,26 +23,82 @@ module Hecks
23
23
 
24
24
  module_function
25
25
 
26
+ # Asks the domain's adapter for the session an already-resolved identity signs into.
27
+ #
28
+ # No adapter or spec double for this port ships in this repository, so every shape
29
+ # below other than `registry` is adapter-defined: the port forwards it untouched.
30
+ #
31
+ # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
32
+ # and handed on to it
33
+ # @param identity_id [Object] adapter-defined identity key, forwarded unchanged
34
+ # @return [Object] adapter-defined session representation
35
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
36
+ # (see `adapter`)
26
37
  def session_for_identity(registry, identity_id:)
27
38
  adapter(registry).session_for_identity(registry, identity_id: identity_id)
28
39
  end
29
40
 
41
+ # Admits a new person, in whatever way this domain's adapter defines admission.
42
+ #
43
+ # The keywords carry the names of a verified sign-in (`Ports::Authentication.verify`
44
+ # answers `issuer`, `subject` and `email`), but nothing in this repository wires the
45
+ # two together, so their shapes here are adapter-defined.
46
+ #
47
+ # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
48
+ # and handed on to it
49
+ # @param email [Object] adapter-defined, forwarded unchanged; the person's email
50
+ # @param issuer [Object] adapter-defined, forwarded unchanged; the OIDC issuer that
51
+ # authenticated the person
52
+ # @param subject [Object] adapter-defined, forwarded unchanged; the OIDC subject the
53
+ # issuer vouches for
54
+ # @return [Object] adapter-defined representation of the newly admitted person
55
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
56
+ # (see `adapter`)
30
57
  def provision(registry, email:, issuer:, subject:)
31
58
  adapter(registry).provision(registry, email: email, issuer: issuer, subject: subject)
32
59
  end
33
60
 
61
+ # Asks the domain's adapter which roles it can grant.
62
+ #
63
+ # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
64
+ # and handed on to it
65
+ # @return [Object] adapter-defined collection of grantable roles
66
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
67
+ # (see `adapter`)
34
68
  def available_roles(registry)
35
69
  adapter(registry).available_roles(registry)
36
70
  end
37
71
 
72
+ # Grants a role to a person, by whatever means the domain's adapter records a grant.
73
+ #
74
+ # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
75
+ # and handed on to it
76
+ # @param email [Object] adapter-defined, forwarded unchanged; the person receiving the role
77
+ # @param role [Object] adapter-defined, forwarded unchanged; the role to grant
78
+ # @return [Object] adapter-defined representation of the grant
79
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
80
+ # (see `adapter`)
38
81
  def grant(registry, email:, role:)
39
82
  adapter(registry).grant(registry, email: email, role: role)
40
83
  end
41
84
 
85
+ # Lists every person the domain's adapter knows about.
86
+ #
87
+ # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
88
+ # and handed on to it
89
+ # @return [Object] adapter-defined collection of people
90
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
91
+ # (see `adapter`)
42
92
  def all_people(registry)
43
93
  adapter(registry).all_people(registry)
44
94
  end
45
95
 
96
+ # Finds the single adapter bound to this port, refusing an ambiguous wiring.
97
+ #
98
+ # @param registry [Runtime::Registry] the booted registry to search
99
+ # @return [Module] the adapter module or class implementing this port
100
+ # @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
101
+ # or the one that does has no Ruby implementation under `Hecks::Adapters`
46
102
  def adapter(registry)
47
103
  implementations = registry.adapters.values.select { |a| a.port == NAME }
48
104
 
@@ -1,11 +1,11 @@
1
1
  module Hecks
2
2
  module Ports
3
3
  module Agent
4
- # WHERE A RAW HASH BECOMES A STRUCT — the one place, so every
4
+ # Where a raw hash becomes a struct — the one place, so every
5
5
  # adapter (the real `claude_code` one, and any scripted double
6
6
  # standing in for it in a spec) is held to the identical shape.
7
7
  # An adapter's whole job ends at "here is what came back, already
8
- # JSON"; whether that hash is USABLE is decided once, here, not
8
+ # JSON"; whether that hash is usable is decided once, here, not
9
9
  # re-decided per adapter.
10
10
  #
11
11
  # Every failure raises `ValidationError` — never lets a bare
@@ -18,16 +18,43 @@ module Hecks
18
18
 
19
19
  module_function
20
20
 
21
+ # Validates an adapter's raw answer to `ask` into `Question` structs.
22
+ #
23
+ # @param raw [Hash{String => Object}] the adapter's parsed reply, expected to hold a
24
+ # `"questions"` array of `{"text" =>, "because" =>}` rows; any other shape is refused
25
+ # @return [Array<Ports::Agent::Question>] one struct per row, in the answer's order;
26
+ # `[]` for an empty `"questions"` array
27
+ # @raise [Ports::Agent::ValidationError] if `raw` is not a Hash, holds no
28
+ # `"questions"` array, or a row's `"text"` or `"because"` is missing or blank
21
29
  def questions(raw)
22
30
  rows(raw, "questions").map { |row| Question.new(text: text!(row, "text"), because: text!(row, "because")) }
23
31
  end
24
32
 
33
+ # Validates an adapter's raw answer to `interpret` into `Proposal` structs.
34
+ #
35
+ # @param raw [Hash{String => Object}] the adapter's parsed reply, expected to hold a
36
+ # `"proposals"` array of `{"verb" =>, "rationale" =>, "arguments" =>}` rows
37
+ # @return [Array<Ports::Agent::Proposal>] one struct per row, its `arguments` an Array
38
+ # of `{name:, field:, value:}` Hashes (see `arguments!`); `[]` for an empty
39
+ # `"proposals"` array
40
+ # @raise [Ports::Agent::ValidationError] if `raw` is not a Hash, holds no
41
+ # `"proposals"` array, or a row has a verb that is not fully qualified, a blank
42
+ # `"rationale"`, or an argument row with no `"name"`
25
43
  def proposals(raw)
26
44
  rows(raw, "proposals").map do |row|
27
45
  Proposal.new(verb: verb!(row), rationale: text!(row, "rationale"), arguments: arguments!(row))
28
46
  end
29
47
  end
30
48
 
49
+ # Validates an adapter's raw answer to `critique` into `Finding` structs.
50
+ #
51
+ # @param raw [Hash{String => Object}] the adapter's parsed reply, expected to hold a
52
+ # `"findings"` array of `{"kind" =>, "severity" =>, "subject" =>, "message" =>}` rows
53
+ # @return [Array<Ports::Agent::Finding>] one struct per row, with `kind` and `severity`
54
+ # as Symbols; `[]` for an empty `"findings"` array
55
+ # @raise [Ports::Agent::ValidationError] if `raw` is not a Hash, holds no
56
+ # `"findings"` array, or a row's kind is outside `CRITIQUE_KINDS`, its severity is
57
+ # outside `SEVERITIES`, or its `"subject"` or `"message"` is missing or blank
31
58
  def findings(raw)
32
59
  rows(raw, "findings").map do |row|
33
60
  Finding.new(kind: kind!(row), severity: severity!(row), subject: text!(row, "subject"),
@@ -35,6 +62,14 @@ module Hecks
35
62
  end
36
63
  end
37
64
 
65
+ # Validates an adapter's raw answer to `suggest_name` into `Suggestion` structs.
66
+ #
67
+ # @param raw [Hash{String => Object}] the adapter's parsed reply, expected to hold a
68
+ # `"names"` array of `{"name" =>, "because" =>, "rejected" =>}` rows
69
+ # @return [Array<Ports::Agent::Suggestion>] one struct per row, its `rejected` an
70
+ # Array of Strings (`[]` when the row carries none); `[]` for an empty `"names"` array
71
+ # @raise [Ports::Agent::ValidationError] if `raw` is not a Hash, holds no `"names"`
72
+ # array, or a row's `"name"` or `"because"` is missing or blank
38
73
  def suggestions(raw)
39
74
  rows(raw, "names").map do |row|
40
75
  Suggestion.new(name: text!(row, "name"), because: text!(row, "because"),
@@ -44,6 +79,13 @@ module Hecks
44
79
 
45
80
  # ── shared checks, each named for what it refuses ───────────────
46
81
 
82
+ # Reads the array of rows an answer holds under one key, refusing any other shape.
83
+ #
84
+ # @param raw [Object] the adapter's reply; only a Hash is accepted
85
+ # @param key [String] the key the rows sit under, such as `"questions"`
86
+ # @return [Array<Object>] the rows exactly as the adapter gave them, unvalidated
87
+ # @raise [Ports::Agent::ValidationError] if `raw` is not a Hash, or `raw[key]` is
88
+ # not an Array
47
89
  def rows(raw, key)
48
90
  raise ValidationError, "expected a Hash back, got #{raw.class}: #{raw.inspect}" unless raw.is_a?(Hash)
49
91
 
@@ -53,6 +95,13 @@ module Hecks
53
95
  raise ValidationError, "no #{key.inspect} array in the answer: #{raw.inspect}"
54
96
  end
55
97
 
98
+ # Reads one required text field off a row, refusing a missing or blank one.
99
+ #
100
+ # @param row [Hash{String => Object}] one row of the adapter's answer
101
+ # @param key [String] the field to read, such as `"because"`
102
+ # @return [String] the field's value as a String, whitespace untouched
103
+ # @raise [Ports::Agent::ValidationError] if the field is nil, false, empty, or only
104
+ # whitespace
56
105
  def text!(row, key)
57
106
  value = row[key]
58
107
  return value.to_s if value && !value.to_s.strip.empty?
@@ -60,12 +109,19 @@ module Hecks
60
109
  raise ValidationError, "#{key.inspect} missing or blank in #{row.inspect}"
61
110
  end
62
111
 
63
- # A VERB THE LANGUAGE COULD NOT EVEN PARSE IS AN ADAPTER FAULT,
112
+ # Reads a proposal row's verb, refusing one that is not fully qualified.
113
+ #
114
+ # A verb the language could not even parse is an adapter fault,
64
115
  # refused right here — the same `not_fully_qualified` shape the
65
116
  # language's own grammar already refuses by, reused rather than
66
117
  # reinvented. A verb naming a real category that turns out to
67
- # describe the wrong fact is NOT this port's business: that one
118
+ # describe the wrong fact is not this port's business: that one
68
119
  # dispatches, and `Interview::Session#offer` is what says no.
120
+ #
121
+ # @param row [Hash{String => Object}] one proposal row of the adapter's answer
122
+ # @return [String] the row's `"verb"`, shaped `Chapter::Aggregate.Command`
123
+ # @raise [Ports::Agent::ValidationError] if the verb is missing or does not match
124
+ # `VERB_PATTERN`
69
125
  def verb!(row)
70
126
  verb = row["verb"].to_s
71
127
  return verb if VERB_PATTERN.match?(verb)
@@ -73,6 +129,12 @@ module Hecks
73
129
  raise ValidationError, "#{verb.inspect} is not a fully-qualified verb (Chapter::Aggregate.Command)"
74
130
  end
75
131
 
132
+ # Reads a finding row's kind, refusing one outside the closed critique vocabulary.
133
+ #
134
+ # @param row [Hash{String => Object}] one finding row of the adapter's answer
135
+ # @return [Symbol] the row's `"kind"`, a member of `CRITIQUE_KINDS`
136
+ # @raise [Ports::Agent::ValidationError] if the kind is missing or not in
137
+ # `CRITIQUE_KINDS`
76
138
  def kind!(row)
77
139
  kind = row["kind"].to_s.to_sym
78
140
  return kind if CRITIQUE_KINDS.include?(kind)
@@ -80,6 +142,12 @@ module Hecks
80
142
  raise ValidationError, "#{kind.inspect} is not a critique kind this port knows (#{CRITIQUE_KINDS.join(', ')})"
81
143
  end
82
144
 
145
+ # Reads a finding row's severity, refusing anything but the two that exist.
146
+ #
147
+ # @param row [Hash{String => Object}] one finding row of the adapter's answer
148
+ # @return [Symbol] the row's `"severity"`, a member of `SEVERITIES`
149
+ # @raise [Ports::Agent::ValidationError] if the severity is missing or not in
150
+ # `SEVERITIES`
83
151
  def severity!(row)
84
152
  severity = row["severity"].to_s.to_sym
85
153
  return severity if SEVERITIES.include?(severity)
@@ -87,9 +155,18 @@ module Hecks
87
155
  raise ValidationError, "#{severity.inspect} is not a severity this port knows (#{SEVERITIES.join(', ')})"
88
156
  end
89
157
 
90
- # ROWS SHAPED EXACTLY AS `Interview::Proposal::Argument` a
91
- # reference's `field` is legitimately blank (a reference IS an
158
+ # Normalises a proposal row's arguments into symbol-keyed, all-String rows.
159
+ #
160
+ # Rows shaped exactly as `Interview::Proposal::Argument` — a
161
+ # reference's `field` is legitimately blank (a reference is an
92
162
  # id), so only `name` is required here.
163
+ #
164
+ # @param row [Hash{String => Object}] one proposal row of the adapter's answer
165
+ # @return [Array<Hash{Symbol => String}>] one `{name:, field:, value:}` Hash per
166
+ # argument, `field` and `value` being `""` when absent; `[]` when the row has no
167
+ # `"arguments"`
168
+ # @raise [Ports::Agent::ValidationError] if an argument row's `"name"` is missing or
169
+ # empty
93
170
  def arguments!(row)
94
171
  Array(row["arguments"]).map do |argument|
95
172
  name = argument["name"].to_s