hecks 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -0,0 +1,646 @@
1
+ require_relative "../projector"
2
+ require_relative "../runtime/refusal_wording"
3
+ require_relative "vocabulary"
4
+
5
+ module Hecks
6
+ module Projections
7
+ # The language's closed sets, projected into the Rust kernel — one
8
+ # `rust/src/kernel/vocab/<table>.rs` per Vocabulary table, each a
9
+ # `pub enum` plus `match`-based accessors with no wildcard arm, so a
10
+ # row the chapter gains is a compile error at every exhaustive match
11
+ # over it rather than a silent gap.
12
+ #
13
+ # Projector.call(:rust_vocabulary, bluebook: <the Bluebook chapter>)
14
+ # # => { "vocab/mod.rs" => "...", "vocab/refusal_template.rs" => "...", ... }
15
+ #
16
+ # Paths are relative to rust/src/kernel/; bin/project_rust_vocabulary
17
+ # writes them. This generalizes the old bin/project_refusal_wording,
18
+ # which read Runtime::RefusalWording::TEMPLATES — a hand-typed copy of
19
+ # the RefusalTemplate rows. This reads the chapter itself.
20
+ #
21
+ # ## Only `TABLES` the kernel needs
22
+ #
23
+ # Each entry in `TABLES` names its enum and its generated extras; a
24
+ # table not listed is not projected. The enum names that differ from
25
+ # the table name keep existing Rust call sites (`RefusalSite`) and the
26
+ # dispatch-step names D1 consumes (`AggregateStep`, `EntityStep`).
27
+ #
28
+ # ## Typed refusal arguments
29
+ #
30
+ # The templates table also reads RefusalSiteArgument: every site gets a
31
+ # `<Variant>Args` struct whose fields are exactly the site's declared
32
+ # arguments (a `&str` per scalar, a `&[&str]` per list) and a
33
+ # `render_args` that formats each one by its row. `render` itself stays
34
+ # private to the generated module, so no call site can pass an argument
35
+ # list by hand — leaving one out does not compile. The generated test
36
+ # pins every site's `render_args` output (empty, single and multiple
37
+ # unsorted lists) against Runtime::RefusalWording.render_with, computed
38
+ # here in Ruby.
39
+ module RustVocabulary
40
+ extend Projector::Target
41
+
42
+ projects_as :rust_vocabulary, declares: "Vocabulary", emits: :files
43
+
44
+ GENERATOR = "bin/project_rust_vocabulary".freeze
45
+ SOURCE = "lib/hecks/language/bluebook/vocabulary.bluebook".freeze
46
+
47
+ # table name => enum name, variant-naming fields, file stem, kind.
48
+ # `:order` tables also get `ORDER` and `position`; `:templates`
49
+ # gets `render`, the typed `<Variant>Args` (read off the `arguments`
50
+ # table) and the placeholder and argument-rendering tests.
51
+ TABLES = {
52
+ "RefusalTemplate" => { enum: "RefusalSite", variant_from: %w[refusal site], file: "refusal_template",
53
+ kind: :templates, arguments: "RefusalSiteArgument" },
54
+ "QueryComparator" => { enum: "QueryComparator", variant_from: %w[name], file: "query_comparator", kind: :set },
55
+ "FieldHint" => { enum: "FieldHint", variant_from: %w[name], file: "field_hint", kind: :set },
56
+ "AggregateDispatchOrder" => { enum: "AggregateStep", variant_from: %w[step], file: "aggregate_dispatch_order",
57
+ kind: :order },
58
+ "EntityDispatchOrder" => { enum: "EntityStep", variant_from: %w[step], file: "entity_dispatch_order",
59
+ kind: :order }
60
+ }.freeze
61
+
62
+ RUST_KEYWORDS = %w[as async await break const continue crate dyn else enum extern false fn for if impl in let loop
63
+ match mod move mut pub ref return self static struct super trait true type unsafe use where
64
+ while abstract become box do final macro override priv typeof unsized virtual yield try].freeze
65
+
66
+ module_function
67
+
68
+ # The projector protocol.
69
+ #
70
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter declaring the
71
+ # Vocabulary aggregate to project
72
+ # @param options [Hash] unused; accepted to satisfy the registry's call shape
73
+ # @return [Hash{String => String}] each generated file's path, relative to
74
+ # `rust/src/kernel/`, mapped to its full Rust source
75
+ def call(bluebook:, options: {}) = render(bluebook)
76
+
77
+ # Renders every file `TABLES` projects: `vocab/mod.rs` plus one `.rs` file
78
+ # per table, each an exhaustive enum with accessors and a pinned test module.
79
+ #
80
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter declaring the
81
+ # Vocabulary aggregate to render
82
+ # @return [Hash{String => String}] each generated file's path, relative to
83
+ # `rust/src/kernel/`, mapped to its full Rust source
84
+ # @raise [ArgumentError] if `bluebook` does not declare a `TABLES`-named table,
85
+ # or the `:arguments` table a `:templates` entry names
86
+ def render(bluebook)
87
+ tables = Vocabulary.tables(bluebook)
88
+ files = TABLES.to_h do |table, spec|
89
+ rows = tables.fetch(table) { raise ArgumentError, "Vocabulary declares no #{table} table" }
90
+ arguments = spec[:arguments] && tables.fetch(spec[:arguments]) do
91
+ raise ArgumentError, "Vocabulary declares no #{spec[:arguments]} table"
92
+ end
93
+ ["vocab/#{spec[:file]}.rs", table_file(table, spec, rows, arguments)]
94
+ end
95
+ { "vocab/mod.rs" => mod_file }.merge(files)
96
+ end
97
+
98
+ # The generated-file banner every projected file opens with.
99
+ #
100
+ # @param table [String] the Vocabulary table name being projected, such as
101
+ # `"RefusalTemplate"`
102
+ # @return [String] the banner, as Rust line comments
103
+ def header(table)
104
+ <<~RUST
105
+ // GENERATED by #{GENERATOR} (Hecks::Projections::RustVocabulary,
106
+ // lib/hecks/projections/rust_vocabulary.rb) from Vocabulary::#{table}
107
+ // (#{SOURCE}).
108
+ // Do not hand-edit — re-run #{GENERATOR} instead.
109
+ RUST
110
+ end
111
+
112
+ # Renders `vocab/mod.rs`, the entry point that re-exports every projected enum.
113
+ #
114
+ # @return [String] `vocab/mod.rs`'s full source: one `pub mod` and `pub use`
115
+ # per entry in `TABLES`
116
+ def mod_file
117
+ mods = TABLES.values.map { |spec| "pub mod #{spec[:file]};" }.join("\n")
118
+ exports = TABLES.values.map { |spec| "pub use #{spec[:file]}::#{spec[:enum]};" }.join("\n")
119
+ <<~RUST
120
+ // GENERATED by #{GENERATOR} (Hecks::Projections::RustVocabulary,
121
+ // lib/hecks/projections/rust_vocabulary.rb) from the Vocabulary
122
+ // aggregate (#{SOURCE}).
123
+ // Do not hand-edit — re-run #{GENERATOR} instead.
124
+ //
125
+ // One module per projected table. Every enum below is matched
126
+ // exhaustively by its own accessors — no wildcard arm — so a row the
127
+ // language gains fails to compile until it is handled.
128
+
129
+ #{mods}
130
+
131
+ #{exports}
132
+ RUST
133
+ end
134
+
135
+ # One table's full `.rs` source: its exhaustive enum, per-field accessors,
136
+ # `from_name`, its `:order`/`:templates` extras, and its `#[cfg(test)]` module.
137
+ #
138
+ # @param table [String] the Vocabulary table name, such as `"RefusalTemplate"`
139
+ # @param spec [Hash] the table's `TABLES` entry (`:enum`, `:variant_from`,
140
+ # `:file`, `:kind`, and, for `:templates`, `:arguments`)
141
+ # @param rows [Array<Hash{String => String}>] the table's rows, as read off
142
+ # the chapter by `Vocabulary.tables`
143
+ # @param argument_rows [Array<Hash{String => String}>, nil] the
144
+ # `RefusalSiteArgument` rows, only when `spec[:kind] == :templates`
145
+ # @return [String] the table's full `.rs` file source
146
+ # @raise [ArgumentError] if two rows would generate the same enum variant name
147
+ def table_file(table, spec, rows, argument_rows = nil)
148
+ variants = rows.map { |row| variant_name(row, spec[:variant_from]) }
149
+ duplicates = variants.tally.select { |_, count| count > 1 }.keys
150
+ raise ArgumentError, "duplicate #{spec[:enum]} variant(s): #{duplicates.join(', ')}" if duplicates.any?
151
+
152
+ enum = spec[:enum]
153
+ fields = rows.first.keys
154
+ by_site = spec[:kind] == :templates ? site_arguments(rows, argument_rows) : nil
155
+ body = [
156
+ header(table),
157
+ "#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]",
158
+ "pub enum #{enum} {",
159
+ variants.map { |variant| " #{variant}," },
160
+ "}",
161
+ "",
162
+ "impl #{enum} {",
163
+ constants(enum, variants, spec[:kind]),
164
+ fields.map { |field| accessor(enum, field, variants, rows) },
165
+ from_name(enum, spec[:kind]),
166
+ extras(enum, variants, spec[:kind]),
167
+ "}",
168
+ "",
169
+ by_site ? argument_types(enum, variants, rows, by_site) : [],
170
+ tests(enum, spec[:kind], variants, rows, by_site)
171
+ ]
172
+ "#{body.flatten.join("\n").rstrip}\n"
173
+ end
174
+
175
+ # Renders the enum's declared-order constant: `ORDER` for `:order`, `ALL` otherwise.
176
+ #
177
+ # @param enum [String] the Rust enum name, such as `"RefusalSite"`
178
+ # @param variants [Array<String>] each row's generated variant name, in
179
+ # declared order
180
+ # @param kind [Symbol] the table's `:kind` (`:order`, `:templates`, or `:set`)
181
+ # @return [Array<String>] the lines declaring the enum's `ORDER` constant
182
+ # (`:order`) or `ALL` constant (otherwise)
183
+ def constants(enum, variants, kind)
184
+ list = variants.map { |variant| " #{enum}::#{variant}," }
185
+ if kind == :order
186
+ [" /// The declared step order, first to last.",
187
+ " pub const ORDER: [#{enum}; #{variants.size}] = [", list, " ];", ""]
188
+ else
189
+ [" /// Every row, in declared order.",
190
+ " pub const ALL: &'static [#{enum}] = &[", list, " ];", ""]
191
+ end
192
+ end
193
+
194
+ # Renders one field's `pub fn` accessor, an exhaustive match over every variant.
195
+ #
196
+ # @param enum [String] the Rust enum name
197
+ # @param field [String] the row field this accessor reads, such as `"name"`
198
+ # @param variants [Array<String>] each row's generated variant name, in
199
+ # declared order, matching `rows`
200
+ # @param rows [Array<Hash{String => String}>] the table's rows, in the same
201
+ # order as `variants`
202
+ # @return [Array<String>] the lines for the field's `pub fn` accessor, one
203
+ # match arm per variant
204
+ def accessor(enum, field, variants, rows)
205
+ arms = variants.zip(rows).map { |variant, row| " #{enum}::#{variant} => #{rust_string(row.fetch(field))}," }
206
+ [" /// The row's declared `#{field}`.",
207
+ " pub fn #{accessor_name(field)}(&self) -> &'static str {",
208
+ " match self {",
209
+ arms,
210
+ " }",
211
+ " }",
212
+ ""]
213
+ end
214
+
215
+ # Built on the enum's own list and name accessor, not a string
216
+ # match, so it needs no wildcard arm either.
217
+ #
218
+ # @param enum [String] the Rust enum name
219
+ # @param kind [Symbol] the table's `:kind` (`:order`, `:templates`, or `:set`)
220
+ # @return [Array<String>] the lines for the enum's `from_name` associated function
221
+ def from_name(enum, kind)
222
+ list = kind == :order ? "ORDER" : "ALL"
223
+ field = kind == :templates ? "site" : name_field(kind)
224
+ [" /// The row whose `#{field}` is `name`, if any.",
225
+ " pub fn from_name(name: &str) -> Option<#{enum}> {",
226
+ " #{enum}::#{list}.iter().copied().find(|row| row.#{accessor_name(field)}() == name)",
227
+ " }",
228
+ ""]
229
+ end
230
+
231
+ # Renders the extra methods a table's `:kind` adds beyond the shared accessors.
232
+ #
233
+ # @param enum [String] the Rust enum name
234
+ # @param variants [Array<String>] each row's generated variant name, in
235
+ # declared order
236
+ # @param kind [Symbol] the table's `:kind` (`:order`, `:templates`, or `:set`)
237
+ # @return [Array<String>] `:order`'s `position` method, `:templates`'s private
238
+ # `render` method, or `[]` for `:set`
239
+ def extras(enum, variants, kind)
240
+ case kind
241
+ when :order
242
+ arms = variants.each_with_index.map { |variant, index| " #{enum}::#{variant} => #{index}," }
243
+ [" /// This step's index in `ORDER`.",
244
+ " pub fn position(&self) -> usize {",
245
+ " match self {",
246
+ arms,
247
+ " }",
248
+ " }"]
249
+ when :templates
250
+ [" /// `RefusalWording.substitute`: replace every `{key}` marker, in the",
251
+ " /// order given. A template is read, never evaluated. Private: call",
252
+ " /// sites go through a site's typed `<Variant>Args::render_args`,",
253
+ " /// which formats and supplies every declared argument.",
254
+ " fn render(&self, values: &[(&str, &str)]) -> String {",
255
+ " let mut text = self.template().to_string();",
256
+ " for (key, value) in values {",
257
+ " text = text.replace(&format!(\"{{{key}}}\"), value);",
258
+ " }",
259
+ " text",
260
+ " }"]
261
+ else
262
+ []
263
+ end
264
+ end
265
+
266
+ # [refusal, site] => that site's argument rows, in declared order —
267
+ # refused unless they name exactly the template's own placeholders,
268
+ # in the order each first appears, with only known formatting rules.
269
+ #
270
+ # @param rows [Array<Hash{String => String}>] the `RefusalTemplate` rows,
271
+ # in declared order
272
+ # @param argument_rows [Array<Hash{String => String}>] the `RefusalSiteArgument`
273
+ # rows for every site
274
+ # @return [Hash{Array<String> => Array<Hash{String => String}>}] each
275
+ # `[refusal, site]` pair mapped to its declared argument rows, in declared order
276
+ # @raise [ArgumentError] if an argument row names a `refusal`/`site` no
277
+ # `RefusalTemplate` row declares, if a site's declared arguments do not match
278
+ # its template's `{placeholder}`s in order, or if `check_rule!` rejects a
279
+ # row's `"shape"`, `"quoting"`, or `"sorted"` value
280
+ def site_arguments(rows, argument_rows)
281
+ grouped = argument_rows.group_by { |row| [row.fetch("refusal"), row.fetch("site")] }
282
+ orphans = grouped.keys - rows.map { |row| [row.fetch("refusal"), row.fetch("site")] }
283
+ raise ArgumentError, "RefusalSiteArgument rows name no RefusalTemplate: #{orphans.inspect}" if orphans.any?
284
+
285
+ rows.to_h do |row|
286
+ key = [row.fetch("refusal"), row.fetch("site")]
287
+ specs = grouped.fetch(key, [])
288
+ wants = row.fetch("template").scan(/\{(\w+)\}/).flatten.uniq
289
+ names = specs.map { |spec| spec.fetch("argument") }
290
+ unless names == wants
291
+ raise ArgumentError, "RefusalSiteArgument for #{key.join('/')} declares #{names.inspect}; " \
292
+ "its template's placeholders are #{wants.inspect}"
293
+ end
294
+ specs.each { |spec| check_rule!(key, spec) }
295
+ [key, specs]
296
+ end
297
+ end
298
+
299
+ # Refuses a `RefusalSiteArgument` row whose formatting fields are not
300
+ # recognized rules.
301
+ #
302
+ # @param key [Array<String>] the `[refusal, site]` pair `spec` belongs to,
303
+ # used in the raised message
304
+ # @param spec [Hash{String => String}] one `RefusalSiteArgument` row
305
+ # (`"shape"`, `"quoting"`, `"sorted"`)
306
+ # @return [void]
307
+ # @raise [ArgumentError] if `spec`'s `"shape"` is not in
308
+ # `Runtime::RefusalWording::SHAPES`, its `"quoting"` is not in
309
+ # `Runtime::RefusalWording::QUOTINGS`, or its `"sorted"` is not
310
+ # `"true"`/`"false"`
311
+ def check_rule!(key, spec)
312
+ where = "#{key.join('/')}.#{spec.fetch('argument')}"
313
+ unless Runtime::RefusalWording::SHAPES.include?(spec.fetch("shape"))
314
+ raise ArgumentError, "#{where}: unknown shape #{spec['shape'].inspect}"
315
+ end
316
+ unless Runtime::RefusalWording::QUOTINGS.include?(spec.fetch("quoting"))
317
+ raise ArgumentError, "#{where}: unknown quoting #{spec['quoting'].inspect}"
318
+ end
319
+ return if %w[true false].include?(spec.fetch("sorted"))
320
+
321
+ raise ArgumentError, "#{where}: sorted must be \"true\" or \"false\""
322
+ end
323
+
324
+ # Renders every `:templates` variant's typed argument struct, plus the two
325
+ # helper functions (`quoted`, `list`) they share.
326
+ #
327
+ # @param enum [String] the Rust enum name
328
+ # @param variants [Array<String>] each row's generated variant name, matching `rows`
329
+ # @param rows [Array<Hash{String => String}>] the `RefusalTemplate` rows, in
330
+ # the same order as `variants`
331
+ # @param by_site [Hash{Array<String> => Array<Hash{String => String}>}] each
332
+ # `[refusal, site]` pair's declared argument rows, as built by `site_arguments`
333
+ # @return [Array<String>] the shared `quoted`/`list` helper functions, followed
334
+ # by every variant's `<Variant>Args` struct and `render_args` implementation
335
+ def argument_types(enum, variants, rows, by_site)
336
+ support = [
337
+ "/// Ruby's `#inspect` of a name, as a refusal quotes it: `{:?}` on a",
338
+ "/// `&str`, the quoting every kernel call site used before",
339
+ "/// RefusalSiteArgument existed.",
340
+ "fn quoted(text: &str) -> String {",
341
+ " format!(\"{text:?}\")",
342
+ "}",
343
+ "",
344
+ "/// A list argument, written the way its RefusalSiteArgument row says:",
345
+ "/// sorted first (before quoting), each item quoted, then joined; an",
346
+ "/// empty list reads `when_empty`.",
347
+ "fn list(items: &[&str], sorted: bool, inspect: bool, separator: &str, when_empty: &str) -> String {",
348
+ " let mut items = items.to_vec();",
349
+ " if sorted {",
350
+ " items.sort_unstable();",
351
+ " }",
352
+ " if items.is_empty() {",
353
+ " return when_empty.to_string();",
354
+ " }",
355
+ " let write = |item: &&str| if inspect { quoted(item) } else { item.to_string() };",
356
+ " items.iter().map(write).collect::<Vec<_>>().join(separator)",
357
+ "}",
358
+ ""
359
+ ]
360
+ support + variants.zip(rows).flat_map do |variant, row|
361
+ args_struct(enum, variant, row, by_site.fetch([row.fetch("refusal"), row.fetch("site")]))
362
+ end
363
+ end
364
+
365
+ # Renders one variant's `<Variant>Args` struct and its `render_args` method.
366
+ #
367
+ # @param enum [String] the Rust enum name
368
+ # @param variant [String] the row's generated variant name
369
+ # @param row [Hash{String => String}] the `RefusalTemplate` row this variant projects
370
+ # @param specs [Array<Hash{String => String}>] the variant's declared
371
+ # `RefusalSiteArgument` rows, in declared order
372
+ # @return [Array<String>] the lines for the variant's `<Variant>Args` struct
373
+ # and its `render_args` method
374
+ def args_struct(enum, variant, row, specs)
375
+ fields = specs.flat_map do |spec|
376
+ type = spec.fetch("shape") == "list" ? "&'a [&'a str]" : "&'a str"
377
+ [" /// #{rule_reading(spec)}", " pub #{rust_field(spec.fetch('argument'))}: #{type},"]
378
+ end
379
+ locals = specs.filter_map { |spec| formatted_local(spec) }
380
+ pairs = specs.map do |spec|
381
+ name = spec.fetch("argument")
382
+ value = formatted_local(spec) ? "#{local_name(name)}.as_str()" : "self.#{rust_field(name)}"
383
+ " (#{rust_string(name)}, #{value}),"
384
+ end
385
+ ["/// `#{enum}::#{variant}`'s arguments — `RefusalWording.render_site(" \
386
+ "#{rust_string(row.fetch('refusal'))}, #{rust_string(row.fetch('site'))}, ...)`.",
387
+ "#[derive(Debug, Clone, Copy)]",
388
+ "pub struct #{variant}Args<'a> {",
389
+ fields,
390
+ "}",
391
+ "",
392
+ "impl #{variant}Args<'_> {",
393
+ " /// The site's wording, every argument formatted by its declared row.",
394
+ " pub fn render_args(&self) -> String {",
395
+ locals.map { |line| " #{line}" },
396
+ " #{enum}::#{variant}.render(&[",
397
+ pairs,
398
+ " ])",
399
+ " }",
400
+ "}",
401
+ ""]
402
+ end
403
+
404
+ # The `let` binding, if any, `args_struct` inserts before a variant's
405
+ # `render_args` computes its argument pairs.
406
+ #
407
+ # @param spec [Hash{String => String}] one `RefusalSiteArgument` row
408
+ # (`"argument"`, `"shape"`, `"quoting"`, and, for a list, `"sorted"`,
409
+ # `"separator"`, `"when_empty"`)
410
+ # @return [String, nil] the `let` line formatting the argument through
411
+ # `list`/`quoted`, or `nil` if the argument needs no formatting local
412
+ # (a scalar written as-is)
413
+ def formatted_local(spec)
414
+ name = spec.fetch("argument")
415
+ field = "self.#{rust_field(name)}"
416
+ if spec.fetch("shape") == "list"
417
+ "let #{local_name(name)} = list(#{field}, #{spec.fetch('sorted')}, #{spec.fetch('quoting') == 'inspect'}, " \
418
+ "#{rust_string(spec.fetch('separator'))}, #{rust_string(spec.fetch('when_empty'))});"
419
+ elsif spec.fetch("quoting") == "inspect"
420
+ "let #{local_name(name)} = quoted(#{field});"
421
+ end
422
+ end
423
+
424
+ # Describes one argument's formatting rule in English, for its struct field's doc comment.
425
+ #
426
+ # @param spec [Hash{String => String}] one `RefusalSiteArgument` row
427
+ # (`"shape"`, `"quoting"`, and, for a list, `"sorted"`, `"separator"`,
428
+ # `"when_empty"`)
429
+ # @return [String] a short English reading of the row's formatting rule,
430
+ # used as the struct field's own doc comment
431
+ def rule_reading(spec)
432
+ quoting = spec.fetch("quoting") == "inspect" ? ", quoted" : ""
433
+ return "scalar#{quoting}" unless spec.fetch("shape") == "list"
434
+
435
+ sorted = spec.fetch("sorted") == "true" ? ", sorted" : ""
436
+ "list#{sorted}#{quoting}, joined #{spec.fetch('separator').inspect}, empty reads #{spec.fetch('when_empty').inspect}"
437
+ end
438
+
439
+ # The local variable name `formatted_local` binds an argument's formatted value to.
440
+ #
441
+ # @param argument [String] the `RefusalSiteArgument` row's `"argument"` name
442
+ # @return [String] the Rust local variable name `formatted_local` binds the
443
+ # formatted value to
444
+ def local_name(argument) = "#{argument}_text"
445
+
446
+ # An argument name, escaped for use as a Rust field/parameter identifier.
447
+ #
448
+ # @param argument [String] the `RefusalSiteArgument` row's `"argument"` name
449
+ # @return [String] `argument`, prefixed with `r#` if it collides with a Rust
450
+ # keyword in `RUST_KEYWORDS`
451
+ def rust_field(argument) = RUST_KEYWORDS.include?(argument) ? "r##{argument}" : argument
452
+
453
+ # Renders the table's `#[cfg(test)]` module: every kind's `from_name` round-trip
454
+ # test, plus `:order`'s and `:templates`' own tests.
455
+ #
456
+ # @param enum [String] the Rust enum name
457
+ # @param kind [Symbol] the table's `:kind` (`:order`, `:templates`, or `:set`)
458
+ # @param variants [Array<String>] each row's generated variant name, only
459
+ # read when `kind == :templates`
460
+ # @param rows [Array<Hash{String => String}>] the table's rows, only read
461
+ # when `kind == :templates`
462
+ # @param by_site [Hash{Array<String> => Array<Hash{String => String}>}, nil]
463
+ # each `[refusal, site]` pair's declared argument rows, only read when
464
+ # `kind == :templates`
465
+ # @return [Array<String>] the `#[cfg(test)]` module's full source lines
466
+ def tests(enum, kind, variants = [], rows = [], by_site = nil)
467
+ list = kind == :order ? "ORDER" : "ALL"
468
+ name = accessor_name(kind == :templates ? "site" : name_field(kind))
469
+ lines = ["#[cfg(test)]",
470
+ "mod tests {",
471
+ " use super::*;",
472
+ ""]
473
+ unless kind == :templates
474
+ lines += [" #[test]",
475
+ " fn every_row_is_found_by_its_own_name() {",
476
+ " for row in #{enum}::#{list}.iter() {",
477
+ " assert_eq!(#{enum}::from_name(row.#{name}()), Some(*row));",
478
+ " }",
479
+ " }",
480
+ ""]
481
+ end
482
+ lines += order_test(enum) if kind == :order
483
+ if kind == :templates
484
+ lines += placeholder_test(enum)
485
+ lines += [""] + render_args_test(variants, rows, by_site)
486
+ end
487
+ lines.pop while lines.last == ""
488
+ lines + ["}"]
489
+ end
490
+
491
+ # The `:order` kind's own test, checked into every order table's test module.
492
+ #
493
+ # @param enum [String] the Rust enum name
494
+ # @return [Array<String>] the test asserting `ORDER`'s index matches each
495
+ # variant's `position`
496
+ def order_test(enum)
497
+ [" #[test]",
498
+ " fn position_is_the_index_in_order() {",
499
+ " for (index, step) in #{enum}::ORDER.iter().enumerate() {",
500
+ " assert_eq!(step.position(), index);",
501
+ " }",
502
+ " }",
503
+ ""]
504
+ end
505
+
506
+ # Placeholders are read off each template's own text, never a second
507
+ # hand-kept list.
508
+ #
509
+ # @param enum [String] the Rust enum name
510
+ # @return [Array<String>] the test asserting every site's `render` leaves
511
+ # no `{...}` placeholder unfilled
512
+ def placeholder_test(enum)
513
+ [" #[test]",
514
+ " fn every_site_renders_with_no_leftover_placeholder() {",
515
+ " for site in #{enum}::ALL {",
516
+ " let template = site.template();",
517
+ " let mut keys: Vec<String> = Vec::new();",
518
+ " let mut current: Option<String> = None;",
519
+ " for ch in template.chars() {",
520
+ " match (ch, current.as_mut()) {",
521
+ " ('{', _) => current = Some(String::new()),",
522
+ " ('}', Some(key)) => {",
523
+ " keys.push(key.clone());",
524
+ " current = None;",
525
+ " }",
526
+ " (_, Some(key)) => key.push(ch),",
527
+ " (_, None) => {}",
528
+ " }",
529
+ " }",
530
+ " let dummies: Vec<(String, String)> = keys.iter().map(|k| (k.clone(), format!(\"<{k}>\"))).collect();",
531
+ " let pairs: Vec<(&str, &str)> = dummies.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();",
532
+ " let rendered = site.render(&pairs);",
533
+ " assert!(",
534
+ " !rendered.contains('{') && !rendered.contains('}'),",
535
+ " \"{site:?} left an unrendered placeholder behind: {rendered:?}\"",
536
+ " );",
537
+ " }",
538
+ " }"]
539
+ end
540
+
541
+ # The Ruby oracle, pinned. Every site renders through its typed
542
+ # `render_args` for each edge case a list argument has — empty, one
543
+ # item, several out of order — and must equal what
544
+ # Runtime::RefusalWording.render_with answers for the same values,
545
+ # computed here at generation time. Scalars carry a `"` so quoting
546
+ # is compared too.
547
+ #
548
+ # @param variants [Array<String>] each row's generated variant name, matching `rows`
549
+ # @param rows [Array<Hash{String => String}>] the `RefusalTemplate` rows, in
550
+ # the same order as `variants`
551
+ # @param by_site [Hash{Array<String> => Array<Hash{String => String}>}] each
552
+ # `[refusal, site]` pair's declared argument rows, as built by `site_arguments`
553
+ # @return [Array<String>] the test asserting every variant's `render_args`
554
+ # output for each `argument_cases` case matches
555
+ # `Runtime::RefusalWording.render_with`, computed here in Ruby
556
+ def render_args_test(variants, rows, by_site)
557
+ asserts = variants.zip(rows).flat_map do |variant, row|
558
+ specs = by_site.fetch([row.fetch("refusal"), row.fetch("site")])
559
+ argument_cases(specs).map do |arguments|
560
+ expected = Runtime::RefusalWording.render_with(row.fetch("template"), specs, arguments)
561
+ fields = specs.map do |spec|
562
+ value = arguments.fetch(spec.fetch("argument").to_sym)
563
+ rust_value = value.is_a?(Array) ? "&[#{value.map { |item| rust_string(item) }.join(', ')}]" : rust_string(value)
564
+ "#{rust_field(spec.fetch('argument'))}: #{rust_value}"
565
+ end
566
+ [" assert_eq!(",
567
+ " #{variant}Args { #{fields.join(', ')} }.render_args(),",
568
+ " #{rust_string(expected)}",
569
+ " );"]
570
+ end
571
+ end
572
+ [" #[test]",
573
+ " fn render_args_matches_ruby_render_site() {",
574
+ asserts,
575
+ " }"].flatten
576
+ end
577
+
578
+ LIST_CASES = [[], ["only \"one\""], %w[zeta alpha mid]].freeze
579
+
580
+ # Builds the argument-value cases `render_args_test` checks a site's
581
+ # `render_args` against.
582
+ #
583
+ # @param specs [Array<Hash{String => String}>] one site's declared
584
+ # `RefusalSiteArgument` rows
585
+ # @return [Array<Hash{Symbol => Object}>] one argument Hash per test case —
586
+ # `LIST_CASES` (empty, one item, several unsorted) if any spec is
587
+ # list-shaped, else a single scalar case, each scalar value carrying a `"`
588
+ def argument_cases(specs)
589
+ cases = specs.any? { |spec| spec.fetch("shape") == "list" } ? LIST_CASES : [nil]
590
+ cases.map do |items|
591
+ specs.to_h do |spec|
592
+ name = spec.fetch("argument")
593
+ [name.to_sym, spec.fetch("shape") == "list" ? items : "#{name} \"x\""]
594
+ end
595
+ end
596
+ end
597
+
598
+ # The row field a table's `from_name`/round-trip test treats as its "name".
599
+ #
600
+ # @param kind [Symbol] the table's `:kind` (`:order`, `:templates`, or `:set`)
601
+ # @return [String] the row field used as this kind's "name" — `"step"` for
602
+ # `:order`, `"name"` otherwise
603
+ def name_field(kind) = kind == :order ? "step" : "name"
604
+
605
+ # `refusal` is a Rust-safe word but reads ambiguously beside the
606
+ # kernel's own `Refusal` enum; the accessor says what it answers.
607
+ #
608
+ # @param field [String] a row field name, such as `"name"` or `"refusal"`
609
+ # @return [String] the Rust accessor method name for `field` —
610
+ # `"refusal_class"` for `"refusal"`, `field` otherwise
611
+ def accessor_name(field) = field == "refusal" ? "refusal_class" : field
612
+
613
+ # Builds one row's Rust enum variant name from its declared `variant_from` fields.
614
+ #
615
+ # @param row [Hash{String => String}] the table row to name
616
+ # @param fields [Array<String>] the row fields, PascalCased and concatenated
617
+ # in order to build the variant name
618
+ # @return [String] the row's generated Rust enum variant name
619
+ # @raise [ArgumentError] if the built name is not a valid Rust variant
620
+ # identifier (uppercase first letter, alphanumeric only)
621
+ def variant_name(row, fields)
622
+ name = fields.map { |field| pascal(row.fetch(field)) }.join
623
+ raise ArgumentError, "#{name.inspect} is not a Rust variant name" unless name.match?(/\A[A-Z][A-Za-z0-9]*\z/)
624
+
625
+ name
626
+ end
627
+
628
+ # PascalCases one field value for use inside a generated variant name.
629
+ #
630
+ # @param text [String, Symbol, nil] the value to PascalCase; each
631
+ # `_`-separated part's first letter is upcased
632
+ # @return [String] `text` converted to PascalCase, or `""` if `text` is `nil`
633
+ def pascal(text) = text.to_s.split("_").map { |part| part[0].to_s.upcase + part[1..].to_s }.join
634
+
635
+ # A Rust string literal: only `\`, `"` and newlines need escaping in
636
+ # the text any vocabulary row holds.
637
+ #
638
+ # @param text [String] the value to render as a Rust string literal
639
+ # @return [String] `text`, backslash/quote/newline-escaped and wrapped in `"..."`
640
+ def rust_string(text)
641
+ escaped = text.to_s.gsub("\\") { "\\\\" }.gsub('"') { "\\\"" }.gsub("\n") { "\\n" }
642
+ "\"#{escaped}\""
643
+ end
644
+ end
645
+ end
646
+ end
@@ -3,11 +3,11 @@ require_relative "../projector"
3
3
 
4
4
  module Hecks
5
5
  module Projections
6
- # THE STORAGE SHAPE of one bluebook — the same structural form
6
+ # The storage shape of one bluebook — the same structural form
7
7
  # `StorageShape.mint_hash` hashes to name an era, which is what makes
8
8
  # a bump/no-bump question answerable by diffing two of these.
9
9
  #
10
- # THE RETROFIT, and the point of it: `Runtime::StorageShape.project`
10
+ # The retrofit, and the point of it: `Runtime::StorageShape.project`
11
11
  # was written long before this framework existed, and it already
12
12
  # takes exactly one bluebook, already returns a plain Hash, already
13
13
  # touches no disk and no live runtime — it even already uses the
@@ -31,6 +31,14 @@ module Hecks
31
31
  # is "every file here registers," not "every projection always
32
32
  # succeeds") but refuses clearly if asked to run with the plugin
33
33
  # unloaded, rather than raising on an undefined constant.
34
+ # Projects the storage shape of `bluebook` — the structural form an era
35
+ # bump/no-bump question is answered by diffing.
36
+ #
37
+ # @param bluebook [Bluebook::Chapter] the bluebook to reduce to its storage shape
38
+ # @param options [Hash] unused; accepted to satisfy the registry's call shape
39
+ # @return [Hash{String => Object}] `Runtime::StorageShape.project`'s Hash: `"name"`
40
+ # and a sorted `"aggregates"` array
41
+ # @raise [RuntimeError] if the era persistence plugin is not loaded
34
42
  def call(bluebook:, options: {})
35
43
  unless Ports::Persistence.plugin?(:era)
36
44
  raise "the :shape projection needs the era persistence plugin loaded " \