hecks 0.2.0 → 0.3.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 (408) 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 +403 -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 +62 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +148 -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 +131 -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 +80 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +391 -0
  31. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
  32. data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
  33. data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
  34. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
  35. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
  36. data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
  37. data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
  38. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
  39. data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
  40. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
  41. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
  42. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
  45. data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
  46. data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
  47. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  48. data/lib/hecks/adapters/driven/prism.rb +80 -0
  49. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  50. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  51. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  52. data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
  53. data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
  54. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
  55. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  56. data/lib/hecks/adapters/driven/sqlite.rb +285 -0
  57. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  58. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  59. data/lib/hecks/adapters/driven.rb +36 -0
  60. data/lib/hecks/adapters.rb +6 -0
  61. data/lib/hecks/behaviors/dsl.rb +96 -0
  62. data/lib/hecks/behaviors/expectations.rb +280 -0
  63. data/lib/hecks/behaviors/ir.rb +31 -0
  64. data/lib/hecks/behaviors/rspec.rb +42 -0
  65. data/lib/hecks/behaviors/runner.rb +96 -0
  66. data/lib/hecks/behaviors.rb +25 -0
  67. data/lib/hecks/bluebook/aggregate.rb +108 -0
  68. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  69. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  70. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  71. data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
  72. data/lib/hecks/bluebook/assembly/marks.rb +227 -0
  73. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  74. data/lib/hecks/bluebook/assembly.rb +78 -0
  75. data/lib/hecks/bluebook/attribute.rb +96 -0
  76. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  77. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  78. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  79. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  80. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  81. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  82. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  83. data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
  84. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  85. data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
  86. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  87. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  88. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  89. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  90. data/lib/hecks/bluebook/chapter.rb +68 -0
  91. data/lib/hecks/bluebook/command.rb +124 -0
  92. data/lib/hecks/bluebook/domain_port.rb +102 -0
  93. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  94. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
  95. data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
  96. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
  98. data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
  99. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  100. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  101. data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
  102. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  103. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  104. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  105. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  106. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  107. data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
  108. data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
  109. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  110. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
  111. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  112. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  113. data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
  114. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  115. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  116. data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
  117. data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
  118. data/lib/hecks/bluebook/dsl.rb +45 -0
  119. data/lib/hecks/bluebook/entity.rb +103 -0
  120. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
  121. data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
  122. data/lib/hecks/bluebook/expression/projection.json +170 -0
  123. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  124. data/lib/hecks/bluebook/expression/resolver.rb +630 -0
  125. data/lib/hecks/bluebook/expression.rb +13 -0
  126. data/lib/hecks/bluebook/hexagon.rb +60 -0
  127. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  128. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  129. data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
  130. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  131. data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
  132. data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
  133. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
  134. data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
  135. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  136. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  137. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  138. data/lib/hecks/bluebook/meta_validator.rb +452 -0
  139. data/lib/hecks/bluebook/model_check.rb +318 -0
  140. data/lib/hecks/bluebook/pattern_subset.rb +120 -0
  141. data/lib/hecks/bluebook/policy.rb +41 -0
  142. data/lib/hecks/bluebook/process_manager.rb +86 -0
  143. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  144. data/lib/hecks/bluebook/project_loader.rb +40 -0
  145. data/lib/hecks/bluebook/project_register.rb +82 -0
  146. data/lib/hecks/bluebook/query.rb +61 -0
  147. data/lib/hecks/bluebook/read_model.rb +109 -0
  148. data/lib/hecks/bluebook/reference.rb +74 -0
  149. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  150. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  151. data/lib/hecks/bluebook/translation.rb +92 -0
  152. data/lib/hecks/bluebook/value_object.rb +58 -0
  153. data/lib/hecks/bluebook.rb +74 -0
  154. data/lib/hecks/codemod.rb +342 -0
  155. data/lib/hecks/construct.rb +71 -0
  156. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  157. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  158. data/lib/hecks/deploy/oidc.json +18 -0
  159. data/lib/hecks/doc/reference.rb +389 -0
  160. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  161. data/lib/hecks/facade/cli_door.rb +116 -0
  162. data/lib/hecks/facade/cli_runner.rb +190 -0
  163. data/lib/hecks/facade/command_request.rb +105 -0
  164. data/lib/hecks/facade/handle.rb +173 -0
  165. data/lib/hecks/facade/json_door.rb +154 -0
  166. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  167. data/lib/hecks/facade/surface/chapter.rb +107 -0
  168. data/lib/hecks/facade/surface.rb +48 -0
  169. data/lib/hecks/facade.rb +44 -0
  170. data/lib/hecks/forms/app.rb +300 -0
  171. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  172. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  173. data/lib/hecks/forms/field_renderer.rb +170 -0
  174. data/lib/hecks/forms/field_shape.rb +232 -0
  175. data/lib/hecks/forms/html.rb +53 -0
  176. data/lib/hecks/forms/index_renderer.rb +35 -0
  177. data/lib/hecks/forms/page.rb +157 -0
  178. data/lib/hecks/forms/params.rb +132 -0
  179. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  180. data/lib/hecks/forms/record_renderer.rb +115 -0
  181. data/lib/hecks/forms/record_table.rb +63 -0
  182. data/lib/hecks/forms/reference_options.rb +30 -0
  183. data/lib/hecks/forms/value_object_shape.rb +46 -0
  184. data/lib/hecks/forms.rb +54 -0
  185. data/lib/hecks/fqn.rb +94 -0
  186. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  187. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  188. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  189. data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
  190. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  191. data/lib/hecks/framework/oidc.json +39 -0
  192. data/lib/hecks/framework.rb +90 -0
  193. data/lib/hecks/freezer.rb +67 -0
  194. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  195. data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
  196. data/lib/hecks/fuzzing/properties.rb +1146 -0
  197. data/lib/hecks/fuzzing/replay.rb +635 -0
  198. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  199. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
  200. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  201. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
  202. data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
  203. data/lib/hecks/fuzzing/value_generator.rb +177 -0
  204. data/lib/hecks/fuzzing.rb +12 -0
  205. data/lib/hecks/grammar/evolve.rb +291 -0
  206. data/lib/hecks/grammar/expression.bluebook +417 -0
  207. data/lib/hecks/grammar/expression_operators.json +1152 -0
  208. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  209. data/lib/hecks/grammar/oidc.json +69 -0
  210. data/lib/hecks/grammar/translation.bluebook +203 -0
  211. data/lib/hecks/grammar.rb +178 -0
  212. data/lib/hecks/ir.rb +126 -0
  213. data/lib/hecks/language/adapter.bluebook +116 -0
  214. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  215. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  216. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  217. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  218. data/lib/hecks/language/bluebook/command.bluebook +437 -0
  219. data/lib/hecks/language/bluebook/entity.bluebook +316 -0
  220. data/lib/hecks/language/bluebook/policy.bluebook +172 -0
  221. data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
  222. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  223. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  224. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  225. data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
  226. data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
  227. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  228. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  229. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  230. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  231. data/lib/hecks/language/oidc.json +313 -0
  232. data/lib/hecks/language/port.bluebook +92 -0
  233. data/lib/hecks/language/translation/translation.bluebook +110 -0
  234. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  235. data/lib/hecks/language/world/wiring.bluebook +62 -0
  236. data/lib/hecks/language/world/world.bluebook +84 -0
  237. data/lib/hecks/literal.rb +125 -0
  238. data/lib/hecks/naming.rb +121 -0
  239. data/lib/hecks/ports/access_control.port +4 -0
  240. data/lib/hecks/ports/access_control.rb +62 -0
  241. data/lib/hecks/ports/agent/answers.rb +104 -0
  242. data/lib/hecks/ports/agent.port +4 -0
  243. data/lib/hecks/ports/agent.rb +167 -0
  244. data/lib/hecks/ports/authentication.port +4 -0
  245. data/lib/hecks/ports/authentication.rb +50 -0
  246. data/lib/hecks/ports/authorization.port +4 -0
  247. data/lib/hecks/ports/authorization.rb +59 -0
  248. data/lib/hecks/ports/clock.port +4 -0
  249. data/lib/hecks/ports/clock.rb +62 -0
  250. data/lib/hecks/ports/extraction.port +4 -0
  251. data/lib/hecks/ports/extraction.rb +37 -0
  252. data/lib/hecks/ports/identity_assignment.port +4 -0
  253. data/lib/hecks/ports/identity_assignment.rb +45 -0
  254. data/lib/hecks/ports/identity_generation.port +4 -0
  255. data/lib/hecks/ports/identity_generation.rb +49 -0
  256. data/lib/hecks/ports/identity_resolution.port +4 -0
  257. data/lib/hecks/ports/identity_resolution.rb +40 -0
  258. data/lib/hecks/ports/loading.port +4 -0
  259. data/lib/hecks/ports/loading.rb +13 -0
  260. data/lib/hecks/ports/persistence/append_only.rb +102 -0
  261. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  262. data/lib/hecks/ports/persistence/execution.rb +18 -0
  263. data/lib/hecks/ports/persistence/lineage.rb +277 -0
  264. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  266. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  267. data/lib/hecks/ports/persistence.port +4 -0
  268. data/lib/hecks/ports/persistence.rb +39 -0
  269. data/lib/hecks/ports/projection.port +4 -0
  270. data/lib/hecks/ports/projection.rb +76 -0
  271. data/lib/hecks/ports/query/in_memory.rb +60 -0
  272. data/lib/hecks/ports/query/ordering.rb +41 -0
  273. data/lib/hecks/ports/query.rb +36 -0
  274. data/lib/hecks/ports.rb +27 -0
  275. data/lib/hecks/projections/diagrams.rb +413 -0
  276. data/lib/hecks/projections/ir.rb +18 -0
  277. data/lib/hecks/projections/model/deviations.rb +98 -0
  278. data/lib/hecks/projections/model.rb +145 -0
  279. data/lib/hecks/projections/oidc.rb +110 -0
  280. data/lib/hecks/projections/parser_table.rb +159 -0
  281. data/lib/hecks/projections/reference.rb +38 -0
  282. data/lib/hecks/projections/shape.rb +31 -0
  283. data/lib/hecks/projections/statements.rb +110 -0
  284. data/lib/hecks/projections/vocabulary.rb +100 -0
  285. data/lib/hecks/projections.rb +32 -0
  286. data/lib/hecks/projector/cli_projector.rb +437 -0
  287. data/lib/hecks/projector/docs_projector.rb +321 -0
  288. data/lib/hecks/projector/exporter.rb +141 -0
  289. data/lib/hecks/projector/ir_projector.rb +18 -0
  290. data/lib/hecks/projector/narrate_projector.rb +243 -0
  291. data/lib/hecks/projector/target.rb +97 -0
  292. data/lib/hecks/projector.rb +186 -0
  293. data/lib/hecks/query_ir.rb +392 -0
  294. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  295. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  296. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  297. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  298. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  299. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  300. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  301. data/lib/hecks/query_specification/common/null_policy.rb +80 -0
  302. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  303. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  304. data/lib/hecks/query_specification/common/options.rb +32 -0
  305. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  306. data/lib/hecks/query_specification/common/specification.rb +10 -0
  307. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  308. data/lib/hecks/query_specification/field_path.rb +89 -0
  309. data/lib/hecks/query_specification/hop_path.rb +130 -0
  310. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  311. data/lib/hecks/query_specification.rb +14 -0
  312. data/lib/hecks/rendering.rb +48 -0
  313. data/lib/hecks/router/namespace_installer.rb +157 -0
  314. data/lib/hecks/router.rb +70 -0
  315. data/lib/hecks/runtime/caller.rb +50 -0
  316. data/lib/hecks/runtime/capability_graph.rb +44 -0
  317. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
  318. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
  319. data/lib/hecks/runtime/command_interpreter.rb +413 -0
  320. data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
  321. data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
  322. data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
  323. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  324. data/lib/hecks/runtime/command_rules/references.rb +181 -0
  325. data/lib/hecks/runtime/command_rules.rb +28 -0
  326. data/lib/hecks/runtime/dependency_planning.rb +231 -0
  327. data/lib/hecks/runtime/dispatcher.rb +286 -0
  328. data/lib/hecks/runtime/entity_element.rb +253 -0
  329. data/lib/hecks/runtime/entity_interpreter.rb +228 -0
  330. data/lib/hecks/runtime/era_check.rb +136 -0
  331. data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
  332. data/lib/hecks/runtime/era_guard.rb +193 -0
  333. data/lib/hecks/runtime/era_tamper.rb +61 -0
  334. data/lib/hecks/runtime/errors.rb +100 -0
  335. data/lib/hecks/runtime/event.rb +51 -0
  336. data/lib/hecks/runtime/identity.rb +128 -0
  337. data/lib/hecks/runtime/instance.rb +120 -0
  338. data/lib/hecks/runtime/interpreting.rb +69 -0
  339. data/lib/hecks/runtime/loader.rb +143 -0
  340. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  341. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  342. data/lib/hecks/runtime/query_interpreter.rb +254 -0
  343. data/lib/hecks/runtime/reaction_invocation.rb +245 -0
  344. data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
  345. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  346. data/lib/hecks/runtime/reference_hop.rb +99 -0
  347. data/lib/hecks/runtime/refusal_wording.rb +124 -0
  348. data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
  349. data/lib/hecks/runtime/registry/verification.rb +158 -0
  350. data/lib/hecks/runtime/registry.rb +218 -0
  351. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  352. data/lib/hecks/runtime/routing.rb +96 -0
  353. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  354. data/lib/hecks/runtime/saga_interpreter.rb +301 -0
  355. data/lib/hecks/runtime/storage_shape.rb +120 -0
  356. data/lib/hecks/runtime/tenant_check.rb +84 -0
  357. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  358. data/lib/hecks/runtime/value/admission.rb +112 -0
  359. data/lib/hecks/runtime/value/coercion.rb +500 -0
  360. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  361. data/lib/hecks/runtime/value.rb +125 -0
  362. data/lib/hecks/runtime.rb +95 -0
  363. data/lib/hecks/translation/audit/approval_digest.rb +31 -0
  364. data/lib/hecks/translation/audit/layer_one.rb +37 -0
  365. data/lib/hecks/translation/audit/layer_two.rb +77 -0
  366. data/lib/hecks/translation/audit/unfed_report.rb +42 -0
  367. data/lib/hecks/translation/audit.rb +70 -0
  368. data/lib/hecks/translation/reattest.rb +72 -0
  369. data/lib/hecks/translation/rule_compiler.rb +120 -0
  370. data/lib/hecks/translation/scaffold/differ.rb +183 -0
  371. data/lib/hecks/translation/scaffold/renderer.rb +41 -0
  372. data/lib/hecks/translation/scaffold/writer.rb +28 -0
  373. data/lib/hecks/translation/scaffold.rb +30 -0
  374. data/lib/hecks/translation.rb +11 -0
  375. data/lib/hecks/version.rb +16 -0
  376. data/lib/hecks/vocabulary.rb +213 -0
  377. data/lib/hecks.rb +118 -6
  378. metadata +401 -222
  379. data/bin/hecks +0 -7
  380. data/bin/hecks-package +0 -65
  381. data/bin/hecks_console +0 -12
  382. data/bin/hecks_serverless +0 -6
  383. data/lib/cli/build.rb +0 -14
  384. data/lib/cli/command_runner.rb +0 -28
  385. data/lib/cli/console.rb +0 -10
  386. data/lib/cli/generate.rb +0 -37
  387. data/lib/cli/hecks-cli.rb +0 -27
  388. data/lib/cli/test.rb +0 -57
  389. data/lib/console/commands.rb +0 -8
  390. data/lib/console/hecks-console.rb +0 -1
  391. data/lib/packager/README.md +0 -0
  392. data/lib/packager/app_runner.rb +0 -21
  393. data/lib/packager/args.rb +0 -26
  394. data/lib/packager/compatibility/fixnum.rb +0 -6
  395. data/lib/packager/hecks.rb +0 -39
  396. data/lib/packager/query_runner.rb +0 -21
  397. data/lib/packager/resources/Dockerfile +0 -11
  398. data/lib/packager/resources/app_binary +0 -7
  399. data/lib/packager/resources/bundle_config +0 -3
  400. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  401. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  402. data/lib/serverless/Domain +0 -32
  403. data/lib/serverless/cli.rb +0 -75
  404. data/lib/serverless/resources/command_name.js +0 -5
  405. data/lib/serverless/resources/environment.js +0 -7
  406. data/lib/serverless/resources/handler.js.tt +0 -28
  407. data/lib/serverless/resources/run_binary.js +0 -22
  408. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,243 @@
1
+ require_relative "docs_projector"
2
+ require_relative "../forms/field_shape"
3
+
4
+ module Hecks
5
+ module Projector
6
+ # A BLUEBOOK, PROJECTED AS PROSE AN SME CAN READ BACK AND CONFIRM.
7
+ #
8
+ # WHAT THIS IS FOR. `DocsProjector` already answers "what can I call and
9
+ # what does it want" for the person implementing against a domain —
10
+ # tables of arguments, shapes, refusal reasons. That is the wrong
11
+ # register for the person who can actually say whether the domain is
12
+ # RIGHT: the subject-matter expert who knows what an account is and has
13
+ # never read a markdown table in their life. This projects the same IR
14
+ # as sentences instead — "Debit — take money out. Issued by a Teller. It
15
+ # only goes through if the balance covers it." — so a domain can be
16
+ # read back to the person who can validate it without them learning the
17
+ # DSL first.
18
+ #
19
+ # SAME SOURCE, SAME GUARANTEE `DocsProjector` gives: nothing here is
20
+ # invented. Every sentence quotes a `description`, `goal`, or `given`
21
+ # already declared in the chapter; where a chapter says nothing, this
22
+ # says nothing rather than manufacturing a sentence out of an
23
+ # identifier. Registered as `:narrate` beside `:docs`, same call shape
24
+ # (`Projector.call(:narrate, bluebook: ...)`), same aggregate-scoping
25
+ # via `options[:aggregate]`.
26
+ #
27
+ # WHAT IT DOES NOT DO: replace `DocsProjector`. A shape table still says
28
+ # "id of a Customer" more precisely than any sentence would, and an
29
+ # implementer still wants that. This is the other document the same IR
30
+ # is owed — one written for the reader who is being asked "is this
31
+ # right?", not "how do I call it?"
32
+ module NarrateProjector
33
+ module_function
34
+
35
+ # `options[:heading]` sets the top heading level, exactly as
36
+ # `DocsProjector` does — so this, too, can be spliced into a larger
37
+ # document rather than always starting at H1.
38
+ def call(bluebook:, options: {})
39
+ depth = (options[:heading] || 1).to_i
40
+ only = options[:aggregate]
41
+
42
+ sections = []
43
+ sections << chapter_intro(bluebook, depth) unless only
44
+ Array(DocsProjector.aggregates(bluebook, only)).each do |aggregate|
45
+ sections << aggregate_narrative(aggregate, only ? depth : depth + 1)
46
+ end
47
+ sections << reactions_narrative(bluebook, depth + 1) unless only
48
+ "#{sections.compact.join("\n\n").rstrip}\n"
49
+ end
50
+
51
+ # ── the chapter ───────────────────────────────────────────────────
52
+
53
+ def chapter_intro(bluebook, depth)
54
+ parts = [DocsProjector.h(depth, bluebook.name)]
55
+ parts << bluebook.vision if bluebook.vision
56
+
57
+ meta = []
58
+ meta << "This is a #{bluebook.classification} domain." if bluebook.classification
59
+ meta << "It was formerly known as `#{bluebook.formerly_known_as}`." if bluebook.formerly_known_as
60
+ parts << meta.join(" ") unless meta.empty?
61
+
62
+ names = bluebook.aggregates.map(&:hecks_name)
63
+ parts << "It's told through #{to_sentence_list(names)}." unless names.empty?
64
+ parts.join("\n\n")
65
+ end
66
+
67
+ # ── one aggregate ─────────────────────────────────────────────────
68
+
69
+ def aggregate_narrative(aggregate, depth)
70
+ parts = [DocsProjector.h(depth, aggregate.hecks_name)]
71
+ parts << aggregate.description if aggregate.description
72
+ parts << identity_sentence(aggregate)
73
+
74
+ refs = aggregate.attributes.select(&:reference?)
75
+ parts << "Each one is linked to #{to_sentence_list(refs.map { |r| "#{a_or_an(r.type.target_name)} #{r.type.target_name}" })}." unless refs.empty?
76
+
77
+ parts << lifecycle_narrative(aggregate)
78
+ parts << verbs_narrative(aggregate, depth + 1)
79
+ parts << queries_narrative(aggregate.queries, aggregate.hecks_name, depth + 1)
80
+
81
+ aggregate.entities.each { |entity| parts << entity_narrative(aggregate, entity, depth + 1) }
82
+ parts.compact.join("\n\n")
83
+ end
84
+
85
+ def identity_sentence(holder)
86
+ return nil if holder.identity_heads.empty?
87
+
88
+ "Every #{holder.hecks_name} is identified by its #{to_sentence_list(holder.identity_heads.map { |h| "`#{h}`" })}."
89
+ end
90
+
91
+ def entity_narrative(aggregate, entity, depth)
92
+ parts = [DocsProjector.h(depth, "#{entity.hecks_name} (within #{aggregate.hecks_name})")]
93
+ parts << entity.description if entity.description
94
+ parts << "Reached through its #{aggregate.hecks_name} — you address it by the #{aggregate.hecks_name}'s " \
95
+ "id together with its own `#{entity.identity_heads.join('`, `')}`."
96
+
97
+ parts << lifecycle_narrative(entity)
98
+ parts << verbs_narrative(entity, depth + 1)
99
+ parts << queries_narrative(entity.queries, entity.hecks_name, depth + 1)
100
+ parts.compact.join("\n\n")
101
+ end
102
+
103
+ # ── the machine ───────────────────────────────────────────────────
104
+
105
+ def lifecycle_narrative(holder)
106
+ lifecycle = holder.lifecycle or return nil
107
+
108
+ sentences = ["It carries a `#{lifecycle.field}`, starting out at `#{lifecycle.default}`."]
109
+ lifecycle.transitions.each do |name, transition|
110
+ froms = Array(transition.from).map { |f| "`#{f}`" }
111
+ sentences << "**#{name}** moves it from #{to_sentence_list(froms, conj: 'or')} to `#{transition.target}`."
112
+ end
113
+ sentences << "A verb not listed here can be issued from any state."
114
+ sentences.join(" ")
115
+ end
116
+
117
+ # ── the verbs ─────────────────────────────────────────────────────
118
+
119
+ def verbs_narrative(holder, depth)
120
+ return nil if holder.commands.empty?
121
+
122
+ header = DocsProjector.h(depth, "What can happen to #{a_or_an(holder.hecks_name)} #{holder.hecks_name}")
123
+ body = holder.commands.map { |command| command_paragraph(command, holder) }.join("\n\n")
124
+ "#{header}\n\n#{body}"
125
+ end
126
+
127
+ def command_paragraph(command, holder)
128
+ # THE GOAL, VERBATIM — same rule `DocsProjector` holds to: quoted
129
+ # exactly as declared, not recased to fit mid-sentence, because the
130
+ # promise this whole projector makes is that a sentence here is a
131
+ # sentence the chapter actually wrote.
132
+ sentences = ["**#{command.hecks_name}**#{command.goal ? " — #{command.goal}." : '.'}"]
133
+ sentences << "Issued by #{a_or_an(command.role)} #{command.role}." if command.role
134
+ # `acts_on.nil?`, NOT `creates?` — `creates?` answers true for every
135
+ # verb an ENTITY declares (it never references itself; see
136
+ # `Command#acts_on`'s own comment), so reading it directly here would
137
+ # tell an SME that `LedgerEntry.Amend` brings a new ledger entry into
138
+ # being, which is exactly backwards.
139
+ sentences << "This is how a new #{holder.hecks_name} comes into being." if command.acts_on.nil?
140
+
141
+ arguments = command.attributes.reject(&:reference?)
142
+ sentences << "It takes #{to_sentence_list(arguments.map { |a| Forms::Humanize.label(a.name.to_s).downcase })}." unless arguments.empty?
143
+
144
+ refs = command.attributes.select(&:reference?)
145
+ sentences << "It's aimed at one existing #{to_sentence_list(refs.map { |r| r.type.target_name })}, by id." unless refs.empty?
146
+
147
+ conditions = conditions_of(command, holder)
148
+ sentences << "It only goes through if #{conditions.join('; ')}." unless conditions.empty?
149
+
150
+ guarantees = command.ensures.map(&:description)
151
+ sentences << "When it succeeds: #{guarantees.join('; ')}." unless guarantees.empty?
152
+
153
+ sentences << "It records `#{command.emits.join('`, `')}` as a fact." unless command.emits.empty?
154
+
155
+ sentences.join(" ")
156
+ end
157
+
158
+ # EVERY REQUIRED CONDITION, STATED AS SOMETHING THAT MUST BE TRUE —
159
+ # the same three sources `DocsProjector#refusals_of` reads (the
160
+ # lifecycle edge, a reference's existence, and the command's own
161
+ # `given`s), but kept positive rather than phrased as a refusal
162
+ # reason. "Refused unless not X" is a sentence a reader has to
163
+ # invert in their head; "only goes through if X" is not.
164
+ def conditions_of(command, holder)
165
+ conditions = []
166
+
167
+ lifecycle = holder.lifecycle
168
+ if lifecycle
169
+ froms = lifecycle.transitions.filter_map { |name, transition|
170
+ Array(transition.from) if name.to_s == command.hecks_name
171
+ }.flatten.uniq
172
+ conditions << "its `#{lifecycle.field}` is currently #{to_sentence_list(froms.map { |f| "`#{f}`" }, conj: 'or')}" unless froms.empty?
173
+ end
174
+
175
+ conditions + command.givens.map(&:description)
176
+ end
177
+
178
+ # ── the reads ─────────────────────────────────────────────────────
179
+
180
+ def queries_narrative(queries, holder_name, depth)
181
+ return nil if queries.empty?
182
+
183
+ header = DocsProjector.h(depth, "Questions you can ask about #{a_or_an(holder_name)} #{holder_name}")
184
+ lines = queries.map do |query|
185
+ shape = query.to_h
186
+ takes = Array(shape[:attributes]).map { |a| Forms::Humanize.label(a[:name].to_s).downcase }
187
+ # `w[:value]` ALREADY WEARS ITS OWN QUOTES OR COLON — it is a
188
+ # `Literal.render`ed string (see lib/hecks/literal.rb), not a raw
189
+ # Ruby value, so wrapping it in `.inspect` here would quote an
190
+ # already-quoted string a second time.
191
+ filters = Array(shape[:wheres]).map { |w| "`#{w[:field]}` #{op_words(w[:op])} #{w[:value]}" }
192
+
193
+ sentence = "- **#{query.hecks_name}**"
194
+ sentence << " (given #{to_sentence_list(takes)})" unless takes.empty?
195
+ sentence << " — #{query.description}" if query.description
196
+ sentence << "." unless sentence.end_with?(".")
197
+ sentence << " Only where #{to_sentence_list(filters)}." unless filters.empty?
198
+ sentence
199
+ end
200
+ "#{header}\n\n#{lines.join("\n")}"
201
+ end
202
+
203
+ def op_words(op)
204
+ { eq: "is", lt: "under", lte: "at most", gt: "over", gte: "at least" }[op.to_s.to_sym] || op.to_s
205
+ end
206
+
207
+ # ── what happens on its own ───────────────────────────────────────
208
+
209
+ def reactions_narrative(bluebook, depth)
210
+ return nil if bluebook.policies.empty? && bluebook.process_managers.empty?
211
+
212
+ parts = [DocsProjector.h(depth, "Reactions")]
213
+ bluebook.policies.each do |policy|
214
+ elsewhere = policy.target_domain ? " in #{policy.target_domain}" : ""
215
+ parts << "Whenever `#{policy.on_event}` happens, `#{policy.trigger_command}` fires on its own#{elsewhere} — nobody has to ask for it."
216
+ end
217
+
218
+ bluebook.process_managers.each do |saga|
219
+ shape = saga.to_h
220
+ parts << "**#{shape[:name]}** is a saga: it starts when `#{shape[:starts_on]}` happens and ends when " \
221
+ "`#{shape[:ends_on]}` happens, with each run tracked by its `#{shape[:correlates_by]}`. Along the " \
222
+ "way it moves through #{Array(shape[:states]).map { |s| "`#{s}`" }.join(' → ')}."
223
+ end
224
+ parts.join("\n\n")
225
+ end
226
+
227
+ # ── small sentence carpentry ──────────────────────────────────────
228
+
229
+ def to_sentence_list(items, conj: "and")
230
+ case items.size
231
+ when 0 then ""
232
+ when 1 then items[0].to_s
233
+ when 2 then "#{items[0]} #{conj} #{items[1]}"
234
+ else "#{items[0..-2].join(', ')}, #{conj} #{items[-1]}"
235
+ end
236
+ end
237
+
238
+ def a_or_an(word)
239
+ %w[a e i o u].include?(word.to_s[0].to_s.downcase) ? "an" : "a"
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,97 @@
1
+ module Hecks
2
+ module Projector
3
+ # WHAT MAKES A MODULE A PROJECTION TARGET. `Projector.register` has
4
+ # always accepted anything answering `call(bluebook:, options:)` —
5
+ # this only removes the second step, so a target declares its own key
6
+ # beside its own implementation instead of being registered from
7
+ # somewhere else that has to be kept in sync with it.
8
+ #
9
+ # module Hecks::Projections::OIDC
10
+ # extend Hecks::Projector::Target
11
+ # projects_as :oidc
12
+ #
13
+ # module_function
14
+ #
15
+ # def call(bluebook:, options: {}) = { ... }
16
+ # end
17
+ #
18
+ # NAMED `Target`, NOT `Projection`, on purpose. "Projection" already
19
+ # means two other things in this codebase: `Ports::Projection` is
20
+ # read-model catch-up (events folded into view state), and `bin/project`
21
+ # forces that catch-up by hand. Neither has anything to do with
22
+ # "canonical IR in, external artifact out". A third meaning under the
23
+ # same word would make the two impossible to grep apart — and from a
24
+ # domain's point of view `project(X)` really does read as "project TO
25
+ # a target", so the narrower word is also the more accurate one.
26
+ module Target
27
+ # Registering at declaration time means `require`ing a target is
28
+ # the whole of installing it — there is no separate manifest that
29
+ # can silently disagree about which targets exist.
30
+ #
31
+ # `requires:` NAMES A CAPABILITY, not a shape word.
32
+ #
33
+ # This began as `from: :chapter` / `from: :any` — two hand-kept
34
+ # symbols, admitted by duck-typing on `.aggregates`, which is a
35
+ # guess at what "is a chapter" means. The capabilities were already
36
+ # real by then: `Hecks::IR` is the ability to emit IR (its own
37
+ # header says so), and `Behaviour::Chapter` is the ability to answer
38
+ # as a chapter. So a projection names the module it needs, and
39
+ # admission is a genuine check rather than a proxy for one.
40
+ #
41
+ # projects_as :ir, requires: Hecks::IR
42
+ # projects_as :vocabulary # chapter, the default
43
+ #
44
+ # It also composes: a projection needing two capabilities names
45
+ # both, instead of a third symbol being invented for the pair.
46
+ #
47
+ # THE FAIL-QUIET THIS CLOSES, unchanged in substance: every
48
+ # construct emits its own IR, so handing a projector an AGGREGATE
49
+ # instead of a chapter is the natural thing to try. `bluebook:` was
50
+ # only ever a PARAMETER NAME, never a contract. `:oidc` failed
51
+ # loudly (no `aggregates` method), but `:shape` returned
52
+ # `{"name" => "Order", "aggregates" => []}` — well-formed,
53
+ # confident, and wrong.
54
+ # `declares:` NAMES AN AGGREGATE THE CHAPTER MUST HAVE.
55
+ #
56
+ # A capability says what a construct can DO; this says what it must
57
+ # CARRY. `:vocabulary` needs a chapter declaring a Vocabulary
58
+ # aggregate, `:parser_table` one declaring Syntax — and both used
59
+ # to state that as a `raise` in their own body, which is a
60
+ # requirement written as behaviour instead of declared. Stated
61
+ # here, the registry refuses before the projection runs and the
62
+ # projection stops carrying a guard about its own admission.
63
+ # `emits:` SAYS WHAT KIND OF ARTIFACT COMES BACK.
64
+ #
65
+ # `:artifact` (the default) is one thing — a Hash, or a String.
66
+ # `:files` is a TREE: a Hash of relative path => contents, which is
67
+ # what a reference-page or codegen projection produces.
68
+ #
69
+ # Declared rather than sniffed, deliberately. Inferring a tree from
70
+ # an ordinary Hash is guesswork — `{"name" => "Pizzas"}` is
71
+ # indistinguishable from a one-file tree — so `write` asks what the
72
+ # projection said rather than inspecting what it returned.
73
+ def projects_as(key, requires: nil, declares: nil, emits: :artifact)
74
+ @projection_emits = emits
75
+ @projection_key = key.to_sym
76
+ @projection_declares = Array(declares)
77
+ @projection_requires = Array(requires)
78
+ Projector.register(@projection_key, self)
79
+ @projection_key
80
+ end
81
+
82
+ def projection_key = @projection_key
83
+
84
+ # Empty means "a chapter" — resolved HERE rather than as a default
85
+ # argument, because Behaviour::Chapter is not loaded yet when this
86
+ # file is.
87
+ def projection_declares = @projection_declares || []
88
+
89
+ def projection_emits = @projection_emits || :artifact
90
+
91
+ def projection_requires
92
+ req = @projection_requires
93
+ req.nil? || req.empty? ? [Bluebook::Behaviour::Chapter] : req
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,186 @@
1
+ module Hecks
2
+ # A named registry of "canonical IR in, external artifact out" tools —
3
+ # §30 of docs/HECKS_IMPLEMENTATION_PLAN.md. Before this existed, every
4
+ # such tool was its own thing with its own call shape: `Exporter`
5
+ # (registry-wide, consumed directly by bin/ir, bin/project_rust, and
6
+ # translation/audit's approval digest — untouched by this file, still
7
+ # exactly what those three read), and `RustProjection::Projector`
8
+ # (`rust/project.rb`, a whole separate Ruby program under a
9
+ # confusingly-identical module name) are the two that already exist.
10
+ # Neither is registered here — retrofitting either is real, separate
11
+ # work (Rust's generator is a whole second toolchain; a UL/OIDC
12
+ # projector doesn't exist yet at all) — but `:ir` is, as a genuine,
13
+ # working, golden-tested example of the shape every future projector
14
+ # (`:rust`, `:ul`, `:openid`, ...) is meant to follow.
15
+ #
16
+ # The unit is ONE bluebook's IR, not a whole booted registry — matching
17
+ # every real projection target (Rust/UL/OIDC all project one domain at
18
+ # a time), and deliberately narrower than `Exporter.call`'s own
19
+ # multi-domain shape.
20
+ # THREE KINDS OF "PROJECT", TOLD APART BY WHAT THEY NEED AS INPUT.
21
+ # Only the first belongs in this registry.
22
+ #
23
+ # A PROJECTION takes a chapter's DECLARATION and answers something
24
+ # that DESCRIBES the domain: its IR, its storage shape, an OIDC scope
25
+ # manifest, the parser's keyword table, the reference pages. Inert,
26
+ # derived, and runnable against any chapter that carries what it
27
+ # declares it needs. These are what `register` holds.
28
+ #
29
+ # AN EXPORT takes a declaration AND its BINDINGS and answers
30
+ # something that IS the domain, running elsewhere — rust/project.rb's
31
+ # generated crate, the WASM artifact, the SAM template
32
+ # bin/project_deploy renders. It needs the `.world`/`.hecksagon` a
33
+ # projection never looks at, because a running system has to know how
34
+ # it is wired. That is the whole reason bin/project_deploy cannot use
35
+ # this protocol: `call(bluebook:, options:)` has no channel for it.
36
+ #
37
+ # A STATE PROJECTION takes RECORDS — a domain after dispatch — and is
38
+ # a read-model question wearing the same word.
39
+ # `bin/expression_projection` is the one of these: its operators are
40
+ # not declared anywhere, they are what exists after
41
+ # `Grammar.expression` replays expression_operators.json's ledger of
42
+ # dispatches. Converting it into this registry would be a category
43
+ # error, however much its name suggests otherwise.
44
+ #
45
+ # ONE WORD, THREE OTHER MEANINGS — worth naming too, because grepping
46
+ # "projection" turns all of these up and none is the above:
47
+ #
48
+ # Ports::Projection read-model catch-up, events folded into state
49
+ # bin/project forces that catch-up by hand
50
+ # RustProjection rust/project.rb's own separate toolchain
51
+ module Projector
52
+ class UnknownProjector < StandardError; end
53
+ # A chapter-scoped projector was handed something that is not a chapter.
54
+ class WrongConstruct < StandardError; end
55
+
56
+ module_function
57
+
58
+ # `projector` needs only to answer `call(bluebook:, options:)` — a
59
+ # module, a class with a class method, or any object responding to
60
+ # `call` all work. Re-registering a name replaces it outright,
61
+ # deliberately unguarded: a spec re-registering a stub under the same
62
+ # name between examples is the ordinary case, not a footgun to fence
63
+ # against.
64
+ def register(name, projector)
65
+ registry[name.to_sym] = projector
66
+ end
67
+
68
+ # `bluebook:` is kept as the keyword because it is the shipped
69
+ # spelling and every existing caller uses it — but what it accepts is
70
+ # any construct that emits IR, and `admits!` is what decides whether
71
+ # THIS target can actually take the one handed over.
72
+ def call(name, bluebook:, options: {})
73
+ projector = registry.fetch(name.to_sym) {
74
+ raise UnknownProjector, "no projector registered for #{name.inspect} — registered: #{registered.sort.inspect}"
75
+ }
76
+ admits!(name, projector, bluebook)
77
+ projector.call(bluebook: bluebook, options: options)
78
+ end
79
+
80
+ # A projection names the CAPABILITIES it needs; this refuses a
81
+ # construct that lacks one, before the projector runs.
82
+ #
83
+ # ONE CHECK COVERS BOTH SHAPES. An ordinary construct INCLUDES its
84
+ # capabilities and a class-shaped one — Command, Entity, ValueObject
85
+ # — EXTENDS them, and `is_a?` consults the singleton chain, so it
86
+ # answers for an extended module as readily as an included one. This
87
+ # started as two checks on the assumption it would not; a spec
88
+ # asserting the assumption failed, which is the only reason the
89
+ # redundant half was noticed.
90
+ def admits!(name, projector, construct)
91
+ needed = projector.respond_to?(:projection_requires) ? projector.projection_requires : []
92
+ missing = needed.reject { |capability| capable?(construct, capability) }
93
+ unless missing.empty?
94
+ raise WrongConstruct,
95
+ "#{name.inspect} needs #{missing.map(&:name).join(' and ')}, and was handed " \
96
+ "#{construct.class} (#{construct.respond_to?(:hecks_name) ? construct.hecks_name : construct.inspect})."
97
+ end
98
+
99
+ declared = projector.respond_to?(:projection_declares) ? projector.projection_declares : []
100
+ absent = declared.reject { |named| construct.aggregate(named) }
101
+ return if absent.empty?
102
+
103
+ raise WrongConstruct,
104
+ "#{name.inspect} needs a chapter declaring #{absent.join(' and ')}; " \
105
+ "#{construct.name} declares no such aggregate."
106
+ end
107
+
108
+ def capable?(construct, capability) = construct.is_a?(capability)
109
+
110
+ # What KIND of artifact a registered target emits — asked of the
111
+ # projection rather than inferred from what it returned.
112
+ def emits_for(name)
113
+ projector = registry.fetch(name.to_sym) { return :artifact }
114
+ projector.respond_to?(:projection_emits) ? projector.projection_emits : :artifact
115
+ end
116
+
117
+ def registered?(name) = registry.key?(name.to_sym)
118
+ def registered = registry.keys
119
+
120
+ def registry
121
+ @registry ||= {}
122
+ end
123
+
124
+ # A target may be addressed by the constant that implements it
125
+ # (`Projections::OIDC`) or by the bare key it registered under
126
+ # (`:oidc`). Both resolve here, so the constant form is added
127
+ # surface rather than a replacement — every `Projector.call(:ir, ...)`
128
+ # written before this existed keeps working untouched.
129
+ def key_for(target)
130
+ return target.projection_key if target.respond_to?(:projection_key) && target.projection_key
131
+
132
+ target
133
+ end
134
+
135
+ # WRITING IS THE CALLER'S CHOICE, NOT THE PROJECTOR'S. A projector
136
+ # returns an artifact and never touches disk, which is what lets
137
+ # spec/projector_spec.rb compare `:ir`'s output against a golden
138
+ # fixture without a tmpdir. `out:` is the only thing that writes.
139
+ #
140
+ # `as:` comes from the projection's own `emits:` declaration rather
141
+ # than from inspecting the artifact. A Hash of path => contents and a
142
+ # Hash that simply happens to hold strings are the same object to
143
+ # Ruby; only the projection knows which it meant.
144
+ def write(artifact, out, as: :artifact)
145
+ return write_tree(artifact, out) if as == :files
146
+
147
+ File.write(out, artifact.is_a?(String) ? artifact : "#{JSON.pretty_generate(artifact)}\n")
148
+ out
149
+ end
150
+
151
+ # Answers the paths written, in the order given — so a caller can
152
+ # report what happened without re-deriving it from the tree.
153
+ def write_tree(files, directory)
154
+ require "fileutils"
155
+ files.map do |relative, contents|
156
+ path = File.join(directory, relative)
157
+ FileUtils.mkdir_p(File.dirname(path))
158
+ File.write(path, contents)
159
+ path
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ require "json"
166
+
167
+ require_relative "projector/exporter"
168
+ require_relative "projector/ir_projector"
169
+ require_relative "projector/target"
170
+ # Neither of these requires this file back — they need `Naming` and nothing
171
+ # else — so unlike a target they are safe to pull in from here.
172
+ require_relative "projector/docs_projector"
173
+ require_relative "projector/narrate_projector"
174
+ require_relative "projector/cli_projector"
175
+
176
+ Hecks::Projector.register(:ir, Hecks::Projector::IRProjector)
177
+ Hecks::Projector.register(:docs, Hecks::Projector::DocsProjector)
178
+ Hecks::Projector.register(:narrate, Hecks::Projector::NarrateProjector)
179
+ Hecks::Projector.register(:cli, Hecks::Projector::CliProjector)
180
+
181
+ # The TARGETS are required from lib/hecks.rb, immediately after this
182
+ # file — deliberately not from here. A target requires this file (it
183
+ # needs `Target` and the registry), so requiring them back from here
184
+ # would close a genuine `circular require considered harmful` loop. Ruby
185
+ # tolerates it; the warning is still right, and load order is the kind of
186
+ # thing that only breaks once someone requires a target on its own.