hecks 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -2,41 +2,51 @@ 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
5
+ # The interviewer — whatever asks the next good question, reads a
6
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.
7
+ # rather than structure, or suggests a name.
12
8
  #
13
- # BUILT LAST, ON PURPOSE, AND STILL OPTIONAL. `.claude/skills/
14
- # interview/SKILL.md` conducts a whole session with NONE of this —
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 —
15
21
  # Claude Code reads `bin/interview state` itself, asks the human
16
22
  # directly with real conversation context, and calls `bin/interview
17
23
  # ask`/`propose`/`accept` itself. This port exists for what that
18
- # cannot reach: a HEADLESS run (`bin/interview suggest`, no model in
24
+ # cannot reach: a headless run (`bin/interview suggest`, no model in
19
25
  # the room otherwise), a batch `critique` over an already-written
20
26
  # chapter, and — the actual point of building it at all — a
21
- # SCRIPTED double (`spec/fixtures/scripted_agent.rb`) that makes the
27
+ # scripted double (`spec/fixtures/scripted_agent.rb`) that makes the
22
28
  # whole loop testable with no real model involved.
23
29
  #
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.
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.
33
41
  #
34
- # PARSING BELONGS TO THIS FILE, NOT THE ADAPTER. An adapter answers
42
+ # ## Where parsing happens
43
+ #
44
+ # Parsing belongs to this file, not the adapter. An adapter answers
35
45
  # with a plain, already-JSON-shaped Hash (a real model's parsed
36
46
  # reply, or a spec double's own hand-built one) — never a Struct —
37
47
  # so every adapter is validated identically here rather than trusting
38
48
  # each one to refuse the same way. Two adapters that parsed
39
- # differently would mean the SAME malformed answer passing through
49
+ # differently would mean the same malformed answer passing through
40
50
  # one and refusing through the other.
41
51
  module Agent
42
52
  NAME = "agent".freeze
@@ -54,7 +64,7 @@ module Hecks
54
64
  # Nothing answered at all — the binary is missing, the subprocess
55
65
  # died, the call timed out. Separate from ValidationError on
56
66
  # purpose: unavailable means retry or fall back to asking the
57
- # human directly; malformed means the PROMPT is wrong, not the
67
+ # human directly; malformed means the prompt is wrong, not the
58
68
  # transport, and retrying identically will not fix it.
59
69
  class Unavailable < StandardError
60
70
  end
@@ -66,26 +76,26 @@ module Hecks
66
76
  # declared, which turns "the model invented a field" into a
67
77
  # failure at the boundary rather than a nil three calls in.
68
78
 
69
- # ONE QUESTION. `because` is the reasoning quoted back so a human
79
+ # One question. `because` is the reasoning quoted back so a human
70
80
  # can see it, not decoration — the same "show your work" the
71
81
  # language's own refusal messages already practice.
72
82
  Question = Struct.new(:text, :because, keyword_init: true)
73
83
 
74
- # ONE PROPOSED DECLARATION, already shaped as
84
+ # One proposed declaration, already shaped as
75
85
  # `Interview::Proposal`'s own `Argument` rows — `{name:, field:,
76
86
  # value:}`, the exact triple `bin/interview propose --arg
77
87
  # name:field:value` already takes and `Interview::Lowering`
78
- # already knows how to lower. Nothing here decides HOW to address
88
+ # already knows how to lower. Nothing here decides how to address
79
89
  # a record or whether a value is bare or wrapped — that was
80
90
  # already the proposal-writer's job in `interview.bluebook`'s own
81
91
  # design (see `Argument`'s own comment there); this only adds the
82
- # PROSE reasoning a human typed does not carry on its own.
92
+ # prose reasoning a human typed does not carry on its own.
83
93
  Proposal = Struct.new(:verb, :arguments, :rationale, keyword_init: true)
84
94
 
85
- # CRITIQUE REUSES `Bluebook::ModelCheck::Finding`'S OWN SHAPE
95
+ # Critique reuses `Bluebook::ModelCheck::Finding`'s own shape
86
96
  # same fields, same severities, so a mechanical finding
87
97
  # (`Session#gaps`) and a judgement (this) print in one list and
88
- # sort together. The KIND vocabulary is NOT shared — ModelCheck's
98
+ # sort together. The kind vocabulary is not shared — ModelCheck's
89
99
  # eleven kinds are structural facts about a graph; these are
90
100
  # opinions about a model, closed here on purpose (an open kind
91
101
  # field is an open prompt, and an open prompt drifts).
@@ -100,54 +110,128 @@ module Hecks
100
110
  def to_s = "#{severity.to_s.upcase.ljust(7)} #{kind.to_s.ljust(20)} #{subject} — #{message}"
101
111
  end
102
112
 
103
- # ONE SUGGESTED NAME. `rejected` carries the near-misses and why
113
+ # One suggested name. `rejected` carries the near-misses and why
104
114
  # they were passed over — free at the point the model is choosing
105
115
  # anyway, and the part a human actually learns from.
106
116
  Suggestion = Struct.new(:name, :because, :rejected, keyword_init: true)
107
117
 
108
118
  module_function
109
119
 
110
- # THE NEXT BEST QUESTION. `state` is the whole picture normally
120
+ # Asks the adapter for the next best question and validates its raw answer into structs.
121
+ #
122
+ # `state` is the whole picture — normally
111
123
  # `Interview::Session#declaration` plus `#gaps`, so an adapter
112
124
  # needs no memory of its own between calls; that is what lets a
113
125
  # headless run be a series of one-shot processes, same as
114
126
  # `bin/interview` itself already is.
127
+ #
128
+ # @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`)
115
141
  def ask(registry, state:, asked: [])
116
142
  Answers.questions(adapter(registry).ask(state: state, asked: asked))
117
143
  end
118
144
 
119
- # PROSE -> PROPOSED DECLARATIONS. Returns `[]` when the sentence
145
+ # Turns a human's sentence into proposed declarations, validated into structs.
146
+ #
147
+ # Returns `[]` when the sentence
120
148
  # carried no declaration at all (a clarifying question back from
121
149
  # the human, say) — a legitimate answer, not a failure.
150
+ #
151
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
152
+ # @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`)
122
164
  def interpret(registry, prose:, state:)
123
165
  Answers.proposals(adapter(registry).interpret(prose: prose, state: state))
124
166
  end
125
167
 
126
- # WHAT IS WRONG WITH THIS AS A MODEL handed everything already
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
127
171
  # known (the language's own refusals, `Session#gaps`'s mechanical
128
172
  # findings) so it spends its judgement on what neither of those
129
173
  # can see, rather than restating them.
174
+ #
175
+ # @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`)
130
191
  def critique(registry, declared:, refusals: [], findings: [])
131
192
  Answers.findings(adapter(registry).critique(declared: declared, refusals: refusals, findings: findings))
132
193
  end
133
194
 
134
- # VOCABULARY HELP. `near` is what the chapter already calls
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
135
198
  # things, so a suggestion cannot collide with a name in use.
136
199
  #
137
- # NAMED `suggest_name`, NOT `name` — the plan's own word for this
200
+ # Named `suggest_name`, not `name` — the plan's own word for this
138
201
  # operation, but `Module#name` already exists and is load-bearing
139
202
  # everywhere (backtraces, RSpec's own description building,
140
- # `inspect`): a module-function called `name` SHADOWS it the
203
+ # `inspect`): a module-function called `name` shadows it the
141
204
  # instant it's defined, and `SomeModule.name` (no args) then
142
205
  # raises `ArgumentError: missing keywords` the next time anything
143
206
  # — not this file, something else entirely — asks the module its
144
207
  # own name. Measured, not theoretical: this exact collision broke
145
208
  # RSpec's own failure-message formatting the first time it was
146
209
  # named `name` here.
210
+ #
211
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
212
+ # @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`)
147
225
  def suggest_name(registry, meaning:, kind:, near: [])
148
226
  Answers.suggestions(adapter(registry).suggest_name(meaning: meaning, kind: kind, near: near))
149
227
  end
150
228
 
229
+ # Finds the single adapter bound to this port, refusing an ambiguous wiring.
230
+ #
231
+ # @param registry [Runtime::Registry] the booted registry to search
232
+ # @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`
151
235
  def adapter(registry)
152
236
  implementations = registry.adapters.values.select { |a| a.port == NAME }
153
237
 
@@ -2,14 +2,14 @@ require_relative "../runtime/registry"
2
2
 
3
3
  module Hecks
4
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
5
+ # The OIDC handshake itself — building the URL a browser goes to,
6
+ # and turning the code a provider sends back into a Hash of verified
7
+ # claims (`issuer`, `subject`, `email`, `email_verified`). Resolved the same way every
8
8
  # other port here resolves its adapter: one adapter registry-wide
9
9
  # answers this, not a per-aggregate binding, since a domain has no
10
10
  # reason to want a different sign-in provider per aggregate.
11
11
  #
12
- # THE OTHER HALF of "who is this" — what a verified pair MEANS to
12
+ # The other half of "who is this" — what a verified pair means to
13
13
  # this app, whether it resolves to an existing Identity — is
14
14
  # `Ports::IdentityResolution`'s job, not this one's. This port only
15
15
  # ever talks to the external provider; it never touches the
@@ -19,19 +19,59 @@ module Hecks
19
19
  module Authentication
20
20
  NAME = "authentication".freeze
21
21
 
22
+ # A sign-in that could not be verified — a state mismatch, a failed code
23
+ # exchange, a missing or invalid ID token. An adapter wraps its own
24
+ # libraries' errors into this, so a caller rescues one class whichever
25
+ # provider answers the port.
22
26
  class ValidationError < StandardError
23
27
  end
24
28
 
25
29
  module_function
26
30
 
31
+ # Builds the provider URL that starts a sign-in, with the CSRF state minted for it.
32
+ #
33
+ # The caller keeps the state (in a session, typically) and hands it back to `verify`
34
+ # as `expected_state`.
35
+ #
36
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
37
+ # @return [Array(String, String)] the URL to send the browser to, then the fresh
38
+ # `state` value embedded in it
39
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
40
+ # (see `adapter`)
41
+ # @raise [KeyError] if the adapter's required configuration is unset
42
+ # (`GoogleAuthentication` reads `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` and
43
+ # `GOOGLE_REDIRECT_URI` with no defaults)
27
44
  def authorization_url(registry)
28
45
  adapter(registry).authorization_url
29
46
  end
30
47
 
48
+ # Exchanges the authorization code a provider sent back for a Hash of verified claims.
49
+ #
50
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
51
+ # @param code [String] the authorization code the provider sent back
52
+ # @param state [String, nil] the state parameter the provider returned; nil is refused
53
+ # @param expected_state [String, nil] the state `authorization_url` handed out before
54
+ # the redirect; nil is refused
55
+ # @return [Hash{Symbol => Object}] the verified claims: `issuer:` and `subject:`
56
+ # (String), `email:` (String, or nil if the token carries none) and
57
+ # `email_verified:` (Boolean) — never the raw token
58
+ # @raise [Ports::Authentication::ValidationError] if either state is nil or the two
59
+ # differ, the code exchange fails, the response has no ID token, or the ID token
60
+ # does not verify
61
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
62
+ # (see `adapter`)
63
+ # @raise [KeyError] if the adapter's required configuration is unset, or a verified
64
+ # token lacks an `iss` or `sub` claim
31
65
  def verify(registry, code:, state:, expected_state:)
32
66
  adapter(registry).verify(code: code, state: state, expected_state: expected_state)
33
67
  end
34
68
 
69
+ # Finds the single adapter bound to this port, refusing an ambiguous wiring.
70
+ #
71
+ # @param registry [Runtime::Registry] the booted registry to search
72
+ # @return [Module] the adapter module or class implementing this port
73
+ # @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
74
+ # or the one that does has no Ruby implementation under `Hecks::Adapters`
35
75
  def adapter(registry)
36
76
  implementations = registry.adapters.values.select { |a| a.port == NAME }
37
77
 
@@ -2,7 +2,7 @@ require_relative "../runtime/registry"
2
2
 
3
3
  module Hecks
4
4
  module Ports
5
- # TWO YES/NO QUESTIONS AND ONE VALUE an application asks BEFORE
5
+ # Two yes/no questions and one value an application asks before
6
6
  # binding a caller — resolved the same way `Ports::IdentityGeneration`
7
7
  # resolves its own adapter: one adapter registry-wide answers this
8
8
  # port, not a per-aggregate binding, since a domain has no reason to
@@ -12,37 +12,79 @@ module Hecks
12
12
  # `SequentialIdentity` and `SecureRandomIdentity` are two
13
13
  # implementations of `identity_generation`.
14
14
  #
15
- # A caller decides what to DO with the answer — dispatch under that
15
+ # A caller decides what to do with the answer — dispatch under that
16
16
  # role, refuse, log, prefer it over some other fallback value — this
17
17
  # only answers the question asked. Nothing here binds a
18
18
  # `Runtime::Caller`.
19
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.
20
+ # `CommandRules::Authorization#refuse_role_mismatch` calls `holds_role?`
21
+ # directly, once a caller binds an `actor_id` and the command's domain
22
+ # has Governance attached (see that rule's own header). `spec/
23
+ # act_as_spec.rb` remains the precedent for the other shape two
24
+ # separate registries, queried directly by name — for whenever
25
+ # Governance is not in the same boot as the caller; this port is the
26
+ # same questions asked through one adapter when it is.
29
27
  module Authorization
30
28
  NAME = "authorization".freeze
31
29
 
32
30
  module_function
33
31
 
32
+ # Answers whether an actor holds a live (not ended) grant of a role.
33
+ #
34
+ # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
35
+ # and handed on to it
36
+ # @param actor_id [String] the actor whose grants are checked
37
+ # @param role [String, Symbol] the role name to look for, compared as a String
38
+ # @param as_of [Integer, nil] Unix epoch seconds (from `Ports::Clock.now`); a grant whose
39
+ # `starts_at` is later, or does not parse as a time, does not count. nil skips the
40
+ # `starts_at` check entirely
41
+ # @param scope [String, nil] the scope the caller acts in; only a grant made for that
42
+ # scope counts. nil skips the scope check, so a grant in any scope counts
43
+ # @return [Boolean] true if at least one live grant of `role` to `actor_id` passes the
44
+ # `as_of` and `scope` checks
45
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
46
+ # (see `adapter`), or the governance-backed adapter finds no single loaded chapter
47
+ # providing `"authorization"`
34
48
  def holds_role?(registry, actor_id:, role:, as_of: nil, scope: nil)
35
49
  adapter(registry).holds_role?(registry, actor_id: actor_id, role: role, as_of: as_of, scope: scope)
36
50
  end
37
51
 
52
+ # Answers whether one role may act as another.
53
+ #
54
+ # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
55
+ # and handed on to it
56
+ # @param from_role [String, Symbol] the role the caller holds, compared as a String
57
+ # @param to_role [String, Symbol] the role the caller wants to act as, compared as a
58
+ # String
59
+ # @return [Boolean] true if a live (not ended) allowance lets `from_role` act as
60
+ # `to_role`
61
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
62
+ # (see `adapter`), or the governance-backed adapter finds no single loaded chapter
63
+ # providing `"authorization"`
38
64
  def authorized_as?(registry, from_role:, to_role:)
39
65
  adapter(registry).authorized_as?(registry, from_role: from_role, to_role: to_role)
40
66
  end
41
67
 
68
+ # Looks up the role an actor holds right now, rather than checking a guessed one.
69
+ #
70
+ # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
71
+ # and handed on to it
72
+ # @param actor_id [String] the actor to look up
73
+ # @return [String, nil] the role name of the actor's first live (not ended) grant, or
74
+ # nil if it has none; the caller supplies any fallback
75
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
76
+ # (see `adapter`), or the governance-backed adapter finds no single loaded chapter
77
+ # providing `"authorization"`
42
78
  def live_role_for(registry, actor_id:)
43
79
  adapter(registry).live_role_for(registry, actor_id: actor_id)
44
80
  end
45
81
 
82
+ # Finds the single adapter bound to this port, refusing an ambiguous wiring.
83
+ #
84
+ # @param registry [Runtime::Registry] the booted registry to search
85
+ # @return [Module] the adapter module or class implementing this port
86
+ # @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
87
+ # or the one that does has no Ruby implementation under `Hecks::Adapters`
46
88
  def adapter(registry)
47
89
  implementations = registry.adapters.values.select { |a| a.port == NAME }
48
90
 
@@ -2,47 +2,66 @@ require_relative "../runtime/registry"
2
2
 
3
3
  module Hecks
4
4
  module Ports
5
- # WHAT TIME IT IS — the one fact a domain cannot derive and must not invent.
5
+ # What time it is — the one fact a domain cannot derive and must not invent.
6
6
  #
7
7
  # Resolved exactly the way `Ports::IdentityGeneration` resolves its own
8
8
  # adapter: one adapter registry-wide implements this, not a per-aggregate
9
9
  # binding, because two aggregates in one running app have no real reason to
10
10
  # disagree about the time.
11
11
  #
12
- # WHY A PORT AND NOT `Time.now`. A staleness rule — "another agent may take
13
- # this claim after fifteen minutes" — is untestable against the real clock:
14
- # a spec for it would either sleep for fifteen minutes or never run at all.
15
- # Bound to a fixed adapter, the same rule is three lines. That is the whole
16
- # argument, and it is the same one `SequentialIdentity` makes next door.
12
+ # ## Why a port and not `Time.now`
17
13
  #
18
- # A PREDICATE STILL CANNOT ASK THE TIME, and this does not change that. The
19
- # expression sublanguage has no clock and should not: a `given` that read
20
- # the time would evaluate differently on two runs over the same record, and
21
- # every replay, audit and fuzz oracle in this repository assumes it does
22
- # not. So `now` remains an ARGUMENT the predicate merely reads, and this
23
- # port is what lets a caller stop typing it — the door fills it in, the
24
- # value is baked into the dispatch, and the recorded step carries the
25
- # concrete number forever after.
14
+ # A staleness rule "another agent may take this claim after fifteen
15
+ # minutes" is untestable against the real clock: a spec for it would
16
+ # either sleep for fifteen minutes or never run at all. Bound to a fixed
17
+ # adapter, the same rule is three lines. That is the whole argument, and
18
+ # it is the same one `SequentialIdentity` makes next door.
26
19
  #
27
- # WHICH IS WHY THE RUNTIME DOES NOT CALL THIS. `IdentityGeneration`'s own
28
- # note works through the replay question for a minted uuid and lands on
29
- # "the value gets baked into the caller's args at the first live dispatch".
30
- # A clock consulted INSIDE the interpreter would not have that property: a
31
- # recorded corpus step replayed tomorrow would silently get tomorrow's
32
- # time, and the fuzzer's oracle and the adapter-agreement gate both compare
33
- # runs of exactly that shape. So the filling happens at the door, where a
34
- # human or an agent is typing, and never on the dispatch path a replay uses.
20
+ # ## Why a predicate still can't ask the time
21
+ #
22
+ # The expression sublanguage has no clock and should not: a `given` that
23
+ # read the time would evaluate differently on two runs over the same
24
+ # record, and every replay, audit and fuzz oracle in this repository
25
+ # assumes it does not. So `now` remains an argument the predicate merely
26
+ # reads, and this port is what lets a caller stop typing it — the door
27
+ # fills it in, the value is baked into the dispatch, and the recorded
28
+ # step carries the concrete number forever after.
29
+ #
30
+ # ## Why the runtime never calls this directly
31
+ #
32
+ # `IdentityGeneration`'s own note works through the replay question for
33
+ # a minted uuid and lands on "the value gets baked into the caller's
34
+ # args at the first live dispatch". A clock consulted inside the
35
+ # interpreter would not have that property: a recorded corpus step
36
+ # replayed tomorrow would silently get tomorrow's time, and the
37
+ # fuzzer's oracle and the adapter-agreement gate both compare runs of
38
+ # exactly that shape. So the filling happens at the door, where a
39
+ # human or an agent is typing, and never on the dispatch path a replay
40
+ # uses.
35
41
  module Clock
36
42
  NAME = "clock".freeze
37
43
 
38
44
  module_function
39
45
 
40
- # SECONDS, AS AN INTEGER, because that is what the sublanguage can do
46
+ # Reads the current time from the bound clock adapter.
47
+ #
48
+ # Seconds, as an integer, because that is what the sublanguage can do
41
49
  # arithmetic on. `claimed_at.value + window.value <= now.value` is
42
50
  # addition and comparison — the only two things it has — and a Time
43
51
  # object supports neither.
52
+ #
53
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
54
+ # @return [Integer] the current time, in Unix epoch seconds
55
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
56
+ # (see `adapter`)
44
57
  def now(registry) = adapter(registry).now
45
58
 
59
+ # Finds the single adapter bound to this port, refusing an ambiguous wiring.
60
+ #
61
+ # @param registry [Runtime::Registry] the booted registry to search
62
+ # @return [Module] the adapter module or class implementing this port
63
+ # @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
64
+ # or the one that does has no Ruby implementation under `Hecks::Adapters`
46
65
  def adapter(registry)
47
66
  implementations = registry.adapters.values.select { |a| a.port == NAME }
48
67
 
@@ -11,8 +11,24 @@ module Hecks
11
11
 
12
12
  module_function
13
13
 
14
+ # Recovers a block's body as canonical source text, so a rule is carried as text.
15
+ #
16
+ # @param block [Proc] a block written in a bluebook file, such as a `given` predicate
17
+ # or an `identified_by` path
18
+ # @return [String, nil] the block body's source, normalised by
19
+ # `Bluebook::Expression::CanonicalForm` (whitespace collapsed, linked replacements
20
+ # applied); nil if the block's file cannot be read, no block starts on its line, or
21
+ # the block has an empty body
22
+ # @raise [Runtime::WiringError] if called outside a boot, or this port does not resolve
23
+ # to exactly one adapter (see `adapter`)
14
24
  def canonical(block) = adapter.canonical(block)
15
25
 
26
+ # Finds the single adapter bound to this port in the registry currently booting.
27
+ #
28
+ # @return [Module] the adapter module or class implementing this port
29
+ # @raise [Runtime::WiringError] if `Hecks.current_registry` is nil (resolved outside a
30
+ # boot), if no adapter, or more than one, implements this port, or if the one that
31
+ # does has no Ruby implementation under `Hecks::Adapters`
16
32
  def adapter
17
33
  registry = Hecks.current_registry
18
34
  unless registry
@@ -2,7 +2,7 @@ require_relative "../runtime/registry"
2
2
 
3
3
  module Hecks
4
4
  module Ports
5
- # WHICH VALUE A CREATING COMMAND'S OWN IDENTITY FIELD GETS, when
5
+ # Which value a creating command's own identity field gets, when
6
6
  # neither a slug-from-another-field nor a sequence-with-prefix
7
7
  # mechanically answers it — a driving app's own console, say,
8
8
  # falls back to this when a collection's identity strategy names
@@ -12,7 +12,7 @@ module Hecks
12
12
  #
13
13
  # Pure delegation, same as every sibling port (AccessControl,
14
14
  # IdentityGeneration): an app's own domain declares what "port"
15
- # actually MEANS for it — mint a real UUID via
15
+ # actually means for it — mint a real UUID via
16
16
  # Ports::IdentityGeneration, derive something else entirely — this
17
17
  # port just resolves the one adapter that domain wired and calls
18
18
  # it, the same "one adapter registry-wide, refuse if zero or many"
@@ -22,10 +22,32 @@ module Hecks
22
22
 
23
23
  module_function
24
24
 
25
+ # Asks the domain's adapter which value a creating command's identity field gets.
26
+ #
27
+ # No adapter, spec double or caller of this port ships in this repository, so every
28
+ # shape below other than `registry` is adapter-defined: the port forwards it untouched.
29
+ #
30
+ # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
31
+ # and handed on to it
32
+ # @param agg_name [Object] adapter-defined, forwarded unchanged; names the aggregate
33
+ # whose identity field is being assigned
34
+ # @param field_name [Object] adapter-defined, forwarded unchanged; names the identity
35
+ # field
36
+ # @param args [Object] adapter-defined, forwarded unchanged; the creating command's own
37
+ # arguments
38
+ # @return [Object] adapter-defined value to assign as the identity
39
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
40
+ # (see `adapter`)
25
41
  def assign(registry, agg_name:, field_name:, args:)
26
42
  adapter(registry).assign(registry, agg_name: agg_name, field_name: field_name, args: args)
27
43
  end
28
44
 
45
+ # Finds the single adapter bound to this port, refusing an ambiguous wiring.
46
+ #
47
+ # @param registry [Runtime::Registry] the booted registry to search
48
+ # @return [Module] the adapter module or class implementing this port
49
+ # @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
50
+ # or the one that does has no Ruby implementation under `Hecks::Adapters`
29
51
  def adapter(registry)
30
52
  implementations = registry.adapters.values.select { |a| a.port == NAME }
31
53