hecks 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +403 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +148 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +131 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +391 -0
  31. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
  32. data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
  33. data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
  34. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
  35. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
  36. data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
  37. data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
  38. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
  39. data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
  40. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
  41. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
  42. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
  45. data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
  46. data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
  47. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  48. data/lib/hecks/adapters/driven/prism.rb +80 -0
  49. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  50. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  51. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  52. data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
  53. data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
  54. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
  55. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  56. data/lib/hecks/adapters/driven/sqlite.rb +285 -0
  57. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  58. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  59. data/lib/hecks/adapters/driven.rb +36 -0
  60. data/lib/hecks/adapters.rb +6 -0
  61. data/lib/hecks/behaviors/dsl.rb +96 -0
  62. data/lib/hecks/behaviors/expectations.rb +280 -0
  63. data/lib/hecks/behaviors/ir.rb +31 -0
  64. data/lib/hecks/behaviors/rspec.rb +42 -0
  65. data/lib/hecks/behaviors/runner.rb +96 -0
  66. data/lib/hecks/behaviors.rb +25 -0
  67. data/lib/hecks/bluebook/aggregate.rb +108 -0
  68. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  69. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  70. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  71. data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
  72. data/lib/hecks/bluebook/assembly/marks.rb +227 -0
  73. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  74. data/lib/hecks/bluebook/assembly.rb +78 -0
  75. data/lib/hecks/bluebook/attribute.rb +96 -0
  76. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  77. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  78. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  79. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  80. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  81. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  82. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  83. data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
  84. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  85. data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
  86. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  87. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  88. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  89. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  90. data/lib/hecks/bluebook/chapter.rb +68 -0
  91. data/lib/hecks/bluebook/command.rb +124 -0
  92. data/lib/hecks/bluebook/domain_port.rb +102 -0
  93. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  94. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
  95. data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
  96. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
  98. data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
  99. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  100. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  101. data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
  102. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  103. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  104. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  105. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  106. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  107. data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
  108. data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
  109. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  110. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
  111. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  112. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  113. data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
  114. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  115. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  116. data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
  117. data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
  118. data/lib/hecks/bluebook/dsl.rb +45 -0
  119. data/lib/hecks/bluebook/entity.rb +103 -0
  120. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
  121. data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
  122. data/lib/hecks/bluebook/expression/projection.json +170 -0
  123. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  124. data/lib/hecks/bluebook/expression/resolver.rb +630 -0
  125. data/lib/hecks/bluebook/expression.rb +13 -0
  126. data/lib/hecks/bluebook/hexagon.rb +60 -0
  127. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  128. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  129. data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
  130. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  131. data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
  132. data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
  133. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
  134. data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
  135. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  136. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  137. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  138. data/lib/hecks/bluebook/meta_validator.rb +452 -0
  139. data/lib/hecks/bluebook/model_check.rb +318 -0
  140. data/lib/hecks/bluebook/pattern_subset.rb +120 -0
  141. data/lib/hecks/bluebook/policy.rb +41 -0
  142. data/lib/hecks/bluebook/process_manager.rb +86 -0
  143. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  144. data/lib/hecks/bluebook/project_loader.rb +40 -0
  145. data/lib/hecks/bluebook/project_register.rb +82 -0
  146. data/lib/hecks/bluebook/query.rb +61 -0
  147. data/lib/hecks/bluebook/read_model.rb +109 -0
  148. data/lib/hecks/bluebook/reference.rb +74 -0
  149. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  150. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  151. data/lib/hecks/bluebook/translation.rb +92 -0
  152. data/lib/hecks/bluebook/value_object.rb +58 -0
  153. data/lib/hecks/bluebook.rb +74 -0
  154. data/lib/hecks/codemod.rb +342 -0
  155. data/lib/hecks/construct.rb +71 -0
  156. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  157. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  158. data/lib/hecks/deploy/oidc.json +18 -0
  159. data/lib/hecks/doc/reference.rb +389 -0
  160. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  161. data/lib/hecks/facade/cli_door.rb +116 -0
  162. data/lib/hecks/facade/cli_runner.rb +190 -0
  163. data/lib/hecks/facade/command_request.rb +105 -0
  164. data/lib/hecks/facade/handle.rb +173 -0
  165. data/lib/hecks/facade/json_door.rb +154 -0
  166. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  167. data/lib/hecks/facade/surface/chapter.rb +107 -0
  168. data/lib/hecks/facade/surface.rb +48 -0
  169. data/lib/hecks/facade.rb +44 -0
  170. data/lib/hecks/forms/app.rb +300 -0
  171. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  172. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  173. data/lib/hecks/forms/field_renderer.rb +170 -0
  174. data/lib/hecks/forms/field_shape.rb +232 -0
  175. data/lib/hecks/forms/html.rb +53 -0
  176. data/lib/hecks/forms/index_renderer.rb +35 -0
  177. data/lib/hecks/forms/page.rb +157 -0
  178. data/lib/hecks/forms/params.rb +132 -0
  179. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  180. data/lib/hecks/forms/record_renderer.rb +115 -0
  181. data/lib/hecks/forms/record_table.rb +63 -0
  182. data/lib/hecks/forms/reference_options.rb +30 -0
  183. data/lib/hecks/forms/value_object_shape.rb +46 -0
  184. data/lib/hecks/forms.rb +54 -0
  185. data/lib/hecks/fqn.rb +94 -0
  186. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  187. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  188. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  189. data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
  190. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  191. data/lib/hecks/framework/oidc.json +39 -0
  192. data/lib/hecks/framework.rb +90 -0
  193. data/lib/hecks/freezer.rb +67 -0
  194. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  195. data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
  196. data/lib/hecks/fuzzing/properties.rb +1146 -0
  197. data/lib/hecks/fuzzing/replay.rb +635 -0
  198. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  199. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
  200. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  201. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
  202. data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
  203. data/lib/hecks/fuzzing/value_generator.rb +177 -0
  204. data/lib/hecks/fuzzing.rb +12 -0
  205. data/lib/hecks/grammar/evolve.rb +291 -0
  206. data/lib/hecks/grammar/expression.bluebook +417 -0
  207. data/lib/hecks/grammar/expression_operators.json +1152 -0
  208. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  209. data/lib/hecks/grammar/oidc.json +69 -0
  210. data/lib/hecks/grammar/translation.bluebook +203 -0
  211. data/lib/hecks/grammar.rb +178 -0
  212. data/lib/hecks/ir.rb +126 -0
  213. data/lib/hecks/language/adapter.bluebook +116 -0
  214. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  215. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  216. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  217. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  218. data/lib/hecks/language/bluebook/command.bluebook +437 -0
  219. data/lib/hecks/language/bluebook/entity.bluebook +316 -0
  220. data/lib/hecks/language/bluebook/policy.bluebook +172 -0
  221. data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
  222. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  223. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  224. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  225. data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
  226. data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
  227. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  228. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  229. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  230. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  231. data/lib/hecks/language/oidc.json +313 -0
  232. data/lib/hecks/language/port.bluebook +92 -0
  233. data/lib/hecks/language/translation/translation.bluebook +110 -0
  234. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  235. data/lib/hecks/language/world/wiring.bluebook +62 -0
  236. data/lib/hecks/language/world/world.bluebook +84 -0
  237. data/lib/hecks/literal.rb +125 -0
  238. data/lib/hecks/naming.rb +121 -0
  239. data/lib/hecks/ports/access_control.port +4 -0
  240. data/lib/hecks/ports/access_control.rb +62 -0
  241. data/lib/hecks/ports/agent/answers.rb +104 -0
  242. data/lib/hecks/ports/agent.port +4 -0
  243. data/lib/hecks/ports/agent.rb +167 -0
  244. data/lib/hecks/ports/authentication.port +4 -0
  245. data/lib/hecks/ports/authentication.rb +50 -0
  246. data/lib/hecks/ports/authorization.port +4 -0
  247. data/lib/hecks/ports/authorization.rb +59 -0
  248. data/lib/hecks/ports/clock.port +4 -0
  249. data/lib/hecks/ports/clock.rb +62 -0
  250. data/lib/hecks/ports/extraction.port +4 -0
  251. data/lib/hecks/ports/extraction.rb +37 -0
  252. data/lib/hecks/ports/identity_assignment.port +4 -0
  253. data/lib/hecks/ports/identity_assignment.rb +45 -0
  254. data/lib/hecks/ports/identity_generation.port +4 -0
  255. data/lib/hecks/ports/identity_generation.rb +49 -0
  256. data/lib/hecks/ports/identity_resolution.port +4 -0
  257. data/lib/hecks/ports/identity_resolution.rb +40 -0
  258. data/lib/hecks/ports/loading.port +4 -0
  259. data/lib/hecks/ports/loading.rb +13 -0
  260. data/lib/hecks/ports/persistence/append_only.rb +102 -0
  261. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  262. data/lib/hecks/ports/persistence/execution.rb +18 -0
  263. data/lib/hecks/ports/persistence/lineage.rb +277 -0
  264. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  266. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  267. data/lib/hecks/ports/persistence.port +4 -0
  268. data/lib/hecks/ports/persistence.rb +39 -0
  269. data/lib/hecks/ports/projection.port +4 -0
  270. data/lib/hecks/ports/projection.rb +76 -0
  271. data/lib/hecks/ports/query/in_memory.rb +60 -0
  272. data/lib/hecks/ports/query/ordering.rb +41 -0
  273. data/lib/hecks/ports/query.rb +36 -0
  274. data/lib/hecks/ports.rb +27 -0
  275. data/lib/hecks/projections/diagrams.rb +413 -0
  276. data/lib/hecks/projections/ir.rb +18 -0
  277. data/lib/hecks/projections/model/deviations.rb +98 -0
  278. data/lib/hecks/projections/model.rb +145 -0
  279. data/lib/hecks/projections/oidc.rb +110 -0
  280. data/lib/hecks/projections/parser_table.rb +159 -0
  281. data/lib/hecks/projections/reference.rb +38 -0
  282. data/lib/hecks/projections/shape.rb +31 -0
  283. data/lib/hecks/projections/statements.rb +110 -0
  284. data/lib/hecks/projections/vocabulary.rb +100 -0
  285. data/lib/hecks/projections.rb +32 -0
  286. data/lib/hecks/projector/cli_projector.rb +437 -0
  287. data/lib/hecks/projector/docs_projector.rb +321 -0
  288. data/lib/hecks/projector/exporter.rb +141 -0
  289. data/lib/hecks/projector/ir_projector.rb +18 -0
  290. data/lib/hecks/projector/narrate_projector.rb +243 -0
  291. data/lib/hecks/projector/target.rb +97 -0
  292. data/lib/hecks/projector.rb +186 -0
  293. data/lib/hecks/query_ir.rb +392 -0
  294. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  295. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  296. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  297. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  298. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  299. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  300. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  301. data/lib/hecks/query_specification/common/null_policy.rb +80 -0
  302. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  303. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  304. data/lib/hecks/query_specification/common/options.rb +32 -0
  305. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  306. data/lib/hecks/query_specification/common/specification.rb +10 -0
  307. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  308. data/lib/hecks/query_specification/field_path.rb +89 -0
  309. data/lib/hecks/query_specification/hop_path.rb +130 -0
  310. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  311. data/lib/hecks/query_specification.rb +14 -0
  312. data/lib/hecks/rendering.rb +48 -0
  313. data/lib/hecks/router/namespace_installer.rb +157 -0
  314. data/lib/hecks/router.rb +70 -0
  315. data/lib/hecks/runtime/caller.rb +50 -0
  316. data/lib/hecks/runtime/capability_graph.rb +44 -0
  317. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
  318. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
  319. data/lib/hecks/runtime/command_interpreter.rb +413 -0
  320. data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
  321. data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
  322. data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
  323. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  324. data/lib/hecks/runtime/command_rules/references.rb +181 -0
  325. data/lib/hecks/runtime/command_rules.rb +28 -0
  326. data/lib/hecks/runtime/dependency_planning.rb +231 -0
  327. data/lib/hecks/runtime/dispatcher.rb +286 -0
  328. data/lib/hecks/runtime/entity_element.rb +253 -0
  329. data/lib/hecks/runtime/entity_interpreter.rb +228 -0
  330. data/lib/hecks/runtime/era_check.rb +136 -0
  331. data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
  332. data/lib/hecks/runtime/era_guard.rb +193 -0
  333. data/lib/hecks/runtime/era_tamper.rb +61 -0
  334. data/lib/hecks/runtime/errors.rb +100 -0
  335. data/lib/hecks/runtime/event.rb +51 -0
  336. data/lib/hecks/runtime/identity.rb +128 -0
  337. data/lib/hecks/runtime/instance.rb +120 -0
  338. data/lib/hecks/runtime/interpreting.rb +69 -0
  339. data/lib/hecks/runtime/loader.rb +143 -0
  340. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  341. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  342. data/lib/hecks/runtime/query_interpreter.rb +254 -0
  343. data/lib/hecks/runtime/reaction_invocation.rb +245 -0
  344. data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
  345. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  346. data/lib/hecks/runtime/reference_hop.rb +99 -0
  347. data/lib/hecks/runtime/refusal_wording.rb +124 -0
  348. data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
  349. data/lib/hecks/runtime/registry/verification.rb +158 -0
  350. data/lib/hecks/runtime/registry.rb +218 -0
  351. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  352. data/lib/hecks/runtime/routing.rb +96 -0
  353. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  354. data/lib/hecks/runtime/saga_interpreter.rb +301 -0
  355. data/lib/hecks/runtime/storage_shape.rb +120 -0
  356. data/lib/hecks/runtime/tenant_check.rb +84 -0
  357. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  358. data/lib/hecks/runtime/value/admission.rb +112 -0
  359. data/lib/hecks/runtime/value/coercion.rb +500 -0
  360. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  361. data/lib/hecks/runtime/value.rb +125 -0
  362. data/lib/hecks/runtime.rb +95 -0
  363. data/lib/hecks/translation/audit/approval_digest.rb +31 -0
  364. data/lib/hecks/translation/audit/layer_one.rb +37 -0
  365. data/lib/hecks/translation/audit/layer_two.rb +77 -0
  366. data/lib/hecks/translation/audit/unfed_report.rb +42 -0
  367. data/lib/hecks/translation/audit.rb +70 -0
  368. data/lib/hecks/translation/reattest.rb +72 -0
  369. data/lib/hecks/translation/rule_compiler.rb +120 -0
  370. data/lib/hecks/translation/scaffold/differ.rb +183 -0
  371. data/lib/hecks/translation/scaffold/renderer.rb +41 -0
  372. data/lib/hecks/translation/scaffold/writer.rb +28 -0
  373. data/lib/hecks/translation/scaffold.rb +30 -0
  374. data/lib/hecks/translation.rb +11 -0
  375. data/lib/hecks/version.rb +16 -0
  376. data/lib/hecks/vocabulary.rb +213 -0
  377. data/lib/hecks.rb +118 -6
  378. metadata +401 -222
  379. data/bin/hecks +0 -7
  380. data/bin/hecks-package +0 -65
  381. data/bin/hecks_console +0 -12
  382. data/bin/hecks_serverless +0 -6
  383. data/lib/cli/build.rb +0 -14
  384. data/lib/cli/command_runner.rb +0 -28
  385. data/lib/cli/console.rb +0 -10
  386. data/lib/cli/generate.rb +0 -37
  387. data/lib/cli/hecks-cli.rb +0 -27
  388. data/lib/cli/test.rb +0 -57
  389. data/lib/console/commands.rb +0 -8
  390. data/lib/console/hecks-console.rb +0 -1
  391. data/lib/packager/README.md +0 -0
  392. data/lib/packager/app_runner.rb +0 -21
  393. data/lib/packager/args.rb +0 -26
  394. data/lib/packager/compatibility/fixnum.rb +0 -6
  395. data/lib/packager/hecks.rb +0 -39
  396. data/lib/packager/query_runner.rb +0 -21
  397. data/lib/packager/resources/Dockerfile +0 -11
  398. data/lib/packager/resources/app_binary +0 -7
  399. data/lib/packager/resources/bundle_config +0 -3
  400. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  401. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  402. data/lib/serverless/Domain +0 -32
  403. data/lib/serverless/cli.rb +0 -75
  404. data/lib/serverless/resources/command_name.js +0 -5
  405. data/lib/serverless/resources/environment.js +0 -7
  406. data/lib/serverless/resources/handler.js.tt +0 -28
  407. data/lib/serverless/resources/run_binary.js +0 -22
  408. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,219 @@
1
+ # A TOOLING-INTERNAL DOMAIN, not a business one — never attached via
2
+ # `uses_framework`, never seen by an end-user domain. `bin/project_deploy`
3
+ # boots this directory directly (Ports::Loading.bootstrap + load_domain,
4
+ # the same pipeline lib/hecks/runtime/loader.rb uses, minus a
5
+ # `.world`/`.hecksagon` this domain has no need for) and dispatches one
6
+ # command against it before generating anything from a domain's own
7
+ # `deployed_to("AwsLambda")` settings.
8
+ #
9
+ # WHY THIS EXISTS: `world.bluebook`'s `Wiring`/`Setting` is a DELIBERATELY
10
+ # open, untyped map — "a verb's schema is not even contained in the file
11
+ # being parsed" (syntax.bluebook's own header on this). That's the right
12
+ # call for `Wiring` itself (an adapter verb's shape genuinely isn't the
13
+ # world language's business), but it leaves `deployed_to`'s ACTUAL shape
14
+ # validated nowhere — `bin/project_deploy` used to check it by hand
15
+ # (`deploy_settings[:adapter] == "AwsLambda" or abort ...`,
16
+ # `fetch(:region) { abort ... }`), the exact raw-Ruby-refusal pattern
17
+ # every other kind of bluebook mistake in this codebase does NOT use.
18
+ # This closes that gap the same way any other rule gets enforced here:
19
+ # as a real command, real invariants, a real corpus-testable refusal —
20
+ # not a hand-written abort string discovered only by running the script.
21
+ Hecks.bluebook "Deploy" do
22
+ aggregate "LambdaTarget" do
23
+ description "One domain's validated AWS Lambda deploy target — what deployed_to(\"AwsLambda\") must resolve to before bin/project_deploy generates anything from it."
24
+
25
+ identified_by :domain
26
+
27
+ attribute :domain, DomainName
28
+ attribute :region, Region
29
+ attribute :memory, Memory
30
+ attribute :timeout, Timeout
31
+ attribute :database, Database
32
+ attribute :web, Web
33
+
34
+ value_object "DomainName" do
35
+ attribute :value, String
36
+ invariant("a domain is named") { !value.to_s.empty? }
37
+ end
38
+
39
+ value_object "Region" do
40
+ attribute :value, String
41
+ invariant("a region is named") { !value.to_s.empty? }
42
+ end
43
+
44
+ # "Postgres" (a plain AWS::RDS::DBInstance, Embryonaut's own live
45
+ # choice — bin/project_deploy's Ruby-side default when a domain's
46
+ # own deployed_to("AwsLambda") names none, so every domain
47
+ # declared before this attribute existed keeps generating the
48
+ # identical template it always has) or "Aurora" (Aurora Serverless
49
+ # v2, Postgres-compatible — the SAME `Adapters::Postgres`/
50
+ # `tokio_postgres` code on both the Ruby and rust/host sides
51
+ # either way; this only changes which CloudFormation resource
52
+ # bin/project_deploy emits, never a runtime adapter).
53
+ # "Shared" — no RDS/VPC of this domain's own at all; it borrows
54
+ # another already-deployed domain's instance instead (the
55
+ # storehouse), isolated from that domain's own data by a native
56
+ # Postgres SCHEMA (this domain's own lowercase name), not a
57
+ # separate database. Requires a sibling `owner "OtherDomain"`
58
+ # setting inside the SAME deployed_to("AwsLambda") block, naming
59
+ # which already-deployed domain owns the shared instance —
60
+ # bin/project_deploy checks for that setting itself (Ruby-level,
61
+ # the same as `pg_version`/`google_oauth_present` below it), not
62
+ # here: which domain owns the instance is a deploy-time fact, not
63
+ # a business invariant this validated aggregate's job to hold.
64
+ value_object "Database" do
65
+ attribute :value, String, one_of: ["Postgres", "Aurora", "Shared"]
66
+ end
67
+
68
+ # "None" (the default — nothing changes for a domain that names
69
+ # neither this nor drops a `lambda_handler.rb`, e.g. Banking) or
70
+ # "Rust" — the domain's OWN Lambda (rust/host) also serves a public
71
+ # web UI in-process (rust/host/src/web.rs), reading HECKS_IR_PATH
72
+ # (a plain-JSON sidecar bin/project_wasm writes beside the .wasm
73
+ # artifact) and dispatching through the SAME in-process
74
+ # dispatch::handle/read the internal {"verb"}/{"read"} path already
75
+ # uses — no second Lambda, no VPC Interface Endpoint, no Ruby.
76
+ # Deliberately NOT "Ruby" as a third member here: a Ruby WebFunction
77
+ # is the pre-existing, SEPARATE `lambda_handler.rb`-file-presence
78
+ # mechanism (Embryonaut's own), untouched by this attribute —
79
+ # bin/project_deploy refuses generation outright if a domain
80
+ # declares `web "Rust"` AND has a `lambda_handler.rb`, rather than
81
+ # silently picking one.
82
+ value_object "Web" do
83
+ attribute :value, String, one_of: ["None", "Rust"]
84
+ end
85
+
86
+ # 128-10240 MB — AWS Lambda's own real ceiling and floor, not a
87
+ # number this domain invented. A typo here used to fail deep inside
88
+ # a real `sam deploy`, sometimes minutes in; now it refuses before
89
+ # a single file is generated.
90
+ value_object "Memory" do
91
+ attribute :value, Integer
92
+ invariant("memory is at least 128 MB") { value >= 128 }
93
+ invariant("memory is at most 10240 MB") { value <= 10240 }
94
+ end
95
+
96
+ # 900s — Lambda's own real ceiling.
97
+ value_object "Timeout" do
98
+ attribute :value, Integer
99
+ invariant("a timeout is positive") { value.positive? }
100
+ invariant("a timeout fits Lambda's own 900s ceiling") { value <= 900 }
101
+ end
102
+
103
+ # ALL FOUR REQUIRED — deliberately, not `optional: true` +
104
+ # `default:`. Confirmed directly against CommandInterpreter's own
105
+ # `normalize_args`/`coerce_declared_arguments` (interpreting.rb):
106
+ # `next unless normalized.key?(attribute.name)` skips coercion
107
+ # entirely for an absent key, so a command-level `default:` is
108
+ # honored ONLY at the JSON/Rust-codegen boundary (`scalar_from_json_
109
+ # expr`'s own domain), never by Ruby's own direct dispatch — a
110
+ # `default:` here would be quietly false advertising. "What memory
111
+ # defaults to when unspecified" stays exactly where it already
112
+ # lived (bin/project_deploy's own `fetch(:memory, 512)`); this
113
+ # command validates a FULLY-RESOLVED target, and bin/project_deploy
114
+ # resolves the default before ever dispatching.
115
+ command "Declare" do
116
+ goal "Validate one domain's AwsLambda deploy target before anything is generated from it"
117
+
118
+ attribute :domain, DomainName
119
+ attribute :region, Region
120
+ attribute :memory, Memory
121
+ attribute :timeout, Timeout
122
+ attribute :database, Database
123
+ attribute :web, Web
124
+
125
+ sets :domain
126
+ sets :region
127
+ sets :memory
128
+ sets :timeout
129
+ sets :database
130
+ sets :web
131
+
132
+ emits "LambdaTargetDeclared"
133
+ end
134
+ end
135
+
136
+ # `bin/project_tenant`'S OWN VALIDATED SHAPE — same split LambdaTarget
137
+ # draws with `bin/project_deploy`: this aggregate validates, that
138
+ # script GENERATES/ACTS (writes `environments/<slug>.world`, ensures
139
+ # the Postgres schema, boots once to prove it). Deliberately NOT a
140
+ # saga/process manager with durable runtime state — every step
141
+ # `bin/project_tenant` takes is already idempotent by construction
142
+ # (`CREATE SCHEMA IF NOT EXISTS` — PostgresEra#connect_for's own,
143
+ # File.write of the overlay — a plain overwrite, `Hecks.boot` itself),
144
+ # the same "generator script, not a business process" shape
145
+ # `bin/project_deploy` already is; a saga's own resumability earns
146
+ # its cost coordinating a real distributed transaction across
147
+ # multiple ports with real failure boundaries (Payments::Payment's
148
+ # own PaymentGateway, say) — three local, idempotent, single-process
149
+ # steps have no such boundary to resume across. A durable, reactable
150
+ # RUNTIME fact ("this tenant exists, is active, is suspended") is a
151
+ # different, later, business-facing thing — deliberately not built
152
+ # here, and deliberately not named Deploy::Tenant to avoid colliding
153
+ # with this validation-only aggregate should it ever land.
154
+ aggregate "Tenant" do
155
+ description "One tenant's validated identity — which realm and Postgres schema `bin/project_tenant` provisions before booting a domain directory under Runtime.boot's own `environment:` overlay for it."
156
+
157
+ identified_by :slug
158
+
159
+ attribute :slug, Slug
160
+ attribute :domain, DomainName
161
+ attribute :realm, Realm
162
+ attribute :schema, Schema
163
+
164
+ # LOWERCASE, NO SPACES — a slug becomes both the `environments/
165
+ # <slug>.world` filename and (by convention, not enforced here —
166
+ # `schema` is its own, separately-declared field precisely so a
167
+ # tenant CAN choose a different schema name than its slug)
168
+ # ordinarily the schema name too.
169
+ value_object "Slug" do
170
+ attribute :value, String, pattern: '^[a-z][a-z0-9_]*$'
171
+ invariant("a tenant slug is named") { !value.to_s.empty? }
172
+ end
173
+
174
+ value_object "DomainName" do
175
+ attribute :value, String
176
+ invariant("a tenant names the domain it is for") { !value.to_s.empty? }
177
+ end
178
+
179
+ value_object "Realm" do
180
+ attribute :value, String
181
+ invariant("a tenant realm is named") { !value.to_s.empty? }
182
+ end
183
+
184
+ # POSTGRES'S OWN REAL IDENTIFIER RULES, loosely — lowercase (this
185
+ # adapter never quotes a mixed-case identifier back out consistently
186
+ # once created), letters/digits/underscores, starting with a letter.
187
+ # NOT a byte-length ceiling too (Postgres's own real 63-byte
188
+ # NAMEDATALEN limit) — the expression grammar invariants compile
189
+ # through admits a narrow arithmetic/comparison subset, not
190
+ # arbitrary method dispatch (`bytesize` included; found live,
191
+ # writing this: `Bluebook::Expression::Evaluator` tries to resolve
192
+ # a dotted chain as a FIELD lookup, not a method call, the same
193
+ # "admits + but not -, no parentheses" narrowness
194
+ # embryonaut_bluebooks/payments' own Refund command already
195
+ # documents). Postgres itself refuses a too-long identifier loudly
196
+ # on its own; not worth fighting the expression grammar for a
197
+ # corner case this rare.
198
+ value_object "Schema" do
199
+ attribute :value, String, pattern: '^[a-z][a-z0-9_]*$'
200
+ invariant("a schema is named") { !value.to_s.empty? }
201
+ end
202
+
203
+ command "Declare" do
204
+ goal "Validate one tenant's shape before bin/project_tenant provisions its schema and overlay world"
205
+
206
+ attribute :slug, Slug
207
+ attribute :domain, DomainName
208
+ attribute :realm, Realm
209
+ attribute :schema, Schema
210
+
211
+ sets :slug
212
+ sets :domain
213
+ sets :realm
214
+ sets :schema
215
+
216
+ emits "TenantDeclared"
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,4 @@
1
+ # The Deploy domain, bound so it boots standing alone.
2
+ Hecks.hecksagon "Deploy" do
3
+ persisted_by "Memory"
4
+ end
@@ -0,0 +1,18 @@
1
+ {
2
+ "audience": "Deploy",
3
+ "scopes": [
4
+ {
5
+ "scope": "deploy:lambda_target.declare",
6
+ "verb": "Deploy::LambdaTarget.Declare",
7
+ "role": null
8
+ },
9
+ {
10
+ "scope": "deploy:tenant.declare",
11
+ "verb": "Deploy::Tenant.Declare",
12
+ "role": null
13
+ }
14
+ ],
15
+ "roles": [
16
+
17
+ ]
18
+ }
@@ -0,0 +1,389 @@
1
+ require_relative "../bluebook/meta_validator"
2
+ require_relative "../naming"
3
+
4
+ module Hecks
5
+ module Doc
6
+ # The DSL reference, projected from the language's own Syntax chapter
7
+ # — the same Keyword/Argument rows the conformance specs hold equal
8
+ # to the live builders. Nothing here is described twice: the tables
9
+ # come from the declaration, the PROSE is hand-written between
10
+ # markers the generator preserves, and the golden spec refuses a
11
+ # tree where the two have drifted.
12
+ #
13
+ # Regenerate with bin/reference. A new word arrives with a TODO
14
+ # sentinel; the coverage gate refuses to let an admitted word ship
15
+ # undocumented; prose for a word the language no longer declares is
16
+ # a hard error naming its orphans — deleting someone's writing is a
17
+ # human's decision.
18
+ module Reference
19
+ GENERATED_END = "<!-- generated:end -->".freeze
20
+ TODO_SENTINEL = "<!-- TODO: document this word -->".freeze
21
+
22
+ # A PAGE'S OWN HAND-WRITTEN OPENING, harvested under a key no word
23
+ # can ever collide with (words are strings off the Syntax chapter;
24
+ # this is a Symbol). It exists so a page can boot ONCE — load a real
25
+ # corpus chapter, wire its hexagon — and have every word's example
26
+ # below run against that single boot, the way a guide's opening
27
+ # `ruby boot` block already does. Without it each word would have to
28
+ # stand up its own domain, and 105 invented chapters would collide
29
+ # on the facade constants `Surface.install` never uninstalls.
30
+ PREAMBLE = :preamble
31
+
32
+ module_function
33
+
34
+ def generated_begin(word) = "<!-- generated:begin word=#{word} -->"
35
+
36
+ # Keyed by REGION rather than by word — the same marker convention,
37
+ # used for the parts of a page that are not about one word: a
38
+ # page's generated lede here, README's generated indexes below.
39
+ def region_begin(id) = "<!-- generated:begin id=#{id} -->"
40
+
41
+ def syntax
42
+ meta = Bluebook::MetaValidator.grammar_registry.bluebook("Bluebook")
43
+ meta.aggregates.find { |aggregate| aggregate.hecks_name == "Syntax" }
44
+ end
45
+
46
+ def rows(name)
47
+ syntax.value_objects.find { |vo| vo.hecks_name == name }
48
+ .members.map { |row| row.to_h.transform_values(&:to_s) }
49
+ end
50
+
51
+ # S14, ADR 0026 — Keyword/Argument are genuine entities of Syntax
52
+ # now, dispatched (not merely declared) so their own `status`
53
+ # really is a lifecycle. `SyntaxBoot.call` discovers the static
54
+ # aggregate-local seed rows (`KeywordSeed`/`ArgumentSeed`), dispatches each
55
+ # one through the real admission/lifecycle door, and hands back the
56
+ # same shape `rows` used to produce — nothing below this needed to
57
+ # change.
58
+ #
59
+ # NO SEPARATE `@keywords ||=` HERE ANYMORE. This module used to
60
+ # memoize its own copy on top of `SyntaxBoot.call`'s own memo — a
61
+ # double cache with no way to invalidate either half, and a real
62
+ # bug: whichever call in the whole process happened to land first
63
+ # got locked in forever, even one caught mid-build missing every
64
+ # Paging-attached word (limit/offset/cursor/nulls). `SyntaxBoot.call`
65
+ # now carries the one cache that matters (keyed on the grammar
66
+ # registry's own chapter set — see its comment) ; this delegates
67
+ # straight through instead of shadowing it.
68
+ def keywords = Bluebook::MetaValidator::SyntaxBoot.call[:keywords]
69
+ def arguments = Bluebook::MetaValidator::SyntaxBoot.call[:arguments]
70
+
71
+ def status_of(row) = row[:status].to_s.empty? ? "admitted" : row[:status].to_s
72
+ def live?(row) = %w[admitted deprecated].include?(status_of(row))
73
+
74
+ def contexts = keywords.map { |row| row[:context] }.uniq
75
+
76
+ def page_name(context) = "#{Naming.snake(context)}.md"
77
+
78
+ # Every reference page, rendered fresh — prose carried over from
79
+ # the committed pages, new words seeded with the sentinel, orphaned
80
+ # prose refused.
81
+ def pages(directory)
82
+ contexts.each_with_object({}) do |context, pages|
83
+ path = File.join(directory, page_name(context))
84
+ prose = File.exist?(path) ? harvest(File.read(path)) : {}
85
+ pages[page_name(context)] = render_page(context, prose, path)
86
+ end.merge("index.md" => render_index)
87
+ end
88
+
89
+ # A WORD ADMITTING TWO FORMS HAS TWO ROWS — syntax.bluebook's own
90
+ # stated rule, and `identified_by` (a block, or a bare argument and
91
+ # none) is the case that made it real again. One SECTION per word all
92
+ # the same: the prose is the word's rather than the form's, and the
93
+ # argument rows join by (word, context) and so already cover every
94
+ # form. Grouped rather than rendered per row, or a reader would meet
95
+ # the same heading and the same paragraph twice.
96
+ def render_page(context, prose, path)
97
+ words = keywords.select { |row| row[:context] == context }.group_by { |row| row[:word] }
98
+ orphans = prose.keys - words.keys - [PREAMBLE]
99
+ unless orphans.empty?
100
+ raise "#{path} carries prose for #{orphans.join(', ')}, which the language no longer " \
101
+ "declares in #{context} — deleting writing is a human's decision, so decide"
102
+ end
103
+
104
+ sections = words.map { |word, forms| render_word(forms, prose[word]) }
105
+ preamble = prose[PREAMBLE].to_s.strip
106
+ <<~PAGE
107
+ # #{context}
108
+
109
+ #{region_begin('page')}
110
+ #{context_lede(context)}
111
+
112
+ *The tables on this page are generated from the language's own
113
+ aggregate-local syntax tables (`lib/hecks/language/**/*.bluebook`)
114
+ by `bin/reference` — do not edit inside the markers. The prose
115
+ between them is hand-written and survives regeneration.*
116
+ #{GENERATED_END}
117
+ #{preamble.empty? ? '' : "\n#{preamble}\n"}
118
+ #{sections.join("\n")}
119
+ PAGE
120
+ end
121
+
122
+ def context_lede(context)
123
+ openers = keywords.select { |row| row[:opens] == context }
124
+ return "Words available at the top of a file." if context == "File"
125
+ return "Words available in the type position of an `attribute`." if context == "Type"
126
+
127
+ inside = openers.map { |row| "`#{row[:word]} do ... end`" }.uniq.join(" / ")
128
+ inside.empty? ? "Words available in the #{context} body." : "Words available inside #{inside}."
129
+ end
130
+
131
+ # One SPELLING per form, everything else off the first row — the
132
+ # columns that differ between two forms of one word are `body` (which
133
+ # is what the spelling shows) and nothing else.
134
+ def render_word(forms, prose)
135
+ row = forms.first
136
+ table = argument_table(row)
137
+ facts = []
138
+ facts << "opens a `#{row[:opens]}` body" unless row[:opens].to_s.empty?
139
+ facts << "fills `#{row[:fills]}`" unless row[:fills].to_s.empty?
140
+ facts << "**status: #{status_of(row)}**" unless status_of(row) == "admitted"
141
+ facts << "was `#{row[:was]}`" unless row[:was].to_s.empty?
142
+ spellings = forms.map { |form| "`#{signature(form)}`" }.join(" / ")
143
+
144
+ <<~WORD
145
+ ## #{row[:word]}
146
+
147
+ #{generated_begin(row[:word])}
148
+ #{spellings}#{facts.empty? ? '' : " — #{facts.join(', ')}"}
149
+ #{table}#{GENERATED_END}
150
+
151
+ #{prose_or_sentinel(prose)}
152
+ WORD
153
+ end
154
+
155
+ def prose_or_sentinel(prose)
156
+ text = prose.to_s.strip
157
+ text.empty? ? TODO_SENTINEL : text
158
+ end
159
+
160
+ def word_arguments(row)
161
+ arguments.select { |arg| arg[:keyword] == row[:word] && arg[:context] == row[:context] }
162
+ end
163
+
164
+ def signature(row)
165
+ positional = word_arguments(row).reject { |arg| arg[:at].to_s.empty? }
166
+ .sort_by { |arg| arg[:at].to_i }
167
+ .map { |arg| arg[:fills].to_s.empty? ? arg[:kind] : arg[:fills] }
168
+ named = word_arguments(row).select { |arg| arg[:at].to_s.empty? }
169
+ .map { |arg| "#{arg[:named]}:" }
170
+ parts = positional + named
171
+ base = parts.empty? ? row[:word] : "#{row[:word]} #{parts.join(', ')}"
172
+ row[:body].to_s == "none" ? base : "#{base} do ... end"
173
+ end
174
+
175
+ def argument_table(row)
176
+ args = word_arguments(row)
177
+ return "" if args.empty?
178
+
179
+ lines = ["", "| argument | kind | required | fills |", "|---|---|---|---|"]
180
+ args.each do |arg|
181
+ name = arg[:at].to_s.empty? ? "`#{arg[:named]}:`" : "positional #{arg[:at]}"
182
+ lines << "| #{name} | #{arg[:kind]} | #{arg[:required]} | #{arg[:fills]} |"
183
+ end
184
+ lines.join("\n") + "\n"
185
+ end
186
+
187
+ def render_index
188
+ listed = contexts.map do |context|
189
+ count = keywords.select { |row| row[:context] == context }.map { |row| row[:word] }.uniq.size
190
+ "- [#{context}](#{page_name(context)}) — #{count} #{count == 1 ? 'word' : 'words'}"
191
+ end
192
+ <<~INDEX
193
+ # The DSL reference
194
+
195
+ One page per context — the place in a file where a word may be
196
+ typed. Generated by `bin/reference` from the Syntax chapter;
197
+ the prose between the generated markers is hand-written and
198
+ survives regeneration.
199
+
200
+ #{listed.join("\n")}
201
+ INDEX
202
+ end
203
+
204
+ # Prose keyed by word: everything between a section's generated
205
+ # region and the next `## ` heading (or end of file).
206
+ #
207
+ # Starts on PREAMBLE rather than nil so the text between the PAGE's
208
+ # own generated lede and its first word heading is carried over too
209
+ # instead of being silently dropped. A page written before that
210
+ # region existed has no generated marker ahead of its first `## `,
211
+ # so nothing is collecting when that heading arrives and no empty
212
+ # preamble is invented — the older shape reads back unchanged.
213
+ def harvest(text)
214
+ prose = {}
215
+ current = PREAMBLE
216
+ collecting = false
217
+ buffer = []
218
+
219
+ # A HEADING INSIDE A FENCE IS NOT A HEADING. `## something` is an
220
+ # ordinary Ruby comment, and now that every word's section carries
221
+ # runnable code, one written at the left margin would otherwise
222
+ # end that section mid-example and orphan the rest of it under a
223
+ # word the language never declared.
224
+ in_fence = false
225
+
226
+ text.each_line do |line|
227
+ in_fence = !in_fence if line.start_with?("```")
228
+
229
+ if !in_fence && (match = line.match(/\A## (\S+)\s*\z/))
230
+ prose[current] = buffer.join.strip if current && collecting
231
+ current = match[1]
232
+ collecting = false
233
+ buffer = []
234
+ elsif line.include?(GENERATED_END)
235
+ collecting = true
236
+ elsif collecting
237
+ buffer << line
238
+ end
239
+ end
240
+ prose[current] = buffer.join.strip if current && collecting
241
+ prose.reject { |_word, text_| text_.empty? || text_ == TODO_SENTINEL }
242
+ end
243
+
244
+ def write!(directory)
245
+ FileUtils.mkdir_p(directory)
246
+ pages(directory).each do |name, content|
247
+ File.write(File.join(directory, name), content)
248
+ end
249
+ end
250
+
251
+ # README's own generated regions — the same marker convention as a
252
+ # reference page, keyed by region id instead of a word, so the
253
+ # index a reader lands on first can't drift from what actually
254
+ # exists on disk either.
255
+ def readme_regions(root)
256
+ {
257
+ "guides" => guide_index(root),
258
+ "reference" => reference_index(root),
259
+ "tools" => tool_table(root),
260
+ "corpus" => corpus_roster(root)
261
+ }
262
+ end
263
+
264
+ def guide_index(root)
265
+ paths = Dir.glob(File.join(root, "docs/implemented/guides/*.md")).sort
266
+ .reject { |p| %w[AUTHORING.md].include?(File.basename(p)) }
267
+ lines = paths.map do |path|
268
+ heading = File.foreach(path).find { |line| line.start_with?("# ") }
269
+ title = heading ? heading.sub(/\A#\s*/, "").strip : File.basename(path)
270
+ "- [#{title}](docs/implemented/guides/#{File.basename(path)})"
271
+ end
272
+ lines.join("\n")
273
+ end
274
+
275
+ def reference_index(root)
276
+ count = contexts.size
277
+ "[The DSL reference](docs/implemented/reference/index.md) — #{count} contexts, generated from " \
278
+ "the aggregate-local tables under `lib/hecks/language/` and held to them by " \
279
+ "`spec/reference_golden_spec.rb`."
280
+ end
281
+
282
+ def tool_table(root)
283
+ scripts = Dir.glob(File.join(root, "bin/*")).select { |p| File.file?(p) }.sort
284
+ rows = scripts.filter_map { |path| [path, tool_summary(path)] }.select { |_, desc| desc }
285
+ lines = ["| tool | |", "|---|---|"]
286
+ rows.each { |path, desc| lines << "| `bin/#{File.basename(path)}` | #{desc} |" }
287
+ lines.join("\n")
288
+ end
289
+
290
+ # The opening comment PARAGRAPH, not just the first line — a table
291
+ # cell that trails off mid-clause reads worse than one that runs a
292
+ # little long and says "...". A code-bearing comment (`field.name`,
293
+ # `pattern:`) makes naive sentence-splitting on "." or ":" cut in
294
+ # the wrong place, so this truncates on LENGTH alone.
295
+ def tool_summary(path)
296
+ comment_lines = []
297
+ started = false
298
+ File.foreach(path).first(10).each do |line|
299
+ if line.start_with?("#") && !line.start_with?("#!")
300
+ next if !started && line.strip == "#"
301
+
302
+ started = true
303
+ comment_lines << line.sub(/\A#\s?/, "").rstrip
304
+ elsif started
305
+ break
306
+ end
307
+ end
308
+ return nil if comment_lines.empty?
309
+
310
+ text = comment_lines.join(" ").squeeze(" ")
311
+ text.length > 140 ? "#{text[0, 137]}..." : text
312
+ end
313
+
314
+ def corpus_roster(root)
315
+ dirs = Dir.glob(File.join(root, "examples/*/")).sort
316
+ lines = dirs.filter_map do |dir|
317
+ name = File.basename(dir.chomp("/"))
318
+ bluebooks = Dir.glob(File.join(dir, "bluebook/*.bluebook")).sort
319
+ bluebooks = Dir.glob(File.join(dir, "*.bluebook")).sort if bluebooks.empty?
320
+ next if bluebooks.empty?
321
+
322
+ vision = bluebooks.filter_map { |bluebook| File.read(bluebook)[/vision\s+"([^"]*)"/, 1] }.first
323
+ "- **#{name}** — #{vision}"
324
+ end
325
+ lines.join("\n")
326
+ end
327
+
328
+ def render_readme(root, text)
329
+ readme_regions(root).reduce(text) do |current, (id, content)|
330
+ pattern = /#{Regexp.escape(region_begin(id))}.*?#{Regexp.escape(GENERATED_END)}/m
331
+ current.sub(pattern) { "#{region_begin(id)}\n#{content}\n#{GENERATED_END}" }
332
+ end
333
+ end
334
+
335
+ def write_readme!(root)
336
+ path = File.join(root, "README.md")
337
+ File.write(path, render_readme(root, File.read(path)))
338
+ end
339
+
340
+ # An example A READER CAN SEE and the harness will actually run.
341
+ # `ruby skip` is display-only by the doctest harness's own rule, and
342
+ # a hidden `<!-- doctest:boot -->` block is setup rather than an
343
+ # example — a word whose only "example" is invisible or inert is a
344
+ # word still shipping on its prose alone, which is the thing this
345
+ # gate exists to refuse.
346
+ EXAMPLE_FENCE = /^```ruby(?: bluebook| boot)?[ \t]*$/.freeze
347
+
348
+ def exemplified?(prose) = prose.to_s.match?(EXAMPLE_FENCE)
349
+
350
+ # EVERY LIVE WORD, PAIRED WITH ITS PROSE. Both coverage gates ask a
351
+ # question about this same walk and differ only in what they ask of
352
+ # the prose, so they share it rather than each re-deriving the page
353
+ # set — the two are meant to move together, and one drifting past
354
+ # the other is how a word ends up counted documented by one and
355
+ # missing to the other.
356
+ #
357
+ # `harvest` already rejects empty prose and the TODO sentinel, so a
358
+ # word with nothing written for it arrives here with a nil.
359
+ def live_words(directory)
360
+ contexts.flat_map { |context|
361
+ path = File.join(directory, page_name(context))
362
+ prose = File.exist?(path) ? harvest(File.read(path)) : {}
363
+ keywords.select { |row| row[:context] == context && live?(row) }
364
+ .map { |row| [row[:word], context, prose[row[:word]]] }
365
+ }.uniq { |word, context, _| [word, context] }
366
+ end
367
+
368
+ def name_of(word, context) = "#{word} (#{context})"
369
+
370
+ # The coverage gate's question: every LIVE word with no prose yet.
371
+ def undocumented(directory)
372
+ live_words(directory).reject { |_word, _context, prose| prose }
373
+ .map { |word, context, _| name_of(word, context) }
374
+ end
375
+
376
+ # The SECOND coverage gate: prose is a declaration, and a
377
+ # declaration nothing runs cannot disagree with anything. A word
378
+ # documented only in sentences can go stale — or describe a word
379
+ # the runtime never wired at all, which this repository has already
380
+ # shipped twice (`read_model`'s where/order_by/limit/offset, and
381
+ # `role`/`goal` on a command). An example that RUNS is the only
382
+ # documentation that can go red.
383
+ def unexemplified(directory)
384
+ live_words(directory).reject { |_word, _context, prose| exemplified?(prose) }
385
+ .map { |word, context, _| name_of(word, context) }
386
+ end
387
+ end
388
+ end
389
+ end