hecks 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
data/lib/hecks/naming.rb CHANGED
@@ -5,22 +5,32 @@ 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
 
19
19
  module_function
20
20
 
21
- # The parts of an identity, joined in declaration order.
21
+ # Joins the parts of an identity in declaration order.
22
+ #
23
+ # @param parts [Array<#to_s>, #to_s] one or more identity segments; a bare
24
+ # value is wrapped in a single-element Array
25
+ # @return [String] the segments joined with `IDENTITY_JOIN`
22
26
  def identity(parts) = Array(parts).join(IDENTITY_JOIN)
23
27
 
28
+ # Strips a namespace path down to its last segment.
29
+ #
30
+ # @param type [Module, String, Symbol, #to_s] a `::`-joined constant path, or
31
+ # anything whose `to_s` is one
32
+ # @return [String] the text after the last `::`, or the whole `to_s` if there
33
+ # is none
24
34
  def demodulise(type)
25
35
  type.to_s.split("::").last.to_s
26
36
  end
@@ -28,10 +38,17 @@ module Hecks
28
38
  # snake_case -> PascalCase. The name a synthesised closed-set value object
29
39
  # takes when an attribute declares one inline. The derivation is part of
30
40
  # the IR contract: the same bluebook must always produce the same name.
41
+ #
42
+ # @param text [String, Symbol, #to_s] a snake_case (or already Pascal) name
43
+ # @return [String] the PascalCase form
31
44
  def pascal(text)
32
45
  text.to_s.split("_").map { |part| part.sub(/\A(.)/) { Regexp.last_match(1).upcase } }.join
33
46
  end
34
47
 
48
+ # PascalCase or camelCase -> snake_case.
49
+ #
50
+ # @param text [String, Symbol, #to_s] a Pascal-, camel-, or already snake-case name
51
+ # @return [String] the lowercase, underscore-separated form
35
52
  def snake(text)
36
53
  text.to_s
37
54
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
@@ -39,7 +56,7 @@ module Hecks
39
56
  .downcase
40
57
  end
41
58
 
42
- # AN IDENTIFIER AS A PERSON WOULD SAY IT — `ATMCard` -> "ATM card",
59
+ # An identifier as a person would say it — `ATMCard` -> "ATM card",
43
60
  # `AccrueInterest` -> "Accrue interest", `daily_limit` -> "Daily
44
61
  # limit", `Back office` -> "Back office". The same two word-boundary
45
62
  # splits `snake` uses, with a space instead of an underscore — plus
@@ -48,6 +65,9 @@ module Hecks
48
65
  # nobody says ("Atm"). First word capitalized, the rest lowercased,
49
66
  # so a headword reads as sentence case whatever casing it was
50
67
  # declared in.
68
+ #
69
+ # @param text [String, Symbol, #to_s] a Pascal-, camel-, or snake-case identifier
70
+ # @return [String] the space-separated, sentence-cased phrase
51
71
  def words(text)
52
72
  parts = text.to_s
53
73
  .tr("_", " ")
@@ -61,9 +81,14 @@ module Hecks
61
81
  end.join(" ")
62
82
  end
63
83
 
64
- # "A, B, and C" / "A or B" — the Oxford-comma list every English
65
- # sentence a projection writes wants; lived in `NarrateProjector`
66
- # alone until a second projection needed it.
84
+ # Joins items into the Oxford-comma list — "A, B, and C" / "A or B" —
85
+ # every English sentence a projection writes wants; lived in
86
+ # `NarrateProjector` alone until a second projection needed it.
87
+ #
88
+ # @param items [Array<#to_s>] the items to join
89
+ # @param conj [String] the word before the last item, such as `"and"` or `"or"`
90
+ # @return [String] `""` for no items, the item's `to_s` for one, and the
91
+ # Oxford-comma join for more
67
92
  def to_sentence_list(items, conj: "and")
68
93
  case items.size
69
94
  when 0 then ""
@@ -73,14 +98,17 @@ module Hecks
73
98
  end
74
99
  end
75
100
 
76
- # The vowel-LETTER heuristic — safe here for the same reason
77
- # `Projections::Statements#article` gives: a construct name is a
78
- # plain word, never "hour" or "university".
101
+ # Picks the article for `word` by its first letter — the vowel-letter
102
+ # heuristic, safe here for the same reason `Projections::Statements#article`
103
+ # gives: a construct name is a plain word, never "hour" or "university".
104
+ #
105
+ # @param word [String, Symbol, #to_s] the word the article precedes
106
+ # @return [String] `"an"` if `word` starts with a vowel letter, `"a"` otherwise
79
107
  def a_or_an(word)
80
108
  %w[a e i o u].include?(word.to_s[0].to_s.downcase) ? "an" : "a"
81
109
  end
82
110
 
83
- # The name a COLLECTION of something takes.
111
+ # The name a collection of something takes.
84
112
  #
85
113
  # There were two of these and one was wrong. A read model's gathered heads
86
114
  # derived their name with a bare `"#{snake(target)}s"`, so the meta-domain's
@@ -89,6 +117,9 @@ module Hecks
89
117
  # the wrong rule against itself. Agreement is not correctness; it never was.
90
118
  #
91
119
  # So: one pluraliser, three rules, and every collection name flows through it.
120
+ #
121
+ # @param text [String, Symbol, #to_s] a singular name
122
+ # @return [String] the pluralised name
92
123
  def plural(text)
93
124
  word = text.to_s
94
125
  return "#{word[0..-2]}ies" if word.match?(/[^aeiou]y\z/)
@@ -97,22 +128,25 @@ module Hecks
97
128
  "#{word}s"
98
129
  end
99
130
 
100
- # `has_many`'s undo — the plural WRITTEN, back to the singular the target
131
+ # `has_many`'s undo — the plural written, back to the singular the target
101
132
  # aggregate is actually named. Deliberately the crude half of a pair: `plural`
102
- # above earns its precision (three suffix rules) because getting a COLLECTION
133
+ # above earns its precision (three suffix rules) because getting a collection
103
134
  # name wrong reads as a typo forever ; this only ever recovers a name someone
104
135
  # already wrote as a real aggregate, so "ies -> y, trailing s dropped" is the
105
136
  # whole rule — enough for `has_many Invoices` to resolve to the aggregate
106
137
  # actually named Invoice.
138
+ #
139
+ # @param text [String, Symbol, #to_s] a plural name
140
+ # @return [String] the singularised name
107
141
  def singularize(text)
108
142
  word = text.to_s
109
143
  return "#{word[0..-4]}y" if word.length > 3 && word.end_with?("ies")
110
144
 
111
- # `plural`'s OWN second rule adds "es" (not bare "s") after
145
+ # `plural`'s own second rule adds "es" (not bare "s") after
112
146
  # s/x/z/ch/sh — undone here the same way, or a word `plural`
113
147
  # itself would have suffixed with "es" comes back missing its
114
148
  # own trailing letter ("Boxes" -> "Boxe", not "Box") once this
115
- # only ever knew how to drop a bare "s". Checked BEFORE the
149
+ # only ever knew how to drop a bare "s". Checked before the
116
150
  # bare-"s" rule below: stripping "es" first and confirming what
117
151
  # is left actually ends in one of those five shapes is what
118
152
  # keeps an ordinary "-es" word (e.g. "Invoices" -> "Invoice")
@@ -124,38 +158,60 @@ module Hecks
124
158
  word
125
159
  end
126
160
 
161
+ # Derives the attribute name a reference to `type` is stored under.
162
+ #
163
+ # @param type [Module, String, Symbol, #to_s] the referenced construct's name
164
+ # or a `::`-joined path to it
165
+ # @return [Symbol] the snake_case, demodulised name, as a Symbol
127
166
  def reference_key(type)
128
167
  snake(demodulise(type)).to_sym
129
168
  end
130
169
 
170
+ # Splits a `qualifier.name` string on its first dot.
171
+ #
172
+ # @param dotted [String, Symbol, #to_s] text, optionally containing a dot
173
+ # @return [Array(String, String)] `[before the first dot, after it]`; the
174
+ # second element is `""` when `dotted` has no dot
131
175
  def split_dotted(dotted)
132
176
  first, second = dotted.to_s.split(".", 2)
133
177
  [first.to_s, second.to_s]
134
178
  end
135
179
 
180
+ # The part of a dotted name before its first dot.
181
+ #
182
+ # @param dotted [String, Symbol, #to_s] text, optionally containing a dot
183
+ # @return [String, nil] the text before the first dot, or nil if `dotted`
184
+ # has no dot
136
185
  def qualifier(dotted)
137
186
  text = dotted.to_s
138
187
  text.include?(".") ? text.split(".", 2).first : nil
139
188
  end
140
189
 
190
+ # The part of a dotted name after its first dot.
191
+ #
192
+ # @param dotted [String, Symbol, #to_s] text, optionally containing a dot
193
+ # @return [String] the text after the first dot, or the whole text if
194
+ # `dotted` has no dot
141
195
  def unqualified(dotted)
142
196
  text = dotted.to_s
143
197
  text.include?(".") ? text.split(".", 2).last : text
144
198
  end
145
199
 
146
- # DOMAIN, AGGREGATE, then the REST dot-joined into one command path.
200
+ # Splits a domain-qualified verb into its domain, aggregate, and command parts.
201
+ #
202
+ # Domain, aggregate, then the REST dot-joined into one command path.
147
203
  #
148
204
  # The `::` boundary between domain and aggregate is unambiguous by
149
205
  # construction — every caller here has already prefixed the domain
150
206
  # itself (`PolicyInterpreter#deliver`, `SagaInterpreter#qualified`,
151
207
  # `Router#dispatch`'s own rebuilt string) before this ever runs. A
152
- # THIRD `::` segment can still show up past that boundary: a bare
208
+ # third `::` segment can still show up past that boundary: a bare
153
209
  # `ScopedConstant` naming a port operation (`command_ref`'s own
154
210
  # comment — `Aggregate::Port::Operation`, three colon-joined
155
- # segments with no `.` of its own) only gets its LAST `::` rewritten
211
+ # segments with no `.` of its own) only gets its last `::` rewritten
156
212
  # to `.` there, at DSL-build time, because nothing at that point
157
213
  # knows yet whether the constant names a port operation or a
158
- # domain-qualified command (`Domain::Aggregate::Command`, the OTHER
214
+ # domain-qualified command (`Domain::Aggregate::Command`, the other
159
215
  # shape `command_ref` documents) — both are textually identical.
160
216
  # Here, past the already-resolved domain boundary, any leftover
161
217
  # `::` is unambiguous: it is that same rewrite artifact, and folding
@@ -166,6 +222,11 @@ module Hecks
166
222
  # .kind_for` and `ReactionInvocation#resolve_target` both already
167
223
  # assume this contract on their own end; this is what actually
168
224
  # delivers it to them.
225
+ #
226
+ # @param verb [String, Symbol, #to_s] a `Domain::Aggregate.command` (or
227
+ # `.query`) path
228
+ # @return [Array(String, String, String), nil] `[domain, aggregate, command]`,
229
+ # or nil if `verb` has no `.` or no `domain::aggregate` before it
169
230
  def split_verb(verb)
170
231
  path, command = verb.to_s.split(".", 2)
171
232
  return nil unless path && command
@@ -178,23 +239,30 @@ module Hecks
178
239
  [domain, aggregate, command]
179
240
  end
180
241
 
242
+ # Rewrites a bare command constant's trailing `::` into `.`, matching a
243
+ # command's own FQN separator; a String or Symbol passes through unchanged.
244
+ #
181
245
  # `trigger Account::Debit` / `dispatch Account::Debit` — a bare
182
- # CONSTANT reference (`ConstShim`'s own `ScopedConstant`, S0b), not
246
+ # constant reference (`ConstShim`'s own `ScopedConstant`, S0b), not
183
247
  # text (ADR 0025, "events and reactions" — command references become
184
- # first-class). Ruby's `::` joins EVERY segment the same way a
248
+ # first-class). Ruby's `::` joins every segment the same way a
185
249
  # constant path always does, but a command's own `hecks_fqn` joins
186
250
  # its aggregate with `.` (`Construct#hecks_separator`'s default,
187
- # only an AGGREGATE overrides it to `::`) — so only the LAST `::`
251
+ # only an aggregate overrides it to `::`) — so only the last `::`
188
252
  # becomes a `.`; everything before it (the chapter, when a domain is
189
253
  # spelled at all: `Banking::Account::Debit`) stays `::`-joined.
190
254
  #
191
- # A STRING PASSES THROUGH UNCHANGED, on purpose — legacy era text
255
+ # A string passes through unchanged, on purpose — legacy era text
192
256
  # (S0a's own shadow-parsed spelling) already mixes `::` (domain) and
193
257
  # `.` (command) correctly on its own, e.g. `"Banking::Account.Debit"`,
194
- # and re-splitting that by content rather than by TYPE would corrupt
258
+ # and re-splitting that by content rather than by type would corrupt
195
259
  # it (its own last `::` sits between the domain and the aggregate,
196
260
  # not the aggregate and the command). Only an actual constant object
197
261
  # — never seen holding a `.` of its own — needs the rewrite at all.
262
+ #
263
+ # @param value [Symbol, String, Module] the command, as a bare constant (a
264
+ # `ScopedConstant` module `ConstShim` resolves) or already-dotted text
265
+ # @return [String] the dotted command reference
198
266
  def command_ref(value)
199
267
  return value.to_s if value.is_a?(::String) || value.is_a?(::Symbol)
200
268
 
@@ -203,6 +271,9 @@ module Hecks
203
271
  path.empty? ? text : "#{path}.#{command}"
204
272
  end
205
273
 
274
+ # The event-side twin of `command_ref`, kept as its own name because an
275
+ # event name is not a command name that happens to share a format.
276
+ #
206
277
  # `emits Account::AccountFrozen` / `on Account::AccountFrozen` — the
207
278
  # event-side twin of `command_ref`, above (ADR 0025, S6 — "events
208
279
  # first-class"). Identical transform (a bare `ScopedConstant`'s last
@@ -212,34 +283,44 @@ module Hecks
212
283
  # own name because the two references mean different things even
213
284
  # though the rewrite is byte-identical: an event name is not a
214
285
  # command name that happens to share a format.
286
+ #
287
+ # @param value [Symbol, String, Module] the event, as a bare constant (a
288
+ # `ScopedConstant` module `ConstShim` resolves) or already-dotted text
289
+ # @return [String] the dotted event reference
215
290
  def event_ref(value) = command_ref(value)
216
291
 
292
+ # Strips a process manager's event reference down to its bare event name.
293
+ #
217
294
  # `transition Account::AccountDebited => "state"` / `starts_on
218
295
  # 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
296
+ # — a process manager's own event references (ADR 0025, S6),
297
+ # deliberately not `event_ref` — found live, not assumed, wiring a
221
298
  # real migrated corpus site into `bin/model_check` for the first
222
299
  # time (2026-08-28): `SagaInterpreter#begin_saga`/`#advance_saga`
223
300
  # match `pm.starts_on`/`pm.handler_for` against `event.name`, which
224
- # `CommandRules::Emission#emit` stamps BARE — a command's own
301
+ # `CommandRules::Emission#emit` stamps bare — a command's own
225
302
  # `emits AccountDebited` never carries its aggregate's name at all
226
303
  # (unlike a policy's cross-aggregate `on`, matched instead by
227
304
  # `Naming.demodulise(event.aggregate)` split apart from the bare
228
305
  # name — `PolicyInterpreter#policies_for`). Handing a saga's own
229
- # matcher the DOTTED `event_ref` form ("Account.AccountDebited")
306
+ # matcher the dotted `event_ref` form ("Account.AccountDebited")
230
307
  # would silently name an event no command in the domain ever
231
308
  # actually emits — caught by `bin/model_check`'s own `deaf_handler`/
232
309
  # `deaf_trigger` findings the moment a real qualified corpus site
233
310
  # existed to trip them, not by any unit test in isolation.
234
311
  #
235
- # A qualifier is still worth WRITING (`Account::`) — the same
312
+ # A qualifier is still worth writing (`Account::`) — the same
236
313
  # provenance a reader gets from `trigger Account::Debit` — it is
237
- # only not worth KEEPING: `demodulise` drops everything but the
314
+ # only not worth keeping: `demodulise` drops everything but the
238
315
  # final segment, so `Account::AccountDebited` and a bare
239
316
  # `AccountDebited` resolve to the identical stored string. A String
240
317
  # passes through unchanged either way, exactly like `command_ref`'s
241
318
  # own legacy branch — this corpus never spelled one dotted to begin
242
319
  # with, so there is nothing here to strip.
320
+ #
321
+ # @param value [Symbol, String, Module] the event, as a bare constant (a
322
+ # `ScopedConstant` module `ConstShim` resolves) or already-dotted text
323
+ # @return [String] the bare event name, with any qualifier stripped
243
324
  def event_name_ref(value) = demodulise(value)
244
325
  end
245
326
  end
@@ -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,77 @@ 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 to resolve the adapter against
32
+ # @param identity_id [Object] adapter-defined identity key, forwarded unchanged
33
+ # @return [Object] adapter-defined session representation
34
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
35
+ # (see `adapter`)
26
36
  def session_for_identity(registry, identity_id:)
27
37
  adapter(registry).session_for_identity(registry, identity_id: identity_id)
28
38
  end
29
39
 
40
+ # Admits a new person, in whatever way this domain's adapter defines admission.
41
+ #
42
+ # The keywords carry the names of a verified sign-in (`Ports::Authentication.verify`
43
+ # answers `issuer`, `subject` and `email`), but nothing in this repository wires the
44
+ # two together, so their shapes here are adapter-defined.
45
+ #
46
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
47
+ # @param email [Object] adapter-defined, forwarded unchanged; the person's email
48
+ # @param issuer [Object] adapter-defined, forwarded unchanged; the OIDC issuer that
49
+ # authenticated the person
50
+ # @param subject [Object] adapter-defined, forwarded unchanged; the OIDC subject the
51
+ # issuer vouches for
52
+ # @return [Object] adapter-defined representation of the newly admitted person
53
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
54
+ # (see `adapter`)
30
55
  def provision(registry, email:, issuer:, subject:)
31
56
  adapter(registry).provision(registry, email: email, issuer: issuer, subject: subject)
32
57
  end
33
58
 
59
+ # Asks the domain's adapter which roles it can grant.
60
+ #
61
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
62
+ # @return [Object] adapter-defined collection of grantable roles
63
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
64
+ # (see `adapter`)
34
65
  def available_roles(registry)
35
66
  adapter(registry).available_roles(registry)
36
67
  end
37
68
 
69
+ # Grants a role to a person, by whatever means the domain's adapter records a grant.
70
+ #
71
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
72
+ # @param email [Object] adapter-defined, forwarded unchanged; the person receiving the role
73
+ # @param role [Object] adapter-defined, forwarded unchanged; the role to grant
74
+ # @return [Object] adapter-defined representation of the grant
75
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
76
+ # (see `adapter`)
38
77
  def grant(registry, email:, role:)
39
78
  adapter(registry).grant(registry, email: email, role: role)
40
79
  end
41
80
 
81
+ # Lists every person the domain's adapter knows about.
82
+ #
83
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
84
+ # @return [Object] adapter-defined collection of people
85
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
86
+ # (see `adapter`)
42
87
  def all_people(registry)
43
88
  adapter(registry).all_people(registry)
44
89
  end
45
90
 
91
+ # Finds the single adapter bound to this port, refusing an ambiguous wiring.
92
+ #
93
+ # @param registry [Runtime::Registry] the booted registry to search
94
+ # @return [Module] the adapter module or class implementing this port
95
+ # @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
96
+ # or the one that does has no Ruby implementation under `Hecks::Adapters`
46
97
  def adapter(registry)
47
98
  implementations = registry.adapters.values.select { |a| a.port == NAME }
48
99
 
@@ -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