hecks 2.9.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +4 -2
  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 +7 -4
  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 +6 -3
  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,248 @@
1
+ //! Rewrites the root `generated/mod.rs` and syncs `rust/Cargo.toml` `[features]`.
2
+ //! Ports `sync_mod_and_cargo!`, keeping its output byte-identical.
3
+
4
+ use std::path::Path;
5
+
6
+ // Both tables come from the reserved-word vocabularies via `hecks project_reserved_names`.
7
+ use crate::reserved_names::{CARGO_RESERVED_DOMAIN_NAMES, RUST_KEYWORDS};
8
+
9
+ /// True if `name` works as a directory, a bare Rust module identifier and a Cargo feature key.
10
+ pub fn valid_domain_mod_name(name: &str) -> bool {
11
+ let mut chars = name.chars();
12
+ let starts_ok = matches!(chars.next(), Some(c) if c == '_' || c.is_ascii_lowercase());
13
+ let rest_ok = chars.all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit());
14
+ starts_ok && rest_ok && !RUST_KEYWORDS.contains(&name) && !CARGO_RESERVED_DOMAIN_NAMES.contains(&name)
15
+ }
16
+
17
+ /// Lists the directories under `out_root`, and those with a `merged.rs` (activatable domains).
18
+ fn scan_generated_dirs(out_root: &Path) -> Result<(Vec<String>, Vec<String>), String> {
19
+ let mut all_dirs = Vec::new();
20
+ for entry in std::fs::read_dir(out_root).map_err(|e| format!("reading {}: {e}", out_root.display()))? {
21
+ let entry = entry.map_err(|e| format!("reading {}: {e}", out_root.display()))?;
22
+ if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
23
+ all_dirs.push(entry.file_name().to_string_lossy().to_string());
24
+ }
25
+ }
26
+ all_dirs.sort();
27
+
28
+ let domains = all_dirs.iter().filter(|name| out_root.join(name).join("merged.rs").is_file()).cloned().collect();
29
+
30
+ Ok((all_dirs, domains))
31
+ }
32
+
33
+ /// Rewrites the root `mod.rs`: a `pub mod` per directory and an `active` re-export per domain.
34
+ ///
35
+ /// Cargo has no mutually-exclusive features, so the default domain's re-export backs off when
36
+ /// another domain feature is on, and two non-default features hit an explicit `compile_error!`
37
+ /// instead of a duplicate-import E0252.
38
+ fn write_root_mod(out_root: &Path, all_dirs: &[String], domains: &[String], target_mod_name: &str) -> Result<(), String> {
39
+ let mut out = String::new();
40
+ out.push_str("// GENERATED by hecks project_rust — re-run it to refresh this list.\n");
41
+ out.push_str("//\n");
42
+ out.push_str("// BUG#25 — every DOMAIN (an entry in `domains`, below: a directory\n");
43
+ out.push_str("// carrying its own merged.rs and therefore its own Cargo feature) is\n");
44
+ out.push_str("// declared behind that SAME feature, so a broken or absent domain's\n");
45
+ out.push_str("// generated code is never even compiled into a build that selects a\n");
46
+ out.push_str("// DIFFERENT domain — `cargo build --features waybill` used to pull in\n");
47
+ out.push_str("// every OTHER domain's `pub mod` unconditionally, so one domain's own\n");
48
+ out.push_str("// non-compiling generated code (a `has_many` field, before this fix)\n");
49
+ out.push_str("// broke every feature's build, not just its own. A shared FRAMEWORK\n");
50
+ out.push_str("// chapter (governance/identity — no merged.rs of its own, so absent\n");
51
+ out.push_str("// from `domains`) stays unconditional: it has no single feature of its\n");
52
+ out.push_str("// own to gate behind, and is compiled in by whichever domain(s) attach\n");
53
+ out.push_str("// it via `uses_framework`.\n");
54
+ for name in all_dirs {
55
+ if domains.iter().any(|d| d == name) {
56
+ out.push_str(&format!("#[cfg(feature = {name:?})]\n"));
57
+ }
58
+ out.push_str(&format!("pub mod {name};\n"));
59
+ }
60
+ out.push('\n');
61
+ out.push_str("// `active` is whichever ONE domain's own merged Store/dispatch table\n");
62
+ out.push_str("// is selected by Cargo feature (rust/Cargo.toml, kept in sync with this\n");
63
+ out.push_str("// list by hecks project_rust) — kernel/cli.rs imports\n");
64
+ out.push_str("// `crate::generated::active::{dispatch_by_name, Store, ...}` unchanged\n");
65
+ out.push_str("// no matter which domain that resolves to.\n");
66
+ out.push_str("//\n");
67
+ out.push_str("// DOMAIN FEATURES ARE MUTUALLY EXCLUSIVE (R5) — Cargo has no native\n");
68
+ out.push_str(&format!("// concept of that, and `default = [{target_mod_name:?}]` in\n"));
69
+ out.push_str("// Cargo.toml stays enabled unless a build passes\n");
70
+ out.push_str("// `--no-default-features`, so a plain `cargo build --features banking`\n");
71
+ out.push_str("// used to enable BOTH the default domain's feature and banking's,\n");
72
+ out.push_str("// producing two `pub use ... as active;` imports and a hard E0252.\n");
73
+ out.push_str("// Fixed two ways below, entirely mechanically from `domains`/\n");
74
+ out.push_str("// `target_mod_name` (this run's new default) — never hand-maintained:\n");
75
+ out.push_str("//\n");
76
+ out.push_str("// 1. THE DEFAULT DOMAIN'S OWN re-export backs off whenever any OTHER\n");
77
+ out.push_str("// domain feature is also enabled, so it never contends for `active`\n");
78
+ out.push_str("// by accident — `cargo build --features banking` (still implicitly\n");
79
+ out.push_str("// carrying the default feature) now resolves to banking alone,\n");
80
+ out.push_str("// same as if `--no-default-features` had been passed explicitly.\n");
81
+ out.push_str("// 2. Two NON-default domain features enabled TOGETHER (a genuine,\n");
82
+ out.push_str("// not-accidental conflict — nothing makes that combination\n");
83
+ out.push_str("// meaningful) still fail the build, but with an explicit\n");
84
+ out.push_str("// `compile_error!` naming both features instead of a cryptic\n");
85
+ out.push_str("// \"defined multiple times\".\n");
86
+ for name in domains {
87
+ let other_domains: Vec<&String> = domains.iter().filter(|n| n.as_str() != name.as_str()).collect();
88
+ if name.as_str() == target_mod_name && !other_domains.is_empty() {
89
+ let guard = other_domains.iter().map(|o| format!("feature = {o:?}")).collect::<Vec<_>>().join(", ");
90
+ out.push_str(&format!("#[cfg(all(feature = {name:?}, not(any({guard}))))]\n"));
91
+ } else {
92
+ out.push_str(&format!("#[cfg(feature = {name:?})]\n"));
93
+ }
94
+ out.push_str(&format!("pub use {name}::merged as active;\n"));
95
+ }
96
+
97
+ let non_default_domains: Vec<&String> = domains.iter().filter(|n| n.as_str() != target_mod_name).collect();
98
+ if non_default_domains.len() > 1 {
99
+ out.push('\n');
100
+ out.push_str("// Explicit conflict guard — see point 2 above. Only fires when TWO\n");
101
+ out.push_str("// non-default domain features are both turned on; the default\n");
102
+ out.push_str("// domain's own feature never reaches here (point 1 already excludes\n");
103
+ out.push_str("// it whenever any other domain feature is present).\n");
104
+ let domains_joined = domains.join(", ");
105
+ for i in 0..non_default_domains.len() {
106
+ for j in (i + 1)..non_default_domains.len() {
107
+ let a = non_default_domains[i];
108
+ let b = non_default_domains[j];
109
+ out.push_str(&format!("#[cfg(all(feature = {a:?}, feature = {b:?}))]\n"));
110
+ out.push_str(&format!(
111
+ "compile_error!(\"domain features are mutually exclusive — enable only one of: {domains_joined} (both {a} and {b} are enabled)\");\n"
112
+ ));
113
+ }
114
+ }
115
+ }
116
+
117
+ let path = out_root.join("mod.rs");
118
+ std::fs::write(&path, out).map_err(|e| format!("writing {}: {e}", path.display()))?;
119
+ println!("wrote {}", path.display());
120
+ Ok(())
121
+ }
122
+
123
+ /// The `[features]` table text, from its header to the next `[section]` header or EOF.
124
+ /// Empty if there is no `[features]` header.
125
+ fn features_table_slice(text: &str) -> &str {
126
+ let Some(start) = text.find("[features]") else { return "" };
127
+ let rest = &text[start..];
128
+ let mut pos = 0;
129
+ for (i, line) in rest.split_inclusive('\n').enumerate() {
130
+ if i > 0 && line.trim_start().starts_with('[') {
131
+ return &rest[..pos];
132
+ }
133
+ pos += line.len();
134
+ }
135
+ rest
136
+ }
137
+
138
+ fn sync_cargo_toml_features(cargo_toml_path: &Path, domains: &[String], target_mod_name: &str) -> Result<(), String> {
139
+ let mut cargo_toml = std::fs::read_to_string(cargo_toml_path).map_err(|e| format!("reading {}: {e}", cargo_toml_path.display()))?;
140
+
141
+ if !cargo_toml.contains("[features]") {
142
+ let preamble = "# ONE FEATURE PER GENERATED DOMAIN — selects which domain's own\n\
143
+ # generated::<domain>::merged module `generated::active` re-exports\n\
144
+ # (rust/src/generated/mod.rs). Kept in sync by hecks project_rust: a\n\
145
+ # feature is added here whenever a new domain is generated, never\n\
146
+ # removed (the domain's own generated/<name>/ directory is the\n\
147
+ # source of truth for whether it still exists). `default` tracks\n\
148
+ # whichever domain was generated MOST RECENTLY, so a bare\n\
149
+ # `cargo build`/`cargo test` keeps behaving exactly as it always\n\
150
+ # has; any other still-generated domain stays reachable via\n\
151
+ # `--no-default-features --features <name>`.\n\
152
+ [features]\n\
153
+ default = []\n\n";
154
+ cargo_toml = format!("{preamble}{cargo_toml}");
155
+ }
156
+
157
+ // Scoped to `[features]`: matching `name = ` file-wide hits `[package]` keys like `version`.
158
+ for name in domains {
159
+ let already_declared = features_table_slice(&cargo_toml).lines().any(|line| {
160
+ line.trim_start().strip_prefix(name.as_str()).map(|rest| rest.trim_start().starts_with('=')).unwrap_or(false)
161
+ });
162
+ if already_declared {
163
+ continue;
164
+ }
165
+ cargo_toml = insert_after_default(&cargo_toml, &format!("{name} = []"));
166
+ }
167
+
168
+ cargo_toml = replace_default_line(&cargo_toml, &format!("default = [{:?}]", target_mod_name));
169
+
170
+ std::fs::write(cargo_toml_path, &cargo_toml).map_err(|e| format!("writing {}: {e}", cargo_toml_path.display()))?;
171
+ println!("wrote {} (default feature: {target_mod_name})", cargo_toml_path.display());
172
+ Ok(())
173
+ }
174
+
175
+ fn find_default_line_span(text: &str) -> Option<(usize, usize)> {
176
+ let mut pos = 0;
177
+ for line in text.split_inclusive('\n') {
178
+ let content = line.strip_suffix('\n').unwrap_or(line);
179
+ if is_default_line(content) {
180
+ return Some((pos, pos + content.len()));
181
+ }
182
+ pos += line.len();
183
+ }
184
+ None
185
+ }
186
+
187
+ // Always re-finds the original `default` line, so features added in one run land in reverse
188
+ // order, matching the Ruby pipeline byte-for-byte.
189
+ fn insert_after_default(text: &str, new_line: &str) -> String {
190
+ match find_default_line_span(text) {
191
+ Some((_start, end)) => {
192
+ let mut out = String::with_capacity(text.len() + new_line.len() + 1);
193
+ out.push_str(&text[..end]);
194
+ out.push('\n');
195
+ out.push_str(new_line);
196
+ out.push_str(&text[end..]);
197
+ out
198
+ }
199
+ None => text.to_string(),
200
+ }
201
+ }
202
+
203
+ fn replace_default_line(text: &str, new_line: &str) -> String {
204
+ match find_default_line_span(text) {
205
+ Some((start, end)) => format!("{}{}{}", &text[..start], new_line, &text[end..]),
206
+ None => text.to_string(),
207
+ }
208
+ }
209
+
210
+ fn is_default_line(line_content: &str) -> bool {
211
+ let trimmed = line_content.trim_start();
212
+ trimmed.strip_prefix("default").map(|rest| rest.trim_start().starts_with('=')).unwrap_or(false)
213
+ }
214
+
215
+ /// Rewrites the root `mod.rs` and syncs `rust/Cargo.toml`'s `[features]` for the domains on disk.
216
+ pub fn run(out_root: &Path, cargo_toml_path: &Path, target_mod_name: &str) -> Result<(), String> {
217
+ let (all_dirs, domains) = scan_generated_dirs(out_root)?;
218
+ write_root_mod(out_root, &all_dirs, &domains, target_mod_name)?;
219
+ sync_cargo_toml_features(cargo_toml_path, &domains, target_mod_name)?;
220
+ Ok(())
221
+ }
222
+
223
+ #[cfg(test)]
224
+ mod tests {
225
+ use super::*;
226
+
227
+ #[test]
228
+ fn inserts_after_the_default_line() {
229
+ let text = "[features]\ndefault = []\n\n";
230
+ let result = insert_after_default(text, "pizzas = []");
231
+ assert_eq!(result, "[features]\ndefault = []\npizzas = []\n\n");
232
+ }
233
+
234
+ #[test]
235
+ fn later_insertions_land_before_earlier_ones_matching_rubys_quirk() {
236
+ let text = "[features]\ndefault = []\n";
237
+ let after_a = insert_after_default(text, "a = []");
238
+ let after_b = insert_after_default(&after_a, "b = []");
239
+ assert_eq!(after_b, "[features]\ndefault = []\nb = []\na = []\n");
240
+ }
241
+
242
+ #[test]
243
+ fn replaces_the_default_line_content() {
244
+ let text = "[features]\ndefault = []\npizzas = []\n";
245
+ let result = replace_default_line(text, "default = [\"banking\"]");
246
+ assert_eq!(result, "[features]\ndefault = [\"banking\"]\npizzas = []\n");
247
+ }
248
+ }
@@ -0,0 +1,421 @@
1
+ //! A minimal, dependency-free, mutable JSON value with a reader and a pretty writer.
2
+ //! The writer matches `JSON.pretty_generate` byte-for-byte, including its empty-array quirk.
3
+
4
+ use std::fmt::Write as _;
5
+
6
+ #[derive(Debug, Clone, PartialEq)]
7
+ pub enum Json {
8
+ Null,
9
+ Bool(bool),
10
+ /// Raw literal text as scanned, so numbers round-trip unchanged.
11
+ Number(String),
12
+ String(String),
13
+ Array(Vec<Json>),
14
+ /// Insertion-ordered pairs; byte-exact re-emission depends on key order.
15
+ Object(Vec<(String, Json)>),
16
+ }
17
+
18
+ impl Json {
19
+ pub fn parse(text: &str) -> Result<Json, String> {
20
+ let mut p = Parser { bytes: text.as_bytes(), pos: 0 };
21
+ p.skip_ws();
22
+ let value = p.parse_value()?;
23
+ p.skip_ws();
24
+ if p.pos != p.bytes.len() {
25
+ return Err(format!("trailing content at byte {}", p.pos));
26
+ }
27
+ Ok(value)
28
+ }
29
+
30
+ /// Looks up `key`, treating a `null` value as missing.
31
+ pub fn get(&self, key: &str) -> Option<&Json> {
32
+ match self {
33
+ Json::Object(pairs) => pairs.iter().find(|(k, v)| k == key && !matches!(v, Json::Null)).map(|(_, v)| v),
34
+ _ => None,
35
+ }
36
+ }
37
+
38
+ pub fn get_mut(&mut self, key: &str) -> Option<&mut Json> {
39
+ match self {
40
+ Json::Object(pairs) => pairs.iter_mut().find(|(k, _)| k == key).map(|(_, v)| v),
41
+ _ => None,
42
+ }
43
+ }
44
+
45
+ pub fn as_str(&self) -> Option<&str> {
46
+ match self {
47
+ Json::String(s) => Some(s.as_str()),
48
+ _ => None,
49
+ }
50
+ }
51
+
52
+ pub fn as_array(&self) -> Option<&[Json]> {
53
+ match self {
54
+ Json::Array(items) => Some(items.as_slice()),
55
+ _ => None,
56
+ }
57
+ }
58
+
59
+ pub fn as_array_mut(&mut self) -> Option<&mut Vec<Json>> {
60
+ match self {
61
+ Json::Array(items) => Some(items),
62
+ _ => None,
63
+ }
64
+ }
65
+
66
+ pub fn as_object(&self) -> Option<&[(String, Json)]> {
67
+ match self {
68
+ Json::Object(pairs) => Some(pairs.as_slice()),
69
+ _ => None,
70
+ }
71
+ }
72
+
73
+ /// Ruby truthiness: everything except `false` and `null` is true.
74
+ pub fn as_bool(&self) -> bool {
75
+ !matches!(self, Json::Bool(false) | Json::Null)
76
+ }
77
+
78
+ /// Iterates an array field; empty for any other value.
79
+ pub fn each(&self) -> &[Json] {
80
+ self.as_array().unwrap_or(&[])
81
+ }
82
+
83
+ /// Sets `key` to `value`, the only mutation the optional-marking pass performs.
84
+ /// Overwrites the existing pair, or appends one if absent.
85
+ pub fn set_bool(&mut self, key: &str, value: bool) {
86
+ if let Json::Object(pairs) = self {
87
+ if let Some((_, existing)) = pairs.iter_mut().find(|(k, _)| k == key) {
88
+ *existing = Json::Bool(value);
89
+ return;
90
+ }
91
+ pairs.push((key.to_string(), Json::Bool(value)));
92
+ }
93
+ }
94
+
95
+ /// Sets `key` to `value`, overwriting in place or else appending last.
96
+ pub fn set(&mut self, key: &str, value: Json) {
97
+ if let Json::Object(pairs) = self {
98
+ if let Some((_, existing)) = pairs.iter_mut().find(|(k, _)| k == key) {
99
+ *existing = value;
100
+ return;
101
+ }
102
+ pairs.push((key.to_string(), value));
103
+ }
104
+ }
105
+ }
106
+
107
+ struct Parser<'a> {
108
+ bytes: &'a [u8],
109
+ pos: usize,
110
+ }
111
+
112
+ impl<'a> Parser<'a> {
113
+ fn peek(&self) -> Option<u8> {
114
+ self.bytes.get(self.pos).copied()
115
+ }
116
+
117
+ fn skip_ws(&mut self) {
118
+ while let Some(b) = self.peek() {
119
+ if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' {
120
+ self.pos += 1;
121
+ } else {
122
+ break;
123
+ }
124
+ }
125
+ }
126
+
127
+ fn expect(&mut self, b: u8) -> Result<(), String> {
128
+ if self.peek() == Some(b) {
129
+ self.pos += 1;
130
+ Ok(())
131
+ } else {
132
+ Err(format!("expected {:?} at byte {}, found {:?}", b as char, self.pos, self.peek().map(|c| c as char)))
133
+ }
134
+ }
135
+
136
+ fn parse_value(&mut self) -> Result<Json, String> {
137
+ self.skip_ws();
138
+ match self.peek() {
139
+ Some(b'{') => self.parse_object(),
140
+ Some(b'[') => self.parse_array(),
141
+ Some(b'"') => Ok(Json::String(self.parse_string()?)),
142
+ Some(b't') => {
143
+ self.expect_literal("true")?;
144
+ Ok(Json::Bool(true))
145
+ }
146
+ Some(b'f') => {
147
+ self.expect_literal("false")?;
148
+ Ok(Json::Bool(false))
149
+ }
150
+ Some(b'n') => {
151
+ self.expect_literal("null")?;
152
+ Ok(Json::Null)
153
+ }
154
+ Some(c) if c == b'-' || c.is_ascii_digit() => self.parse_number(),
155
+ other => Err(format!("unexpected byte {:?} at {}", other.map(|c| c as char), self.pos)),
156
+ }
157
+ }
158
+
159
+ fn expect_literal(&mut self, lit: &str) -> Result<(), String> {
160
+ let end = self.pos + lit.len();
161
+ if end <= self.bytes.len() && &self.bytes[self.pos..end] == lit.as_bytes() {
162
+ self.pos = end;
163
+ Ok(())
164
+ } else {
165
+ Err(format!("expected literal {lit:?} at byte {}", self.pos))
166
+ }
167
+ }
168
+
169
+ fn parse_object(&mut self) -> Result<Json, String> {
170
+ self.expect(b'{')?;
171
+ let mut pairs = Vec::new();
172
+ self.skip_ws();
173
+ if self.peek() == Some(b'}') {
174
+ self.pos += 1;
175
+ return Ok(Json::Object(pairs));
176
+ }
177
+ loop {
178
+ self.skip_ws();
179
+ let key = self.parse_string()?;
180
+ self.skip_ws();
181
+ self.expect(b':')?;
182
+ let value = self.parse_value()?;
183
+ pairs.push((key, value));
184
+ self.skip_ws();
185
+ match self.peek() {
186
+ Some(b',') => {
187
+ self.pos += 1;
188
+ }
189
+ Some(b'}') => {
190
+ self.pos += 1;
191
+ break;
192
+ }
193
+ other => return Err(format!("expected ',' or '}}' at byte {}, found {:?}", self.pos, other.map(|c| c as char))),
194
+ }
195
+ }
196
+ Ok(Json::Object(pairs))
197
+ }
198
+
199
+ fn parse_array(&mut self) -> Result<Json, String> {
200
+ self.expect(b'[')?;
201
+ let mut items = Vec::new();
202
+ self.skip_ws();
203
+ if self.peek() == Some(b']') {
204
+ self.pos += 1;
205
+ return Ok(Json::Array(items));
206
+ }
207
+ loop {
208
+ let value = self.parse_value()?;
209
+ items.push(value);
210
+ self.skip_ws();
211
+ match self.peek() {
212
+ Some(b',') => {
213
+ self.pos += 1;
214
+ }
215
+ Some(b']') => {
216
+ self.pos += 1;
217
+ break;
218
+ }
219
+ other => return Err(format!("expected ',' or ']' at byte {}, found {:?}", self.pos, other.map(|c| c as char))),
220
+ }
221
+ }
222
+ Ok(Json::Array(items))
223
+ }
224
+
225
+ fn parse_string(&mut self) -> Result<String, String> {
226
+ self.expect(b'"')?;
227
+ let mut out = String::new();
228
+ loop {
229
+ match self.peek() {
230
+ None => return Err("unterminated string".to_string()),
231
+ Some(b'"') => {
232
+ self.pos += 1;
233
+ break;
234
+ }
235
+ Some(b'\\') => {
236
+ self.pos += 1;
237
+ match self.peek() {
238
+ Some(b'"') => {
239
+ out.push('"');
240
+ self.pos += 1;
241
+ }
242
+ Some(b'\\') => {
243
+ out.push('\\');
244
+ self.pos += 1;
245
+ }
246
+ Some(b'/') => {
247
+ out.push('/');
248
+ self.pos += 1;
249
+ }
250
+ Some(b'n') => {
251
+ out.push('\n');
252
+ self.pos += 1;
253
+ }
254
+ Some(b't') => {
255
+ out.push('\t');
256
+ self.pos += 1;
257
+ }
258
+ Some(b'r') => {
259
+ out.push('\r');
260
+ self.pos += 1;
261
+ }
262
+ Some(b'b') => {
263
+ out.push('\u{8}');
264
+ self.pos += 1;
265
+ }
266
+ Some(b'f') => {
267
+ out.push('\u{c}');
268
+ self.pos += 1;
269
+ }
270
+ Some(b'u') => {
271
+ self.pos += 1;
272
+ let hex = std::str::from_utf8(&self.bytes[self.pos..self.pos + 4]).map_err(|e| e.to_string())?;
273
+ let code = u32::from_str_radix(hex, 16).map_err(|e| e.to_string())?;
274
+ self.pos += 4;
275
+ if let Some(c) = char::from_u32(code) {
276
+ out.push(c);
277
+ }
278
+ }
279
+ other => return Err(format!("bad escape {:?}", other.map(|c| c as char))),
280
+ }
281
+ }
282
+ Some(_) => {
283
+ // One UTF-8 char at a time so multi-byte text survives.
284
+ let start = self.pos;
285
+ let rest = std::str::from_utf8(&self.bytes[start..]).map_err(|e| e.to_string())?;
286
+ let ch = rest.chars().next().ok_or("empty remainder")?;
287
+ out.push(ch);
288
+ self.pos += ch.len_utf8();
289
+ }
290
+ }
291
+ }
292
+ Ok(out)
293
+ }
294
+
295
+ // Keeps the raw literal text, so numbers round-trip unchanged.
296
+ fn parse_number(&mut self) -> Result<Json, String> {
297
+ let start = self.pos;
298
+ if self.peek() == Some(b'-') {
299
+ self.pos += 1;
300
+ }
301
+ while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
302
+ self.pos += 1;
303
+ }
304
+ if self.peek() == Some(b'.') {
305
+ self.pos += 1;
306
+ while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
307
+ self.pos += 1;
308
+ }
309
+ }
310
+ if matches!(self.peek(), Some(b'e') | Some(b'E')) {
311
+ self.pos += 1;
312
+ if matches!(self.peek(), Some(b'+') | Some(b'-')) {
313
+ self.pos += 1;
314
+ }
315
+ while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
316
+ self.pos += 1;
317
+ }
318
+ }
319
+ let text = std::str::from_utf8(&self.bytes[start..self.pos]).map_err(|e| e.to_string())?;
320
+ Ok(Json::Number(text.to_string()))
321
+ }
322
+ }
323
+
324
+ /// Renders `value` exactly as `JSON.pretty_generate` does for the pinned json gem.
325
+ pub fn write(value: &Json) -> String {
326
+ let mut out = String::new();
327
+ write_value(&mut out, value, 0);
328
+ out
329
+ }
330
+
331
+ fn indent(out: &mut String, depth: usize) {
332
+ for _ in 0..depth {
333
+ out.push_str(" ");
334
+ }
335
+ }
336
+
337
+ fn write_value(out: &mut String, value: &Json, depth: usize) {
338
+ match value {
339
+ Json::Null => out.push_str("null"),
340
+ Json::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
341
+ Json::Number(n) => out.push_str(n),
342
+ Json::String(s) => write_string(out, s),
343
+ Json::Array(items) => write_array(out, items, depth),
344
+ Json::Object(pairs) => write_object(out, pairs, depth),
345
+ }
346
+ }
347
+
348
+ fn write_string(out: &mut String, text: &str) {
349
+ out.push('"');
350
+ for ch in text.chars() {
351
+ match ch {
352
+ '"' => out.push_str("\\\""),
353
+ '\\' => out.push_str("\\\\"),
354
+ '\n' => out.push_str("\\n"),
355
+ '\t' => out.push_str("\\t"),
356
+ '\r' => out.push_str("\\r"),
357
+ c if (c as u32) < 0x20 => {
358
+ let _ = write!(out, "\\u{:04x}", c as u32);
359
+ }
360
+ c => out.push(c),
361
+ }
362
+ }
363
+ out.push('"');
364
+ }
365
+
366
+ fn write_array(out: &mut String, items: &[Json], depth: usize) {
367
+ if items.is_empty() {
368
+ // Empty arrays render with a blank line, matching the pinned json gem.
369
+ out.push_str("[\n\n");
370
+ indent(out, depth);
371
+ out.push(']');
372
+ return;
373
+ }
374
+ out.push_str("[\n");
375
+ for (idx, item) in items.iter().enumerate() {
376
+ indent(out, depth + 1);
377
+ write_value(out, item, depth + 1);
378
+ if idx + 1 < items.len() {
379
+ out.push(',');
380
+ }
381
+ out.push('\n');
382
+ }
383
+ indent(out, depth);
384
+ out.push(']');
385
+ }
386
+
387
+ fn write_object(out: &mut String, pairs: &[(String, Json)], depth: usize) {
388
+ // An empty object needs no special case: the loop runs zero times.
389
+ out.push_str("{\n");
390
+ for (idx, (key, val)) in pairs.iter().enumerate() {
391
+ indent(out, depth + 1);
392
+ write_string(out, key);
393
+ out.push_str(": ");
394
+ write_value(out, val, depth + 1);
395
+ if idx + 1 < pairs.len() {
396
+ out.push(',');
397
+ }
398
+ out.push('\n');
399
+ }
400
+ indent(out, depth);
401
+ out.push('}');
402
+ }
403
+
404
+ #[cfg(test)]
405
+ mod tests {
406
+ use super::*;
407
+
408
+ #[test]
409
+ fn round_trips_an_empty_object() {
410
+ let text = "{\n}";
411
+ let value = Json::parse(text).expect("parses");
412
+ assert_eq!(write(&value), text);
413
+ }
414
+
415
+ #[test]
416
+ fn set_bool_overwrites_an_existing_key() {
417
+ let mut value = Json::parse("{\n \"optional\": false\n}").expect("parses");
418
+ value.set_bool("optional", true);
419
+ assert_eq!(write(&value), "{\n \"optional\": true\n}");
420
+ }
421
+ }