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,177 @@
1
+ require_relative "../runtime/value"
2
+
3
+ module Hecks
4
+ module Fuzzing
5
+ # One attribute, one value — in the exact JSON shape the hand-written
6
+ # corpus already uses (a value-object-typed attribute is a nested hash keyed
7
+ # by its own field names ; a reference is the BARE ID of the head it points
8
+ # at, because that is what a reference is). Everything
9
+ # returned is a plain, JSON-safe Ruby value : String keys throughout, never
10
+ # symbols, so a generated step can be dumped straight to JSON.
11
+ #
12
+ # Biased toward the edge cases a hand-written corpus tends to under-sample —
13
+ # empty strings, quotes, commas, unicode, zero, negative numbers — because
14
+ # `marks.rb` names literal-encoding loss "the largest family of bug in this
15
+ # codebase," and a fixed corpus of a few dozen examples rarely happens to
16
+ # exercise the boundary that actually breaks. But mostly not : an attribute
17
+ # named `email`/`currency` almost always needs a specific shape just to get
18
+ # PAST its own invariant (`address.include?("@")`, `currency.size == 3`) —
19
+ # banking's very first fuzz run never got past Customer.Register, because
20
+ # no plain random word ever contains "@". Reaching deep state matters more
21
+ # than edge-casing every single field every single time, so the "normal"
22
+ # path is now name-aware for the couple of shapes that are genuinely
23
+ # common across domains, and edge cases stay real but less frequent.
24
+ module ValueGenerator
25
+ module_function
26
+
27
+ EDGE_CASE_PROBABILITY = 0.2
28
+ INVALID_MEMBER_PROBABILITY = 0.1
29
+ INVALID_REFERENCE_PROBABILITY = 0.2
30
+ # `Value.for_attribute` → `fields_for`'s own bare-scalar branch
31
+ # (lib/hecks/runtime/value/coercion.rb) has accepted a bare
32
+ # `"large"` in place of `{"value" => "large"}` for any single-field
33
+ # value object since 86727afd — but until this generator actually
34
+ # PRODUCES that shape, nothing exercises it: neither the adapter-
35
+ # agreement gate nor the Rust/WASM `from_json` codegen (rust/project/
36
+ # json_codec.rb) can ever be caught drifting on a shape they're
37
+ # never handed.
38
+ BARE_SCALAR_PROBABILITY = 0.2
39
+
40
+ STRING_EDGE_CASES = [
41
+ "", "x", "with \"quotes\"", "with, a comma", "with a \\backslash",
42
+ "unicode héllo wörld 🎉", "x" * 200, " leading and trailing "
43
+ ].freeze
44
+ INTEGER_EDGE_CASES = [0, -1, 2_147_483_647, -2_147_483_648].freeze
45
+ FLOAT_EDGE_CASES = [0.0, -0.5, -100.25].freeze
46
+ WORDS = %w[alpha bravo charlie delta echo foxtrot golf hotel india juliet].freeze
47
+ CURRENCY_CODES = %w[USD EUR GBP JPY].freeze
48
+
49
+ # The value for one command/query/value-object attribute. `known_ids` is
50
+ # `{aggregate_or_entity_name => [scalar id, ...]}`, supplied by the
51
+ # sequence generator — a reference draws a real id from it most of the
52
+ # time and a fabricated one sometimes, to exercise NotFound as often as
53
+ # the happy path.
54
+ # `context` carries the enclosing value object's own declared name down
55
+ # into a nested primitive field — `EmailAddress`'s `address` field is
56
+ # what actually needs to look email-shaped, but the field itself is
57
+ # just named "address"; the VO's own name is where "email" lives. A
58
+ # combined hint catches both spellings without needing to guess which
59
+ # level a domain happened to name the thing on.
60
+ def value_for(attribute, aggregate, random:, known_ids: {}, context: nil)
61
+ return reference_value(attribute, random: random, known_ids: known_ids) if attribute.reference?
62
+
63
+ # Local first, falling back to a same-chapter identity value object
64
+ # declared on a sibling aggregate — the exact resolution
65
+ # `Value.value_object_for` already does for a real dispatch
66
+ # (runtime/value/coercion.rb). `safe_deposit_boxes.bluebook`'s own
67
+ # `attribute :customer, CustomerNumber` is declared on SafeDepositBox
68
+ # but CustomerNumber itself lives on Customer; without this fallback
69
+ # the generator raised "does not know primitive type" for any such
70
+ # cross-aggregate identity attribute rather than reusing the same
71
+ # lookup the runtime relies on.
72
+ value_object = Runtime::Value.value_object_for(aggregate, attribute.type.to_s)
73
+ return object_for(value_object, aggregate, random: random, known_ids: known_ids) if value_object
74
+
75
+ primitive(attribute.type.to_s, random: random, name: "#{context} #{attribute.name}")
76
+ end
77
+
78
+ def object_for(value_object, aggregate, random:, known_ids:)
79
+ fields =
80
+ if value_object.closed_set? && !value_object.members.empty?
81
+ return invalid_member(value_object, random: random) if random.rand < INVALID_MEMBER_PROBABILITY
82
+
83
+ member = value_object.members.sample(random: random)
84
+ member.to_h { |field, value| [field.to_s, value] }
85
+ else
86
+ value_object.attributes.to_h do |field|
87
+ [field.name.to_s,
88
+ value_for(field, aggregate, random: random, known_ids: known_ids, context: value_object.hecks_name)]
89
+ end
90
+ end
91
+
92
+ # `fields.size == 1` — the SAME test `Behaviour::ValueObject#
93
+ # sole_attribute` names: a genuinely single-field value object,
94
+ # not merely "this particular closed-set member happened to pick
95
+ # one field." Unwrapping ONLY here, not in `invalid_member`
96
+ # above — a deliberately-wrong combination stays a Hash so its
97
+ # own wrongness is what gets exercised, not a second, unrelated
98
+ # shape question.
99
+ return fields.values.first if fields.size == 1 && random.rand < BARE_SCALAR_PROBABILITY
100
+
101
+ fields
102
+ end
103
+
104
+ # A combination that (almost certainly) isn't one of the closed set's
105
+ # admitted rows — deliberately, to exercise the refusal a `one_of`
106
+ # exists to enforce, not just its happy path.
107
+ def invalid_member(value_object, random:)
108
+ value_object.attributes.to_h { |field|
109
+ [field.name.to_s, primitive(field.type.to_s, random: random, name: field.name.to_s)]
110
+ }
111
+ end
112
+
113
+ # THE ID ITSELF. This minted `{"value" => id}` back when a reference was
114
+ # stored wrapped ; the payload gate refuses that shape now, so a fuzzer
115
+ # still emitting it would have every generated reference refused and
116
+ # the SILENT guard would report the fuzzer broken rather than
117
+ # the runtime.
118
+ def reference_value(attribute, random:, known_ids:)
119
+ pool = known_ids[attribute.type.target_name.to_s] || []
120
+ return "missing-#{random.bytes(4).unpack1('H*')}" if pool.empty? || random.rand < INVALID_REFERENCE_PROBABILITY
121
+
122
+ pool.sample(random: random)
123
+ end
124
+
125
+ def primitive(type_name, random:, name: nil)
126
+ case type_name
127
+ when "String" then string_value(random, name: name)
128
+ when "Integer" then integer_value(random)
129
+ when "Float" then float_value(random)
130
+ when "TrueClass", "FalseClass" then random.rand(2).zero?
131
+ else raise ArgumentError, "ValueGenerator does not know primitive type #{type_name.inspect}"
132
+ end
133
+ end
134
+
135
+ def string_value(random, name: nil)
136
+ return STRING_EDGE_CASES.sample(random: random) if random.rand < EDGE_CASE_PROBABILITY
137
+ return email_value(random) if name&.match?(/email/i)
138
+ return CURRENCY_CODES.sample(random: random) if name&.match?(/currency/i)
139
+
140
+ Array.new(random.rand(1..3)) { WORDS.sample(random: random) }.join(" ")
141
+ end
142
+
143
+ def email_value(random)
144
+ "#{WORDS.sample(random: random)}@#{WORDS.sample(random: random)}.example"
145
+ end
146
+
147
+ # Skewed positive : `cents.positive?`/`!cents.negative?`-style
148
+ # invariants are common across this codebase's example domains, and a
149
+ # sequence that can never get past one never reaches the state a deeper
150
+ # bug would need. Zero and negative are still real, reachable outcomes —
151
+ # via the edge-case pool, deliberately, not by starving them entirely.
152
+ def integer_value(random)
153
+ return INTEGER_EDGE_CASES.sample(random: random) if random.rand < EDGE_CASE_PROBABILITY
154
+
155
+ random.rand(1..1000)
156
+ end
157
+
158
+ def float_value(random)
159
+ return FLOAT_EDGE_CASES.sample(random: random) if random.rand < EDGE_CASE_PROBABILITY
160
+
161
+ random.rand(0.01..1000.0).round(2)
162
+ end
163
+
164
+ # The bare scalar a generated IDENTITY value stands for, for recording into
165
+ # `known_ids`. An identity is declared as a value object, so this opens one ;
166
+ # a reference pointing at this record is already that scalar and needs no
167
+ # opening at all. The two used to be the same reading and are not any more.
168
+ def scalar_of(identity_value)
169
+ identity_value.is_a?(Hash) ? identity_value.values.first.to_s : identity_value.to_s
170
+ end
171
+
172
+ def random_id(random)
173
+ "gen-#{random.bytes(4).unpack1('H*')}"
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,12 @@
1
+ module Hecks
2
+ # The fuzzing toolkit bin/fuzz and bin/generate drive. Not required by
3
+ # lib/hecks.rb on purpose — a booted domain never needs it.
4
+ module Fuzzing
5
+ end
6
+ end
7
+
8
+ require_relative "fuzzing/value_generator"
9
+ require_relative "fuzzing/invalid_value_generator"
10
+ require_relative "fuzzing/sequence_generator"
11
+ require_relative "fuzzing/replay"
12
+ require_relative "fuzzing/properties"
@@ -0,0 +1,291 @@
1
+ module Hecks
2
+ module Grammar
3
+ # The file surgery under bin/evolve: reading and rewriting the
4
+ # aggregate-local KeywordSeed/ArgumentSeed rows as TEXT, so a
5
+ # proposed word enters the table exactly as a hand would write it
6
+ # and an admitted one loses its ceremony (an absent status reads as
7
+ # admitted — the grown-column convention).
8
+ #
9
+ # Text, not IR, on purpose: the syntax table is source, its comments
10
+ # and grouping are part of the declaration, and a rewrite that
11
+ # round-tripped it through the IR would flatten both. Everything
12
+ # here touches only bare `member ` lines inside Keyword's own body
13
+ # (S3, ADR 0025 — no `one_of do ... end` wrapper anymore) and leaves
14
+ # every other byte alone.
15
+ module Evolve
16
+ class Refusal < StandardError; end
17
+
18
+ module_function
19
+
20
+ def syntax_paths
21
+ Dir.glob(File.expand_path("../language/**/*.bluebook", __dir__)).select do |path|
22
+ source = File.read(path)
23
+ source.include?('value_object "KeywordSeed"') || source.include?('value_object "ArgumentSeed"')
24
+ end
25
+ end
26
+
27
+ # Kept as a narrow compatibility door for callers deliberately doing
28
+ # single-file surgery. Normal operation uses `syntax_paths` and discovers
29
+ # the owning concept from the row itself.
30
+ def syntax_path = syntax_paths.first
31
+
32
+ def paths_for(path) = path ? Array(path) : syntax_paths
33
+
34
+ # The Keyword one_of's member rows, parsed leniently off the text —
35
+ # enough to know each row's (word, context, status), which is all
36
+ # the tool ever asks.
37
+ def keyword_rows(path = nil)
38
+ paths_for(path).flat_map do |candidate|
39
+ blocks = seed_blocks(File.read(candidate), "KeywordSeed")
40
+ raise Refusal, "source declares no KeywordSeed value object" if path && blocks.empty?
41
+
42
+ blocks.flat_map do |block|
43
+ block.scan(/^\s*member (.+)$/).map do |(cells)|
44
+ row = cells.scan(/(\w+): "((?:[^"\\]|\\.)*)"/).to_h
45
+ { word: row["word"], context: row["context"],
46
+ status: row.fetch("status", "admitted"), was: row["was"] }
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def propose(word:, context:, body: "none", inner: "", opens: "", fills: "", path: nil)
53
+ raise Refusal, "#{context}.#{word} is already declared — one row per (word, context, form)" if keyword_rows(path).any? { |row| row[:word] == word && row[:context] == context }
54
+
55
+ path = owner_path(context: context, word: word, opens: opens, paths: paths_for(path))
56
+ source = File.read(path)
57
+ block = keyword_blocks(source).find { |candidate| candidate.include?(%(context: "#{context}")) } || keyword_block(source)
58
+ indent = block[/^(\s*)member /, 1] || " "
59
+ row = %(#{indent}member word: "#{word}", context: "#{context}", body: "#{body}", ) +
60
+ %(inner: "#{inner}", opens: "#{opens}", fills: "#{fills}", status: "proposed"\n)
61
+
62
+ # At the END of the one_of — grouping by context is a courtesy of
63
+ # the hand; a proposed row sits at the bottom until admission,
64
+ # when whoever admits it may move it home.
65
+ closing = block.rindex(/^\s*end\s*$/)
66
+ updated = block[0...closing] + row + block[closing..]
67
+ File.write(path, source.sub(block, updated))
68
+ end
69
+
70
+ def set_status(word:, context:, to:, path: nil)
71
+ raise Refusal, "#{to.inspect} is not a station a word's life admits" unless %w[proposed admitted deprecated retired].include?(to)
72
+
73
+ path = path_holding_keyword(word, context, paths_for(path))
74
+ source = File.read(path)
75
+ block = keyword_blocks(source).find { |candidate| candidate.lines.any? { |line| member_row?(line, word, context) } }
76
+ rows = block.lines.select { |line| member_row?(line, word, context) }
77
+ raise Refusal, "#{context}.#{word} is not declared" if rows.empty?
78
+
79
+ updated = block.lines.map do |line|
80
+ next line unless member_row?(line, word, context)
81
+
82
+ stripped = line.sub(/,\s*status: "[^"]*"/, "")
83
+ # Admitted is the default and stays UNSPELLED — only a word
84
+ # entering or leaving the language carries its status.
85
+ to == "admitted" ? stripped : stripped.sub(/\n\z/, %(, status: "#{to}"\n))
86
+ end.join
87
+
88
+ File.write(path, source.sub(block, updated))
89
+ end
90
+
91
+ # A rename respells the row's word and holds the old spelling in
92
+ # `was:` — one hop only. Renaming an already-renamed word refuses
93
+ # until the language grows real eras for its own words; renaming
94
+ # onto a spelling the context already declares refuses too. The
95
+ # word's Argument rows follow it — row-aware now, not the blind
96
+ # substitution this used to be (see `cascade_argument_rename`).
97
+ def rename(word:, context:, to:, path: nil)
98
+ row = keyword_rows(path).find { |r| r[:word] == word && r[:context] == context }
99
+ raise Refusal, "#{context}.#{word} is not declared" unless row
100
+ raise Refusal, "#{context}.#{word} was already #{row[:was]} — one rename hop, then eras" if row[:was]
101
+ raise Refusal, "#{context}.#{to} is already declared — a rename cannot land on a living word" if keyword_rows(path).any? { |r| r[:word] == to && r[:context] == context }
102
+
103
+ paths = paths_for(path)
104
+ path = path_holding_keyword(word, context, paths)
105
+ source = File.read(path)
106
+ block = keyword_blocks(source).find { |candidate| candidate.lines.any? { |line| member_row?(line, word, context) } }
107
+ updated = block.lines.map do |line|
108
+ next line unless member_row?(line, word, context)
109
+
110
+ line.sub(%(word: "#{word}"), %(word: "#{to}"))
111
+ .sub(/\n\z/, %(, was: "#{word}"\n))
112
+ end.join
113
+ source = source.sub(block, updated)
114
+ File.write(path, source)
115
+
116
+ cascade_argument_rename(keyword: word, context: context, to: to, path: paths)
117
+ end
118
+
119
+ def member_row?(line, word, context)
120
+ line =~ /^\s*member / && line.include?(%(word: "#{word}")) && line.include?(%(context: "#{context}"))
121
+ end
122
+
123
+ def path_holding_keyword(word, context, paths = syntax_paths)
124
+ paths.find do |candidate|
125
+ keyword_blocks(File.read(candidate)).any? { |block| block.lines.any? { |line| member_row?(line, word, context) } }
126
+ end || raise(Refusal, "#{context}.#{word} is not declared")
127
+ end
128
+
129
+ def path_holding_argument(keyword, context, at, named, paths = syntax_paths)
130
+ paths.find do |candidate|
131
+ argument_blocks(File.read(candidate)).any? { |block| block.lines.any? { |line| argument_row?(line, keyword, context, at, named) } }
132
+ end || raise(Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is not declared")
133
+ end
134
+
135
+ # A new row belongs wherever that context's existing rows live. File is
136
+ # intentionally wider than one aggregate; for a new entry point, `opens`
137
+ # identifies the aggregate concept whose file should own it.
138
+ def owner_path(context:, word:, opens: "", paths: syntax_paths)
139
+ if context == "File" && !opens.to_s.empty?
140
+ aggregate_path = paths.find { |candidate| File.read(candidate).match?(/^\s*aggregate "#{Regexp.escape(opens)}" do$/) }
141
+ return aggregate_path if aggregate_path
142
+ end
143
+
144
+ paths.find do |candidate|
145
+ source = File.read(candidate)
146
+ %w[KeywordSeed ArgumentSeed].any? do |seed|
147
+ seed_blocks(source, seed).any? { |block| block.include?(%(context: "#{context}")) }
148
+ end
149
+ end || raise(Refusal, "no aggregate-local syntax table owns context #{context.inspect} for #{word}")
150
+ end
151
+
152
+ def seed_blocks(source, name)
153
+ opener = /^([ \t]*)value_object "#{Regexp.escape(name)}" do$/
154
+ source.to_enum(:scan, opener).map do
155
+ match = Regexp.last_match
156
+ start = match.begin(0)
157
+ indent = match[1]
158
+ closing = source.index(/^#{Regexp.escape(indent)}end\s*$/, match.end(0))
159
+ closing = source.index(/\n/, closing) + 1
160
+ source[start...closing]
161
+ end
162
+ end
163
+
164
+ def seed_block(source, name, required: false)
165
+ block = seed_blocks(source, name).first
166
+ unless block
167
+ raise Refusal, "source declares no #{name} value object" if required
168
+
169
+ return
170
+ end
171
+ block
172
+ end
173
+
174
+ # From `value_object "KeywordSeed"` to ITS OWN closing `end` — `member`
175
+ # rows sit bare now (S3, ADR 0025 — the `one_of do ... end` wrapper
176
+ # is gone), so the first bare `end` line after the opener already
177
+ # IS the value object's own, the same fact the original one_of-
178
+ # nested version of this method leaned on (nothing else nested
179
+ # inside it either, before or after).
180
+ def keyword_blocks(source) = seed_blocks(source, "KeywordSeed")
181
+ def keyword_block(source) = seed_block(source, "KeywordSeed", required: true)
182
+
183
+ # ── the Argument rows — a word's own arguments, at last with tooling
184
+ # of their own rather than the rename-only cascade above. A word may
185
+ # carry SEVERAL argument rows (one per position, one per named
186
+ # kwarg), so identity here is the full (keyword, context, at, named)
187
+ # tuple, not the two-field key a Keyword row answers to.
188
+
189
+ def argument_rows(path = nil)
190
+ paths_for(path).flat_map do |candidate|
191
+ blocks = seed_blocks(File.read(candidate), "ArgumentSeed")
192
+ raise Refusal, "source declares no ArgumentSeed value object" if path && blocks.empty?
193
+
194
+ blocks.flat_map do |block|
195
+ block.scan(/^\s*member (.+)$/).map do |(cells)|
196
+ row = cells.scan(/(\w+): "((?:[^"\\]|\\.)*)"/).to_h
197
+ { keyword: row["keyword"], context: row["context"], at: row["at"].to_s,
198
+ named: row["named"].to_s, kind: row["kind"], required: row["required"],
199
+ fills: row["fills"].to_s, status: row.fetch("status", "admitted") }
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ # `pairs_shape` — for a `pairs` argument that fills ONE field with a
206
+ # whole key/value list rather than naming a field per pair (the
207
+ # shape `Handler.dispatch`'s own `with:` already carries). Without
208
+ # it, `spec/syntax_conformance_spec.rb` reads a pairs argument
209
+ # naming a single field as a row that "names a single field, which
210
+ # it cannot fill" — correctly, since the two shapes are genuinely
211
+ # different and only one of them can be checked the same way.
212
+ def propose_argument(keyword:, context:, kind:, required: "false", at: "", named: "", fills: "",
213
+ pairs_shape: nil, path: nil)
214
+ if argument_rows(path).any? { |r| argument_identity(r) == [keyword, context, at, named] }
215
+ raise Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is " \
216
+ "already declared — one row per (keyword, context, at, named)"
217
+ end
218
+
219
+ path = owner_path(context: context, word: keyword, paths: paths_for(path))
220
+ source = File.read(path)
221
+ block = argument_blocks(source).find { |candidate| candidate.include?(%(context: "#{context}")) } || argument_block(source)
222
+ indent = block[/^(\s*)member /, 1] || " "
223
+ shape = pairs_shape.to_s.empty? ? "" : %(pairs_shape: "#{pairs_shape}", )
224
+ row = %(#{indent}member keyword: "#{keyword}", context: "#{context}", at: "#{at}", ) +
225
+ %(named: "#{named}", kind: "#{kind}", required: "#{required}", fills: "#{fills}", ) +
226
+ shape + %(status: "proposed"\n)
227
+
228
+ closing = block.rindex(/^\s*end\s*$/)
229
+ updated = block[0...closing] + row + block[closing..]
230
+ File.write(path, source.sub(block, updated))
231
+ end
232
+
233
+ def set_argument_status(keyword:, context:, to:, at: "", named: "", path: nil)
234
+ raise Refusal, "#{to.inspect} is not a station an argument's life admits" unless %w[proposed admitted deprecated retired].include?(to)
235
+
236
+ path = path_holding_argument(keyword, context, at, named, paths_for(path))
237
+ source = File.read(path)
238
+ block = argument_blocks(source).find { |candidate| candidate.lines.any? { |line| argument_row?(line, keyword, context, at, named) } }
239
+ rows = block.lines.select { |line| argument_row?(line, keyword, context, at, named) }
240
+ raise Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is not " \
241
+ "declared" if rows.empty?
242
+
243
+ updated = block.lines.map do |line|
244
+ next line unless argument_row?(line, keyword, context, at, named)
245
+
246
+ stripped = line.sub(/,\s*status: "[^"]*"/, "")
247
+ to == "admitted" ? stripped : stripped.sub(/\n\z/, %(, status: "#{to}"\n))
248
+ end.join
249
+
250
+ File.write(path, source.sub(block, updated))
251
+ end
252
+
253
+ def argument_row?(line, keyword, context, at, named)
254
+ line =~ /^\s*member / &&
255
+ line.include?(%(keyword: "#{keyword}")) && line.include?(%(context: "#{context}")) &&
256
+ line.include?(%(at: "#{at}")) && line.include?(%(named: "#{named}"))
257
+ end
258
+
259
+ def argument_identity(row) = [row[:keyword], row[:context], row[:at], row[:named]]
260
+
261
+ # The rename cascade, ROW-AWARE — only the rows that actually belong
262
+ # to the renamed word, spelling updated in place, rather than a
263
+ # blind `gsub` on every `keyword: "word",` substring in the file
264
+ # (which a coincidentally-matching row elsewhere could have
265
+ # corrupted, and which read nothing before writing).
266
+ def cascade_argument_rename(keyword:, context:, to:, path: nil)
267
+ paths_for(path).each do |candidate|
268
+ source = File.read(candidate)
269
+ original = source
270
+ argument_blocks(source).each do |block|
271
+ updated = block.lines.map do |line|
272
+ next line unless line =~ /^\s*member / &&
273
+ line.include?(%(keyword: "#{keyword}")) && line.include?(%(context: "#{context}"))
274
+
275
+ line.sub(%(keyword: "#{keyword}"), %(keyword: "#{to}"))
276
+ end.join
277
+ source = source.sub(block, updated) if updated != block
278
+ end
279
+ File.write(candidate, source) if source != original
280
+ end
281
+ end
282
+
283
+ # From `value_object "ArgumentSeed"` to ITS OWN closing `end` — see
284
+ # `keyword_block`'s own comment for why the first bare `end` after
285
+ # the opener is already the right one, now that `member` rows sit
286
+ # bare (S3, ADR 0025).
287
+ def argument_blocks(source) = seed_blocks(source, "ArgumentSeed")
288
+ def argument_block(source) = seed_block(source, "ArgumentSeed", required: true)
289
+ end
290
+ end
291
+ end