hecks 0.2.0 → 1.0.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 (419) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +427 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +145 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,174 @@
1
+ module Hecks
2
+ module Naming
3
+ # WHAT SEPARATES THE PARTS OF A DERIVED IDENTITY.
4
+ #
5
+ # An identity of several parts is their JOIN, and the join has to be spelled the
6
+ # same everywhere or two readers name two different records off one declaration.
7
+ # It was spelled three ways at once here — "::" for an aggregate under a
8
+ # chapter, "." for everything under an aggregate, "#" for the three that keyed
9
+ # off position — and each was written where it happened to be needed. Once the
10
+ # runtime derived the same identity, the runtime made a FOURTH, and a reference
11
+ # that resolved by string comparison found nothing.
12
+ IDENTITY_JOIN = ":".freeze
13
+
14
+ module_function
15
+
16
+ # The parts of an identity, joined in declaration order.
17
+ def identity(parts) = Array(parts).join(IDENTITY_JOIN)
18
+
19
+ def demodulise(type)
20
+ type.to_s.split("::").last.to_s
21
+ end
22
+
23
+ # snake_case -> PascalCase. The name a synthesised closed-set value object
24
+ # takes when an attribute declares one inline. The derivation is part of
25
+ # the IR contract: the same bluebook must always produce the same name.
26
+ def pascal(text)
27
+ text.to_s.split("_").map { |part| part.sub(/\A(.)/) { Regexp.last_match(1).upcase } }.join
28
+ end
29
+
30
+ def snake(text)
31
+ text.to_s
32
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
33
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
34
+ .downcase
35
+ end
36
+
37
+ # The name a COLLECTION of something takes.
38
+ #
39
+ # There were two of these and one was wrong. A read model's gathered heads
40
+ # derived their name with a bare `"#{snake(target)}s"`, so the meta-domain's
41
+ # own whole-bluebook read model handed back `querys`, `entitys`, `policys`
42
+ # and `dispatchs` — and every check was green, because the checks compared
43
+ # the wrong rule against itself. Agreement is not correctness; it never was.
44
+ #
45
+ # So: one pluraliser, three rules, and every collection name flows through it.
46
+ def plural(text)
47
+ word = text.to_s
48
+ return "#{word[0..-2]}ies" if word.match?(/[^aeiou]y\z/)
49
+ return "#{word}es" if word.match?(/(s|x|z|ch|sh)\z/)
50
+
51
+ "#{word}s"
52
+ end
53
+
54
+ # `has_many`'s undo — the plural WRITTEN, back to the singular the target
55
+ # aggregate is actually named. Deliberately the crude half of a pair: `plural`
56
+ # above earns its precision (three suffix rules) because getting a COLLECTION
57
+ # name wrong reads as a typo forever ; this only ever recovers a name someone
58
+ # already wrote as a real aggregate, so "ies -> y, trailing s dropped" is the
59
+ # whole rule — enough for `has_many Invoices` to resolve to the aggregate
60
+ # actually named Invoice.
61
+ def singularize(text)
62
+ word = text.to_s
63
+ return "#{word[0..-4]}y" if word.length > 3 && word.end_with?("ies")
64
+
65
+ # `plural`'s OWN second rule adds "es" (not bare "s") after
66
+ # s/x/z/ch/sh — undone here the same way, or a word `plural`
67
+ # itself would have suffixed with "es" comes back missing its
68
+ # own trailing letter ("Boxes" -> "Boxe", not "Box") once this
69
+ # only ever knew how to drop a bare "s". Checked BEFORE the
70
+ # bare-"s" rule below: stripping "es" first and confirming what
71
+ # is left actually ends in one of those five shapes is what
72
+ # keeps an ordinary "-es" word (e.g. "Invoices" -> "Invoice")
73
+ # from also losing a letter it never doubled.
74
+ if word.length > 3 && word.end_with?("es") && word[0..-3].match?(/(s|x|z|ch|sh)\z/)
75
+ return word[0..-3]
76
+ end
77
+
78
+ return word[0..-2] if word.length > 1 && word.end_with?("s")
79
+
80
+ word
81
+ end
82
+
83
+ def reference_key(type)
84
+ snake(demodulise(type)).to_sym
85
+ end
86
+
87
+ def split_dotted(dotted)
88
+ first, second = dotted.to_s.split(".", 2)
89
+ [first.to_s, second.to_s]
90
+ end
91
+
92
+ def qualifier(dotted)
93
+ text = dotted.to_s
94
+ text.include?(".") ? text.split(".", 2).first : nil
95
+ end
96
+
97
+ def unqualified(dotted)
98
+ text = dotted.to_s
99
+ text.include?(".") ? text.split(".", 2).last : text
100
+ end
101
+
102
+ def split_verb(verb)
103
+ path, command = verb.to_s.split(".", 2)
104
+ domain, aggregate = path.to_s.split("::", 2)
105
+ return nil unless domain && aggregate && command
106
+
107
+ [domain, aggregate, command]
108
+ end
109
+
110
+ # `trigger Account::Debit` / `dispatch Account::Debit` — a bare
111
+ # CONSTANT reference (`ConstShim`'s own `ScopedConstant`, S0b), not
112
+ # text (ADR 0025, "events and reactions" — command references become
113
+ # first-class). Ruby's `::` joins EVERY segment the same way a
114
+ # constant path always does, but a command's own `hecks_fqn` joins
115
+ # its aggregate with `.` (`Construct#hecks_separator`'s default,
116
+ # only an AGGREGATE overrides it to `::`) — so only the LAST `::`
117
+ # becomes a `.`; everything before it (the chapter, when a domain is
118
+ # spelled at all: `Banking::Account::Debit`) stays `::`-joined.
119
+ #
120
+ # A STRING PASSES THROUGH UNCHANGED, on purpose — legacy era text
121
+ # (S0a's own shadow-parsed spelling) already mixes `::` (domain) and
122
+ # `.` (command) correctly on its own, e.g. `"Banking::Account.Debit"`,
123
+ # and re-splitting that by content rather than by TYPE would corrupt
124
+ # it (its own last `::` sits between the domain and the aggregate,
125
+ # not the aggregate and the command). Only an actual constant object
126
+ # — never seen holding a `.` of its own — needs the rewrite at all.
127
+ def command_ref(value)
128
+ return value.to_s if value.is_a?(::String) || value.is_a?(::Symbol)
129
+
130
+ text = value.to_s
131
+ path, _, command = text.rpartition("::")
132
+ path.empty? ? text : "#{path}.#{command}"
133
+ end
134
+
135
+ # `emits Account::AccountFrozen` / `on Account::AccountFrozen` — the
136
+ # event-side twin of `command_ref`, above (ADR 0025, S6 — "events
137
+ # first-class"). Identical transform (a bare `ScopedConstant`'s last
138
+ # `::` becomes `.`, a String passes through unchanged for legacy
139
+ # `shadow_parse` text and for corpus sites this pass didn't migrate —
140
+ # see that method's own comment for why both rules exist), given its
141
+ # own name because the two references mean different things even
142
+ # though the rewrite is byte-identical: an event name is not a
143
+ # command name that happens to share a format.
144
+ def event_ref(value) = command_ref(value)
145
+
146
+ # `transition Account::AccountDebited => "state"` / `starts_on
147
+ # Transfer::TransferRequested` / `ends_on Transfer::TransferSettled`
148
+ # — a process manager's OWN event references (ADR 0025, S6),
149
+ # DELIBERATELY NOT `event_ref` — found live, not assumed, wiring a
150
+ # real migrated corpus site into `bin/model_check` for the first
151
+ # time (2026-08-28): `SagaInterpreter#begin_saga`/`#advance_saga`
152
+ # match `pm.starts_on`/`pm.handler_for` against `event.name`, which
153
+ # `CommandRules::Emission#emit` stamps BARE — a command's own
154
+ # `emits AccountDebited` never carries its aggregate's name at all
155
+ # (unlike a policy's cross-aggregate `on`, matched instead by
156
+ # `Naming.demodulise(event.aggregate)` split apart from the bare
157
+ # name — `PolicyInterpreter#policies_for`). Handing a saga's own
158
+ # matcher the DOTTED `event_ref` form ("Account.AccountDebited")
159
+ # would silently name an event no command in the domain ever
160
+ # actually emits — caught by `bin/model_check`'s own `deaf_handler`/
161
+ # `deaf_trigger` findings the moment a real qualified corpus site
162
+ # existed to trip them, not by any unit test in isolation.
163
+ #
164
+ # A qualifier is still worth WRITING (`Account::`) — the same
165
+ # provenance a reader gets from `trigger Account::Debit` — it is
166
+ # only not worth KEEPING: `demodulise` drops everything but the
167
+ # final segment, so `Account::AccountDebited` and a bare
168
+ # `AccountDebited` resolve to the identical stored string. A String
169
+ # passes through unchanged either way, exactly like `command_ref`'s
170
+ # own legacy branch — this corpus never spelled one dotted to begin
171
+ # with, so there is nothing here to strip.
172
+ def event_name_ref(value) = demodulise(value)
173
+ end
174
+ end
@@ -0,0 +1,9 @@
1
+ Hecks.port "access_control" do
2
+ verb "granted_by"
3
+ signal :reply
4
+ answers :session_for_identity
5
+ answers :provision
6
+ answers :available_roles
7
+ answers :grant
8
+ answers :all_people
9
+ end
@@ -0,0 +1,62 @@
1
+ require_relative "../runtime/registry"
2
+
3
+ module Hecks
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.
21
+ module AccessControl
22
+ NAME = "access_control"
23
+
24
+ module_function
25
+
26
+ def session_for_identity(registry, identity_id:)
27
+ adapter(registry).session_for_identity(registry, identity_id: identity_id)
28
+ end
29
+
30
+ def provision(registry, email:, issuer:, subject:)
31
+ adapter(registry).provision(registry, email: email, issuer: issuer, subject: subject)
32
+ end
33
+
34
+ def available_roles(registry)
35
+ adapter(registry).available_roles(registry)
36
+ end
37
+
38
+ def grant(registry, email:, role:)
39
+ adapter(registry).grant(registry, email: email, role: role)
40
+ end
41
+
42
+ def all_people(registry)
43
+ adapter(registry).all_people(registry)
44
+ end
45
+
46
+ def adapter(registry)
47
+ implementations = registry.adapters.values.select { |a| a.port == NAME }
48
+
49
+ case implementations.size
50
+ when 1 then registry.adapter_class(implementations.first.name)
51
+ when 0
52
+ raise Runtime::WiringError,
53
+ "no adapter implements the #{NAME} port — nothing can answer who's allowed in"
54
+ else
55
+ raise Runtime::WiringError,
56
+ "#{implementations.size} adapters implement the #{NAME} port " \
57
+ "(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,104 @@
1
+ module Hecks
2
+ module Ports
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.
16
+ module Answers
17
+ VERB_PATTERN = /\A[A-Za-z]+::[A-Za-z]+\.[A-Za-z]+\z/
18
+
19
+ module_function
20
+
21
+ def questions(raw)
22
+ rows(raw, "questions").map { |row| Question.new(text: text!(row, "text"), because: text!(row, "because")) }
23
+ end
24
+
25
+ def proposals(raw)
26
+ rows(raw, "proposals").map do |row|
27
+ Proposal.new(verb: verb!(row), rationale: text!(row, "rationale"), arguments: arguments!(row))
28
+ end
29
+ end
30
+
31
+ def findings(raw)
32
+ rows(raw, "findings").map do |row|
33
+ Finding.new(kind: kind!(row), severity: severity!(row), subject: text!(row, "subject"),
34
+ message: text!(row, "message"))
35
+ end
36
+ end
37
+
38
+ def suggestions(raw)
39
+ rows(raw, "names").map do |row|
40
+ Suggestion.new(name: text!(row, "name"), because: text!(row, "because"),
41
+ rejected: Array(row["rejected"]).map(&:to_s))
42
+ end
43
+ end
44
+
45
+ # ── shared checks, each named for what it refuses ───────────────
46
+
47
+ def rows(raw, key)
48
+ raise ValidationError, "expected a Hash back, got #{raw.class}: #{raw.inspect}" unless raw.is_a?(Hash)
49
+
50
+ value = raw[key]
51
+ return value if value.is_a?(Array)
52
+
53
+ raise ValidationError, "no #{key.inspect} array in the answer: #{raw.inspect}"
54
+ end
55
+
56
+ def text!(row, key)
57
+ value = row[key]
58
+ return value.to_s if value && !value.to_s.strip.empty?
59
+
60
+ raise ValidationError, "#{key.inspect} missing or blank in #{row.inspect}"
61
+ end
62
+
63
+ # A VERB THE LANGUAGE COULD NOT EVEN PARSE IS AN ADAPTER FAULT,
64
+ # refused right here — the same `not_fully_qualified` shape the
65
+ # language's own grammar already refuses by, reused rather than
66
+ # reinvented. A verb naming a real category that turns out to
67
+ # describe the wrong fact is NOT this port's business: that one
68
+ # dispatches, and `Interview::Session#offer` is what says no.
69
+ def verb!(row)
70
+ verb = row["verb"].to_s
71
+ return verb if VERB_PATTERN.match?(verb)
72
+
73
+ raise ValidationError, "#{verb.inspect} is not a fully-qualified verb (Chapter::Aggregate.Command)"
74
+ end
75
+
76
+ def kind!(row)
77
+ kind = row["kind"].to_s.to_sym
78
+ return kind if CRITIQUE_KINDS.include?(kind)
79
+
80
+ raise ValidationError, "#{kind.inspect} is not a critique kind this port knows (#{CRITIQUE_KINDS.join(', ')})"
81
+ end
82
+
83
+ def severity!(row)
84
+ severity = row["severity"].to_s.to_sym
85
+ return severity if SEVERITIES.include?(severity)
86
+
87
+ raise ValidationError, "#{severity.inspect} is not a severity this port knows (#{SEVERITIES.join(', ')})"
88
+ end
89
+
90
+ # ROWS SHAPED EXACTLY AS `Interview::Proposal::Argument` — a
91
+ # reference's `field` is legitimately blank (a reference IS an
92
+ # id), so only `name` is required here.
93
+ def arguments!(row)
94
+ Array(row["arguments"]).map do |argument|
95
+ name = argument["name"].to_s
96
+ raise ValidationError, "argument row has no name: #{argument.inspect}" if name.empty?
97
+
98
+ { name: name, field: argument["field"].to_s, value: argument["value"].to_s }
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,8 @@
1
+ Hecks.port "agent" do
2
+ verb "asked_by"
3
+ signal :reply
4
+ answers :ask
5
+ answers :interpret
6
+ answers :critique
7
+ answers :suggest_name
8
+ end
@@ -0,0 +1,167 @@
1
+ require_relative "../runtime/registry"
2
+
3
+ module Hecks
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. Resolved the singleton
8
+ # way every other port here is (`Ports::IdentityGeneration`'s own
9
+ # zero/one/many — one interviewer per process, never per-aggregate),
10
+ # because a second adapter answering this port is exactly as
11
+ # unchoosable as a second one minting identities.
12
+ #
13
+ # BUILT LAST, ON PURPOSE, AND STILL OPTIONAL. `.claude/skills/
14
+ # interview/SKILL.md` conducts a whole session with NONE of this —
15
+ # Claude Code reads `bin/interview state` itself, asks the human
16
+ # directly with real conversation context, and calls `bin/interview
17
+ # ask`/`propose`/`accept` itself. This port exists for what that
18
+ # cannot reach: a HEADLESS run (`bin/interview suggest`, no model in
19
+ # the room otherwise), a batch `critique` over an already-written
20
+ # chapter, and — the actual point of building it at all — a
21
+ # SCRIPTED double (`spec/fixtures/scripted_agent.rb`) that makes the
22
+ # whole loop testable with no real model involved.
23
+ #
24
+ # WHAT THIS PORT IS NOT. It does not judge whether a declaration is
25
+ # WELL-FORMED — that is the meta-domain's own job, answered by a real
26
+ # dispatch refusing or not (`Interview::Session#offer`). This port
27
+ # only ever produces SHAPE: a question, a proposed declaration, a
28
+ # judgement about taste, a name. A proposal naming a category the
29
+ # language does not declare is an ADAPTER fault, refused HERE, in
30
+ # `Answers`; a proposal naming a real category but describing the
31
+ # wrong domain fact is dispatched anyway, so the LANGUAGE gets to say
32
+ # why. That line is the whole design.
33
+ #
34
+ # PARSING BELONGS TO THIS FILE, NOT THE ADAPTER. An adapter answers
35
+ # with a plain, already-JSON-shaped Hash (a real model's parsed
36
+ # reply, or a spec double's own hand-built one) — never a Struct —
37
+ # so every adapter is validated identically here rather than trusting
38
+ # each one to refuse the same way. Two adapters that parsed
39
+ # differently would mean the SAME malformed answer passing through
40
+ # one and refusing through the other.
41
+ module Agent
42
+ NAME = "agent"
43
+
44
+ # The answer came back and could not be used — no JSON in it, a
45
+ # category the language does not declare, a severity outside the
46
+ # two that exist. A foreign failure (JSON::ParserError, a missing
47
+ # key) wrapped into something this port owns, the same shape
48
+ # `Ports::Authentication::ValidationError` already is, so no
49
+ # caller ever rescues a raw parser error leaking out of an
50
+ # adapter.
51
+ ValidationError = Class.new(StandardError)
52
+
53
+ # Nothing answered at all — the binary is missing, the subprocess
54
+ # died, the call timed out. Separate from ValidationError on
55
+ # purpose: unavailable means retry or fall back to asking the
56
+ # human directly; malformed means the PROMPT is wrong, not the
57
+ # transport, and retrying identically will not fix it.
58
+ Unavailable = Class.new(StandardError)
59
+
60
+ # ── what the four operations hand back ──────────────────────────
61
+ #
62
+ # Structs, not hashes, for the reason `Bluebook::ModelCheck::Finding`
63
+ # already is one: a `keyword_init` Struct raises on a key nobody
64
+ # declared, which turns "the model invented a field" into a
65
+ # failure at the boundary rather than a nil three calls in.
66
+
67
+ # ONE QUESTION. `because` is the reasoning quoted back so a human
68
+ # can see it, not decoration — the same "show your work" the
69
+ # language's own refusal messages already practice.
70
+ Question = Struct.new(:text, :because, keyword_init: true)
71
+
72
+ # ONE PROPOSED DECLARATION, already shaped as
73
+ # `Interview::Proposal`'s own `Argument` rows — `{name:, field:,
74
+ # value:}`, the exact triple `bin/interview propose --arg
75
+ # name:field:value` already takes and `Interview::Lowering`
76
+ # already knows how to lower. Nothing here decides HOW to address
77
+ # a record or whether a value is bare or wrapped — that was
78
+ # already the proposal-writer's job in `interview.bluebook`'s own
79
+ # design (see `Argument`'s own comment there); this only adds the
80
+ # PROSE reasoning a human typed does not carry on its own.
81
+ Proposal = Struct.new(:verb, :arguments, :rationale, keyword_init: true)
82
+
83
+ # CRITIQUE REUSES `Bluebook::ModelCheck::Finding`'S OWN SHAPE —
84
+ # same fields, same severities, so a mechanical finding
85
+ # (`Session#gaps`) and a judgement (this) print in one list and
86
+ # sort together. The KIND vocabulary is NOT shared — ModelCheck's
87
+ # eleven kinds are structural facts about a graph; these are
88
+ # opinions about a model, closed here on purpose (an open kind
89
+ # field is an open prompt, and an open prompt drifts).
90
+ CRITIQUE_KINDS = %i[
91
+ anemic_aggregate wrong_boundary crud_verb leaky_value_object
92
+ missing_lifecycle missing_invariant ubiquitous_language
93
+ overreaching_identity untold_rule
94
+ ].freeze
95
+ SEVERITIES = %i[error warning].freeze
96
+
97
+ Finding = Struct.new(:kind, :severity, :subject, :message, keyword_init: true) do
98
+ def to_s = "#{severity.to_s.upcase.ljust(7)} #{kind.to_s.ljust(20)} #{subject} — #{message}"
99
+ end
100
+
101
+ # ONE SUGGESTED NAME. `rejected` carries the near-misses and why
102
+ # they were passed over — free at the point the model is choosing
103
+ # anyway, and the part a human actually learns from.
104
+ Suggestion = Struct.new(:name, :because, :rejected, keyword_init: true)
105
+
106
+ module_function
107
+
108
+ # THE NEXT BEST QUESTION. `state` is the whole picture — normally
109
+ # `Interview::Session#declaration` plus `#gaps`, so an adapter
110
+ # needs no memory of its own between calls; that is what lets a
111
+ # headless run be a series of one-shot processes, same as
112
+ # `bin/interview` itself already is.
113
+ def ask(registry, state:, asked: [])
114
+ Answers.questions(adapter(registry).ask(state: state, asked: asked))
115
+ end
116
+
117
+ # PROSE -> PROPOSED DECLARATIONS. Returns `[]` when the sentence
118
+ # carried no declaration at all (a clarifying question back from
119
+ # the human, say) — a legitimate answer, not a failure.
120
+ def interpret(registry, prose:, state:)
121
+ Answers.proposals(adapter(registry).interpret(prose: prose, state: state))
122
+ end
123
+
124
+ # WHAT IS WRONG WITH THIS AS A MODEL — handed everything already
125
+ # known (the language's own refusals, `Session#gaps`'s mechanical
126
+ # findings) so it spends its judgement on what neither of those
127
+ # can see, rather than restating them.
128
+ def critique(registry, declared:, refusals: [], findings: [])
129
+ Answers.findings(adapter(registry).critique(declared: declared, refusals: refusals, findings: findings))
130
+ end
131
+
132
+ # VOCABULARY HELP. `near` is what the chapter already calls
133
+ # things, so a suggestion cannot collide with a name in use.
134
+ #
135
+ # NAMED `suggest_name`, NOT `name` — the plan's own word for this
136
+ # operation, but `Module#name` already exists and is load-bearing
137
+ # everywhere (backtraces, RSpec's own description building,
138
+ # `inspect`): a module-function called `name` SHADOWS it the
139
+ # instant it's defined, and `SomeModule.name` (no args) then
140
+ # raises `ArgumentError: missing keywords` the next time anything
141
+ # — not this file, something else entirely — asks the module its
142
+ # own name. Measured, not theoretical: this exact collision broke
143
+ # RSpec's own failure-message formatting the first time it was
144
+ # named `name` here.
145
+ def suggest_name(registry, meaning:, kind:, near: [])
146
+ Answers.suggestions(adapter(registry).suggest_name(meaning: meaning, kind: kind, near: near))
147
+ end
148
+
149
+ def adapter(registry)
150
+ implementations = registry.adapters.values.select { |a| a.port == NAME }
151
+
152
+ case implementations.size
153
+ when 1 then registry.adapter_class(implementations.first.name)
154
+ when 0
155
+ raise Runtime::WiringError,
156
+ "no adapter implements the #{NAME} port — nothing can conduct an interview"
157
+ else
158
+ raise Runtime::WiringError,
159
+ "#{implementations.size} adapters implement the #{NAME} port " \
160
+ "(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ require_relative "agent/answers"
@@ -0,0 +1,6 @@
1
+ Hecks.port "authentication" do
2
+ verb "authenticated_by"
3
+ signal :reply
4
+ answers :authorization_url
5
+ answers :verify
6
+ end
@@ -0,0 +1,50 @@
1
+ require_relative "../runtime/registry"
2
+
3
+ module Hecks
4
+ module Ports
5
+ # THE OIDC HANDSHAKE ITSELF — building the URL a browser goes to,
6
+ # and turning the code a provider sends back into a verified
7
+ # (issuer, subject, email) triple. Resolved the same way every
8
+ # other port here resolves its adapter: one adapter registry-wide
9
+ # answers this, not a per-aggregate binding, since a domain has no
10
+ # reason to want a different sign-in provider per aggregate.
11
+ #
12
+ # THE OTHER HALF of "who is this" — what a verified pair MEANS to
13
+ # this app, whether it resolves to an existing Identity — is
14
+ # `Ports::IdentityResolution`'s job, not this one's. This port only
15
+ # ever talks to the external provider; it never touches the
16
+ # registry's own Identity/Governance data (it takes `registry`
17
+ # purely to resolve which adapter answers it, same as every port
18
+ # here already does).
19
+ module Authentication
20
+ NAME = "authentication"
21
+
22
+ ValidationError = Class.new(StandardError)
23
+
24
+ module_function
25
+
26
+ def authorization_url(registry)
27
+ adapter(registry).authorization_url
28
+ end
29
+
30
+ def verify(registry, code:, state:, expected_state:)
31
+ adapter(registry).verify(code: code, state: state, expected_state: expected_state)
32
+ end
33
+
34
+ def adapter(registry)
35
+ implementations = registry.adapters.values.select { |a| a.port == NAME }
36
+
37
+ case implementations.size
38
+ when 1 then registry.adapter_class(implementations.first.name)
39
+ when 0
40
+ raise Runtime::WiringError,
41
+ "no adapter implements the #{NAME} port — nothing can authenticate a sign-in"
42
+ else
43
+ raise Runtime::WiringError,
44
+ "#{implementations.size} adapters implement the #{NAME} port " \
45
+ "(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,7 @@
1
+ Hecks.port "authorization" do
2
+ verb "authorized_by"
3
+ signal :reply
4
+ answers :holds_role?
5
+ answers :authorized_as?
6
+ answers :live_role_for
7
+ end
@@ -0,0 +1,62 @@
1
+ require_relative "../runtime/registry"
2
+
3
+ module Hecks
4
+ module Ports
5
+ # TWO YES/NO QUESTIONS AND ONE VALUE an application asks BEFORE
6
+ # binding a caller — resolved the same way `Ports::IdentityGeneration`
7
+ # resolves its own adapter: one adapter registry-wide answers this
8
+ # port, not a per-aggregate binding, since a domain has no reason to
9
+ # want a different authorization source per aggregate. What answers
10
+ # it is deliberately not named here — the governance-backed adapter
11
+ # is one implementation, not the only possible one, the same way
12
+ # `SequentialIdentity` and `SecureRandomIdentity` are two
13
+ # implementations of `identity_generation`.
14
+ #
15
+ # A caller decides what to DO with the answer — dispatch under that
16
+ # role, refuse, log, prefer it over some other fallback value — this
17
+ # only answers the question asked. Nothing here binds a
18
+ # `Runtime::Caller`.
19
+ #
20
+ # STALE AS OF THE `as_of`/`scope` split below, this used to also say
21
+ # nothing here is consulted by `CommandRules::Authorization` — it now
22
+ # is: `refuse_role_mismatch` calls `holds_role?` directly, once a
23
+ # caller binds an `actor_id` and the command's domain has Governance
24
+ # attached (see that rule's own header). `spec/act_as_spec.rb`
25
+ # remains the precedent for the OTHER shape — two separate
26
+ # registries, queried directly by name — for whenever Governance is
27
+ # not in the same boot as the caller; this port is the same
28
+ # questions asked through one adapter when it is.
29
+ module Authorization
30
+ NAME = "authorization"
31
+
32
+ module_function
33
+
34
+ def holds_role?(registry, actor_id:, role:, as_of: nil, scope: nil)
35
+ adapter(registry).holds_role?(registry, actor_id: actor_id, role: role, as_of: as_of, scope: scope)
36
+ end
37
+
38
+ def authorized_as?(registry, from_role:, to_role:)
39
+ adapter(registry).authorized_as?(registry, from_role: from_role, to_role: to_role)
40
+ end
41
+
42
+ def live_role_for(registry, actor_id:)
43
+ adapter(registry).live_role_for(registry, actor_id: actor_id)
44
+ end
45
+
46
+ def adapter(registry)
47
+ implementations = registry.adapters.values.select { |a| a.port == NAME }
48
+
49
+ case implementations.size
50
+ when 1 then registry.adapter_class(implementations.first.name)
51
+ when 0
52
+ raise Runtime::WiringError,
53
+ "no adapter implements the #{NAME} port — nothing can answer a role check"
54
+ else
55
+ raise Runtime::WiringError,
56
+ "#{implementations.size} adapters implement the #{NAME} port " \
57
+ "(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ Hecks.port "clock" do
2
+ verb "timed_by"
3
+ signal :reply
4
+ answers :now
5
+ end