hecks 3.14.0 → 5.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 (427) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/exe/hecks +1 -1
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +9 -16
  12. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  13. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  14. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  15. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  16. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  17. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  18. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  19. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  20. data/lib/hecks/bluebook/translation.rb +18 -10
  21. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  22. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  23. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  24. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  25. data/lib/hecks/bluebook.rb +2 -0
  26. data/lib/hecks/chapters.rb +3 -3
  27. data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
  28. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  29. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  30. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  31. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  32. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  33. data/lib/hecks/composer/adapters/jev.rb +66 -0
  34. data/lib/hecks/composer/agent.rb +113 -0
  35. data/lib/hecks/composer/assistant.rb +110 -0
  36. data/lib/hecks/composer/behavior_run.rb +53 -0
  37. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  38. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  39. data/lib/hecks/composer/board.rb +65 -0
  40. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  41. data/lib/hecks/composer/board_edit/source.rb +84 -0
  42. data/lib/hecks/composer/board_edit.rb +144 -0
  43. data/lib/hecks/composer/check.rb +89 -0
  44. data/lib/hecks/composer/domains.rb +81 -0
  45. data/lib/hecks/composer/jobs.rb +134 -0
  46. data/lib/hecks/composer/layout.rb +37 -0
  47. data/lib/hecks/composer/live.rb +150 -0
  48. data/lib/hecks/composer/outline.rb +48 -0
  49. data/lib/hecks/composer/page.html +989 -0
  50. data/lib/hecks/composer/panels.rb +142 -0
  51. data/lib/hecks/composer/parse.rb +83 -0
  52. data/lib/hecks/composer/plain.rb +28 -0
  53. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  54. data/lib/hecks/composer/session.rb +141 -0
  55. data/lib/hecks/composer/toolbox.rb +166 -0
  56. data/lib/hecks/composer/web.rb +156 -0
  57. data/lib/hecks/composer/workbench.rb +51 -0
  58. data/lib/hecks/composer/workspace.rb +81 -0
  59. data/lib/hecks/composer.rb +17 -0
  60. data/lib/hecks/corpus/adapters.rb +22 -0
  61. data/lib/hecks/corpus/rust.rb +1 -1
  62. data/lib/hecks/corpus/worlds.rb +19 -0
  63. data/lib/hecks/corpus.rb +5 -0
  64. data/lib/hecks/forms/app/commands.rb +2 -3
  65. data/lib/hecks/forms/app/queries.rb +2 -3
  66. data/lib/hecks/forms/app/records.rb +17 -5
  67. data/lib/hecks/forms/app/responses.rb +3 -1
  68. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  69. data/lib/hecks/forms/field_controls.rb +2 -2
  70. data/lib/hecks/forms/field_renderer.rb +1 -1
  71. data/lib/hecks/forms/field_shape.rb +36 -17
  72. data/lib/hecks/forms/index_renderer.rb +2 -1
  73. data/lib/hecks/forms/page.rb +1 -1
  74. data/lib/hecks/forms/params.rb +0 -13
  75. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  76. data/lib/hecks/forms/record_renderer.rb +11 -7
  77. data/lib/hecks/forms/rule_words.rb +100 -0
  78. data/lib/hecks/forms/value_object_shape.rb +2 -1
  79. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  80. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  81. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  82. data/lib/hecks/gate/stages.yml +25 -0
  83. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  84. data/lib/hecks/grammar/evolve.rb +0 -3
  85. data/lib/hecks/grammar/translation.bluebook +1 -1
  86. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  87. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  88. data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
  89. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  90. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  91. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  92. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  93. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  94. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  95. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  96. data/lib/hecks/hecks/codebase.behaviors +6 -0
  97. data/lib/hecks/hecks/codebase.bluebook +21 -0
  98. data/lib/hecks/hecks/composer.behaviors +68 -0
  99. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  100. data/lib/hecks/hecks/custodian.behaviors +0 -6
  101. data/lib/hecks/hecks/custodian.bluebook +3 -80
  102. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  103. data/lib/hecks/hecks/hecks.world +1 -1
  104. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  105. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  106. data/lib/hecks/kernel_capabilities.rb +1 -1
  107. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  108. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  109. data/lib/hecks/ports/access_control.rb +2 -2
  110. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  111. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  112. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  113. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  114. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  118. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  119. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  120. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  121. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  122. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  123. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  124. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  125. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  126. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  127. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  128. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  129. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  130. data/lib/hecks/ports.rb +0 -1
  131. data/lib/hecks/projection_files.rb +3 -1
  132. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  133. data/lib/hecks/projections/glossary/entries.rb +10 -4
  134. data/lib/hecks/projections/glossary/groups.rb +42 -0
  135. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  136. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  137. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  138. data/lib/hecks/projections/glossary/html.rb +1 -2
  139. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  140. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  141. data/lib/hecks/projections/glossary/page.css +143 -153
  142. data/lib/hecks/projections/glossary/page.js +63 -3
  143. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  144. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  145. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  146. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  147. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  148. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  149. data/lib/hecks/projections/glossary.rb +2 -1
  150. data/lib/hecks/projections/plain_english.rb +97 -0
  151. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  152. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  153. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  154. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  155. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  156. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
  157. data/lib/hecks/projections/statements/actions.rb +99 -0
  158. data/lib/hecks/projections/statements/relationships.rb +86 -0
  159. data/lib/hecks/projections/statements.rb +15 -24
  160. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  161. data/lib/hecks/projector/exporter/translations.rb +18 -35
  162. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  163. data/lib/hecks/projector/exporter.rb +5 -1
  164. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  165. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  166. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  167. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  168. data/lib/hecks/runtime/outbox.rb +2 -9
  169. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  170. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  171. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  172. data/lib/hecks/runtime/value.rb +10 -21
  173. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  174. data/lib/hecks/rust_build/wasm.rb +0 -12
  175. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  176. data/lib/hecks/rust_format.rb +77 -0
  177. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  178. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  179. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  180. data/lib/hecks/tools/translation_audit.rb +8 -10
  181. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  182. data/lib/hecks/version.rb +1 -1
  183. data/lib/hecks/vocabulary.rb +2 -1
  184. data/lib/hecks.rb +3 -0
  185. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
  186. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
  187. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
  188. data/rust/Cargo.toml +40 -0
  189. data/rust/build/Cargo.toml +36 -0
  190. data/rust/build/clippy.toml +4 -0
  191. data/rust/build/src/adapters_pass.rs +196 -0
  192. data/rust/build/src/binding_pass.rs +54 -13
  193. data/rust/build/src/build_artifact.rs +23 -5
  194. data/rust/build/src/cargo_sync.rs +69 -26
  195. data/rust/build/src/lineage_pass.rs +101 -87
  196. data/rust/build/src/main.rs +11 -2
  197. data/rust/build/src/pipeline.rs +80 -19
  198. data/rust/build/src/reserved_names.rs +6 -50
  199. data/rust/build/src/resolve.rs +82 -35
  200. data/rust/build/src/rustfmt.rs +100 -0
  201. data/rust/build/src/seams_pass.rs +85 -16
  202. data/rust/build/src/subprocess.rs +30 -6
  203. data/rust/build/src/tmp.rs +5 -2
  204. data/rust/build/src/translation_pass.rs +72 -22
  205. data/rust/build/src/world_pass.rs +109 -0
  206. data/rust/clippy.toml +4 -0
  207. data/rust/codegen/Cargo.toml +36 -0
  208. data/rust/codegen/clippy.toml +4 -0
  209. data/rust/codegen/src/asks.rs +40 -14
  210. data/rust/codegen/src/attr.rs +2 -2
  211. data/rust/codegen/src/bridging.rs +676 -117
  212. data/rust/codegen/src/commands.rs +979 -373
  213. data/rust/codegen/src/constraints.rs +88 -25
  214. data/rust/codegen/src/dependency_planning.rs +128 -34
  215. data/rust/codegen/src/domain_generator.rs +306 -170
  216. data/rust/codegen/src/exemplar.rs +87 -32
  217. data/rust/codegen/src/expr_emitter.rs +54 -6
  218. data/rust/codegen/src/fielded.rs +193 -41
  219. data/rust/codegen/src/hecks_naming.rs +124 -16
  220. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  221. data/rust/codegen/src/json.rs +80 -44
  222. data/rust/codegen/src/json_codec.rs +585 -149
  223. data/rust/codegen/src/literal.rs +104 -138
  224. data/rust/codegen/src/main.rs +151 -52
  225. data/rust/codegen/src/manifest.rs +76 -14
  226. data/rust/codegen/src/mutations.rs +1212 -239
  227. data/rust/codegen/src/naming.rs +111 -60
  228. data/rust/codegen/src/optional_pass.rs +90 -28
  229. data/rust/codegen/src/ports.rs +5 -4
  230. data/rust/codegen/src/prelude.rs +139 -49
  231. data/rust/codegen/src/presenters.rs +335 -0
  232. data/rust/codegen/src/queries.rs +601 -226
  233. data/rust/codegen/src/reactions.rs +268 -74
  234. data/rust/codegen/src/read_models.rs +574 -229
  235. data/rust/codegen/src/reference_specs.rs +16 -3
  236. data/rust/codegen/src/registry.rs +128 -79
  237. data/rust/codegen/src/reserved_names.rs +6 -50
  238. data/rust/codegen/src/shared.rs +71 -22
  239. data/rust/codegen/src/sidecars.rs +15 -5
  240. data/rust/codegen/src/skip_reason.rs +8 -2
  241. data/rust/codegen/src/types.rs +306 -108
  242. data/rust/host/Cargo.lock +1 -0
  243. data/rust/host/Cargo.toml +40 -4
  244. data/rust/host/HECKS_RELEASE +1 -1
  245. data/rust/host/clippy.toml +4 -0
  246. data/rust/host/src/api.rs +698 -218
  247. data/rust/host/src/auth.rs +491 -214
  248. data/rust/host/src/b64.rs +91 -0
  249. data/rust/host/src/bin/expr_harness.rs +6 -2
  250. data/rust/host/src/bin/lineage_harness.rs +47 -11
  251. data/rust/host/src/bin/mint_harness.rs +70 -37
  252. data/rust/host/src/bin/validate_harness.rs +16 -8
  253. data/rust/host/src/boot.rs +144 -68
  254. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  255. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  256. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  257. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  258. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  259. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  260. data/rust/host/src/checkout.rs +78 -32
  261. data/rust/host/src/clock.rs +8 -0
  262. data/rust/host/src/commerce_ir.rs +333 -95
  263. data/rust/host/src/crypto.rs +33 -0
  264. data/rust/host/src/dispatch.rs +1037 -331
  265. data/rust/host/src/expr_json.rs +1021 -273
  266. data/rust/host/src/extension.rs +24 -4
  267. data/rust/host/src/field_hints.rs +11 -7
  268. data/rust/host/src/fuzz_support.rs +85 -13
  269. data/rust/host/src/host.rs +100 -0
  270. data/rust/host/src/http.rs +22 -0
  271. data/rust/host/src/ir.rs +326 -16
  272. data/rust/host/src/journal.rs +277 -176
  273. data/rust/host/src/lambda_client.rs +219 -53
  274. data/rust/host/src/lib.rs +16 -4
  275. data/rust/host/src/log.rs +62 -11
  276. data/rust/host/src/mint.rs +1153 -326
  277. data/rust/host/src/needs.rs +185 -44
  278. data/rust/host/src/payments/keystore.rs +197 -45
  279. data/rust/host/src/payments/roles.rs +39 -7
  280. data/rust/host/src/payments/tests.rs +1427 -282
  281. data/rust/host/src/payments.rs +391 -112
  282. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  283. data/rust/host/src/platform_signin/config.rs +11 -11
  284. data/rust/host/src/platform_signin/cookie.rs +2 -2
  285. data/rust/host/src/platform_signin/extension.rs +2 -1
  286. data/rust/host/src/platform_signin/handler.rs +9 -8
  287. data/rust/host/src/platform_signin/jti.rs +4 -2
  288. data/rust/host/src/platform_signin/jwks.rs +2 -1
  289. data/rust/host/src/platform_signin/jws.rs +2 -37
  290. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  291. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  292. data/rust/host/src/platform_signin/pages.rs +20 -20
  293. data/rust/host/src/platform_signin/session.rs +8 -1
  294. data/rust/host/src/platform_signin/settings.rs +6 -6
  295. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  296. data/rust/host/src/platform_signin/verify.rs +41 -41
  297. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  298. data/rust/host/src/presentation.rs +207 -63
  299. data/rust/host/src/presentation_write.rs +502 -187
  300. data/rust/host/src/query_step.rs +69 -18
  301. data/rust/host/src/rate_limit.rs +541 -142
  302. data/rust/host/src/reference_transform.rs +77 -25
  303. data/rust/host/src/reference_validate.rs +443 -104
  304. data/rust/host/src/resend.rs +190 -55
  305. data/rust/host/src/secrets.rs +116 -21
  306. data/rust/host/src/server.rs +555 -104
  307. data/rust/host/src/storage_shape.rs +88 -21
  308. data/rust/host/src/test_pg.rs +17 -7
  309. data/rust/host/src/ui_schema.rs +495 -207
  310. data/rust/host/src/wasm_runner.rs +21 -6
  311. data/rust/host/src/web/commerce.rs +101 -23
  312. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  313. data/rust/host/src/web/newsletter/template.rs +26 -7
  314. data/rust/host/src/web/newsletter/tests.rs +112 -23
  315. data/rust/host/src/web/newsletter.rs +209 -59
  316. data/rust/host/src/web/newsletter_send.rs +251 -79
  317. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  318. data/rust/host/src/web/registration_receipt.rs +100 -24
  319. data/rust/host/src/web/registrations.rs +1384 -332
  320. data/rust/host/src/web/resend_webhook.rs +138 -80
  321. data/rust/host/src/web.rs +2416 -508
  322. data/rust/lsp/Cargo.toml +36 -0
  323. data/rust/lsp/clippy.toml +4 -0
  324. data/rust/lsp/src/json.rs +29 -22
  325. data/rust/lsp/src/main.rs +81 -31
  326. data/rust/lsp/src/outline.rs +23 -7
  327. data/rust/lsp/src/rpc.rs +17 -4
  328. data/rust/parser/Cargo.toml +36 -0
  329. data/rust/parser/clippy.toml +4 -0
  330. data/rust/parser/src/build/closed_sets.rs +6 -1
  331. data/rust/parser/src/build/identity.rs +4 -3
  332. data/rust/parser/src/build/mod.rs +1 -1
  333. data/rust/parser/src/build/named_pattern.rs +4 -1
  334. data/rust/parser/src/build/naming.rs +2 -80
  335. data/rust/parser/src/build/pattern_subset.rs +6 -8
  336. data/rust/parser/src/build/query_derive.rs +3 -1
  337. data/rust/parser/src/build/query_inference.rs +14 -5
  338. data/rust/parser/src/build/read_model.rs +4 -1
  339. data/rust/parser/src/build/references.rs +4 -3
  340. data/rust/parser/src/canonical.rs +34 -15
  341. data/rust/parser/src/emit.rs +187 -105
  342. data/rust/parser/src/expr/ast_json.rs +134 -42
  343. data/rust/parser/src/expr/evaluator.rs +27 -9
  344. data/rust/parser/src/expr/mod.rs +54 -13
  345. data/rust/parser/src/expr/resolver.rs +299 -92
  346. data/rust/parser/src/ir.rs +35 -3
  347. data/rust/parser/src/keywords.rs +7168 -516
  348. data/rust/parser/src/lex.rs +11 -25
  349. data/rust/parser/src/lib.rs +1 -0
  350. data/rust/parser/src/main.rs +65 -6
  351. data/rust/parser/src/parse/adapter.rs +157 -0
  352. data/rust/parser/src/parse/aggregate.rs +19 -10
  353. data/rust/parser/src/parse/chapter.rs +16 -17
  354. data/rust/parser/src/parse/command.rs +35 -16
  355. data/rust/parser/src/parse/domain_port.rs +4 -7
  356. data/rust/parser/src/parse/entity.rs +21 -11
  357. data/rust/parser/src/parse/hecksagon.rs +10 -4
  358. data/rust/parser/src/parse/lifecycle.rs +9 -5
  359. data/rust/parser/src/parse/mod.rs +22 -91
  360. data/rust/parser/src/parse/needs.rs +21 -12
  361. data/rust/parser/src/parse/policy.rs +2 -2
  362. data/rust/parser/src/parse/process_manager.rs +9 -11
  363. data/rust/parser/src/parse/query.rs +8 -4
  364. data/rust/parser/src/parse/read_model.rs +57 -16
  365. data/rust/parser/src/parse/value_object.rs +7 -2
  366. data/rust/parser/src/parse/world.rs +322 -0
  367. data/rust/parser/src/ruby_value.rs +7 -6
  368. data/rust/src/exemplar/json.rs +4 -4
  369. data/rust/src/exemplar/mutations.rs +18 -0
  370. data/rust/src/exemplar/read_models.rs +1 -10
  371. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  372. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  373. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  374. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  375. data/rust/src/kernel/cli.rs +244 -114
  376. data/rust/src/kernel/dispatch.rs +323 -115
  377. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  378. data/rust/src/kernel/expr.rs +203 -86
  379. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  380. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  381. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  382. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  383. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  384. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  385. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  386. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  387. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  388. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  389. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  390. data/rust/src/kernel/expression_operators/text.rs +120 -27
  391. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  392. data/rust/src/kernel/json.rs +244 -148
  393. data/rust/src/kernel/mod.rs +47 -33
  394. data/rust/src/kernel/named_query.rs +63 -15
  395. data/rust/src/kernel/needs.rs +112 -25
  396. data/rust/src/kernel/orchestrate.rs +980 -581
  397. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  398. data/rust/src/kernel/pattern.rs +107 -34
  399. data/rust/src/kernel/present.rs +261 -0
  400. data/rust/src/kernel/query_comparators.rs +118 -41
  401. data/rust/src/kernel/query_ordering.rs +13 -5
  402. data/rust/src/kernel/read_model.rs +479 -181
  403. data/rust/src/kernel/reference_lookup.rs +122 -26
  404. data/rust/src/kernel/repository.rs +293 -66
  405. data/rust/src/kernel/routing.rs +58 -42
  406. data/rust/src/kernel/ruby_float.rs +139 -0
  407. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  408. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  409. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  410. data/rust/src/kernel/vocab/mod.rs +6 -6
  411. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  412. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  413. data/rust/src/lib.rs +50 -0
  414. data/rust/src/main.rs +4 -1
  415. data/rust/web/Cargo.toml +36 -0
  416. data/rust/web/clippy.toml +4 -0
  417. metadata +85 -15
  418. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  419. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  420. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  421. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  422. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  423. data/lib/hecks/ports/authentication.port +0 -6
  424. data/lib/hecks/ports/authentication.rb +0 -67
  425. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  426. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  427. data/rust/host/src/approval.rs +0 -652
@@ -2,11 +2,12 @@
2
2
  //! Kept separate from `expr::Value`; zero Cargo dependencies (ADR 0012).
3
3
 
4
4
  use super::Refusal;
5
+ use std::fmt::Write as _;
5
6
 
6
7
  #[derive(Debug, Clone)]
7
8
  pub enum Json {
8
- Object(Vec<(String, Json)>),
9
- Array(Vec<Json>),
9
+ Object(Vec<(String, Self)>),
10
+ Array(Vec<Self>),
10
11
  Str(String),
11
12
  /// The second field is the exact source text (BUG#147) — set only when the literal's
12
13
  /// magnitude means the `f64` approximation would not reproduce it, e.g. an integer past
@@ -28,14 +29,14 @@ impl PartialEq for Json {
28
29
  /// in the first place; otherwise the numeric comparison is unchanged.
29
30
  fn eq(&self, other: &Self) -> bool {
30
31
  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,
32
+ (Self::Object(a), Self::Object(b)) => a == b,
33
+ (Self::Array(a), Self::Array(b)) => a == b,
34
+ (Self::Str(a), Self::Str(b)) => a == b,
35
+ (Self::Num(_, Some(araw)), Self::Num(_, Some(braw))) => araw == braw,
36
+ (Self::Num(a, _), Self::Num(b, _)) => a == b,
37
+ (Self::Float(a), Self::Float(b)) => a == b,
38
+ (Self::Bool(a), Self::Bool(b)) => a == b,
39
+ (Self::Null, Self::Null) => true,
39
40
  _ => false,
40
41
  }
41
42
  }
@@ -45,42 +46,46 @@ impl PartialEq for Json {
45
46
  impl super::Fielded for Json {
46
47
  fn field(&self, name: &str) -> Option<super::Field<'_>> {
47
48
  use super::{Field, Value};
48
- let Json::Object(pairs) = self else { return None };
49
+ let Self::Object(pairs) = self else {
50
+ return None;
51
+ };
49
52
  let (_, found) = pairs.iter().find(|(k, _)| k == name)?;
50
53
  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
- },
54
+ Self::Object(_) => Field::Nested(found),
55
+ Self::Array(items) => Field::Value(Value::List(items.len())),
56
+ Self::Str(s) => Field::Value(Value::Str(s.clone())),
57
+ Self::Num(n, raw) => {
58
+ Field::Value(exact_i64(*n, raw.as_deref()).map_or(Value::Float(*n), Value::Int))
59
+ }
58
60
  // 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),
61
+ Self::Float(n) => Field::Value(Value::Float(*n)),
62
+ Self::Bool(b) => Field::Value(Value::Bool(*b)),
63
+ Self::Null => Field::Value(Value::Nil),
62
64
  })
63
65
  }
64
66
 
65
67
  fn items(&self, name: &str) -> Option<Vec<super::Field<'_>>> {
66
68
  use super::{Field, Value};
67
- let Json::Object(pairs) = self else { return None };
69
+ let Self::Object(pairs) = self else {
70
+ return None;
71
+ };
68
72
  let (_, found) = pairs.iter().find(|(k, _)| k == name)?;
69
- let Json::Array(items) = found else { return None };
73
+ let Self::Array(items) = found else {
74
+ return None;
75
+ };
70
76
  Some(
71
77
  items
72
78
  .iter()
73
79
  .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),
80
+ Self::Object(_) => Field::Nested(item),
81
+ Self::Array(inner) => Field::Value(Value::List(inner.len())),
82
+ Self::Str(s) => Field::Value(Value::Str(s.clone())),
83
+ Self::Num(n, raw) => Field::Value(
84
+ exact_i64(*n, raw.as_deref()).map_or(Value::Float(*n), Value::Int),
85
+ ),
86
+ Self::Float(n) => Field::Value(Value::Float(*n)),
87
+ Self::Bool(b) => Field::Value(Value::Bool(*b)),
88
+ Self::Null => Field::Value(Value::Nil),
84
89
  })
85
90
  .collect(),
86
91
  )
@@ -88,7 +93,9 @@ impl super::Fielded for Json {
88
93
 
89
94
  // A `Json` has no declaration to consult, so one key of any name is the whole shape test.
90
95
  fn as_scalar(&self) -> Option<super::Value> {
91
- let Json::Object(pairs) = self else { return None };
96
+ let Self::Object(pairs) = self else {
97
+ return None;
98
+ };
92
99
  if pairs.len() != 1 {
93
100
  return None;
94
101
  }
@@ -100,28 +107,33 @@ impl super::Fielded for Json {
100
107
  }
101
108
 
102
109
  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())
110
+ pub fn obj(fields: Vec<(&str, Self)>) -> Self {
111
+ Self::Object(
112
+ fields
113
+ .into_iter()
114
+ .map(|(k, v)| (k.to_string(), v))
115
+ .collect(),
116
+ )
105
117
  }
106
118
 
107
- pub fn str(s: impl Into<String>) -> Json {
108
- Json::Str(s.into())
119
+ pub fn str(s: impl Into<String>) -> Self {
120
+ Self::Str(s.into())
109
121
  }
110
122
 
111
123
  /// The raw text is always the exact `i64`, not derived from the `f64` cast: `i as f64`
112
124
  /// itself rounds near the `i64` boundaries, which would otherwise reintroduce BUG#147's
113
125
  /// 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()))
126
+ pub fn int(i: i64) -> Self {
127
+ Self::Num(i as f64, Some(i.to_string()))
116
128
  }
117
129
 
118
- pub fn float(f: f64) -> Json {
119
- Json::Float(f)
130
+ pub const fn float(f: f64) -> Self {
131
+ Self::Float(f)
120
132
  }
121
133
 
122
- pub fn get(&self, key: &str) -> Option<&Json> {
134
+ pub fn get(&self, key: &str) -> Option<&Self> {
123
135
  match self {
124
- Json::Object(fields) => fields.iter().find(|(k, _)| k == key).map(|(_, v)| v),
136
+ Self::Object(fields) => fields.iter().find(|(k, _)| k == key).map(|(_, v)| v),
125
137
  _ => None,
126
138
  }
127
139
  }
@@ -129,11 +141,11 @@ impl Json {
129
141
  /// A tolerant dotted-path walk: a non-object reached mid-path is the answer.
130
142
  ///
131
143
  /// A `Reference<X>` argument arrives as a bare string where a wrapped value is expected.
132
- pub fn dig(&self, path: &str) -> Option<&Json> {
144
+ pub fn dig(&self, path: &str) -> Option<&Self> {
133
145
  let mut current = self;
134
146
  for segment in path.split('.') {
135
147
  match current {
136
- Json::Object(_) => current = current.get(segment)?,
148
+ Self::Object(_) => current = current.get(segment)?,
137
149
  _ => return Some(current),
138
150
  }
139
151
  }
@@ -146,13 +158,13 @@ impl Json {
146
158
  /// an Object stays compact JSON.
147
159
  pub fn inspect(&self) -> String {
148
160
  match self {
149
- Json::Str(s) => {
161
+ Self::Str(s) => {
150
162
  let mut out = String::new();
151
163
  write_escaped_string(s, &mut out);
152
164
  out
153
165
  }
154
- Json::Null => "nil".to_string(),
155
- Json::Array(items) => {
166
+ Self::Null => "nil".to_string(),
167
+ Self::Array(items) => {
156
168
  let mut out = String::from("[");
157
169
  for (i, item) in items.iter().enumerate() {
158
170
  if i > 0 {
@@ -173,17 +185,17 @@ impl Json {
173
185
  /// Arrays, Objects and fractions fall back to compact JSON; they never match a member.
174
186
  pub fn ruby_to_s(&self) -> String {
175
187
  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),
188
+ Self::Str(s) => s.clone(),
189
+ Self::Null => String::new(),
190
+ Self::Bool(b) => b.to_string(),
191
+ Self::Num(n, _) if n.fract() == 0.0 && n.abs() < 1e15 => format!("{}", *n as i64),
180
192
  _ => self.to_json_string(),
181
193
  }
182
194
  }
183
195
 
184
196
  pub fn as_str(&self) -> Option<&str> {
185
197
  match self {
186
- Json::Str(s) => Some(s),
198
+ Self::Str(s) => Some(s),
187
199
  _ => None,
188
200
  }
189
201
  }
@@ -194,28 +206,28 @@ impl Json {
194
206
  /// range and `i64::MAX` round-trips through `f64` inexactly even though it fits in `i64`.
195
207
  pub fn as_i64(&self) -> Option<i64> {
196
208
  match self {
197
- Json::Num(n, raw) => exact_i64(*n, raw),
209
+ Self::Num(n, raw) => exact_i64(*n, raw.as_deref()),
198
210
  _ => None,
199
211
  }
200
212
  }
201
213
 
202
- pub fn as_f64(&self) -> Option<f64> {
214
+ pub const fn as_f64(&self) -> Option<f64> {
203
215
  match self {
204
- Json::Num(n, _) | Json::Float(n) => Some(*n),
216
+ Self::Num(n, _) | Self::Float(n) => Some(*n),
205
217
  _ => None,
206
218
  }
207
219
  }
208
220
 
209
- pub fn as_bool(&self) -> Option<bool> {
221
+ pub const fn as_bool(&self) -> Option<bool> {
210
222
  match self {
211
- Json::Bool(b) => Some(*b),
223
+ Self::Bool(b) => Some(*b),
212
224
  _ => None,
213
225
  }
214
226
  }
215
227
 
216
- pub fn as_array(&self) -> Option<&[Json]> {
228
+ pub fn as_array(&self) -> Option<&[Self]> {
217
229
  match self {
218
- Json::Array(items) => Some(items),
230
+ Self::Array(items) => Some(items),
219
231
  _ => None,
220
232
  }
221
233
  }
@@ -223,17 +235,17 @@ impl Json {
223
235
  /// Wraps a bare scalar as a one-field object under `field_name`; an object passes through.
224
236
  ///
225
237
  /// 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 {
238
+ pub fn coerce_single_field(&self, field_name: &str) -> Self {
227
239
  match self {
228
- Json::Object(_) => self.clone(),
229
- other => Json::obj(vec![(field_name, other.clone())]),
240
+ Self::Object(_) => self.clone(),
241
+ other => Self::obj(vec![(field_name, other.clone())]),
230
242
  }
231
243
  }
232
244
 
233
245
  /// How a refusal quotes an offered value: compact JSON for an Array or Object, else `inspect`.
234
246
  pub fn describe(&self) -> String {
235
247
  match self {
236
- Json::Array(_) | Json::Object(_) => self.to_json_string(),
248
+ Self::Array(_) | Self::Object(_) => self.to_json_string(),
237
249
  other => other.inspect(),
238
250
  }
239
251
  }
@@ -241,9 +253,9 @@ impl Json {
241
253
  /// Refuses `TypeMismatch` when a multi-field value object is offered as a non-object.
242
254
  ///
243
255
  /// 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> {
256
+ pub fn expect_value_object_shape(&self, name: &str, type_name: &str) -> Result<&Self, Refusal> {
245
257
  match self {
246
- Json::Object(_) => Ok(self),
258
+ Self::Object(_) => Ok(self),
247
259
  other => Err(Refusal::TypeMismatch(
248
260
  super::refusal_wording::TypeMismatchValueObjectShapeArgs {
249
261
  name,
@@ -256,9 +268,10 @@ impl Json {
256
268
  }
257
269
 
258
270
  /// 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")))
271
+ pub fn require(&self, key: &str, struct_name: &str) -> Result<&Self, Refusal> {
272
+ self.get(key).ok_or_else(|| {
273
+ Refusal::TypeMismatch(format!("{struct_name}.{key}: missing from JSON args"))
274
+ })
262
275
  }
263
276
 
264
277
  /// The keys of a command's args that are not in `known`, sorted.
@@ -266,8 +279,12 @@ impl Json {
266
279
  /// Sorted because refusal wording is pinned by corpus fixtures and cannot depend on key order.
267
280
  pub fn unknown_keys(&self, known: &[&str]) -> Vec<String> {
268
281
  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();
282
+ Self::Object(fields) => {
283
+ let mut unknown: Vec<String> = fields
284
+ .iter()
285
+ .filter(|(k, _)| !known.contains(&k.as_str()))
286
+ .map(|(k, _)| k.clone())
287
+ .collect();
271
288
  unknown.sort();
272
289
  unknown
273
290
  }
@@ -279,8 +296,8 @@ impl Json {
279
296
  ///
280
297
  /// The typed args carry declared defaults that the raw `args_json` lacks, while the raw
281
298
  /// 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 {
299
+ pub fn overlay(base: &Self, patch: &Self) -> Self {
300
+ let (Self::Object(base_fields), Self::Object(patch_fields)) = (base, patch) else {
284
301
  return base.clone();
285
302
  };
286
303
  let mut merged = base_fields.clone();
@@ -290,25 +307,31 @@ impl Json {
290
307
  None => merged.push((key.clone(), value.clone())),
291
308
  }
292
309
  }
293
- Json::Object(merged)
310
+ Self::Object(merged)
294
311
  }
295
312
 
296
313
  /// Adds each target key of `pairs` holding its source key's value, keeping existing args.
297
314
  ///
298
315
  /// 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 {
316
+ pub fn with_aliases(&self, pairs: &[(&str, &str)]) -> Self {
317
+ let Self::Object(fields) = self else {
301
318
  return self.clone();
302
319
  };
303
320
  let mut merged = fields.clone();
304
321
  for (target_key, source_key) in pairs {
305
- let Some(value) = fields.iter().find(|(k, _)| k == source_key).map(|(_, v)| v.clone()) else { continue };
322
+ let Some(value) = fields
323
+ .iter()
324
+ .find(|(k, _)| k == source_key)
325
+ .map(|(_, v)| v.clone())
326
+ else {
327
+ continue;
328
+ };
306
329
  match merged.iter_mut().find(|(k, _)| k == target_key) {
307
330
  Some(entry) => entry.1 = value,
308
331
  None => merged.push((target_key.to_string(), value)),
309
332
  }
310
333
  }
311
- Json::Object(merged)
334
+ Self::Object(merged)
312
335
  }
313
336
 
314
337
  /// Stringifies a scalar leaf as an identity component.
@@ -319,19 +342,21 @@ impl Json {
319
342
  /// carries it at its full, arbitrary-precision magnitude.
320
343
  pub fn to_id_component(&self) -> Result<String, Refusal> {
321
344
  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"))),
345
+ Self::Str(s) if s.is_empty() => Err(Refusal::TypeMismatch(
346
+ "identity component must not be empty".to_string(),
347
+ )),
348
+ Self::Str(s) => Ok(s.clone()),
349
+ // In range: a plain integer, as Ruby prints it. Out of range or fractional: the
350
+ // exact source text when there is one (a Bignum past i64), else the float's own
351
+ // digits; `as i64` would saturate.
352
+ Self::Num(n, raw) => Ok(exact_i64(*n, raw.as_deref()).map_or_else(
353
+ || raw.clone().unwrap_or_else(|| n.to_string()),
354
+ |i| i.to_string(),
355
+ )),
356
+ Self::Bool(b) => Ok(b.to_string()),
357
+ other => Err(Refusal::TypeMismatch(format!(
358
+ "cannot use {other:?} as an identity component"
359
+ ))),
335
360
  }
336
361
  }
337
362
 
@@ -340,13 +365,28 @@ impl Json {
340
365
  /// A present-but-blank key is an ordinary non-matching value there; non-scalars still refuse.
341
366
  pub fn to_id_component_lenient(&self) -> Result<String, Refusal> {
342
367
  match self {
343
- Json::Str(s) => Ok(s.clone()),
368
+ Self::Str(s) => Ok(s.clone()),
344
369
  other => other.to_id_component(),
345
370
  }
346
371
  }
347
372
 
348
- pub fn parse(input: &str) -> Result<Json, String> {
349
- let mut parser = Parser { chars: input.chars().peekable(), depth: 0 };
373
+ pub fn parse(input: &str) -> Result<Self, String> {
374
+ Self::parse_with(input, false)
375
+ }
376
+
377
+ /// Parses `input` keeping every number literal's source text, so writing the value back
378
+ /// reproduces each number exactly as given (`0.0` stays `0.0`, not `0`). For a document that
379
+ /// is only reshaped and written out again, never evaluated.
380
+ pub fn parse_keeping_number_text(input: &str) -> Result<Self, String> {
381
+ Self::parse_with(input, true)
382
+ }
383
+
384
+ fn parse_with(input: &str, keep_number_text: bool) -> Result<Self, String> {
385
+ let mut parser = Parser {
386
+ chars: input.chars().peekable(),
387
+ depth: 0,
388
+ keep_number_text,
389
+ };
350
390
  let value = parser.parse_value()?;
351
391
  parser.skip_ws();
352
392
  Ok(value)
@@ -360,7 +400,7 @@ impl Json {
360
400
 
361
401
  fn write(&self, out: &mut String) {
362
402
  match self {
363
- Json::Object(fields) => {
403
+ Self::Object(fields) => {
364
404
  out.push('{');
365
405
  for (i, (k, v)) in fields.iter().enumerate() {
366
406
  if i > 0 {
@@ -372,7 +412,7 @@ impl Json {
372
412
  }
373
413
  out.push('}');
374
414
  }
375
- Json::Array(items) => {
415
+ Self::Array(items) => {
376
416
  out.push('[');
377
417
  for (i, v) in items.iter().enumerate() {
378
418
  if i > 0 {
@@ -382,21 +422,21 @@ impl Json {
382
422
  }
383
423
  out.push(']');
384
424
  }
385
- Json::Str(s) => write_escaped_string(s, out),
425
+ Self::Str(s) => write_escaped_string(s, out),
386
426
  // The exact source text wins when there is one (BUG#147): re-deriving digits from
387
427
  // `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)),
428
+ Self::Num(_, Some(raw)) => out.push_str(raw),
429
+ Self::Num(n, None) => out.push_str(&canonical_integer_digits(*n)),
390
430
  // Always a decimal point (`10.0`), as Ruby's Float#to_json; `Num` must not grow one.
391
- Json::Float(n) => {
431
+ Self::Float(n) => {
392
432
  let rendered = n.to_string();
393
433
  out.push_str(&rendered);
394
434
  if !rendered.contains('.') && !rendered.contains('e') && !rendered.contains('E') {
395
435
  out.push_str(".0");
396
436
  }
397
437
  }
398
- Json::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
399
- Json::Null => out.push_str("null"),
438
+ Self::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
439
+ Self::Null => out.push_str("null"),
400
440
  }
401
441
  }
402
442
  }
@@ -419,8 +459,9 @@ fn integral_i64(n: f64) -> Option<i64> {
419
459
  /// wrong `i64` via `integral_i64`'s `f64` path alone. Parsing the original digits sidesteps that
420
460
  /// gap; `raw` is `None` for anything short enough that the gap never opens, so this is the same
421
461
  /// 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))
462
+ fn exact_i64(n: f64, raw: Option<&str>) -> Option<i64> {
463
+ raw.and_then(|r| r.parse::<i64>().ok())
464
+ .or_else(|| integral_i64(n))
424
465
  }
425
466
 
426
467
  /// The digits `write` prints for a `Num` with no exact source text: whichever of the two
@@ -456,16 +497,24 @@ mod integral_i64_tests {
456
497
  fn accepts_i64_boundaries() {
457
498
  assert_eq!(integral_i64(i64::MIN as f64), Some(i64::MIN));
458
499
  // Largest f64 below 2^63; `i64::MAX` itself rounds up to 2^63 in f64.
459
- let largest_safe = 9223372036854774784.0_f64;
500
+ let largest_safe = 9_223_372_036_854_774_784.0_f64;
460
501
  assert_eq!(integral_i64(largest_safe), Some(largest_safe as i64));
461
502
  }
462
503
 
463
504
  #[test]
464
505
  fn accepts_i64_saturation_boundary() {
465
506
  // `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");
507
+ assert_eq!(
508
+ integral_i64(2f64.powi(63)),
509
+ None,
510
+ "2^63 must not saturate to i64::MAX"
511
+ );
467
512
  // 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");
513
+ assert_eq!(
514
+ integral_i64(-(2f64.powi(64))),
515
+ None,
516
+ "well below i64::MIN must not saturate to i64::MIN"
517
+ );
469
518
  }
470
519
 
471
520
  #[test]
@@ -478,18 +527,34 @@ mod integral_i64_tests {
478
527
  #[test]
479
528
  fn to_id_component_reflects_true_magnitude_instead_of_a_clamped_one() {
480
529
  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:?}");
530
+ let id = huge
531
+ .to_id_component()
532
+ .expect("numbers are always usable as an id component");
533
+ assert_ne!(
534
+ id,
535
+ i64::MAX.to_string(),
536
+ "must not silently clamp to i64::MAX"
537
+ );
538
+ assert!(
539
+ id.starts_with('1'),
540
+ "should reflect the real magnitude, got {id:?}"
541
+ );
484
542
  }
485
543
 
486
544
  #[test]
487
545
  fn parse_preserves_a_huge_integers_exact_digits_past_f64_precision() {
488
546
  // BUG#147: 2^100 (2**100, the fuzzer's own INTEGER_EDGE_CASES bignum), and the
489
547
  // signed 31-digit repro from Roster::Roster.AddSeat's routing_key mutation.
490
- for literal in ["-1267650600228229401496703205376", "1267650600228229401496703205376"] {
548
+ for literal in [
549
+ "-1267650600228229401496703205376",
550
+ "1267650600228229401496703205376",
551
+ ] {
491
552
  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");
553
+ assert_eq!(
554
+ parsed.to_json_string(),
555
+ literal,
556
+ "re-emitted JSON must not lose a single digit"
557
+ );
493
558
  assert_eq!(
494
559
  parsed.to_id_component().unwrap(),
495
560
  literal,
@@ -523,8 +588,14 @@ mod integral_i64_tests {
523
588
  fn a_bignum_and_an_ordinary_number_are_still_comparable_and_never_falsely_equal() {
524
589
  let huge = Json::parse("1267650600228229401496703205376").unwrap();
525
590
  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());
591
+ assert_ne!(
592
+ huge, other_huge,
593
+ "different bignums must not collide via f64 rounding"
594
+ );
595
+ assert_eq!(
596
+ huge,
597
+ Json::parse("1267650600228229401496703205376").unwrap()
598
+ );
528
599
  assert_ne!(huge, Json::Num(5.0, None));
529
600
  }
530
601
 
@@ -532,7 +603,10 @@ mod integral_i64_tests {
532
603
  fn to_id_component_refuses_an_empty_string() {
533
604
  // Ruby treats a blank identity part as absent; a blank id must not be accepted.
534
605
  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");
606
+ assert!(
607
+ blank.to_id_component().is_err(),
608
+ "an empty string must not be usable as an identity component"
609
+ );
536
610
  }
537
611
 
538
612
  #[test]
@@ -545,14 +619,21 @@ mod integral_i64_tests {
545
619
  fn to_id_component_lenient_accepts_an_empty_string() {
546
620
  // A blank key is truthy in Ruby and flows on as a non-matching value.
547
621
  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");
622
+ assert_eq!(
623
+ blank.to_id_component_lenient().unwrap(),
624
+ "",
625
+ "a present, blank string must be a valid (non-matching) component"
626
+ );
549
627
  }
550
628
 
551
629
  #[test]
552
630
  fn to_id_component_lenient_still_refuses_a_non_scalar() {
553
631
  // Only the blank-string case differs from `to_id_component`.
554
632
  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");
633
+ assert!(
634
+ list.to_id_component_lenient().is_err(),
635
+ "a non-scalar must still refuse, same as to_id_component"
636
+ );
556
637
  }
557
638
  }
558
639
 
@@ -570,7 +651,9 @@ mod value_object_shape_tests {
570
651
  #[test]
571
652
  fn an_object_passes_through_untouched() {
572
653
  let object = Json::obj(vec![("given", Json::Str("Ada".into()))]);
573
- assert!(object.expect_value_object_shape("name", "PersonName").is_ok());
654
+ assert!(object
655
+ .expect_value_object_shape("name", "PersonName")
656
+ .is_ok());
574
657
  }
575
658
 
576
659
  #[test]
@@ -579,7 +662,10 @@ mod value_object_shape_tests {
579
662
  refusal_text(&Json::Str("Ada".into())),
580
663
  "name is a PersonName — pass its fields as an object, not \"Ada\""
581
664
  );
582
- assert_eq!(refusal_text(&Json::Num(7.0, None)), "name is a PersonName — pass its fields as an object, not 7");
665
+ assert_eq!(
666
+ refusal_text(&Json::Num(7.0, None)),
667
+ "name is a PersonName — pass its fields as an object, not 7"
668
+ );
583
669
  }
584
670
 
585
671
  #[test]
@@ -616,7 +702,9 @@ fn write_escaped_string(s: &str, out: &mut String) {
616
702
  // Ruby's `JSON.generate` writes these two short, not as `\u0008` / `\u000c`.
617
703
  '\u{8}' => out.push_str("\\b"),
618
704
  '\u{c}' => out.push_str("\\f"),
619
- c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
705
+ c if (c as u32) < 0x20 => {
706
+ let _ = write!(out, "\\u{:04x}", c as u32);
707
+ }
620
708
  c => out.push(c),
621
709
  }
622
710
  }
@@ -627,6 +715,7 @@ fn write_escaped_string(s: &str, out: &mut String) {
627
715
  struct Parser<'a> {
628
716
  chars: std::iter::Peekable<std::str::Chars<'a>>,
629
717
  depth: usize,
718
+ keep_number_text: bool,
630
719
  }
631
720
 
632
721
  /// How deeply arrays and objects may nest before `parse` refuses. Each level is a recursive call,
@@ -634,13 +723,26 @@ struct Parser<'a> {
634
723
  /// refusing. 128 is `serde_json`'s own recursion limit, so the host and this parser refuse alike.
635
724
  const MAX_DEPTH: usize = 128;
636
725
 
637
- impl<'a> Parser<'a> {
726
+ impl Parser<'_> {
638
727
  fn skip_ws(&mut self) {
639
728
  while matches!(self.chars.peek(), Some(c) if c.is_whitespace()) {
640
729
  self.chars.next();
641
730
  }
642
731
  }
643
732
 
733
+ /// Appends the next char to `out` when it satisfies `pred`, reporting whether it did.
734
+ fn take_one(&mut self, out: &mut String, pred: impl Fn(char) -> bool) -> bool {
735
+ self.chars
736
+ .next_if(|c| pred(*c))
737
+ .map(|c| out.push(c))
738
+ .is_some()
739
+ }
740
+
741
+ /// Appends every consecutive ASCII digit to `out`.
742
+ fn take_digits(&mut self, out: &mut String) {
743
+ while self.take_one(out, |c| c.is_ascii_digit()) {}
744
+ }
745
+
644
746
  fn expect(&mut self, ch: char) -> Result<(), String> {
645
747
  match self.chars.next() {
646
748
  Some(c) if c == ch => Ok(()),
@@ -665,7 +767,7 @@ impl<'a> Parser<'a> {
665
767
  Some('{') => self.parse_object(),
666
768
  Some('[') => self.parse_array(),
667
769
  Some('"') => self.parse_string().map(Json::Str),
668
- Some('t') | Some('f') => self.parse_bool(),
770
+ Some('t' | 'f') => self.parse_bool(),
669
771
  Some('n') => self.parse_null(),
670
772
  Some(c) if *c == '-' || c.is_ascii_digit() => self.parse_number(),
671
773
  other => Err(format!("unexpected {other:?} in JSON")),
@@ -673,7 +775,10 @@ impl<'a> Parser<'a> {
673
775
  }
674
776
 
675
777
  // Counts one level of nesting for the duration of `body`; refuses past `MAX_DEPTH`.
676
- fn nested(&mut self, body: impl FnOnce(&mut Self) -> Result<Json, String>) -> Result<Json, String> {
778
+ fn nested(
779
+ &mut self,
780
+ body: impl FnOnce(&mut Self) -> Result<Json, String>,
781
+ ) -> Result<Json, String> {
677
782
  if self.depth >= MAX_DEPTH {
678
783
  return Err(format!("JSON nests deeper than {MAX_DEPTH} levels"));
679
784
  }
@@ -708,7 +813,7 @@ impl<'a> Parser<'a> {
708
813
  fields.push((key, value));
709
814
  self.skip_ws();
710
815
  match self.chars.next() {
711
- Some(',') => continue,
816
+ Some(',') => {}
712
817
  Some('}') => break,
713
818
  other => return Err(format!("expected ',' or '}}' in object, got {other:?}")),
714
819
  }
@@ -728,7 +833,7 @@ impl<'a> Parser<'a> {
728
833
  items.push(self.parse_value()?);
729
834
  self.skip_ws();
730
835
  match self.chars.next() {
731
- Some(',') => continue,
836
+ Some(',') => {}
732
837
  Some(']') => break,
733
838
  other => return Err(format!("expected ',' or ']' in array, got {other:?}")),
734
839
  }
@@ -752,8 +857,10 @@ impl<'a> Parser<'a> {
752
857
  Some('b') => out.push('\u{8}'),
753
858
  Some('f') => out.push('\u{c}'),
754
859
  Some('u') => {
755
- let hex: String = (0..4).map(|_| self.chars.next().unwrap_or('0')).collect();
756
- let code = u32::from_str_radix(&hex, 16).map_err(|_| format!("bad \\u escape {hex:?}"))?;
860
+ let hex: String =
861
+ (0..4).map(|_| self.chars.next().unwrap_or('0')).collect();
862
+ let code = u32::from_str_radix(&hex, 16)
863
+ .map_err(|_| format!("bad \\u escape {hex:?}"))?;
757
864
  out.push(char::from_u32(code).unwrap_or('\u{FFFD}'));
758
865
  }
759
866
  other => return Err(format!("bad escape sequence \\{other:?}")),
@@ -785,26 +892,14 @@ impl<'a> Parser<'a> {
785
892
 
786
893
  fn parse_number(&mut self) -> Result<Json, String> {
787
894
  let mut s = String::new();
788
- if self.chars.peek() == Some(&'-') {
789
- s.push(self.chars.next().unwrap());
790
- }
791
- while matches!(self.chars.peek(), Some(c) if c.is_ascii_digit()) {
792
- s.push(self.chars.next().unwrap());
793
- }
794
- if self.chars.peek() == Some(&'.') {
795
- s.push(self.chars.next().unwrap());
796
- while matches!(self.chars.peek(), Some(c) if c.is_ascii_digit()) {
797
- s.push(self.chars.next().unwrap());
798
- }
895
+ self.take_one(&mut s, |c| c == '-');
896
+ self.take_digits(&mut s);
897
+ if self.take_one(&mut s, |c| c == '.') {
898
+ self.take_digits(&mut s);
799
899
  }
800
- if matches!(self.chars.peek(), Some('e') | Some('E')) {
801
- s.push(self.chars.next().unwrap());
802
- if matches!(self.chars.peek(), Some('+') | Some('-')) {
803
- s.push(self.chars.next().unwrap());
804
- }
805
- while matches!(self.chars.peek(), Some(c) if c.is_ascii_digit()) {
806
- s.push(self.chars.next().unwrap());
807
- }
900
+ if self.take_one(&mut s, |c| c == 'e' || c == 'E') {
901
+ self.take_one(&mut s, |c| c == '+' || c == '-');
902
+ self.take_digits(&mut s);
808
903
  }
809
904
  let value: f64 = s.parse().map_err(|e| format!("bad number {s:?}: {e}"))?;
810
905
  // A plain integer literal keeps its exact source digits (BUG#147) whenever `write`'s own
@@ -813,7 +908,8 @@ impl<'a> Parser<'a> {
813
908
  // A literal with a `.`/exponent is a genuine `Float`, and Ruby's `Float` is the same
814
909
  // `f64` Rust's is, so there is nothing to preserve there.
815
910
  let is_plain_integer = !s.contains('.') && !s.contains(['e', 'E']);
816
- let keeps_source = is_plain_integer && canonical_integer_digits(value) != s;
911
+ let keeps_source =
912
+ self.keep_number_text || (is_plain_integer && canonical_integer_digits(value) != s);
817
913
  // `1e999` parses to infinity with no source text kept, and `write` would print it as `inf`:
818
914
  // not JSON, so refuse it. A plain integer keeps its digits, which `write` re-emits as-is.
819
915
  if !value.is_finite() && !keeps_source {