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,779 @@
1
+ //! The JSON value the WASM/CLI boundary speaks, with a hand-written parser and writer.
2
+ //! Kept separate from `expr::Value`; zero Cargo dependencies (ADR 0012).
3
+
4
+ use super::Refusal;
5
+
6
+ #[derive(Debug, Clone)]
7
+ pub enum Json {
8
+ Object(Vec<(String, Json)>),
9
+ Array(Vec<Json>),
10
+ Str(String),
11
+ /// The second field is the exact source text (BUG#147) — set only when the literal's
12
+ /// magnitude means the `f64` approximation would not reproduce it, e.g. an integer past
13
+ /// `f64`'s ~53-bit exact range. `None` for anything the `f64` already renders exactly,
14
+ /// which is every ordinary number and everything built in memory (`Json::float`, an
15
+ /// arithmetic result, a codegen literal). Never set for `Float`: Ruby's own `Float` is the
16
+ /// same IEEE 754 binary64 as Rust's, so there is no cross-language precision gap to close.
17
+ Num(f64, Option<String>),
18
+ /// A declared-Float value: always written with a decimal point (`10.0`), unlike `Num`.
19
+ Float(f64),
20
+ Bool(bool),
21
+ Null,
22
+ }
23
+
24
+ impl PartialEq for Json {
25
+ /// The source text (`Num`'s second field) is provenance, not part of the value: two `Num`s
26
+ /// compare equal by magnitude, exactly as they did before that field existed. When both
27
+ /// sides carry exact text, comparing the text avoids the `f64` rounding that put them there
28
+ /// in the first place; otherwise the numeric comparison is unchanged.
29
+ fn eq(&self, other: &Self) -> bool {
30
+ match (self, other) {
31
+ (Json::Object(a), Json::Object(b)) => a == b,
32
+ (Json::Array(a), Json::Array(b)) => a == b,
33
+ (Json::Str(a), Json::Str(b)) => a == b,
34
+ (Json::Num(_, Some(araw)), Json::Num(_, Some(braw))) => araw == braw,
35
+ (Json::Num(a, _), Json::Num(b, _)) => a == b,
36
+ (Json::Float(a), Json::Float(b)) => a == b,
37
+ (Json::Bool(a), Json::Bool(b)) => a == b,
38
+ (Json::Null, Json::Null) => true,
39
+ _ => false,
40
+ }
41
+ }
42
+ }
43
+
44
+ /// A `Json` as a `Fielded`, for evaluating a policy's `where { ... }` against an event payload.
45
+ impl super::Fielded for Json {
46
+ fn field(&self, name: &str) -> Option<super::Field<'_>> {
47
+ use super::{Field, Value};
48
+ let Json::Object(pairs) = self else { return None };
49
+ let (_, found) = pairs.iter().find(|(k, _)| k == name)?;
50
+ Some(match found {
51
+ Json::Object(_) => Field::Nested(found),
52
+ Json::Array(items) => Field::Value(Value::List(items.len())),
53
+ Json::Str(s) => Field::Value(Value::Str(s.clone())),
54
+ Json::Num(n, raw) => match exact_i64(*n, raw) {
55
+ Some(i) => Field::Value(Value::Int(i)),
56
+ None => Field::Value(Value::Float(*n)),
57
+ },
58
+ // Float already carries the Integer-or-Float answer that `Num` must guess.
59
+ Json::Float(n) => Field::Value(Value::Float(*n)),
60
+ Json::Bool(b) => Field::Value(Value::Bool(*b)),
61
+ Json::Null => Field::Value(Value::Nil),
62
+ })
63
+ }
64
+
65
+ fn items(&self, name: &str) -> Option<Vec<super::Field<'_>>> {
66
+ use super::{Field, Value};
67
+ let Json::Object(pairs) = self else { return None };
68
+ let (_, found) = pairs.iter().find(|(k, _)| k == name)?;
69
+ let Json::Array(items) = found else { return None };
70
+ Some(
71
+ items
72
+ .iter()
73
+ .map(|item| match item {
74
+ Json::Object(_) => Field::Nested(item),
75
+ Json::Array(inner) => Field::Value(Value::List(inner.len())),
76
+ Json::Str(s) => Field::Value(Value::Str(s.clone())),
77
+ Json::Num(n, raw) => match exact_i64(*n, raw) {
78
+ Some(i) => Field::Value(Value::Int(i)),
79
+ None => Field::Value(Value::Float(*n)),
80
+ },
81
+ Json::Float(n) => Field::Value(Value::Float(*n)),
82
+ Json::Bool(b) => Field::Value(Value::Bool(*b)),
83
+ Json::Null => Field::Value(Value::Nil),
84
+ })
85
+ .collect(),
86
+ )
87
+ }
88
+
89
+ // A `Json` has no declaration to consult, so one key of any name is the whole shape test.
90
+ fn as_scalar(&self) -> Option<super::Value> {
91
+ let Json::Object(pairs) = self else { return None };
92
+ if pairs.len() != 1 {
93
+ return None;
94
+ }
95
+ match self.field(&pairs[0].0) {
96
+ Some(super::Field::Value(v)) => Some(v),
97
+ _ => None,
98
+ }
99
+ }
100
+ }
101
+
102
+ impl Json {
103
+ pub fn obj(fields: Vec<(&str, Json)>) -> Json {
104
+ Json::Object(fields.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
105
+ }
106
+
107
+ pub fn str(s: impl Into<String>) -> Json {
108
+ Json::Str(s.into())
109
+ }
110
+
111
+ /// The raw text is always the exact `i64`, not derived from the `f64` cast: `i as f64`
112
+ /// itself rounds near the `i64` boundaries, which would otherwise reintroduce BUG#147's
113
+ /// class of loss for an in-range value built straight from a Rust integer.
114
+ pub fn int(i: i64) -> Json {
115
+ Json::Num(i as f64, Some(i.to_string()))
116
+ }
117
+
118
+ pub fn float(f: f64) -> Json {
119
+ Json::Float(f)
120
+ }
121
+
122
+ pub fn get(&self, key: &str) -> Option<&Json> {
123
+ match self {
124
+ Json::Object(fields) => fields.iter().find(|(k, _)| k == key).map(|(_, v)| v),
125
+ _ => None,
126
+ }
127
+ }
128
+
129
+ /// A tolerant dotted-path walk: a non-object reached mid-path is the answer.
130
+ ///
131
+ /// A `Reference<X>` argument arrives as a bare string where a wrapped value is expected.
132
+ pub fn dig(&self, path: &str) -> Option<&Json> {
133
+ let mut current = self;
134
+ for segment in path.split('.') {
135
+ match current {
136
+ Json::Object(_) => current = current.get(segment)?,
137
+ _ => return Some(current),
138
+ }
139
+ }
140
+ Some(current)
141
+ }
142
+
143
+ /// Ruby's `#inspect` for quoting an offered value in a refusal message.
144
+ ///
145
+ /// An Array prints as `[5, 4]` (comma and space), not the compact wire form;
146
+ /// an Object stays compact JSON.
147
+ pub fn inspect(&self) -> String {
148
+ match self {
149
+ Json::Str(s) => {
150
+ let mut out = String::new();
151
+ write_escaped_string(s, &mut out);
152
+ out
153
+ }
154
+ Json::Null => "nil".to_string(),
155
+ Json::Array(items) => {
156
+ let mut out = String::from("[");
157
+ for (i, item) in items.iter().enumerate() {
158
+ if i > 0 {
159
+ out.push_str(", ");
160
+ }
161
+ out.push_str(&item.inspect());
162
+ }
163
+ out.push(']');
164
+ out
165
+ }
166
+ // Numbers and bools already print like Ruby; an Object stays compact JSON.
167
+ _ => self.to_json_string(),
168
+ }
169
+ }
170
+
171
+ /// Ruby's `#to_s` for a raw offered value, as a closed-set membership check compares it.
172
+ ///
173
+ /// Arrays, Objects and fractions fall back to compact JSON; they never match a member.
174
+ pub fn ruby_to_s(&self) -> String {
175
+ match self {
176
+ Json::Str(s) => s.clone(),
177
+ Json::Null => String::new(),
178
+ Json::Bool(b) => b.to_string(),
179
+ Json::Num(n, _) if n.fract() == 0.0 && n.abs() < 1e15 => format!("{}", *n as i64),
180
+ _ => self.to_json_string(),
181
+ }
182
+ }
183
+
184
+ pub fn as_str(&self) -> Option<&str> {
185
+ match self {
186
+ Json::Str(s) => Some(s),
187
+ _ => None,
188
+ }
189
+ }
190
+
191
+ /// `None` for a fractional number or one outside `i64`, never a truncation or saturation.
192
+ ///
193
+ /// Reads the exact source text first (BUG#147): a whole number between `f64`'s ~53-bit exact
194
+ /// range and `i64::MAX` round-trips through `f64` inexactly even though it fits in `i64`.
195
+ pub fn as_i64(&self) -> Option<i64> {
196
+ match self {
197
+ Json::Num(n, raw) => exact_i64(*n, raw),
198
+ _ => None,
199
+ }
200
+ }
201
+
202
+ pub fn as_f64(&self) -> Option<f64> {
203
+ match self {
204
+ Json::Num(n, _) | Json::Float(n) => Some(*n),
205
+ _ => None,
206
+ }
207
+ }
208
+
209
+ pub fn as_bool(&self) -> Option<bool> {
210
+ match self {
211
+ Json::Bool(b) => Some(*b),
212
+ _ => None,
213
+ }
214
+ }
215
+
216
+ pub fn as_array(&self) -> Option<&[Json]> {
217
+ match self {
218
+ Json::Array(items) => Some(items),
219
+ _ => None,
220
+ }
221
+ }
222
+
223
+ /// Wraps a bare scalar as a one-field object under `field_name`; an object passes through.
224
+ ///
225
+ /// Mirrors `Value.for_attribute`'s bare-value admission for a single-attribute value object.
226
+ pub fn coerce_single_field(&self, field_name: &str) -> Json {
227
+ match self {
228
+ Json::Object(_) => self.clone(),
229
+ other => Json::obj(vec![(field_name, other.clone())]),
230
+ }
231
+ }
232
+
233
+ /// How a refusal quotes an offered value: compact JSON for an Array or Object, else `inspect`.
234
+ pub fn describe(&self) -> String {
235
+ match self {
236
+ Json::Array(_) | Json::Object(_) => self.to_json_string(),
237
+ other => other.inspect(),
238
+ }
239
+ }
240
+
241
+ /// Refuses `TypeMismatch` when a multi-field value object is offered as a non-object.
242
+ ///
243
+ /// Runs at the `from_json` call site, since only the caller knows `name`.
244
+ pub fn expect_value_object_shape(&self, name: &str, type_name: &str) -> Result<&Json, Refusal> {
245
+ match self {
246
+ Json::Object(_) => Ok(self),
247
+ other => Err(Refusal::TypeMismatch(
248
+ super::refusal_wording::TypeMismatchValueObjectShapeArgs {
249
+ name,
250
+ r#type: type_name,
251
+ offered: &other.describe(),
252
+ }
253
+ .render_args(),
254
+ )),
255
+ }
256
+ }
257
+
258
+ /// A required-field lookup that refuses `TypeMismatch` when the key is missing.
259
+ pub fn require(&self, key: &str, struct_name: &str) -> Result<&Json, Refusal> {
260
+ self.get(key)
261
+ .ok_or_else(|| Refusal::TypeMismatch(format!("{struct_name}.{key}: missing from JSON args")))
262
+ }
263
+
264
+ /// The keys of a command's args that are not in `known`, sorted.
265
+ ///
266
+ /// Sorted because refusal wording is pinned by corpus fixtures and cannot depend on key order.
267
+ pub fn unknown_keys(&self, known: &[&str]) -> Vec<String> {
268
+ match self {
269
+ Json::Object(fields) => {
270
+ let mut unknown: Vec<String> = fields.iter().filter(|(k, _)| !known.contains(&k.as_str())).map(|(k, _)| k.clone()).collect();
271
+ unknown.sort();
272
+ unknown
273
+ }
274
+ _ => Vec::new(),
275
+ }
276
+ }
277
+
278
+ /// Merges `patch` over `base`: `patch` wins per top-level key, other `base` keys are kept.
279
+ ///
280
+ /// The typed args carry declared defaults that the raw `args_json` lacks, while the raw
281
+ /// args keep identity/reference arguments no attribute names. Non-objects return `base`.
282
+ pub fn overlay(base: &Json, patch: &Json) -> Json {
283
+ let (Json::Object(base_fields), Json::Object(patch_fields)) = (base, patch) else {
284
+ return base.clone();
285
+ };
286
+ let mut merged = base_fields.clone();
287
+ for (key, value) in patch_fields {
288
+ match merged.iter_mut().find(|(k, _)| k == key) {
289
+ Some(entry) => entry.1 = value.clone(),
290
+ None => merged.push((key.clone(), value.clone())),
291
+ }
292
+ }
293
+ Json::Object(merged)
294
+ }
295
+
296
+ /// Adds each target key of `pairs` holding its source key's value, keeping existing args.
297
+ ///
298
+ /// A source key that is absent adds nothing. Non-objects pass through.
299
+ pub fn with_aliases(&self, pairs: &[(&str, &str)]) -> Json {
300
+ let Json::Object(fields) = self else {
301
+ return self.clone();
302
+ };
303
+ let mut merged = fields.clone();
304
+ for (target_key, source_key) in pairs {
305
+ let Some(value) = fields.iter().find(|(k, _)| k == source_key).map(|(_, v)| v.clone()) else { continue };
306
+ match merged.iter_mut().find(|(k, _)| k == target_key) {
307
+ Some(entry) => entry.1 = value,
308
+ None => merged.push((target_key.to_string(), value)),
309
+ }
310
+ }
311
+ Json::Object(merged)
312
+ }
313
+
314
+ /// Stringifies a scalar leaf as an identity component.
315
+ ///
316
+ /// An empty string refuses, as `Runtime::Identity.of` treats a blank part as absent.
317
+ /// A number past `i64` prints its exact source digits (BUG#147), never a clamped or
318
+ /// `f64`-rounded value: an identity component is routing data, never arithmetic, and Ruby
319
+ /// carries it at its full, arbitrary-precision magnitude.
320
+ pub fn to_id_component(&self) -> Result<String, Refusal> {
321
+ match self {
322
+ Json::Str(s) if s.is_empty() => {
323
+ Err(Refusal::TypeMismatch("identity component must not be empty".to_string()))
324
+ }
325
+ Json::Str(s) => Ok(s.clone()),
326
+ Json::Num(n, raw) => match exact_i64(*n, raw) {
327
+ // In range: a plain integer, as Ruby prints it.
328
+ Some(i) => Ok(i.to_string()),
329
+ // Out of range or fractional: the exact source text when there is one (a
330
+ // Bignum past i64), else the float's own digits; `as i64` would saturate.
331
+ None => Ok(raw.clone().unwrap_or_else(|| n.to_string())),
332
+ },
333
+ Json::Bool(b) => Ok(b.to_string()),
334
+ other => Err(Refusal::TypeMismatch(format!("cannot use {other:?} as an identity component"))),
335
+ }
336
+ }
337
+
338
+ /// Like `to_id_component`, but a blank string is accepted, for entity-element addressing.
339
+ ///
340
+ /// A present-but-blank key is an ordinary non-matching value there; non-scalars still refuse.
341
+ pub fn to_id_component_lenient(&self) -> Result<String, Refusal> {
342
+ match self {
343
+ Json::Str(s) => Ok(s.clone()),
344
+ other => other.to_id_component(),
345
+ }
346
+ }
347
+
348
+ pub fn parse(input: &str) -> Result<Json, String> {
349
+ let mut parser = Parser { chars: input.chars().peekable() };
350
+ let value = parser.parse_value()?;
351
+ parser.skip_ws();
352
+ Ok(value)
353
+ }
354
+
355
+ pub fn to_json_string(&self) -> String {
356
+ let mut out = String::new();
357
+ self.write(&mut out);
358
+ out
359
+ }
360
+
361
+ fn write(&self, out: &mut String) {
362
+ match self {
363
+ Json::Object(fields) => {
364
+ out.push('{');
365
+ for (i, (k, v)) in fields.iter().enumerate() {
366
+ if i > 0 {
367
+ out.push(',');
368
+ }
369
+ write_escaped_string(k, out);
370
+ out.push(':');
371
+ v.write(out);
372
+ }
373
+ out.push('}');
374
+ }
375
+ Json::Array(items) => {
376
+ out.push('[');
377
+ for (i, v) in items.iter().enumerate() {
378
+ if i > 0 {
379
+ out.push(',');
380
+ }
381
+ v.write(out);
382
+ }
383
+ out.push(']');
384
+ }
385
+ Json::Str(s) => write_escaped_string(s, out),
386
+ // The exact source text wins when there is one (BUG#147): re-deriving digits from
387
+ // `n` would re-round a value the `f64` approximation already lost precision on.
388
+ Json::Num(_, Some(raw)) => out.push_str(raw),
389
+ Json::Num(n, None) => out.push_str(&canonical_integer_digits(*n)),
390
+ // Always a decimal point (`10.0`), as Ruby's Float#to_json; `Num` must not grow one.
391
+ Json::Float(n) => {
392
+ let rendered = n.to_string();
393
+ out.push_str(&rendered);
394
+ if !rendered.contains('.') && !rendered.contains('e') && !rendered.contains('E') {
395
+ out.push_str(".0");
396
+ }
397
+ }
398
+ Json::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
399
+ Json::Null => out.push_str("null"),
400
+ }
401
+ }
402
+ }
403
+
404
+ /// Whether `n` is a whole number within `i64`; `as i64` would silently saturate outside it.
405
+ ///
406
+ /// `i64::MAX as f64` rounds up to 2^63, so the strict `<` excludes exactly the saturating value.
407
+ fn integral_i64(n: f64) -> Option<i64> {
408
+ if n.fract() == 0.0 && n >= i64::MIN as f64 && n < i64::MAX as f64 {
409
+ Some(n as i64)
410
+ } else {
411
+ None
412
+ }
413
+ }
414
+
415
+ /// A `Num`'s exact `i64` value, when it has one (BUG#147).
416
+ ///
417
+ /// Prefers the source text: `f64` only represents every integer exactly up to 2^53, so a whole
418
+ /// number with more digits than that — while still well within `i64` — can already round to the
419
+ /// wrong `i64` via `integral_i64`'s `f64` path alone. Parsing the original digits sidesteps that
420
+ /// gap; `raw` is `None` for anything short enough that the gap never opens, so this is the same
421
+ /// answer `integral_i64` already gave for every value the old, one-field `Num` could represent.
422
+ fn exact_i64(n: f64, raw: &Option<String>) -> Option<i64> {
423
+ raw.as_deref().and_then(|r| r.parse::<i64>().ok()).or_else(|| integral_i64(n))
424
+ }
425
+
426
+ /// The digits `write` prints for a `Num` with no exact source text: whichever of the two
427
+ /// renderings `write`/`to_id_component` have always used is faithful to `n` itself.
428
+ ///
429
+ /// Shared with `parse_number`, which compares a literal's digits against this same rendering to
430
+ /// decide whether the literal needs to keep its source text at all.
431
+ fn canonical_integer_digits(n: f64) -> String {
432
+ if n.fract() == 0.0 && n.abs() < 1e15 {
433
+ (n as i64).to_string()
434
+ } else {
435
+ n.to_string()
436
+ }
437
+ }
438
+
439
+ #[cfg(test)]
440
+ mod integral_i64_tests {
441
+ use super::*;
442
+
443
+ #[test]
444
+ fn accepts_ordinary_whole_numbers() {
445
+ assert_eq!(integral_i64(0.0), Some(0));
446
+ assert_eq!(integral_i64(42.0), Some(42));
447
+ assert_eq!(integral_i64(-42.0), Some(-42));
448
+ }
449
+
450
+ #[test]
451
+ fn rejects_fractional_numbers() {
452
+ assert_eq!(integral_i64(25.5), None);
453
+ }
454
+
455
+ #[test]
456
+ fn accepts_i64_boundaries() {
457
+ assert_eq!(integral_i64(i64::MIN as f64), Some(i64::MIN));
458
+ // Largest f64 below 2^63; `i64::MAX` itself rounds up to 2^63 in f64.
459
+ let largest_safe = 9223372036854774784.0_f64;
460
+ assert_eq!(integral_i64(largest_safe), Some(largest_safe as i64));
461
+ }
462
+
463
+ #[test]
464
+ fn accepts_i64_saturation_boundary() {
465
+ // `as i64` would saturate on either of these.
466
+ assert_eq!(integral_i64(2f64.powi(63)), None, "2^63 must not saturate to i64::MAX");
467
+ // Near 2^63 a small nudge rounds back to the same float, so use 2^64.
468
+ assert_eq!(integral_i64(-(2f64.powi(64))), None, "well below i64::MIN must not saturate to i64::MIN");
469
+ }
470
+
471
+ #[test]
472
+ fn as_i64_refuses_out_of_range_where_the_old_cast_would_have_saturated() {
473
+ // Pins the regression: a huge number must not saturate to `Some(i64::MAX)`.
474
+ let huge = Json::Num(1e30, None);
475
+ assert_eq!(huge.as_i64(), None);
476
+ }
477
+
478
+ #[test]
479
+ fn to_id_component_reflects_true_magnitude_instead_of_a_clamped_one() {
480
+ let huge = Json::Num(1e30, None);
481
+ let id = huge.to_id_component().expect("numbers are always usable as an id component");
482
+ assert_ne!(id, i64::MAX.to_string(), "must not silently clamp to i64::MAX");
483
+ assert!(id.starts_with('1'), "should reflect the real magnitude, got {id:?}");
484
+ }
485
+
486
+ #[test]
487
+ fn parse_preserves_a_huge_integers_exact_digits_past_f64_precision() {
488
+ // BUG#147: 2^100 (2**100, the fuzzer's own INTEGER_EDGE_CASES bignum), and the
489
+ // signed 31-digit repro from Roster::Roster.AddSeat's routing_key mutation.
490
+ for literal in ["-1267650600228229401496703205376", "1267650600228229401496703205376"] {
491
+ let parsed = Json::parse(literal).expect("a bare JSON integer parses");
492
+ assert_eq!(parsed.to_json_string(), literal, "re-emitted JSON must not lose a single digit");
493
+ assert_eq!(
494
+ parsed.to_id_component().unwrap(),
495
+ literal,
496
+ "an identity component must carry the caller's exact digits, not a rounded f64"
497
+ );
498
+ // Genuinely out of i64's range: still correctly refused, not silently truncated.
499
+ assert_eq!(parsed.as_i64(), None);
500
+ }
501
+ }
502
+
503
+ #[test]
504
+ fn parse_preserves_a_whole_number_between_f64s_exact_range_and_i64_max() {
505
+ // 2^60: fits comfortably in `i64`, but has more digits than `f64` can hold exactly, so
506
+ // the naive `s.parse::<f64>()` path alone already rounds it to the wrong integer.
507
+ let literal = "1152921504606846977"; // 2^60 + 1
508
+ let parsed = Json::parse(literal).expect("a bare JSON integer parses");
509
+ assert_eq!(parsed.as_i64(), Some(1_152_921_504_606_846_977));
510
+ assert_eq!(parsed.to_json_string(), literal);
511
+ assert_eq!(parsed.to_id_component().unwrap(), literal);
512
+ }
513
+
514
+ #[test]
515
+ fn parse_leaves_an_ordinary_integer_undisturbed() {
516
+ // No source text is retained when the f64 path was already exact — same value either way.
517
+ let parsed = Json::parse("42").unwrap();
518
+ assert_eq!(parsed, Json::Num(42.0, None));
519
+ assert_eq!(parsed.to_json_string(), "42");
520
+ }
521
+
522
+ #[test]
523
+ fn a_bignum_and_an_ordinary_number_are_still_comparable_and_never_falsely_equal() {
524
+ let huge = Json::parse("1267650600228229401496703205376").unwrap();
525
+ let other_huge = Json::parse("1267650600228229401496703205377").unwrap();
526
+ assert_ne!(huge, other_huge, "different bignums must not collide via f64 rounding");
527
+ assert_eq!(huge, Json::parse("1267650600228229401496703205376").unwrap());
528
+ assert_ne!(huge, Json::Num(5.0, None));
529
+ }
530
+
531
+ #[test]
532
+ fn to_id_component_refuses_an_empty_string() {
533
+ // Ruby treats a blank identity part as absent; a blank id must not be accepted.
534
+ let blank = Json::Str(String::new());
535
+ assert!(blank.to_id_component().is_err(), "an empty string must not be usable as an identity component");
536
+ }
537
+
538
+ #[test]
539
+ fn to_id_component_still_accepts_a_real_string() {
540
+ let real = Json::Str("acct-1".to_string());
541
+ assert_eq!(real.to_id_component().unwrap(), "acct-1");
542
+ }
543
+
544
+ #[test]
545
+ fn to_id_component_lenient_accepts_an_empty_string() {
546
+ // A blank key is truthy in Ruby and flows on as a non-matching value.
547
+ let blank = Json::Str(String::new());
548
+ assert_eq!(blank.to_id_component_lenient().unwrap(), "", "a present, blank string must be a valid (non-matching) component");
549
+ }
550
+
551
+ #[test]
552
+ fn to_id_component_lenient_still_refuses_a_non_scalar() {
553
+ // Only the blank-string case differs from `to_id_component`.
554
+ let list = Json::Array(vec![]);
555
+ assert!(list.to_id_component_lenient().is_err(), "a non-scalar must still refuse, same as to_id_component");
556
+ }
557
+ }
558
+
559
+ #[cfg(test)]
560
+ mod value_object_shape_tests {
561
+ use super::*;
562
+
563
+ fn refusal_text(json: &Json) -> String {
564
+ match json.expect_value_object_shape("name", "PersonName") {
565
+ Err(Refusal::TypeMismatch(text)) => text,
566
+ other => panic!("expected a TypeMismatch refusal, got {other:?}"),
567
+ }
568
+ }
569
+
570
+ #[test]
571
+ fn an_object_passes_through_untouched() {
572
+ let object = Json::obj(vec![("given", Json::Str("Ada".into()))]);
573
+ assert!(object.expect_value_object_shape("name", "PersonName").is_ok());
574
+ }
575
+
576
+ #[test]
577
+ fn a_scalar_is_refused_in_rubys_wording() {
578
+ assert_eq!(
579
+ refusal_text(&Json::Str("Ada".into())),
580
+ "name is a PersonName — pass its fields as an object, not \"Ada\""
581
+ );
582
+ assert_eq!(refusal_text(&Json::Num(7.0, None)), "name is a PersonName — pass its fields as an object, not 7");
583
+ }
584
+
585
+ #[test]
586
+ fn an_array_is_described_as_compact_json() {
587
+ let list = Json::Array(vec![Json::Str("Ada".into()), Json::Str("Lovelace".into())]);
588
+ assert_eq!(
589
+ refusal_text(&list),
590
+ "name is a PersonName — pass its fields as an object, not [\"Ada\",\"Lovelace\"]"
591
+ );
592
+ }
593
+ }
594
+
595
+ fn write_escaped_string(s: &str, out: &mut String) {
596
+ out.push('"');
597
+ for c in s.chars() {
598
+ match c {
599
+ '"' => out.push_str("\\\""),
600
+ '\\' => out.push_str("\\\\"),
601
+ '\n' => out.push_str("\\n"),
602
+ '\t' => out.push_str("\\t"),
603
+ '\r' => out.push_str("\\r"),
604
+ c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
605
+ c => out.push(c),
606
+ }
607
+ }
608
+ out.push('"');
609
+ }
610
+
611
+ // Recursive descent over chars; the input is a valid `&str`, so no UTF-8 byte handling is needed.
612
+ struct Parser<'a> {
613
+ chars: std::iter::Peekable<std::str::Chars<'a>>,
614
+ }
615
+
616
+ impl<'a> Parser<'a> {
617
+ fn skip_ws(&mut self) {
618
+ while matches!(self.chars.peek(), Some(c) if c.is_whitespace()) {
619
+ self.chars.next();
620
+ }
621
+ }
622
+
623
+ fn expect(&mut self, ch: char) -> Result<(), String> {
624
+ match self.chars.next() {
625
+ Some(c) if c == ch => Ok(()),
626
+ other => Err(format!("expected {ch:?}, got {other:?}")),
627
+ }
628
+ }
629
+
630
+ fn consume_literal(&mut self, lit: &str) -> bool {
631
+ let save = self.chars.clone();
632
+ for expected in lit.chars() {
633
+ if self.chars.next() != Some(expected) {
634
+ self.chars = save;
635
+ return false;
636
+ }
637
+ }
638
+ true
639
+ }
640
+
641
+ fn parse_value(&mut self) -> Result<Json, String> {
642
+ self.skip_ws();
643
+ match self.chars.peek() {
644
+ Some('{') => self.parse_object(),
645
+ Some('[') => self.parse_array(),
646
+ Some('"') => self.parse_string().map(Json::Str),
647
+ Some('t') | Some('f') => self.parse_bool(),
648
+ Some('n') => self.parse_null(),
649
+ Some(c) if *c == '-' || c.is_ascii_digit() => self.parse_number(),
650
+ other => Err(format!("unexpected {other:?} in JSON")),
651
+ }
652
+ }
653
+
654
+ fn parse_object(&mut self) -> Result<Json, String> {
655
+ self.expect('{')?;
656
+ let mut fields = Vec::new();
657
+ self.skip_ws();
658
+ if self.chars.peek() == Some(&'}') {
659
+ self.chars.next();
660
+ return Ok(Json::Object(fields));
661
+ }
662
+ loop {
663
+ self.skip_ws();
664
+ let key = self.parse_string()?;
665
+ self.skip_ws();
666
+ self.expect(':')?;
667
+ let value = self.parse_value()?;
668
+ fields.push((key, value));
669
+ self.skip_ws();
670
+ match self.chars.next() {
671
+ Some(',') => continue,
672
+ Some('}') => break,
673
+ other => return Err(format!("expected ',' or '}}' in object, got {other:?}")),
674
+ }
675
+ }
676
+ Ok(Json::Object(fields))
677
+ }
678
+
679
+ fn parse_array(&mut self) -> Result<Json, String> {
680
+ self.expect('[')?;
681
+ let mut items = Vec::new();
682
+ self.skip_ws();
683
+ if self.chars.peek() == Some(&']') {
684
+ self.chars.next();
685
+ return Ok(Json::Array(items));
686
+ }
687
+ loop {
688
+ items.push(self.parse_value()?);
689
+ self.skip_ws();
690
+ match self.chars.next() {
691
+ Some(',') => continue,
692
+ Some(']') => break,
693
+ other => return Err(format!("expected ',' or ']' in array, got {other:?}")),
694
+ }
695
+ }
696
+ Ok(Json::Array(items))
697
+ }
698
+
699
+ fn parse_string(&mut self) -> Result<String, String> {
700
+ self.expect('"')?;
701
+ let mut out = String::new();
702
+ loop {
703
+ match self.chars.next() {
704
+ Some('"') => break,
705
+ Some('\\') => match self.chars.next() {
706
+ Some('"') => out.push('"'),
707
+ Some('\\') => out.push('\\'),
708
+ Some('/') => out.push('/'),
709
+ Some('n') => out.push('\n'),
710
+ Some('t') => out.push('\t'),
711
+ Some('r') => out.push('\r'),
712
+ Some('b') => out.push('\u{8}'),
713
+ Some('f') => out.push('\u{c}'),
714
+ Some('u') => {
715
+ let hex: String = (0..4).map(|_| self.chars.next().unwrap_or('0')).collect();
716
+ let code = u32::from_str_radix(&hex, 16).map_err(|_| format!("bad \\u escape {hex:?}"))?;
717
+ out.push(char::from_u32(code).unwrap_or('\u{FFFD}'));
718
+ }
719
+ other => return Err(format!("bad escape sequence \\{other:?}")),
720
+ },
721
+ Some(c) => out.push(c),
722
+ None => return Err("unterminated string".to_string()),
723
+ }
724
+ }
725
+ Ok(out)
726
+ }
727
+
728
+ fn parse_bool(&mut self) -> Result<Json, String> {
729
+ if self.consume_literal("true") {
730
+ Ok(Json::Bool(true))
731
+ } else if self.consume_literal("false") {
732
+ Ok(Json::Bool(false))
733
+ } else {
734
+ Err("expected true/false".to_string())
735
+ }
736
+ }
737
+
738
+ fn parse_null(&mut self) -> Result<Json, String> {
739
+ if self.consume_literal("null") {
740
+ Ok(Json::Null)
741
+ } else {
742
+ Err("expected null".to_string())
743
+ }
744
+ }
745
+
746
+ fn parse_number(&mut self) -> Result<Json, String> {
747
+ let mut s = String::new();
748
+ if self.chars.peek() == Some(&'-') {
749
+ s.push(self.chars.next().unwrap());
750
+ }
751
+ while matches!(self.chars.peek(), Some(c) if c.is_ascii_digit()) {
752
+ s.push(self.chars.next().unwrap());
753
+ }
754
+ if self.chars.peek() == Some(&'.') {
755
+ s.push(self.chars.next().unwrap());
756
+ while matches!(self.chars.peek(), Some(c) if c.is_ascii_digit()) {
757
+ s.push(self.chars.next().unwrap());
758
+ }
759
+ }
760
+ if matches!(self.chars.peek(), Some('e') | Some('E')) {
761
+ s.push(self.chars.next().unwrap());
762
+ if matches!(self.chars.peek(), Some('+') | Some('-')) {
763
+ s.push(self.chars.next().unwrap());
764
+ }
765
+ while matches!(self.chars.peek(), Some(c) if c.is_ascii_digit()) {
766
+ s.push(self.chars.next().unwrap());
767
+ }
768
+ }
769
+ let value: f64 = s.parse().map_err(|e| format!("bad number {s:?}: {e}"))?;
770
+ // A plain integer literal keeps its exact source digits (BUG#147) whenever `write`'s own
771
+ // rendering of the parsed `f64` would not reproduce them — i.e. whenever the round trip
772
+ // through `f64` has already lost precision, whether or not the value still fits `i64`.
773
+ // A literal with a `.`/exponent is a genuine `Float`, and Ruby's `Float` is the same
774
+ // `f64` Rust's is, so there is nothing to preserve there.
775
+ let is_plain_integer = !s.contains('.') && !s.contains(['e', 'E']);
776
+ let raw = if is_plain_integer && canonical_integer_digits(value) != s { Some(s) } else { None };
777
+ Ok(Json::Num(value, raw))
778
+ }
779
+ }