hecks 3.14.0 → 4.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 (419) 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/lib/hecks/adapters/driven/prism.rb +0 -2
  9. data/lib/hecks/adapters/driven.rb +0 -2
  10. data/lib/hecks/adapters/driving/json.rb +1 -7
  11. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  12. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  13. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  14. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  15. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  16. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  17. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  18. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  19. data/lib/hecks/bluebook/translation.rb +18 -10
  20. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  21. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  22. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  23. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  24. data/lib/hecks/bluebook.rb +2 -0
  25. data/lib/hecks/chapters.rb +3 -3
  26. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  27. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  28. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  29. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  30. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  31. data/lib/hecks/composer/adapters/jev.rb +66 -0
  32. data/lib/hecks/composer/agent.rb +113 -0
  33. data/lib/hecks/composer/assistant.rb +110 -0
  34. data/lib/hecks/composer/behavior_run.rb +53 -0
  35. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  36. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  37. data/lib/hecks/composer/board.rb +65 -0
  38. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  39. data/lib/hecks/composer/board_edit/source.rb +84 -0
  40. data/lib/hecks/composer/board_edit.rb +144 -0
  41. data/lib/hecks/composer/check.rb +89 -0
  42. data/lib/hecks/composer/domains.rb +81 -0
  43. data/lib/hecks/composer/jobs.rb +134 -0
  44. data/lib/hecks/composer/layout.rb +37 -0
  45. data/lib/hecks/composer/live.rb +150 -0
  46. data/lib/hecks/composer/outline.rb +48 -0
  47. data/lib/hecks/composer/page.html +989 -0
  48. data/lib/hecks/composer/panels.rb +142 -0
  49. data/lib/hecks/composer/parse.rb +83 -0
  50. data/lib/hecks/composer/plain.rb +28 -0
  51. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  52. data/lib/hecks/composer/session.rb +141 -0
  53. data/lib/hecks/composer/toolbox.rb +166 -0
  54. data/lib/hecks/composer/web.rb +156 -0
  55. data/lib/hecks/composer/workbench.rb +51 -0
  56. data/lib/hecks/composer/workspace.rb +81 -0
  57. data/lib/hecks/composer.rb +17 -0
  58. data/lib/hecks/corpus/adapters.rb +22 -0
  59. data/lib/hecks/corpus/rust.rb +1 -1
  60. data/lib/hecks/corpus/worlds.rb +19 -0
  61. data/lib/hecks/corpus.rb +5 -0
  62. data/lib/hecks/forms/app/commands.rb +2 -3
  63. data/lib/hecks/forms/app/queries.rb +2 -3
  64. data/lib/hecks/forms/app/records.rb +17 -5
  65. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  66. data/lib/hecks/forms/field_controls.rb +2 -2
  67. data/lib/hecks/forms/field_renderer.rb +1 -1
  68. data/lib/hecks/forms/field_shape.rb +36 -17
  69. data/lib/hecks/forms/index_renderer.rb +2 -1
  70. data/lib/hecks/forms/page.rb +1 -1
  71. data/lib/hecks/forms/params.rb +0 -13
  72. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  73. data/lib/hecks/forms/record_renderer.rb +11 -7
  74. data/lib/hecks/forms/rule_words.rb +100 -0
  75. data/lib/hecks/forms/value_object_shape.rb +2 -1
  76. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  77. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  78. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  79. data/lib/hecks/gate/stages.yml +25 -0
  80. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  81. data/lib/hecks/grammar/evolve.rb +0 -3
  82. data/lib/hecks/grammar/translation.bluebook +1 -1
  83. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  84. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  85. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  86. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  87. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  88. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  89. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  90. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  91. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  92. data/lib/hecks/hecks/codebase.behaviors +6 -0
  93. data/lib/hecks/hecks/codebase.bluebook +21 -0
  94. data/lib/hecks/hecks/composer.behaviors +68 -0
  95. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  96. data/lib/hecks/hecks/custodian.behaviors +0 -6
  97. data/lib/hecks/hecks/custodian.bluebook +3 -80
  98. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  99. data/lib/hecks/hecks/hecks.world +1 -1
  100. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  101. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  102. data/lib/hecks/kernel_capabilities.rb +1 -1
  103. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  104. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  105. data/lib/hecks/ports/access_control.rb +2 -2
  106. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  107. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  108. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  109. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  110. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  111. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  112. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  113. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  114. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  118. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  119. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  120. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  121. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  122. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  123. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  124. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  125. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  126. data/lib/hecks/ports.rb +0 -1
  127. data/lib/hecks/projection_files.rb +3 -1
  128. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  129. data/lib/hecks/projections/glossary/entries.rb +10 -4
  130. data/lib/hecks/projections/glossary/groups.rb +42 -0
  131. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  132. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  133. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  134. data/lib/hecks/projections/glossary/html.rb +1 -2
  135. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  136. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  137. data/lib/hecks/projections/glossary/page.css +143 -153
  138. data/lib/hecks/projections/glossary/page.js +63 -3
  139. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  140. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  141. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  142. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  143. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  144. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  145. data/lib/hecks/projections/glossary.rb +2 -1
  146. data/lib/hecks/projections/plain_english.rb +97 -0
  147. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  148. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  149. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  150. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  151. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  152. data/lib/hecks/projections/statements/actions.rb +99 -0
  153. data/lib/hecks/projections/statements/relationships.rb +86 -0
  154. data/lib/hecks/projections/statements.rb +15 -24
  155. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  156. data/lib/hecks/projector/exporter/translations.rb +18 -35
  157. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  158. data/lib/hecks/projector/exporter.rb +5 -1
  159. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  160. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  161. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  162. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  163. data/lib/hecks/runtime/outbox.rb +2 -9
  164. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  165. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  166. data/lib/hecks/runtime/value.rb +13 -0
  167. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  168. data/lib/hecks/rust_build/wasm.rb +0 -12
  169. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  170. data/lib/hecks/rust_format.rb +77 -0
  171. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  172. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  173. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  174. data/lib/hecks/tools/translation_audit.rb +8 -10
  175. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  176. data/lib/hecks/version.rb +1 -1
  177. data/lib/hecks/vocabulary.rb +2 -1
  178. data/lib/hecks.rb +3 -0
  179. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  180. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  181. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json} +1 -1
  182. data/rust/Cargo.toml +40 -0
  183. data/rust/build/Cargo.toml +36 -0
  184. data/rust/build/clippy.toml +4 -0
  185. data/rust/build/src/adapters_pass.rs +196 -0
  186. data/rust/build/src/binding_pass.rs +54 -13
  187. data/rust/build/src/build_artifact.rs +23 -5
  188. data/rust/build/src/cargo_sync.rs +69 -26
  189. data/rust/build/src/lineage_pass.rs +101 -87
  190. data/rust/build/src/main.rs +11 -2
  191. data/rust/build/src/pipeline.rs +80 -19
  192. data/rust/build/src/reserved_names.rs +6 -50
  193. data/rust/build/src/resolve.rs +82 -35
  194. data/rust/build/src/rustfmt.rs +100 -0
  195. data/rust/build/src/seams_pass.rs +85 -16
  196. data/rust/build/src/subprocess.rs +30 -6
  197. data/rust/build/src/tmp.rs +5 -2
  198. data/rust/build/src/translation_pass.rs +72 -22
  199. data/rust/build/src/world_pass.rs +109 -0
  200. data/rust/clippy.toml +4 -0
  201. data/rust/codegen/Cargo.toml +36 -0
  202. data/rust/codegen/clippy.toml +4 -0
  203. data/rust/codegen/src/asks.rs +40 -14
  204. data/rust/codegen/src/attr.rs +2 -2
  205. data/rust/codegen/src/bridging.rs +676 -117
  206. data/rust/codegen/src/commands.rs +979 -373
  207. data/rust/codegen/src/constraints.rs +88 -25
  208. data/rust/codegen/src/dependency_planning.rs +128 -34
  209. data/rust/codegen/src/domain_generator.rs +300 -170
  210. data/rust/codegen/src/exemplar.rs +87 -32
  211. data/rust/codegen/src/expr_emitter.rs +54 -6
  212. data/rust/codegen/src/fielded.rs +193 -41
  213. data/rust/codegen/src/hecks_naming.rs +124 -16
  214. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  215. data/rust/codegen/src/json.rs +80 -44
  216. data/rust/codegen/src/json_codec.rs +585 -149
  217. data/rust/codegen/src/literal.rs +104 -138
  218. data/rust/codegen/src/main.rs +145 -52
  219. data/rust/codegen/src/manifest.rs +76 -14
  220. data/rust/codegen/src/mutations.rs +1212 -239
  221. data/rust/codegen/src/naming.rs +111 -60
  222. data/rust/codegen/src/optional_pass.rs +90 -28
  223. data/rust/codegen/src/ports.rs +5 -4
  224. data/rust/codegen/src/prelude.rs +139 -49
  225. data/rust/codegen/src/queries.rs +601 -226
  226. data/rust/codegen/src/reactions.rs +268 -74
  227. data/rust/codegen/src/read_models.rs +606 -205
  228. data/rust/codegen/src/reference_specs.rs +16 -3
  229. data/rust/codegen/src/registry.rs +128 -79
  230. data/rust/codegen/src/reserved_names.rs +6 -50
  231. data/rust/codegen/src/shared.rs +71 -22
  232. data/rust/codegen/src/sidecars.rs +15 -5
  233. data/rust/codegen/src/skip_reason.rs +8 -2
  234. data/rust/codegen/src/types.rs +306 -108
  235. data/rust/host/Cargo.lock +1 -0
  236. data/rust/host/Cargo.toml +40 -4
  237. data/rust/host/HECKS_RELEASE +1 -1
  238. data/rust/host/clippy.toml +4 -0
  239. data/rust/host/src/api.rs +698 -218
  240. data/rust/host/src/auth.rs +491 -214
  241. data/rust/host/src/b64.rs +91 -0
  242. data/rust/host/src/bin/expr_harness.rs +6 -2
  243. data/rust/host/src/bin/lineage_harness.rs +47 -11
  244. data/rust/host/src/bin/mint_harness.rs +70 -37
  245. data/rust/host/src/bin/validate_harness.rs +16 -8
  246. data/rust/host/src/boot.rs +144 -68
  247. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  248. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  249. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  250. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  251. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  252. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  253. data/rust/host/src/checkout.rs +78 -32
  254. data/rust/host/src/clock.rs +8 -0
  255. data/rust/host/src/commerce_ir.rs +333 -95
  256. data/rust/host/src/crypto.rs +33 -0
  257. data/rust/host/src/dispatch.rs +990 -331
  258. data/rust/host/src/expr_json.rs +1021 -273
  259. data/rust/host/src/extension.rs +24 -4
  260. data/rust/host/src/field_hints.rs +11 -7
  261. data/rust/host/src/fuzz_support.rs +85 -13
  262. data/rust/host/src/host.rs +100 -0
  263. data/rust/host/src/http.rs +22 -0
  264. data/rust/host/src/ir.rs +326 -16
  265. data/rust/host/src/journal.rs +277 -176
  266. data/rust/host/src/lambda_client.rs +219 -53
  267. data/rust/host/src/lib.rs +16 -4
  268. data/rust/host/src/log.rs +62 -11
  269. data/rust/host/src/mint.rs +1153 -326
  270. data/rust/host/src/needs.rs +185 -44
  271. data/rust/host/src/payments/keystore.rs +197 -45
  272. data/rust/host/src/payments/roles.rs +39 -7
  273. data/rust/host/src/payments/tests.rs +1427 -282
  274. data/rust/host/src/payments.rs +391 -112
  275. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  276. data/rust/host/src/platform_signin/config.rs +11 -11
  277. data/rust/host/src/platform_signin/cookie.rs +2 -2
  278. data/rust/host/src/platform_signin/extension.rs +2 -1
  279. data/rust/host/src/platform_signin/handler.rs +9 -8
  280. data/rust/host/src/platform_signin/jti.rs +4 -2
  281. data/rust/host/src/platform_signin/jwks.rs +2 -1
  282. data/rust/host/src/platform_signin/jws.rs +2 -37
  283. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  284. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  285. data/rust/host/src/platform_signin/pages.rs +20 -20
  286. data/rust/host/src/platform_signin/session.rs +8 -1
  287. data/rust/host/src/platform_signin/settings.rs +6 -6
  288. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  289. data/rust/host/src/platform_signin/verify.rs +41 -41
  290. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  291. data/rust/host/src/presentation.rs +207 -63
  292. data/rust/host/src/presentation_write.rs +502 -187
  293. data/rust/host/src/query_step.rs +69 -18
  294. data/rust/host/src/rate_limit.rs +541 -142
  295. data/rust/host/src/reference_transform.rs +77 -25
  296. data/rust/host/src/reference_validate.rs +443 -104
  297. data/rust/host/src/resend.rs +190 -55
  298. data/rust/host/src/secrets.rs +116 -21
  299. data/rust/host/src/server.rs +483 -104
  300. data/rust/host/src/storage_shape.rs +88 -21
  301. data/rust/host/src/test_pg.rs +17 -7
  302. data/rust/host/src/ui_schema.rs +495 -207
  303. data/rust/host/src/wasm_runner.rs +21 -6
  304. data/rust/host/src/web/commerce.rs +101 -23
  305. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  306. data/rust/host/src/web/newsletter/template.rs +26 -7
  307. data/rust/host/src/web/newsletter/tests.rs +112 -23
  308. data/rust/host/src/web/newsletter.rs +209 -59
  309. data/rust/host/src/web/newsletter_send.rs +251 -79
  310. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  311. data/rust/host/src/web/registration_receipt.rs +100 -24
  312. data/rust/host/src/web/registrations.rs +1384 -332
  313. data/rust/host/src/web/resend_webhook.rs +138 -80
  314. data/rust/host/src/web.rs +2416 -508
  315. data/rust/lsp/Cargo.toml +36 -0
  316. data/rust/lsp/clippy.toml +4 -0
  317. data/rust/lsp/src/json.rs +29 -22
  318. data/rust/lsp/src/main.rs +81 -31
  319. data/rust/lsp/src/outline.rs +23 -7
  320. data/rust/lsp/src/rpc.rs +17 -4
  321. data/rust/parser/Cargo.toml +36 -0
  322. data/rust/parser/clippy.toml +4 -0
  323. data/rust/parser/src/build/closed_sets.rs +6 -1
  324. data/rust/parser/src/build/identity.rs +4 -3
  325. data/rust/parser/src/build/mod.rs +1 -1
  326. data/rust/parser/src/build/named_pattern.rs +4 -1
  327. data/rust/parser/src/build/naming.rs +2 -80
  328. data/rust/parser/src/build/pattern_subset.rs +6 -8
  329. data/rust/parser/src/build/query_derive.rs +3 -1
  330. data/rust/parser/src/build/query_inference.rs +14 -5
  331. data/rust/parser/src/build/read_model.rs +4 -1
  332. data/rust/parser/src/build/references.rs +4 -3
  333. data/rust/parser/src/canonical.rs +34 -15
  334. data/rust/parser/src/emit.rs +187 -105
  335. data/rust/parser/src/expr/ast_json.rs +134 -42
  336. data/rust/parser/src/expr/evaluator.rs +27 -9
  337. data/rust/parser/src/expr/mod.rs +54 -13
  338. data/rust/parser/src/expr/resolver.rs +299 -92
  339. data/rust/parser/src/ir.rs +35 -3
  340. data/rust/parser/src/keywords.rs +7168 -516
  341. data/rust/parser/src/lex.rs +11 -25
  342. data/rust/parser/src/lib.rs +1 -0
  343. data/rust/parser/src/main.rs +65 -6
  344. data/rust/parser/src/parse/adapter.rs +157 -0
  345. data/rust/parser/src/parse/aggregate.rs +19 -10
  346. data/rust/parser/src/parse/chapter.rs +16 -17
  347. data/rust/parser/src/parse/command.rs +35 -16
  348. data/rust/parser/src/parse/domain_port.rs +4 -7
  349. data/rust/parser/src/parse/entity.rs +21 -11
  350. data/rust/parser/src/parse/hecksagon.rs +10 -4
  351. data/rust/parser/src/parse/lifecycle.rs +9 -5
  352. data/rust/parser/src/parse/mod.rs +22 -91
  353. data/rust/parser/src/parse/needs.rs +21 -12
  354. data/rust/parser/src/parse/policy.rs +2 -2
  355. data/rust/parser/src/parse/process_manager.rs +9 -11
  356. data/rust/parser/src/parse/query.rs +8 -4
  357. data/rust/parser/src/parse/read_model.rs +57 -16
  358. data/rust/parser/src/parse/value_object.rs +7 -2
  359. data/rust/parser/src/parse/world.rs +322 -0
  360. data/rust/parser/src/ruby_value.rs +7 -6
  361. data/rust/src/exemplar/json.rs +4 -4
  362. data/rust/src/exemplar/mutations.rs +18 -0
  363. data/rust/src/exemplar/read_models.rs +1 -10
  364. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  365. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  366. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  367. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  368. data/rust/src/kernel/cli.rs +229 -112
  369. data/rust/src/kernel/dispatch.rs +323 -115
  370. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  371. data/rust/src/kernel/expr.rs +203 -86
  372. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  373. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  374. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  375. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  376. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  377. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  378. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  379. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  380. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  381. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  382. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  383. data/rust/src/kernel/expression_operators/text.rs +120 -27
  384. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  385. data/rust/src/kernel/json.rs +230 -148
  386. data/rust/src/kernel/mod.rs +46 -33
  387. data/rust/src/kernel/named_query.rs +63 -15
  388. data/rust/src/kernel/needs.rs +112 -25
  389. data/rust/src/kernel/orchestrate.rs +980 -581
  390. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  391. data/rust/src/kernel/pattern.rs +107 -34
  392. data/rust/src/kernel/query_comparators.rs +118 -41
  393. data/rust/src/kernel/query_ordering.rs +13 -5
  394. data/rust/src/kernel/read_model.rs +479 -181
  395. data/rust/src/kernel/reference_lookup.rs +122 -26
  396. data/rust/src/kernel/repository.rs +293 -66
  397. data/rust/src/kernel/routing.rs +58 -42
  398. data/rust/src/kernel/ruby_float.rs +139 -0
  399. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  400. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  401. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  402. data/rust/src/kernel/vocab/mod.rs +6 -6
  403. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  404. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  405. data/rust/src/lib.rs +50 -0
  406. data/rust/src/main.rs +4 -1
  407. data/rust/web/Cargo.toml +36 -0
  408. data/rust/web/clippy.toml +4 -0
  409. metadata +82 -15
  410. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  411. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  412. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  413. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  414. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  415. data/lib/hecks/ports/authentication.port +0 -6
  416. data/lib/hecks/ports/authentication.rb +0 -67
  417. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  418. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  419. 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,16 @@ 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
+ let mut parser = Parser {
375
+ chars: input.chars().peekable(),
376
+ depth: 0,
377
+ };
350
378
  let value = parser.parse_value()?;
351
379
  parser.skip_ws();
352
380
  Ok(value)
@@ -360,7 +388,7 @@ impl Json {
360
388
 
361
389
  fn write(&self, out: &mut String) {
362
390
  match self {
363
- Json::Object(fields) => {
391
+ Self::Object(fields) => {
364
392
  out.push('{');
365
393
  for (i, (k, v)) in fields.iter().enumerate() {
366
394
  if i > 0 {
@@ -372,7 +400,7 @@ impl Json {
372
400
  }
373
401
  out.push('}');
374
402
  }
375
- Json::Array(items) => {
403
+ Self::Array(items) => {
376
404
  out.push('[');
377
405
  for (i, v) in items.iter().enumerate() {
378
406
  if i > 0 {
@@ -382,21 +410,21 @@ impl Json {
382
410
  }
383
411
  out.push(']');
384
412
  }
385
- Json::Str(s) => write_escaped_string(s, out),
413
+ Self::Str(s) => write_escaped_string(s, out),
386
414
  // The exact source text wins when there is one (BUG#147): re-deriving digits from
387
415
  // `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)),
416
+ Self::Num(_, Some(raw)) => out.push_str(raw),
417
+ Self::Num(n, None) => out.push_str(&canonical_integer_digits(*n)),
390
418
  // Always a decimal point (`10.0`), as Ruby's Float#to_json; `Num` must not grow one.
391
- Json::Float(n) => {
419
+ Self::Float(n) => {
392
420
  let rendered = n.to_string();
393
421
  out.push_str(&rendered);
394
422
  if !rendered.contains('.') && !rendered.contains('e') && !rendered.contains('E') {
395
423
  out.push_str(".0");
396
424
  }
397
425
  }
398
- Json::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
399
- Json::Null => out.push_str("null"),
426
+ Self::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
427
+ Self::Null => out.push_str("null"),
400
428
  }
401
429
  }
402
430
  }
@@ -419,8 +447,9 @@ fn integral_i64(n: f64) -> Option<i64> {
419
447
  /// wrong `i64` via `integral_i64`'s `f64` path alone. Parsing the original digits sidesteps that
420
448
  /// gap; `raw` is `None` for anything short enough that the gap never opens, so this is the same
421
449
  /// 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))
450
+ fn exact_i64(n: f64, raw: Option<&str>) -> Option<i64> {
451
+ raw.and_then(|r| r.parse::<i64>().ok())
452
+ .or_else(|| integral_i64(n))
424
453
  }
425
454
 
426
455
  /// The digits `write` prints for a `Num` with no exact source text: whichever of the two
@@ -456,16 +485,24 @@ mod integral_i64_tests {
456
485
  fn accepts_i64_boundaries() {
457
486
  assert_eq!(integral_i64(i64::MIN as f64), Some(i64::MIN));
458
487
  // Largest f64 below 2^63; `i64::MAX` itself rounds up to 2^63 in f64.
459
- let largest_safe = 9223372036854774784.0_f64;
488
+ let largest_safe = 9_223_372_036_854_774_784.0_f64;
460
489
  assert_eq!(integral_i64(largest_safe), Some(largest_safe as i64));
461
490
  }
462
491
 
463
492
  #[test]
464
493
  fn accepts_i64_saturation_boundary() {
465
494
  // `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");
495
+ assert_eq!(
496
+ integral_i64(2f64.powi(63)),
497
+ None,
498
+ "2^63 must not saturate to i64::MAX"
499
+ );
467
500
  // 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");
501
+ assert_eq!(
502
+ integral_i64(-(2f64.powi(64))),
503
+ None,
504
+ "well below i64::MIN must not saturate to i64::MIN"
505
+ );
469
506
  }
470
507
 
471
508
  #[test]
@@ -478,18 +515,34 @@ mod integral_i64_tests {
478
515
  #[test]
479
516
  fn to_id_component_reflects_true_magnitude_instead_of_a_clamped_one() {
480
517
  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:?}");
518
+ let id = huge
519
+ .to_id_component()
520
+ .expect("numbers are always usable as an id component");
521
+ assert_ne!(
522
+ id,
523
+ i64::MAX.to_string(),
524
+ "must not silently clamp to i64::MAX"
525
+ );
526
+ assert!(
527
+ id.starts_with('1'),
528
+ "should reflect the real magnitude, got {id:?}"
529
+ );
484
530
  }
485
531
 
486
532
  #[test]
487
533
  fn parse_preserves_a_huge_integers_exact_digits_past_f64_precision() {
488
534
  // BUG#147: 2^100 (2**100, the fuzzer's own INTEGER_EDGE_CASES bignum), and the
489
535
  // signed 31-digit repro from Roster::Roster.AddSeat's routing_key mutation.
490
- for literal in ["-1267650600228229401496703205376", "1267650600228229401496703205376"] {
536
+ for literal in [
537
+ "-1267650600228229401496703205376",
538
+ "1267650600228229401496703205376",
539
+ ] {
491
540
  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");
541
+ assert_eq!(
542
+ parsed.to_json_string(),
543
+ literal,
544
+ "re-emitted JSON must not lose a single digit"
545
+ );
493
546
  assert_eq!(
494
547
  parsed.to_id_component().unwrap(),
495
548
  literal,
@@ -523,8 +576,14 @@ mod integral_i64_tests {
523
576
  fn a_bignum_and_an_ordinary_number_are_still_comparable_and_never_falsely_equal() {
524
577
  let huge = Json::parse("1267650600228229401496703205376").unwrap();
525
578
  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());
579
+ assert_ne!(
580
+ huge, other_huge,
581
+ "different bignums must not collide via f64 rounding"
582
+ );
583
+ assert_eq!(
584
+ huge,
585
+ Json::parse("1267650600228229401496703205376").unwrap()
586
+ );
528
587
  assert_ne!(huge, Json::Num(5.0, None));
529
588
  }
530
589
 
@@ -532,7 +591,10 @@ mod integral_i64_tests {
532
591
  fn to_id_component_refuses_an_empty_string() {
533
592
  // Ruby treats a blank identity part as absent; a blank id must not be accepted.
534
593
  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");
594
+ assert!(
595
+ blank.to_id_component().is_err(),
596
+ "an empty string must not be usable as an identity component"
597
+ );
536
598
  }
537
599
 
538
600
  #[test]
@@ -545,14 +607,21 @@ mod integral_i64_tests {
545
607
  fn to_id_component_lenient_accepts_an_empty_string() {
546
608
  // A blank key is truthy in Ruby and flows on as a non-matching value.
547
609
  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");
610
+ assert_eq!(
611
+ blank.to_id_component_lenient().unwrap(),
612
+ "",
613
+ "a present, blank string must be a valid (non-matching) component"
614
+ );
549
615
  }
550
616
 
551
617
  #[test]
552
618
  fn to_id_component_lenient_still_refuses_a_non_scalar() {
553
619
  // Only the blank-string case differs from `to_id_component`.
554
620
  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");
621
+ assert!(
622
+ list.to_id_component_lenient().is_err(),
623
+ "a non-scalar must still refuse, same as to_id_component"
624
+ );
556
625
  }
557
626
  }
558
627
 
@@ -570,7 +639,9 @@ mod value_object_shape_tests {
570
639
  #[test]
571
640
  fn an_object_passes_through_untouched() {
572
641
  let object = Json::obj(vec![("given", Json::Str("Ada".into()))]);
573
- assert!(object.expect_value_object_shape("name", "PersonName").is_ok());
642
+ assert!(object
643
+ .expect_value_object_shape("name", "PersonName")
644
+ .is_ok());
574
645
  }
575
646
 
576
647
  #[test]
@@ -579,7 +650,10 @@ mod value_object_shape_tests {
579
650
  refusal_text(&Json::Str("Ada".into())),
580
651
  "name is a PersonName — pass its fields as an object, not \"Ada\""
581
652
  );
582
- assert_eq!(refusal_text(&Json::Num(7.0, None)), "name is a PersonName — pass its fields as an object, not 7");
653
+ assert_eq!(
654
+ refusal_text(&Json::Num(7.0, None)),
655
+ "name is a PersonName — pass its fields as an object, not 7"
656
+ );
583
657
  }
584
658
 
585
659
  #[test]
@@ -616,7 +690,9 @@ fn write_escaped_string(s: &str, out: &mut String) {
616
690
  // Ruby's `JSON.generate` writes these two short, not as `\u0008` / `\u000c`.
617
691
  '\u{8}' => out.push_str("\\b"),
618
692
  '\u{c}' => out.push_str("\\f"),
619
- c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
693
+ c if (c as u32) < 0x20 => {
694
+ let _ = write!(out, "\\u{:04x}", c as u32);
695
+ }
620
696
  c => out.push(c),
621
697
  }
622
698
  }
@@ -634,13 +710,26 @@ struct Parser<'a> {
634
710
  /// refusing. 128 is `serde_json`'s own recursion limit, so the host and this parser refuse alike.
635
711
  const MAX_DEPTH: usize = 128;
636
712
 
637
- impl<'a> Parser<'a> {
713
+ impl Parser<'_> {
638
714
  fn skip_ws(&mut self) {
639
715
  while matches!(self.chars.peek(), Some(c) if c.is_whitespace()) {
640
716
  self.chars.next();
641
717
  }
642
718
  }
643
719
 
720
+ /// Appends the next char to `out` when it satisfies `pred`, reporting whether it did.
721
+ fn take_one(&mut self, out: &mut String, pred: impl Fn(char) -> bool) -> bool {
722
+ self.chars
723
+ .next_if(|c| pred(*c))
724
+ .map(|c| out.push(c))
725
+ .is_some()
726
+ }
727
+
728
+ /// Appends every consecutive ASCII digit to `out`.
729
+ fn take_digits(&mut self, out: &mut String) {
730
+ while self.take_one(out, |c| c.is_ascii_digit()) {}
731
+ }
732
+
644
733
  fn expect(&mut self, ch: char) -> Result<(), String> {
645
734
  match self.chars.next() {
646
735
  Some(c) if c == ch => Ok(()),
@@ -665,7 +754,7 @@ impl<'a> Parser<'a> {
665
754
  Some('{') => self.parse_object(),
666
755
  Some('[') => self.parse_array(),
667
756
  Some('"') => self.parse_string().map(Json::Str),
668
- Some('t') | Some('f') => self.parse_bool(),
757
+ Some('t' | 'f') => self.parse_bool(),
669
758
  Some('n') => self.parse_null(),
670
759
  Some(c) if *c == '-' || c.is_ascii_digit() => self.parse_number(),
671
760
  other => Err(format!("unexpected {other:?} in JSON")),
@@ -673,7 +762,10 @@ impl<'a> Parser<'a> {
673
762
  }
674
763
 
675
764
  // 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> {
765
+ fn nested(
766
+ &mut self,
767
+ body: impl FnOnce(&mut Self) -> Result<Json, String>,
768
+ ) -> Result<Json, String> {
677
769
  if self.depth >= MAX_DEPTH {
678
770
  return Err(format!("JSON nests deeper than {MAX_DEPTH} levels"));
679
771
  }
@@ -708,7 +800,7 @@ impl<'a> Parser<'a> {
708
800
  fields.push((key, value));
709
801
  self.skip_ws();
710
802
  match self.chars.next() {
711
- Some(',') => continue,
803
+ Some(',') => {}
712
804
  Some('}') => break,
713
805
  other => return Err(format!("expected ',' or '}}' in object, got {other:?}")),
714
806
  }
@@ -728,7 +820,7 @@ impl<'a> Parser<'a> {
728
820
  items.push(self.parse_value()?);
729
821
  self.skip_ws();
730
822
  match self.chars.next() {
731
- Some(',') => continue,
823
+ Some(',') => {}
732
824
  Some(']') => break,
733
825
  other => return Err(format!("expected ',' or ']' in array, got {other:?}")),
734
826
  }
@@ -752,8 +844,10 @@ impl<'a> Parser<'a> {
752
844
  Some('b') => out.push('\u{8}'),
753
845
  Some('f') => out.push('\u{c}'),
754
846
  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:?}"))?;
847
+ let hex: String =
848
+ (0..4).map(|_| self.chars.next().unwrap_or('0')).collect();
849
+ let code = u32::from_str_radix(&hex, 16)
850
+ .map_err(|_| format!("bad \\u escape {hex:?}"))?;
757
851
  out.push(char::from_u32(code).unwrap_or('\u{FFFD}'));
758
852
  }
759
853
  other => return Err(format!("bad escape sequence \\{other:?}")),
@@ -785,26 +879,14 @@ impl<'a> Parser<'a> {
785
879
 
786
880
  fn parse_number(&mut self) -> Result<Json, String> {
787
881
  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
- }
799
- }
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
- }
882
+ self.take_one(&mut s, |c| c == '-');
883
+ self.take_digits(&mut s);
884
+ if self.take_one(&mut s, |c| c == '.') {
885
+ self.take_digits(&mut s);
886
+ }
887
+ if self.take_one(&mut s, |c| c == 'e' || c == 'E') {
888
+ self.take_one(&mut s, |c| c == '+' || c == '-');
889
+ self.take_digits(&mut s);
808
890
  }
809
891
  let value: f64 = s.parse().map_err(|e| format!("bad number {s:?}: {e}"))?;
810
892
  // A plain integer literal keeps its exact source digits (BUG#147) whenever `write`'s own