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,445 @@
1
+ require "set"
2
+
3
+ module Hecks
4
+ module Bluebook
5
+ # Lightweight formal methods over the IR — the same family as TLA+/
6
+ # Alloy/P: every lifecycle is a declared finite state machine and
7
+ # every process manager a declared protocol, and BOTH are already
8
+ # data, not code, so they can be MODEL-CHECKED rather than merely
9
+ # executed. Static analysis only — no bluebook boots twice, no
10
+ # runtime is touched — over what meta_validator/judge.rb and the
11
+ # builders' own `validate!` methods leave uncovered (an undeclared
12
+ # transition target, a dispatch to nowhere, a compensation nothing
13
+ # can ever reach).
14
+ #
15
+ # THE RARE PROPERTY THIS RESTS ON: the model IS the implementation.
16
+ # A checker over TLA+ verifies a SPEC a human keeps in sync with
17
+ # code by hand ; this verifies the same IR the runtime dispatches
18
+ # against, so there is no second copy to drift.
19
+ module ModelCheck
20
+ Finding = Struct.new(:kind, :severity, :subject, :message, keyword_init: true) do
21
+ def to_s = "#{severity.to_s.upcase.ljust(7)} #{kind.to_s.ljust(20)} #{subject} — #{message}"
22
+ end
23
+
24
+ # A FINDING SHIPPED, NOT SILENCED — the coverage-gate idiom, empty
25
+ # allowlists enforced BOTH directions (spec/model_check_spec.rb holds
26
+ # this exact table: an error the checker reports and this does not
27
+ # name is a regression, an entry the checker no longer reports is
28
+ # stale and must be deleted). bin/model_check reads this same
29
+ # constant, so the tool and the spec can never drift apart.
30
+ #
31
+ # "banking"/ExternalSettlement — found on the first real run:
32
+ # ExternalSettlement declares `ends_on "ExternalTransferSent"`, and
33
+ # ExternalTransfer.Send genuinely emits it — the event is real, and
34
+ # the AGGREGATE reaches "sent" (its own, separate lifecycle) — but
35
+ # the SAGA'S protocol has no `on "ExternalTransferSent"` handler, so
36
+ # its own `state "sent"` is unreachable through the chain the
37
+ # checker walks, and the saga's own bookkeeping (saga_log, ends_on)
38
+ # never closes it. Real domain activity is unaffected; the saga's
39
+ # OWN tracking of it is not. Left named rather than redesigning a
40
+ # corpus fixture that is not this checker's to redesign.
41
+ # S7, ADR 0025 — the ExternalSettlement finding this used to
42
+ # allowlist is GONE, not just quieted: its "sent" state was a
43
+ # `state "x"` line never named by any handler's own from:/to:, a
44
+ # pure declaration-drift artifact. States are DERIVED from the
45
+ # transitions that name them now (ProcessManagerBuilder#derived_
46
+ # states), so a state nothing ever transitions into or out of no
47
+ # longer exists to be unreachable — the finding this allowlisted
48
+ # cannot occur any more, by construction.
49
+ #
50
+ # "banking"/NotifyOnClosure, FlagKeyReturn — real, confirmed
51
+ # findings, not bugs to fix. `across "Notifications"` names a
52
+ # domain that does not exist anywhere in this repo — no
53
+ # Notifications bluebook, no hecksagon, nothing to `uses_framework`
54
+ # or `subscribe` to. This is deliberate: `spec/runtime/policy_spec.
55
+ # rb` (a test literally named "records a reaction it cannot
56
+ # deliver rather than swallowing it") and `lib/hecks/runtime/
57
+ # errors.rb`'s own `UnknownVerb` comment both treat "target domain
58
+ # not loaded" as the EXPECTED outcome for it — Notifications is
59
+ # used on purpose to exercise the undelivered-reaction runtime
60
+ # path, not left half-built. There is no real `subscribe` line to
61
+ # add (no event of Notifications' own to name) and no real domain
62
+ # to point `uses_framework` at.
63
+ ALLOWED_FINDINGS = {
64
+ "banking" => [
65
+ [:unacknowledged_relationship, "NotifyOnClosure"],
66
+ [:unknown_target_domain, "NotifyOnClosure"],
67
+ [:unacknowledged_relationship, "FlagKeyReturn"],
68
+ [:unknown_target_domain, "FlagKeyReturn"]
69
+ ]
70
+ }.freeze
71
+
72
+ module_function
73
+
74
+ # `hecksagon:`/`known_domains:` — both optional, both `nil`-safe
75
+ # (every existing caller with no sibling hecksagon, or checking one
76
+ # domain in isolation, behaves exactly as before). `hecksagon` is
77
+ # THIS bluebook's own sibling wiring file, if the caller loaded one
78
+ # (see `emitted_events`'s own comment on why a caller that didn't
79
+ # simply finds none, correctly). `known_domains` is the caller's
80
+ # OWN corpus-wide view — every bluebook/hecksagon name it has
81
+ # booted anywhere, across every domain it has looked at, not just
82
+ # this one — used only to catch a typo'd `across`/`uses_framework`
83
+ # target; see `cross_domain_policy_findings`'s own comment for why
84
+ # this can only ever be a corpus-scoped heuristic, never a general
85
+ # correctness guarantee.
86
+ def call(bluebook, hecksagon: nil, known_domains: nil)
87
+ findings = []
88
+ bluebook.aggregates.each do |aggregate|
89
+ findings.concat(lifecycle_findings(aggregate, aggregate))
90
+ aggregate.entities.each { |entity| findings.concat(lifecycle_findings(aggregate, entity)) }
91
+ end
92
+ bluebook.process_managers.each { |pm| findings.concat(saga_findings(bluebook, pm)) }
93
+ bluebook.policies.each { |policy| findings.concat(policy_findings(bluebook, policy, hecksagon, known_domains)) }
94
+ findings
95
+ end
96
+
97
+ # ── lifecycles (aggregate AND entity — a piece may declare one too) ──
98
+
99
+ def lifecycle_findings(aggregate, declaring)
100
+ lifecycle = declaring.lifecycle
101
+ return [] unless lifecycle
102
+
103
+ subject = declaring.equal?(aggregate) ? aggregate.hecks_name : "#{aggregate.hecks_name}::#{declaring.hecks_name}"
104
+ commands = Array(declaring.commands).map(&:hecks_name)
105
+
106
+ findings = []
107
+ findings.concat(unknown_transition_commands(lifecycle, commands, subject))
108
+
109
+ full = full_states(lifecycle)
110
+ reached = reachable_states(lifecycle)
111
+
112
+ (full - reached.to_a).each do |state|
113
+ findings << Finding.new(kind: :unreachable_state, severity: :error, subject: subject,
114
+ message: "#{state.inspect} is declared (in a transition's from: or target) " \
115
+ "but no path from #{lifecycle.default.inspect} ever reaches it")
116
+ end
117
+
118
+ lifecycle.transitions.each do |command, transition|
119
+ next unless transition.constrained?
120
+ next if Array(transition.from).any? { |source| reached.include?(source) }
121
+
122
+ findings << Finding.new(kind: :dead_transition, severity: :error, subject: subject,
123
+ message: "#{command} from #{Array(transition.from).inspect} can never fire — " \
124
+ "none of those states is ever reached")
125
+ end
126
+
127
+ any_unconstrained = lifecycle.transitions.any? { |_, t| !t.constrained? }
128
+ (reached - terminal_exempt(lifecycle)).each do |state|
129
+ next if any_unconstrained
130
+ next if lifecycle.transitions.any? { |_, t| t.constrained? && Array(t.from).include?(state) }
131
+
132
+ findings << Finding.new(kind: :stuck_state, severity: :warning, subject: subject,
133
+ message: "#{state.inspect} is reached but no transition ever leaves it — " \
134
+ "fine if that is meant to be terminal")
135
+ end
136
+
137
+ findings
138
+ end
139
+
140
+ def unknown_transition_commands(lifecycle, commands, subject)
141
+ lifecycle.transitions.filter_map do |command, _transition|
142
+ next if commands.include?(command)
143
+
144
+ Finding.new(kind: :unknown_command, severity: :error, subject: subject,
145
+ message: "a transition names #{command.inspect}, which this construct declares no command for")
146
+ end
147
+ end
148
+
149
+ # default, every declared target, and every declared from — a
150
+ # from-only state (declared nowhere as a target) is real and is
151
+ # exactly the hole `Lifecycle#states` leaves: it answers default
152
+ # plus targets only.
153
+ def full_states(lifecycle)
154
+ (
155
+ [lifecycle.default] +
156
+ lifecycle.transitions.map { |_, t| t.target } +
157
+ lifecycle.transitions.flat_map { |_, t| Array(t.from) }
158
+ ).uniq
159
+ end
160
+
161
+ # Least fixpoint from the default state: an UNCONSTRAINED
162
+ # transition always fires, from wherever the machine is ; a
163
+ # constrained one fires once any of its named sources is reached.
164
+ def reachable_states(lifecycle)
165
+ reached = Set.new([lifecycle.default])
166
+ loop do
167
+ grown = false
168
+ lifecycle.transitions.each do |_, transition|
169
+ next if reached.include?(transition.target)
170
+ next if transition.constrained? && Array(transition.from).none? { |source| reached.include?(source) }
171
+
172
+ reached << transition.target
173
+ grown = true
174
+ end
175
+ break unless grown
176
+ end
177
+ reached
178
+ end
179
+
180
+ # A state with no OUTGOING declared path at all is exempt from the
181
+ # stuck-state WARNING for a different reason than "it fires an
182
+ # unconstrained transition" — the default state of a lifecycle
183
+ # with only constrained transitions is legitimately allowed to sit
184
+ # forever, since nothing about *entering* it via default implies
185
+ # anything must eventually move it, unlike a state a transition
186
+ # explicitly delivered somewhere.
187
+ def terminal_exempt(_lifecycle) = []
188
+
189
+ # ── process managers / sagas ──────────────────────────────────────
190
+
191
+ def saga_findings(bluebook, pm)
192
+ findings = []
193
+ emitted = emitted_events(bluebook)
194
+ verbs = verbs_of(bluebook)
195
+
196
+ [pm.starts_on, pm.ends_on].compact.each do |event|
197
+ next if emitted.include?(bare(event))
198
+
199
+ findings << Finding.new(kind: :deaf_trigger, severity: :error, subject: pm.name,
200
+ message: "starts_on/ends_on names #{event.inspect}, which no command in this " \
201
+ "domain emits")
202
+ end
203
+
204
+ reached = pm_reachable_states(pm, emitted)
205
+ (Array(pm.states) - reached.to_a).each do |state|
206
+ findings << Finding.new(kind: :unreachable_pm_state, severity: :error, subject: pm.name,
207
+ message: "#{state.inspect} is declared but no handler chain from " \
208
+ "#{pm.states.first.inspect} ever reaches it")
209
+ end
210
+
211
+ pm.handlers.each do |handler|
212
+ # The compensating leg answers REFUSED, a synthetic trigger no
213
+ # command ever emits by name (ProcessManager::REFUSED) — not
214
+ # a deaf handler, the one handler this domain's own events can
215
+ # never satisfy on purpose.
216
+ if handler.event_type != ProcessManager::REFUSED && !emitted.include?(bare(handler.event_type))
217
+ findings << Finding.new(kind: :deaf_handler, severity: :error, subject: pm.name,
218
+ message: "a handler answers #{handler.event_type.inspect}, which no command " \
219
+ "in this domain emits")
220
+ end
221
+
222
+ handler.dispatches.each do |dispatch|
223
+ # SAME-DOMAIN, same as `SagaInterpreter#qualified` — a dispatch
224
+ # naming no domain at all (the ordinary shape a bare command
225
+ # constant now produces, S6) means THIS one, and is compared
226
+ # against `verbs_of`'s own fully-qualified spelling qualified
227
+ # the identical way, not left bare to miss it on a technicality.
228
+ qualified = dispatch.command_name.include?("::") ? dispatch.command_name : "#{bluebook.name}::#{dispatch.command_name}"
229
+ next if verbs.include?(qualified)
230
+
231
+ findings << Finding.new(kind: :unknown_dispatch, severity: :error, subject: pm.name,
232
+ message: "dispatches #{dispatch.command_name.inspect}, which this domain " \
233
+ "declares no command at — cross-domain dispatch is out of this " \
234
+ "checker's scope, same as CommandRules#resolve_references")
235
+ end
236
+
237
+ # A `compensates` DECLARED WITH NOWHERE TO EVER FIRE — the exact
238
+ # shape of the real bug this whole feature closes ("the
239
+ # reversal was written and never armed"), caught at build/
240
+ # model-check time instead of discovered in production. No
241
+ # handler anywhere answers REFUSED (`pm.saga?` false) means
242
+ # `SagaInterpreter#unwind` never runs for this process
243
+ # manager at all, so a declared `compensates` is structurally
244
+ # unreachable — not a warning about style, a dead declaration.
245
+ if !pm.saga? && handler.dispatches.any?(&:compensates)
246
+ handler.dispatches.select(&:compensates).each do |dispatch|
247
+ findings << Finding.new(kind: :unarmed_compensation, severity: :error, subject: pm.name,
248
+ message: "#{dispatch.command_name} compensates #{dispatch.compensates.command_name}, " \
249
+ "but no handler anywhere in this saga answers a refusal — the " \
250
+ "compensation is declared and can never fire")
251
+ end
252
+ end
253
+ end
254
+
255
+ if pm.saga? && !reached.include?(pm.saga.from_state)
256
+ findings << Finding.new(kind: :dead_compensation, severity: :error, subject: pm.name,
257
+ message: "the compensation leaves #{pm.saga.from_state.inspect}, which no " \
258
+ "handler chain ever reaches — a refusal here can never fire it")
259
+ end
260
+
261
+ findings
262
+ end
263
+
264
+ # A handler edge is only usable in the closure if it can actually
265
+ # FIRE — REFUSED always can (it is a compensation trigger, not an
266
+ # event), and any other handler needs its event genuinely emitted.
267
+ # Without this, a deaf handler's declared from_state -> to_state
268
+ # pair reads as connected even though nothing can ever traverse
269
+ # it, which would hide exactly the states this walk exists to
270
+ # catch (a state only "reachable" through a handler that itself
271
+ # never fires).
272
+ def pm_reachable_states(pm, emitted)
273
+ return Set.new if Array(pm.states).empty?
274
+
275
+ reached = Set.new([pm.states.first])
276
+ loop do
277
+ grown = false
278
+ pm.handlers.each do |handler|
279
+ next unless handler.event_type == ProcessManager::REFUSED || emitted.include?(bare(handler.event_type))
280
+ next unless reached.include?(handler.from_state)
281
+ next if reached.include?(handler.to_state)
282
+
283
+ reached << handler.to_state
284
+ grown = true
285
+ end
286
+ break unless grown
287
+ end
288
+ reached
289
+ end
290
+
291
+ # ── policies ───────────────────────────────────────────────────────
292
+
293
+ def policy_findings(bluebook, policy, hecksagon, known_domains)
294
+ return cross_domain_policy_findings(policy, hecksagon, known_domains) if policy.target_domain
295
+
296
+ emitted = emitted_events(bluebook)
297
+ findings = []
298
+
299
+ # `policy.event_name` (Naming.unqualified) — NOT `bare`, which only
300
+ # strips a "::" domain qualifier. An AGGREGATE-scoped policy's
301
+ # `on_event` carries a "." aggregate qualifier instead (PolicyBuilder
302
+ # stores whatever was typed, verbatim — see `on "Account.
303
+ # AccountFrozen"`), and `bare` left it untouched, silently comparing
304
+ # "Account.AccountFrozen" against a list of bare emitted names that
305
+ # can never contain it. Latent until now : banking's one aggregate-
306
+ # scoped same-domain policy check would have caught it, but its only
307
+ # prior aggregate-scoped policy (ReviewOnFreeze) is also cross-domain
308
+ # (`across "Compliance"`), which exits this method one line above
309
+ # before the mismatch is ever reached.
310
+ unless emitted.include?(policy.event_name)
311
+ findings << Finding.new(kind: :deaf_policy, severity: :error, subject: policy.name,
312
+ message: "on #{policy.on_event.inspect}, which no command in this domain emits")
313
+ end
314
+
315
+ # `trigger` is spelled "Aggregate.Command" (or "Entity.Command" one
316
+ # level down), completed to an FQN by PolicyInterpreter#deliver as
317
+ # "#{domain}::#{trigger_command}" — the same join `verbs_of` builds
318
+ # independently, so the two spellings have to be compared as FQNs,
319
+ # never as bare command names.
320
+ unless verbs_of(bluebook).include?("#{bluebook.name}::#{policy.trigger_command}")
321
+ findings << Finding.new(kind: :unknown_trigger, severity: :error, subject: policy.name,
322
+ message: "trigger #{policy.trigger_command.inspect} resolves to no command " \
323
+ "this domain declares")
324
+ end
325
+
326
+ findings
327
+ end
328
+
329
+ # ── cross-domain policies (Context Mapping) ───────────────────────
330
+ #
331
+ # `uses_framework "X"` already IS a Shared Kernel relationship — it
332
+ # merges X's own bluebook into THIS registry, no boundary. A cross-
333
+ # domain `policy ... across: "X"` already IS a Customer/Supplier
334
+ # relationship — it dispatches into X over real cross-Lambda RPC in
335
+ # the Rust host (`rust/host/src/lambda_client.rs`). Neither is a new
336
+ # word; this makes the CHOICE between them checked instead of a
337
+ # prose comment nobody enforces (`examples/banking/bluebook/
338
+ # banking.hecksagon`'s own hand-written note explaining why
339
+ # Compliance is reached via `across`, never `uses_framework`).
340
+ #
341
+ # NO NEW KEYWORD ANYWHERE — ADR 0025 principle 1 ("one idea, one
342
+ # spelling") refuses a `relationship:`/`as:` argument that would
343
+ # just restate, as a string, the fact the chosen keyword (
344
+ # `uses_framework` vs `across`) already states completely. The
345
+ # DDD vocabulary (Shared Kernel, Customer/Supplier) lives here, in
346
+ # the finding's own name and this comment, and in prose docs — not
347
+ # in the grammar.
348
+ def cross_domain_policy_findings(policy, hecksagon, known_domains)
349
+ return [] unless hecksagon # no sibling hecksagon loaded — nothing to check a relationship against.
350
+
351
+ target = policy.target_domain
352
+ findings = []
353
+
354
+ if hecksagon.framework_members.include?(target)
355
+ # SHARED KERNEL AND CUSTOMER/SUPPLIER ARE MUTUALLY EXCLUSIVE
356
+ # CLAIMS about the SAME target — `uses_framework` means "X is
357
+ # loaded in-process, right here"; `across` means "X is a
358
+ # separate deployment, reached only by RPC." Declaring both is
359
+ # either a pointless RPC to a domain already local, or a
360
+ # `uses_framework` that isn't really doing what its name says.
361
+ findings << Finding.new(kind: :contradictory_relationship, severity: :error, subject: policy.name,
362
+ message: "across #{target.inspect} dispatches over RPC (Customer/Supplier), " \
363
+ "but this hecksagon also uses_framework #{target.inspect} (Shared " \
364
+ "Kernel) — #{target} is already loaded in-process here, so the two " \
365
+ "relationship declarations contradict each other for the same " \
366
+ "target domain")
367
+ elsif hecksagon.subscriptions.none? { |subscribed| Naming.qualifier(subscribed) == target }
368
+ # THIS IS WHAT FINALLY GIVES `subscribe` REAL TEETH — checked
369
+ # here, at model-check time, still never routed at runtime
370
+ # (nothing dispatches off a `subscribe` line; see hecksagon.md's
371
+ # own "checked, not routed" section). ADR 0025 names `subscribe`
372
+ # by number as failing the corpus-use bar; this is the real use.
373
+ findings << Finding.new(kind: :unacknowledged_relationship, severity: :error, subject: policy.name,
374
+ message: "across #{target.inspect} declares a Customer/Supplier " \
375
+ "relationship, but nothing in this hecksagon records the " \
376
+ "expectation — add subscribe \"#{target}.SomeEvent\" for what " \
377
+ "you expect back from it, or uses_framework #{target.inspect} to " \
378
+ "attach it in-process instead")
379
+ end
380
+
381
+ # TYPO DETECTION, DELIBERATELY WEAKER — `known_domains` can only
382
+ # ever be a MONOREPO-SCOPED heuristic: a real external hecks
383
+ # consumer's own domain (this repo's own embryonaut/lifeadelics-
384
+ # shaped case) lives in a genuinely separate repository this
385
+ # corpus scan can never see, so a target this check cannot find
386
+ # is "unknown to THIS corpus," never proof of a typo. Two real,
387
+ # legitimate reasons a target is unresolvable — genuinely
388
+ # undefined by design (the corpus's own "Notifications," used
389
+ # deliberately to exercise the undelivered-reaction runtime path)
390
+ # and real-but-external (a separate repository) — both go in
391
+ # `ALLOWED_FINDINGS`, the same judged-exception mechanism this
392
+ # file already uses for ExternalSettlement, rather than a new
393
+ # keyword invented to declare "this one's fine."
394
+ if known_domains && !known_domains.include?(target)
395
+ findings << Finding.new(kind: :unknown_target_domain, severity: :error, subject: policy.name,
396
+ message: "across #{target.inspect} names a domain nowhere in the corpus " \
397
+ "this check has booted — a typo, or a real domain intentionally " \
398
+ "outside this corpus (undefined by design, or living in a " \
399
+ "separate repository) belongs in ALLOWED_FINDINGS, named and " \
400
+ "explained, not silently assumed correct")
401
+ end
402
+
403
+ findings
404
+ end
405
+
406
+ # ── shared enumeration ────────────────────────────────────────────
407
+
408
+ # A PORT OPERATION EMITS TOO — the primary/driving port an adapter
409
+ # outside the bluebook calls through (see hecksagon_builder.rb) is a
410
+ # second, real source of events, alongside a command's own `emits`.
411
+ # Ports attach to the aggregate/bluebook from the SIBLING `.hecksagon`
412
+ # file, not this one — a caller that boots only the `.bluebook` (as
413
+ # the fixtures under spec/fixtures/model_check/ do, having no
414
+ # hecksagon at all) simply finds none, which is correct : nothing
415
+ # can be deaf to an event that isn't even wired up yet.
416
+ def emitted_events(bluebook)
417
+ aggregate_emits = bluebook.aggregates.flat_map do |aggregate|
418
+ aggregate.commands.map(&:emits) +
419
+ aggregate.entities.flat_map { |entity| entity.commands.map(&:emits) } +
420
+ aggregate.ports.flat_map { |port| port.operations.map(&:emits) }
421
+ end
422
+ chapter_emits = bluebook.ports.flat_map { |port| port.operations.map(&:emits) }
423
+
424
+ (aggregate_emits + chapter_emits).flatten.uniq
425
+ end
426
+
427
+ # Fully-qualified, the same spelling DispatchSpec#command_name
428
+ # carries and fuzzing/sequence_generator/catalog.rb builds
429
+ # independently for the same reason: a saga dispatch and a fuzzer
430
+ # step both have to name a verb the same way the door does.
431
+ def verbs_of(bluebook)
432
+ bluebook.aggregates.flat_map do |aggregate|
433
+ verbs = aggregate.commands.map { |command| "#{bluebook.name}::#{aggregate.hecks_name}.#{command.hecks_name}" }
434
+ verbs + aggregate.entities.flat_map do |entity|
435
+ entity.commands.map { |command|
436
+ "#{bluebook.name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{command.hecks_name}"
437
+ }
438
+ end
439
+ end
440
+ end
441
+
442
+ def bare(event) = event.to_s.split("::").last
443
+ end
444
+ end
445
+ end
@@ -0,0 +1,184 @@
1
+ module Hecks
2
+ module Bluebook
3
+ # WHICH REGEXES A BLUEBOOK MAY SAY.
4
+ #
5
+ # A `pattern:` is a fact about a value, carried in a bluebook — declared
6
+ # data, not Ruby code, so it must not lean on what any one engine happens
7
+ # to accept. Regex engines disagree in two different ways :
8
+ #
9
+ # ONLY A BACKTRACKING ENGINE CAN MATCH IT — lookahead, lookbehind,
10
+ # backreferences, atomic groups, possessive quantifiers. None of these
11
+ # can be matched in linear time, and linear-time engines refuse them
12
+ # outright. Refused here for the same reason.
13
+ #
14
+ # EVERY ENGINE PARSES IT AND THEY MEAN DIFFERENT THINGS — the dangerous
15
+ # half, because nothing errors. `\d` `\w` `\s` are ASCII in some engines
16
+ # and Unicode in others ; `[:digit:]` and friends flip the same way in
17
+ # the other direction. Both families are refused, and a domain spells
18
+ # the range it means.
19
+ #
20
+ # What remains — explicit ranges, alternation, quantifiers, anchors,
21
+ # groups — reads identically everywhere, with `^` and `$` as LINE anchors
22
+ # (Ruby's reading). The evidence is spec/corpus/fixtures/patterns.json.
23
+ module PatternSubset
24
+ Rejection = Struct.new(:construct, :reason)
25
+
26
+ REASONS = {
27
+ backreference:
28
+ "backreferences cannot be matched in linear time and portable " \
29
+ "engines refuse them ; a declared pattern may not depend on one",
30
+ named_backreference:
31
+ "a named backreference is still a backreference — it cannot be " \
32
+ "matched in linear time ; a declared pattern may not depend on one",
33
+ perl_class:
34
+ "engines read it in OPPOSITE directions : ASCII in some and " \
35
+ "Unicode in others, so an Arabic-Indic digit satisfies one and not " \
36
+ "the other. Spell the range you mean — [0-9], [A-Za-z0-9_], [ \t] " \
37
+ "— which every engine reads the same way",
38
+ posix_class:
39
+ "[:digit:] and friends flip between ASCII and Unicode across " \
40
+ "engines — the mirror of the perl classes, and wrong in the same " \
41
+ "way. Spell the range you mean",
42
+ lookahead:
43
+ "lookahead cannot be matched in linear time and portable engines " \
44
+ "refuse it ; a declared pattern may not depend on it",
45
+ lookbehind:
46
+ "lookbehind cannot be matched in linear time and portable engines " \
47
+ "refuse it ; a declared pattern may not depend on it",
48
+ atomic_group:
49
+ "an atomic group is a backtracking-engine control knob — " \
50
+ "linear-time engines reject `(?>` as a syntax error",
51
+ possessive:
52
+ "a possessive quantifier is a backtracking-engine control knob — " \
53
+ "linear-time engines reject it as a syntax error"
54
+ }.freeze
55
+
56
+ module_function
57
+
58
+ # nil when the pattern is admitted, a Rejection when it is not.
59
+ #
60
+ # A CHARACTER WALK, deliberately plain : the subset is defined by this
61
+ # walk, and a cleverer spelling would hide what it admits. An escaped
62
+ # construct is a LITERAL, not a violation — `\(\?=` is the three
63
+ # characters "(?=" and says nothing about lookahead — which is why this
64
+ # steps over each backslash pair rather than matching the pattern as a
65
+ # whole.
66
+ def validate(pattern)
67
+ chars = pattern.to_s.chars
68
+ index = 0
69
+ # A CHARACTER-CLASS INTERIOR IS A DIFFERENT ALPHABET : inside `[...]`,
70
+ # `*`, `+`, `?`, `(`, `?` are literal characters, not quantifiers or
71
+ # group syntax — `[*+]` means "a literal asterisk or plus". `]` is
72
+ # only the class's close when it isn't the first character after `[`
73
+ # or `[^` (where it is itself a literal, per POSIX bracket-expression
74
+ # rules).
75
+ in_class = false
76
+ class_start = nil
77
+
78
+ while index < chars.length
79
+ if chars[index] == "\\"
80
+ nxt = chars[index + 1]
81
+ return refuse(:backreference) if nxt&.match?(/[1-9]/)
82
+ return refuse(:named_backreference) if %w[k g].include?(nxt)
83
+ return refuse(:perl_class) if %w[d D w W s S].include?(nxt)
84
+
85
+ index += nxt ? 2 : 1
86
+ next
87
+ end
88
+
89
+ if in_class
90
+ if chars[index] == "]" && index != class_start
91
+ in_class = false
92
+ index += 1
93
+ next
94
+ end
95
+
96
+ return refuse(:posix_class) if posix_class_at?(chars, index)
97
+
98
+ index += 1
99
+ next
100
+ end
101
+
102
+ if chars[index] == "["
103
+ return refuse(:posix_class) if posix_class_at?(chars, index)
104
+
105
+ in_class = true
106
+ class_start = index + 1
107
+ class_start += 1 if chars[class_start] == "^"
108
+ index += 1
109
+ next
110
+ end
111
+
112
+ if chars[index] == "(" && chars[index + 1] == "?"
113
+ return refuse(:lookahead) if %w[= !].include?(chars[index + 2])
114
+ return refuse(:lookbehind) if chars[index + 2] == "<" && %w[= !].include?(chars[index + 3])
115
+ return refuse(:atomic_group) if chars[index + 2] == ">"
116
+ end
117
+
118
+ return refuse(:possessive) if possessive_at?(chars, index)
119
+
120
+ index += 1
121
+ end
122
+
123
+ nil
124
+ end
125
+
126
+ # SPELLED OUT, not derived from the key : these strings are the refusal
127
+ # a caller reads.
128
+ CONSTRUCTS = {
129
+ backreference: "backreference",
130
+ named_backreference: "named backreference",
131
+ perl_class: "perl character class",
132
+ posix_class: "posix bracket class",
133
+ lookahead: "lookahead",
134
+ lookbehind: "lookbehind",
135
+ atomic_group: "atomic group",
136
+ possessive: "possessive quantifier"
137
+ }.freeze
138
+
139
+ def refuse(key) = Rejection.new(CONSTRUCTS.fetch(key), REASONS.fetch(key))
140
+
141
+ def posix_class_at?(chars, index)
142
+ return false unless chars[index] == "[" && chars[index + 1] == ":"
143
+
144
+ cursor = index + 2
145
+ cursor += 1 while chars[cursor]&.match?(/[a-zA-Z]/)
146
+ chars[cursor] == ":" && chars[cursor + 1] == "]"
147
+ end
148
+
149
+ # A possessive quantifier is `*+`, `++`, `?+`, or a bounded `{n}`/{n,m}`
150
+ # immediately followed by `+` — only checked OUTSIDE a character class,
151
+ # where `*`, `+`, `?`, `{`, `}` are quantifier syntax rather than
152
+ # literal characters.
153
+ def possessive_at?(chars, index)
154
+ return true if %w[* + ?].include?(chars[index]) && chars[index + 1] == "+"
155
+ return false unless chars[index] == "{"
156
+
157
+ len = bounded_quantifier_length(chars, index)
158
+ !len.nil? && chars[index + len] == "+"
159
+ end
160
+
161
+ # Length of a `{n}` / `{n,}` / `{n,m}` bound starting at `index`, or nil
162
+ # if what's there isn't one.
163
+ def bounded_quantifier_length(chars, index)
164
+ cursor = index + 1
165
+ digit_seen = false
166
+
167
+ while chars[cursor]&.match?(/[0-9]/)
168
+ digit_seen = true
169
+ cursor += 1
170
+ end
171
+ return nil unless digit_seen
172
+
173
+ if chars[cursor] == ","
174
+ cursor += 1
175
+ cursor += 1 while chars[cursor]&.match?(/[0-9]/)
176
+ end
177
+
178
+ return nil unless chars[cursor] == "}"
179
+
180
+ cursor - index + 1
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,41 @@
1
+ # GENERATED — projected from the language's own Policy aggregate.
2
+ # DO NOT EDIT: the holding half is rendered, and Behaviour::Policy
3
+ # is where anything hand-written belongs.
4
+ require_relative "behaviour/policy"
5
+
6
+ module Hecks
7
+ module Bluebook
8
+ class Policy
9
+ include Hecks::IR
10
+ include Behaviour::Policy
11
+
12
+ emits_ir(
13
+ name: :name,
14
+ on_event: :on_event,
15
+ trigger_command: :trigger_command,
16
+ target_domain: :target_domain,
17
+ where: :where,
18
+ for_each: :for_each,
19
+ with_spec: -> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } }
20
+ )
21
+
22
+ attr_reader :name, :on_event, :trigger_command, :target_domain, :where, :for_each, :with_spec
23
+
24
+ # AGGREGATE, DECLARED AND DELIBERATELY OFF THE WIRE
25
+ # the wire format is a pinned contract, and it does not carry
26
+ # where a policy was written before the builder hoisted it
27
+ attr_accessor :aggregate
28
+
29
+ def initialize(name:, on_event: nil, trigger_command: nil, target_domain: nil, where: nil, for_each: nil, with_spec: [], aggregate: nil)
30
+ @name = name.to_s
31
+ @on_event = on_event
32
+ @trigger_command = trigger_command
33
+ @target_domain = target_domain
34
+ @where = where
35
+ @for_each = for_each
36
+ @with_spec = with_spec
37
+ @aggregate = aggregate&.to_s
38
+ end
39
+ end
40
+ end
41
+ end