hecks 2.8.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (886) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +50 -7
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +32 -100
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +10 -32
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  25. data/lib/hecks/adapters/driven/postgres.rb +149 -136
  26. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  27. data/lib/hecks/adapters/driven/prism.rb +48 -38
  28. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  29. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  30. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  31. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  32. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  33. data/lib/hecks/adapters/driven/tenant_provisioner.rb +76 -62
  34. data/lib/hecks/adapters/driven.rb +5 -36
  35. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  36. data/lib/hecks/behaviors/dsl.rb +7 -25
  37. data/lib/hecks/behaviors/expectations.rb +32 -142
  38. data/lib/hecks/behaviors/ir.rb +7 -20
  39. data/lib/hecks/behaviors/rspec.rb +6 -17
  40. data/lib/hecks/behaviors/runner.rb +10 -34
  41. data/lib/hecks/behaviors.rb +4 -8
  42. data/lib/hecks/bench/cli.rb +62 -0
  43. data/lib/hecks/bench/environment.rb +71 -0
  44. data/lib/hecks/bench/postgres_probe.rb +30 -0
  45. data/lib/hecks/bench/report.rb +76 -0
  46. data/lib/hecks/bench/ruby_runner.rb +93 -0
  47. data/lib/hecks/bench/run.rb +55 -0
  48. data/lib/hecks/bench/rust_runner.rb +101 -0
  49. data/lib/hecks/bench/stats.rb +46 -0
  50. data/lib/hecks/bench/suite.rb +98 -0
  51. data/lib/hecks/bench/workload.rb +89 -0
  52. data/lib/hecks/bench.rb +17 -0
  53. data/lib/hecks/bluebook/aggregate.rb +9 -75
  54. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  55. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  56. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  57. data/lib/hecks/bluebook/assembly/contracts.rb +56 -174
  58. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  59. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  60. data/lib/hecks/bluebook/assembly.rb +6 -45
  61. data/lib/hecks/bluebook/attribute.rb +12 -61
  62. data/lib/hecks/bluebook/behaviour/aggregate.rb +39 -39
  63. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  64. data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
  65. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  66. data/lib/hecks/bluebook/behaviour/domain_port.rb +11 -20
  67. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  68. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  69. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  70. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  71. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
  73. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  75. data/lib/hecks/bluebook/capabilities.rb +17 -49
  76. data/lib/hecks/bluebook/chapter.rb +16 -50
  77. data/lib/hecks/bluebook/command.rb +23 -81
  78. data/lib/hecks/bluebook/domain_port.rb +49 -67
  79. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  80. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  81. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  82. data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
  83. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +96 -708
  84. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +63 -169
  85. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +3 -1
  86. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  87. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  88. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +42 -89
  89. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  90. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  91. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +148 -197
  92. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  93. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  94. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  95. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  96. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  97. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  98. data/lib/hecks/bluebook/dsl/query_builder.rb +38 -43
  99. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  100. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  101. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  102. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  103. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  104. data/lib/hecks/bluebook/dsl/world_builder.rb +46 -111
  105. data/lib/hecks/bluebook/entity.rb +13 -71
  106. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  107. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  108. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  109. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  110. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  111. data/lib/hecks/bluebook/expression/resolver.rb +39 -661
  112. data/lib/hecks/bluebook/hexagon.rb +18 -33
  113. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  114. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  115. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  116. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  117. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  118. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  119. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +25 -176
  120. data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
  121. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -306
  122. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  123. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  124. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  125. data/lib/hecks/bluebook/meta_validator.rb +87 -352
  126. data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
  127. data/lib/hecks/bluebook/model_check.rb +122 -596
  128. data/lib/hecks/bluebook/pattern_subset.rb +49 -69
  129. data/lib/hecks/bluebook/process_manager.rb +13 -83
  130. data/lib/hecks/bluebook/project_loader.rb +3 -39
  131. data/lib/hecks/bluebook/project_register.rb +12 -60
  132. data/lib/hecks/bluebook/query.rb +27 -51
  133. data/lib/hecks/bluebook/read_model.rb +57 -81
  134. data/lib/hecks/bluebook/reference.rb +7 -41
  135. data/lib/hecks/bluebook/smoke_test.rb +9 -99
  136. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  137. data/lib/hecks/bluebook/translation.rb +26 -62
  138. data/lib/hecks/bluebook/value_object.rb +11 -36
  139. data/lib/hecks/bluebook.rb +5 -22
  140. data/lib/hecks/cache_dir.rb +4 -23
  141. data/lib/hecks/canonical_json.rb +30 -0
  142. data/lib/hecks/chapters.rb +102 -0
  143. data/lib/hecks/cli/behaviors.rb +61 -0
  144. data/lib/hecks/cli/canonicalise.rb +25 -0
  145. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  146. data/lib/hecks/cli/check_era.rb +88 -0
  147. data/lib/hecks/cli/console.rb +66 -0
  148. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  149. data/lib/hecks/cli/doc_coverage.rb +32 -0
  150. data/lib/hecks/cli/document.rb +5 -23
  151. data/lib/hecks/cli/expression_projection.rb +41 -0
  152. data/lib/hecks/cli/follow.rb +88 -0
  153. data/lib/hecks/cli/history.rb +44 -0
  154. data/lib/hecks/cli/ir.rb +4 -6
  155. data/lib/hecks/cli/mcp.rb +5 -6
  156. data/lib/hecks/cli/model_check.rb +24 -88
  157. data/lib/hecks/cli/pattern_cases.rb +47 -0
  158. data/lib/hecks/cli/present.rb +39 -0
  159. data/lib/hecks/cli/project_cli.rb +223 -64
  160. data/lib/hecks/cli/project_diagrams.rb +4 -10
  161. data/lib/hecks/cli/project_glossary.rb +44 -0
  162. data/lib/hecks/cli/reference.rb +46 -0
  163. data/lib/hecks/cli/refresh_projections.rb +44 -0
  164. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  165. data/lib/hecks/cli/release.rb +70 -0
  166. data/lib/hecks/cli/release_gem.rb +59 -0
  167. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  168. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  169. data/lib/hecks/cli/run.rb +15 -61
  170. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  171. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  172. data/lib/hecks/cli/shape.rb +70 -0
  173. data/lib/hecks/cli/smoke_http.rb +230 -0
  174. data/lib/hecks/cli/smoke_test.rb +7 -19
  175. data/lib/hecks/cli/spec_example.rb +27 -0
  176. data/lib/hecks/cli/statements.rb +32 -0
  177. data/lib/hecks/cli/stores.rb +4 -6
  178. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  179. data/lib/hecks/cli.rb +216 -73
  180. data/lib/hecks/codemod.rb +324 -0
  181. data/lib/hecks/construct.rb +8 -34
  182. data/lib/hecks/corpus.rb +423 -0
  183. data/lib/hecks/deploy/bluebook/deploy.bluebook +617 -158
  184. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -11
  185. data/lib/hecks/deploy/oidc.json +82 -2
  186. data/lib/hecks/doc/reference.rb +351 -0
  187. data/lib/hecks/doors/cli_door.rb +196 -0
  188. data/lib/hecks/doors/cli_runner.rb +297 -0
  189. data/lib/hecks/{facade → doors}/command_request.rb +14 -34
  190. data/lib/hecks/doors/handle.rb +206 -0
  191. data/lib/hecks/doors/json_door.rb +132 -0
  192. data/lib/hecks/doors/launcher_options.rb +129 -0
  193. data/lib/hecks/{cli → doors}/mcp_door.rb +25 -120
  194. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  195. data/lib/hecks/doors/ruby_door/aggregate_door.rb +162 -0
  196. data/lib/hecks/doors/ruby_door/chapter.rb +72 -0
  197. data/lib/hecks/doors/ruby_door.rb +77 -0
  198. data/lib/hecks/doors.rb +61 -0
  199. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  200. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  201. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  202. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  203. data/lib/hecks/embryonaut_bluebook.rb +5 -72
  204. data/lib/hecks/engine_agreement.rb +156 -0
  205. data/lib/hecks/facade.rb +23 -51
  206. data/lib/hecks/forms/app.rb +21 -76
  207. data/lib/hecks/forms/banking_presentation.rb +50 -0
  208. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  209. data/lib/hecks/forms/field_renderer.rb +17 -156
  210. data/lib/hecks/forms/field_shape.rb +31 -222
  211. data/lib/hecks/forms/html.rb +12 -47
  212. data/lib/hecks/forms/index_renderer.rb +1 -4
  213. data/lib/hecks/forms/page.rb +3 -9
  214. data/lib/hecks/forms/params.rb +14 -60
  215. data/lib/hecks/forms/port_argument.rb +2 -19
  216. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  217. data/lib/hecks/forms/record_renderer.rb +8 -75
  218. data/lib/hecks/forms/record_table.rb +7 -50
  219. data/lib/hecks/forms/reference_options.rb +3 -30
  220. data/lib/hecks/forms/value_object_shape.rb +8 -35
  221. data/lib/hecks/forms.rb +5 -33
  222. data/lib/hecks/fqn.rb +5 -12
  223. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  224. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  225. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  226. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  227. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  228. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  229. data/lib/hecks/framework.rb +11 -80
  230. data/lib/hecks/freezer.rb +5 -39
  231. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  232. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  233. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  234. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  235. data/lib/hecks/fuzzing/differential.rb +156 -0
  236. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  237. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  238. data/lib/hecks/fuzzing/form_census.rb +136 -0
  239. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  240. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  241. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  242. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  243. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  244. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  245. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  246. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  247. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  248. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  249. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  250. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  251. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  252. data/lib/hecks/fuzzing/properties.rb +162 -0
  253. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  254. data/lib/hecks/fuzzing/replay.rb +505 -0
  255. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  256. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  257. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  258. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  259. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  260. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  261. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  262. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  263. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  264. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  265. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  266. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  267. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  268. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  269. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  270. data/lib/hecks/fuzzing.rb +23 -0
  271. data/lib/hecks/grammar/evolve.rb +336 -0
  272. data/lib/hecks/grammar/expression.bluebook +15 -40
  273. data/lib/hecks/grammar/translation.bluebook +3 -6
  274. data/lib/hecks/grammar.rb +16 -41
  275. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  276. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  277. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  278. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  279. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  280. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  281. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  282. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  283. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  284. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  285. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  286. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  287. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  288. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  289. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  290. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  291. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  292. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  293. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  294. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  295. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  296. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  297. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  298. data/lib/hecks/hecks/adapters/git.rb +102 -0
  299. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  300. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  301. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  302. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  303. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  304. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  305. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  306. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  307. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  308. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  309. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  310. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  311. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  312. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  313. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  314. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  315. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  316. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  317. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  318. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  319. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  320. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  321. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  322. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  323. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  324. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  325. data/lib/hecks/hecks/codebase.behaviors +172 -0
  326. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  327. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  328. data/lib/hecks/hecks/custodian.behaviors +179 -0
  329. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  330. data/lib/hecks/hecks/deploy.behaviors +74 -0
  331. data/lib/hecks/hecks/deploy.world +12 -0
  332. data/lib/hecks/hecks/environments/memory.world +10 -0
  333. data/lib/hecks/hecks/governance.world +7 -0
  334. data/lib/hecks/hecks/hecks.behaviors +62 -0
  335. data/lib/hecks/hecks/hecks.bluebook +124 -0
  336. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  337. data/lib/hecks/hecks/hecks.world +42 -0
  338. data/lib/hecks/ir.rb +14 -83
  339. data/lib/hecks/kernel_capabilities.rb +194 -0
  340. data/lib/hecks/language/adapter.bluebook +2 -7
  341. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  342. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  343. data/lib/hecks/language/bluebook/bluebook.bluebook +26 -83
  344. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  345. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  346. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  347. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  348. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  349. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  350. data/lib/hecks/language/bluebook/query.bluebook +19 -43
  351. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  352. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  353. data/lib/hecks/language/bluebook/vocabulary.bluebook +83 -198
  354. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  355. data/lib/hecks/language/hecksagon/domain_port.bluebook +14 -19
  356. data/lib/hecks/language/hecksagon/hecksagon.bluebook +26 -25
  357. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  358. data/lib/hecks/language/oidc.json +35 -0
  359. data/lib/hecks/language/port.bluebook +4 -17
  360. data/lib/hecks/language/translation/translation.bluebook +3 -9
  361. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  362. data/lib/hecks/language/world/wiring.bluebook +1 -3
  363. data/lib/hecks/language/world/world.bluebook +5 -7
  364. data/lib/hecks/literal.rb +8 -50
  365. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  366. data/lib/hecks/naming.rb +29 -137
  367. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  368. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  369. data/lib/hecks/ports/access_control.rb +7 -23
  370. data/lib/hecks/ports/agent/answers.rb +5 -23
  371. data/lib/hecks/ports/agent.rb +47 -172
  372. data/lib/hecks/ports/authentication.rb +12 -39
  373. data/lib/hecks/ports/authorization.rb +16 -43
  374. data/lib/hecks/ports/clock.rb +5 -45
  375. data/lib/hecks/ports/extraction.rb +7 -12
  376. data/lib/hecks/ports/identity_assignment.rb +8 -29
  377. data/lib/hecks/ports/identity_generation.rb +6 -29
  378. data/lib/hecks/ports/identity_resolution.rb +2 -10
  379. data/lib/hecks/ports/key_vault.rb +9 -45
  380. data/lib/hecks/ports/persistence/append_only.rb +61 -64
  381. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  382. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  383. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  384. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  385. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +44 -144
  386. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  387. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +20 -85
  388. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
  389. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +18 -24
  390. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  391. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  392. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  393. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  394. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  395. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  396. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  397. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  398. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +19 -76
  399. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  400. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  401. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  402. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  403. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  404. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +43 -127
  405. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  406. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +131 -555
  407. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  408. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  409. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  410. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  411. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  412. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  413. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  414. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +50 -47
  415. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
  416. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  417. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  418. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  419. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  420. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  421. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  422. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  423. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  424. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  425. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  426. data/lib/hecks/ports/persistence/state_codec.rb +47 -108
  427. data/lib/hecks/ports/persistence.rb +7 -14
  428. data/lib/hecks/ports/projection.rb +42 -84
  429. data/lib/hecks/ports/query/in_memory.rb +10 -58
  430. data/lib/hecks/ports/query/ordering.rb +11 -32
  431. data/lib/hecks/ports/query.rb +14 -37
  432. data/lib/hecks/ports.rb +2 -8
  433. data/lib/hecks/projection_files.rb +285 -0
  434. data/lib/hecks/projections/bootstrap_table.rb +12 -44
  435. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  436. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  437. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  438. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  439. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  440. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  441. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  442. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  443. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  444. data/lib/hecks/projections/deploy/fargate.rb +40 -207
  445. data/lib/hecks/projections/deploy/lambda.rb +191 -708
  446. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  447. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  448. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  449. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  450. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  451. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  452. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  453. data/lib/hecks/projections/deploy/preview/template.rb +3 -59
  454. data/lib/hecks/projections/deploy/preview.rb +2 -67
  455. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  456. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  457. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  458. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  459. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  460. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  461. data/lib/hecks/projections/deploy/shared.rb +48 -147
  462. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  463. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  464. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  465. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  466. data/lib/hecks/projections/diagrams.rb +22 -584
  467. data/lib/hecks/projections/glossary/html.rb +14 -130
  468. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  469. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  470. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  471. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  472. data/lib/hecks/projections/glossary.rb +30 -222
  473. data/lib/hecks/projections/ir.rb +3 -10
  474. data/lib/hecks/projections/model/deviations.rb +16 -43
  475. data/lib/hecks/projections/model.rb +12 -51
  476. data/lib/hecks/projections/oidc.rb +12 -56
  477. data/lib/hecks/projections/parser_table.rb +13 -32
  478. data/lib/hecks/projections/reference.rb +4 -18
  479. data/lib/hecks/projections/rust_vocabulary.rb +13 -251
  480. data/lib/hecks/projections/shape.rb +3 -29
  481. data/lib/hecks/projections/statements.rb +7 -86
  482. data/lib/hecks/projections/vocabulary.rb +6 -22
  483. data/lib/hecks/projections.rb +2 -16
  484. data/lib/hecks/projector/cli_projector.rb +154 -400
  485. data/lib/hecks/projector/docs_projector.rb +10 -201
  486. data/lib/hecks/projector/exporter.rb +42 -134
  487. data/lib/hecks/projector/ir_projector.rb +2 -9
  488. data/lib/hecks/projector/narrate_projector.rb +15 -189
  489. data/lib/hecks/projector/target.rb +13 -76
  490. data/lib/hecks/projector.rb +30 -121
  491. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  492. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  493. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  494. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  495. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  496. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  497. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  498. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  499. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  500. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  501. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  502. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  503. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  504. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  505. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  506. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  507. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  508. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  509. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  510. data/lib/hecks/quality_control/cli/child.rb +45 -0
  511. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  512. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  513. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  514. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  515. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  516. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  517. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  518. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  519. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  520. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  521. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  522. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  523. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  524. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  525. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  526. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  527. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  528. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  529. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  530. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  531. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  532. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  533. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  534. data/lib/hecks/query_ir.rb +346 -0
  535. data/lib/hecks/query_ir_mcp.rb +198 -0
  536. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  537. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  538. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  539. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  540. data/lib/hecks/query_specification/common/options.rb +2 -6
  541. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  542. data/lib/hecks/query_specification/field_path.rb +37 -77
  543. data/lib/hecks/query_specification/hop_path.rb +12 -76
  544. data/lib/hecks/release/gem_pin.rb +2 -26
  545. data/lib/hecks/release/runner/ci_publisher.rb +5 -8
  546. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  547. data/lib/hecks/release/runner/commands.rb +2 -4
  548. data/lib/hecks/release/runner/console.rb +6 -0
  549. data/lib/hecks/release/runner/gem_publisher.rb +15 -9
  550. data/lib/hecks/release/runner/npm_publisher.rb +9 -53
  551. data/lib/hecks/release/runner/preflight.rb +29 -20
  552. data/lib/hecks/release/runner/published.rb +8 -26
  553. data/lib/hecks/release/runner/tagger.rb +31 -23
  554. data/lib/hecks/release/runner.rb +60 -38
  555. data/lib/hecks/rendering.rb +4 -26
  556. data/lib/hecks/router/namespace_installer.rb +15 -39
  557. data/lib/hecks/router.rb +4 -8
  558. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  559. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  560. data/lib/hecks/runtime/boot_gates.rb +3 -18
  561. data/lib/hecks/runtime/caller.rb +19 -59
  562. data/lib/hecks/runtime/capability_graph.rb +5 -21
  563. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  564. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +27 -160
  565. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  566. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  567. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  568. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  569. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  570. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  571. data/lib/hecks/runtime/dependency_planning.rb +50 -104
  572. data/lib/hecks/runtime/dispatcher.rb +134 -243
  573. data/lib/hecks/runtime/entity_element.rb +40 -462
  574. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  575. data/lib/hecks/runtime/errors.rb +32 -115
  576. data/lib/hecks/runtime/event.rb +3 -25
  577. data/lib/hecks/runtime/identity.rb +18 -154
  578. data/lib/hecks/runtime/instance.rb +18 -150
  579. data/lib/hecks/runtime/interpreting.rb +14 -70
  580. data/lib/hecks/runtime/invocation.rb +28 -164
  581. data/lib/hecks/runtime/loader.rb +109 -194
  582. data/lib/hecks/runtime/outbox.rb +39 -262
  583. data/lib/hecks/runtime/policy_interpreter.rb +43 -202
  584. data/lib/hecks/runtime/port_operation_interpreter.rb +28 -97
  585. data/lib/hecks/runtime/query_interpreter.rb +122 -187
  586. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  587. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  588. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  589. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  590. data/lib/hecks/runtime/reference_hop.rb +8 -81
  591. data/lib/hecks/runtime/refusal_wording.rb +28 -87
  592. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  593. data/lib/hecks/runtime/registry/verification.rb +213 -227
  594. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  595. data/lib/hecks/runtime/registry.rb +156 -247
  596. data/lib/hecks/runtime/remote_dispatcher.rb +68 -154
  597. data/lib/hecks/runtime/routing.rb +4 -15
  598. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  599. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  600. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  601. data/lib/hecks/runtime/tenant_check.rb +6 -68
  602. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  603. data/lib/hecks/runtime/value/admission.rb +16 -67
  604. data/lib/hecks/runtime/value/coercion.rb +162 -760
  605. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  606. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  607. data/lib/hecks/runtime/value.rb +15 -84
  608. data/lib/hecks/runtime.rb +35 -69
  609. data/lib/hecks/rust_build/conformance.rb +158 -0
  610. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  611. data/lib/hecks/rust_build/coverage.rb +218 -0
  612. data/lib/hecks/rust_build/native_build.rb +124 -0
  613. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  614. data/lib/hecks/rust_build/project_rust.rb +332 -0
  615. data/lib/hecks/rust_build/wasm.rb +105 -0
  616. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  617. data/lib/hecks/rust_build.rb +144 -0
  618. data/lib/hecks/storehouse.rb +155 -582
  619. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +10 -34
  620. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  621. data/lib/hecks/three_zero/forms.yml +127 -0
  622. data/lib/hecks/three_zero.rb +12 -0
  623. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  624. data/lib/hecks/tools/comment_style.rb +1070 -0
  625. data/lib/hecks/tools/corpus_report.rb +87 -0
  626. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  627. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  628. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  629. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  630. data/lib/hecks/tools/era_reattest.rb +146 -0
  631. data/lib/hecks/tools/evolve_run.rb +189 -0
  632. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  633. data/lib/hecks/tools/heki_compaction.rb +100 -0
  634. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  635. data/lib/hecks/tools/journal_compaction.rb +89 -0
  636. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  637. data/lib/hecks/tools/query_ir_run.rb +81 -0
  638. data/lib/hecks/tools/regeneration_run.rb +166 -0
  639. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  640. data/lib/hecks/tools/sequence_script.rb +62 -0
  641. data/lib/hecks/tools/tail_merge.rb +117 -0
  642. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  643. data/lib/hecks/tools/translation_audit.rb +208 -0
  644. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  645. data/lib/hecks/tools.rb +86 -0
  646. data/lib/hecks/vendoring/git_environment.rb +2 -8
  647. data/lib/hecks/vendoring/git_source.rb +4 -10
  648. data/lib/hecks/vendoring.rb +7 -46
  649. data/lib/hecks/version.rb +3 -14
  650. data/lib/hecks.rb +52 -90
  651. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  652. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  653. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  654. data/qa/settings.yml +66 -0
  655. data/rust/.cargo/config.toml +6 -0
  656. data/rust/.gitignore +2 -0
  657. data/rust/Cargo.lock +54 -0
  658. data/rust/Cargo.toml +61 -0
  659. data/rust/build/.cargo/config.toml +3 -0
  660. data/rust/build/.gitignore +1 -0
  661. data/rust/build/Cargo.lock +7 -0
  662. data/rust/build/Cargo.toml +19 -0
  663. data/rust/build/src/build_artifact.rs +47 -0
  664. data/rust/build/src/cargo_sync.rs +248 -0
  665. data/rust/build/src/json.rs +421 -0
  666. data/rust/build/src/lineage_pass.rs +285 -0
  667. data/rust/build/src/main.rs +51 -0
  668. data/rust/build/src/optional_pass.rs +150 -0
  669. data/rust/build/src/pipeline.rs +264 -0
  670. data/rust/build/src/reserved_names.rs +76 -0
  671. data/rust/build/src/resolve.rs +124 -0
  672. data/rust/build/src/root.rs +25 -0
  673. data/rust/build/src/sidecars.rs +73 -0
  674. data/rust/build/src/subprocess.rs +51 -0
  675. data/rust/build/src/tmp.rs +31 -0
  676. data/rust/codegen/.cargo/config.toml +3 -0
  677. data/rust/codegen/.gitignore +1 -0
  678. data/rust/codegen/Cargo.lock +7 -0
  679. data/rust/codegen/Cargo.toml +20 -0
  680. data/rust/codegen/src/attr.rs +32 -0
  681. data/rust/codegen/src/bridging.rs +481 -0
  682. data/rust/codegen/src/commands.rs +1226 -0
  683. data/rust/codegen/src/constraints.rs +109 -0
  684. data/rust/codegen/src/dependency_planning.rs +209 -0
  685. data/rust/codegen/src/domain_generator.rs +1288 -0
  686. data/rust/codegen/src/exemplar.rs +230 -0
  687. data/rust/codegen/src/expr_emitter.rs +87 -0
  688. data/rust/codegen/src/fielded.rs +192 -0
  689. data/rust/codegen/src/hecks_naming.rs +33 -0
  690. data/rust/codegen/src/json.rs +335 -0
  691. data/rust/codegen/src/json_codec.rs +697 -0
  692. data/rust/codegen/src/literal.rs +207 -0
  693. data/rust/codegen/src/main.rs +290 -0
  694. data/rust/codegen/src/manifest.rs +227 -0
  695. data/rust/codegen/src/mutations.rs +784 -0
  696. data/rust/codegen/src/naming.rs +286 -0
  697. data/rust/codegen/src/ports.rs +208 -0
  698. data/rust/codegen/src/prelude.rs +116 -0
  699. data/rust/codegen/src/queries.rs +786 -0
  700. data/rust/codegen/src/reactions.rs +532 -0
  701. data/rust/codegen/src/read_models.rs +697 -0
  702. data/rust/codegen/src/reference_specs.rs +40 -0
  703. data/rust/codegen/src/registry.rs +966 -0
  704. data/rust/codegen/src/reserved_names.rs +76 -0
  705. data/rust/codegen/src/shared.rs +93 -0
  706. data/rust/codegen/src/skip_reason.rs +25 -0
  707. data/rust/codegen/src/types.rs +362 -0
  708. data/rust/host/.cargo/config.toml +3 -0
  709. data/rust/host/.gitignore +1 -0
  710. data/rust/host/Cargo.lock +4297 -0
  711. data/rust/host/Cargo.toml +61 -0
  712. data/rust/host/HECKS_RELEASE +1 -0
  713. data/rust/host/rds-ca-bundle.pem +2736 -0
  714. data/rust/host/src/api.rs +1596 -0
  715. data/rust/host/src/approval.rs +652 -0
  716. data/rust/host/src/auth.rs +1287 -0
  717. data/rust/host/src/bin/expr_harness.rs +46 -0
  718. data/rust/host/src/bin/lineage_harness.rs +119 -0
  719. data/rust/host/src/bin/mint_harness.rs +135 -0
  720. data/rust/host/src/checkout.rs +265 -0
  721. data/rust/host/src/dispatch.rs +1443 -0
  722. data/rust/host/src/expr_json.rs +1117 -0
  723. data/rust/host/src/field_hints.rs +52 -0
  724. data/rust/host/src/ir.rs +733 -0
  725. data/rust/host/src/journal.rs +874 -0
  726. data/rust/host/src/lambda_client.rs +378 -0
  727. data/rust/host/src/log.rs +147 -0
  728. data/rust/host/src/main.rs +474 -0
  729. data/rust/host/src/mint.rs +1865 -0
  730. data/rust/host/src/payments/keystore.rs +330 -0
  731. data/rust/host/src/payments/tests.rs +1400 -0
  732. data/rust/host/src/payments.rs +767 -0
  733. data/rust/host/src/presentation.rs +698 -0
  734. data/rust/host/src/presentation_write.rs +1431 -0
  735. data/rust/host/src/query_step.rs +149 -0
  736. data/rust/host/src/rate_limit.rs +904 -0
  737. data/rust/host/src/reference_transform.rs +241 -0
  738. data/rust/host/src/reference_validate.rs +338 -0
  739. data/rust/host/src/resend.rs +296 -0
  740. data/rust/host/src/secrets.rs +106 -0
  741. data/rust/host/src/server.rs +663 -0
  742. data/rust/host/src/storage_shape.rs +175 -0
  743. data/rust/host/src/test_pg.rs +40 -0
  744. data/rust/host/src/ui_schema.rs +1351 -0
  745. data/rust/host/src/wasm_runner.rs +191 -0
  746. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  747. data/rust/host/src/web/newsletter/tests.rs +104 -0
  748. data/rust/host/src/web/newsletter.rs +310 -0
  749. data/rust/host/src/web/newsletter_send.rs +357 -0
  750. data/rust/host/src/web/registrations.rs +1505 -0
  751. data/rust/host/src/web.rs +2937 -0
  752. data/rust/lsp/.gitignore +1 -0
  753. data/rust/lsp/Cargo.lock +7 -0
  754. data/rust/lsp/Cargo.toml +16 -0
  755. data/rust/lsp/README.md +133 -0
  756. data/rust/lsp/src/diagnostics.rs +165 -0
  757. data/rust/lsp/src/json.rs +385 -0
  758. data/rust/lsp/src/main.rs +399 -0
  759. data/rust/lsp/src/outline.rs +177 -0
  760. data/rust/lsp/src/rpc.rs +67 -0
  761. data/rust/parser/.cargo/config.toml +5 -0
  762. data/rust/parser/.gitignore +1 -0
  763. data/rust/parser/Cargo.lock +7 -0
  764. data/rust/parser/Cargo.toml +21 -0
  765. data/rust/parser/src/build/closed_sets.rs +27 -0
  766. data/rust/parser/src/build/identity.rs +276 -0
  767. data/rust/parser/src/build/mod.rs +12 -0
  768. data/rust/parser/src/build/naming.rs +141 -0
  769. data/rust/parser/src/build/pattern_subset.rs +161 -0
  770. data/rust/parser/src/build/query_derive.rs +72 -0
  771. data/rust/parser/src/build/query_inference.rs +269 -0
  772. data/rust/parser/src/build/query_options.rs +100 -0
  773. data/rust/parser/src/build/read_model.rs +94 -0
  774. data/rust/parser/src/build/references.rs +32 -0
  775. data/rust/parser/src/canonical.rs +114 -0
  776. data/rust/parser/src/diag.rs +55 -0
  777. data/rust/parser/src/emit.rs +953 -0
  778. data/rust/parser/src/expr/ast_json.rs +147 -0
  779. data/rust/parser/src/expr/evaluator.rs +81 -0
  780. data/rust/parser/src/expr/mod.rs +84 -0
  781. data/rust/parser/src/expr/resolver.rs +497 -0
  782. data/rust/parser/src/ir.rs +387 -0
  783. data/rust/parser/src/keywords.rs +585 -0
  784. data/rust/parser/src/lex.rs +662 -0
  785. data/rust/parser/src/main.rs +252 -0
  786. data/rust/parser/src/parse/aggregate.rs +612 -0
  787. data/rust/parser/src/parse/chapter.rs +517 -0
  788. data/rust/parser/src/parse/command.rs +637 -0
  789. data/rust/parser/src/parse/domain_port.rs +127 -0
  790. data/rust/parser/src/parse/entity.rs +527 -0
  791. data/rust/parser/src/parse/file.rs +67 -0
  792. data/rust/parser/src/parse/hecksagon.rs +231 -0
  793. data/rust/parser/src/parse/lifecycle.rs +162 -0
  794. data/rust/parser/src/parse/mod.rs +1262 -0
  795. data/rust/parser/src/parse/policy.rs +87 -0
  796. data/rust/parser/src/parse/process_manager.rs +305 -0
  797. data/rust/parser/src/parse/query.rs +100 -0
  798. data/rust/parser/src/parse/read_model.rs +214 -0
  799. data/rust/parser/src/parse/value_object.rs +250 -0
  800. data/rust/parser/src/ruby_value.rs +422 -0
  801. data/rust/project/bridging.rb +257 -0
  802. data/rust/project/commands.rb +877 -0
  803. data/rust/project/constraints.rb +86 -0
  804. data/rust/project/dependency_planning.rb +178 -0
  805. data/rust/project/domain_generator.rb +980 -0
  806. data/rust/project/exemplar.rb +148 -0
  807. data/rust/project/expr_emitter.rb +72 -0
  808. data/rust/project/fielded.rb +141 -0
  809. data/rust/project/json_codec.rb +641 -0
  810. data/rust/project/mutations.rb +721 -0
  811. data/rust/project/naming.rb +205 -0
  812. data/rust/project/ports.rb +65 -0
  813. data/rust/project/queries.rb +493 -0
  814. data/rust/project/reactions.rb +317 -0
  815. data/rust/project/read_models.rb +690 -0
  816. data/rust/project/reference_specs.rb +27 -0
  817. data/rust/project/registry.rb +396 -0
  818. data/rust/project/skip_reason.rb +29 -0
  819. data/rust/project/types.rb +225 -0
  820. data/rust/project/write_if_changed.rb +83 -0
  821. data/rust/project.rb +30 -0
  822. data/rust/project_rust_pipeline.rb +383 -0
  823. data/rust/src/exemplar/commands.rs +279 -0
  824. data/rust/src/exemplar/constraints.rs +18 -0
  825. data/rust/src/exemplar/fielded.rs +280 -0
  826. data/rust/src/exemplar/json.rs +277 -0
  827. data/rust/src/exemplar/mod.rs +17 -0
  828. data/rust/src/exemplar/mutations.rs +80 -0
  829. data/rust/src/exemplar/queries.rs +33 -0
  830. data/rust/src/exemplar/reactions.rs +88 -0
  831. data/rust/src/exemplar/read_models.rs +48 -0
  832. data/rust/src/exemplar/registry.rs +166 -0
  833. data/rust/src/exemplar/types.rs +44 -0
  834. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  835. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  836. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  837. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  838. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  839. data/rust/src/kernel/cli.rs +585 -0
  840. data/rust/src/kernel/dispatch.rs +1090 -0
  841. data/rust/src/kernel/expr.rs +458 -0
  842. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  843. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  844. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  845. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  846. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  847. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  848. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  849. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  850. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  851. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  852. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  853. data/rust/src/kernel/expression_operators/text.rs +79 -0
  854. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  855. data/rust/src/kernel/json.rs +779 -0
  856. data/rust/src/kernel/mod.rs +133 -0
  857. data/rust/src/kernel/named_query.rs +219 -0
  858. data/rust/src/kernel/orchestrate.rs +1810 -0
  859. data/rust/src/kernel/pattern.rs +307 -0
  860. data/rust/src/kernel/query_comparators.rs +206 -0
  861. data/rust/src/kernel/query_ordering.rs +103 -0
  862. data/rust/src/kernel/read_model.rs +914 -0
  863. data/rust/src/kernel/reference_lookup.rs +149 -0
  864. data/rust/src/kernel/repository.rs +468 -0
  865. data/rust/src/kernel/routing.rs +612 -0
  866. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  867. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  868. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  869. data/rust/src/kernel/vocab/mod.rs +20 -0
  870. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  871. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  872. data/rust/src/lib.rs +8 -0
  873. data/rust/src/main.rs +16 -0
  874. data/rust/web/.gitignore +1 -0
  875. data/rust/web/Cargo.lock +119 -0
  876. data/rust/web/Cargo.toml +18 -0
  877. data/rust/web/src/lib.rs +8 -0
  878. metadata +478 -12
  879. data/lib/hecks/facade/cli_door.rb +0 -178
  880. data/lib/hecks/facade/cli_runner.rb +0 -271
  881. data/lib/hecks/facade/handle.rb +0 -293
  882. data/lib/hecks/facade/json_door.rb +0 -247
  883. data/lib/hecks/facade/surface/aggregate_door.rb +0 -222
  884. data/lib/hecks/facade/surface/chapter.rb +0 -116
  885. data/lib/hecks/facade/surface.rb +0 -61
  886. data/lib/hecks/mcp_door_scope.rb +0 -176
@@ -8,46 +8,25 @@ require_relative "invariant_violation"
8
8
  module Hecks
9
9
  module Runtime
10
10
  class Value
11
- # The class-side engine: how a raw argument or stored field becomes a
12
- # typed Value. Extended into Value, so every method here reads as
13
- # `Value.for`, `Value.build`, … — `self` is the Value class.
11
+ # Class-side coercion engine for Value, extended in so its methods read
12
+ # as `Value.for`, `Value.build`, and so on.
14
13
  module Coercion
15
- # The four `SHAPES` an attribute's value can take — named here because
16
- # `for_attribute` immediately below is the one place that actually
17
- # branches on all four, and nowhere else in the language collects
18
- # them into a single closed list. `Attribute#list?`/`#optional?`
19
- # are real predicates on the IR node itself (bluebook/attribute.rb);
20
- # `:scalar` and `:composite` are not named predicates there — they
21
- # fall out of whether `aggregate.value_object(attribute.type)`
22
- # resolves to something, read directly in the `coerced =` line
23
- # below — but the branch is exactly as real, so it gets a name here
24
- # too rather than staying anonymous.
25
- #
26
- # A second runtime's kernel ports this method by hand (rust/src/
27
- # kernel/attribute_shapes/*.rs — one file per name in this array,
28
- # generated into a Rust enum by bin/project_kernel_capabilities so
29
- # every match over it is compiler-checked exhaustive). If a fifth
30
- # branch is ever added to `for_attribute`, add its name here in the
31
- # same breath — this list is that port's only source of truth for
32
- # "which shapes exist," and a shape missing from it is a shape the
33
- # generated Rust enum, and therefore the kernel, can never learn
34
- # about no matter how correct the Ruby below is.
14
+ # The complete set of attribute value shapes. Mirrored by hand into a
15
+ # generated Rust enum (hecks project_kernel_capabilities) — adding a shape
16
+ # here without a matching Rust file leaves the kernel unaware of it.
35
17
  SHAPES = %i[scalar list optional composite].freeze
36
18
 
37
19
  # Coerces `value` for one of `aggregate`'s declared attributes, by name.
38
20
  #
39
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
40
- # `name` is looked up on
21
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] construct to look
22
+ # up `name` on
41
23
  # @param name [String, Symbol] the declared attribute name
42
24
  # @param value [Object] the raw value to coerce
43
- # @return [Runtime::Value, Object, nil] `value` unchanged when `aggregate`
44
- # declares no such attribute; otherwise `for_attribute`'s own result
45
- # @raise [Runtime::TypeMismatch] if `value` cannot be coerced to the
46
- # attribute's declared type
47
- # @raise [Runtime::UnknownArgument] if `value` is a Hash naming a field the
48
- # attribute's own value-object type does not declare
49
- # @raise [Runtime::InvariantViolation] if a coerced value object breaks one
50
- # of its own invariants
25
+ # @return [Runtime::Value, Object, nil] the coerced value, or `value`
26
+ # unchanged if `aggregate` declares no such attribute
27
+ # @raise [Runtime::TypeMismatch] if `value` cannot be coerced to the declared type
28
+ # @raise [Runtime::UnknownArgument] if `value` is a Hash naming an undeclared field
29
+ # @raise [Runtime::InvariantViolation] if a coerced value object breaks an invariant
51
30
  def for(aggregate, name, value)
52
31
  attribute = aggregate.attribute(name)
53
32
  return value unless attribute
@@ -55,60 +34,18 @@ module Hecks
55
34
  for_attribute(aggregate, attribute, value)
56
35
  end
57
36
 
58
- # The four `SHAPES` above, in the order this method actually checks
59
- # them: `optional`/nil-passthrough first (a value that isn't there
60
- # has no shape left to branch on), `list` second (a list of
61
- # elements, hydrated as entities), then — inside `coerced =` —
62
- # `composite` (the type names a declared value object, rebuilt
63
- # recursively via `build`) with `scalar` as what's left once
64
- # neither of those applies (the raw value, passed through
65
- # unchanged).
66
- # `boundary: false` is the query door (`QueryInterpreter#normalize_args`):
67
- # a query attribute's declared type names the argument for callers and
68
- # generators, never a runtime shape — comparison unwraps both sides
69
- # itself, so a `reference: {value: ...}` offered against a `String`
70
- # query field is the documented allowance (see banking's own
71
- # `Account.OpenForCustomer`), not a C3.8 mismatch.
72
- # `argument: true` is the command/entity/port argument door only
73
- # (`Interpreting#normalize_args`, every command/entity/port
74
- # dispatch): the one place a nil for a non-optional attribute is
75
- # the caller leaving a required argument empty (C3.7), absorbed
76
- # via the type's own field defaults when every field has one
77
- # (`nil_argument` below). Every other caller — `sets` copying an
78
- # optional argument into state, hydration, entity elements,
79
- # identity, defaults — is state assembly, where nil is a
80
- # legitimate "absent is not empty" value the aggregate's own
81
- # attribute may hold. `QueryInterpreter#normalize_args` never
82
- # passes `argument: true` — a null required value-object-typed
83
- # query argument is checked, and refused, entirely on its own
84
- # side (`null_vo_argument!`, query_interpreter.rb) precisely so
85
- # it does not reach this default-absorbing fallback (QualityControl
86
- # BUG#36 — a query's own null VO argument must refuse regardless
87
- # of any default, unlike a command's).
37
+ # Coerces `value` for a single, already-resolved `attribute`, branching on
38
+ # its declared shape (list, reference, composite, or bare scalar).
88
39
  #
89
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
90
- # `attribute` is declared on
91
- # @param attribute [Bluebook::Attribute, nil] the attribute to coerce `value`
92
- # for; nil is treated as an unknown attribute (no shape to coerce against)
93
- # @param value [Object] the raw value to coerce
94
- # @param boundary [Boolean] whether a bare-primitive attribute's own scalar
95
- # shape is checked; false at the query door, where a declared type names
96
- # the argument for callers/generators only
97
- # @param argument [Boolean] whether this is the command/entity/port argument
98
- # door, where a nil for a non-optional attribute is a left-empty argument
99
- # (C3.7) rather than ordinary state-assembly nil
100
- # @return [Runtime::Value, Object, nil] the coerced value: a built `Value`
101
- # for a composite-typed attribute, a frozen Array for a `has_many`
102
- # reference or hydrated entity list, a joined identity String for a
103
- # scalar reference, or `value` passed through unchanged for a bare
104
- # primitive or an unknown attribute; nil for an absent optional attribute
105
- # @raise [Runtime::TypeMismatch] if `value` cannot be coerced to the
106
- # attribute's declared type, or a required reference/attribute is offered
107
- # as a wrong-shaped value
108
- # @raise [Runtime::UnknownArgument] if `value` is a Hash naming a field the
109
- # attribute's own value-object type does not declare
110
- # @raise [Runtime::InvariantViolation] if a coerced value object breaks one
111
- # of its own invariants
40
+ # `boundary: false` is the query door, where a declared type documents the
41
+ # argument for callers/generators rather than naming a shape to enforce.
42
+ # `argument: true` is the command/entity/port dispatch door, where a nil for
43
+ # a required attribute is a left-empty argument (C3.7), not ordinary state nil.
44
+ #
45
+ # @raise [Runtime::TypeMismatch] if `value` cannot be coerced, or a required
46
+ # reference/attribute is a wrong-shaped value
47
+ # @raise [Runtime::UnknownArgument] if `value` is a Hash naming an undeclared field
48
+ # @raise [Runtime::InvariantViolation] if a coerced value object breaks an invariant
112
49
  def for_attribute(aggregate, attribute, value, boundary: true, argument: false)
113
50
  return nil_or_missing(aggregate, attribute, value, argument) if attribute.nil? || value.nil?
114
51
  return reference_list(attribute, value) if attribute.list? && attribute.reference?
@@ -116,14 +53,9 @@ module Hecks
116
53
  return hydrate_entity_list(aggregate, attribute, value) if attribute.list? # :list
117
54
  return value unless aggregate.respond_to?(:value_object)
118
55
 
119
- # The set the attribute names is checked where the attribute is known.
120
- # `build` below sees only the value object, never which attribute asked
121
- # for it, so a command argument's `admits:` has to be read here — this
122
- # is the door every argument and every head field comes through.
123
- #
124
- # After coercion, not before: a scalar arrives wrapped in whatever holder
125
- # its type names (`{value: "append"}` for an OpName), and checking the
126
- # raw payload would be checking the envelope.
56
+ # `admits:` is checked here, where the attribute is known — `build` only
57
+ # sees the value object. Checked after coercion: a scalar arrives wrapped
58
+ # in its type's own holder, and checking the raw payload would be wrong.
127
59
  value_object = value_object_for(aggregate, attribute.type)
128
60
  return bare_primitive(aggregate, attribute, value, boundary) if value_object.nil?
129
61
 
@@ -137,22 +69,9 @@ module Hecks
137
69
  coerced
138
70
  end
139
71
 
140
- # NIL is not a value for a non-optional argument (C3.7/C3.8). An
141
- # `optional:` attribute and a load from the store pass nil through
142
- # as they always did; a command argument offered as null for a
143
- # required attribute is
144
- # refused as the field the caller left empty — a value object is
145
- # built from no fields, so its first required field refuses with
146
- # exactly the wording the Rust side's `from_json` gives it
147
- # ("Money.cents expects Integer, got nil"), and a bare scalar
148
- # refuses through `check_bare_primitive`'s own wording. Lists and
149
- # references keep their nil passthrough (their absence is an empty
150
- # relationship, `validate_relationship_cardinality`'s business).
151
- # The `attribute.nil?`/`value.nil?` branch of `for_attribute`,
152
- # pulled out on its own — an unknown attribute has no shape left
153
- # to branch on, and a nil value is either an ordinary absence
154
- # (state assembly, hydration, a query ask) or, at the argument
155
- # door only, `nil_argument`'s own C3.7 refusal.
72
+ # A required argument's nil is refused (C3.7); state assembly, hydration
73
+ # and query nils pass through unchanged. Lists/references keep their own
74
+ # nil passthrough regardless of `argument`.
156
75
  private def nil_or_missing(aggregate, attribute, value, argument)
157
76
  return value if attribute.nil? || !value.nil?
158
77
 
@@ -172,31 +91,21 @@ module Hecks
172
91
  expected: attribute.type, offered: "nil")
173
92
  end
174
93
 
175
- # A bare primitive is type-checked at the boundary too (C3.8,
176
- # docs/semantics/bluebook-semantics.md) — the same two predicates a
177
- # value object's own fields get, so wrong-typed caller input is a
178
- # TypeMismatch refusal here, never an evaluation fault later. Its
179
- # `admits:` set is still checked, exactly as a value object's is.
94
+ # A bare primitive is boundary-checked too (C3.8) — wrong-typed input
95
+ # refuses here as a TypeMismatch, never later as a broken predicate.
96
+ # Its `admits:` set is still checked, exactly as a value object's is.
180
97
  private def bare_primitive(aggregate, attribute, value, boundary)
181
98
  check_bare_primitive(aggregate, attribute, value) if boundary
182
99
  admit_declared_set(aggregate, attribute, value)
183
100
  value
184
101
  end
185
102
 
186
- # Resolves the value-object class `type` names, searching `aggregate`'s own
187
- # declarations first, then its chapter's other aggregates.
188
- #
189
- # Aggregate-local value objects remain authoritative, which permits
190
- # intentional duplication. An ordinary fact may also name an identity
191
- # value object declared on another aggregate; that shape is borrowed
192
- # only when every chapter declaration with the name agrees.
103
+ # Resolves the value-object class `type` names: `aggregate`'s own
104
+ # declarations first, then its chapter's other aggregates if they agree.
193
105
  #
194
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct to
195
- # search first
106
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] construct to search first
196
107
  # @param type [String, Symbol, #to_s] the declared type name to resolve
197
- # @return [Class, nil] the `Bluebook::ValueObject` subclass `type` names; nil
198
- # if `aggregate` declares none, and its chapter's other aggregates disagree
199
- # on the shape of every same-named one (or declare none either)
108
+ # @return [Class, nil] the `Bluebook::ValueObject` subclass `type` names, or nil
200
109
  def value_object_for(aggregate, type)
201
110
  local = aggregate.value_object(type)
202
111
  return local if local
@@ -214,18 +123,11 @@ module Hecks
214
123
  # Coerces a reference-typed attribute's offered value into the target's own
215
124
  # canonical identity string.
216
125
  #
217
- # Retained relationships store canonical target identities, not Ruby
218
- # Value wrappers. Raw scalar IDs remain a compatibility input. A named
219
- # identity VO omits its minted aggregate field at the command boundary;
220
- # a bespoke compound VO may instead name the target heads directly.
221
- # Neither form requires reverse-splitting a canonical ID.
222
- #
223
126
  # @param attribute [Bluebook::Attribute] the reference-typed attribute
224
- # @param value [Object] the offered value: a bare scalar identity, a
225
- # `Runtime::Value`, or a Hash naming the target's own identity fields
226
- # @return [String, Object] the joined canonical identity String once every
227
- # identity part resolves; `value` unchanged otherwise (a bare scalar, an
228
- # unresolvable target, or a shape `sole_scalar_identity` cannot unwrap)
127
+ # @param value [Object] a bare scalar identity, a `Runtime::Value`, or a Hash
128
+ # naming the target's own identity fields
129
+ # @return [String, Object] the joined identity String, or `value` unchanged
130
+ # when it cannot be resolved
229
131
  def reference_identity(attribute, value)
230
132
  return value unless value.is_a?(self) || value.is_a?(Hash)
231
133
 
@@ -245,42 +147,13 @@ module Hecks
245
147
  Naming.identity(parts)
246
148
  end
247
149
 
248
- # A command may redeclare a `reference_to` field under its own,
249
- # differently-named single-attribute value object (`attribute
250
- # :venue, VenueHandle; sets :venue`, not `reference_to Venue` —
251
- # QualityControl BUG#121, `qa/stress_domains/generated_revalued_
252
- # shape`'s own `Hangar.Repoint`) instead of naming the target's
253
- # own identity field(s) directly. `identity_part`'s path walk
254
- # above only ever matches an incoming shape that already uses the
255
- # target's own field names (or is the target's own identity value
256
- # object, `direct_identity_head`) — an ad hoc wrapper around a
257
- # bare scalar under some other field name (`VenueHandle`'s own
258
- # `value`, not `Venue`'s own `code`) fails every path lookup, and
259
- # without this would fall through to the unresolved `return value`
260
- # above, storing the wrapped Value. That leaves one reference field on
261
- # one aggregate holding two different shapes depending on which
262
- # command last wrote it — `Open`'s own bare `reference_to Venue`
263
- # argument was never wrapped in the first place (the top guard
264
- # clause passes a bare scalar straight through), so it always
265
- # stored the canonical bare identity this class's own header
266
- # comment promises ("canonical target identities, not Ruby Value
267
- # wrappers") — while `Repoint` silently kept the wrapper instead.
268
- #
269
- # Unambiguous only when both sides admit exactly one scalar: the
270
- # target names exactly one identity path (`paths.one?` — a
271
- # compound identity has no single field either side could stand
272
- # in for) and the offered value is itself a single-attribute
273
- # value object (`sole_attribute` — a multi-field VO has no one
274
- # scalar to unwrap either). `materialize_unwrapped` recurses
275
- # through any further single-field wrapping the same way it
276
- # already does for `Value.materialize_unwrapped`'s other callers,
277
- # landing on the bare scalar `Open`'s own path already produces
278
- # for the identical target field.
150
+ # Unwraps `value` to a bare scalar when `paths` names exactly one identity
151
+ # field and `value` is itself a single-attribute value object — the only
152
+ # case where unwrapping cannot be ambiguous.
279
153
  #
280
154
  # @param value [Object] the offered reference value to unwrap
281
155
  # @param paths [Array<String>] the target's own declared identity paths
282
- # @return [Object, nil] the unwrapped scalar when `value` is a single-field
283
- # value object and `paths` names exactly one path; nil otherwise
156
+ # @return [Object, nil] the unwrapped scalar, or nil if it does not apply
284
157
  def sole_scalar_identity(value, paths)
285
158
  return nil unless value.is_a?(self) && paths.one?
286
159
  return nil unless value.value_object.sole_attribute
@@ -288,14 +161,11 @@ module Hecks
288
161
  materialize_unwrapped(value)
289
162
  end
290
163
 
291
- # Whether `value` is itself the target's own (single) identity
292
- # value object — pure, self-contained: reads only `value` and
293
- # `target`, decides nothing about any particular path.
164
+ # Whether `value` is itself the target's own single identity value object.
294
165
  #
295
166
  # @param value [Object] the offered reference value to check
296
167
  # @param target [Bluebook::Aggregate] the reference's own resolved target
297
- # @return [String, nil] the target's own single identity head, as a String,
298
- # when `value` is that head's own declared value-object type; nil otherwise
168
+ # @return [String, nil] the target's identity head name, or nil
299
169
  def direct_identity_head(value, target)
300
170
  return nil unless value.is_a?(self) && target.identity_heads.one?
301
171
 
@@ -303,29 +173,21 @@ module Hecks
303
173
  target.attribute(head)&.type.to_s == value.type_name ? head.to_s : nil
304
174
  end
305
175
 
306
- # One identity path's own value out of the materialized hash —
307
- # stripping a leading segment already covered by
308
- # `direct_identity_head`, then walking the rest. Pure given its
309
- # three inputs; extracted from `reference_identity` alongside
310
- # `direct_identity_head` above purely to keep that method to its
311
- # own guard-clause shape.
176
+ # One identity path's own value out of the materialized hash, stripping a
177
+ # leading segment already covered by `direct_identity_head`.
312
178
  #
313
- # @param materialized [Hash, Object] the offered reference value, already
314
- # materialized to a plain Hash (or scalar, for a path that finds nothing)
179
+ # @param materialized [Hash, Object] the offered reference value as a Hash
315
180
  # @param path [String, Symbol] one dotted identity path to dig
316
- # @param direct_head [String, nil] the leading segment to strip, when `path`
317
- # restates the head `direct_identity_head` already matched
318
- # @return [Object, nil] the value found by walking `path`'s segments; nil if
319
- # any segment is missing or a non-Hash is dug into before the path ends
181
+ # @param direct_head [String, nil] leading segment to strip, if present
182
+ # @return [Object, nil] the value found by walking `path`, or nil if missing
320
183
  def identity_part(materialized, path, direct_head)
321
184
  segments = path.to_s.split(".")
322
185
  segments.shift if direct_head && segments.first == direct_head
323
186
  segments.reduce(materialized) do |held, segment|
324
187
  next nil unless held.is_a?(Hash)
325
188
 
326
- # `key?` decides which spelling answers, never `||` — a
327
- # genuinely-held `false` must not fall through to the
328
- # other spelling (usually absent) and read as `nil`.
189
+ # `key?` decides which spelling answers — a genuinely-held `false`
190
+ # must not fall through to the other spelling and read as `nil`.
329
191
  sym = segment.to_sym
330
192
  held.key?(sym) ? held[sym] : held[segment]
331
193
  end
@@ -350,40 +212,21 @@ module Hecks
350
212
  # defaults, nested normalization and validation run.
351
213
  #
352
214
  # @param value_object [Class] the target `Bluebook::ValueObject` subclass
353
- # @param name [String, Symbol] the attribute or argument name, quoted in a
354
- # refusal
355
- # @param value [Hash, Runtime::Value, Object] the offered value: a Hash of
356
- # fields, an already-built `Value` of a differently-named but same-shaped
357
- # type, or a bare scalar for a single-field value object
215
+ # @param name [String, Symbol] the attribute or argument name, quoted in a refusal
216
+ # @param value [Hash, Runtime::Value, Object] a Hash of fields, an already-built
217
+ # `Value`, or a bare scalar for a single-field value object
358
218
  # @return [Hash{Symbol => Object}] the offered fields, keyed by attribute name
359
219
  # @raise [Runtime::TypeMismatch] if `value` is a bare scalar and `value_object`
360
220
  # declares more than one field
361
221
  def fields_for(value_object, name, value)
362
222
  return value.transform_keys(&:to_sym) if value.is_a?(Hash)
363
- # Mutations may legitimately carry a value object into a differently
364
- # named value-object slot with the same declared fields (for example,
365
- # PositiveMoney into an Account's Money balance). Rebuild the target
366
- # type from its state; callers at the public boundary still have to
367
- # supply an object rather than a scalar.
223
+ # A same-shaped value object may fill a differently-named slot (e.g.
224
+ # PositiveMoney into an Account's Money balance) — rebuild from its state.
368
225
  return value.to_h if value.is_a?(self)
369
226
 
370
- # Vendored addition, not (yet) upstream hecks (migration
371
- # plan task 5): a bare scalar auto-wraps into a single-field
372
- # value object's sole attribute -- the same shape
373
- # #from_identifier already establishes for identity coercion
374
- # (`build(value_object, { fields.first.name => identifier }) if
375
- # fields.size == 1`), made consistent here for mutation
376
- # coercion too. Real, corpus-wide gap: a synthesised single-
377
- # field wrapper (Part 3a's bare-primitive auto-synthesis, the
378
- # norm for a VO-typed aggregate field) is exactly the shape
379
- # #rewrap_arithmetic_result hands back a raw scalar result to
380
- # -- without this, every phantom-field increment/multiply on a
381
- # single-field-wrapped attribute refused with "pass its fields
382
- # as an object, not <scalar>" the instant it tried to re-wrap
383
- # its own correctly-computed result. Multi-field VOs still
384
- # refuse below, unchanged -- only the genuinely unambiguous
385
- # single-field case auto-wraps, matching from_identifier's own
386
- # precedent exactly.
227
+ # A bare scalar auto-wraps into a single-field value object's sole
228
+ # attribute, matching `from_identifier`'s own precedent. Multi-field
229
+ # value objects still refuse below.
387
230
  return { value_object.attributes.first.name => value } if value_object.attributes.size == 1
388
231
 
389
232
  raise TypeMismatch,
@@ -392,76 +235,27 @@ module Hecks
392
235
  offered: Rendering.describe(value))
393
236
  end
394
237
 
395
- # `build`'s own recursive twin of `for_attribute`'s single-level
396
- # normalization — a value object's own composite-typed fields
397
- # (`Pizza.price_cents`, a `Price`) never otherwise pass back
398
- # through `fields_for`, so a bare scalar or partial Hash for one
399
- # of those sails past the outer VO's own shape check (`Pizza`
400
- # itself has two fields, so nothing unwraps there) and lands
401
- # stored one field down exactly as handed in — found live: once
402
- # the fuzzer actually generated the bare-scalar shape
403
- # `fields_for` has accepted at the top level since 86727afd, a
404
- # nested `Price` stored as a raw Integer broke every later
405
- # dotted-path read (`pizza.price_cents.cents`) expecting one
406
- # more level of Hash.
238
+ # `build`'s own recursive twin of `for_attribute`, normalizing a value
239
+ # object's own composite-typed fields into their declared shape.
407
240
  #
408
- # Stays a plain Hash, never a nested `Value` — `Value#with`'s own
409
- # header and `materialize_unwrapped`'s comment already depend on
410
- # a value-object-typed field of another value object staying a
411
- # plain Hash once stored, and this does not change that; it only
412
- # makes sure that Hash has the shape its own type declares.
413
- # `aggregate` is the one thing this needs beyond `fields_for`'s own
414
- # scope — a nested type can only be resolved through `aggregate.
415
- # value_object(name)`, so callers with no aggregate in reach
416
- # (`Value#with`, always re-setting an already-scalar arithmetic
417
- # field) simply skip this and keep their own behavior unchanged.
418
- # Recurses into each nested field's own validation too, not only its
419
- # shape — found live alongside the shape bug this method's header
420
- # already describes: a nested `Price`/`Size` (a value-object-typed
421
- # field of another value object, e.g. `Pizza.price_cents`,
422
- # `Pizza.size`) had its Hash shape normalized here but never ran
423
- # `validate!` — `build`, below, only ever validated the outer value
424
- # object's own direct fields, so a negative `price_cents.cents` or an
425
- # out-of-`one_of` `size.value` sailed through a `Pizza`-typed command
426
- # argument untouched, while the exact same nested type declared as a
427
- # direct, top-level command attribute (`SafeDepositBox.Rent`'s own
428
- # `attribute :size, Size`) was already checked correctly. `apply_
429
- # defaults` runs first, same as the outer value object gets in
430
- # `build`, so a nested field's own default is filled in before its
431
- # own invariants read it.
241
+ # Stays a plain Hash, never a nested `Value` — `Value#with` depends on that.
432
242
  #
433
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity, nil] the construct
434
- # nested types are resolved against; a no-op if it does not respond to
435
- # `value_object`
436
- # @param value_object [Class] the `Bluebook::ValueObject` subclass `fields`
437
- # belongs to
438
- # @param fields [Hash{Symbol => Object}] the outer value object's own fields,
439
- # already defaulted; written in place
440
- # @return [Hash{Symbol => Object}] `fields`, with every composite-typed field
441
- # normalized (and, for a non-list one, validated) into its own declared shape
442
- # @raise [Runtime::TypeMismatch] if a nested field cannot be coerced to its
443
- # declared type
444
- # @raise [Runtime::UnknownArgument] if a nested field's own Hash names a
445
- # field its declared type does not declare
446
- # @raise [Runtime::InvariantViolation] if a nested field breaks one of its
447
- # own invariants
243
+ # @param aggregate [Bluebook::Aggregate, Entity, nil] nested-type lookup scope
244
+ # @param value_object [Class] the value object `fields` belongs to
245
+ # @param fields [Hash{Symbol => Object}] the outer fields, already defaulted
246
+ # @return [Hash{Symbol => Object}] `fields` with composite fields normalized
247
+ # @raise [Runtime::TypeMismatch] if a nested field cannot be coerced
248
+ # @raise [Runtime::UnknownArgument] if a nested field names an undeclared key
249
+ # @raise [Runtime::InvariantViolation] if a nested field breaks its own invariant
448
250
  def normalize_composite_fields(aggregate, value_object, fields)
449
251
  return fields unless aggregate.respond_to?(:value_object)
450
252
 
451
253
  value_object.attributes.each do |attribute|
452
254
  next unless fields.key?(attribute.name)
453
255
 
454
- # A list member read back from the store hydrates like a top-level
455
- # list — `list_of(Entity)` elements get their fields coerced,
456
- # `list_of(ValueObject)` elements become Values — so a value object
457
- # holding a list loads into the same shape the live dispatch that
458
- # wrote it held. Found by PR A3 (every adapter through the state
459
- # codec): chess-style `sets :positions, append: { pieces:
460
- # state(:pieces) }` snapshots read back from Heki/Sqlite/Postgres
461
- # (and now Memory's codec copy) with raw element hashes, so a
462
- # `given` comparing a snapshot piece's `id` Value to a live one
463
- # never matched after a restart. Load door only: an input list
464
- # member is left exactly as before, refusals unchanged.
256
+ # A list member hydrates the same as a top-level list, so a value
257
+ # read back from the store matches the shape a live dispatch wrote.
258
+ # Load door only — an input list member is left as offered.
465
259
  if attribute.list?
466
260
  fields[attribute.name] = for_attribute(aggregate, attribute, fields[attribute.name]) if trusting_stored_state?
467
261
  next
@@ -482,13 +276,12 @@ module Hecks
482
276
  fields
483
277
  end
484
278
 
485
- # Fills every declared attribute `fields` does not already hold with its
486
- # own declared `default:`, when it has one.
279
+ # Fills every declared attribute `fields` lacks with its own `default:`.
487
280
  #
488
281
  # @param value_object [Class] the `Bluebook::ValueObject` subclass whose
489
282
  # declared defaults are read
490
283
  # @param fields [Hash{Symbol => Object}] the offered fields; written in place
491
- # @return [Hash{Symbol => Object}] `fields`, with each declared default filled in
284
+ # @return [Hash{Symbol => Object}] `fields`, with defaults filled in
492
285
  def apply_defaults(value_object, fields)
493
286
  value_object.attributes.each_with_object(fields) do |attribute, completed|
494
287
  completed[attribute.name] = attribute.default unless completed.key?(attribute.name) || attribute.default.nil?
@@ -496,36 +289,27 @@ module Hecks
496
289
  end
497
290
 
498
291
  # The full door a value object's own fields pass through — shared by
499
- # `build` (the outer value object) and `normalize_composite_fields`
500
- # (every nested one), so a nested `Price`/`Size` is refused exactly
501
- # the same way, with exactly the same wording, as the identical type
502
- # declared directly on a command.
292
+ # `build` and `normalize_composite_fields`, so a nested field refuses
293
+ # exactly like the same type declared directly on a command.
503
294
  #
504
295
  # @param value_object [Class] the `Bluebook::ValueObject` subclass to
505
296
  # validate `fields` against
506
- # @param fields [Hash{Symbol => Object}] the already-defaulted, already
507
- # nested-normalized fields to check
297
+ # @param fields [Hash{Symbol => Object}] already-defaulted, nested-normalized fields
508
298
  # @return [void]
509
- # @raise [Runtime::UnknownArgument] if `fields` names a key `value_object`
510
- # does not declare
511
- # @raise [Runtime::TypeMismatch] if a required field is missing, a numeric
512
- # or pattern-constrained field is the wrong shape, or a scalar field
513
- # arrives as a composite
514
- # @raise [Runtime::InvariantViolation] if `fields` breaks one of
515
- # `value_object`'s own declared invariants
299
+ # @raise [Runtime::UnknownArgument] if `fields` names an undeclared key
300
+ # @raise [Runtime::TypeMismatch] if a required field is missing or wrong-shaped
301
+ # @raise [Runtime::InvariantViolation] if `fields` breaks a declared invariant
516
302
  def validate!(value_object, fields)
517
- # C6.3 (docs/semantics/bluebook-semantics.md) — a value object is
518
- # validated on construction from input only; state read back from
519
- # the store is trusted as it was written, so tightening an
520
- # invariant never makes an old record unreadable (migration is
521
- # the era system's job). `hydrate` — the one load door — sets
522
- # the flag; every input door leaves it unset.
303
+ # C6.3 — a value object validates on construction from input only; state
304
+ # read back from the store is trusted as written, so tightening an
305
+ # invariant never makes an old record unreadable. `hydrate` sets this flag.
523
306
  return if trusting_stored_state?
524
307
 
525
308
  check_unknown_fields(value_object, fields)
526
309
  check_required_fields(value_object, fields)
527
310
  admit_member(value_object, fields)
528
311
  check_admitted(value_object, fields)
312
+ check_list_shapes(value_object, fields)
529
313
  check_numeric_fields(value_object, fields)
530
314
  check_scalar_shapes(value_object, fields)
531
315
  check_patterns(value_object, fields)
@@ -545,16 +329,12 @@ module Hecks
545
329
  # @param value_object [Class] the `Bluebook::ValueObject` subclass to build
546
330
  # @param fields [Hash{Symbol, String => Object}] the offered field values,
547
331
  # either key spelling
548
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity, nil] the construct
549
- # a nested composite field is resolved against; nil skips nested
550
- # normalization entirely
332
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity, nil] construct a
333
+ # nested composite field resolves against; nil skips nested normalization
551
334
  # @return [Runtime::Value] the built, validated value object
552
- # @raise [Runtime::UnknownArgument] if `fields` (or a nested field) names a
553
- # key its own type does not declare
554
- # @raise [Runtime::TypeMismatch] if a field (or a nested one) cannot be
555
- # coerced to its declared type
556
- # @raise [Runtime::InvariantViolation] if the built value object (or a
557
- # nested one) breaks one of its own invariants
335
+ # @raise [Runtime::UnknownArgument] if a field names an undeclared key
336
+ # @raise [Runtime::TypeMismatch] if a field cannot be coerced to its declared type
337
+ # @raise [Runtime::InvariantViolation] if the built value object breaks an invariant
558
338
  def build(value_object, fields, aggregate = nil)
559
339
  fields = apply_defaults(value_object, fields.transform_keys(&:to_sym))
560
340
  fields = normalize_composite_fields(aggregate, value_object, fields)
@@ -562,22 +342,15 @@ module Hecks
562
342
  new(value_object, fields)
563
343
  end
564
344
 
565
- # State arrives decoded or not at all (Phase 2, Track A, PR A4).
566
- # Every persistence adapter reads through `Ports::Persistence::
567
- # StateCodec.decode` (A3), which symbolizes every top-level key, and
568
- # the runtime's own callers (entity elements, the remote dispatcher's
569
- # `symbolize_names:` parse, Era's audit) build symbol-keyed state
570
- # themselves. A String key here is an adapter or caller that skipped
571
- # the codec, so it is refused by name rather than respelled: a silent
572
- # `to_sym` here would hide exactly this kind of bypass instead. Always
573
- # on, because it costs one `is_a?` per key, no more than a `to_sym` would.
345
+ # State arrives decoded or not at all: every persistence adapter symbolizes
346
+ # keys before this point, so a String key here means a caller skipped that
347
+ # step — refused by name rather than silently respelled.
574
348
  #
575
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
576
- # whose declared attributes coerce `state`'s own values
349
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] construct whose
350
+ # declared attributes coerce `state`'s own values
577
351
  # @param state [Hash{Symbol => Object}] the stored state to hydrate; every
578
352
  # key must already be a Symbol
579
- # @return [Hash{Symbol => Object}] `state`, coerced through every declared
580
- # attribute it names
353
+ # @return [Hash{Symbol => Object}] `state`, coerced through each declared attribute
581
354
  # @raise [Runtime::WiringError] if `state` holds any non-Symbol key
582
355
  def hydrate(aggregate, state)
583
356
  undecoded = state.keys.grep_v(Symbol)
@@ -600,9 +373,8 @@ module Hecks
600
373
  # Marks the block as loading trusted, already-validated stored state, so
601
374
  # `validate!` skips its own checks for the block's duration.
602
375
  #
603
- # `Thread.current`-backed, not a plain ivar, so two threads hydrating
604
- # concurrently on the same `Value` singleton class never see or clear
605
- # each other's flag.
376
+ # Thread-local, not a plain ivar, so two threads hydrating concurrently
377
+ # never see or clear each other's flag.
606
378
  #
607
379
  # @yield the code that should see `trusting_stored_state?` true
608
380
  # @return [Object] the block's result
@@ -614,59 +386,27 @@ module Hecks
614
386
  Thread.current[TRUSTED_LOAD_KEY] = previous
615
387
  end
616
388
 
617
- # Reports whether the current thread is inside a `trusting_stored_state` block.
389
+ # Whether the current thread is inside a `trusting_stored_state` block.
618
390
  #
619
- # @return [Boolean] true if a `trusting_stored_state` block is on this
620
- # thread's own call stack
391
+ # @return [Boolean] true if on this thread's own call stack
621
392
  def trusting_stored_state? = Thread.current[TRUSTED_LOAD_KEY] == true
622
393
 
623
- # QualityControl BUG#125 — the one narrow door `check_scalar_shapes`
624
- # keeps open, now that a non-string scalar is otherwise refused for a
625
- # String-typed field. `MetaValidator::Judge#send_to` — the single
626
- # choke point every one of the language's own self-hosted dispatches
627
- # goes through while walking a bluebook's declarations into the
628
- # "Bluebook" meta-domain — wraps itself in this, and nothing else
629
- # does. `Judge#appends`' generic `POSITION` handling
630
- # (judge.rb#appends) keys purely off a field being named "position",
631
- # the convention every other append list actually uses it for
632
- # (ValueObject::Member, ProcessManager::Handler, ... — all really
633
- # `Position`/Integer-typed); `Normalise`'s own `NormalisationRule`
634
- # happens to also name its own domain field "position"
635
- # (bluebook.bluebook), but declares it `RuleText` (String) — so the
636
- # same walk-index substitution (`Judge#v(index)`) hands it a raw
637
- # Integer too, on every domain's very first boot (the language
638
- # self-judges its own grammar via `MetaValidator.fresh_runtime`'s
639
- # fixpoint). Confirmed (QualityControl BUG#125 investigation): the
640
- # resulting value is never read back — `normalisations` is an
641
- # `ELSEWHERE`/`derived` field spliced straight from
642
- # `Expression::CanonicalForm.table` (assembly/contracts.rb), so the
643
- # judged record holding the Integer is discarded whole — this is a
644
- # walk-index/domain-field name collision inside `Judge#appends`, not
645
- # a genuine semantic need for `position` to arrive numeric. Fixing
646
- # that collision at its own root is a separate, larger change to
647
- # self-hosted bootstrap mechanics that every domain's boot depends
648
- # on; this flag only ever loosens scalar-shape checking for the
649
- # meta-grammar's own value objects (RuleText, BluebookName, Position,
650
- # …) that Judge itself constructs while walking a bluebook's
651
- # declarations — never for a real domain's own declared value
652
- # objects (PieceId, Money, …), which Judge never dispatches commands
653
- # against. `offer` (judge.rb) already converts a `TypeMismatch` here
654
- # into a recorded refusal rather than letting it propagate, but
655
- # `MetaValidator.call` raises the instant `refusals` is non-empty
656
- # (meta_validator.rb) — so, unexempted, this would fail every
657
- # domain's boot, not just the language's own bootstrap. Composite
658
- # shapes (Array/Hash) stay refused unconditionally, bootstrap or not
659
- # — nothing Judge does ever legitimately needs those for a scalar
660
- # field.
394
+ # `MetaValidator::Judge#send_to` walks a bluebook's own declarations through
395
+ # the self-hosted "Bluebook" meta-domain, and its generic append handling
396
+ # keys off a field's name ("position") rather than its declared type — so
397
+ # the language's own grammar can hand this a raw Integer for a String-typed
398
+ # meta field on every domain's first boot. This flag loosens
399
+ # `check_scalar_shapes`'s String check only for that one caller; composite
400
+ # shapes (Array/Hash) stay refused unconditionally, bootstrap or not, and no
401
+ # real domain's own declared value objects are affected.
661
402
  BOOTSTRAP_KEY = :hecks_judge_bootstrapping
662
403
 
663
- # Marks the block as `MetaValidator::Judge#send_to`'s own self-hosted
664
- # bootstrap dispatch, so `check_scalar_shapes` loosens its `String` check
665
- # for the block's duration.
404
+ # Marks the block as `MetaValidator::Judge#send_to`'s self-hosted bootstrap
405
+ # dispatch, so `check_scalar_shapes` loosens its `String` check for the
406
+ # block's duration.
666
407
  #
667
- # `Thread.current`-backed, not a plain ivar, so two threads bootstrapping
668
- # concurrently on the same `Value` singleton class never see or clear
669
- # each other's flag.
408
+ # Thread-local, not a plain ivar, so two threads bootstrapping concurrently
409
+ # never see or clear each other's flag.
670
410
  #
671
411
  # @yield the code that should see `judge_bootstrapping?` true
672
412
  # @return [Object] the block's result
@@ -678,83 +418,22 @@ module Hecks
678
418
  Thread.current[BOOTSTRAP_KEY] = previous
679
419
  end
680
420
 
681
- # Reports whether the current thread is inside a `judge_bootstrapping` block.
421
+ # Whether the current thread is inside a `judge_bootstrapping` block.
682
422
  #
683
- # @return [Boolean] true if a `judge_bootstrapping` block is on this
684
- # thread's own call stack
423
+ # @return [Boolean] true if on this thread's own call stack
685
424
  def judge_bootstrapping? = Thread.current[BOOTSTRAP_KEY] == true
686
425
 
687
- # An identity names its field — `identified_by :number` — and the
688
- # path is what reaches the scalar, never a guess at which field a
689
- # one-field value object might mean. A declaration that names no
690
- # field is refused when the bluebook loads, so nothing has to be
691
- # unwrapped later.
692
- #
693
- # `scalar` below is a different job and stays: rendering a value object
694
- # into a column or a message, where there is no path to consult.
695
-
696
- # A reference is the id itself — refused at the payload gate if it
697
- # arrives as anything else — so there is nothing to open inside it.
698
-
699
- # A reference is an ID, so anything else is not one.
426
+ # A reference is an ID; refused at the payload gate if it arrives as a
427
+ # Hash or `Runtime::Value` instead. `nil` stays legitimate for an optional
428
+ # reference — a required reference's own `nil` is refused elsewhere (C3.7).
700
429
  #
701
- # Nothing coerces a reference — `for_attribute` misses on
702
- # "Reference<Account>", which is no value object's name, and hands the
703
- # argument straight through. That is why the wrapped form went in
704
- # unnoticed for as long as it did: there was no place it could be
705
- # refused, so whatever the first caller wrote became the shape.
706
- #
707
- # This is that place. It sits at the payload gate rather than inside
708
- # coercion because the sentence names the command, and `for_attribute`
709
- # never learns which command it is serving.
710
- #
711
- # Widened past the object shape by BUG#27 (QualityControl ledger,
712
- # found live on `qa/stress_domains/referral_chain`'s `Member.Join`/
713
- # `Referral.Issue`). Without this widening, a bare Boolean, Array, or
714
- # `null` would sail through here untouched — nothing but Hash/Value
715
- # ever refused — then get `.to_s`'d into a lookup key by `CommandRules::
716
- # References#reference_key` ("true", "false", "[8, 8]") and answer
717
- # NotFound, or, for `null`, skip the lookup outright
718
- # (`next if held.nil?`, command_rules/references.rb) and let the
719
- # command run on to whatever its own `given` happened to say — a
720
- # shape error misreading as a missing record, or as an unrelated
721
- # domain refusal. Rust's generated `from_json` requires a JSON
722
- # string for a required reference field before anything else runs
723
- # (`JoinArgs.sponsor: expected String`); this closes the same gate
724
- # at the same DISPATCH_ORDER step Ruby already runs it at
725
- # (`normalize_args`, `Vocabulary::AggregateDispatchOrder`/
726
- # `EntityDispatchOrder`), strictly before `resolve_references` ever
727
- # receives a value to look up — so the two engines now agree on
728
- # both kind and order, not just kind.
729
- #
730
- # `nil` stays legitimate for a `reference_to ..., optional: true`
731
- # argument (`Improvement.Open`'s own `reference_to Angle, optional:
732
- # true` — `qa/bluebook/quality_control.bluebook`): the caller
733
- # genuinely may have nothing to name yet, and `nil_argument`
734
- # (interpreting.rb) already passes an optional reference's `nil`
735
- # through untouched. A required reference offered as `null` is a
736
- # caller leaving a required argument empty in every other sense
737
- # this runtime already refuses (C3.7) — refusing it here, rather
738
- # than falling through to `resolve_references`' own nil-skip and
739
- # then whatever the command's `given` happens to say, is what
740
- # actually names the empty argument instead of something else.
741
- #
742
- # A `has_many` reference's own Array shape is still never refused
743
- # by its wrapper (`Array(value).find { ... }` only inspects the
744
- # list's elements) — a reference is never a scalar list-of-lists
745
- # today, and inventing a rule for a shape the language cannot
746
- # declare is how decoration gets written. `reference_list` (below)
747
- # already owns "not an Array at all" for that case.
748
- #
749
- # @param command [Class] the command class (`Bluebook::Command` subclass)
750
- # `attribute` is declared on, named in a refusal
751
- # @param attribute [Bluebook::Attribute] the attribute to check; a no-op
752
- # unless it is reference-typed
430
+ # @param command [Class] the command `attribute` is declared on, named in a refusal
431
+ # @param attribute [Bluebook::Attribute] the attribute to check; a no-op unless
432
+ # reference-typed
753
433
  # @param value [Object] the offered value
754
434
  # @return [void]
755
- # @raise [Runtime::TypeMismatch] if `value` (or, for a `has_many` reference,
756
- # any of its elements) is a Hash or a `Runtime::Value` rather than a plain
757
- # identity, or a required scalar reference is anything but a String
435
+ # @raise [Runtime::TypeMismatch] if `value` (or an element, for `has_many`) is a
436
+ # Hash or `Runtime::Value` rather than a plain identity
758
437
  def refuse_object_reference(command, attribute, value)
759
438
  return unless attribute.reference?
760
439
 
@@ -775,13 +454,28 @@ module Hecks
775
454
  known_by: known_by(attribute))
776
455
  end
777
456
 
778
- # "an object" for the Hash/Value shape — the original wording this
779
- # method always gave, pinned byte for byte by
780
- # `spec/runtime/reference_shape_spec.rb`, kept unchanged by BUG#27's
781
- # widening. `Rendering.describe` for everything else: `true`,
782
- # `false`, `nil`, `[8, 8]` — the same rendering every other
783
- # TypeMismatch in this file already uses for "here is what you
784
- # actually sent."
457
+ # A list argument is an Array whatever its element type, so a lone scalar offered for
458
+ # it is refused; nil stays legitimate, as for any optional argument. The single-element
459
+ # form belongs to the `append`/`remove` effects, which take one element and are
460
+ # coerced against the aggregate's own list, never through this gate.
461
+ #
462
+ # @param command [#hecks_name] what `attribute` is declared on, named in a refusal
463
+ # @param attribute [Bluebook::Attribute] the attribute to check; a no-op unless list-typed
464
+ # (a `has_many` reference list has its own refusal)
465
+ # @param value [Object] the offered value
466
+ # @return [void]
467
+ # @raise [Runtime::TypeMismatch] if `value` is neither nil nor an Array
468
+ def refuse_scalar_list(command, attribute, value)
469
+ return unless attribute.list? && !attribute.reference? && !value.nil? && !value.is_a?(Array)
470
+
471
+ raise TypeMismatch, RefusalWording.render_site(
472
+ "TypeMismatch", "numeric_field", type: command.hecks_name, field: attribute.name,
473
+ expected: "list_of(#{attribute.type})", offered: Rendering.describe(value)
474
+ )
475
+ end
476
+
477
+ # "an object" for the Hash/Value shape; `Rendering.describe` otherwise —
478
+ # the same rendering every other TypeMismatch in this file uses.
785
479
  #
786
480
  # @param value [Object] the wrongly-shaped offered value to describe
787
481
  # @return [String] `"an object"` for a Hash or `Runtime::Value`; otherwise
@@ -792,22 +486,13 @@ module Hecks
792
486
  Rendering.describe(value)
793
487
  end
794
488
 
795
- # "(Account is known by number)" — what to send instead. No article, on
796
- # purpose: "an Account" and "a Customer" differ by the target's first
797
- # letter, and a refusal pinned byte-for-byte should not hinge on an
798
- # article-choosing rule. Silent when the target is another chapter's,
799
- # where this runtime cannot see what it is known by.
800
- #
801
- # Every head, because a caller has to pass every one. This read
802
- # `identified_by`, which is the single head and is nil the moment an
803
- # identity has two parts — so a composite target fell through the guard
804
- # and the refusal went silent exactly where it had the most to say. A
805
- # single-path target reads as it always did.
489
+ # "(Account is known by number)" — what to send instead. No article, since
490
+ # "an Account" vs "a Customer" would make a pinned refusal hinge on spelling.
806
491
  #
807
492
  # @param attribute [Bluebook::Attribute] the reference-typed attribute to
808
- # describe the target's own identity heads for
809
- # @return [String] `" (Target is known by head1, head2)"`, or `""` when the
810
- # target cannot be resolved or declares no identity heads
493
+ # describe the target's identity heads for
494
+ # @return [String] `" (Target is known by head1, head2)"`, or `""` if the
495
+ # target or its identity heads cannot be resolved
811
496
  def known_by(attribute)
812
497
  heads = Array(attribute.type.resolve&.identity_heads)
813
498
  return "" if heads.empty?
@@ -818,12 +503,9 @@ module Hecks
818
503
  # Renders a value object into the bare scalar its one field holds — for a
819
504
  # column or a message, where there is no path to consult.
820
505
  #
821
- # @param value [Object] the value to render; passed through unless a
822
- # `Runtime::Value`
823
- # @return [Object] `value` unchanged when it is not a `Runtime::Value`;
824
- # otherwise its one field's own value
825
- # @raise [Runtime::TypeMismatch] if `value` is a `Runtime::Value` with more
826
- # than one field
506
+ # @param value [Object] the value to render; passed through unless a `Runtime::Value`
507
+ # @return [Object] `value` unchanged, or its one field's own value
508
+ # @raise [Runtime::TypeMismatch] if `value` has more than one field
827
509
  def scalar(value)
828
510
  return value unless value.is_a?(self)
829
511
 
@@ -835,17 +517,13 @@ module Hecks
835
517
 
836
518
  # Coerces a derived identity string back into `attribute`'s own declared type.
837
519
  #
838
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
520
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] construct
839
521
  # `attribute` is declared on
840
- # @param attribute [Bluebook::Attribute] the identity attribute to coerce
841
- # `identifier` for
842
- # @param identifier [String, Object] the derived identity, typically a
843
- # String (`Identity.of`/`Identity.from`'s own return)
844
- # @return [Runtime::Value, String, Object] a built value object when
845
- # `attribute`'s type names a single-field value object; `identifier`
846
- # unchanged otherwise
847
- # @raise [Runtime::TypeMismatch] if `attribute`'s type names a value object
848
- # with more than one field
522
+ # @param attribute [Bluebook::Attribute] the identity attribute to coerce for
523
+ # @param identifier [String, Object] the derived identity
524
+ # @return [Runtime::Value, String, Object] a built value object for a
525
+ # single-field value-object type; `identifier` unchanged otherwise
526
+ # @raise [Runtime::TypeMismatch] if the type names a multi-field value object
849
527
  def from_identifier(aggregate, attribute, identifier)
850
528
  value_object = value_object_for(aggregate, attribute.type)
851
529
  return identifier unless value_object
@@ -859,33 +537,9 @@ module Hecks
859
537
  raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "composite_identity", type: value_object.hecks_name)
860
538
  end
861
539
 
862
- # Vendored fix, not (yet) upstream hecks (migration plan
863
- # task 9): `identifier` here is always the derived identity
864
- # string -- `Identity.of`/`Identity.from` intentionally return
865
- # one (correct for naming a repository key), and
866
- # `Runtime::Instance#materialize_identity!` calls `from_identifier`
867
- # with exactly that string on every fresh hydration -- but when
868
- # the identity field's own declared type is Integer/Float (not
869
- # the overwhelmingly common String), seeding it straight from
870
- # that string round-trips a correctly-derived identity back in
871
- # as the wrong Ruby type -- and #build's own
872
- # `check_numeric_fields` (added specifically to catch a genuine
873
- # caller mismatch) then refused the runtime's own internal
874
- # identity seed instead, on every dispatch, valid input or not.
875
- #
876
- # Reuses this same file's own `NUMERIC` table (declared-type ->
877
- # expected-Ruby-class, already read by `check_numeric_fields`)
878
- # to decide which declared types need converting, and
879
- # Kernel#Integer/#Float to do the converting. A genuinely
880
- # malformed identifier (should never happen, since an identity
881
- # is always derived from a correctly-typed field in the first
882
- # place, but this stays defensive rather than assume it) passes
883
- # back unconverted, and `check_numeric_fields` refuses it
884
- # exactly as it always has -- preserving its real job of
885
- # catching a genuine caller mismatch, not just this migration's
886
- # own runtime-internal one.
540
+ # Converts a derived numeric identity string back before `check_numeric_fields` runs.
887
541
  private def coerce_identifier(field, identifier)
888
- return identifier unless identifier.is_a?(String) && NUMERIC.key?(field.type.to_s)
542
+ return identifier unless identifier.is_a?(String) && FieldChecks::NUMERIC.key?(field.type.to_s)
889
543
 
890
544
  case field.type.to_s
891
545
  when "Integer" then Integer(identifier)
@@ -904,258 +558,6 @@ module Hecks
904
558
  def canonical_fields(fields)
905
559
  JSON.generate(fields.sort_by { |name, _| name.to_s }.to_h)
906
560
  end
907
-
908
- # A field declared Integer or Float must arrive as one.
909
- #
910
- # Without this a String sails into a numeric field and the failure surfaces
911
- # later, inside a predicate, as `positive? expects a number, got "three"` —
912
- # an EvaluationError, which is not a domain refusal. So the runtime broke
913
- # where the domain should have said no, and the run contract recorded the
914
- # crash beside genuine refusals as though the domain had judged it.
915
- #
916
- # C3.8 — the boundary check for an attribute whose type is a bare
917
- # primitive rather than a value object: `Integer`/`Float` by exact
918
- # numeric class (`NUMERIC`), `String`/booleans by rejecting only a
919
- # composite shape (`COMPOSITE_SHAPES`) — a looser test than
920
- # `check_scalar_shapes` holds a value object's own `String` field to
921
- # (QualityControl BUG#125 has that one also refuse
922
- # a non-string scalar; a bare `String` argument here still admits
923
- # any other scalar — a bare-primitive
924
- # attribute was never part of BUG#125's own investigation or fix,
925
- # and whether it needs the same tightening, and against what real
926
- # Judge dependency if any, is still open); worded by the same
927
- # template with the owning construct as `type`. The Rust boundary is
928
- # stricter there too, recorded in the clause.
929
- private def check_bare_primitive(owner, attribute, value)
930
- type = attribute.type.to_s
931
- expected = NUMERIC[type]
932
- mistyped = if expected
933
- !value.is_a?(expected)
934
- elsif NON_NUMERIC_SCALARS.include?(type)
935
- COMPOSITE_SHAPES.any? { |shape| value.is_a?(shape) }
936
- else
937
- false
938
- end
939
- if mistyped
940
- raise TypeMismatch,
941
- RefusalWording.render_site("TypeMismatch", "numeric_field",
942
- type: owner.hecks_name, field: attribute.name,
943
- expected: type, offered: Rendering.describe(value))
944
- end
945
-
946
- check_numeric_bounds(owner.hecks_name, attribute.name, value)
947
- end
948
-
949
- # C3.3/C3.4 — the value model's own bounds, held at every boundary:
950
- # an Integer must fit in signed 64 bits, a Float must be finite.
951
- # One check for value-object fields and bare-primitive arguments
952
- # alike (`check_numeric_fields` and `check_bare_primitive`).
953
- INT64_RANGE = (-(2**63))..((2**63) - 1)
954
-
955
- private def check_numeric_bounds(type_name, field_name, given)
956
- if given.is_a?(Integer) && !INT64_RANGE.cover?(given)
957
- raise TypeMismatch,
958
- RefusalWording.render_site("TypeMismatch", "integer_range",
959
- type: type_name, field: field_name, offered: Rendering.describe(given))
960
- end
961
- return unless given.is_a?(Float) && !given.finite?
962
-
963
- raise TypeMismatch,
964
- RefusalWording.render_site("TypeMismatch", "non_finite_field",
965
- type: type_name, field: field_name, offered: Rendering.describe(given))
966
- end
967
-
968
- # QualityControl BUG#41 — a value object refuses a key it does not
969
- # declare, the same way a command's own payload does
970
- # (`CommandInterpreter::ArgumentGate#refuse_unknown_arguments`,
971
- # argument_gate.rb) — reusing that method's exact refusal wording
972
- # (`UnknownArgument unknown_args`, refusal_wording.rb:
973
- # "{command} does not declare {unknown} — it takes {declared}")
974
- # rather than inventing a new template, because every generated
975
- # Rust value-object `from_json` already renders this refusal
976
- # through that identical site: `rust/project/json_codec.rb`'s
977
- # `emit_unknown_argument_check` (mirrored byte-for-byte in
978
- # `rust/codegen/src/json_codec.rs`) emits `v.unknown_keys(&[...])`
979
- # and the same "{name} does not declare {unknown} — it takes
980
- # {declared}" format string for every value object's own
981
- # `from_json` — `GameLabel::from_json`
982
- # (rust/src/generated/chess/game.rs) is simply the first case
983
- # this gap was reproduced against.
984
- #
985
- # Ruby never had an equivalent check anywhere in this `validate!`
986
- # door before now — `fields[attribute.name]` reads only the
987
- # declared attributes, so any other key a caller's Hash carried
988
- # was silently ignored. `fields` here only ever holds what a
989
- # caller (or `for_attribute`'s own recursive coercion) offered
990
- # for this value object — built by `fields_for`'s plain
991
- # key-symbolizing (never a Hash the runtime pads with bookkeeping
992
- # keys of its own; confirmed by reading every call site that
993
- # reaches `validate!`) — so there is nothing legitimate here to
994
- # exempt.
995
- #
996
- # Checked first, before `check_required_fields` and everything
997
- # after it — matching Rust's own `from_json`, which checks
998
- # `unknown_keys` before reading a single declared field. So a
999
- # Hash offering both an unrecognized key and a missing required
1000
- # one (BUG#41's own second demonstration case: `label: {extra:
1001
- # "bogus"}` — unknown and missing `value`) refuses the same
1002
- # UnknownArgument on both engines, not two different refusal
1003
- # kinds for one malformed call.
1004
- private def check_unknown_fields(value_object, fields)
1005
- known = value_object.attributes.map { |attribute| attribute.name.to_sym }
1006
- unknown = (fields.keys.map(&:to_sym) - known).sort
1007
- return if unknown.empty?
1008
-
1009
- declared = value_object.attributes.map(&:name)
1010
- raise UnknownArgument,
1011
- RefusalWording.render_site("UnknownArgument", "unknown_args",
1012
- command: value_object.hecks_name, unknown: unknown,
1013
- declared: declared)
1014
- end
1015
-
1016
- # C3.7 — a value object is a typed field product: every non-optional
1017
- # field arrives, or construction refuses. A missing field and a null
1018
- # one are the same absence (`fields[name]` reads nil for both), worded
1019
- # as the type mismatch it is — "{type}.{field} expects {expected}, got
1020
- # nil" — the identical string the Rust side's generated `from_json`
1021
- # gives the same input, so the corpus can pin it on both. Checked
1022
- # first among the field-content checks (after `check_unknown_fields`'s
1023
- # own structural gate above, BUG#41): without this, an invariant
1024
- # reading a field that never arrived would be exactly the thing that
1025
- # answers "invariant violated" (or nothing at all — `ToppingName`'s
1026
- # `{value: null}` would be accepted and stored). A `default:` has
1027
- # already been filled in by `apply_defaults`; a list field's absence
1028
- # is an empty list, never a refusal.
1029
- private def check_required_fields(value_object, fields)
1030
- value_object.attributes.each do |attribute|
1031
- next if attribute.optional? || attribute.list?
1032
- next unless fields[attribute.name].nil?
1033
-
1034
- raise TypeMismatch,
1035
- RefusalWording.render_site("TypeMismatch", "numeric_field",
1036
- type: value_object.hecks_name, field: attribute.name,
1037
- expected: attribute.type, offered: "nil")
1038
- end
1039
- end
1040
-
1041
- # Checked before invariants, because an invariant reading a mistyped field
1042
- # is exactly the thing that would otherwise explode.
1043
- NUMERIC = { "Integer" => Integer, "Float" => Numeric }.freeze
1044
- private def check_numeric_fields(value_object, fields)
1045
- value_object.attributes.each do |attribute|
1046
- expected = NUMERIC[attribute.type.to_s]
1047
- next unless expected
1048
-
1049
- given = fields[attribute.name]
1050
- next if given.nil?
1051
-
1052
- unless given.is_a?(expected)
1053
- raise TypeMismatch,
1054
- RefusalWording.render_site("TypeMismatch", "numeric_field",
1055
- type: value_object.hecks_name, field: attribute.name,
1056
- expected: attribute.type, offered: Rendering.describe(given))
1057
- end
1058
-
1059
- # PRD 05 (numeric-boundary-coverage) — `given.is_a?(expected)`
1060
- # alone waves a NaN or an Infinity straight through: both are
1061
- # real `Float`s, so `is_a?(Numeric)`/`is_a?(Float)` is true for
1062
- # either. Never exercised before this, because
1063
- # `ValueGenerator::FLOAT_EDGE_CASES` had no non-finite value in
1064
- # it — the fuzzer could not have found this on its own until
1065
- # the table was widened alongside this fix. Left unchecked, a
1066
- # non-finite Float reaches `CommandRules::Arithmetic#clamp`
1067
- # (`current.clamp(min, max)` — `ArgumentError: comparison of
1068
- # Float with X failed`, a genuine Ruby-level crash, not a
1069
- # domain refusal, exactly the same failure mode this method's
1070
- # own header describes for a mistyped field) or all the way to
1071
- # storage, where `JSON.generate`/`#to_json` raises
1072
- # `JSON::GeneratorError: NaN/Infinity not allowed in JSON` the
1073
- # moment anything tries to persist or replay it — again a raw
1074
- # crash, not a refusal. `-0.0` is deliberately not refused
1075
- # here: it is finite, round-trips through JSON as `-0.0`
1076
- # cleanly (confirmed empirically), and is a legitimate,
1077
- # meaningful float value (a signed zero), not a corruption
1078
- # risk — only NaN and +/-Infinity are.
1079
- check_numeric_bounds(value_object.hecks_name, attribute.name, given)
1080
- end
1081
- end
1082
-
1083
- # A field declared `String` (or a boolean) must not arrive as a
1084
- # composite — an Array or a Hash (or a nested Value) standing in for
1085
- # what has to be a leaf scalar. A `String` field, further, must not
1086
- # arrive as any other non-composite scalar either (Integer, Float,
1087
- # true/false) — QualityControl BUG#125, matching Rust's generated
1088
- # `from_json`, which requires a JSON string node for a String-typed
1089
- # field unconditionally and refuses anything else, including a JSON
1090
- # number or boolean. Without this, Ruby would tolerate exactly that
1091
- # (found live: `Chess::Piece.Capture`'s `PieceId`, String-typed,
1092
- # offered a bignum `id` — Ruby let it pass and failed later on an
1093
- # unrelated field, Rust refused on `id` itself, immediately) — except
1094
- # inside `judge_bootstrapping?` (above), the one caller genuinely
1095
- # relying on that leniency; see that flag's own comment for why.
1096
- #
1097
- # `TrueClass`/`FalseClass` stay laxer than `check_numeric_fields`
1098
- # above: for those two, this still only enforces that the shape
1099
- # isn't a collection, not the exact Ruby class — narrower than the
1100
- # `String` case above because BUG#125 investigated and fixed String
1101
- # specifically; a boolean field's own scalar-shape tolerance is a
1102
- # separate, uninvestigated question left exactly as it was. No
1103
- # scalar field, of any declared type, can ever legitimately be
1104
- # handed an Array or a Hash — that shape is always wrong, and always
1105
- # was: `InvalidValueGenerator#array_for_scalar`'s own corruption is
1106
- # deliberately built to be refused (see that file's header), and
1107
- # until this check existed it sailed straight through for a
1108
- # String/boolean field the way it never could for an Integer/Float
1109
- # one (`check_numeric_fields` above already catches an Array offered
1110
- # for those). Found live via bin/fuzz, seed 17 on the fixtures
1111
- # domain : an Array standing in for a single-field identity's
1112
- # declared `String`, `.to_s`'d into a record id downstream.
1113
- COMPOSITE_SHAPES = [Array, ::Hash].freeze
1114
- NON_NUMERIC_SCALARS = %w[String TrueClass FalseClass].freeze
1115
- private def check_scalar_shapes(value_object, fields)
1116
- value_object.attributes.each do |attribute|
1117
- type = attribute.type.to_s
1118
- next unless NON_NUMERIC_SCALARS.include?(type)
1119
-
1120
- given = fields[attribute.name]
1121
- next if given.nil?
1122
-
1123
- composite = COMPOSITE_SHAPES.any? { |shape| given.is_a?(shape) }
1124
- non_string_scalar = type == "String" && !composite && !given.is_a?(String) && !judge_bootstrapping?
1125
- next unless composite || non_string_scalar
1126
-
1127
- raise TypeMismatch,
1128
- RefusalWording.render_site("TypeMismatch", "numeric_field",
1129
- type: value_object.hecks_name, field: attribute.name,
1130
- expected: attribute.type, offered: Rendering.describe(given))
1131
- end
1132
- end
1133
-
1134
- # A field declared with a pattern must match it.
1135
- #
1136
- # Beside check_numeric_fields and for the same reason : a value that does
1137
- # not look like what it claims to be is the domain saying no, and it should
1138
- # say so here rather than let the wrong shape travel on and surface as a
1139
- # broken predicate later.
1140
- #
1141
- # Which regexes may be written at all is PatternSubset's job, enforced when
1142
- # the bluebook is declared — so by the time a value arrives here the pattern
1143
- # is already a vetted, unambiguous one, and this is a plain match.
1144
- private def check_patterns(value_object, fields)
1145
- value_object.attributes.each do |attribute|
1146
- pattern = attribute.pattern
1147
- next unless pattern
1148
-
1149
- given = fields[attribute.name]
1150
- next if given.nil?
1151
- next if given.is_a?(String) && Regexp.new(pattern).match?(given)
1152
-
1153
- raise TypeMismatch,
1154
- RefusalWording.render_site("TypeMismatch", "pattern_mismatch",
1155
- type: value_object.hecks_name, field: attribute.name,
1156
- pattern: pattern, offered: Rendering.describe(given))
1157
- end
1158
- end
1159
561
  end
1160
562
  end
1161
563
  end