hecks 2.8.0 → 2.9.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 (404) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +4 -6
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +17 -98
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +5 -48
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +31 -141
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -194
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -111
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +43 -305
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
  114. data/lib/hecks/bluebook/model_check.rb +100 -597
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +4 -23
  128. data/lib/hecks/cli/document.rb +3 -21
  129. data/lib/hecks/cli/ir.rb +3 -5
  130. data/lib/hecks/cli/mcp.rb +2 -3
  131. data/lib/hecks/cli/mcp_door.rb +19 -114
  132. data/lib/hecks/cli/model_check.rb +18 -85
  133. data/lib/hecks/cli/project_cli.rb +7 -40
  134. data/lib/hecks/cli/project_diagrams.rb +0 -7
  135. data/lib/hecks/cli/run.rb +12 -58
  136. data/lib/hecks/cli/smoke_test.rb +2 -16
  137. data/lib/hecks/cli/stores.rb +1 -2
  138. data/lib/hecks/cli.rb +18 -24
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +82 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +9 -90
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +61 -64
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +150 -678
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +2 -15
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +42 -134
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +3 -6
  336. data/lib/hecks/release/runner/commands.rb +2 -4
  337. data/lib/hecks/release/runner/gem_publisher.rb +2 -3
  338. data/lib/hecks/release/runner/npm_publisher.rb +6 -20
  339. data/lib/hecks/release/runner/preflight.rb +4 -19
  340. data/lib/hecks/release/runner/tagger.rb +2 -9
  341. data/lib/hecks/release/runner.rb +3 -32
  342. data/lib/hecks/rendering.rb +4 -26
  343. data/lib/hecks/router/namespace_installer.rb +12 -36
  344. data/lib/hecks/router.rb +4 -8
  345. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  346. data/lib/hecks/runtime/boot_gates.rb +3 -18
  347. data/lib/hecks/runtime/caller.rb +19 -59
  348. data/lib/hecks/runtime/capability_graph.rb +5 -21
  349. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  350. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  351. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  352. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  353. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  354. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  355. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  356. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  357. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  358. data/lib/hecks/runtime/dispatcher.rb +82 -241
  359. data/lib/hecks/runtime/entity_element.rb +40 -462
  360. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  361. data/lib/hecks/runtime/errors.rb +32 -115
  362. data/lib/hecks/runtime/event.rb +3 -25
  363. data/lib/hecks/runtime/identity.rb +18 -154
  364. data/lib/hecks/runtime/instance.rb +18 -150
  365. data/lib/hecks/runtime/interpreting.rb +11 -70
  366. data/lib/hecks/runtime/invocation.rb +28 -164
  367. data/lib/hecks/runtime/loader.rb +48 -186
  368. data/lib/hecks/runtime/outbox.rb +38 -261
  369. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  370. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  371. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  372. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  373. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  374. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  375. data/lib/hecks/runtime/reference_hop.rb +8 -81
  376. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  377. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  378. data/lib/hecks/runtime/registry/verification.rb +52 -225
  379. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  380. data/lib/hecks/runtime/registry.rb +99 -244
  381. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  382. data/lib/hecks/runtime/routing.rb +4 -15
  383. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  384. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  385. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  386. data/lib/hecks/runtime/tenant_check.rb +6 -68
  387. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  388. data/lib/hecks/runtime/value/admission.rb +16 -67
  389. data/lib/hecks/runtime/value/coercion.rb +155 -641
  390. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  391. data/lib/hecks/runtime/value.rb +12 -84
  392. data/lib/hecks/runtime.rb +17 -65
  393. data/lib/hecks/storehouse.rb +141 -568
  394. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  395. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  396. data/lib/hecks/vendoring/git_environment.rb +2 -8
  397. data/lib/hecks/vendoring/git_source.rb +4 -10
  398. data/lib/hecks/vendoring.rb +7 -46
  399. data/lib/hecks/version.rb +3 -14
  400. data/lib/hecks.rb +26 -81
  401. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  402. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  403. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  404. metadata +3 -2
data/lib/hecks/naming.rb CHANGED
@@ -1,19 +1,9 @@
1
1
  module Hecks
2
- # Every derived-name and identity-joining rule the language relies on —
3
- # pluralisation/singularisation, snake/Pascal casing, dotted-path and verb
4
- # splitting, and the command/event reference rewrites — collected here so
5
- # two readers never invent two different spellings for the same
6
- # derivation.
2
+ # Derived-name and identity-joining rules: casing, pluralisation, dotted-path
3
+ # and verb splitting, command/event reference rewrites.
7
4
  module Naming
8
- # What separates the parts of a derived identity.
9
- #
10
- # An identity of several parts is their join, and the join has to be spelled the
11
- # same everywhere or two readers name two different records off one declaration.
12
- # It was spelled three ways at once here — "::" for an aggregate under a
13
- # chapter, "." for everything under an aggregate, "#" for the three that keyed
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
16
- # that resolved by string comparison found nothing.
5
+ # What separates the parts of a derived identity. Every reader must spell the
6
+ # join the same way, or string-compared references resolve to nothing.
17
7
  IDENTITY_JOIN = ":".freeze
18
8
 
19
9
  module_function
@@ -35,9 +25,8 @@ module Hecks
35
25
  type.to_s.split("::").last.to_s
36
26
  end
37
27
 
38
- # snake_case -> PascalCase. The name a synthesised closed-set value object
39
- # takes when an attribute declares one inline. The derivation is part of
40
- # the IR contract: the same bluebook must always produce the same name.
28
+ # snake_case -> PascalCase. Names a closed-set value object synthesised from an
29
+ # inline attribute; the derivation is part of the IR contract.
41
30
  #
42
31
  # @param text [String, Symbol, #to_s] a snake_case (or already Pascal) name
43
32
  # @return [String] the PascalCase form
@@ -56,15 +45,8 @@ module Hecks
56
45
  .downcase
57
46
  end
58
47
 
59
- # An identifier as a person would say it — `ATMCard` -> "ATM card",
60
- # `AccrueInterest` -> "Accrue interest", `daily_limit` -> "Daily
61
- # limit", `Back office` -> "Back office". The same two word-boundary
62
- # splits `snake` uses, with a space instead of an underscore — plus
63
- # the one thing `snake` cannot give back: an all-caps run stays an
64
- # acronym ("ATM", "KYC") instead of being lowercased into a word
65
- # nobody says ("Atm"). First word capitalized, the rest lowercased,
66
- # so a headword reads as sentence case whatever casing it was
67
- # declared in.
48
+ # An identifier as a sentence-cased phrase: `ATMCard` -> "ATM card",
49
+ # `daily_limit` -> "Daily limit". All-caps runs stay acronyms.
68
50
  #
69
51
  # @param text [String, Symbol, #to_s] a Pascal-, camel-, or snake-case identifier
70
52
  # @return [String] the space-separated, sentence-cased phrase
@@ -81,9 +63,7 @@ module Hecks
81
63
  end.join(" ")
82
64
  end
83
65
 
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.
66
+ # Joins items into an Oxford-comma list: "A, B, and C" / "A or B".
87
67
  #
88
68
  # @param items [Array<#to_s>] the items to join
89
69
  # @param conj [String] the word before the last item, such as `"and"` or `"or"`
@@ -98,9 +78,8 @@ module Hecks
98
78
  end
99
79
  end
100
80
 
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".
81
+ # Picks the article for `word` by its first letter. Construct names are plain
82
+ # words, never "hour" or "university", so the vowel-letter heuristic holds.
104
83
  #
105
84
  # @param word [String, Symbol, #to_s] the word the article precedes
106
85
  # @return [String] `"an"` if `word` starts with a vowel letter, `"a"` otherwise
@@ -108,15 +87,8 @@ module Hecks
108
87
  %w[a e i o u].include?(word.to_s[0].to_s.downcase) ? "an" : "a"
109
88
  end
110
89
 
111
- # The name a collection of something takes.
112
- #
113
- # There were two of these and one was wrong. A read model's gathered heads
114
- # derived their name with a bare `"#{snake(target)}s"`, so the meta-domain's
115
- # own whole-bluebook read model handed back `querys`, `entitys`, `policys`
116
- # and `dispatchs` — and every check was green, because the checks compared
117
- # the wrong rule against itself. Agreement is not correctness; it never was.
118
- #
119
- # So: one pluraliser, three rules, and every collection name flows through it.
90
+ # The name a collection of something takes: "y" -> "ies", sibilants take "es",
91
+ # everything else "s". Every collection name must flow through this one rule.
120
92
  #
121
93
  # @param text [String, Symbol, #to_s] a singular name
122
94
  # @return [String] the pluralised name
@@ -128,13 +100,9 @@ module Hecks
128
100
  "#{word}s"
129
101
  end
130
102
 
131
- # `has_many`'s undo — the plural written, back to the singular the target
132
- # aggregate is actually named. Deliberately the crude half of a pair: `plural`
133
- # above earns its precision (three suffix rules) because getting a collection
134
- # name wrong reads as a typo forever ; this only ever recovers a name someone
135
- # already wrote as a real aggregate, so "ies -> y, trailing s dropped" is the
136
- # whole rule — enough for `has_many Invoices` to resolve to the aggregate
137
- # actually named Invoice.
103
+ # Recovers the singular aggregate name from a `has_many` plural. Deliberately
104
+ # crude ("ies" -> "y", sibilant "es" and trailing "s" dropped): it only has to
105
+ # find a name someone already wrote.
138
106
  #
139
107
  # @param text [String, Symbol, #to_s] a plural name
140
108
  # @return [String] the singularised name
@@ -142,15 +110,8 @@ module Hecks
142
110
  word = text.to_s
143
111
  return "#{word[0..-4]}y" if word.length > 3 && word.end_with?("ies")
144
112
 
145
- # `plural`'s own second rule adds "es" (not bare "s") after
146
- # s/x/z/ch/sh — undone here the same way, or a word `plural`
147
- # itself would have suffixed with "es" comes back missing its
148
- # own trailing letter ("Boxes" -> "Boxe", not "Box") once this
149
- # only ever knew how to drop a bare "s". Checked before the
150
- # bare-"s" rule below: stripping "es" first and confirming what
151
- # is left actually ends in one of those five shapes is what
152
- # keeps an ordinary "-es" word (e.g. "Invoices" -> "Invoice")
153
- # from also losing a letter it never doubled.
113
+ # Undo `plural`'s "es" after s/x/z/ch/sh before the bare-"s" rule, so "Boxes"
114
+ # gives "Box" while "Invoices" still gives "Invoice".
154
115
  return word[0..-3] if word.length > 3 && word.end_with?("es") && word[0..-3].match?(/(s|x|z|ch|sh)\z/)
155
116
 
156
117
  return word[0..-2] if word.length > 1 && word.end_with?("s")
@@ -199,29 +160,9 @@ module Hecks
199
160
 
200
161
  # Splits a domain-qualified verb into its domain, aggregate, and command parts.
201
162
  #
202
- # Domain, aggregate, then the REST dot-joined into one command path.
203
- #
204
- # The `::` boundary between domain and aggregate is unambiguous by
205
- # construction — every caller here has already prefixed the domain
206
- # itself (`PolicyInterpreter#deliver`, `SagaInterpreter#qualified`,
207
- # `Router#dispatch`'s own rebuilt string) before this ever runs. A
208
- # third `::` segment can still show up past that boundary: a bare
209
- # `ScopedConstant` naming a port operation (`command_ref`'s own
210
- # comment — `Aggregate::Port::Operation`, three colon-joined
211
- # segments with no `.` of its own) only gets its last `::` rewritten
212
- # to `.` there, at DSL-build time, because nothing at that point
213
- # knows yet whether the constant names a port operation or a
214
- # domain-qualified command (`Domain::Aggregate::Command`, the other
215
- # shape `command_ref` documents) — both are textually identical.
216
- # Here, past the already-resolved domain boundary, any leftover
217
- # `::` is unambiguous: it is that same rewrite artifact, and folding
218
- # it into the dot-joined tail recovers exactly the
219
- # `Aggregate::Port.Operation` shape a working port dispatch already
220
- # expects (`spec/port_operation_interpreter_spec.rb`'s own
221
- # `"Payments::Payment.PaymentGateway.Receive"`). `Outbox::Fanout
222
- # .kind_for` and `ReactionInvocation#resolve_target` both already
223
- # assume this contract on their own end; this is what actually
224
- # delivers it to them.
163
+ # A `::` left after the domain and aggregate is the last-`::`-to-`.` rewrite
164
+ # `command_ref` leaves on a port operation; it folds into the dotted command
165
+ # tail (`Aggregate::Port.Operation`).
225
166
  #
226
167
  # @param verb [String, Symbol, #to_s] a `Domain::Aggregate.command` (or
227
168
  # `.query`) path
@@ -239,26 +180,11 @@ module Hecks
239
180
  [domain, aggregate, command]
240
181
  end
241
182
 
242
- # Rewrites a bare command constant's trailing `::` into `.`, matching a
243
- # command's own FQN separator; a String or Symbol passes through unchanged.
183
+ # Rewrites a bare command constant's last `::` into `.`, matching a command's
184
+ # FQN separator (`Banking::Account::Debit` -> `Banking::Account.Debit`).
244
185
  #
245
- # `trigger Account::Debit` / `dispatch Account::Debit` — a bare
246
- # constant reference (`ConstShim`'s own `ScopedConstant`, S0b), not
247
- # text (ADR 0025, "events and reactions" — command references become
248
- # first-class). Ruby's `::` joins every segment the same way a
249
- # constant path always does, but a command's own `hecks_fqn` joins
250
- # its aggregate with `.` (`Construct#hecks_separator`'s default,
251
- # only an aggregate overrides it to `::`) — so only the last `::`
252
- # becomes a `.`; everything before it (the chapter, when a domain is
253
- # spelled at all: `Banking::Account::Debit`) stays `::`-joined.
254
- #
255
- # A string passes through unchanged, on purpose — legacy era text
256
- # (S0a's own shadow-parsed spelling) already mixes `::` (domain) and
257
- # `.` (command) correctly on its own, e.g. `"Banking::Account.Debit"`,
258
- # and re-splitting that by content rather than by type would corrupt
259
- # it (its own last `::` sits between the domain and the aggregate,
260
- # not the aggregate and the command). Only an actual constant object
261
- # — never seen holding a `.` of its own — needs the rewrite at all.
186
+ # A String or Symbol passes through: era text already mixes `::` and `.`
187
+ # correctly, and re-splitting it by content would corrupt it.
262
188
  #
263
189
  # @param value [Symbol, String, Module] the command, as a bare constant (a
264
190
  # `ScopedConstant` module `ConstShim` resolves) or already-dotted text
@@ -271,18 +197,8 @@ module Hecks
271
197
  path.empty? ? text : "#{path}.#{command}"
272
198
  end
273
199
 
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
- #
277
- # `emits Account::AccountFrozen` / `on Account::AccountFrozen` — the
278
- # event-side twin of `command_ref`, above (ADR 0025, S6 — "events
279
- # first-class"). Identical transform (a bare `ScopedConstant`'s last
280
- # `::` becomes `.`, a String passes through unchanged for legacy
281
- # `shadow_parse` text and for corpus sites this pass didn't migrate —
282
- # see that method's own comment for why both rules exist), given its
283
- # own name because the two references mean different things even
284
- # though the rewrite is byte-identical: an event name is not a
285
- # command name that happens to share a format.
200
+ # The event-side twin of `command_ref`. Same rewrite, separate name, because an
201
+ # event reference is not a command reference that happens to share a format.
286
202
  #
287
203
  # @param value [Symbol, String, Module] the event, as a bare constant (a
288
204
  # `ScopedConstant` module `ConstShim` resolves) or already-dotted text
@@ -291,32 +207,8 @@ module Hecks
291
207
 
292
208
  # Strips a process manager's event reference down to its bare event name.
293
209
  #
294
- # `transition Account::AccountDebited => "state"` / `starts_on
295
- # Transfer::TransferRequested` / `ends_on Transfer::TransferSettled`
296
- # — a process manager's own event references (ADR 0025, S6),
297
- # deliberately not `event_ref` — found live, not assumed, wiring a
298
- # real migrated corpus site into `bin/model_check` for the first
299
- # time (2026-08-28): `SagaInterpreter#begin_saga`/`#advance_saga`
300
- # match `pm.starts_on`/`pm.handler_for` against `event.name`, which
301
- # `CommandRules::Emission#emit` stamps bare — a command's own
302
- # `emits AccountDebited` never carries its aggregate's name at all
303
- # (unlike a policy's cross-aggregate `on`, matched instead by
304
- # `Naming.demodulise(event.aggregate)` split apart from the bare
305
- # name — `PolicyInterpreter#policies_for`). Handing a saga's own
306
- # matcher the dotted `event_ref` form ("Account.AccountDebited")
307
- # would silently name an event no command in the domain ever
308
- # actually emits — caught by `bin/model_check`'s own `deaf_handler`/
309
- # `deaf_trigger` findings the moment a real qualified corpus site
310
- # existed to trip them, not by any unit test in isolation.
311
- #
312
- # A qualifier is still worth writing (`Account::`) — the same
313
- # provenance a reader gets from `trigger Account::Debit` — it is
314
- # only not worth keeping: `demodulise` drops everything but the
315
- # final segment, so `Account::AccountDebited` and a bare
316
- # `AccountDebited` resolve to the identical stored string. A String
317
- # passes through unchanged either way, exactly like `command_ref`'s
318
- # own legacy branch — this corpus never spelled one dotted to begin
319
- # with, so there is nothing here to strip.
210
+ # Not `event_ref`: saga matching compares against `event.name`, which is stamped
211
+ # bare, so a dotted "Account.AccountDebited" would name an event nothing emits.
320
212
  #
321
213
  # @param value [Symbol, String, Module] the event, as a bare constant (a
322
214
  # `ScopedConstant` module `ConstShim` resolves) or already-dotted text
@@ -2,22 +2,9 @@ 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
6
- # half of sign-in `Ports::Authentication` deliberately stays out of
7
- # (see that port's own header: it only ever talks to the external
8
- # provider, never this registry's own data). Resolved the same way
9
- # every other port here resolves its adapter: one adapter
10
- # registry-wide answers this, since an app has no reason to want a
11
- # different "who's allowed in" source per aggregate.
12
- #
13
- # Every domain shapes this differently — who its own "a person who
14
- # can sign in" aggregate is, what admits them, what grants them
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
17
- # grant should win over whatever an aggregate's own role field
18
- # says) lives on `Ports::Authorization#live_role_for` instead —
19
- # a domain's adapter here calls that directly, rather than this
20
- # port re-deriving Governance-query logic another port already owns.
5
+ # Who can sign in and what role they hold: the domain-specific half of sign-in.
6
+ # Pure delegation to the one adapter bound registry-wide; adapters that need a
7
+ # live Governance grant call `Ports::Authorization#live_role_for`.
21
8
  module AccessControl
22
9
  NAME = "access_control".freeze
23
10
 
@@ -39,16 +26,13 @@ module Hecks
39
26
 
40
27
  # Admits a new person, in whatever way this domain's adapter defines admission.
41
28
  #
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.
29
+ # The keywords carry the names of a verified sign-in (`Ports::Authentication.verify`),
30
+ # but nothing here wires the two together, so their shapes are adapter-defined.
45
31
  #
46
32
  # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
47
33
  # @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
34
+ # @param issuer [Object] adapter-defined, forwarded unchanged; the OIDC issuer
35
+ # @param subject [Object] adapter-defined, forwarded unchanged; the OIDC subject
52
36
  # @return [Object] adapter-defined representation of the newly admitted person
53
37
  # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
54
38
  # (see `adapter`)
@@ -1,18 +1,8 @@
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
5
- # adapter (the real `claude_code` one, and any scripted double
6
- # standing in for it in a spec) is held to the identical shape.
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
9
- # re-decided per adapter.
10
- #
11
- # Every failure raises `ValidationError` — never lets a bare
12
- # `NoMethodError` on nil, or a `KeyError`, reach a caller looking
13
- # for an agent-shaped failure. `bin/interview` catches exactly one
14
- # class for "the model answered badly," not a grab-bag of Ruby's
15
- # own.
4
+ # Turns an adapter's raw reply hash into structs, so every adapter is held to
5
+ # one shape. Every failure raises `ValidationError`, never a bare `NoMethodError`.
16
6
  module Answers
17
7
  VERB_PATTERN = /\A[A-Za-z]+::[A-Za-z]+\.[A-Za-z]+\z/
18
8
 
@@ -77,8 +67,6 @@ module Hecks
77
67
  end
78
68
  end
79
69
 
80
- # ── shared checks, each named for what it refuses ───────────────
81
-
82
70
  # Reads the array of rows an answer holds under one key, refusing any other shape.
83
71
  #
84
72
  # @param raw [Object] the adapter's reply; only a Hash is accepted
@@ -111,12 +99,8 @@ module Hecks
111
99
 
112
100
  # Reads a proposal row's verb, refusing one that is not fully qualified.
113
101
  #
114
- # A verb the language could not even parse is an adapter fault,
115
- # refused right here — the same `not_fully_qualified` shape the
116
- # language's own grammar already refuses by, reused rather than
117
- # reinvented. A verb naming a real category that turns out to
118
- # describe the wrong fact is not this port's business: that one
119
- # dispatches, and `Interview::Session#offer` is what says no.
102
+ # Only the shape is checked; whether the verb fits the fact is
103
+ # `Interview::Session#offer`'s call.
120
104
  #
121
105
  # @param row [Hash{String => Object}] one proposal row of the adapter's answer
122
106
  # @return [String] the row's `"verb"`, shaped `Chapter::Aggregate.Command`
@@ -157,9 +141,7 @@ module Hecks
157
141
 
158
142
  # Normalises a proposal row's arguments into symbol-keyed, all-String rows.
159
143
  #
160
- # Rows shaped exactly as `Interview::Proposal::Argument` — a
161
- # reference's `field` is legitimately blank (a reference is an
162
- # id), so only `name` is required here.
144
+ # Only `name` is required: a reference's `field` is legitimately blank.
163
145
  #
164
146
  # @param row [Hash{String => Object}] one proposal row of the adapter's answer
165
147
  # @return [Array<Hash{Symbol => String}>] one `{name:, field:, value:}` Hash per
@@ -2,103 +2,28 @@ require_relative "../runtime/registry"
2
2
 
3
3
  module Hecks
4
4
  module Ports
5
- # The interviewer — whatever asks the next good question, reads a
6
- # sentence back into proposed declarations, judges a model on taste
7
- # rather than structure, or suggests a name.
8
- #
9
- # ## Why a singleton port
10
- #
11
- # Resolved the same zero/one/many way every other port here is
12
- # (`Ports::IdentityGeneration`'s own singleton adapter — one
13
- # interviewer per process, never per-aggregate), because a second
14
- # adapter answering this port is exactly as unchoosable as a second
15
- # one minting identities.
16
- #
17
- # ## Why this exists
18
- #
19
- # Built last, on purpose, and still optional. `.claude/skills/
20
- # interview/SKILL.md` conducts a whole session with none of this —
21
- # Claude Code reads `bin/interview state` itself, asks the human
22
- # directly with real conversation context, and calls `bin/interview
23
- # ask`/`propose`/`accept` itself. This port exists for what that
24
- # cannot reach: a headless run (`bin/interview suggest`, no model in
25
- # the room otherwise), a batch `critique` over an already-written
26
- # chapter, and — the actual point of building it at all — a
27
- # scripted double (`spec/fixtures/scripted_agent.rb`) that makes the
28
- # whole loop testable with no real model involved.
29
- #
30
- # ## What this port is not
31
- #
32
- # It does not judge whether a declaration is well-formed — that is
33
- # the meta-domain's own job, answered by a real dispatch refusing or
34
- # not (`Interview::Session#offer`). This port only ever produces
35
- # shape: a question, a proposed declaration, a judgement about
36
- # taste, a name. A proposal naming a category the language does not
37
- # declare is an adapter fault, refused here, in `Answers`; a
38
- # proposal naming a real category but describing the wrong domain
39
- # fact is dispatched anyway, so the language gets to say why. That
40
- # line is the whole design.
41
- #
42
- # ## Where parsing happens
43
- #
44
- # Parsing belongs to this file, not the adapter. An adapter answers
45
- # with a plain, already-JSON-shaped Hash (a real model's parsed
46
- # reply, or a spec double's own hand-built one) — never a Struct —
47
- # so every adapter is validated identically here rather than trusting
48
- # each one to refuse the same way. Two adapters that parsed
49
- # differently would mean the same malformed answer passing through
50
- # one and refusing through the other.
5
+ # The interviewer: asks questions, reads prose into proposed declarations, critiques a
6
+ # model on taste, and suggests names. One adapter per process.
51
7
  module Agent
52
8
  NAME = "agent".freeze
53
9
 
54
- # The answer came back and could not be used — no JSON in it, a
55
- # category the language does not declare, a severity outside the
56
- # two that exist. A foreign failure (JSON::ParserError, a missing
57
- # key) wrapped into something this port owns, the same shape
58
- # `Ports::Authentication::ValidationError` already is, so no
59
- # caller ever rescues a raw parser error leaking out of an
60
- # adapter.
10
+ # The adapter's answer arrived but was unusable; retrying the same prompt will not help.
61
11
  class ValidationError < StandardError
62
12
  end
63
13
 
64
- # Nothing answered at all — the binary is missing, the subprocess
65
- # died, the call timed out. Separate from ValidationError on
66
- # purpose: unavailable means retry or fall back to asking the
67
- # human directly; malformed means the prompt is wrong, not the
68
- # transport, and retrying identically will not fix it.
14
+ # Nothing answered (missing binary, dead subprocess, timeout); retry or ask the human.
69
15
  class Unavailable < StandardError
70
16
  end
71
17
 
72
- # ── what the four operations hand back ──────────────────────────
73
- #
74
- # Structs, not hashes, for the reason `Bluebook::ModelCheck::Finding`
75
- # already is one: a `keyword_init` Struct raises on a key nobody
76
- # declared, which turns "the model invented a field" into a
77
- # failure at the boundary rather than a nil three calls in.
78
-
79
- # One question. `because` is the reasoning quoted back so a human
80
- # can see it, not decoration — the same "show your work" the
81
- # language's own refusal messages already practice.
18
+ # keyword_init Structs raise on an undeclared key, so an invented field fails at the edge.
19
+
20
+ # One question; `because` is the reasoning shown to the human.
82
21
  Question = Struct.new(:text, :because, keyword_init: true)
83
22
 
84
- # One proposed declaration, already shaped as
85
- # `Interview::Proposal`'s own `Argument` rows — `{name:, field:,
86
- # value:}`, the exact triple `bin/interview propose --arg
87
- # name:field:value` already takes and `Interview::Lowering`
88
- # already knows how to lower. Nothing here decides how to address
89
- # a record or whether a value is bare or wrapped — that was
90
- # already the proposal-writer's job in `interview.bluebook`'s own
91
- # design (see `Argument`'s own comment there); this only adds the
92
- # prose reasoning a human typed does not carry on its own.
23
+ # `arguments` are `{name:, field:, value:}` rows, as `bin/interview propose --arg` takes.
93
24
  Proposal = Struct.new(:verb, :arguments, :rationale, keyword_init: true)
94
25
 
95
- # Critique reuses `Bluebook::ModelCheck::Finding`'s own shape —
96
- # same fields, same severities, so a mechanical finding
97
- # (`Session#gaps`) and a judgement (this) print in one list and
98
- # sort together. The kind vocabulary is not shared — ModelCheck's
99
- # eleven kinds are structural facts about a graph; these are
100
- # opinions about a model, closed here on purpose (an open kind
101
- # field is an open prompt, and an open prompt drifts).
26
+ # Kinds are closed: an open kind is an open prompt, and an open prompt drifts.
102
27
  CRITIQUE_KINDS = %i[
103
28
  anemic_aggregate wrong_boundary crud_verb leaky_value_object
104
29
  missing_lifecycle missing_invariant ubiquitous_language
@@ -106,132 +31,82 @@ module Hecks
106
31
  ].freeze
107
32
  SEVERITIES = %i[error warning].freeze
108
33
 
34
+ # Same shape as `Bluebook::ModelCheck::Finding`, so gaps and critiques sort together.
109
35
  Finding = Struct.new(:kind, :severity, :subject, :message, keyword_init: true) do
110
36
  def to_s = "#{severity.to_s.upcase.ljust(7)} #{kind.to_s.ljust(20)} #{subject} — #{message}"
111
37
  end
112
38
 
113
- # One suggested name. `rejected` carries the near-misses and why
114
- # they were passed over — free at the point the model is choosing
115
- # anyway, and the part a human actually learns from.
39
+ # `rejected` holds the near-misses and why each was passed over.
116
40
  Suggestion = Struct.new(:name, :because, :rejected, keyword_init: true)
117
41
 
42
+ # Adapters return plain Hashes; validating here makes every adapter refuse the same way.
118
43
  module_function
119
44
 
120
- # Asks the adapter for the next best question and validates its raw answer into structs.
121
- #
122
- # `state` is the whole picture — normally
123
- # `Interview::Session#declaration` plus `#gaps`, so an adapter
124
- # needs no memory of its own between calls; that is what lets a
125
- # headless run be a series of one-shot processes, same as
126
- # `bin/interview` itself already is.
45
+ # Asks the adapter for the next question.
127
46
  #
128
47
  # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
129
- # @param state [Hash] the interview's whole current picture (declaration plus gaps),
130
- # JSON-able because an adapter may serialise it into a prompt
131
- # @param asked [Array<Object>] JSON-able record of the questions already asked, so the
132
- # adapter does not repeat one; `[]` when nothing has been asked
133
- # @return [Array<Ports::Agent::Question>] the questions the adapter answered with, in
134
- # its order; `[]` if it offered none
135
- # @raise [Ports::Agent::ValidationError] if the answer is not a Hash holding a
136
- # `"questions"` array whose rows each carry a non-blank `"text"` and `"because"`
137
- # @raise [Ports::Agent::Unavailable] if the adapter cannot answer at all (missing
138
- # binary, failed subprocess, timeout, or an exhausted scripted queue)
139
- # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
140
- # (see `adapter`)
48
+ # @param state [Hash] declaration plus gaps; adapters keep no memory between calls
49
+ # @param asked [Array<Object>] questions already asked, so the adapter does not repeat one
50
+ # @return [Array<Ports::Agent::Question>]
51
+ # @raise [Ports::Agent::ValidationError] unless the answer is a Hash whose `"questions"`
52
+ # rows each carry a non-blank `"text"` and `"because"`
53
+ # @raise [Ports::Agent::Unavailable] if the adapter cannot answer
54
+ # @raise [Runtime::WiringError] unless exactly one adapter implements the port
141
55
  def ask(registry, state:, asked: [])
142
56
  Answers.questions(adapter(registry).ask(state: state, asked: asked))
143
57
  end
144
58
 
145
- # Turns a human's sentence into proposed declarations, validated into structs.
146
- #
147
- # Returns `[]` when the sentence
148
- # carried no declaration at all (a clarifying question back from
149
- # the human, say) — a legitimate answer, not a failure.
59
+ # Turns a sentence into proposed declarations; `[]` when it named none.
150
60
  #
151
61
  # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
152
62
  # @param prose [String] a human's plain-English sentence
153
- # @param state [Hash] the interview's whole current picture, JSON-able because an
154
- # adapter may serialise it into a prompt
155
- # @return [Array<Ports::Agent::Proposal>] one proposal per declaration the sentence
156
- # named; `[]` when it named none
157
- # @raise [Ports::Agent::ValidationError] if the answer is not a Hash holding a
158
- # `"proposals"` array, a row's `"verb"` is not `Chapter::Aggregate.Command`, its
159
- # `"rationale"` is blank, or one of its argument rows has no `"name"`
160
- # @raise [Ports::Agent::Unavailable] if the adapter cannot answer at all (missing
161
- # binary, failed subprocess, timeout, or an exhausted scripted queue)
162
- # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
163
- # (see `adapter`)
63
+ # @param state [Hash] declaration plus gaps
64
+ # @return [Array<Ports::Agent::Proposal>]
65
+ # @raise [Ports::Agent::ValidationError] unless the answer is a Hash whose `"proposals"`
66
+ # rows have a `Chapter::Aggregate.Command` verb, a rationale and named arguments
67
+ # @raise [Ports::Agent::Unavailable] if the adapter cannot answer
68
+ # @raise [Runtime::WiringError] unless exactly one adapter implements the port
164
69
  def interpret(registry, prose:, state:)
165
70
  Answers.proposals(adapter(registry).interpret(prose: prose, state: state))
166
71
  end
167
72
 
168
- # Asks the adapter to judge a declared model on taste, validated into structs.
169
- #
170
- # What is wrong with this as a model — handed everything already
171
- # known (the language's own refusals, `Session#gaps`'s mechanical
172
- # findings) so it spends its judgement on what neither of those
173
- # can see, rather than restating them.
73
+ # Asks the adapter to judge a declared model on taste.
174
74
  #
175
75
  # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
176
- # @param declared [Hash] the chapter as declared so far, JSON-able because an adapter
177
- # may serialise it into a prompt
178
- # @param refusals [Array<Object>] JSON-able refusals the language itself already
179
- # raised; `[]` when there are none
180
- # @param findings [Array<Object>] JSON-able mechanical findings `Session#gaps` already
181
- # found; `[]` when there are none
182
- # @return [Array<Ports::Agent::Finding>] the adapter's judgements; `[]` when it has
183
- # nothing worth saying
184
- # @raise [Ports::Agent::ValidationError] if the answer is not a Hash holding a
185
- # `"findings"` array, a row's `"kind"` is outside `CRITIQUE_KINDS`, its `"severity"`
186
- # is outside `SEVERITIES`, or its `"subject"` or `"message"` is blank
187
- # @raise [Ports::Agent::Unavailable] if the adapter cannot answer at all (missing
188
- # binary, failed subprocess, timeout, or an exhausted scripted queue)
189
- # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
190
- # (see `adapter`)
76
+ # @param declared [Hash] the chapter as declared so far
77
+ # @param refusals [Array<Object>] refusals the language already raised
78
+ # @param findings [Array<Object>] `Session#gaps` findings; both are passed so the
79
+ # adapter spends its judgement on what neither can see
80
+ # @return [Array<Ports::Agent::Finding>]
81
+ # @raise [Ports::Agent::ValidationError] if a kind is outside `CRITIQUE_KINDS` or a
82
+ # severity outside `SEVERITIES`
83
+ # @raise [Ports::Agent::Unavailable] if the adapter cannot answer
191
84
  def critique(registry, declared:, refusals: [], findings: [])
192
85
  Answers.findings(adapter(registry).critique(declared: declared, refusals: refusals, findings: findings))
193
86
  end
194
87
 
195
- # Asks the adapter to suggest a name for a construct, validated into structs.
196
- #
197
- # Vocabulary help. `near` is what the chapter already calls
198
- # things, so a suggestion cannot collide with a name in use.
88
+ # Suggests names for a construct.
199
89
  #
200
- # Named `suggest_name`, not `name` — the plan's own word for this
201
- # operation, but `Module#name` already exists and is load-bearing
202
- # everywhere (backtraces, RSpec's own description building,
203
- # `inspect`): a module-function called `name` shadows it the
204
- # instant it's defined, and `SomeModule.name` (no args) then
205
- # raises `ArgumentError: missing keywords` the next time anything
206
- # — not this file, something else entirely — asks the module its
207
- # own name. Measured, not theoretical: this exact collision broke
208
- # RSpec's own failure-message formatting the first time it was
209
- # named `name` here.
90
+ # Not called `name`: a module function of that name shadows `Module#name`.
210
91
  #
211
92
  # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
212
93
  # @param meaning [String] what the new name needs to mean
213
- # @param kind [String] the kind of construct being named, such as `"event"`; an
214
- # adapter interpolates it into its prompt
215
- # @param near [Array<String>] names already in use in this chapter, which a suggestion
216
- # must not collide with; `[]` when there are none
217
- # @return [Array<Ports::Agent::Suggestion>] the suggested names, each with its
218
- # `rejected` near-misses as Strings
219
- # @raise [Ports::Agent::ValidationError] if the answer is not a Hash holding a
220
- # `"names"` array whose rows each carry a non-blank `"name"` and `"because"`
221
- # @raise [Ports::Agent::Unavailable] if the adapter cannot answer at all (missing
222
- # binary, failed subprocess, timeout, or an exhausted scripted queue)
223
- # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
224
- # (see `adapter`)
94
+ # @param kind [String] the construct being named, such as `"event"`
95
+ # @param near [Array<String>] names already in use; a suggestion must not collide
96
+ # @return [Array<Ports::Agent::Suggestion>]
97
+ # @raise [Ports::Agent::ValidationError] unless the answer is a Hash whose `"names"`
98
+ # rows each carry a non-blank `"name"` and `"because"`
99
+ # @raise [Ports::Agent::Unavailable] if the adapter cannot answer
225
100
  def suggest_name(registry, meaning:, kind:, near: [])
226
101
  Answers.suggestions(adapter(registry).suggest_name(meaning: meaning, kind: kind, near: near))
227
102
  end
228
103
 
229
- # Finds the single adapter bound to this port, refusing an ambiguous wiring.
104
+ # Finds the single adapter bound to this port.
230
105
  #
231
106
  # @param registry [Runtime::Registry] the booted registry to search
232
107
  # @return [Module] the adapter module or class implementing this port
233
- # @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
234
- # or the one that does has no Ruby implementation under `Hecks::Adapters`
108
+ # @raise [Runtime::WiringError] if none, or more than one, implements the port, or the
109
+ # one that does has no Ruby implementation under `Hecks::Adapters`
235
110
  def adapter(registry)
236
111
  implementations = registry.adapters.values.select { |a| a.port == NAME }
237
112