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,438 @@
1
+ require_relative "fuzzing/target_capabilities"
2
+
3
+ module Hecks
4
+ # **The corpus, discovered** — every place in this repo that holds a real
5
+ # domain, named once.
6
+ #
7
+ # One table here, rather than each consumer spelling out its own copy —
8
+ # spec/corpus_spec.rb, spec/model_check_spec.rb, bin/model_check,
9
+ # spec/parser_parity_spec.rb, bin/fuzz, Fuzzing::CombinationMiner all did,
10
+ # and the copies had already drifted: only the model checker saw
11
+ # `qa/bluebook`, only parser parity saw `spec/fixtures`, and nothing but
12
+ # bin/fuzz's own sweep ever saw `qa/stress_domains`. Each consumer names
13
+ # the `KINDS` it walks rather than re-deriving where those kinds live.
14
+ #
15
+ # Plain Dir/File only — bin/ scripts require this before (or without)
16
+ # booting anything.
17
+ module Corpus
18
+ ROOT = File.expand_path("../..", __dir__).freeze
19
+
20
+ Member = Struct.new(:stem, :kind, :path)
21
+
22
+ # **One domain per directory** — its bluebooks sit in `<dir>/bluebook/`
23
+ # or directly in `<dir>` (see `bluebook_files`). Stemmed by directory.
24
+ DIRECTORY_KINDS = {
25
+ example: "examples/*",
26
+ stress: "qa/stress_domains/*",
27
+ semantics: "spec/corpus/semantics/domains/*",
28
+ # A PACKAGE VENDORED VIA `uses_embryonaut_bluebook` — same real
29
+ # mechanism `uses_framework` is (`lib/hecks/embryonaut_bluebook.rb`'s
30
+ # own header: "same shape as Framework"), one level further out: not
31
+ # a chapter shipped inside THIS gem's own `lib/hecks/framework/
32
+ # bluebook/` (the `:framework` FILE_KIND, below), but a directory
33
+ # nested inside the CONSUMING example's own checkout
34
+ # (`<domain>/vendor/embryonaut_bluebooks/<name>/bluebook/`,
35
+ # EmbryonautBluebook.load!'s own resolution path) — hence its own
36
+ # DIRECTORY_KIND rather than reuse of `:framework`'s shape. First
37
+ # real member: docs/decisions/0058's own
38
+ # examples/embryonaut_vendoring_demo/vendor/embryonaut_bluebooks/widgets.
39
+ vendored: "examples/*/vendor/embryonaut_bluebooks/*"
40
+ }.freeze
41
+
42
+ # **One chapter per file**. Stemmed by the path below the glob's fixed
43
+ # prefix, so a nested fixture keeps its subdirectory (`eras/base`)
44
+ # and never collides with a same-named file elsewhere in the kind.
45
+ FILE_KINDS = {
46
+ grammar: "lib/hecks/grammar/*.bluebook",
47
+ framework: "lib/hecks/framework/bluebook/*.bluebook",
48
+ qa: "qa/bluebook/*.bluebook",
49
+ language: "lib/hecks/language/**/*.bluebook",
50
+ deploy: "lib/hecks/deploy/bluebook/*.bluebook",
51
+ tenancy: "lib/hecks/tenancy/bluebook/*.bluebook",
52
+ fixture: "spec/fixtures/**/*.bluebook"
53
+ }.freeze
54
+
55
+ KINDS = (DIRECTORY_KINDS.keys + FILE_KINDS.keys).freeze
56
+
57
+ # Where a bluebook the sweep does not boot goes instead. Not a filter:
58
+ # nothing leaves `sweepable_domains` without naming the check that owns
59
+ # it, and spec/corpus_accounting_spec.rb proves each destination exists
60
+ # and actually exercises what is routed to it.
61
+ #
62
+ # check: :named_in — `destination` names every routed file
63
+ # (`names: :each_file`) or the given text
64
+ # check: :gitignored — `destination` holds the ignore rule `names`,
65
+ # and nothing matching is committed
66
+ # check: :gap — no check exercises these yet. Listed so the
67
+ # gap is visible; the accounting spec keeps it
68
+ # pending and fails the moment one appears.
69
+ #
70
+ # Ordered — a bluebook belongs to the first route it matches, so a
71
+ # specific destination sits above the catch-all for its shape.
72
+ Route = Struct.new(:pattern, :check, :destination, :names, :why)
73
+
74
+ ROUTES = [
75
+ Route.new(%r{\Atmp/}, :gitignored, ".gitignore", "tmp/",
76
+ "scratch output — generated and mined candidate domains, fuzz failures — never committed"),
77
+ Route.new(%r{/\.aws-sam/}, :gitignored, ".gitignore", ".aws-sam/",
78
+ "SAM build output — a vendored copy of real sources, never committed"),
79
+ Route.new(%r{/data/eras/}, :gitignored, ".gitignore", "**/data/eras/",
80
+ "era snapshots a file adapter writes at runtime, never committed"),
81
+ Route.new(%r{\Arust/}, :named_in, "rust/parser/tests/gates.rs", :each_file,
82
+ "the Rust parser's own fixtures, each loaded by its gate tests"),
83
+ Route.new(%r{/translations/}, :named_in, "spec/translation/committed_edges_spec.rb", "bluebook/translations",
84
+ "translation edges, not chapters; each must load, chain era to era, and end at the storage " \
85
+ "shape its bluebook declares today"),
86
+ Route.new(%r{\Alib/hecks/forms/examples/}, :named_in, "spec/forms/app_spec.rb", :each_file,
87
+ "a Forms presentation config wearing the .bluebook extension"),
88
+ Route.new(%r{\Aspec/fixtures/eras/}, :named_in, "spec/runtime/storage_shape_spec.rb", "fixtures/eras",
89
+ "deliberately conflicting versions of one domain; each must classify as the verdict " \
90
+ "its filename declares (bump_* / same_*)"),
91
+ Route.new(%r{\Aspec/fixtures/model_check/}, :named_in, "spec/model_check_spec.rb", :each_file,
92
+ "domains broken on purpose; each must produce exactly the finding kinds it is built to trigger"),
93
+ # bin/fuzz sweeps it too, once Fuzzing::Replay coerces value-object
94
+ # args before recomputing givens — today its oracle reads raw args
95
+ # while dispatch coerces, so checkout_fixture's VO-reading given
96
+ # reads as wrongly admitted.
97
+ Route.new(%r{\Aspec/fixtures/rust_host/}, :named_in, "rust/host/src/web.rs", "checkout_fixture",
98
+ "the Rust host's checkout fixture, pinned by its web and /api tests")
99
+ ].freeze
100
+
101
+ module_function
102
+
103
+ # Lists corpus members of the given kinds (every kind, by default).
104
+ #
105
+ # @param kinds [Array<Symbol>] corpus kinds to include, from `KINDS`; every kind
106
+ # when empty
107
+ # @param root [String] repository root to search under
108
+ # @return [Array<Member>] matching members
109
+ # @raise [ArgumentError] if `kinds` names a kind not in `KINDS`
110
+ def members(*kinds, root: ROOT)
111
+ kinds = KINDS if kinds.empty?
112
+ kinds.flat_map do |kind|
113
+ if (glob = DIRECTORY_KINDS[kind])
114
+ Dir.glob(File.join(root, glob)).select { |path| File.directory?(path) }.sort
115
+ .map { |dir| Member.new(File.basename(dir), kind, dir) }
116
+ elsif (glob = FILE_KINDS[kind])
117
+ prefix = File.join(root, glob[/\A[^*]*/].chomp("/"))
118
+ Dir.glob(File.join(root, glob))
119
+ .map { |file| Member.new(file.delete_prefix("#{prefix}/").delete_suffix(".bluebook"), kind, file) }
120
+ else
121
+ raise ArgumentError, "unknown corpus kind #{kind.inspect} — known: #{KINDS.join(', ')}"
122
+ end
123
+ end
124
+ end
125
+
126
+ # What a boot loads for a member: a directory kind's bluebook
127
+ # directory, a file kind's own file. `nil` for a directory holding no
128
+ # bluebook at all.
129
+ #
130
+ # @param member [Member] the corpus member
131
+ # @return [String, nil] the path to boot, or nil when a directory member holds
132
+ # no bluebook
133
+ def source_of(member)
134
+ DIRECTORY_KINDS.key?(member.kind) ? bluebook_dir(member.path) : member.path
135
+ end
136
+
137
+ # Where a domain path keeps its bluebooks — `<domain>/bluebook/*.bluebook`
138
+ # (every example and stress domain), or the directory itself
139
+ # (`qa/bluebook`). `nil` when neither holds a bluebook.
140
+ #
141
+ # @param domain_path [String] path to a domain directory
142
+ # @return [Array<String>, nil] `.bluebook` file paths found, or nil when none
143
+ def bluebook_files(domain_path)
144
+ [File.join(domain_path, "bluebook"), domain_path].each do |dir|
145
+ files = Dir[File.join(dir, "*.bluebook")]
146
+ return files unless files.empty?
147
+ end
148
+ nil
149
+ end
150
+
151
+ # The directory holding `domain_path`'s bluebooks.
152
+ #
153
+ # @param domain_path [String] path to a domain directory
154
+ # @return [String, nil] directory of the first `.bluebook` file found, or nil
155
+ # when `domain_path` holds none
156
+ def bluebook_dir(domain_path)
157
+ files = bluebook_files(domain_path)
158
+ files && File.dirname(files.first)
159
+ end
160
+
161
+ # The route a repo-relative path takes instead of the sweep — the
162
+ # first one it matches — or `nil` when the sweep boots it.
163
+ #
164
+ # @param relative_path [String] a corpus member's path, relative to the repo root
165
+ # @return [Route, nil] the first matching route, or nil when the sweep boots it
166
+ def route_for(relative_path)
167
+ ROUTES.find { |route| route.pattern.match?(relative_path) }
168
+ end
169
+
170
+ # What bin/model_check and spec/model_check_spec.rb walk — every kind,
171
+ # less the language (examined as one judged chapter, not file by file)
172
+ # and deploy chapters (the SAM projector's own inputs), and less any
173
+ # member a route already sends to a destination of its own: the
174
+ # broken-on-purpose model_check fixtures must produce their findings
175
+ # there, so a clean-corpus gate here would be the wrong check for them.
176
+ MODEL_CHECK_KINDS = %i[example grammar framework vendored qa stress fixture].freeze
177
+
178
+ # Every corpus member `bin/model_check` and `spec/model_check_spec.rb` walk.
179
+ #
180
+ # @param root [String] repository root to search under
181
+ # @return [Array<Member>] members of `MODEL_CHECK_KINDS`, less any routed elsewhere
182
+ def model_check_members(root: ROOT)
183
+ members(*MODEL_CHECK_KINDS, root: root).reject { |member| route_for(member.path.delete_prefix("#{root}/")) }
184
+ end
185
+
186
+ # **The ledger sweeps itself** — its own chapter is a domain like any
187
+ # other, and the one rotation member that is neither an example nor a
188
+ # stress domain.
189
+ ROTATION_LEDGER = { "quality_control" => "qa/bluebook" }.freeze
190
+
191
+ # What the QA rotation is made of — every example and stress domain
192
+ # this repository owns, plus the ledger, as `reference => repo-relative
193
+ # path`: exactly the shape `Target.path` is stored in.
194
+ #
195
+ # Derived, because the hand-kept version silently went stale.
196
+ # `bin/qa_seed_targets` carried a literal list naming three of the
197
+ # thirteen stress domains; the other ten were authored, argued for in
198
+ # their own NOTES.md, several promoted by `bin/qa_generated_domains
199
+ # --promote` — and never swept once, because a `Target` row is what
200
+ # puts a domain in the rotation and nothing tied that list to the
201
+ # corpus. Promotion only ever printed the `target.identify` line for a
202
+ # human to run.
203
+ #
204
+ # @param root [String] repository root to search under
205
+ # @return [Hash{String => String}] each rotation member's stem/reference mapped
206
+ # to its repo-relative path
207
+ def rotation_targets(root: ROOT)
208
+ members(:example, :stress, root: root)
209
+ .to_h { |member| [member.stem, member.path.delete_prefix("#{root}/")] }
210
+ .merge(ROTATION_LEDGER)
211
+ end
212
+
213
+ # Every bootable domain in the project, not a hand-kept list — any
214
+ # directory holding a `.bluebook` no route sends elsewhere, a
215
+ # `bluebook/` folder standing for the domain directory around it.
216
+ #
217
+ # @param root [String] repository root to search under
218
+ # @return [Array<String>] absolute paths of every sweepable domain directory
219
+ def sweepable_domains(root = ROOT)
220
+ Dir.chdir(root) do
221
+ Dir.glob("**/*.bluebook")
222
+ .reject { |path| route_for(path) }
223
+ .map { |path| File.dirname(path) }
224
+ .map { |dir| File.basename(dir) == "bluebook" ? File.dirname(dir) : dir }
225
+ .uniq.sort
226
+ .map { |dir| File.join(root, dir) }
227
+ end
228
+ end
229
+
230
+ # The domain directory a member stands for, spelled the way
231
+ # `sweepable_domains` spells it: a `bluebook/` folder is its parent.
232
+ #
233
+ # @param member [Member] the corpus member
234
+ # @return [String] the member's owning domain directory path
235
+ def domain_dir_of(member)
236
+ dir = File.directory?(member.path) ? member.path : File.dirname(member.path)
237
+ File.basename(dir) == "bluebook" ? File.dirname(dir) : dir
238
+ end
239
+
240
+ # ── The Rust-facing corpus ─────────────────────────────────────────
241
+ #
242
+ # Every Rust-facing list (the fuzz bridge, the codegen drift check,
243
+ # rust coverage, codegen parity) reads `rust/Cargo.toml`'s `[features]`
244
+ # as its one source, rather than being typed out by hand per consumer
245
+ # and drifting from it — hand-typed, fuzzing once saw only 8 of 20
246
+ # features. The `rust/src/generated/` modules split into buckets, and
247
+ # spec/corpus_rust_spec.rb proves every Cargo feature and every
248
+ # generated module lands in exactly one of them:
249
+ #
250
+ # rust_domains an in-repo domain directory with a Cargo feature
251
+ # of its own: fuzzed, regenerated, coverage- and
252
+ # parity-checked
253
+ # framework chapters no feature and no merged.rs; written as a side
254
+ # effect of every `uses_framework` domain's regen
255
+ # RUST_ELSEWHERE a feature with no in-repo domain directory, and
256
+ # the check that owns it instead
257
+ GENERATED_DIR = "rust/src/generated".freeze
258
+ RUST_DOMAIN_KINDS = %i[example stress fixture].freeze
259
+
260
+ RustDomain = Struct.new(:feature, :dir, :kind)
261
+
262
+ # `check: :named_in` — `destination` must name `names`.
263
+ # `check: :external` — no in-repo domain directory may carry the name;
264
+ # the day one does, it belongs in rust_domains.
265
+ Elsewhere = Struct.new(:check, :destination, :names, :why)
266
+
267
+ RUST_ELSEWHERE = {
268
+ "meta" => Elsewhere.new(:named_in, "spec/codegen_parity_spec.rb", "bluebook_language",
269
+ "the self-hosted grammar (lib/hecks/language), not a domain directory — every " \
270
+ "bin/project_rust run rewrites it (so the drift check diffs it), codegen parity " \
271
+ "checks it as bluebook_language, and there is no directory to fuzz"),
272
+ "embryonaut" => Elsewhere.new(:external, "~/Projects/embryonautfoundersapp", "embryonaut",
273
+ "an external product's domain — its bluebook, regeneration and parity are owed " \
274
+ "by its own repo; here bin/rust_coverage checks only the committed snapshot")
275
+ }.freeze
276
+
277
+ # **Shrink-only**. A generated module `bin/rust_coverage` still reports a
278
+ # gap for. `bin/corpus --rust-coverage` requires each of these to
279
+ # still fail, so an entry that starts passing breaks the build until
280
+ # it is deleted here.
281
+ RUST_COVERAGE_PENDING = {}.freeze
282
+
283
+ # The Cargo `[features]` table's raw text.
284
+ #
285
+ # @param root [String] repository root to search under
286
+ # @return [String] the table's text, or `""` when `rust/Cargo.toml` has none
287
+ def cargo_features_table(root: ROOT)
288
+ File.read(File.join(root, "rust/Cargo.toml"))[Fuzzing::TargetCapabilities::FEATURES_TABLE] || ""
289
+ end
290
+
291
+ # Every feature name Cargo declares with no dependency array of its own.
292
+ #
293
+ # @param root [String] repository root to search under
294
+ # @return [Array<String>] feature names, in file order
295
+ def cargo_features(root: ROOT)
296
+ cargo_features_table(root: root).scan(/^(\w+)\s*=\s*\[\]/).flatten
297
+ end
298
+
299
+ # The feature `bin/project_rust` last wrote as Cargo's `default`.
300
+ #
301
+ # @param root [String] repository root to search under
302
+ # @return [String, nil] the default feature's name, or nil when Cargo.toml
303
+ # declares none
304
+ def cargo_default(root: ROOT)
305
+ cargo_features_table(root: root)[/^default\s*=\s*\["(\w+)"\]/, 1]
306
+ end
307
+
308
+ # Every in-repo domain directory whose name is a Cargo feature, sorted
309
+ # by path. When the module is already generated, its metadata.rs stamp
310
+ # decides which directory it came from — a directory name alone is not
311
+ # enough: spec/fixtures/qa_discover_external_domains vendors a second
312
+ # `examples/pizzas` that no Cargo feature was ever generated from.
313
+ #
314
+ # @param root [String] repository root to search under
315
+ # @return [Array<RustDomain>] each Rust-facing domain, sorted by directory path
316
+ def rust_domains(root: ROOT)
317
+ features = cargo_features(root: root)
318
+ members(*RUST_DOMAIN_KINDS, root: root)
319
+ .map { |member| [domain_dir_of(member), member.kind] }
320
+ .uniq(&:first)
321
+ .select { |dir, _| rust_domain_dir?(dir, features, root) }
322
+ .sort_by(&:first)
323
+ .map { |dir, kind| RustDomain.new(File.basename(dir).downcase, dir, kind) }
324
+ end
325
+
326
+ # Tells whether `dir` is the real source directory for its Cargo feature.
327
+ #
328
+ # @param dir [String] a candidate domain directory
329
+ # @param features [Array<String>] known Cargo feature names
330
+ # @param root [String] repository root `dir` is rooted under
331
+ # @return [Boolean]
332
+ def rust_domain_dir?(dir, features, root)
333
+ feature = File.basename(dir).downcase
334
+ source = generated_source(feature, root: root)
335
+ features.include?(feature) && (source.nil? || source == dir.delete_prefix("#{root}/"))
336
+ end
337
+
338
+ # Where a generated module came from, read off the stamp bin/project_rust
339
+ # writes into its metadata.rs — `examples/pizzas`, `/abs/path/embryonaut`,
340
+ # `the self-hosted language (lib/hecks/language/bluebook)`, with any
341
+ # ` (uses_framework "X")` or ` (uses_embryonaut_bluebook "X")` suffix
342
+ # dropped. `nil` when not generated.
343
+ SOURCE_STAMP = %r{
344
+ GENERATED\ by\ bin/project_rust\ —\ (.+?)
345
+ (?:\ \((?:uses_framework|uses_embryonaut_bluebook)\ "\w+"\))?
346
+ 's\ own\ canonical\ IR,
347
+ }x
348
+
349
+ # Where a generated module came from, read off the stamp `bin/project_rust`
350
+ # writes into its metadata.rs.
351
+ #
352
+ # @param module_name [String] a generated module's name
353
+ # @param root [String] repository root to search under
354
+ # @return [String, nil] the source path the stamp names, or nil when the module
355
+ # is not generated
356
+ def generated_source(module_name, root: ROOT)
357
+ metadata = File.join(root, GENERATED_DIR, module_name, "metadata.rs")
358
+ File.file?(metadata) ? File.read(metadata)[SOURCE_STAMP, 1] : nil
359
+ end
360
+
361
+ # Every module directory under `rust/src/generated`.
362
+ #
363
+ # @param root [String] repository root to search under
364
+ # @return [Array<String>] generated module names, sorted
365
+ def generated_modules(root: ROOT)
366
+ Dir.children(File.join(root, GENERATED_DIR)).select { |name| File.directory?(File.join(root, GENERATED_DIR, name)) }.sort
367
+ end
368
+
369
+ # Tells whether `feature` has already been generated (has a merged.rs).
370
+ #
371
+ # @param feature [String] a Cargo feature / domain name
372
+ # @param root [String] repository root to search under
373
+ # @return [Boolean]
374
+ def generated?(feature, root: ROOT)
375
+ File.file?(File.join(root, GENERATED_DIR, feature, "merged.rs"))
376
+ end
377
+
378
+ # Chapters attached via `kind` (`:framework` or `:vendored`) that
379
+ # `bin/project_rust` writes a module for as a side effect of the
380
+ # attaching domain — a module, no merged.rs of its own. Both kinds
381
+ # share this shape: `bin/project_rust`'s own generated module name is
382
+ # always `chapter_name.downcase` (rust/src/generated/mod.rs's own
383
+ # convention), and for either kind that downcased name is always the
384
+ # original lowercase directory stem again — confirmed live for a
385
+ # framework member and for a vendored package alike (a vendored
386
+ # package's own bluebook stem doesn't have to equal its declared
387
+ # chapter name the way a framework member's does — `EmbryonautBluebook.
388
+ # load!` resolves it as `Naming.pascal(dir_name)`, e.g. "widgets" ->
389
+ # "Widgets" — but downcasing a `Naming.pascal`-derived name always
390
+ # returns the original lowercase stem again).
391
+ #
392
+ # @param kind [Symbol] :framework or :vendored
393
+ # @param root [String] repository root to search under
394
+ # @return [Array<String>] stems with a generated module but no merged.rs
395
+ # of their own
396
+ def rust_side_chapters(kind, root: ROOT)
397
+ modules = generated_modules(root: root)
398
+ members(kind, root: root).map(&:stem)
399
+ .select { |stem| modules.include?(stem) && !generated?(stem, root: root) }
400
+ end
401
+
402
+ def rust_framework_chapters(root: ROOT)
403
+ rust_side_chapters(:framework, root: root)
404
+ end
405
+
406
+ def rust_vendored_chapters(root: ROOT)
407
+ rust_side_chapters(:vendored, root: root)
408
+ end
409
+
410
+ # THE REGENERATION ORDER the drift check runs. Sorted by path, so
411
+ # which domain runs last — and so wins Cargo's `default`, mod.rs's cfg
412
+ # comments and the shared framework modules' attribution stamp — is a
413
+ # fact of the sorted list, not a hand-picked order: today that is
414
+ # `has_many_fixture`. spec/corpus_rust_spec.rb pins last == default.
415
+ #
416
+ # @param root [String] repository root to search under
417
+ # @return [Array<RustDomain>] already-generated Rust domains, in regeneration order
418
+ def rust_regen_order(root: ROOT)
419
+ rust_domains(root: root).select { |domain| generated?(domain.feature, root: root) }
420
+ end
421
+
422
+ # The chapter a bluebook declares — `Hecks.bluebook "<Name>"`, read
423
+ # off the file rather than guessed from its name (a grammar chapter's
424
+ # file is named after its role, `aggregate.bluebook`, while its
425
+ # chapter is always "Bluebook"). Scans the whole file: a framework
426
+ # member's header comment can run past any fixed line cap.
427
+ #
428
+ # @param bluebook_path [String, Array<String>] a `.bluebook` file path, or an
429
+ # array whose first element is used
430
+ # @return [String, nil] the declared chapter name, or nil when the file never
431
+ # declares one
432
+ def chapter_name_of(bluebook_path)
433
+ bluebook_path = Array(bluebook_path).first
434
+ header = File.foreach(bluebook_path).find { |line| line =~ /\A\s*Hecks\.bluebook\s+"([^"]+)"/ }
435
+ header && Regexp.last_match(1)
436
+ end
437
+ end
438
+ end
@@ -1,4 +1,23 @@
1
1
  # The Deploy domain, bound so it boots standing alone.
2
2
  Hecks.hecksagon "Deploy" do
3
3
  persisted_by "Memory"
4
+
5
+ # THE IMPERATIVE HALF, BEHIND A REAL PORT — `bin/project_tenant`
6
+ # dispatches this `asks` once `Tenant.Declare` has already validated
7
+ # the request; `TenantProvisioner` (lib/hecks/adapters/driven/
8
+ # tenant_provisioner.rb) writes the overlay file and answers or
9
+ # refuses. `database`/`adapter`/`directory` are declared here, not on
10
+ # `Tenant` itself — CLI-only facts this validated request never
11
+ # needed to hold as its own state. `directory` is the real
12
+ # filesystem path being provisioned — NOT re-derivable from `Tenant`'s
13
+ # own `domain` (its declared NAME, e.g. "Scratch", never a path).
14
+ Deploy::Tenant.port "TenantProvisioning" do
15
+ asks "Provision", to: Tenant do
16
+ attribute :database, DatabaseName
17
+ attribute :adapter, AdapterName
18
+ attribute :directory, DirectoryPath
19
+ answers "TenantProvisioned"
20
+ refuses "ProvisioningRefused"
21
+ end
22
+ end
4
23
  end