hecks 1.2.0 → 1.4.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 (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
data/lib/hecks/codemod.rb CHANGED
@@ -1,49 +1,50 @@
1
1
  require_relative "bluebook/meta_validator"
2
+ require_relative "corpus"
2
3
 
3
4
  module Hecks
4
- # SHARED MACHINERY for a codemod that migrates real `.bluebook` source
5
+ # Shared machinery for a codemod that migrates real `.bluebook` source
5
6
  # once a DSL builder change makes some previously-required declaration
6
7
  # optional/redundant — pulled out of `bin/codemod_implicit_append_fields`
7
8
  # (the first one built), which needed three real, hard-won fixes before
8
9
  # it could be trusted: a process-lifetime AST cache with no
9
10
  # invalidation, a batch-revert granularity that let one unsafe
10
11
  # candidate sink every other safe one sharing its boot, and (in the
11
- # SPINE the codemod migrates FOR, not here) an append-at-end insertion
12
+ # spine the codemod migrates for, not here) an append-at-end insertion
12
13
  # that only round-tripped correctly for whichever field happened to be
13
14
  # last. None of those are guessable in advance; they only surface by
14
15
  # actually running a real edit against real self-hosted code. This
15
- # module is that lesson, kept — the NEXT codemod plugs in two rule-
16
+ # module is that lesson, kept — the next codemod plugs in two rule-
16
17
  # specific procs (`find_candidates`, `apply_candidate`) and inherits
17
18
  # the boot/safety-net machinery rather than rediscovering it.
18
19
  #
19
- # A CODEMOD IS NOT PATTERN-MATCHING ALONE. Deciding "is this line safe
20
- # to delete" means knowing what the RUNTIME would resolve it to — so
20
+ # **A codemod is not pattern-matching alone**. Deciding "is this line safe
21
+ # to delete" means knowing what the runtime would resolve it to — so
21
22
  # every codemod built on this module follows the same three steps:
22
23
  # 1. Boot the real domain (or the self-hosted meta-domain) and read
23
- # its IR to find CANDIDATES — provided by the caller's own
24
+ # its IR to find candidates — provided by the caller's own
24
25
  # `find_candidates`, since the actual redundancy rule is specific
25
26
  # to whichever spine change this migration serves.
26
27
  # 2. Locate and remove each candidate's own source text — the
27
28
  # caller's own `apply_candidate`.
28
29
  # 3. Re-boot from the edited text and diff the full IR export
29
30
  # against the pre-edit export. Byte-identical -> keep. Anything
30
- # else (including a RAISED exception — the self-hosted
31
- # meta-domain DISPATCHES itself into being, S14, so a bad edit
31
+ # else (including a raised exception — the self-hosted
32
+ # meta-domain dispatches itself into being, S14, so a bad edit
32
33
  # can surface as a runtime refusal, not just a differing export)
33
- # -> revert and report SKIPPED, never silently guessed past.
34
+ # -> revert and report skipped, never silently guessed past.
34
35
  #
35
- # DESIGN-TIME CHECKLIST for the SPINE change a future codemod migrates
36
- # corpus text for — both items below are real bugs THIS module's own
36
+ # Design-time checklist for the spine change a future codemod migrates
37
+ # corpus text for — both items below are real bugs this module's own
37
38
  # first use found, not hypothetical:
38
- # - Does the resolved value get inserted into an ORDER-SENSITIVE
39
+ # - Does the resolved value get inserted into an order-sensitive
39
40
  # list (the exported IR is array-order-sensitive throughout)? If
40
- # so, the spine's own insertion must preserve the ORIGINAL
41
+ # so, the spine's own insertion must preserve the original
41
42
  # position, not just append — an append-at-end insertion only
42
43
  # round-trips correctly for a value that already happened to be
43
44
  # last.
44
- # - Does resolution depend on ANOTHER construct already being
45
+ # - Does resolution depend on another construct already being
45
46
  # declared (the aggregate/entity a `sets`/`append:` field resolves
46
- # against)? If a creator command can be declared BEFORE the
47
+ # against)? If a creator command can be declared before the
47
48
  # construct it creates (real, live: `command "Handler"` before
48
49
  # `entity "Handler"`, one file down), one-pass resolution
49
50
  # genuinely cannot see it yet — not a codemod bug, a structural
@@ -51,16 +52,16 @@ module Hecks
51
52
  module Codemod
52
53
  ROOT = File.expand_path("../..", __dir__)
53
54
 
54
- EXAMPLE_ROOTS = Dir.glob(File.join(ROOT, "examples", "*")).select { |p| File.directory?(p) }.sort
55
+ EXAMPLE_ROOTS = Corpus.members(:example, root: ROOT).map(&:path)
55
56
  META_FILES = (Dir.glob(File.join(ROOT, "lib/hecks/grammar/*.bluebook")) +
56
57
  Dir.glob(File.join(ROOT, "lib/hecks/framework/bluebook/*.bluebook")) +
57
58
  Dir.glob(File.join(ROOT, "lib/hecks/language/bluebook/**/*.bluebook"))).sort
58
59
 
59
- # THE SAME LIGHTWEIGHT PATH `spec/spec_helper.rb`'s own
60
+ # The same lightweight path `spec/spec_helper.rb`'s own
60
61
  # `boot_in_memory` uses — `Hecks.with_registry` satisfies
61
62
  # `Hecks.bluebook`'s own `collect`'s "loaded outside a boot" check
62
63
  # without `Hecks.boot`'s full era-check/adapter-wiring path, which
63
- # needs a LIVE Postgres connection for `compliance` and would make
64
+ # needs a live Postgres connection for `compliance` and would make
64
65
  # every codemod depend on a database it has no reason to touch — a
65
66
  # codemod only ever reads a chapter's own declared IR, never a
66
67
  # stored record.
@@ -72,11 +73,11 @@ module Hecks
72
73
  def self.export_json(registry) = Hecks::Projector::Exporter.json(registry)
73
74
 
74
75
  # `Hecks::Adapters::Prism` caches a file's parsed AST for the
75
- # life of the PROCESS, keyed by path — fine for every existing
76
+ # life of the process, keyed by path — fine for every existing
76
77
  # caller (a file loads once per process: one `bin/ir` run, one
77
- # rspec worker), but a codemod legitimately reloads the SAME path
78
+ # rspec worker), but a codemod legitimately reloads the same path
78
79
  # after editing it, and a stale cached tree reports a
79
- # `given`/`ensures` block at its OLD line number, which no longer
80
+ # `given`/`ensures` block at its old line number, which no longer
80
81
  # matches the freshly re-executed file's own `block.source_location`
81
82
  # — surfacing as "did not survive extraction" on a perfectly valid
82
83
  # file. `Prism.forget` is the real invalidation API this module's
@@ -104,10 +105,10 @@ module Hecks
104
105
  registry
105
106
  end
106
107
 
107
- # `forget_all`, not a single `forget` — the meta-domain is NINE
108
+ # `forget_all`, not a single `forget` — the meta-domain is nine
108
109
  # files (`MetaValidator::GRAMMAR_FILES`) merged into one registry,
109
110
  # and a caller here (the codemod runner) doesn't generally know in
110
- # advance which ONE it just edited.
111
+ # advance which one it just edited.
111
112
  def self.boot_meta
112
113
  Hecks::Adapters::Prism.forget_all
113
114
  Hecks::Bluebook::MetaValidator.instance_variable_set(:@grammar_registry, nil)
@@ -123,7 +124,7 @@ module Hecks
123
124
  # Walks every aggregate (and every nested entity, recursively)
124
125
  # across every chapter in a booted registry, yielding [owning
125
126
  # construct, command] pairs — `construct` is whichever
126
- # Aggregate/Entity actually OWNS the command, the same distinction
127
+ # Aggregate/Entity actually owns the command, the same distinction
127
128
  # `AggregateBuilder#command` vs `EntityBuilder#command` already
128
129
  # draws. Generic enough for any rule that needs to walk real
129
130
  # commands, not specific to the attribute-redundancy rule.
@@ -143,7 +144,7 @@ module Hecks
143
144
  construct.attributes.find { |attr| attr.name.to_s == name.to_s }
144
145
  end
145
146
 
146
- # A LIST attribute's own element construct — the value object or
147
+ # A list attribute's own element construct — the value object or
147
148
  # entity `list_of(...)` names, resolved by `hecks_name` the same way
148
149
  # `AttributeCollector#resolve_identity_field!` already does. Shared
149
150
  # because "what does this list actually hold" is a question any
@@ -157,7 +158,7 @@ module Hecks
157
158
  pool.find { |c| c.hecks_name.to_s == list_attr.type.to_s }
158
159
  end
159
160
 
160
- # Either a raised exception OR a differing export counts as unsafe
161
+ # Either a raised exception or a differing export counts as unsafe
161
162
  # — see the module header on why the meta-domain specifically can
162
163
  # raise. Returns [value_or_nil, error_message_or_nil].
163
164
  def self.safely
@@ -166,11 +167,11 @@ module Hecks
166
167
  [nil, "#{e.class}: #{e.message}"]
167
168
  end
168
169
 
169
- # THE REUSABLE RUNNER — every real bug fix this module carries lives
170
+ # **The reusable runner** — every real bug fix this module carries lives
170
171
  # here, not in a caller's own script. A caller supplies:
171
172
  #
172
173
  # find_candidates: ->(registry) { [...] }
173
- # Given a booted registry, return every CANDIDATE this rule
174
+ # Given a booted registry, return every candidate this rule
174
175
  # could migrate. A candidate is whatever shape the caller wants
175
176
  # — `apply_candidate` is the only other thing that reads it.
176
177
  #
@@ -178,7 +179,7 @@ module Hecks
178
179
  # Given one file's current text and one candidate, return the
179
180
  # edited text and whether a match was actually found/removed —
180
181
  # `false` (text unchanged) when the candidate doesn't apply to
181
- # THIS file, which is how the meta-domain's multi-file search
182
+ # this file, which is how the meta-domain's multi-file search
182
183
  # below finds the right one without the caller needing to know
183
184
  # which file a candidate lives in ahead of time.
184
185
  #
@@ -279,10 +280,10 @@ module Hecks
279
280
  end
280
281
  end
281
282
 
282
- # PER-CANDIDATE, not one batched write-then-verify — the
283
- # meta-domain is ONE shared registry (SyntaxBoot DISPATCHES it
283
+ # Per-candidate, not one batched write-then-verify — the
284
+ # meta-domain is one shared registry (SyntaxBoot dispatches it
284
285
  # into itself, S14), so a single unsafe candidate among many would
285
- # otherwise sink every OTHER, genuinely safe candidate in the same
286
+ # otherwise sink every other, genuinely safe candidate in the same
286
287
  # run: this module's first real run found exactly that (25
287
288
  # candidates, one dispatch-time break, all 25 reverted as a batch
288
289
  # before this per-candidate loop existed). `before_meta` stays the
@@ -291,8 +292,8 @@ module Hecks
291
292
  # still equal the pristine original after each kept edit, by
292
293
  # definition, no moving target needed.
293
294
  # Same shape and same reason as run_example_domains just above (see
294
- # its own comment) — the write/verify/revert sequence, PER
295
- # CANDIDATE (this method's own comment explains why it cannot
295
+ # its own comment) — the write/verify/revert sequence, per
296
+ # candidate (this method's own comment explains why it cannot
296
297
  # batch), is one coherent unit; splitting it would scatter
297
298
  # before_meta/live_meta/applied_by_file state across methods for
298
299
  # no gain.
@@ -323,7 +324,7 @@ module Hecks
323
324
  live_meta[target_file] = text
324
325
  Codemod::META_FILES.each { |f| File.write(f, live_meta[f]) }
325
326
 
326
- # DRY RUN STILL VERIFIES — see run_example_domains' own
327
+ # **Dry run still verifies** — see run_example_domains' own
327
328
  # comment; the edit is always written and rebooted for real,
328
329
  # then always reverted afterward when dry-run (whether or not
329
330
  # it was safe) so the next candidate is judged against the
@@ -1,14 +1,14 @@
1
1
  module Hecks
2
2
  # The invisible field a built construct carries.
3
3
  #
4
- # A construct is a RECORD WITH AN OWNER CHAIN — the chapter (Bluebook)
4
+ # A construct is a record with an owner chain — the chapter (Bluebook)
5
5
  # owns its aggregates, an aggregate owns everything declared on it — and the
6
6
  # bluebook identity is carried in its own field, under a `hecks_` prefix that
7
7
  # no domain attribute can collide with. Invisible means exactly that: not an
8
8
  # attribute, not a key in `to_h`, not a reader on instances. Framework
9
9
  # metadata about the construct, not part of the domain it describes.
10
10
  #
11
- # The identity is COMPUTED by walking owners rather than stamped, so nothing
11
+ # The identity is computed by walking owners rather than stamped, so nothing
12
12
  # has to be re-stamped when a chapter is assembled after its aggregates:
13
13
  #
14
14
  # Pizzas the chapter — no owner
@@ -17,7 +17,7 @@ module Hecks
17
17
  #
18
18
  # That spelling is not invented here. It is the id `MetaValidator::Judge`
19
19
  # already mints in `#identify`, so a construct and the meta-domain's
20
- # record OF that construct carry the same identity, and there is no
20
+ # record of that construct carry the same identity, and there is no
21
21
  # translation table between them to be quietly wrong in.
22
22
  #
23
23
  # Usage:
@@ -37,7 +37,7 @@ module Hecks
37
37
  attr_writer :hecks_name
38
38
 
39
39
  # A chapter is the only construct that legitimately has no owner. Everything
40
- # else is DECLARED IN something, so a missing owner is an unstamped construct
40
+ # else is declared in something, so a missing owner is an unstamped construct
41
41
  # rather than a top — see hecks_fqn.
42
42
  attr_writer :hecks_root
43
43
 
@@ -47,11 +47,11 @@ module Hecks
47
47
  def hecks_name = @hecks_name
48
48
 
49
49
  # How this construct joins its owner. An aggregate is a member of its
50
- # chapter's namespace (`::`) ; everything else is declared ON its owner
50
+ # chapter's namespace (`::`) ; everything else is declared on its owner
51
51
  # (`.`). Overridden by Aggregate, defaulted here for every other construct.
52
52
  def hecks_separator = "."
53
53
 
54
- # REFUSES rather than guesses. A construct with no owner and no claim to be a
54
+ # Refuses rather than guesses. A construct with no owner and no claim to be a
55
55
  # chapter has simply not been stamped yet — entity commands are in that state
56
56
  # while entities are still IR objects — and answering the bare name would be a
57
57
  # plausible half-truth that no test would notice. That shape of falsehood is
@@ -0,0 +1,317 @@
1
+ require_relative "fuzzing/target_capabilities"
2
+
3
+ module Hecks
4
+ # **The corpus, discovered** — every place in this repo that holds a real
5
+ # domain, named once.
6
+ #
7
+ # This used to be spelled out separately by every consumer that walks
8
+ # it — spec/corpus_spec.rb, spec/model_check_spec.rb, bin/model_check,
9
+ # spec/parser_parity_spec.rb, bin/fuzz, Fuzzing::CombinationMiner — and
10
+ # the copies had already drifted: only the model checker saw
11
+ # `qa/bluebook`, only parser parity saw `spec/fixtures`, and nothing but
12
+ # bin/fuzz's own sweep ever saw `qa/stress_domains`. One table here, and
13
+ # each consumer names the KINDS it walks rather than re-deriving where
14
+ # those kinds live.
15
+ #
16
+ # Plain Dir/File only — bin/ scripts require this before (or without)
17
+ # booting anything.
18
+ module Corpus
19
+ ROOT = File.expand_path("../..", __dir__).freeze
20
+
21
+ Member = Struct.new(:stem, :kind, :path)
22
+
23
+ # **One domain per directory** — its bluebooks sit in `<dir>/bluebook/`
24
+ # or directly in `<dir>` (see `bluebook_files`). Stemmed by directory.
25
+ DIRECTORY_KINDS = {
26
+ example: "examples/*",
27
+ stress: "qa/stress_domains/*",
28
+ semantics: "spec/corpus/semantics/domains/*"
29
+ }.freeze
30
+
31
+ # **One chapter per file**. Stemmed by the path below the glob's fixed
32
+ # prefix, so a nested fixture keeps its subdirectory (`eras/base`)
33
+ # and never collides with a same-named file elsewhere in the kind.
34
+ FILE_KINDS = {
35
+ grammar: "lib/hecks/grammar/*.bluebook",
36
+ framework: "lib/hecks/framework/bluebook/*.bluebook",
37
+ qa: "qa/bluebook/*.bluebook",
38
+ language: "lib/hecks/language/**/*.bluebook",
39
+ deploy: "lib/hecks/deploy/bluebook/*.bluebook",
40
+ fixture: "spec/fixtures/**/*.bluebook"
41
+ }.freeze
42
+
43
+ KINDS = (DIRECTORY_KINDS.keys + FILE_KINDS.keys).freeze
44
+
45
+ # Where a bluebook the sweep does not boot goes instead. Not a filter:
46
+ # nothing leaves `sweepable_domains` without naming the check that owns
47
+ # it, and spec/corpus_accounting_spec.rb proves each destination exists
48
+ # and actually exercises what is routed to it.
49
+ #
50
+ # check: :named_in — `destination` names every routed file
51
+ # (`names: :each_file`) or the given text
52
+ # check: :gitignored — `destination` holds the ignore rule `names`,
53
+ # and nothing matching is committed
54
+ # check: :gap — no check exercises these yet. Listed so the
55
+ # gap is visible; the accounting spec keeps it
56
+ # pending and fails the moment one appears.
57
+ #
58
+ # Ordered — a bluebook belongs to the first route it matches, so a
59
+ # specific destination sits above the catch-all for its shape.
60
+ Route = Struct.new(:pattern, :check, :destination, :names, :why)
61
+
62
+ ROUTES = [
63
+ Route.new(%r{\Atmp/}, :gitignored, ".gitignore", "tmp/",
64
+ "scratch output — generated and mined candidate domains, fuzz failures — never committed"),
65
+ Route.new(%r{/\.aws-sam/}, :gitignored, ".gitignore", ".aws-sam/",
66
+ "SAM build output — a vendored copy of real sources, never committed"),
67
+ Route.new(%r{/data/eras/}, :gitignored, ".gitignore", "**/data/eras/",
68
+ "era snapshots a file adapter writes at runtime, never committed"),
69
+ Route.new(%r{\Arust/}, :named_in, "rust/parser/tests/gates.rs", :each_file,
70
+ "the Rust parser's own fixtures, each loaded by its gate tests"),
71
+ Route.new(%r{/translations/}, :named_in, "spec/translation/committed_edges_spec.rb", "bluebook/translations",
72
+ "translation edges, not chapters; each must load, chain era to era, and end at the storage " \
73
+ "shape its bluebook declares today"),
74
+ Route.new(%r{\Alib/hecks/forms/examples/}, :named_in, "spec/forms/app_spec.rb", :each_file,
75
+ "a Forms presentation config wearing the .bluebook extension"),
76
+ Route.new(%r{\Aspec/fixtures/eras/}, :named_in, "spec/runtime/storage_shape_spec.rb", "fixtures/eras",
77
+ "deliberately conflicting versions of one domain; each must classify as the verdict " \
78
+ "its filename declares (bump_* / same_*)"),
79
+ Route.new(%r{\Aspec/fixtures/model_check/}, :named_in, "spec/model_check_spec.rb", :each_file,
80
+ "domains broken on purpose; each must produce exactly the finding kinds it is built to trigger"),
81
+ # bin/fuzz sweeps it too, once Fuzzing::Replay coerces value-object
82
+ # args before recomputing givens — today its oracle reads raw args
83
+ # while dispatch coerces, so checkout_fixture's VO-reading given
84
+ # reads as wrongly admitted.
85
+ Route.new(%r{\Aspec/fixtures/rust_host/}, :named_in, "rust/host/src/web.rs", "checkout_fixture",
86
+ "the Rust host's checkout fixture, pinned by its web and /api tests")
87
+ ].freeze
88
+
89
+ module_function
90
+
91
+ def members(*kinds, root: ROOT)
92
+ kinds = KINDS if kinds.empty?
93
+ kinds.flat_map do |kind|
94
+ if (glob = DIRECTORY_KINDS[kind])
95
+ Dir.glob(File.join(root, glob)).select { |path| File.directory?(path) }.sort
96
+ .map { |dir| Member.new(File.basename(dir), kind, dir) }
97
+ elsif (glob = FILE_KINDS[kind])
98
+ prefix = File.join(root, glob[/\A[^*]*/].chomp("/"))
99
+ Dir.glob(File.join(root, glob))
100
+ .map { |file| Member.new(file.delete_prefix("#{prefix}/").delete_suffix(".bluebook"), kind, file) }
101
+ else
102
+ raise ArgumentError, "unknown corpus kind #{kind.inspect} — known: #{KINDS.join(', ')}"
103
+ end
104
+ end
105
+ end
106
+
107
+ # What a boot loads for a member: a directory kind's bluebook
108
+ # directory, a file kind's own file. `nil` for a directory holding no
109
+ # bluebook at all.
110
+ def source_of(member)
111
+ DIRECTORY_KINDS.key?(member.kind) ? bluebook_dir(member.path) : member.path
112
+ end
113
+
114
+ # Where a domain path keeps its bluebooks — `<domain>/bluebook/*.bluebook`
115
+ # (every example and stress domain), or the directory itself
116
+ # (`qa/bluebook`). `nil` when neither holds a bluebook.
117
+ def bluebook_files(domain_path)
118
+ [File.join(domain_path, "bluebook"), domain_path].each do |dir|
119
+ files = Dir[File.join(dir, "*.bluebook")]
120
+ return files unless files.empty?
121
+ end
122
+ nil
123
+ end
124
+
125
+ def bluebook_dir(domain_path)
126
+ files = bluebook_files(domain_path)
127
+ files && File.dirname(files.first)
128
+ end
129
+
130
+ # The route a repo-relative path takes instead of the sweep — the
131
+ # first one it matches — or `nil` when the sweep boots it.
132
+ def route_for(relative_path)
133
+ ROUTES.find { |route| route.pattern.match?(relative_path) }
134
+ end
135
+
136
+ # What bin/model_check and spec/model_check_spec.rb walk — every kind,
137
+ # less the language (examined as one judged chapter, not file by file)
138
+ # and deploy chapters (the SAM projector's own inputs), and less any
139
+ # member a route already sends to a destination of its own: the
140
+ # broken-on-purpose model_check fixtures must produce their findings
141
+ # there, so a clean-corpus gate here would be the wrong check for them.
142
+ MODEL_CHECK_KINDS = %i[example grammar framework qa stress fixture].freeze
143
+
144
+ def model_check_members(root: ROOT)
145
+ members(*MODEL_CHECK_KINDS, root: root).reject { |member| route_for(member.path.delete_prefix("#{root}/")) }
146
+ end
147
+
148
+ # **The ledger sweeps itself** — its own chapter is a domain like any
149
+ # other, and the one rotation member that is neither an example nor a
150
+ # stress domain.
151
+ ROTATION_LEDGER = { "quality_control" => "qa/bluebook" }.freeze
152
+
153
+ # What the QA rotation is made of — every example and stress domain
154
+ # this repository owns, plus the ledger, as `reference => repo-relative
155
+ # path`: exactly the shape `Target.path` is stored in.
156
+ #
157
+ # Derived, because the hand-kept version silently went stale.
158
+ # `bin/qa_seed_targets` carried a literal list naming three of the
159
+ # thirteen stress domains; the other ten were authored, argued for in
160
+ # their own NOTES.md, several promoted by `bin/qa_generated_domains
161
+ # --promote` — and never swept once, because a `Target` row is what
162
+ # puts a domain in the rotation and nothing tied that list to the
163
+ # corpus. Promotion only ever printed the `target.identify` line for a
164
+ # human to run.
165
+ def rotation_targets(root: ROOT)
166
+ members(:example, :stress, root: root)
167
+ .to_h { |member| [member.stem, member.path.delete_prefix("#{root}/")] }
168
+ .merge(ROTATION_LEDGER)
169
+ end
170
+
171
+ # Every bootable domain in the project, not a hand-kept list — any
172
+ # directory holding a `.bluebook` no route sends elsewhere, a
173
+ # `bluebook/` folder standing for the domain directory around it.
174
+ def sweepable_domains(root = ROOT)
175
+ Dir.chdir(root) do
176
+ Dir.glob("**/*.bluebook")
177
+ .reject { |path| route_for(path) }
178
+ .map { |path| File.dirname(path) }
179
+ .map { |dir| File.basename(dir) == "bluebook" ? File.dirname(dir) : dir }
180
+ .uniq.sort
181
+ .map { |dir| File.join(root, dir) }
182
+ end
183
+ end
184
+
185
+ # The domain directory a member stands for, spelled the way
186
+ # `sweepable_domains` spells it: a `bluebook/` folder is its parent.
187
+ def domain_dir_of(member)
188
+ dir = File.directory?(member.path) ? member.path : File.dirname(member.path)
189
+ File.basename(dir) == "bluebook" ? File.dirname(dir) : dir
190
+ end
191
+
192
+ # ── The Rust-facing corpus ─────────────────────────────────────────
193
+ #
194
+ # Every Rust-facing list (the fuzz bridge, the codegen drift check,
195
+ # rust coverage, codegen parity) used to be typed out by hand, and
196
+ # each had drifted from `rust/Cargo.toml`'s `[features]`: fuzzing saw
197
+ # 8 of 20 features. These read the one source instead. The
198
+ # `rust/src/generated/` modules split into buckets, and
199
+ # spec/corpus_rust_spec.rb proves every Cargo feature and every
200
+ # generated module lands in exactly one of them:
201
+ #
202
+ # rust_domains an in-repo domain directory with a Cargo feature
203
+ # of its own: fuzzed, regenerated, coverage- and
204
+ # parity-checked
205
+ # framework chapters no feature and no merged.rs; written as a side
206
+ # effect of every `uses_framework` domain's regen
207
+ # RUST_ELSEWHERE a feature with no in-repo domain directory, and
208
+ # the check that owns it instead
209
+ GENERATED_DIR = "rust/src/generated".freeze
210
+ RUST_DOMAIN_KINDS = %i[example stress fixture].freeze
211
+
212
+ RustDomain = Struct.new(:feature, :dir, :kind)
213
+
214
+ # `check: :named_in` — `destination` must name `names`.
215
+ # `check: :external` — no in-repo domain directory may carry the name;
216
+ # the day one does, it belongs in rust_domains.
217
+ Elsewhere = Struct.new(:check, :destination, :names, :why)
218
+
219
+ RUST_ELSEWHERE = {
220
+ "meta" => Elsewhere.new(:named_in, "spec/codegen_parity_spec.rb", "bluebook_language",
221
+ "the self-hosted grammar (lib/hecks/language), not a domain directory — every " \
222
+ "bin/project_rust run rewrites it (so the drift check diffs it), codegen parity " \
223
+ "checks it as bluebook_language, and there is no directory to fuzz"),
224
+ "embryonaut" => Elsewhere.new(:external, "~/Projects/embryonautfoundersapp", "embryonaut",
225
+ "an external product's domain — its bluebook, regeneration and parity are owed " \
226
+ "by its own repo; here bin/rust_coverage checks only the committed snapshot")
227
+ }.freeze
228
+
229
+ # **Shrink-only**. A generated module `bin/rust_coverage` still reports a
230
+ # gap for. `bin/corpus --rust-coverage` requires each of these to
231
+ # still fail, so an entry that starts passing breaks the build until
232
+ # it is deleted here.
233
+ RUST_COVERAGE_PENDING = {}.freeze
234
+
235
+ def cargo_features_table(root: ROOT)
236
+ File.read(File.join(root, "rust/Cargo.toml"))[Fuzzing::TargetCapabilities::FEATURES_TABLE] || ""
237
+ end
238
+
239
+ def cargo_features(root: ROOT)
240
+ cargo_features_table(root: root).scan(/^(\w+)\s*=\s*\[\]/).flatten
241
+ end
242
+
243
+ # The feature `bin/project_rust` last wrote as Cargo's `default`.
244
+ def cargo_default(root: ROOT)
245
+ cargo_features_table(root: root)[/^default\s*=\s*\["(\w+)"\]/, 1]
246
+ end
247
+
248
+ # Every in-repo domain directory whose name is a Cargo feature, sorted
249
+ # by path. When the module is already generated, its metadata.rs stamp
250
+ # decides which directory it came from — a directory name alone is not
251
+ # enough: spec/fixtures/qa_discover_external_domains vendors a second
252
+ # `examples/pizzas` that no Cargo feature was ever generated from.
253
+ def rust_domains(root: ROOT)
254
+ features = cargo_features(root: root)
255
+ members(*RUST_DOMAIN_KINDS, root: root)
256
+ .map { |member| [domain_dir_of(member), member.kind] }
257
+ .uniq(&:first)
258
+ .select { |dir, _| rust_domain_dir?(dir, features, root) }
259
+ .sort_by(&:first)
260
+ .map { |dir, kind| RustDomain.new(File.basename(dir).downcase, dir, kind) }
261
+ end
262
+
263
+ def rust_domain_dir?(dir, features, root)
264
+ feature = File.basename(dir).downcase
265
+ source = generated_source(feature, root: root)
266
+ features.include?(feature) && (source.nil? || source == dir.delete_prefix("#{root}/"))
267
+ end
268
+
269
+ # Where a generated module came from, read off the stamp bin/project_rust
270
+ # writes into its metadata.rs — `examples/pizzas`, `/abs/path/embryonaut`,
271
+ # `the self-hosted language (lib/hecks/language/bluebook)`, with any
272
+ # ` (uses_framework "X")` suffix dropped. `nil` when not generated.
273
+ SOURCE_STAMP = %r{GENERATED by bin/project_rust — (.+?)(?: \(uses_framework "\w+"\))?'s own canonical IR,}
274
+
275
+ def generated_source(module_name, root: ROOT)
276
+ metadata = File.join(root, GENERATED_DIR, module_name, "metadata.rs")
277
+ File.file?(metadata) ? File.read(metadata)[SOURCE_STAMP, 1] : nil
278
+ end
279
+
280
+ def generated_modules(root: ROOT)
281
+ Dir.children(File.join(root, GENERATED_DIR)).select { |name| File.directory?(File.join(root, GENERATED_DIR, name)) }.sort
282
+ end
283
+
284
+ def generated?(feature, root: ROOT)
285
+ File.file?(File.join(root, GENERATED_DIR, feature, "merged.rs"))
286
+ end
287
+
288
+ # Framework members `bin/project_rust` writes a module for as a side
289
+ # effect of some `uses_framework` domain — a module, no merged.rs.
290
+ def rust_framework_chapters(root: ROOT)
291
+ modules = generated_modules(root: root)
292
+ members(:framework, root: root).map(&:stem)
293
+ .select { |stem| modules.include?(stem) && !generated?(stem, root: root) }
294
+ end
295
+
296
+ # The regeneration order the drift check runs. Sorted by path, so
297
+ # which domain runs last — and so wins Cargo's `default`, mod.rs's cfg
298
+ # comments and the shared framework modules' attribution stamp — is a
299
+ # fact of the sorted list, not a hand-picked order. (The old hand list
300
+ # put waybill last; PR #667 moved to the sort, which makes
301
+ # has_many_fixture last.) spec/corpus_rust_spec.rb pins last == default.
302
+ def rust_regen_order(root: ROOT)
303
+ rust_domains(root: root).select { |domain| generated?(domain.feature, root: root) }
304
+ end
305
+
306
+ # The chapter a bluebook declares — `Hecks.bluebook "<Name>"`, read
307
+ # off the file rather than guessed from its name (a grammar chapter's
308
+ # file is named after its role, `aggregate.bluebook`, while its
309
+ # chapter is always "Bluebook"). Scans the whole file: a framework
310
+ # member's header comment can run past any fixed line cap.
311
+ def chapter_name_of(bluebook_path)
312
+ bluebook_path = Array(bluebook_path).first
313
+ header = File.foreach(bluebook_path).find { |line| line =~ /\A\s*Hecks\.bluebook\s+"([^"]+)"/ }
314
+ header && Regexp.last_match(1)
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,95 @@
1
+ # Deprecation warnings this gem raises about its own API. Not a singleton
2
+ # method on `Hecks` itself: the self-hosted language declares every word
3
+ # that module answers (spec/syntax_conformance_spec.rb), and a warning
4
+ # about an old call shape is not a word of the language — callers name
5
+ # `Hecks::Deprecation.call` in full.
6
+ module Hecks
7
+ # One warning per call site, naming the caller's line — never a line
8
+ # inside this gem. A deprecated shape usually reaches the runtime through
9
+ # a forwarding door (`Hecks::Router`, a namespace shortcut, a
10
+ # `RemoteDispatcher`), so the site reported is the first frame outside
11
+ # `lib/hecks`, which is the line a reader can actually change.
12
+ #
13
+ # Hecks::Deprecation.call(:legacy_dispatch_args, "pass facts in with:")
14
+ # # warns once: "spec/foo_spec.rb:12: warning: pass facts in with:"
15
+ #
16
+ # Per key, in this order:
17
+ # `allowing(key) { ... }` — suppressed for the block, this thread only
18
+ # (the spec that tests a deprecation itself)
19
+ # `raise_on!(key)` — raises `Hecks::Deprecation::Error` (the
20
+ # test suite, so no spec reintroduces it);
21
+ # with a block, only where the block says so,
22
+ # given the site, so a suite can raise on
23
+ # new sites while a known worklist warns
24
+ # HECKS_SILENCE_DEPRECATIONS=1 — silent
25
+ # otherwise — `Kernel#warn`, once per key and call site
26
+ module Deprecation
27
+ class Error < StandardError
28
+ end
29
+
30
+ LIB_ROOT = File.expand_path(__dir__)
31
+ LIB_ENTRY = File.expand_path("../hecks.rb", __dir__)
32
+
33
+ EVERYWHERE = ->(_site) { true }
34
+
35
+ @seen = {}
36
+ @raising = {}
37
+ @mutex = Mutex.new
38
+
39
+ class << self
40
+ # { key => where it raises instead of warning }.
41
+ attr_reader :raising
42
+
43
+ def raise_on!(*keys, &where)
44
+ keys.each { |key| @raising[key] = where || EVERYWHERE }
45
+ keys
46
+ end
47
+
48
+ def call(key, message)
49
+ return if allowed?(key)
50
+
51
+ uplevel, site = external_frame
52
+ raise Error, "#{site}: #{message}" if @raising[key]&.call(site)
53
+ return if ENV["HECKS_SILENCE_DEPRECATIONS"] == "1"
54
+ return unless first_sighting?(key, site)
55
+
56
+ Kernel.warn(message, uplevel: uplevel)
57
+ end
58
+
59
+ def allowing(key)
60
+ allowed = Thread.current[:hecks_allowed_deprecations] ||= []
61
+ allowed.push(key)
62
+ yield
63
+ ensure
64
+ allowed.pop
65
+ end
66
+
67
+ # Forget every site already warned about — for specs only.
68
+ def reset! = @mutex.synchronize { @seen.clear }
69
+
70
+ # The first caller frame outside this gem — where a deprecated call
71
+ # was written.
72
+ def external?(frame)
73
+ path = frame.absolute_path || frame.path
74
+ !(path.start_with?("<internal:") || path == LIB_ENTRY || path.start_with?("#{LIB_ROOT}/"))
75
+ end
76
+
77
+ private
78
+
79
+ def allowed?(key) = Array(Thread.current[:hecks_allowed_deprecations]).include?(key)
80
+
81
+ def first_sighting?(key, site)
82
+ @mutex.synchronize { @seen.key?([key, site]) ? false : (@seen[[key, site]] = true) }
83
+ end
84
+
85
+ # `uplevel:` counts from the frame that calls `Kernel.warn` — `call`,
86
+ # this method's caller — and `caller_locations(1)` starts at that
87
+ # same frame, so the index found here is the uplevel to pass.
88
+ def external_frame
89
+ frames = caller_locations(1)
90
+ index = frames.index { |frame| external?(frame) } || (frames.size - 1)
91
+ [index, "#{frames[index].path}:#{frames[index].lineno}"]
92
+ end
93
+ end
94
+ end
95
+ end