hecks 2.9.0 → 3.0.1

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 (600) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +51 -6
  3. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  6. data/lib/hecks/adapters/driven/memory.rb +16 -3
  7. data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
  8. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  9. data/lib/hecks/adapters/driven/postgres.rb +13 -4
  10. data/lib/hecks/adapters/driven/prism.rb +41 -8
  11. data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
  12. data/lib/hecks/behaviors/expectations.rb +1 -1
  13. data/lib/hecks/behaviors.rb +1 -1
  14. data/lib/hecks/bench/cli.rb +62 -0
  15. data/lib/hecks/bench/environment.rb +71 -0
  16. data/lib/hecks/bench/postgres_probe.rb +30 -0
  17. data/lib/hecks/bench/report.rb +76 -0
  18. data/lib/hecks/bench/ruby_runner.rb +93 -0
  19. data/lib/hecks/bench/run.rb +55 -0
  20. data/lib/hecks/bench/rust_runner.rb +101 -0
  21. data/lib/hecks/bench/stats.rb +46 -0
  22. data/lib/hecks/bench/suite.rb +98 -0
  23. data/lib/hecks/bench/workload.rb +89 -0
  24. data/lib/hecks/bench.rb +17 -0
  25. data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
  26. data/lib/hecks/bluebook/attribute.rb +1 -1
  27. data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
  28. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
  29. data/lib/hecks/bluebook/chapter.rb +6 -2
  30. data/lib/hecks/bluebook/domain_port.rb +36 -5
  31. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
  32. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
  33. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
  34. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
  35. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
  36. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
  37. data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
  38. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
  39. data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
  40. data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
  41. data/lib/hecks/bluebook/expression/resolver.rb +3 -2
  42. data/lib/hecks/bluebook/hexagon.rb +13 -5
  43. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
  44. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
  45. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  46. data/lib/hecks/bluebook/meta_validator.rb +9 -4
  47. data/lib/hecks/bluebook/model_check.rb +23 -0
  48. data/lib/hecks/bluebook/pattern_subset.rb +38 -0
  49. data/lib/hecks/bluebook/query.rb +20 -5
  50. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  51. data/lib/hecks/canonical_json.rb +30 -0
  52. data/lib/hecks/chapters.rb +102 -0
  53. data/lib/hecks/cli/behaviors.rb +61 -0
  54. data/lib/hecks/cli/canonicalise.rb +25 -0
  55. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  56. data/lib/hecks/cli/check_era.rb +88 -0
  57. data/lib/hecks/cli/console.rb +66 -0
  58. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  59. data/lib/hecks/cli/doc_coverage.rb +32 -0
  60. data/lib/hecks/cli/document.rb +2 -2
  61. data/lib/hecks/cli/expression_projection.rb +41 -0
  62. data/lib/hecks/cli/follow.rb +88 -0
  63. data/lib/hecks/cli/history.rb +44 -0
  64. data/lib/hecks/cli/ir.rb +1 -1
  65. data/lib/hecks/cli/mcp.rb +3 -3
  66. data/lib/hecks/cli/model_check.rb +15 -8
  67. data/lib/hecks/cli/pattern_cases.rb +47 -0
  68. data/lib/hecks/cli/present.rb +39 -0
  69. data/lib/hecks/cli/project_cli.rb +224 -32
  70. data/lib/hecks/cli/project_diagrams.rb +4 -3
  71. data/lib/hecks/cli/project_glossary.rb +44 -0
  72. data/lib/hecks/cli/reference.rb +46 -0
  73. data/lib/hecks/cli/refresh_projections.rb +44 -0
  74. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  75. data/lib/hecks/cli/release.rb +70 -0
  76. data/lib/hecks/cli/release_gem.rb +59 -0
  77. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  78. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  79. data/lib/hecks/cli/run.rb +3 -3
  80. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  81. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  82. data/lib/hecks/cli/shape.rb +70 -0
  83. data/lib/hecks/cli/smoke_http.rb +230 -0
  84. data/lib/hecks/cli/smoke_test.rb +5 -3
  85. data/lib/hecks/cli/spec_example.rb +27 -0
  86. data/lib/hecks/cli/statements.rb +32 -0
  87. data/lib/hecks/cli/stores.rb +4 -5
  88. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  89. data/lib/hecks/cli.rb +201 -52
  90. data/lib/hecks/codemod.rb +324 -0
  91. data/lib/hecks/corpus.rb +423 -0
  92. data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
  93. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
  94. data/lib/hecks/deploy/oidc.json +82 -2
  95. data/lib/hecks/doc/reference.rb +351 -0
  96. data/lib/hecks/doors/cli_door.rb +196 -0
  97. data/lib/hecks/doors/cli_runner.rb +297 -0
  98. data/lib/hecks/{facade → doors}/command_request.rb +1 -1
  99. data/lib/hecks/{facade → doors}/handle.rb +2 -2
  100. data/lib/hecks/{facade → doors}/json_door.rb +4 -4
  101. data/lib/hecks/doors/launcher_options.rb +129 -0
  102. data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
  103. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  104. data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
  105. data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
  106. data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
  107. data/lib/hecks/doors.rb +61 -0
  108. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
  109. data/lib/hecks/embryonaut_bluebook.rb +1 -1
  110. data/lib/hecks/engine_agreement.rb +156 -0
  111. data/lib/hecks/facade.rb +24 -37
  112. data/lib/hecks/forms/app.rb +6 -6
  113. data/lib/hecks/forms/banking_presentation.rb +50 -0
  114. data/lib/hecks/forms/field_shape.rb +2 -1
  115. data/lib/hecks/forms/port_argument.rb +1 -1
  116. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  117. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  118. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  119. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  120. data/lib/hecks/fuzzing/differential.rb +156 -0
  121. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  122. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  123. data/lib/hecks/fuzzing/form_census.rb +136 -0
  124. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  125. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  126. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  127. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  128. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  129. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  130. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  131. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  132. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  133. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  134. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  135. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  136. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  137. data/lib/hecks/fuzzing/properties.rb +162 -0
  138. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  139. data/lib/hecks/fuzzing/replay.rb +505 -0
  140. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  141. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  142. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  143. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  144. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  145. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  146. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  147. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  148. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  149. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  150. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  151. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  152. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  153. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  154. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  155. data/lib/hecks/fuzzing.rb +23 -0
  156. data/lib/hecks/grammar/evolve.rb +336 -0
  157. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  158. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  159. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  160. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  161. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  162. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  163. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  164. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  165. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  166. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  167. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  168. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  169. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  170. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  171. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  172. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  173. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  174. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  175. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  176. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  177. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  178. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  179. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  180. data/lib/hecks/hecks/adapters/git.rb +102 -0
  181. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  182. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  183. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  184. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  185. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  186. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  187. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  188. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  189. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  190. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  191. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  192. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  193. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  194. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  195. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  196. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  197. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  198. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  199. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  200. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  201. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  202. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  203. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  204. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  205. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  206. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  207. data/lib/hecks/hecks/codebase.behaviors +172 -0
  208. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  209. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  210. data/lib/hecks/hecks/custodian.behaviors +179 -0
  211. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  212. data/lib/hecks/hecks/deploy.behaviors +74 -0
  213. data/lib/hecks/hecks/deploy.world +12 -0
  214. data/lib/hecks/hecks/environments/memory.world +10 -0
  215. data/lib/hecks/hecks/governance.world +7 -0
  216. data/lib/hecks/hecks/hecks.behaviors +62 -0
  217. data/lib/hecks/hecks/hecks.bluebook +124 -0
  218. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  219. data/lib/hecks/hecks/hecks.world +42 -0
  220. data/lib/hecks/kernel_capabilities.rb +194 -0
  221. data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
  222. data/lib/hecks/language/bluebook/query.bluebook +7 -0
  223. data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
  224. data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
  225. data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
  226. data/lib/hecks/mcp_stdio_guard.rb +2 -2
  227. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  228. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  229. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
  231. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +15 -5
  237. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  238. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
  239. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
  240. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  241. data/lib/hecks/ports/persistence/state_codec.rb +24 -5
  242. data/lib/hecks/projection_files.rb +285 -0
  243. data/lib/hecks/projections/bootstrap_table.rb +1 -1
  244. data/lib/hecks/projections/deploy/fargate.rb +5 -5
  245. data/lib/hecks/projections/deploy/lambda.rb +43 -32
  246. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  247. data/lib/hecks/projections/deploy/preview/template.rb +1 -1
  248. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  249. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  250. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  251. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  252. data/lib/hecks/projections/deploy/shared.rb +1 -1
  253. data/lib/hecks/projections/diagrams.rb +3 -3
  254. data/lib/hecks/projections/model/deviations.rb +1 -1
  255. data/lib/hecks/projections/parser_table.rb +4 -4
  256. data/lib/hecks/projections/reference.rb +2 -3
  257. data/lib/hecks/projections/rust_vocabulary.rb +1 -1
  258. data/lib/hecks/projector/cli_projector.rb +106 -37
  259. data/lib/hecks/projector.rb +1 -1
  260. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  261. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  262. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  263. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  264. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  265. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  266. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  267. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  268. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  269. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  270. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  271. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  272. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  273. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  274. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  275. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  276. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  277. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  278. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  279. data/lib/hecks/quality_control/cli/child.rb +45 -0
  280. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  281. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  282. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  283. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  284. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  285. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  286. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  287. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  288. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  289. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  290. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  291. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  292. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  293. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  294. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  295. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  296. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  297. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  298. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  299. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  300. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  301. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  302. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  303. data/lib/hecks/query_ir.rb +346 -0
  304. data/lib/hecks/query_ir_mcp.rb +198 -0
  305. data/lib/hecks/release/runner/ci_publisher.rb +2 -2
  306. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  307. data/lib/hecks/release/runner/console.rb +6 -0
  308. data/lib/hecks/release/runner/gem_publisher.rb +14 -7
  309. data/lib/hecks/release/runner/npm_publisher.rb +6 -36
  310. data/lib/hecks/release/runner/preflight.rb +26 -2
  311. data/lib/hecks/release/runner/published.rb +8 -26
  312. data/lib/hecks/release/runner/tagger.rb +30 -15
  313. data/lib/hecks/release/runner.rb +57 -6
  314. data/lib/hecks/router/namespace_installer.rb +3 -3
  315. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  316. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
  317. data/lib/hecks/runtime/dependency_planning.rb +20 -2
  318. data/lib/hecks/runtime/dispatcher.rb +53 -3
  319. data/lib/hecks/runtime/interpreting.rb +3 -0
  320. data/lib/hecks/runtime/loader.rb +65 -12
  321. data/lib/hecks/runtime/outbox.rb +1 -1
  322. data/lib/hecks/runtime/policy_interpreter.rb +5 -2
  323. data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
  324. data/lib/hecks/runtime/query_interpreter.rb +77 -0
  325. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  326. data/lib/hecks/runtime/refusal_wording.rb +23 -1
  327. data/lib/hecks/runtime/registry/verification.rb +163 -4
  328. data/lib/hecks/runtime/registry.rb +57 -3
  329. data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
  330. data/lib/hecks/runtime/value/coercion.rb +23 -135
  331. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  332. data/lib/hecks/runtime/value.rb +4 -1
  333. data/lib/hecks/runtime.rb +20 -6
  334. data/lib/hecks/rust_build/conformance.rb +158 -0
  335. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  336. data/lib/hecks/rust_build/coverage.rb +218 -0
  337. data/lib/hecks/rust_build/native_build.rb +124 -0
  338. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  339. data/lib/hecks/rust_build/project_rust.rb +332 -0
  340. data/lib/hecks/rust_build/wasm.rb +105 -0
  341. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  342. data/lib/hecks/rust_build.rb +144 -0
  343. data/lib/hecks/storehouse.rb +16 -16
  344. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
  345. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
  346. data/lib/hecks/three_zero/forms.yml +127 -0
  347. data/lib/hecks/three_zero.rb +12 -0
  348. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  349. data/lib/hecks/tools/comment_style.rb +1070 -0
  350. data/lib/hecks/tools/corpus_report.rb +87 -0
  351. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  352. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  353. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  354. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  355. data/lib/hecks/tools/era_reattest.rb +146 -0
  356. data/lib/hecks/tools/evolve_run.rb +189 -0
  357. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  358. data/lib/hecks/tools/heki_compaction.rb +100 -0
  359. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  360. data/lib/hecks/tools/journal_compaction.rb +89 -0
  361. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  362. data/lib/hecks/tools/query_ir_run.rb +81 -0
  363. data/lib/hecks/tools/regeneration_run.rb +166 -0
  364. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  365. data/lib/hecks/tools/sequence_script.rb +62 -0
  366. data/lib/hecks/tools/tail_merge.rb +117 -0
  367. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  368. data/lib/hecks/tools/translation_audit.rb +208 -0
  369. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  370. data/lib/hecks/tools.rb +86 -0
  371. data/lib/hecks/version.rb +1 -1
  372. data/lib/hecks.rb +32 -15
  373. data/qa/settings.yml +66 -0
  374. data/rust/.cargo/config.toml +6 -0
  375. data/rust/.gitignore +2 -0
  376. data/rust/Cargo.lock +54 -0
  377. data/rust/Cargo.toml +61 -0
  378. data/rust/build/.cargo/config.toml +3 -0
  379. data/rust/build/.gitignore +1 -0
  380. data/rust/build/Cargo.lock +7 -0
  381. data/rust/build/Cargo.toml +19 -0
  382. data/rust/build/src/build_artifact.rs +47 -0
  383. data/rust/build/src/cargo_sync.rs +248 -0
  384. data/rust/build/src/json.rs +421 -0
  385. data/rust/build/src/lineage_pass.rs +285 -0
  386. data/rust/build/src/main.rs +51 -0
  387. data/rust/build/src/optional_pass.rs +150 -0
  388. data/rust/build/src/pipeline.rs +264 -0
  389. data/rust/build/src/reserved_names.rs +76 -0
  390. data/rust/build/src/resolve.rs +124 -0
  391. data/rust/build/src/root.rs +25 -0
  392. data/rust/build/src/sidecars.rs +73 -0
  393. data/rust/build/src/subprocess.rs +51 -0
  394. data/rust/build/src/tmp.rs +31 -0
  395. data/rust/codegen/.cargo/config.toml +3 -0
  396. data/rust/codegen/.gitignore +1 -0
  397. data/rust/codegen/Cargo.lock +7 -0
  398. data/rust/codegen/Cargo.toml +20 -0
  399. data/rust/codegen/src/attr.rs +32 -0
  400. data/rust/codegen/src/bridging.rs +481 -0
  401. data/rust/codegen/src/commands.rs +1226 -0
  402. data/rust/codegen/src/constraints.rs +109 -0
  403. data/rust/codegen/src/dependency_planning.rs +209 -0
  404. data/rust/codegen/src/domain_generator.rs +1288 -0
  405. data/rust/codegen/src/exemplar.rs +230 -0
  406. data/rust/codegen/src/expr_emitter.rs +87 -0
  407. data/rust/codegen/src/fielded.rs +192 -0
  408. data/rust/codegen/src/hecks_naming.rs +33 -0
  409. data/rust/codegen/src/json.rs +335 -0
  410. data/rust/codegen/src/json_codec.rs +697 -0
  411. data/rust/codegen/src/literal.rs +207 -0
  412. data/rust/codegen/src/main.rs +290 -0
  413. data/rust/codegen/src/manifest.rs +227 -0
  414. data/rust/codegen/src/mutations.rs +784 -0
  415. data/rust/codegen/src/naming.rs +286 -0
  416. data/rust/codegen/src/ports.rs +208 -0
  417. data/rust/codegen/src/prelude.rs +116 -0
  418. data/rust/codegen/src/queries.rs +786 -0
  419. data/rust/codegen/src/reactions.rs +532 -0
  420. data/rust/codegen/src/read_models.rs +697 -0
  421. data/rust/codegen/src/reference_specs.rs +40 -0
  422. data/rust/codegen/src/registry.rs +966 -0
  423. data/rust/codegen/src/reserved_names.rs +76 -0
  424. data/rust/codegen/src/shared.rs +93 -0
  425. data/rust/codegen/src/skip_reason.rs +25 -0
  426. data/rust/codegen/src/types.rs +362 -0
  427. data/rust/host/.cargo/config.toml +3 -0
  428. data/rust/host/.gitignore +1 -0
  429. data/rust/host/Cargo.lock +4297 -0
  430. data/rust/host/Cargo.toml +61 -0
  431. data/rust/host/HECKS_RELEASE +1 -0
  432. data/rust/host/rds-ca-bundle.pem +2736 -0
  433. data/rust/host/src/api.rs +1596 -0
  434. data/rust/host/src/approval.rs +652 -0
  435. data/rust/host/src/auth.rs +1287 -0
  436. data/rust/host/src/bin/expr_harness.rs +46 -0
  437. data/rust/host/src/bin/lineage_harness.rs +119 -0
  438. data/rust/host/src/bin/mint_harness.rs +135 -0
  439. data/rust/host/src/checkout.rs +265 -0
  440. data/rust/host/src/dispatch.rs +1443 -0
  441. data/rust/host/src/expr_json.rs +1117 -0
  442. data/rust/host/src/field_hints.rs +52 -0
  443. data/rust/host/src/ir.rs +733 -0
  444. data/rust/host/src/journal.rs +874 -0
  445. data/rust/host/src/lambda_client.rs +378 -0
  446. data/rust/host/src/log.rs +147 -0
  447. data/rust/host/src/main.rs +474 -0
  448. data/rust/host/src/mint.rs +1865 -0
  449. data/rust/host/src/payments/keystore.rs +330 -0
  450. data/rust/host/src/payments/tests.rs +1400 -0
  451. data/rust/host/src/payments.rs +767 -0
  452. data/rust/host/src/presentation.rs +698 -0
  453. data/rust/host/src/presentation_write.rs +1431 -0
  454. data/rust/host/src/query_step.rs +149 -0
  455. data/rust/host/src/rate_limit.rs +904 -0
  456. data/rust/host/src/reference_transform.rs +241 -0
  457. data/rust/host/src/reference_validate.rs +338 -0
  458. data/rust/host/src/resend.rs +296 -0
  459. data/rust/host/src/secrets.rs +106 -0
  460. data/rust/host/src/server.rs +663 -0
  461. data/rust/host/src/storage_shape.rs +175 -0
  462. data/rust/host/src/test_pg.rs +40 -0
  463. data/rust/host/src/ui_schema.rs +1351 -0
  464. data/rust/host/src/wasm_runner.rs +191 -0
  465. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  466. data/rust/host/src/web/newsletter/tests.rs +104 -0
  467. data/rust/host/src/web/newsletter.rs +310 -0
  468. data/rust/host/src/web/newsletter_send.rs +357 -0
  469. data/rust/host/src/web/registrations.rs +1505 -0
  470. data/rust/host/src/web.rs +2937 -0
  471. data/rust/lsp/.gitignore +1 -0
  472. data/rust/lsp/Cargo.lock +7 -0
  473. data/rust/lsp/Cargo.toml +16 -0
  474. data/rust/lsp/README.md +133 -0
  475. data/rust/lsp/src/diagnostics.rs +165 -0
  476. data/rust/lsp/src/json.rs +385 -0
  477. data/rust/lsp/src/main.rs +399 -0
  478. data/rust/lsp/src/outline.rs +177 -0
  479. data/rust/lsp/src/rpc.rs +67 -0
  480. data/rust/parser/.cargo/config.toml +5 -0
  481. data/rust/parser/.gitignore +1 -0
  482. data/rust/parser/Cargo.lock +7 -0
  483. data/rust/parser/Cargo.toml +21 -0
  484. data/rust/parser/src/build/closed_sets.rs +27 -0
  485. data/rust/parser/src/build/identity.rs +276 -0
  486. data/rust/parser/src/build/mod.rs +12 -0
  487. data/rust/parser/src/build/naming.rs +141 -0
  488. data/rust/parser/src/build/pattern_subset.rs +161 -0
  489. data/rust/parser/src/build/query_derive.rs +72 -0
  490. data/rust/parser/src/build/query_inference.rs +269 -0
  491. data/rust/parser/src/build/query_options.rs +100 -0
  492. data/rust/parser/src/build/read_model.rs +94 -0
  493. data/rust/parser/src/build/references.rs +32 -0
  494. data/rust/parser/src/canonical.rs +114 -0
  495. data/rust/parser/src/diag.rs +55 -0
  496. data/rust/parser/src/emit.rs +953 -0
  497. data/rust/parser/src/expr/ast_json.rs +147 -0
  498. data/rust/parser/src/expr/evaluator.rs +81 -0
  499. data/rust/parser/src/expr/mod.rs +84 -0
  500. data/rust/parser/src/expr/resolver.rs +497 -0
  501. data/rust/parser/src/ir.rs +387 -0
  502. data/rust/parser/src/keywords.rs +585 -0
  503. data/rust/parser/src/lex.rs +662 -0
  504. data/rust/parser/src/main.rs +252 -0
  505. data/rust/parser/src/parse/aggregate.rs +612 -0
  506. data/rust/parser/src/parse/chapter.rs +517 -0
  507. data/rust/parser/src/parse/command.rs +637 -0
  508. data/rust/parser/src/parse/domain_port.rs +127 -0
  509. data/rust/parser/src/parse/entity.rs +527 -0
  510. data/rust/parser/src/parse/file.rs +67 -0
  511. data/rust/parser/src/parse/hecksagon.rs +231 -0
  512. data/rust/parser/src/parse/lifecycle.rs +162 -0
  513. data/rust/parser/src/parse/mod.rs +1262 -0
  514. data/rust/parser/src/parse/policy.rs +87 -0
  515. data/rust/parser/src/parse/process_manager.rs +305 -0
  516. data/rust/parser/src/parse/query.rs +100 -0
  517. data/rust/parser/src/parse/read_model.rs +214 -0
  518. data/rust/parser/src/parse/value_object.rs +250 -0
  519. data/rust/parser/src/ruby_value.rs +422 -0
  520. data/rust/project/bridging.rb +257 -0
  521. data/rust/project/commands.rb +877 -0
  522. data/rust/project/constraints.rb +86 -0
  523. data/rust/project/dependency_planning.rb +178 -0
  524. data/rust/project/domain_generator.rb +980 -0
  525. data/rust/project/exemplar.rb +148 -0
  526. data/rust/project/expr_emitter.rb +72 -0
  527. data/rust/project/fielded.rb +141 -0
  528. data/rust/project/json_codec.rb +641 -0
  529. data/rust/project/mutations.rb +721 -0
  530. data/rust/project/naming.rb +205 -0
  531. data/rust/project/ports.rb +65 -0
  532. data/rust/project/queries.rb +493 -0
  533. data/rust/project/reactions.rb +317 -0
  534. data/rust/project/read_models.rb +690 -0
  535. data/rust/project/reference_specs.rb +27 -0
  536. data/rust/project/registry.rb +396 -0
  537. data/rust/project/skip_reason.rb +29 -0
  538. data/rust/project/types.rb +225 -0
  539. data/rust/project/write_if_changed.rb +83 -0
  540. data/rust/project.rb +30 -0
  541. data/rust/project_rust_pipeline.rb +383 -0
  542. data/rust/src/exemplar/commands.rs +279 -0
  543. data/rust/src/exemplar/constraints.rs +18 -0
  544. data/rust/src/exemplar/fielded.rs +280 -0
  545. data/rust/src/exemplar/json.rs +277 -0
  546. data/rust/src/exemplar/mod.rs +17 -0
  547. data/rust/src/exemplar/mutations.rs +80 -0
  548. data/rust/src/exemplar/queries.rs +33 -0
  549. data/rust/src/exemplar/reactions.rs +88 -0
  550. data/rust/src/exemplar/read_models.rs +48 -0
  551. data/rust/src/exemplar/registry.rs +166 -0
  552. data/rust/src/exemplar/types.rs +44 -0
  553. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  554. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  555. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  556. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  557. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  558. data/rust/src/kernel/cli.rs +585 -0
  559. data/rust/src/kernel/dispatch.rs +1090 -0
  560. data/rust/src/kernel/expr.rs +458 -0
  561. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  562. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  563. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  564. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  565. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  566. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  567. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  568. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  569. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  570. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  571. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  572. data/rust/src/kernel/expression_operators/text.rs +79 -0
  573. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  574. data/rust/src/kernel/json.rs +779 -0
  575. data/rust/src/kernel/mod.rs +133 -0
  576. data/rust/src/kernel/named_query.rs +219 -0
  577. data/rust/src/kernel/orchestrate.rs +1810 -0
  578. data/rust/src/kernel/pattern.rs +307 -0
  579. data/rust/src/kernel/query_comparators.rs +206 -0
  580. data/rust/src/kernel/query_ordering.rs +103 -0
  581. data/rust/src/kernel/read_model.rs +914 -0
  582. data/rust/src/kernel/reference_lookup.rs +149 -0
  583. data/rust/src/kernel/repository.rs +468 -0
  584. data/rust/src/kernel/routing.rs +612 -0
  585. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  586. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  587. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  588. data/rust/src/kernel/vocab/mod.rs +20 -0
  589. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  590. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  591. data/rust/src/lib.rs +8 -0
  592. data/rust/src/main.rs +16 -0
  593. data/rust/web/.gitignore +1 -0
  594. data/rust/web/Cargo.lock +119 -0
  595. data/rust/web/Cargo.toml +18 -0
  596. data/rust/web/src/lib.rs +8 -0
  597. metadata +477 -12
  598. data/lib/hecks/facade/cli_door.rb +0 -96
  599. data/lib/hecks/facade/cli_runner.rb +0 -127
  600. data/lib/hecks/mcp_door_scope.rb +0 -95
@@ -0,0 +1,27 @@
1
+ module RustProjection
2
+ module Projector
3
+ module_function
4
+
5
+ # Per-attribute reference specs for one declaring node, computed at codegen time.
6
+ # Mirrors the walk in `CommandRules::References#dereference`.
7
+ def reference_specs(domain_name, attributes)
8
+ attributes.filter_map do |attr|
9
+ target = reference_target(attr[:type])
10
+ next unless target
11
+
12
+ { field: attr[:name].to_s, as_name: attr[:name].to_s.sub(/_id\z/, ""), target: "#{domain_name}::#{target}" }
13
+ end
14
+ end
15
+
16
+ def emit_reference_spec(spec)
17
+ "crate::kernel::ReferenceSpec { field: #{spec[:field].inspect}, as_name: #{spec[:as_name].inspect}, target: #{spec[:target].inspect} }"
18
+ end
19
+
20
+ # `&[]` when empty: call sites need a `&'static [ReferenceSpec]` expression.
21
+ def emit_reference_specs_literal(specs)
22
+ return "&[]" if specs.empty?
23
+
24
+ "&[#{specs.map { |s| emit_reference_spec(s) }.join(', ')}]"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,396 @@
1
+ # Emits the generated Rust registry and JSON command router for a projected domain.
2
+ module RustProjection
3
+ module Projector
4
+ module_function
5
+
6
+ # Statement refusing a caller whose role differs from the command's declared `role:`.
7
+ # Emitted at router level, where the caller's role arrives as `caller_role`.
8
+ # `command_name` is the short name ("Open"), matching Ruby's refusal wording.
9
+ def emit_role_check(role, command_name)
10
+ return nil unless role
11
+
12
+ Exemplar.render("role_check", '"TmplRole"' => role.inspect, '"TmplCommandName"' => command_name.to_s.inspect)
13
+ end
14
+
15
+ # The `kernel::ArgumentGates` literal for one command, consumed by
16
+ # `decode_aggregate_arguments`/`decode_entity_arguments`, which own the step order.
17
+ # Role and reference checks are closures because they need `store`.
18
+ def emit_argument_gates_literal(args_path, invariant_check_lines, role_line, reference_lines)
19
+ normalize = ["let args = #{args_path}::from_json(v)?;", *Array(invariant_check_lines).map { |line| squeeze(line) }, "Ok(args)"].join(" ")
20
+ role = role_line ? "&|| { #{squeeze(role_line)} Ok(()) }" : "&|| Ok(())"
21
+ references =
22
+ if reference_lines.empty?
23
+ "&|_args: &#{args_path}| Ok(())"
24
+ else
25
+ "&|args: &#{args_path}| { #{reference_lines.map { |line| squeeze(line) }.join(' ')} Ok(()) }"
26
+ end
27
+
28
+ "crate::kernel::ArgumentGates { " \
29
+ "decode_arguments: &#{args_path}::decode_arguments, " \
30
+ "refuse_unknown_arguments: &#{args_path}::refuse_unknown_arguments, " \
31
+ "refuse_absent_arguments: &#{args_path}::refuse_absent_arguments, " \
32
+ "normalize_args: &|v: &crate::kernel::Json| { #{normalize} }, " \
33
+ "refuse_role_mismatch: #{role}, " \
34
+ "resolve_references: #{references} }"
35
+ end
36
+
37
+ # Collapses a multi-line emitted statement to one line so it can sit in a closure.
38
+ def squeeze(text) = text.to_s.split("\n").map(&:strip).reject(&:empty?).join(" ")
39
+
40
+ def emit_reference_check(check)
41
+ ident = rust_ident_field(check[:field])
42
+ target_subs = { '"TmplTarget"' => check[:target_name].inspect, '"tmpl_heads"' => check[:heads].inspect }
43
+
44
+ if check[:list_item]
45
+ list_subs = { "tmpl_target_mod" => check[:target_mod], "tmpl_list_field" => ident,
46
+ "&item.tmpl_element_field" => check[:list_item] }
47
+ Exemplar.render("reference_check_list", target_subs.merge(list_subs))
48
+ elsif check[:optional]
49
+ Exemplar.render("reference_check_optional", target_subs.merge("tmpl_target_mod" => check[:target_mod], "tmpl_optional_field" => ident))
50
+ else
51
+ Exemplar.render("reference_check_required", target_subs.merge("tmpl_target_mod" => check[:target_mod], "tmpl_field" => ident))
52
+ end
53
+ end
54
+
55
+ # Rust statement refusing a cross-tenant reference (`check` is built by
56
+ # `tenant_boundary_checks` in domain_generator.rb). Hand-built, not an Exemplar: the target
57
+ # accessor is either a single-attribute value object or a bare scalar, which the fixed
58
+ # reference_check_* templates have no slot for. `{:?}` matches Ruby's `#inspect` for both.
59
+ def emit_tenant_boundary_check(check)
60
+ ref_ident = rust_ident_field(check[:reference_field])
61
+ own_expr = "args.#{rust_ident_field(check[:own_accessor])}.clone()"
62
+
63
+ target_accessor = check[:target_accessor]
64
+ target_expr =
65
+ if target_accessor.include?(".")
66
+ head, inner = target_accessor.split(".", 2)
67
+ "record.#{rust_ident_field(head)}.as_ref().map(|v| v.#{rust_ident_field(inner)}.clone())"
68
+ else
69
+ "record.#{rust_ident_field(target_accessor)}.clone()"
70
+ end
71
+
72
+ "if let Some(record) = store.#{check[:target_mod]}.find(&args.#{ref_ident}) { " \
73
+ "if let Some(target_tenant) = #{target_expr} { " \
74
+ "let own_tenant = #{own_expr}; " \
75
+ "if target_tenant != own_tenant { " \
76
+ "return Err(crate::kernel::Refusal::Unauthorized(crate::kernel::refusal_wording::UnauthorizedCrossTenantReferenceArgs { " \
77
+ "aggregate: #{check[:aggregate_name].inspect}, " \
78
+ "field: #{check[:own_tenant_field].to_s.inspect}, " \
79
+ "tenant: &format!(\"{:?}\", own_tenant), " \
80
+ "attribute: #{check[:reference_field].to_s.inspect}, " \
81
+ "target: #{check[:target_name].inspect}, " \
82
+ "target_field: #{check[:target_tenant_field].to_s.inspect}, " \
83
+ "other: &format!(\"{:?}\", target_tenant)" \
84
+ " }.render_args())); " \
85
+ "} } }"
86
+ end
87
+
88
+ # Builds registry.rs: `Store`, `instances()`, `from_seed`, the query lookup and the JSON
89
+ # command router. `aggregates` is accumulated by domain_generator.rb. Paths are absolute
90
+ # (`crate::generated::<chapter>::<mod>`) so single- and multi-chapter callers resolve alike;
91
+ # `domain_name` is read per aggregate so a merged registry labels each dump correctly.
92
+ def emit_registry(aggregates)
93
+ chapter_path = ->(a) { "crate::generated::#{a[:chapter_mod]}::#{a[:mod]}" }
94
+ store_fields = aggregates.map { |a| " pub #{a[:mod]}: crate::kernel::InMemoryRepository<#{chapter_path.call(a)}::#{a[:record]}>," }
95
+ store_inits = aggregates.map { |a| " #{a[:mod]}: crate::kernel::InMemoryRepository::new()," }
96
+
97
+ dump_arms = aggregates.map do |a|
98
+ prefix = "#{a[:domain_name]}::#{a[:name]}#"
99
+ <<~RUST.rstrip
100
+ for (id, record) in self.#{a[:mod]}.entries() {
101
+ instances.push((format!("{}{}", #{prefix.inspect}, id), record.to_json()));
102
+ }
103
+ RUST
104
+ end
105
+
106
+ # Inverse of `instances()`: one `if let` per aggregate on its "Domain::Aggregate#" prefix.
107
+ # Unrecognized seed keys are skipped, not refused, so a host can seed loosely.
108
+ seed_arms = aggregates.map do |a|
109
+ mod_path = chapter_path.call(a)
110
+ prefix = "#{a[:domain_name]}::#{a[:name]}#"
111
+ <<~RUST.rstrip
112
+ if let Some(id) = key.strip_prefix(#{prefix.inspect}) {
113
+ store.#{a[:mod]}.save(id, #{mod_path}::#{a[:record]}::from_json(value)?);
114
+ continue;
115
+ }
116
+ RUST
117
+ end
118
+
119
+ # Query lookup: one `if` per aggregate on the bare "Domain::Aggregate" prefix (no "#";
120
+ # the whole string names one aggregate). Called by the "query" step in kernel/cli.rs.
121
+ query_arms = aggregates.map do |a|
122
+ prefix = "#{a[:domain_name]}::#{a[:name]}"
123
+ <<~RUST.rstrip
124
+ if aggregate == #{prefix.inspect} {
125
+ return Some(self.#{a[:mod]}.json_entries(|record| record.to_json()).map(|(id, json)| (id.clone(), json.clone())).collect());
126
+ }
127
+ RUST
128
+ end
129
+
130
+ # Borrowed twin of `query_arms`: the same prefixes, visiting each cached row in place.
131
+ scan_each_arms = aggregates.map do |a|
132
+ prefix = "#{a[:domain_name]}::#{a[:name]}"
133
+ <<~RUST.rstrip
134
+ if aggregate == #{prefix.inspect} {
135
+ for (id, json) in self.#{a[:mod]}.json_entries(|record| record.to_json()) {
136
+ visit(id, json);
137
+ }
138
+ return true;
139
+ }
140
+ RUST
141
+ end
142
+
143
+ aggregate_arms = aggregates.flat_map do |a|
144
+ mod_path = chapter_path.call(a)
145
+ a[:commands].map do |c|
146
+ # Identity extras (bare `identified_by` heads that are not attributes) are read from
147
+ # `facts_json`: no
148
+ # typed struct carries — then passed positionally into
149
+ # `dispatch_call` in commands.rb's `fn_signature` order. Absent is `NotFound`.
150
+ extra_names = Array(c[:identity_extra_params])
151
+ extra_idents = extra_names.map { |name| rust_ident_field(name) }
152
+ extra_lines = extra_names.zip(extra_idents).map do |name, ident|
153
+ "let #{ident} = facts_json.dig(#{name.to_s.inspect}).ok_or_else(|| crate::kernel::Refusal::NotFound(#{"#{c[:verb]} creates a #{a[:record]} — pass #{name}".inspect}.to_string()))?.to_id_component()?;"
154
+ end
155
+ extra_pass = extra_idents.map { |ident| "&#{ident}, " }.join
156
+
157
+ # A creating command's `dispatch_*` takes `route` right after `repo`.
158
+ # `tenant_boundary_check` is computed eagerly here and applied deferred in `dispatch()`.
159
+ dispatch_call = "#{mod_path}::dispatch_#{c[:fn]}(&mut store.#{a[:mod]}, #{c[:creates] ? "route, #{extra_pass}" : '&id, '}args, mutations, owner_deref, command_deref, tenant_boundary_check)"
160
+ # The route supplies an acting command's `id`; otherwise `extract_id` reads `facts_json`.
161
+ # A wholly missing identity is `NotFound` (acting_no_identity), as in `hydrate_existing`;
162
+ # `render_args` keeps the wording declared in one place.
163
+ acting_no_identity_args =
164
+ "crate::kernel::refusal_wording::NotFoundActingNoIdentityArgs { " \
165
+ "command: #{c[:name].to_s.inspect}, aggregate: #{a[:record].to_s.inspect}, " \
166
+ "identity: #{Array(a[:identified_by]).join(', ').inspect} }.render_args()"
167
+ # Route depth is checked before the argument gates for a flat-facts call and after them
168
+ # for an explicit `with:` call, as in Ruby's `Invocation.from_call`. A fixed order
169
+ # either way regressed, so `explicit_with()` picks the order per call.
170
+ route_precheck_line = "if let Some(route) = route { route.require_depth(0)?; }"
171
+ not_found_expr = "crate::kernel::Refusal::NotFound(#{acting_no_identity_args})"
172
+ # Identity resolution runs after every argument gate, as Ruby's `step_hydrate` does, so a
173
+ # malformed identity never masks an unknown, absent or ill-typed argument.
174
+ id_line =
175
+ if c[:creates]
176
+ nil
177
+ else
178
+ "let id = match route { Some(route) => route.aggregate().to_string(), None => #{mod_path}::#{a[:record]}::extract_id(facts_json).map_err(|_| #{not_found_expr})?, };"
179
+ end
180
+ # The kernel runs the argument gates in `AggregateStep::ORDER`; this arm does not.
181
+ # Role and reference checks are closures because they need `store`, and they finish
182
+ # borrowing it before `dispatch_call` takes its `&mut`.
183
+ gates_expr = "crate::kernel::decode_aggregate_arguments(facts_json, &#{emit_argument_gates_literal("#{mod_path}::#{c[:args_struct]}", c[:invariant_check_lines], emit_role_check(c[:role], c[:name]), c[:reference_checks].map { |check| emit_reference_check(check) })})?"
184
+ # One if/else expression, so exactly one route-check order runs per call.
185
+ args_line =
186
+ "let args = if invocation.explicit_with() { let args = #{gates_expr}; #{route_precheck_line} args } " \
187
+ "else { #{route_precheck_line} #{gates_expr} };"
188
+ # Computed here because it needs `store`; applied deferred by `kernel::dispatch()` where
189
+ # Ruby's `resolve_state_references` runs, after every earlier dispatch step. Each check's
190
+ # `return Err(...)` sits in a closure so the first violation is captured as a value.
191
+ tenant_boundary_check_bodies = Array(c[:tenant_boundary_checks]).map { |check| emit_tenant_boundary_check(check) }
192
+ tenant_boundary_check_line =
193
+ if tenant_boundary_check_bodies.empty?
194
+ "let tenant_boundary_check: Result<(), crate::kernel::Refusal> = Ok(());"
195
+ else
196
+ "let tenant_boundary_check: Result<(), crate::kernel::Refusal> = " \
197
+ "(|| -> Result<(), crate::kernel::Refusal> { #{tenant_boundary_check_bodies.join(' ')} Ok(()) })();"
198
+ end
199
+
200
+ # `given`/`ensures` cross-aggregate dereference is resolved here because it needs `store`
201
+ # and must end its borrow before the `&mut store.<mod>` in the dispatch call. A creating
202
+ # command has no record yet, so `owner_deref` is empty; `command_deref` covers it.
203
+ owner_deref_expr = c[:creates] ? "Vec::new()" : "crate::kernel::owner_deref(&*store, REFERENCE_TABLE, #{"#{a[:domain_name]}::#{a[:name]}".inspect}, &id)"
204
+ deref_lines = [
205
+ "let owner_deref = #{owner_deref_expr};",
206
+ "let command_deref = crate::kernel::command_deref(&*store, REFERENCE_TABLE, #{emit_reference_specs_literal(c[:reference_specs])}, &args);",
207
+ ]
208
+
209
+ body = ["let invocation = crate::kernel::CommandInvocation::from_json(args_json)?;",
210
+ "let route = invocation.route();",
211
+ "let facts_json = invocation.facts();",
212
+ args_line,
213
+ # Identity resolves after the gates; identity extras are read with it.
214
+ id_line, *extra_lines, tenant_boundary_check_line,
215
+ *deref_lines,
216
+ "let payload = crate::kernel::Json::overlay(facts_json, &args.to_json());",
217
+ "#{dispatch_call}.map(|(_, events)| stamp_payload(events, &payload))"].compact.reject(&:empty?)
218
+
219
+ " #{c[:verb].inspect} => {\n#{body.map { |line| " #{line}" }.join("\n")}\n }"
220
+ end
221
+ end
222
+
223
+ # Entity commands never create: parent and element identity are both read from the raw
224
+ # JSON. The generated function is `dispatch_entity_<fn>`.
225
+ entity_arms = aggregates.flat_map do |a|
226
+ mod_path = chapter_path.call(a)
227
+ a[:entity_commands].map do |c|
228
+ # Argument gates: see `aggregate_arms`; `decode_entity_arguments` walks
229
+ # `EntityStep::ORDER`.
230
+ gates_line = "let args = crate::kernel::decode_entity_arguments(facts_json, &#{emit_argument_gates_literal("#{mod_path}::#{c[:args_struct]}", c[:invariant_check_lines], emit_role_check(c[:role], c[:name]), c[:reference_checks].map { |check| emit_reference_check(check) })})?;"
231
+ dispatch_call = "#{mod_path}::dispatch_entity_#{c[:fn]}(&mut store.#{a[:mod]}, &parent_id, &element_id, &element_wants, args, mutations, owner_deref, command_deref).map(|(_, events)| stamp_payload(events, &payload))"
232
+
233
+ # `element_wants` is the caller-offered scalar identity values (`emit_extract_wants`),
234
+ # computed unconditionally alongside `element_id`.
235
+ # `owner_deref` is taken off `parent_id`, like an aggregate `Act`, so re-seeded parent
236
+ # `projects` fields resolve; `command_deref` also gets a "parent" node for `parent.X`
237
+ # givens.
238
+ # An entity's own reference attributes are not dereferenced (none in the corpus has one).
239
+ # An explicit route addresses one entity (`require_depth(1)`) and is checked ahead of the
240
+ # argument gates, as Ruby resolves `to:` independently of facts; unrouted calls fall back
241
+ # to `extract_id`/`extract_wants`. A failed `extract_id` becomes Ruby's NotFound wording
242
+ # (entity_parent_no_identity, entity_element_no_identity), not `TypeMismatch`.
243
+ entity_parent_no_identity_message = "#{c[:name]} acts on a #{a[:record]}'s #{c[:entity_name]} — pass #{Array(a[:identified_by]).join(', ')}:"
244
+ entity_element_no_identity_message = "#{c[:name]} acts on one #{c[:entity_name]} — pass #{c[:entity_identity_reading]}:"
245
+ body = ["let invocation = crate::kernel::CommandInvocation::from_json(args_json)?;",
246
+ "let route = invocation.route();",
247
+ "let facts_json = invocation.facts();",
248
+ # `element_id` uses `extract_id_lenient`: only an absent identity key is refused;
249
+ # a blank one flows through as a non-matching id. `parent_id` stays strict.
250
+ "if let Some(route) = route { route.require_depth(1)?; }",
251
+ gates_line,
252
+ "let (parent_id, element_id, element_wants) = match route { Some(route) => { let element_id = route.entities()[0].clone(); (route.aggregate().to_string(), element_id.clone(), element_id) }, None => { let parent_id = #{mod_path}::#{a[:record]}::extract_id(facts_json).map_err(|_| crate::kernel::Refusal::NotFound(#{entity_parent_no_identity_message.inspect}.to_string()))?; let element_id = #{mod_path}::#{c[:entity_record]}::extract_id_lenient(facts_json).map_err(|_| crate::kernel::Refusal::NotFound(#{entity_element_no_identity_message.inspect}.to_string()))?; let element_wants = #{mod_path}::#{c[:entity_record]}::extract_wants(facts_json); (parent_id, element_id, element_wants) }, };",
253
+ "let owner_deref = crate::kernel::owner_deref(&*store, REFERENCE_TABLE, #{"#{a[:domain_name]}::#{a[:name]}".inspect}, &parent_id);",
254
+ "let mut command_deref = crate::kernel::command_deref(&*store, REFERENCE_TABLE, #{emit_reference_specs_literal(c[:reference_specs])}, &args);",
255
+ "if let Some(parent_node) = crate::kernel::parent_deref(&*store, REFERENCE_TABLE, #{"#{a[:domain_name]}::#{a[:name]}".inspect}, &parent_id) { command_deref.push((\"parent\", parent_node)); }",
256
+ "let payload = crate::kernel::Json::overlay(facts_json, &args.to_json());",
257
+ dispatch_call].compact.reject(&:empty?)
258
+
259
+ " #{c[:verb].inspect} => {\n#{body.map { |line| " #{line}" }.join("\n")}\n }"
260
+ end
261
+ end
262
+
263
+ # Commands owned by an entity nested two levels deep
264
+ # (`dispatch_entity_<entity>_<nested>_<fn>`). With `c[:unrouted_supported]` the arm mirrors
265
+ # `entity_arms` one hop deeper; otherwise it requires an explicit route (`require_depth(2)`).
266
+ nested_entity_arms = aggregates.flat_map do |a|
267
+ mod_path = chapter_path.call(a)
268
+ Array(a[:nested_entity_commands]).map do |c|
269
+ # Argument gates: see `aggregate_arms`; `decode_entity_arguments` walks
270
+ # `EntityStep::ORDER`.
271
+ gates_line = "let args = crate::kernel::decode_entity_arguments(facts_json, &#{emit_argument_gates_literal("#{mod_path}::#{c[:args_struct]}", c[:invariant_check_lines], emit_role_check(c[:role], c[:name]), c[:reference_checks].map { |check| emit_reference_check(check) })})?;"
272
+ dispatch_call = "#{mod_path}::dispatch_entity_#{c[:fn]}(&mut store.#{a[:mod]}, &parent_id, &hop1_id, &hop1_wants, &hop2_id, &hop2_wants, args, mutations, owner_deref, command_deref).map(|(_, events)| stamp_payload(events, &payload))"
273
+
274
+ # Gates run before identity and the depth check leads, as in `entity_arms`. A failed
275
+ # `extract_id` per hop becomes that hop's own NotFound wording, as in `locate_chain`;
276
+ # the parent uses the joined entity path.
277
+ entity_parent_no_identity_message = "#{c[:name]} acts on a #{a[:record]}'s #{c[:entity_name]}.#{c[:nested_name]} — pass #{Array(a[:identified_by]).join(', ')}:"
278
+ hop1_no_identity_message = "#{c[:name]} acts on one #{c[:entity_name]} — pass #{c[:entity_identity_reading]}:"
279
+ hop2_no_identity_message = "#{c[:name]} acts on one #{c[:nested_name]} — pass #{c[:nested_identity_reading]}:"
280
+ # Every hop uses `extract_id_lenient` (see `entity_arms`); `parent_id` stays strict.
281
+ route_binding =
282
+ if c[:unrouted_supported]
283
+ "let (parent_id, hop1_id, hop1_wants, hop2_id, hop2_wants) = match route { Some(route) => { let hop1_id = route.entities()[0].clone(); let hop2_id = route.entities()[1].clone(); (route.aggregate().to_string(), hop1_id.clone(), hop1_id, hop2_id.clone(), hop2_id) }, None => { let parent_id = #{mod_path}::#{a[:record]}::extract_id(facts_json).map_err(|_| crate::kernel::Refusal::NotFound(#{entity_parent_no_identity_message.inspect}.to_string()))?; let hop1_id = #{mod_path}::#{c[:entity_record]}::extract_id_lenient(facts_json).map_err(|_| crate::kernel::Refusal::NotFound(#{hop1_no_identity_message.inspect}.to_string()))?; let hop1_wants = #{mod_path}::#{c[:entity_record]}::extract_wants(facts_json); let hop2_id = #{mod_path}::#{c[:nested_record]}::extract_id_lenient(facts_json).map_err(|_| crate::kernel::Refusal::NotFound(#{hop2_no_identity_message.inspect}.to_string()))?; let hop2_wants = #{mod_path}::#{c[:nested_record]}::extract_wants(facts_json); (parent_id, hop1_id, hop1_wants, hop2_id, hop2_wants) }, };"
284
+ else
285
+ "let route = route.ok_or_else(|| crate::kernel::Refusal::TypeMismatch(#{"#{c[:verb]} addresses an entity nested two levels deep — requires an explicit to: { aggregate:, entities: [...] } route".inspect}.to_string()))?; let parent_id = route.aggregate().to_string(); let hop1_id = route.entities()[0].clone(); let hop2_id = route.entities()[1].clone(); let hop1_wants = hop1_id.clone(); let hop2_wants = hop2_id.clone();"
286
+ end
287
+
288
+ body = ["let invocation = crate::kernel::CommandInvocation::from_json(args_json)?;",
289
+ "let route = invocation.route();",
290
+ "let facts_json = invocation.facts();",
291
+ "if let Some(route) = route { route.require_depth(2)?; }",
292
+ gates_line,
293
+ route_binding,
294
+ # `owner_deref` uses the top-level aggregate's reference fields.
295
+ "let owner_deref = crate::kernel::owner_deref(&*store, REFERENCE_TABLE, #{"#{a[:domain_name]}::#{a[:name]}".inspect}, &parent_id);",
296
+ "let command_deref = crate::kernel::command_deref(&*store, REFERENCE_TABLE, #{emit_reference_specs_literal(c[:reference_specs])}, &args);",
297
+ "let payload = crate::kernel::Json::overlay(facts_json, &args.to_json());",
298
+ dispatch_call].compact.reject(&:empty?)
299
+
300
+ " #{c[:verb].inspect} => {\n#{body.map { |line| " #{line}" }.join("\n")}\n }"
301
+ end
302
+ end
303
+
304
+ # Port operations: a command arm without `creates` or a role check (a port has no caller
305
+ # role). Reference checks are emitted here because they need `store`. The receiver comes from
306
+ # the route or `legacy_receiver_field` and is stripped from the facts `from_json` sees; its
307
+ # existence is checked once here.
308
+ port_arms = aggregates.flat_map do |a|
309
+ mod_path = chapter_path.call(a)
310
+ a[:ports].map do |p|
311
+ reference_lines = p[:reference_checks].map { |check| emit_reference_check(check) }
312
+ legacy_receiver = p[:legacy_receiver_field] ? "Some(#{p[:legacy_receiver_field].to_s.inspect})" : "None"
313
+ to_receiver = p[:to_receiver_field] ? "Some(#{p[:to_receiver_field].to_s.inspect})" : "None"
314
+ dispatch_call = "#{mod_path}::dispatch_operation_#{p[:fn]}(&id, args).map(|events| stamp_payload(events, &payload))"
315
+
316
+ body = ["let invocation = crate::kernel::CommandInvocation::from_json(args_json)?;",
317
+ "let (id, port_facts) = invocation.split_aggregate_receiver(#{legacy_receiver}, #{to_receiver})?;",
318
+ "let facts_json = &port_facts;",
319
+ "let _instance = store.#{a[:mod]}.find(&id).ok_or_else(|| crate::kernel::Refusal::NotFound(format!(\"#{a[:name]} {:?} does not exist\", id)))?;",
320
+ "let args = #{mod_path}::#{p[:args_struct]}::from_json(facts_json)?;", *reference_lines,
321
+ "let payload = crate::kernel::Json::overlay(facts_json, &args.to_json());",
322
+ dispatch_call].compact
323
+
324
+ " #{p[:verb].inspect} => {\n#{body.map { |line| " #{line}" }.join("\n")}\n }"
325
+ end
326
+ end
327
+
328
+ dispatch_arms = aggregate_arms + entity_arms + nested_entity_arms + port_arms
329
+
330
+ header = <<~RUST
331
+ // GENERATED by hecks project_rust — the JSON command router
332
+ // `kernel::cli` dispatches every step through. Do not hand-edit —
333
+ // re-run hecks project_rust instead.
334
+ #![allow(dead_code, unused_variables)]
335
+
336
+ // `Repository::save` (from_seed, below) is a TRAIT method —
337
+ // `InMemoryRepository`'s own inherent methods (entries(), used
338
+ // by instances()) need no import, but save() does.
339
+ use crate::kernel::Repository;
340
+
341
+ RUST
342
+
343
+ body = Exemplar.render(
344
+ "registry_file",
345
+ "TmplStore2" => "Store",
346
+ " pub tmpl_field: crate::kernel::InMemoryRepository<i64>," => store_fields.join("\n"),
347
+ " tmpl_field: tmpl_store_fields_placeholder()," => store_inits.join("\n"),
348
+ "tmpl_dump_arm_placeholder();" => dump_arms.join("\n"),
349
+ "tmpl_seed_arm_placeholder();" => seed_arms.join("\n"),
350
+ "tmpl_query_arm_placeholder();" => query_arms.join("\n"),
351
+ "tmpl_scan_each_arm_placeholder();" => scan_each_arms.join("\n"),
352
+ '"tmpl_verb" => { tmpl_dispatch_arm_placeholder() }' => dispatch_arms.join("\n")
353
+ )
354
+
355
+ "#{header}#{body}"
356
+ end
357
+
358
+ # The domain-wide `REFERENCE_TABLE` plus `Store`'s `ReferenceLookup` impl, which needs
359
+ # `store` to fetch targets. One row per aggregate, from `a[:reference_specs]`.
360
+ def emit_reference_table(aggregates)
361
+ rows = aggregates.map do |a|
362
+ qualified = "#{a[:domain_name]}::#{a[:name]}"
363
+ " (#{qualified.inspect}, #{emit_reference_specs_literal(a[:reference_specs])}),"
364
+ end
365
+
366
+ <<~RUST
367
+ pub static REFERENCE_TABLE: crate::kernel::ReferenceTable = &[
368
+ #{rows.join("\n")}
369
+ ];
370
+ RUST
371
+ end
372
+
373
+ # `find_fielded`: one `if` per aggregate, boxing the found record as a type-erased
374
+ # `Fielded`. An undeclared or ungenerated target (e.g. cross-domain) yields `None`.
375
+ def emit_reference_lookup(aggregates)
376
+ arms = aggregates.map do |a|
377
+ prefix = "#{a[:domain_name]}::#{a[:name]}"
378
+ <<~RUST.rstrip
379
+ if target == #{prefix.inspect} {
380
+ return self.#{a[:mod]}.find(id).map(|r| Box::new(r) as Box<dyn crate::kernel::Fielded>);
381
+ }
382
+ RUST
383
+ end
384
+
385
+ <<~RUST
386
+ #{emit_reference_table(aggregates)}
387
+ impl crate::kernel::ReferenceLookup for Store {
388
+ fn find_fielded(&self, target: &str, id: &str) -> Option<Box<dyn crate::kernel::Fielded>> {
389
+ #{arms.join("\n")}
390
+ None
391
+ }
392
+ }
393
+ RUST
394
+ end
395
+ end
396
+ end
@@ -0,0 +1,29 @@
1
+ module RustProjection
2
+ module Projector
3
+ module_function
4
+
5
+ # A skip reason: the message text (a String) plus the machine-readable
6
+ # `construct` family (`rootless`, `optional_source`, ...) that forced the skip.
7
+ #
8
+ # Consumers read `construct` from manifest.json, so the prose is free to change.
9
+ # Re-wording an inner reason must go through `reskip`; interpolation drops `construct`.
10
+ class SkipReason < String
11
+ attr_reader :construct
12
+
13
+ def initialize(construct, text)
14
+ raise ArgumentError, "a skip reason needs a construct family" if construct.to_s.empty?
15
+
16
+ super(text)
17
+ @construct = construct.to_s.freeze
18
+ end
19
+ end
20
+
21
+ def skip(construct, text) = SkipReason.new(construct, text)
22
+
23
+ # Re-words `inner` while keeping its construct; `fallback` applies only to a plain String.
24
+ def reskip(inner, text, fallback: nil)
25
+ construct = inner.respond_to?(:construct) ? inner.construct : fallback
26
+ SkipReason.new(construct, text)
27
+ end
28
+ end
29
+ end