hecks 0.2.0 → 1.0.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 (419) 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 +427 -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 +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -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 +145 -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 +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,200 @@
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
+ # PRD 05 (numeric-boundary-coverage) — Bignum (`2**100`, past i64's
45
+ # own ceiling, which Ruby's own `Integer` has no such ceiling for —
46
+ # `rust/src/kernel/json.rs`'s own `integral_i64` doc comment names
47
+ # exactly this: a Rust kernel value CANNOT represent it, so this
48
+ # exercises a real cross-runtime capability gap, not a Ruby-only
49
+ # edge) and its negative twin. Both round-trip through Ruby's own
50
+ # arithmetic/JSON cleanly (confirmed directly: `(2**100).clamp(...)`
51
+ # and `JSON.generate(2**100)` both just work — Integer has no
52
+ # ceiling here), so nothing in THIS runtime needed a fix for these;
53
+ # they're included so a real generated sequence occasionally
54
+ # produces the value at all, since nothing had, repo-wide, before.
55
+ INTEGER_EDGE_CASES = [0, -1, 2_147_483_647, -2_147_483_648, 2**100, -(2**100)].freeze
56
+ # NaN and +/-Infinity — the real find (see `spec/runtime/
57
+ # numeric_boundary_spec.rb`): `Value::Coercion#check_numeric_fields`
58
+ # used to let all three sail through untyped-checked (each really
59
+ # is a Float), reaching either `CommandRules::Arithmetic#clamp`
60
+ # (raw `ArgumentError`, not a domain refusal) or `JSON.generate`
61
+ # (`JSON::GeneratorError`, also not a domain refusal) — both fixed
62
+ # at the source now, so these are safe to generate. -0.0 is
63
+ # deliberately included too even though it was ALREADY safe
64
+ # (finite, round-trips through JSON as `-0.0` cleanly) — a signed
65
+ # zero is exactly the kind of boundary a hand-written corpus never
66
+ # happens to type, and the fuzzer existing to cover it is the
67
+ # point.
68
+ FLOAT_EDGE_CASES = [0.0, -0.0, -0.5, -100.25, Float::NAN, Float::INFINITY, -Float::INFINITY].freeze
69
+ WORDS = %w[alpha bravo charlie delta echo foxtrot golf hotel india juliet].freeze
70
+ CURRENCY_CODES = %w[USD EUR GBP JPY].freeze
71
+
72
+ # The value for one command/query/value-object attribute. `known_ids` is
73
+ # `{aggregate_or_entity_name => [scalar id, ...]}`, supplied by the
74
+ # sequence generator — a reference draws a real id from it most of the
75
+ # time and a fabricated one sometimes, to exercise NotFound as often as
76
+ # the happy path.
77
+ # `context` carries the enclosing value object's own declared name down
78
+ # into a nested primitive field — `EmailAddress`'s `address` field is
79
+ # what actually needs to look email-shaped, but the field itself is
80
+ # just named "address"; the VO's own name is where "email" lives. A
81
+ # combined hint catches both spellings without needing to guess which
82
+ # level a domain happened to name the thing on.
83
+ def value_for(attribute, aggregate, random:, known_ids: {}, context: nil)
84
+ return reference_value(attribute, random: random, known_ids: known_ids) if attribute.reference?
85
+
86
+ # Local first, falling back to a same-chapter identity value object
87
+ # declared on a sibling aggregate — the exact resolution
88
+ # `Value.value_object_for` already does for a real dispatch
89
+ # (runtime/value/coercion.rb). `safe_deposit_boxes.bluebook`'s own
90
+ # `attribute :customer, CustomerNumber` is declared on SafeDepositBox
91
+ # but CustomerNumber itself lives on Customer; without this fallback
92
+ # the generator raised "does not know primitive type" for any such
93
+ # cross-aggregate identity attribute rather than reusing the same
94
+ # lookup the runtime relies on.
95
+ value_object = Runtime::Value.value_object_for(aggregate, attribute.type.to_s)
96
+ return object_for(value_object, aggregate, random: random, known_ids: known_ids) if value_object
97
+
98
+ primitive(attribute.type.to_s, random: random, name: "#{context} #{attribute.name}")
99
+ end
100
+
101
+ def object_for(value_object, aggregate, random:, known_ids:)
102
+ fields =
103
+ if value_object.closed_set? && !value_object.members.empty?
104
+ return invalid_member(value_object, random: random) if random.rand < INVALID_MEMBER_PROBABILITY
105
+
106
+ member = value_object.members.sample(random: random)
107
+ member.to_h { |field, value| [field.to_s, value] }
108
+ else
109
+ value_object.attributes.to_h do |field|
110
+ [field.name.to_s,
111
+ value_for(field, aggregate, random: random, known_ids: known_ids, context: value_object.hecks_name)]
112
+ end
113
+ end
114
+
115
+ # `fields.size == 1` — the SAME test `Behaviour::ValueObject#
116
+ # sole_attribute` names: a genuinely single-field value object,
117
+ # not merely "this particular closed-set member happened to pick
118
+ # one field." Unwrapping ONLY here, not in `invalid_member`
119
+ # above — a deliberately-wrong combination stays a Hash so its
120
+ # own wrongness is what gets exercised, not a second, unrelated
121
+ # shape question.
122
+ return fields.values.first if fields.size == 1 && random.rand < BARE_SCALAR_PROBABILITY
123
+
124
+ fields
125
+ end
126
+
127
+ # A combination that (almost certainly) isn't one of the closed set's
128
+ # admitted rows — deliberately, to exercise the refusal a `one_of`
129
+ # exists to enforce, not just its happy path.
130
+ def invalid_member(value_object, random:)
131
+ value_object.attributes.to_h { |field|
132
+ [field.name.to_s, primitive(field.type.to_s, random: random, name: field.name.to_s)]
133
+ }
134
+ end
135
+
136
+ # THE ID ITSELF. This minted `{"value" => id}` back when a reference was
137
+ # stored wrapped ; the payload gate refuses that shape now, so a fuzzer
138
+ # still emitting it would have every generated reference refused and
139
+ # the SILENT guard would report the fuzzer broken rather than
140
+ # the runtime.
141
+ def reference_value(attribute, random:, known_ids:)
142
+ pool = known_ids[attribute.type.target_name.to_s] || []
143
+ return "missing-#{random.bytes(4).unpack1('H*')}" if pool.empty? || random.rand < INVALID_REFERENCE_PROBABILITY
144
+
145
+ pool.sample(random: random)
146
+ end
147
+
148
+ def primitive(type_name, random:, name: nil)
149
+ case type_name
150
+ when "String" then string_value(random, name: name)
151
+ when "Integer" then integer_value(random)
152
+ when "Float" then float_value(random)
153
+ when "TrueClass", "FalseClass" then random.rand(2).zero?
154
+ else raise ArgumentError, "ValueGenerator does not know primitive type #{type_name.inspect}"
155
+ end
156
+ end
157
+
158
+ def string_value(random, name: nil)
159
+ return STRING_EDGE_CASES.sample(random: random) if random.rand < EDGE_CASE_PROBABILITY
160
+ return email_value(random) if name&.match?(/email/i)
161
+ return CURRENCY_CODES.sample(random: random) if name&.match?(/currency/i)
162
+
163
+ Array.new(random.rand(1..3)) { WORDS.sample(random: random) }.join(" ")
164
+ end
165
+
166
+ def email_value(random)
167
+ "#{WORDS.sample(random: random)}@#{WORDS.sample(random: random)}.example"
168
+ end
169
+
170
+ # Skewed positive : `cents.positive?`/`!cents.negative?`-style
171
+ # invariants are common across this codebase's example domains, and a
172
+ # sequence that can never get past one never reaches the state a deeper
173
+ # bug would need. Zero and negative are still real, reachable outcomes —
174
+ # via the edge-case pool, deliberately, not by starving them entirely.
175
+ def integer_value(random)
176
+ return INTEGER_EDGE_CASES.sample(random: random) if random.rand < EDGE_CASE_PROBABILITY
177
+
178
+ random.rand(1..1000)
179
+ end
180
+
181
+ def float_value(random)
182
+ return FLOAT_EDGE_CASES.sample(random: random) if random.rand < EDGE_CASE_PROBABILITY
183
+
184
+ random.rand(0.01..1000.0).round(2)
185
+ end
186
+
187
+ # The bare scalar a generated IDENTITY value stands for, for recording into
188
+ # `known_ids`. An identity is declared as a value object, so this opens one ;
189
+ # a reference pointing at this record is already that scalar and needs no
190
+ # opening at all. The two used to be the same reading and are not any more.
191
+ def scalar_of(identity_value)
192
+ identity_value.is_a?(Hash) ? identity_value.values.first.to_s : identity_value.to_s
193
+ end
194
+
195
+ def random_id(random)
196
+ "gen-#{random.bytes(4).unpack1('H*')}"
197
+ end
198
+ end
199
+ end
200
+ 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,324 @@
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
+ # bin/evolve's own `--name value` flag reader. Only consumes the
21
+ # NEXT argv element as the value when that element doesn't itself
22
+ # look like a flag — otherwise `--foo --bar` would swallow `--bar`
23
+ # as `--foo`'s value (and `--bar` would then never be seen at
24
+ # all), and a value-less `--foo` at the end of argv would bypass
25
+ # whatever default `foo` promised instead of falling back to it.
26
+ def option(argv, name, default = nil)
27
+ index = argv.index("--#{name}")
28
+ return default unless index
29
+
30
+ value = argv[index + 1]
31
+ value.nil? || value.start_with?("-") ? default : value
32
+ end
33
+
34
+ # The snapshot/restore ceremony bin/evolve wraps every mutating
35
+ # command in: read every `paths` file, run the block, and put
36
+ # every file back exactly as it was if the block raises partway
37
+ # through — `rename`'s keyword-row write followed by its
38
+ # argument cascade across several files (`cascade_argument_rename`
39
+ # above) included, not only a declared gate failing after the
40
+ # block has already returned cleanly (bin/evolve's own `guarded`
41
+ # still handles that half on its own, since it depends on running
42
+ # the gate specs, not on anything this method knows about). A
43
+ # clean return leaves the snapshots unused; the caller decides
44
+ # from there whether the tree stands.
45
+ def restore_on_raise(paths)
46
+ snapshots = paths.to_h { |path| [path, File.read(path)] }
47
+ yield
48
+ rescue StandardError
49
+ snapshots.each { |path, content| File.write(path, content) }
50
+ raise
51
+ end
52
+
53
+ def syntax_paths
54
+ Dir.glob(File.expand_path("../language/**/*.bluebook", __dir__)).select do |path|
55
+ source = File.read(path)
56
+ source.include?('value_object "KeywordSeed"') || source.include?('value_object "ArgumentSeed"')
57
+ end
58
+ end
59
+
60
+ # Kept as a narrow compatibility door for callers deliberately doing
61
+ # single-file surgery. Normal operation uses `syntax_paths` and discovers
62
+ # the owning concept from the row itself.
63
+ def syntax_path = syntax_paths.first
64
+
65
+ def paths_for(path) = path ? Array(path) : syntax_paths
66
+
67
+ # The Keyword one_of's member rows, parsed leniently off the text —
68
+ # enough to know each row's (word, context, status), which is all
69
+ # the tool ever asks.
70
+ def keyword_rows(path = nil)
71
+ paths_for(path).flat_map do |candidate|
72
+ blocks = seed_blocks(File.read(candidate), "KeywordSeed")
73
+ raise Refusal, "source declares no KeywordSeed value object" if path && blocks.empty?
74
+
75
+ blocks.flat_map do |block|
76
+ block.scan(/^\s*member (.+)$/).map do |(cells)|
77
+ row = cells.scan(/(\w+): "((?:[^"\\]|\\.)*)"/).to_h
78
+ { word: row["word"], context: row["context"],
79
+ status: row.fetch("status", "admitted"), was: row["was"] }
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ def propose(word:, context:, body: "none", inner: "", opens: "", fills: "", path: nil)
86
+ 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 }
87
+
88
+ path = owner_path(context: context, word: word, opens: opens, paths: paths_for(path))
89
+ source = File.read(path)
90
+ block = keyword_blocks(source).find { |candidate| candidate.include?(%(context: "#{context}")) } || keyword_block(source)
91
+ indent = block[/^(\s*)member /, 1] || " "
92
+ row = %(#{indent}member word: "#{word}", context: "#{context}", body: "#{body}", ) +
93
+ %(inner: "#{inner}", opens: "#{opens}", fills: "#{fills}", status: "proposed"\n)
94
+
95
+ # At the END of the one_of — grouping by context is a courtesy of
96
+ # the hand; a proposed row sits at the bottom until admission,
97
+ # when whoever admits it may move it home.
98
+ closing = block.rindex(/^\s*end\s*$/)
99
+ updated = block[0...closing] + row + block[closing..]
100
+ File.write(path, source.sub(block, updated))
101
+ end
102
+
103
+ def set_status(word:, context:, to:, path: nil)
104
+ raise Refusal, "#{to.inspect} is not a station a word's life admits" unless %w[proposed admitted deprecated retired].include?(to)
105
+
106
+ path = path_holding_keyword(word, context, paths_for(path))
107
+ source = File.read(path)
108
+ block = keyword_blocks(source).find { |candidate| candidate.lines.any? { |line| member_row?(line, word, context) } }
109
+ rows = block.lines.select { |line| member_row?(line, word, context) }
110
+ raise Refusal, "#{context}.#{word} is not declared" if rows.empty?
111
+
112
+ updated = block.lines.map do |line|
113
+ next line unless member_row?(line, word, context)
114
+
115
+ stripped = line.sub(/,\s*status: "[^"]*"/, "")
116
+ # Admitted is the default and stays UNSPELLED — only a word
117
+ # entering or leaving the language carries its status.
118
+ to == "admitted" ? stripped : stripped.sub(/\n\z/, %(, status: "#{to}"\n))
119
+ end.join
120
+
121
+ File.write(path, source.sub(block, updated))
122
+ end
123
+
124
+ # A rename respells the row's word and holds the old spelling in
125
+ # `was:` — one hop only. Renaming an already-renamed word refuses
126
+ # until the language grows real eras for its own words; renaming
127
+ # onto a spelling the context already declares refuses too. The
128
+ # word's Argument rows follow it — row-aware now, not the blind
129
+ # substitution this used to be (see `cascade_argument_rename`).
130
+ def rename(word:, context:, to:, path: nil)
131
+ row = keyword_rows(path).find { |r| r[:word] == word && r[:context] == context }
132
+ raise Refusal, "#{context}.#{word} is not declared" unless row
133
+ raise Refusal, "#{context}.#{word} was already #{row[:was]} — one rename hop, then eras" if row[:was]
134
+ 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 }
135
+
136
+ paths = paths_for(path)
137
+ path = path_holding_keyword(word, context, paths)
138
+ source = File.read(path)
139
+ block = keyword_blocks(source).find { |candidate| candidate.lines.any? { |line| member_row?(line, word, context) } }
140
+ updated = block.lines.map do |line|
141
+ next line unless member_row?(line, word, context)
142
+
143
+ line.sub(%(word: "#{word}"), %(word: "#{to}"))
144
+ .sub(/\n\z/, %(, was: "#{word}"\n))
145
+ end.join
146
+ source = source.sub(block, updated)
147
+ File.write(path, source)
148
+
149
+ cascade_argument_rename(keyword: word, context: context, to: to, path: paths)
150
+ end
151
+
152
+ def member_row?(line, word, context)
153
+ line =~ /^\s*member / && line.include?(%(word: "#{word}")) && line.include?(%(context: "#{context}"))
154
+ end
155
+
156
+ def path_holding_keyword(word, context, paths = syntax_paths)
157
+ paths.find do |candidate|
158
+ keyword_blocks(File.read(candidate)).any? { |block| block.lines.any? { |line| member_row?(line, word, context) } }
159
+ end || raise(Refusal, "#{context}.#{word} is not declared")
160
+ end
161
+
162
+ def path_holding_argument(keyword, context, at, named, paths = syntax_paths)
163
+ paths.find do |candidate|
164
+ argument_blocks(File.read(candidate)).any? { |block| block.lines.any? { |line| argument_row?(line, keyword, context, at, named) } }
165
+ end || raise(Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is not declared")
166
+ end
167
+
168
+ # A new row belongs wherever that context's existing rows live. File is
169
+ # intentionally wider than one aggregate; for a new entry point, `opens`
170
+ # identifies the aggregate concept whose file should own it.
171
+ def owner_path(context:, word:, opens: "", paths: syntax_paths)
172
+ if context == "File" && !opens.to_s.empty?
173
+ aggregate_path = paths.find { |candidate| File.read(candidate).match?(/^\s*aggregate "#{Regexp.escape(opens)}" do$/) }
174
+ return aggregate_path if aggregate_path
175
+ end
176
+
177
+ paths.find do |candidate|
178
+ source = File.read(candidate)
179
+ %w[KeywordSeed ArgumentSeed].any? do |seed|
180
+ seed_blocks(source, seed).any? { |block| block.include?(%(context: "#{context}")) }
181
+ end
182
+ end || raise(Refusal, "no aggregate-local syntax table owns context #{context.inspect} for #{word}")
183
+ end
184
+
185
+ def seed_blocks(source, name)
186
+ opener = /^([ \t]*)value_object "#{Regexp.escape(name)}" do$/
187
+ source.to_enum(:scan, opener).map do
188
+ match = Regexp.last_match
189
+ start = match.begin(0)
190
+ indent = match[1]
191
+ closing = source.index(/^#{Regexp.escape(indent)}end\s*$/, match.end(0))
192
+ closing = source.index(/\n/, closing) + 1
193
+ source[start...closing]
194
+ end
195
+ end
196
+
197
+ def seed_block(source, name, required: false)
198
+ block = seed_blocks(source, name).first
199
+ unless block
200
+ raise Refusal, "source declares no #{name} value object" if required
201
+
202
+ return
203
+ end
204
+ block
205
+ end
206
+
207
+ # From `value_object "KeywordSeed"` to ITS OWN closing `end` — `member`
208
+ # rows sit bare now (S3, ADR 0025 — the `one_of do ... end` wrapper
209
+ # is gone), so the first bare `end` line after the opener already
210
+ # IS the value object's own, the same fact the original one_of-
211
+ # nested version of this method leaned on (nothing else nested
212
+ # inside it either, before or after).
213
+ def keyword_blocks(source) = seed_blocks(source, "KeywordSeed")
214
+ def keyword_block(source) = seed_block(source, "KeywordSeed", required: true)
215
+
216
+ # ── the Argument rows — a word's own arguments, at last with tooling
217
+ # of their own rather than the rename-only cascade above. A word may
218
+ # carry SEVERAL argument rows (one per position, one per named
219
+ # kwarg), so identity here is the full (keyword, context, at, named)
220
+ # tuple, not the two-field key a Keyword row answers to.
221
+
222
+ def argument_rows(path = nil)
223
+ paths_for(path).flat_map do |candidate|
224
+ blocks = seed_blocks(File.read(candidate), "ArgumentSeed")
225
+ raise Refusal, "source declares no ArgumentSeed value object" if path && blocks.empty?
226
+
227
+ blocks.flat_map do |block|
228
+ block.scan(/^\s*member (.+)$/).map do |(cells)|
229
+ row = cells.scan(/(\w+): "((?:[^"\\]|\\.)*)"/).to_h
230
+ { keyword: row["keyword"], context: row["context"], at: row["at"].to_s,
231
+ named: row["named"].to_s, kind: row["kind"], required: row["required"],
232
+ fills: row["fills"].to_s, status: row.fetch("status", "admitted") }
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ # `pairs_shape` — for a `pairs` argument that fills ONE field with a
239
+ # whole key/value list rather than naming a field per pair (the
240
+ # shape `Handler.dispatch`'s own `with:` already carries). Without
241
+ # it, `spec/syntax_conformance_spec.rb` reads a pairs argument
242
+ # naming a single field as a row that "names a single field, which
243
+ # it cannot fill" — correctly, since the two shapes are genuinely
244
+ # different and only one of them can be checked the same way.
245
+ def propose_argument(keyword:, context:, kind:, required: "false", at: "", named: "", fills: "",
246
+ pairs_shape: nil, path: nil)
247
+ if argument_rows(path).any? { |r| argument_identity(r) == [keyword, context, at, named] }
248
+ raise Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is " \
249
+ "already declared — one row per (keyword, context, at, named)"
250
+ end
251
+
252
+ path = owner_path(context: context, word: keyword, paths: paths_for(path))
253
+ source = File.read(path)
254
+ block = argument_blocks(source).find { |candidate| candidate.include?(%(context: "#{context}")) } || argument_block(source)
255
+ indent = block[/^(\s*)member /, 1] || " "
256
+ shape = pairs_shape.to_s.empty? ? "" : %(pairs_shape: "#{pairs_shape}", )
257
+ row = %(#{indent}member keyword: "#{keyword}", context: "#{context}", at: "#{at}", ) +
258
+ %(named: "#{named}", kind: "#{kind}", required: "#{required}", fills: "#{fills}", ) +
259
+ shape + %(status: "proposed"\n)
260
+
261
+ closing = block.rindex(/^\s*end\s*$/)
262
+ updated = block[0...closing] + row + block[closing..]
263
+ File.write(path, source.sub(block, updated))
264
+ end
265
+
266
+ def set_argument_status(keyword:, context:, to:, at: "", named: "", path: nil)
267
+ raise Refusal, "#{to.inspect} is not a station an argument's life admits" unless %w[proposed admitted deprecated retired].include?(to)
268
+
269
+ path = path_holding_argument(keyword, context, at, named, paths_for(path))
270
+ source = File.read(path)
271
+ block = argument_blocks(source).find { |candidate| candidate.lines.any? { |line| argument_row?(line, keyword, context, at, named) } }
272
+ rows = block.lines.select { |line| argument_row?(line, keyword, context, at, named) }
273
+ raise Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is not " \
274
+ "declared" if rows.empty?
275
+
276
+ updated = block.lines.map do |line|
277
+ next line unless argument_row?(line, keyword, context, at, named)
278
+
279
+ stripped = line.sub(/,\s*status: "[^"]*"/, "")
280
+ to == "admitted" ? stripped : stripped.sub(/\n\z/, %(, status: "#{to}"\n))
281
+ end.join
282
+
283
+ File.write(path, source.sub(block, updated))
284
+ end
285
+
286
+ def argument_row?(line, keyword, context, at, named)
287
+ line =~ /^\s*member / &&
288
+ line.include?(%(keyword: "#{keyword}")) && line.include?(%(context: "#{context}")) &&
289
+ line.include?(%(at: "#{at}")) && line.include?(%(named: "#{named}"))
290
+ end
291
+
292
+ def argument_identity(row) = [row[:keyword], row[:context], row[:at], row[:named]]
293
+
294
+ # The rename cascade, ROW-AWARE — only the rows that actually belong
295
+ # to the renamed word, spelling updated in place, rather than a
296
+ # blind `gsub` on every `keyword: "word",` substring in the file
297
+ # (which a coincidentally-matching row elsewhere could have
298
+ # corrupted, and which read nothing before writing).
299
+ def cascade_argument_rename(keyword:, context:, to:, path: nil)
300
+ paths_for(path).each do |candidate|
301
+ source = File.read(candidate)
302
+ original = source
303
+ argument_blocks(source).each do |block|
304
+ updated = block.lines.map do |line|
305
+ next line unless line =~ /^\s*member / &&
306
+ line.include?(%(keyword: "#{keyword}")) && line.include?(%(context: "#{context}"))
307
+
308
+ line.sub(%(keyword: "#{keyword}"), %(keyword: "#{to}"))
309
+ end.join
310
+ source = source.sub(block, updated) if updated != block
311
+ end
312
+ File.write(candidate, source) if source != original
313
+ end
314
+ end
315
+
316
+ # From `value_object "ArgumentSeed"` to ITS OWN closing `end` — see
317
+ # `keyword_block`'s own comment for why the first bare `end` after
318
+ # the opener is already the right one, now that `member` rows sit
319
+ # bare (S3, ADR 0025).
320
+ def argument_blocks(source) = seed_blocks(source, "ArgumentSeed")
321
+ def argument_block(source) = seed_block(source, "ArgumentSeed", required: true)
322
+ end
323
+ end
324
+ end