hecks 1.3.0 → 1.4.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 (355) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +5 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/prism.rb +4 -4
  23. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  24. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  25. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  26. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  27. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  28. data/lib/hecks/adapters/driven.rb +4 -4
  29. data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
  30. data/lib/hecks/behaviors/dsl.rb +2 -2
  31. data/lib/hecks/behaviors/expectations.rb +23 -23
  32. data/lib/hecks/behaviors/ir.rb +1 -1
  33. data/lib/hecks/behaviors/rspec.rb +1 -1
  34. data/lib/hecks/behaviors/runner.rb +2 -2
  35. data/lib/hecks/behaviors.rb +1 -1
  36. data/lib/hecks/bluebook/aggregate.rb +13 -13
  37. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  38. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  39. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  40. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  41. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  42. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  43. data/lib/hecks/bluebook/assembly.rb +9 -9
  44. data/lib/hecks/bluebook/attribute.rb +9 -9
  45. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  46. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  47. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  48. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  49. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
  50. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  51. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  52. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  53. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  54. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  55. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  56. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  57. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  58. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  59. data/lib/hecks/bluebook/capabilities.rb +27 -0
  60. data/lib/hecks/bluebook/chapter.rb +28 -9
  61. data/lib/hecks/bluebook/command.rb +12 -12
  62. data/lib/hecks/bluebook/domain_port.rb +9 -9
  63. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  64. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  65. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  66. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  67. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  68. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  69. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  70. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  72. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  73. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  74. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  75. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  76. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  77. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  78. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  79. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  80. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  81. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  82. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  83. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  84. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  85. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  86. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  87. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  88. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  89. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  90. data/lib/hecks/bluebook/entity.rb +11 -11
  91. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  92. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  93. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  94. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  95. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  97. data/lib/hecks/bluebook/hexagon.rb +1 -1
  98. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  99. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
  101. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  102. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  103. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  104. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  105. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  106. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  107. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  108. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  109. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  110. data/lib/hecks/bluebook/model_check.rb +170 -84
  111. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  112. data/lib/hecks/bluebook/policy.rb +15 -13
  113. data/lib/hecks/bluebook/process_manager.rb +14 -14
  114. data/lib/hecks/bluebook/project_register.rb +6 -6
  115. data/lib/hecks/bluebook/query.rb +4 -4
  116. data/lib/hecks/bluebook/read_model.rb +14 -14
  117. data/lib/hecks/bluebook/reference.rb +8 -8
  118. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  119. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  120. data/lib/hecks/bluebook/translation.rb +4 -4
  121. data/lib/hecks/bluebook/value_object.rb +6 -6
  122. data/lib/hecks/bluebook.rb +2 -2
  123. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  124. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  125. data/lib/hecks/codemod.rb +36 -35
  126. data/lib/hecks/construct.rb +6 -6
  127. data/lib/hecks/corpus.rb +317 -0
  128. data/lib/hecks/deprecation.rb +95 -0
  129. data/lib/hecks/doc/reference.rb +19 -19
  130. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  131. data/lib/hecks/facade/cli_door.rb +69 -10
  132. data/lib/hecks/facade/cli_runner.rb +105 -24
  133. data/lib/hecks/facade/command_request.rb +23 -0
  134. data/lib/hecks/facade/handle.rb +79 -32
  135. data/lib/hecks/facade/json_door.rb +106 -25
  136. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  137. data/lib/hecks/facade/surface/chapter.rb +26 -17
  138. data/lib/hecks/facade/surface.rb +16 -3
  139. data/lib/hecks/facade.rb +15 -4
  140. data/lib/hecks/forms/app.rb +46 -30
  141. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  142. data/lib/hecks/forms/field_renderer.rb +142 -6
  143. data/lib/hecks/forms/field_shape.rb +180 -19
  144. data/lib/hecks/forms/html.rb +51 -7
  145. data/lib/hecks/forms/index_renderer.rb +14 -2
  146. data/lib/hecks/forms/params.rb +120 -23
  147. data/lib/hecks/forms/port_argument.rb +2 -2
  148. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  149. data/lib/hecks/forms/record_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_table.rb +1 -1
  151. data/lib/hecks/forms/value_object_shape.rb +3 -3
  152. data/lib/hecks/forms.rb +24 -4
  153. data/lib/hecks/fqn.rb +1 -1
  154. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  155. data/lib/hecks/framework.rb +48 -17
  156. data/lib/hecks/freezer.rb +11 -11
  157. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  158. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  159. data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
  160. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  161. data/lib/hecks/fuzzing/differential.rb +158 -0
  162. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  163. data/lib/hecks/fuzzing/era_boundary.rb +37 -18
  164. data/lib/hecks/fuzzing/form_census.rb +35 -20
  165. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  166. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  167. data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
  168. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  169. data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
  170. data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
  171. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
  172. data/lib/hecks/fuzzing/properties/guards.rb +42 -42
  173. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  174. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
  175. data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
  176. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  177. data/lib/hecks/fuzzing/properties.rb +64 -40
  178. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  179. data/lib/hecks/fuzzing/replay.rb +114 -114
  180. data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
  181. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  182. data/lib/hecks/fuzzing/self_consistency.rb +141 -112
  183. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
  184. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  185. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  186. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  187. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
  188. data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
  189. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  190. data/lib/hecks/fuzzing/structural_skips.rb +23 -130
  191. data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
  192. data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
  193. data/lib/hecks/fuzzing/value_generator.rb +14 -14
  194. data/lib/hecks/fuzzing.rb +5 -0
  195. data/lib/hecks/grammar/evolve.rb +10 -10
  196. data/lib/hecks/grammar.rb +7 -7
  197. data/lib/hecks/ir.rb +13 -13
  198. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  199. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  200. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  201. data/lib/hecks/language/oidc.json +5 -0
  202. data/lib/hecks/literal.rb +9 -9
  203. data/lib/hecks/naming.rb +25 -25
  204. data/lib/hecks/ports/access_control.rb +58 -2
  205. data/lib/hecks/ports/agent/answers.rb +83 -6
  206. data/lib/hecks/ports/agent.rb +119 -35
  207. data/lib/hecks/ports/authentication.rb +44 -4
  208. data/lib/hecks/ports/authorization.rb +53 -11
  209. data/lib/hecks/ports/clock.rb +42 -23
  210. data/lib/hecks/ports/extraction.rb +16 -0
  211. data/lib/hecks/ports/identity_assignment.rb +24 -2
  212. data/lib/hecks/ports/identity_generation.rb +17 -3
  213. data/lib/hecks/ports/identity_resolution.rb +18 -1
  214. data/lib/hecks/ports/loading.rb +4 -0
  215. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  216. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  217. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  218. data/lib/hecks/ports/persistence/execution.rb +4 -0
  219. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  220. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  221. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  222. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  224. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  225. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  226. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  241. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  242. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  247. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  248. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  249. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  252. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  253. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  254. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  255. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  256. data/lib/hecks/ports/persistence.rb +36 -1
  257. data/lib/hecks/ports/projection.rb +61 -7
  258. data/lib/hecks/ports/query/in_memory.rb +3 -3
  259. data/lib/hecks/ports/query/ordering.rb +6 -6
  260. data/lib/hecks/ports/query.rb +35 -0
  261. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  262. data/lib/hecks/projections/diagrams.rb +75 -75
  263. data/lib/hecks/projections/glossary/html.rb +3 -3
  264. data/lib/hecks/projections/glossary/markdown.rb +3 -3
  265. data/lib/hecks/projections/glossary/mermaid.rb +1 -1
  266. data/lib/hecks/projections/glossary/sections.rb +1 -1
  267. data/lib/hecks/projections/glossary/sentences.rb +5 -5
  268. data/lib/hecks/projections/glossary.rb +15 -15
  269. data/lib/hecks/projections/ir.rb +1 -1
  270. data/lib/hecks/projections/model/deviations.rb +18 -17
  271. data/lib/hecks/projections/model.rb +25 -21
  272. data/lib/hecks/projections/oidc.rb +7 -7
  273. data/lib/hecks/projections/parser_table.rb +5 -5
  274. data/lib/hecks/projections/reference.rb +3 -3
  275. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  276. data/lib/hecks/projections/shape.rb +2 -2
  277. data/lib/hecks/projections/statements.rb +11 -11
  278. data/lib/hecks/projections/vocabulary.rb +9 -9
  279. data/lib/hecks/projections.rb +5 -3
  280. data/lib/hecks/projector/cli_projector.rb +29 -29
  281. data/lib/hecks/projector/docs_projector.rb +13 -13
  282. data/lib/hecks/projector/exporter.rb +42 -21
  283. data/lib/hecks/projector/ir_projector.rb +1 -1
  284. data/lib/hecks/projector/narrate_projector.rb +11 -11
  285. data/lib/hecks/projector/target.rb +13 -13
  286. data/lib/hecks/projector.rb +15 -15
  287. data/lib/hecks/query_ir.rb +47 -47
  288. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  289. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  290. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  291. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  292. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  293. data/lib/hecks/query_specification/common/options.rb +25 -0
  294. data/lib/hecks/query_specification/field_path.rb +69 -15
  295. data/lib/hecks/query_specification/hop_path.rb +57 -20
  296. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  297. data/lib/hecks/rendering.rb +3 -3
  298. data/lib/hecks/router/namespace_installer.rb +3 -3
  299. data/lib/hecks/router.rb +1 -1
  300. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  301. data/lib/hecks/runtime/boot_gates.rb +3 -3
  302. data/lib/hecks/runtime/caller.rb +8 -8
  303. data/lib/hecks/runtime/capability_graph.rb +2 -2
  304. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  305. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
  306. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  307. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  308. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  309. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  310. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  311. data/lib/hecks/runtime/command_rules/references.rb +35 -35
  312. data/lib/hecks/runtime/command_rules.rb +3 -0
  313. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  314. data/lib/hecks/runtime/dispatcher.rb +283 -90
  315. data/lib/hecks/runtime/entity_element.rb +151 -67
  316. data/lib/hecks/runtime/entity_interpreter.rb +108 -82
  317. data/lib/hecks/runtime/errors.rb +19 -19
  318. data/lib/hecks/runtime/event.rb +6 -6
  319. data/lib/hecks/runtime/identity.rb +22 -22
  320. data/lib/hecks/runtime/instance.rb +39 -14
  321. data/lib/hecks/runtime/interpreting.rb +12 -12
  322. data/lib/hecks/runtime/invocation.rb +276 -0
  323. data/lib/hecks/runtime/loader.rb +14 -14
  324. data/lib/hecks/runtime/outbox.rb +23 -23
  325. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  326. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  327. data/lib/hecks/runtime/query_interpreter.rb +55 -64
  328. data/lib/hecks/runtime/reaction_invocation.rb +20 -20
  329. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  330. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  331. data/lib/hecks/runtime/reference_hop.rb +6 -6
  332. data/lib/hecks/runtime/refusal_wording.rb +92 -115
  333. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  334. data/lib/hecks/runtime/registry/verification.rb +36 -26
  335. data/lib/hecks/runtime/registry.rb +45 -28
  336. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  337. data/lib/hecks/runtime/routing.rb +10 -153
  338. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  339. data/lib/hecks/runtime/saga_interpreter.rb +64 -64
  340. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  341. data/lib/hecks/runtime/tenant_check.rb +9 -9
  342. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  343. data/lib/hecks/runtime/value/admission.rb +75 -30
  344. data/lib/hecks/runtime/value/coercion.rb +318 -127
  345. data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
  346. data/lib/hecks/runtime/value.rb +21 -21
  347. data/lib/hecks/runtime.rb +7 -7
  348. data/lib/hecks/storehouse.rb +64 -64
  349. data/lib/hecks/version.rb +3 -3
  350. data/lib/hecks/vocabulary.rb +205 -4
  351. data/lib/hecks.rb +13 -11
  352. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  353. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  354. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  355. metadata +22 -2
@@ -12,7 +12,7 @@ module Hecks
12
12
  # typed Value. Extended into Value, so every method here reads as
13
13
  # `Value.for`, `Value.build`, … — `self` is the Value class.
14
14
  module Coercion
15
- # THE FOUR SHAPES AN ATTRIBUTE'S VALUE CAN TAKE — named here because
15
+ # The four SHAPES an attribute's value can take — named here because
16
16
  # `for_attribute` immediately below is the one place that actually
17
17
  # branches on all four, and nowhere else in the language collects
18
18
  # them into a single closed list. `Attribute#list?`/`#optional?`
@@ -23,7 +23,7 @@ module Hecks
23
23
  # below — but the branch is exactly as real, so it gets a name here
24
24
  # too rather than staying anonymous.
25
25
  #
26
- # A SECOND RUNTIME'S KERNEL PORTS THIS METHOD BY HAND (rust/src/
26
+ # A second runtime's kernel ports this method by hand (rust/src/
27
27
  # kernel/attribute_shapes/*.rs — one file per name in this array,
28
28
  # generated into a Rust enum by bin/project_kernel_capabilities so
29
29
  # every match over it is compiler-checked exhaustive). If a fifth
@@ -49,13 +49,13 @@ module Hecks
49
49
  # recursively via `build`) with `scalar` as what's left once
50
50
  # neither of those applies (the raw value, passed through
51
51
  # unchanged).
52
- # `boundary: false` is the QUERY door (`QueryInterpreter#normalize_args`):
52
+ # `boundary: false` is the query door (`QueryInterpreter#normalize_args`):
53
53
  # a query attribute's declared type names the argument for callers and
54
54
  # generators, never a runtime shape — comparison unwraps both sides
55
55
  # itself, so a `reference: {value: ...}` offered against a `String`
56
56
  # query field is the documented allowance (see banking's own
57
57
  # `Account.OpenForCustomer`), not a C3.8 mismatch.
58
- # `argument: true` is the COMMAND/ENTITY/PORT argument door only
58
+ # `argument: true` is the command/entity/port argument door only
59
59
  # (`Interpreting#normalize_args`, every command/entity/port
60
60
  # dispatch): the one place a nil for a non-optional attribute is
61
61
  # the caller leaving a required argument empty (C3.7), absorbed
@@ -66,9 +66,9 @@ module Hecks
66
66
  # legitimate "absent is not empty" value the aggregate's own
67
67
  # attribute may hold. `QueryInterpreter#normalize_args` never
68
68
  # passes `argument: true` — a null required value-object-typed
69
- # QUERY argument is checked, and refused, entirely on its own
69
+ # query argument is checked, and refused, entirely on its own
70
70
  # side (`null_vo_argument!`, query_interpreter.rb) precisely so
71
- # it does NOT reach this default-absorbing fallback (QualityControl
71
+ # it does not reach this default-absorbing fallback (QualityControl
72
72
  # BUG#36 — a query's own null VO argument must refuse regardless
73
73
  # of any default, unlike a command's).
74
74
  def for_attribute(aggregate, attribute, value, boundary: true, argument: false)
@@ -78,12 +78,12 @@ module Hecks
78
78
  return hydrate_entity_list(aggregate, attribute, value) if attribute.list? # :list
79
79
  return value unless aggregate.respond_to?(:value_object)
80
80
 
81
- # THE SET THE ATTRIBUTE NAMES IS CHECKED WHERE THE ATTRIBUTE IS KNOWN.
81
+ # The set the attribute names is checked where the attribute is known.
82
82
  # `build` below sees only the value object, never which attribute asked
83
83
  # for it, so a command argument's `admits:` has to be read here — this
84
84
  # is the door every argument and every head field comes through.
85
85
  #
86
- # AFTER coercion, not before: a scalar arrives wrapped in whatever holder
86
+ # After coercion, not before: a scalar arrives wrapped in whatever holder
87
87
  # its type names (`{value: "append"}` for an OpName), and checking the
88
88
  # raw payload would be checking the envelope.
89
89
  value_object = value_object_for(aggregate, attribute.type)
@@ -99,12 +99,12 @@ module Hecks
99
99
  coerced
100
100
  end
101
101
 
102
- # NIL IS NOT A VALUE FOR A NON-OPTIONAL ARGUMENT (C3.7/C3.8). An
102
+ # NIL is not a value for a non-optional argument (C3.7/C3.8). An
103
103
  # `optional:` attribute and a load from the store pass nil through
104
104
  # as they always did; a command argument offered as null for a
105
105
  # required attribute is
106
106
  # refused as the field the caller left empty — a value object is
107
- # BUILT from no fields, so its first required field refuses with
107
+ # built from no fields, so its first required field refuses with
108
108
  # exactly the wording the Rust side's `from_json` gives it
109
109
  # ("Money.cents expects Integer, got nil"), and a bare scalar
110
110
  # refuses through `check_bare_primitive`'s own wording. Lists and
@@ -112,7 +112,7 @@ module Hecks
112
112
  # relationship, `validate_relationship_cardinality`'s business).
113
113
  # The `attribute.nil?`/`value.nil?` branch of `for_attribute`,
114
114
  # pulled out on its own — an unknown attribute has no shape left
115
- # to branch on, and a nil VALUE is either an ordinary absence
115
+ # to branch on, and a nil value is either an ordinary absence
116
116
  # (state assembly, hydration, a query ask) or, at the argument
117
117
  # door only, `nil_argument`'s own C3.7 refusal.
118
118
  private def nil_or_missing(aggregate, attribute, value, argument)
@@ -129,12 +129,12 @@ module Hecks
129
129
  return build(value_object, {}, aggregate) if value_object
130
130
 
131
131
  raise TypeMismatch,
132
- RefusalWording.render("TypeMismatch", "numeric_field",
133
- type: aggregate.hecks_name, field: attribute.name,
134
- expected: attribute.type, offered: "nil")
132
+ RefusalWording.render_site("TypeMismatch", "numeric_field",
133
+ type: aggregate.hecks_name, field: attribute.name,
134
+ expected: attribute.type, offered: "nil")
135
135
  end
136
136
 
137
- # A BARE PRIMITIVE IS TYPE-CHECKED AT THE BOUNDARY TOO (C3.8,
137
+ # A bare primitive is type-checked at the boundary too (C3.8,
138
138
  # docs/semantics/bluebook-semantics.md) — the same two predicates a
139
139
  # value object's own fields get, so wrong-typed caller input is a
140
140
  # TypeMismatch refusal here, never an evaluation fault later. Its
@@ -180,11 +180,51 @@ module Hecks
180
180
 
181
181
  direct_head = direct_identity_head(value, target)
182
182
  parts = paths.map { |path| identity_part(materialized, path, direct_head) }
183
- return value if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
183
+ if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
184
+ return sole_scalar_identity(value, paths) || value
185
+ end
184
186
 
185
187
  Naming.identity(parts)
186
188
  end
187
189
 
190
+ # A command may redeclare a `reference_to` field under its own,
191
+ # differently-named single-attribute value object (`attribute
192
+ # :venue, VenueHandle; sets :venue`, not `reference_to Venue` —
193
+ # QualityControl BUG#121, `qa/stress_domains/generated_revalued_
194
+ # shape`'s own `Hangar.Repoint`) instead of naming the target's
195
+ # own identity field(s) directly. `identity_part`'s path walk
196
+ # above only ever matches an incoming shape that already uses the
197
+ # target's own field names (or is the target's own identity value
198
+ # object, `direct_identity_head`) — an ad hoc wrapper around a
199
+ # bare scalar under some other field name (`VenueHandle`'s own
200
+ # `value`, not `Venue`'s own `code`) fails every path lookup, and
201
+ # used to fall through to the unresolved `return value` above,
202
+ # storing the wrapped Value. That leaves one reference field on
203
+ # one aggregate holding two different shapes depending on which
204
+ # command last wrote it — `Open`'s own bare `reference_to Venue`
205
+ # argument was never wrapped in the first place (the top guard
206
+ # clause passes a bare scalar straight through), so it always
207
+ # stored the canonical bare identity this class's own header
208
+ # comment promises ("canonical target identities, not Ruby Value
209
+ # wrappers") — while `Repoint` silently kept the wrapper instead.
210
+ #
211
+ # Unambiguous only when both sides admit exactly one scalar: the
212
+ # target names exactly one identity path (`paths.one?` — a
213
+ # compound identity has no single field either side could stand
214
+ # in for) and the offered value is itself a single-attribute
215
+ # value object (`sole_attribute` — a multi-field VO has no one
216
+ # scalar to unwrap either). `materialize_unwrapped` recurses
217
+ # through any further single-field wrapping the same way it
218
+ # already does for `Value.materialize_unwrapped`'s other callers,
219
+ # landing on the bare scalar `Open`'s own path already produces
220
+ # for the identical target field.
221
+ def sole_scalar_identity(value, paths)
222
+ return nil unless value.is_a?(self) && paths.one?
223
+ return nil unless value.value_object.sole_attribute
224
+
225
+ materialize_unwrapped(value)
226
+ end
227
+
188
228
  # Whether `value` is itself the target's own (single) identity
189
229
  # value object — pure, self-contained: reads only `value` and
190
230
  # `target`, decides nothing about any particular path.
@@ -235,14 +275,14 @@ module Hecks
235
275
 
236
276
  # Vendored addition, not (yet) upstream hecks (migration
237
277
  # plan task 5): a bare scalar auto-wraps into a single-field
238
- # value object's sole attribute -- the SAME shape
278
+ # value object's sole attribute -- the same shape
239
279
  # #from_identifier already establishes for identity coercion
240
280
  # (`build(value_object, { fields.first.name => identifier }) if
241
- # fields.size == 1`), made consistent here for MUTATION
281
+ # fields.size == 1`), made consistent here for mutation
242
282
  # coercion too. Real, corpus-wide gap: a synthesised single-
243
283
  # field wrapper (Part 3a's bare-primitive auto-synthesis, the
244
284
  # norm for a VO-typed aggregate field) is exactly the shape
245
- # #rewrap_arithmetic_result hands back a raw scalar RESULT to
285
+ # #rewrap_arithmetic_result hands back a raw scalar result to
246
286
  # -- without this, every phantom-field increment/multiply on a
247
287
  # single-field-wrapped attribute refused with "pass its fields
248
288
  # as an object, not <scalar>" the instant it tried to re-wrap
@@ -253,27 +293,27 @@ module Hecks
253
293
  return { value_object.attributes.first.name => value } if value_object.attributes.size == 1
254
294
 
255
295
  raise TypeMismatch,
256
- RefusalWording.render("TypeMismatch", "value_object_shape",
257
- name: name, type: value_object.hecks_name,
258
- offered: Rendering.describe(value))
296
+ RefusalWording.render_site("TypeMismatch", "value_object_shape",
297
+ name: name, type: value_object.hecks_name,
298
+ offered: Rendering.describe(value))
259
299
  end
260
300
 
261
301
  # `build`'s own recursive twin of `for_attribute`'s single-level
262
- # normalization — a value object's OWN composite-typed fields
302
+ # normalization — a value object's own composite-typed fields
263
303
  # (`Pizza.price_cents`, a `Price`) never otherwise pass back
264
304
  # through `fields_for`, so a bare scalar or partial Hash for one
265
- # of THOSE sails past the outer VO's own shape check (`Pizza`
305
+ # of those sails past the outer VO's own shape check (`Pizza`
266
306
  # itself has two fields, so nothing unwraps there) and lands
267
307
  # stored one field down exactly as handed in — found live: once
268
308
  # the fuzzer actually generated the bare-scalar shape
269
- # `fields_for` has accepted at the TOP level since 86727afd, a
309
+ # `fields_for` has accepted at the top level since 86727afd, a
270
310
  # nested `Price` stored as a raw Integer broke every later
271
311
  # dotted-path read (`pizza.price_cents.cents`) expecting one
272
312
  # more level of Hash.
273
313
  #
274
314
  # Stays a plain Hash, never a nested `Value` — `Value#with`'s own
275
315
  # header and `materialize_unwrapped`'s comment already depend on
276
- # a value-object-typed field of ANOTHER value object staying a
316
+ # a value-object-typed field of another value object staying a
277
317
  # plain Hash once stored, and this does not change that; it only
278
318
  # makes sure that Hash has the shape its own type declares.
279
319
  # `aggregate` is the one thing `build` didn't used to need — a
@@ -281,12 +321,12 @@ module Hecks
281
321
  # value_object(name)`, so callers with no aggregate in reach
282
322
  # (`Value#with`, always re-setting an already-scalar arithmetic
283
323
  # field) simply skip this and keep their prior behavior.
284
- # RECURSES INTO EACH NESTED FIELD'S OWN VALIDATION TOO, not only its
324
+ # Recurses into each nested field's own validation too, not only its
285
325
  # shape — found live alongside the shape bug this method's header
286
326
  # already describes: a nested `Price`/`Size` (a value-object-typed
287
- # field of ANOTHER value object, e.g. `Pizza.price_cents`,
327
+ # field of another value object, e.g. `Pizza.price_cents`,
288
328
  # `Pizza.size`) had its Hash shape normalized here but never ran
289
- # `validate!` — `build`, below, only ever validated the OUTER value
329
+ # `validate!` — `build`, below, only ever validated the outer value
290
330
  # object's own direct fields, so a negative `price_cents.cents` or an
291
331
  # out-of-`one_of` `size.value` sailed through a `Pizza`-typed command
292
332
  # argument untouched, while the exact same nested type declared as a
@@ -299,7 +339,23 @@ module Hecks
299
339
  return fields unless aggregate.respond_to?(:value_object)
300
340
 
301
341
  value_object.attributes.each do |attribute|
302
- next if attribute.list? || !fields.key?(attribute.name)
342
+ next unless fields.key?(attribute.name)
343
+
344
+ # A list member read back from the store hydrates like a top-level
345
+ # list — `list_of(Entity)` elements get their fields coerced,
346
+ # `list_of(ValueObject)` elements become Values — so a value object
347
+ # holding a list loads into the same shape the live dispatch that
348
+ # wrote it held. Found by PR A3 (every adapter through the state
349
+ # codec): chess-style `sets :positions, append: { pieces:
350
+ # state(:pieces) }` snapshots read back from Heki/Sqlite/Postgres
351
+ # (and now Memory's codec copy) with raw element hashes, so a
352
+ # `given` comparing a snapshot piece's `id` Value to a live one
353
+ # never matched after a restart. Load door only: an input list
354
+ # member is left exactly as before, refusals unchanged.
355
+ if attribute.list?
356
+ fields[attribute.name] = for_attribute(aggregate, attribute, fields[attribute.name]) if trusting_stored_state?
357
+ next
358
+ end
303
359
 
304
360
  raw = fields[attribute.name]
305
361
  next if raw.nil? || raw.is_a?(self)
@@ -322,20 +378,21 @@ module Hecks
322
378
  end
323
379
  end
324
380
 
325
- # THE FULL DOOR A VALUE OBJECT'S OWN FIELDS PASS THROUGH — shared by
381
+ # The full door a value object's own fields pass through — shared by
326
382
  # `build` (the outer value object) and `normalize_composite_fields`
327
383
  # (every nested one), so a nested `Price`/`Size` is refused exactly
328
384
  # the same way, with exactly the same wording, as the identical type
329
385
  # declared directly on a command.
330
386
  def validate!(value_object, fields)
331
387
  # C6.3 (docs/semantics/bluebook-semantics.md) — a value object is
332
- # validated on CONSTRUCTION FROM INPUT only; state read back from
388
+ # validated on construction from input only; state read back from
333
389
  # the store is trusted as it was written, so tightening an
334
390
  # invariant never makes an old record unreadable (migration is
335
391
  # the era system's job). `hydrate` — the one load door — sets
336
392
  # the flag; every input door leaves it unset.
337
393
  return if trusting_stored_state?
338
394
 
395
+ check_unknown_fields(value_object, fields)
339
396
  check_required_fields(value_object, fields)
340
397
  admit_member(value_object, fields)
341
398
  check_admitted(value_object, fields)
@@ -346,9 +403,9 @@ module Hecks
346
403
  next if Bluebook::Expression::Evaluator.call_rule(invariant, fields)
347
404
 
348
405
  raise InvariantViolation,
349
- RefusalWording.render("InvariantViolation", "value_object_invariant",
350
- name: value_object.hecks_name, description: invariant.description,
351
- offered: canonical_fields(fields))
406
+ RefusalWording.render_site("InvariantViolation", "value_object_invariant",
407
+ name: value_object.hecks_name, description: invariant.description,
408
+ offered: canonical_fields(fields))
352
409
  end
353
410
  end
354
411
 
@@ -359,10 +416,26 @@ module Hecks
359
416
  new(value_object, fields)
360
417
  end
361
418
 
419
+ # State arrives decoded or not at all (Phase 2, Track A, PR A4).
420
+ # Every persistence adapter reads through `Ports::Persistence::
421
+ # StateCodec.decode` (A3), which symbolizes every top-level key, and
422
+ # the runtime's own callers (entity elements, the remote dispatcher's
423
+ # `symbolize_names:` parse, Era's audit) build symbol-keyed state
424
+ # themselves. A String key here is an adapter or caller that skipped
425
+ # the codec, so it is refused by name rather than respelled: the
426
+ # silent `to_sym` that used to sit here is exactly what hid such a
427
+ # bypass. Always on, because it costs one `is_a?` per key, the same
428
+ # as the `to_sym` it replaced.
362
429
  def hydrate(aggregate, state)
430
+ undecoded = state.keys.grep_v(Symbol)
431
+ unless undecoded.empty?
432
+ raise WiringError,
433
+ "#{aggregate.name} state reached hydration with non-Symbol keys #{undecoded.inspect} — " \
434
+ "decode stored state through Hecks::Ports::Persistence::StateCodec.decode first"
435
+ end
436
+
363
437
  trusting_stored_state do
364
- state.each_with_object({}) do |(name, value), hydrated|
365
- key = name.to_sym
438
+ state.each_with_object({}) do |(key, value), hydrated|
366
439
  attribute = aggregate.attribute(key)
367
440
  hydrated[key] = attribute ? for_attribute(aggregate, attribute, value) : value
368
441
  end
@@ -381,9 +454,59 @@ module Hecks
381
454
 
382
455
  def trusting_stored_state? = Thread.current[TRUSTED_LOAD_KEY] == true
383
456
 
457
+ # QualityControl BUG#125 — the one narrow door `check_scalar_shapes`
458
+ # keeps open, now that a non-string scalar is otherwise refused for a
459
+ # String-typed field. `MetaValidator::Judge#send_to` — the single
460
+ # choke point every one of the language's own self-hosted dispatches
461
+ # goes through while walking a bluebook's declarations into the
462
+ # "Bluebook" meta-domain — wraps itself in this, and nothing else
463
+ # does. `Judge#appends`' generic `POSITION` handling
464
+ # (judge.rb#appends) keys purely off a field being named "position",
465
+ # the convention every other append list actually uses it for
466
+ # (ValueObject::Member, ProcessManager::Handler, ... — all really
467
+ # `Position`/Integer-typed); `Normalise`'s own `NormalisationRule`
468
+ # happens to also name its own domain field "position"
469
+ # (bluebook.bluebook), but declares it `RuleText` (String) — so the
470
+ # same walk-index substitution (`Judge#v(index)`) hands it a raw
471
+ # Integer too, on every domain's very first boot (the language
472
+ # self-judges its own grammar via `MetaValidator.fresh_runtime`'s
473
+ # fixpoint). Confirmed (QualityControl BUG#125 investigation): the
474
+ # resulting value is never read back — `normalisations` is an
475
+ # `ELSEWHERE`/`derived` field spliced straight from
476
+ # `Expression::CanonicalForm.table` (assembly/contracts.rb), so the
477
+ # judged record holding the Integer is discarded whole — this is a
478
+ # walk-index/domain-field name collision inside `Judge#appends`, not
479
+ # a genuine semantic need for `position` to arrive numeric. Fixing
480
+ # that collision at its own root is a separate, larger change to
481
+ # self-hosted bootstrap mechanics that every domain's boot depends
482
+ # on; this flag only ever loosens scalar-shape checking for the
483
+ # meta-grammar's own value objects (RuleText, BluebookName, Position,
484
+ # …) that Judge itself constructs while walking a bluebook's
485
+ # declarations — never for a real domain's own declared value
486
+ # objects (PieceId, Money, …), which Judge never dispatches commands
487
+ # against. `offer` (judge.rb) already converts a `TypeMismatch` here
488
+ # into a recorded refusal rather than letting it propagate, but
489
+ # `MetaValidator.call` raises the instant `refusals` is non-empty
490
+ # (meta_validator.rb) — so, unexempted, this would fail every
491
+ # domain's boot, not just the language's own bootstrap. Composite
492
+ # shapes (Array/Hash) stay refused unconditionally, bootstrap or not
493
+ # — nothing Judge does ever legitimately needs those for a scalar
494
+ # field.
495
+ BOOTSTRAP_KEY = :hecks_judge_bootstrapping
496
+
497
+ def judge_bootstrapping
498
+ previous = Thread.current[BOOTSTRAP_KEY]
499
+ Thread.current[BOOTSTRAP_KEY] = true
500
+ yield
501
+ ensure
502
+ Thread.current[BOOTSTRAP_KEY] = previous
503
+ end
504
+
505
+ def judge_bootstrapping? = Thread.current[BOOTSTRAP_KEY] == true
506
+
384
507
  # `Value.identifier` used to live here: hand it a one-field value object
385
508
  # and it opened it, so `identified_by :number` could pass for an identity
386
- # and the runtime would guess which field was meant. THAT GUESS IS GONE.
509
+ # and the runtime would guess which field was meant. That guess is gone.
387
510
  # An identity names its field — `identified_by :number` — and the
388
511
  # path is what reaches the scalar. A declaration that names no field is
389
512
  # refused when the bluebook loads, so nothing has to be unwrapped later.
@@ -392,12 +515,12 @@ module Hecks
392
515
  # into a column or a message, where there is no path to consult.
393
516
 
394
517
  # `Value.reference_id` lived here, opening a reference to find the id
395
- # inside it. A reference IS the id now — refused at the payload gate if it
518
+ # inside it. A reference is the id now — refused at the payload gate if it
396
519
  # arrives as anything else — so there is nothing left to open. The comment
397
- # it carried said retiring it meant changing how references are STORED ;
520
+ # it carried said retiring it meant changing how references are stored ;
398
521
  # that is what happened.
399
522
 
400
- # A REFERENCE IS AN ID, SO ANYTHING ELSE IS NOT ONE.
523
+ # A reference is an ID, so anything else is not one.
401
524
  #
402
525
  # Nothing coerces a reference — `for_attribute` misses on
403
526
  # "Reference<Account>", which is no value object's name, and hands the
@@ -406,10 +529,10 @@ module Hecks
406
529
  # refused, so whatever the first caller wrote became the shape.
407
530
  #
408
531
  # This is that place. It sits at the payload gate rather than inside
409
- # coercion because the sentence names the COMMAND, and `for_attribute`
532
+ # coercion because the sentence names the command, and `for_attribute`
410
533
  # never learns which command it is serving.
411
534
  #
412
- # WIDENED PAST THE OBJECT SHAPE BY BUG#27 (QualityControl ledger,
535
+ # Widened past the object shape by BUG#27 (QualityControl ledger,
413
536
  # found live on `qa/stress_domains/referral_chain`'s `Member.Join`/
414
537
  # `Referral.Issue`). A bare Boolean, Array, or `null` used to sail
415
538
  # through here untouched — nothing but Hash/Value ever refused —
@@ -426,23 +549,23 @@ module Hecks
426
549
  # (`normalize_args`, `Vocabulary::AggregateDispatchOrder`/
427
550
  # `EntityDispatchOrder`), strictly before `resolve_references` ever
428
551
  # receives a value to look up — so the two engines now agree on
429
- # BOTH kind and order, not just kind.
552
+ # both kind and order, not just kind.
430
553
  #
431
- # `nil` STAYS LEGITIMATE for a `reference_to ..., optional: true`
554
+ # `nil` stays legitimate for a `reference_to ..., optional: true`
432
555
  # argument (`Improvement.Open`'s own `reference_to Angle, optional:
433
556
  # true` — `qa/bluebook/quality_control.bluebook`): the caller
434
557
  # genuinely may have nothing to name yet, and `nil_argument`
435
558
  # (interpreting.rb) already passes an optional reference's `nil`
436
- # through untouched. A REQUIRED reference offered as `null` is a
437
- # caller leaving a required argument empty in every OTHER sense
438
- # this runtime already refuses (C3.7) — refusing it HERE, rather
559
+ # through untouched. A required reference offered as `null` is a
560
+ # caller leaving a required argument empty in every other sense
561
+ # this runtime already refuses (C3.7) — refusing it here, rather
439
562
  # than falling through to `resolve_references`' own nil-skip and
440
563
  # then whatever the command's `given` happens to say, is what
441
564
  # actually names the empty argument instead of something else.
442
565
  #
443
566
  # A `has_many` reference's own Array shape is still never refused
444
- # by ITS wrapper (`Array(value).find { ... }` only inspects the
445
- # list's ELEMENTS) — a reference is never a scalar list-of-lists
567
+ # by its wrapper (`Array(value).find { ... }` only inspects the
568
+ # list's elements) — a reference is never a scalar list-of-lists
446
569
  # today, and inventing a rule for a shape the language cannot
447
570
  # declare is how decoration gets written. `reference_list` (below)
448
571
  # already owns "not an Array at all" for that case.
@@ -460,13 +583,13 @@ module Hecks
460
583
  end
461
584
 
462
585
  raise TypeMismatch,
463
- RefusalWording.render("TypeMismatch", "reference_wrong_shape",
464
- command: command.hecks_name, attribute: attribute.name,
465
- offered: reference_shape_description(offered),
466
- known_by: known_by(attribute))
586
+ RefusalWording.render_site("TypeMismatch", "reference_wrong_shape",
587
+ command: command.hecks_name, attribute: attribute.name,
588
+ offered: reference_shape_description(offered),
589
+ known_by: known_by(attribute))
467
590
  end
468
591
 
469
- # "an object" for the Hash/Value shape — the ORIGINAL wording this
592
+ # "an object" for the Hash/Value shape — the original wording this
470
593
  # method always gave, pinned byte for byte by
471
594
  # `spec/runtime/reference_shape_spec.rb`, kept unchanged by BUG#27's
472
595
  # widening. `Rendering.describe` for everything else: `true`,
@@ -485,8 +608,8 @@ module Hecks
485
608
  # article-choosing rule. Silent when the target is another chapter's,
486
609
  # where this runtime cannot see what it is known by.
487
610
  #
488
- # EVERY HEAD, because a caller has to pass every one. This read
489
- # `identified_by`, which is the SINGLE head and is nil the moment an
611
+ # Every head, because a caller has to pass every one. This read
612
+ # `identified_by`, which is the single head and is nil the moment an
490
613
  # identity has two parts — so a composite target fell through the guard
491
614
  # and the refusal went silent exactly where it had the most to say. A
492
615
  # single-path target reads as it always did.
@@ -503,7 +626,7 @@ module Hecks
503
626
  fields = value.to_h
504
627
  return fields.values.first if fields.size == 1
505
628
 
506
- raise TypeMismatch, RefusalWording.render("TypeMismatch", "multi_field_scalar", type: value.type_name)
629
+ raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "multi_field_scalar", type: value.type_name)
507
630
  end
508
631
 
509
632
  def from_identifier(aggregate, attribute, identifier)
@@ -516,29 +639,29 @@ module Hecks
516
639
  return build(value_object, { field.name => coerce_identifier(field, identifier) })
517
640
  end
518
641
 
519
- raise TypeMismatch, RefusalWording.render("TypeMismatch", "composite_identity", type: value_object.hecks_name)
642
+ raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "composite_identity", type: value_object.hecks_name)
520
643
  end
521
644
 
522
645
  # Vendored fix, not (yet) upstream hecks (migration plan
523
- # task 9): `identifier` here is always the DERIVED IDENTITY
524
- # STRING -- `Identity.of`/`Identity.from` intentionally return
646
+ # task 9): `identifier` here is always the derived identity
647
+ # string -- `Identity.of`/`Identity.from` intentionally return
525
648
  # one (correct for naming a repository key), and
526
649
  # `Runtime::Instance#materialize_identity!` calls `from_identifier`
527
650
  # with exactly that string on every fresh hydration -- but when
528
- # the identity field's OWN declared type is Integer/Float (not
651
+ # the identity field's own declared type is Integer/Float (not
529
652
  # the overwhelmingly common String), seeding it straight from
530
653
  # that string round-trips a correctly-derived identity back in
531
- # as the WRONG Ruby type -- and #build's own
654
+ # as the wrong Ruby type -- and #build's own
532
655
  # `check_numeric_fields` (added specifically to catch a genuine
533
- # CALLER mismatch) then refused the runtime's OWN internal
656
+ # caller mismatch) then refused the runtime's own internal
534
657
  # identity seed instead, on every dispatch, valid input or not.
535
658
  #
536
- # Reuses THIS SAME FILE's own `NUMERIC` table (declared-type ->
659
+ # Reuses this same file's own `NUMERIC` table (declared-type ->
537
660
  # expected-Ruby-class, already read by `check_numeric_fields`)
538
- # to decide WHICH declared types need converting, and
661
+ # to decide which declared types need converting, and
539
662
  # Kernel#Integer/#Float to do the converting. A genuinely
540
663
  # malformed identifier (should never happen, since an identity
541
- # is always derived FROM a correctly-typed field in the first
664
+ # is always derived from a correctly-typed field in the first
542
665
  # place, but this stays defensive rather than assume it) passes
543
666
  # back unconverted, and `check_numeric_fields` refuses it
544
667
  # exactly as it always has -- preserving its real job of
@@ -560,24 +683,27 @@ module Hecks
560
683
  JSON.generate(fields.sort_by { |name, _| name.to_s }.to_h)
561
684
  end
562
685
 
563
- # A field declared Integer or Float must ARRIVE as one.
686
+ # A field declared Integer or Float must arrive as one.
564
687
  #
565
688
  # Without this a String sails into a numeric field and the failure surfaces
566
689
  # later, inside a predicate, as `positive? expects a number, got "three"` —
567
- # an EvaluationError, which is NOT a domain refusal. So the runtime broke
690
+ # an EvaluationError, which is not a domain refusal. So the runtime broke
568
691
  # where the domain should have said no, and the run contract recorded the
569
692
  # crash beside genuine refusals as though the domain had judged it.
570
693
  #
571
694
  # C3.8 — the boundary check for an attribute whose type is a bare
572
695
  # primitive rather than a value object: `Integer`/`Float` by exact
573
- # numeric class (`NUMERIC`), `String`/booleans by rejecting a
574
- # composite shape (`COMPOSITE_SHAPES`) — identical to what
575
- # `check_numeric_fields`/`check_scalar_shapes` hold a value
576
- # object's own fields to, worded by the same template with the
577
- # owning construct as `type`. A `String` field still admits any
578
- # other scalar (the self-hosted grammar's own bootstrap relies on
579
- # it see `check_scalar_shapes`' comment); the Rust boundary is
580
- # stricter there, recorded in the clause.
696
+ # numeric class (`NUMERIC`), `String`/booleans by rejecting only a
697
+ # composite shape (`COMPOSITE_SHAPES`) — not the same as
698
+ # `check_scalar_shapes` holds a value object's own `String` field to
699
+ # any more (QualityControl BUG#125 tightened that one to also refuse
700
+ # a non-string scalar; a bare `String` argument here still admits
701
+ # any other scalar, left exactly as it was a bare-primitive
702
+ # attribute was never part of BUG#125's own investigation or fix,
703
+ # and whether it needs the same tightening, and against what real
704
+ # Judge dependency if any, is still open); worded by the same
705
+ # template with the owning construct as `type`. The Rust boundary is
706
+ # stricter there too, recorded in the clause.
581
707
  private def check_bare_primitive(owner, attribute, value)
582
708
  type = attribute.type.to_s
583
709
  expected = NUMERIC[type]
@@ -590,9 +716,9 @@ module Hecks
590
716
  end
591
717
  if mistyped
592
718
  raise TypeMismatch,
593
- RefusalWording.render("TypeMismatch", "numeric_field",
594
- type: owner.hecks_name, field: attribute.name,
595
- expected: type, offered: Rendering.describe(value))
719
+ RefusalWording.render_site("TypeMismatch", "numeric_field",
720
+ type: owner.hecks_name, field: attribute.name,
721
+ expected: type, offered: Rendering.describe(value))
596
722
  end
597
723
 
598
724
  check_numeric_bounds(owner.hecks_name, attribute.name, value)
@@ -607,40 +733,90 @@ module Hecks
607
733
  private def check_numeric_bounds(type_name, field_name, given)
608
734
  if given.is_a?(Integer) && !INT64_RANGE.cover?(given)
609
735
  raise TypeMismatch,
610
- RefusalWording.render("TypeMismatch", "integer_range",
611
- type: type_name, field: field_name, offered: Rendering.describe(given))
736
+ RefusalWording.render_site("TypeMismatch", "integer_range",
737
+ type: type_name, field: field_name, offered: Rendering.describe(given))
612
738
  end
613
739
  return unless given.is_a?(Float) && !given.finite?
614
740
 
615
741
  raise TypeMismatch,
616
- RefusalWording.render("TypeMismatch", "non_finite_field",
617
- type: type_name, field: field_name, offered: Rendering.describe(given))
742
+ RefusalWording.render_site("TypeMismatch", "non_finite_field",
743
+ type: type_name, field: field_name, offered: Rendering.describe(given))
618
744
  end
619
745
 
620
- # C3.7A VALUE OBJECT IS A TYPED FIELD PRODUCT: every non-optional
746
+ # QualityControl BUG#41 a value object refuses a key it does not
747
+ # declare, the same way a command's own payload does
748
+ # (`CommandInterpreter::ArgumentGate#refuse_unknown_arguments`,
749
+ # argument_gate.rb) — reusing that method's exact refusal wording
750
+ # (`UnknownArgument unknown_args`, refusal_wording.rb:
751
+ # "{command} does not declare {unknown} — it takes {declared}")
752
+ # rather than inventing a new template, because every generated
753
+ # Rust value-object `from_json` already renders this refusal
754
+ # through that identical site: `rust/project/json_codec.rb`'s
755
+ # `emit_unknown_argument_check` (mirrored byte-for-byte in
756
+ # `rust/codegen/src/json_codec.rs`) emits `v.unknown_keys(&[...])`
757
+ # and the same "{name} does not declare {unknown} — it takes
758
+ # {declared}" format string for every value object's own
759
+ # `from_json` — `GameLabel::from_json`
760
+ # (rust/src/generated/chess/game.rs) is simply the first case
761
+ # this gap was reproduced against.
762
+ #
763
+ # Ruby never had an equivalent check anywhere in this `validate!`
764
+ # door before now — `fields[attribute.name]` reads only the
765
+ # declared attributes, so any other key a caller's Hash carried
766
+ # was silently ignored. `fields` here only ever holds what a
767
+ # caller (or `for_attribute`'s own recursive coercion) offered
768
+ # for this value object — built by `fields_for`'s plain
769
+ # key-symbolizing (never a Hash the runtime pads with bookkeeping
770
+ # keys of its own; confirmed by reading every call site that
771
+ # reaches `validate!`) — so there is nothing legitimate here to
772
+ # exempt.
773
+ #
774
+ # Checked first, before `check_required_fields` and everything
775
+ # after it — matching Rust's own `from_json`, which checks
776
+ # `unknown_keys` before reading a single declared field. So a
777
+ # Hash offering both an unrecognized key and a missing required
778
+ # one (BUG#41's own second demonstration case: `label: {extra:
779
+ # "bogus"}` — unknown and missing `value`) refuses the same
780
+ # UnknownArgument on both engines, not two different refusal
781
+ # kinds for one malformed call.
782
+ private def check_unknown_fields(value_object, fields)
783
+ known = value_object.attributes.map { |attribute| attribute.name.to_sym }
784
+ unknown = (fields.keys.map(&:to_sym) - known).sort
785
+ return if unknown.empty?
786
+
787
+ declared = value_object.attributes.map(&:name)
788
+ raise UnknownArgument,
789
+ RefusalWording.render_site("UnknownArgument", "unknown_args",
790
+ command: value_object.hecks_name, unknown: unknown,
791
+ declared: declared)
792
+ end
793
+
794
+ # C3.7 — a value object is a typed field product: every non-optional
621
795
  # field arrives, or construction refuses. A missing field and a null
622
796
  # one are the same absence (`fields[name]` reads nil for both), worded
623
797
  # as the type mismatch it is — "{type}.{field} expects {expected}, got
624
798
  # nil" — the identical string the Rust side's generated `from_json`
625
799
  # gives the same input, so the corpus can pin it on both. Checked
626
- # FIRST: an invariant reading a field that never arrived is exactly
627
- # the thing that used to answer "invariant violated" (or nothing at
628
- # all `ToppingName`'s `{value: null}` used to be accepted and
629
- # stored). A `default:` has already been filled in by `apply_defaults`;
630
- # a list field's absence is an empty list, never a refusal.
800
+ # first among the field-content checks (after `check_unknown_fields`'s
801
+ # own structural gate above, BUG#41): an invariant reading a field
802
+ # that never arrived is exactly the thing that used to answer
803
+ # "invariant violated" (or nothing at all `ToppingName`'s
804
+ # `{value: null}` used to be accepted and stored). A `default:` has
805
+ # already been filled in by `apply_defaults`; a list field's absence
806
+ # is an empty list, never a refusal.
631
807
  private def check_required_fields(value_object, fields)
632
808
  value_object.attributes.each do |attribute|
633
809
  next if attribute.optional? || attribute.list?
634
810
  next unless fields[attribute.name].nil?
635
811
 
636
812
  raise TypeMismatch,
637
- RefusalWording.render("TypeMismatch", "numeric_field",
638
- type: value_object.hecks_name, field: attribute.name,
639
- expected: attribute.type, offered: "nil")
813
+ RefusalWording.render_site("TypeMismatch", "numeric_field",
814
+ type: value_object.hecks_name, field: attribute.name,
815
+ expected: attribute.type, offered: "nil")
640
816
  end
641
817
  end
642
818
 
643
- # Checked BEFORE invariants, because an invariant reading a mistyped field
819
+ # Checked before invariants, because an invariant reading a mistyped field
644
820
  # is exactly the thing that used to explode.
645
821
  NUMERIC = { "Integer" => Integer, "Float" => Numeric }.freeze
646
822
  private def check_numeric_fields(value_object, fields)
@@ -653,9 +829,9 @@ module Hecks
653
829
 
654
830
  unless given.is_a?(expected)
655
831
  raise TypeMismatch,
656
- RefusalWording.render("TypeMismatch", "numeric_field",
657
- type: value_object.hecks_name, field: attribute.name,
658
- expected: attribute.type, offered: Rendering.describe(given))
832
+ RefusalWording.render_site("TypeMismatch", "numeric_field",
833
+ type: value_object.hecks_name, field: attribute.name,
834
+ expected: attribute.type, offered: Rendering.describe(given))
659
835
  end
660
836
 
661
837
  # PRD 05 (numeric-boundary-coverage) — `given.is_a?(expected)`
@@ -673,8 +849,8 @@ module Hecks
673
849
  # storage, where `JSON.generate`/`#to_json` raises
674
850
  # `JSON::GeneratorError: NaN/Infinity not allowed in JSON` the
675
851
  # moment anything tries to persist or replay it — again a raw
676
- # crash, not a refusal. `-0.0` is deliberately NOT refused
677
- # here: it IS finite, round-trips through JSON as `-0.0`
852
+ # crash, not a refusal. `-0.0` is deliberately not refused
853
+ # here: it is finite, round-trips through JSON as `-0.0`
678
854
  # cleanly (confirmed empirically), and is a legitimate,
679
855
  # meaningful float value (a signed zero), not a corruption
680
856
  # risk — only NaN and +/-Infinity are.
@@ -683,45 +859,60 @@ module Hecks
683
859
  end
684
860
 
685
861
  # A field declared `String` (or a boolean) must not arrive as a
686
- # COMPOSITE — an Array or a Hash (or a nested Value) standing in for
687
- # what has to be a leaf scalar.
862
+ # composite — an Array or a Hash (or a nested Value) standing in for
863
+ # what has to be a leaf scalar. A `String` field, further, must not
864
+ # arrive as any other non-composite scalar either (Integer, Float,
865
+ # true/false) — QualityControl BUG#125, matching Rust's generated
866
+ # `from_json`, which requires a JSON string node for a String-typed
867
+ # field unconditionally and refuses anything else, including a JSON
868
+ # number or boolean. Ruby used to tolerate exactly that (found live:
869
+ # `Chess::Piece.Capture`'s `PieceId`, String-typed, offered a bignum
870
+ # `id` — Ruby let it pass and failed later on an unrelated field,
871
+ # Rust refused on `id` itself, immediately) — no longer, except
872
+ # inside `judge_bootstrapping?` (above), the one caller genuinely
873
+ # relying on the old leniency; see that flag's own comment for why.
688
874
  #
689
- # Deliberately laxer than `check_numeric_fields` above : it does not
690
- # enforce the exact Ruby class, only that the shape isn't a collection.
691
- # `Judge#v` the language's own self-hosted grammar validation
692
- # hands a String-typed field (`Normalise`'s `position`, a `RuleText`)
693
- # a raw Integer walk-index on purpose, on every boot, and that has
694
- # always been tolerated ; a full String-vs-Integer check here would
695
- # refuse the runtime's own bootstrap. But no scalar field, of any
696
- # declared type, can ever legitimately be handed an Array or a Hash —
697
- # that shape is always wrong, and always was: `InvalidValueGenerator#
698
- # array_for_scalar`'s own corruption is deliberately built to be
699
- # REFUSED (see that file's header), and until this check existed it
700
- # sailed straight through for a String/boolean field the way it never
701
- # could for an Integer/Float one (`check_numeric_fields` above already
702
- # catches an Array offered for those). Found live via bin/fuzz, seed
703
- # 17 on the fixtures domain : an Array standing in for a single-field
704
- # identity's declared `String`, `.to_s`'d into a record id downstream.
875
+ # `TrueClass`/`FalseClass` stay laxer than `check_numeric_fields`
876
+ # above: for those two, this still only enforces that the shape
877
+ # isn't a collection, not the exact Ruby class narrower than the
878
+ # `String` case above because BUG#125 investigated and fixed String
879
+ # specifically; a boolean field's own scalar-shape tolerance is a
880
+ # separate, uninvestigated question left exactly as it was. No
881
+ # scalar field, of any declared type, can ever legitimately be
882
+ # handed an Array or a Hash — that shape is always wrong, and always
883
+ # was: `InvalidValueGenerator#array_for_scalar`'s own corruption is
884
+ # deliberately built to be refused (see that file's header), and
885
+ # until this check existed it sailed straight through for a
886
+ # String/boolean field the way it never could for an Integer/Float
887
+ # one (`check_numeric_fields` above already catches an Array offered
888
+ # for those). Found live via bin/fuzz, seed 17 on the fixtures
889
+ # domain : an Array standing in for a single-field identity's
890
+ # declared `String`, `.to_s`'d into a record id downstream.
705
891
  COMPOSITE_SHAPES = [Array, ::Hash].freeze
706
892
  NON_NUMERIC_SCALARS = %w[String TrueClass FalseClass].freeze
707
893
  private def check_scalar_shapes(value_object, fields)
708
894
  value_object.attributes.each do |attribute|
709
- next unless NON_NUMERIC_SCALARS.include?(attribute.type.to_s)
895
+ type = attribute.type.to_s
896
+ next unless NON_NUMERIC_SCALARS.include?(type)
710
897
 
711
898
  given = fields[attribute.name]
712
- next if given.nil? || COMPOSITE_SHAPES.none? { |shape| given.is_a?(shape) }
899
+ next if given.nil?
900
+
901
+ composite = COMPOSITE_SHAPES.any? { |shape| given.is_a?(shape) }
902
+ non_string_scalar = type == "String" && !composite && !given.is_a?(String) && !judge_bootstrapping?
903
+ next unless composite || non_string_scalar
713
904
 
714
905
  raise TypeMismatch,
715
- RefusalWording.render("TypeMismatch", "numeric_field",
716
- type: value_object.hecks_name, field: attribute.name,
717
- expected: attribute.type, offered: Rendering.describe(given))
906
+ RefusalWording.render_site("TypeMismatch", "numeric_field",
907
+ type: value_object.hecks_name, field: attribute.name,
908
+ expected: attribute.type, offered: Rendering.describe(given))
718
909
  end
719
910
  end
720
911
 
721
- # A field declared with a PATTERN must match it.
912
+ # A field declared with a pattern must match it.
722
913
  #
723
914
  # Beside check_numeric_fields and for the same reason : a value that does
724
- # not look like what it claims to be is the DOMAIN saying no, and it should
915
+ # not look like what it claims to be is the domain saying no, and it should
725
916
  # say so here rather than let the wrong shape travel on and surface as a
726
917
  # broken predicate later.
727
918
  #
@@ -738,9 +929,9 @@ module Hecks
738
929
  next if given.is_a?(String) && Regexp.new(pattern).match?(given)
739
930
 
740
931
  raise TypeMismatch,
741
- RefusalWording.render("TypeMismatch", "pattern_mismatch",
742
- type: value_object.hecks_name, field: attribute.name,
743
- pattern: pattern, offered: Rendering.describe(given))
932
+ RefusalWording.render_site("TypeMismatch", "pattern_mismatch",
933
+ type: value_object.hecks_name, field: attribute.name,
934
+ pattern: pattern, offered: Rendering.describe(given))
744
935
  end
745
936
  end
746
937
  end