hecks 1.2.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 (358) 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 +60 -2
  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/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -2,17 +2,17 @@ require "time"
2
2
 
3
3
  module Hecks
4
4
  module Adapters
5
- # THE `authorization` PORT, FULFILLED BY GOVERNANCE — same registry,
5
+ # **The `authorization` port, fulfilled by governance** — same registry,
6
6
  # same boot, so "ask Governance" is a dispatch against records
7
7
  # already sitting in the store this adapter is handed, not a bridge
8
8
  # to a second runtime. `Runtime::Dispatcher.new(registry)` is cheap
9
9
  # to build fresh per call (`Registry#capability_graph`'s own
10
10
  # neighbor, `#repository`, does the same kind of on-demand build) —
11
11
  # nothing here holds one across calls, so there is no boot-order
12
- # dependency on WHEN Governance's bluebook loads relative to this
12
+ # dependency on when Governance's bluebook loads relative to this
13
13
  # adapter, only that it has by the time `holds_role?` is called.
14
14
  #
15
- # AN ACTIVE ASSIGNMENT, not merely a historical one : `RoleAssignment`
15
+ # An active assignment, not merely a historical one : `RoleAssignment`
16
16
  # answers with every assignment an actor has ever held
17
17
  # (`AssignmentsForActor`'s own description — "currently or
18
18
  # historically") and leaves `ends_at` for the caller to read, the
@@ -21,7 +21,7 @@ module Hecks
21
21
  module GovernanceAuthorization
22
22
  module_function
23
23
 
24
- # `as_of` and `scope` are BOTH optional, same opt-in shape
24
+ # `as_of` and `scope` are both optional, same opt-in shape
25
25
  # `refuse_role_mismatch` already gives `actor_id` itself — an
26
26
  # unbound `as_of` skips the `starts_at` check and an unbound
27
27
  # `scope` skips the `scope` check, exactly the behavior before
@@ -31,7 +31,7 @@ module Hecks
31
31
  # for why the dispatch path must not consult the clock itself.
32
32
  def holds_role?(registry, actor_id:, role:, as_of: nil, scope: nil)
33
33
  rows = Runtime::Dispatcher.new(registry).query(
34
- "Governance::RoleAssignment.AssignmentsForActor",
34
+ provided_verb(registry, :assignments),
35
35
  actor_id: { value: actor_id.to_s }
36
36
  )
37
37
 
@@ -43,20 +43,20 @@ module Hecks
43
43
  end
44
44
  end
45
45
 
46
- # `scope` UNCHECKED WHEN NOT STATED, same as every other opt-in
46
+ # `scope` unchecked when not stated, same as every other opt-in
47
47
  # field here — a caller that never says which scope it is acting
48
48
  # in gets the pre-scope behavior: any live assignment for the role
49
49
  # authorizes, everywhere. A caller that does state one only
50
- # authorizes against an assignment granted for THAT scope.
50
+ # authorizes against an assignment granted for that scope.
51
51
  def in_scope?(row, scope)
52
52
  scope.nil? || row[:scope][:value] == scope.to_s
53
53
  end
54
54
 
55
- # `starts_at` IS A FREE-TEXT STRING in the bluebook (`Timestamp`'s
55
+ # `starts_at` is a free-text string in the bluebook (`Timestamp`'s
56
56
  # only invariant is "present", not any particular format) — parsed
57
57
  # here with `Time.parse` rather than compared lexically, since
58
58
  # nothing guarantees every caller writes it zero-padded ISO 8601.
59
- # FAILS CLOSED : a `starts_at` that does not parse is treated as
59
+ # Fails closed : a `starts_at` that does not parse is treated as
60
60
  # not-yet-started rather than silently ignored, the same direction
61
61
  # every other check in this method already fails.
62
62
  def started?(row, as_of)
@@ -67,21 +67,21 @@ module Hecks
67
67
  false
68
68
  end
69
69
 
70
- # THE OTHER HALF — may role X act as role Y. `RoleTransition.Allowed`
70
+ # **The other half** — may role X act as role Y. `RoleTransition.Allowed`
71
71
  # is identified by the exact pair, so at most one row ever comes
72
72
  # back ; still read as `.any?` rather than trusting that structurally,
73
73
  # the same defensiveness `holds_role?` already has to have anyway
74
74
  # since `AssignmentsForActor` can return several.
75
75
  def authorized_as?(registry, from_role:, to_role:)
76
76
  rows = Runtime::Dispatcher.new(registry).query(
77
- "Governance::RoleTransition.Allowed",
77
+ provided_verb(registry, :transitions),
78
78
  from_role: { value: from_role.to_s }, to_role: { value: to_role.to_s }
79
79
  )
80
80
 
81
81
  rows.any? { |row| row[:ends_at].nil? }
82
82
  end
83
83
 
84
- # THE ROLE ITSELF, not just a yes/no about one — the same
84
+ # The role itself, not just a yes/no about one — the same
85
85
  # `AssignmentsForActor` query `holds_role?` runs, just returning
86
86
  # the live (non-revoked) row's `role_name` instead of comparing it
87
87
  # against a caller-supplied guess. `nil` for no live assignment at
@@ -89,13 +89,31 @@ module Hecks
89
89
  # a default) is domain-specific and does not belong here.
90
90
  def live_role_for(registry, actor_id:)
91
91
  rows = Runtime::Dispatcher.new(registry).query(
92
- "Governance::RoleAssignment.AssignmentsForActor",
92
+ provided_verb(registry, :assignments),
93
93
  actor_id: { value: actor_id.to_s }
94
94
  )
95
95
 
96
96
  live = rows.find { |row| row[:ends_at].nil? }
97
97
  live && live[:role_name][:value]
98
98
  end
99
+
100
+ # The verb, read from the provider's own declaration — `provides
101
+ # "authorization", assignments: ..., transitions: ...` on whichever
102
+ # loaded chapter declares it (Governance's, in every boot today).
103
+ # Exactly one provider, the same "the runtime will not choose for
104
+ # you" rule `Ports::Authorization.adapter` applies to adapters.
105
+ def provided_verb(registry, key)
106
+ providers = registry.authorization_providers
107
+ unless providers.size == 1
108
+ raise Runtime::WiringError,
109
+ "#{providers.size} loaded chapters provide \"authorization\"" \
110
+ "#{" (#{providers.map(&:name).sort.join(', ')})" unless providers.empty?} — " \
111
+ "a role lookup needs exactly one (framework members declaring it: " \
112
+ "#{Framework.providers_of(Bluebook::Capabilities::AUTHORIZATION).join(', ')})"
113
+ end
114
+
115
+ providers.first.provided_verb(Bluebook::Capabilities::AUTHORIZATION, key)
116
+ end
99
117
  end
100
118
  end
101
119
  end
@@ -11,14 +11,71 @@ module Hecks
11
11
 
12
12
  File.readlines(@journal_path, chomp: true).reject(&:empty?).map do |line|
13
13
  value = JSON.parse(line)
14
- state = value["state"]&.transform_keys(&:to_sym)
14
+ state = Ports::Persistence::StateCodec.decode(@aggregate, value["state"])
15
15
  Ports::Persistence::Entry.new(operation: value.fetch("operation"), id: value.fetch("id"), state: state,
16
16
  mirrors: value["mirrors"])
17
17
  end
18
18
  end
19
19
 
20
+ # An explicit, opt-in maintenance operation — never run
21
+ # automatically after an ordinary save/delete. Heki's journal is
22
+ # not a disposable write-ahead log: it is this adapter's own
23
+ # answer to `entries`, and `entries` is a real port contract
24
+ # (`Ports::Persistence::AppendOnly`'s own required-methods list)
25
+ # read in full, forever, by `Ports::Projection::Worker#catch_up!`
26
+ # and `Registry#projection_current?` to catch a projection up to
27
+ # its authoritative source, and by `bin/history` to show "every
28
+ # journal entry a domain's append-only adapters hold" — the same
29
+ # contract Postgres/Sqlite/D1 uphold by way of a journal table
30
+ # that is never pruned. A real example (`examples/banking`,
31
+ # `persisted_by("Heki")` + `projected_by("SqliteProjection")`)
32
+ # depends on this today. Compacting throws that full history away
33
+ # for whatever happened before the call — correct only for an
34
+ # aggregate nothing ever projects from; callers (`bin/
35
+ # heki_compact`) are responsible for confirming that first.
36
+ #
37
+ # Crash-safety ordering: `write` below is the exact same
38
+ # temp-file, fsync, atomic-rename sequence `save`/`delete`
39
+ # already use — the snapshot it produces is confirmed durably on
40
+ # disk before this method ever touches the journal. Only once
41
+ # that succeeds does `truncate_journal!` run. A crash between the
42
+ # two leaves old (now fully redundant) journal lines in place;
43
+ # replaying them again over the fresh snapshot on the next boot
44
+ # is idempotent — the same value gets set again, never a wrong
45
+ # one — so nothing is lost, just a little wasted replay work
46
+ # once. A crash before `write` completes leaves the journal
47
+ # fully intact and the prior snapshot untouched, exactly today's
48
+ # existing crash-recovery guarantee.
49
+ def compact!
50
+ with_lock do
51
+ current = replay_journal(read_snapshot)
52
+ write(current)
53
+ truncate_journal!
54
+ @store = current
55
+ end
56
+ end
57
+
20
58
  private
21
59
 
60
+ def truncate_journal!
61
+ return unless File.exist?(@journal_path)
62
+
63
+ # A single `truncate(0)` syscall on an already-open file
64
+ # descriptor — the file's length changes atomically at the
65
+ # filesystem level, so there is no "half truncated" state to
66
+ # observe even under a crash mid-call. `fsync` below makes
67
+ # that change durable before this method returns; without it
68
+ # a crash could still leave the old (harmless-to-replay)
69
+ # content on disk after a normal return, which is fine per
70
+ # the crash-safety note above, but the durable case is the
71
+ # one actually worth returning success for.
72
+ File.open(@journal_path, "r+b") do |file|
73
+ file.truncate(0)
74
+ file.flush
75
+ file.fsync
76
+ end
77
+ end
78
+
22
79
  def replay_journal(records)
23
80
  return records unless File.exist?(@journal_path)
24
81
 
@@ -38,7 +95,8 @@ module Hecks
38
95
  end
39
96
 
40
97
  def append_entry(operation, id, state)
41
- line = "#{JSON.generate(operation: operation, id: id.to_s, state: state, mirrors: @entry_mirrors)}\n"
98
+ encoded = Ports::Persistence::StateCodec.encode(@aggregate, state)
99
+ line = "#{JSON.generate(operation: operation, id: id.to_s, state: encoded, mirrors: @entry_mirrors)}\n"
42
100
 
43
101
  # One write, not JSON-then-newline as two: two concurrent
44
102
  # appends can only interleave *between* writes, never inside
@@ -4,8 +4,8 @@ require_relative "journal"
4
4
  module Hecks
5
5
  module Adapters
6
6
  class Heki
7
- # THE OPTIONAL saga-persistence capability (§2), Heki's own shape —
8
- # a SIBLING snapshot+journal file pair, built the exact same way an
7
+ # The optional saga-persistence capability (§2), Heki's own shape —
8
+ # a sibling snapshot+journal file pair, built the exact same way an
9
9
  # aggregate's own persistence already is: `Snapshot`/`Journal`
10
10
  # (heki/snapshot.rb, heki/journal.rb) operate generically on
11
11
  # `@path`/`@journal_path`/`@entry_mirrors` and never touch
@@ -15,16 +15,16 @@ module Hecks
15
15
  # Reserved file name (`hecks_saga_instances.heki`, matching the
16
16
  # `hecks_`-prefix convention every other new saga table in this
17
17
  # work uses) avoids colliding with any real aggregate's own
18
- # `storage_name`. Lives in the SAME directory an aggregate's own
18
+ # `storage_name`. Lives in the same directory an aggregate's own
19
19
  # `.heki` file would (`File.dirname(@path)`, `Heki`'s own call
20
20
  # below) — which, since Heki's `resolve_path` has no per-domain
21
- # component at all, is typically shared across EVERY domain
21
+ # component at all, is typically shared across every domain
22
22
  # booted from the same `root`. `domain` is therefore carried
23
23
  # inside each record and filtered on read, the same reason
24
24
  # Postgres's own `hecks_saga_instances` keeps an explicit `domain`
25
25
  # column under schema isolation (§3).
26
26
  #
27
- # ONE flat records hash, keyed by a composite string (Heki's own
27
+ # One flat records hash, keyed by a composite string (Heki's own
28
28
  # snapshot format is id-keyed, not tuple-keyed) — never exposed
29
29
  # outside this class; `each_saga` yields the five real fields a
30
30
  # caller actually wants, not the internal key shape.
@@ -30,7 +30,7 @@ module Hecks
30
30
  @path = resolve_path(settings, root)
31
31
  @journal_path = "#{@path}.journal"
32
32
  @events = []
33
- # THE OPTIONAL saga-persistence capability's own scoping (§2/§4)
33
+ # The optional saga-persistence capability's own scoping (§2/§4)
34
34
  # — falls back to the aggregate's own name for a directly-
35
35
  # instantiated adapter (specs), same fallback shape Postgres's
36
36
  # own @domain already uses.
@@ -65,7 +65,7 @@ module Hecks
65
65
  # threads this through; Heki's own never did, which made
66
66
  # `none_in_state?` (Ports::Query::InMemory) unconditionally
67
67
  # return `true` (its own graceful "no registry, no way to look
68
- # the target up" default) for EVERY `none_in_state` where-clause
68
+ # the target up" default) for every `none_in_state` where-clause
69
69
  # against a Heki-backed aggregate — silently excluding nothing,
70
70
  # always, no matter the actual target state.
71
71
  def query(specification, args = {}, context: {})
@@ -86,7 +86,11 @@ module Hecks
86
86
  # overwrite that write on disk rather than layer on top of it.
87
87
  def project(entry)
88
88
  current = read
89
- entry.save? ? current[entry.id] = entry.state.dup : current.delete(entry.id)
89
+ if entry.save?
90
+ current[entry.id] = Ports::Persistence::StateCodec.encode(@aggregate, entry.state)
91
+ else
92
+ current.delete(entry.id)
93
+ end
90
94
  write(current)
91
95
  @store = current
92
96
  entry
@@ -114,7 +118,7 @@ module Hecks
114
118
 
115
119
  def record_event(event) = @events << event
116
120
 
117
- # ── the OPTIONAL saga-persistence capability (§2) — Heki's own
121
+ # ── the optional saga-persistence capability (§2) — Heki's own
118
122
  # shape (a sibling snapshot+journal file pair, `SagaStore`,
119
123
  # heki/saga_store.rb) rather than a table in a store this adapter
120
124
  # doesn't have.
@@ -134,11 +138,13 @@ module Hecks
134
138
  @saga_store ||= SagaStore.new(File.dirname(@path))
135
139
  end
136
140
 
141
+ # `record` is the snapshot/journal's own string-keyed JSON — decoded
142
+ # deep through the state codec, never symbolized one level by hand.
137
143
  def instance(id, record)
138
144
  Runtime::Instance.new(
139
145
  aggregate: @aggregate,
140
146
  id: id,
141
- state: record.transform_keys(&:to_sym)
147
+ state: Ports::Persistence::StateCodec.decode(@aggregate, record)
142
148
  )
143
149
  end
144
150
 
@@ -152,10 +158,10 @@ module Hecks
152
158
  end
153
159
 
154
160
  # `dir: :default` — a bare Symbol, the framework's own convention
155
- # for "a DECLARED value that resolves by convention, never a silent
161
+ # for "a declared value that resolves by convention, never a silent
156
162
  # fallback" — used to crash `File.join` outright
157
163
  # (`TypeError: no implicit conversion of Symbol into String`):
158
- # `resolve_path` only ever checked for a MISSING `dir` setting,
164
+ # `resolve_path` only ever checked for a missing `dir` setting,
159
165
  # never a Symbol one. Treated the same as no setting at all — falls
160
166
  # back to the existing "data" default, not a new special case.
161
167
  def resolve_path(settings, root)
@@ -1,7 +1,7 @@
1
1
  module Hecks
2
2
  module Adapters
3
- # THE `identity_resolution` PORT, FULFILLED BY THE Identity FRAMEWORK
4
- # BLUEBOOK — same registry, same boot, so this is a dispatch against
3
+ # The `identity_resolution` port, fulfilled by the Identity framework
4
+ # bluebook — same registry, same boot, so this is a dispatch against
5
5
  # records already sitting in the store this adapter is handed, not a
6
6
  # bridge to a second runtime. Same reasoning `GovernanceAuthorization`
7
7
  # gives for itself, one port over.
@@ -6,7 +6,7 @@ require_relative "../../runtime/errors"
6
6
 
7
7
  module Hecks
8
8
  module Adapters
9
- # SHARED BY every adapter that holds decoded Ruby records rather than
9
+ # Shared by every adapter that holds decoded Ruby records rather than
10
10
  # running real SQL (Memory, Lambda, Heki) — the identical dotted-path
11
11
  # value-object member-picking Postgres/Sqlite/D1's own
12
12
  # order_expression does (numeric member wins, else the one-field
@@ -16,7 +16,7 @@ module Hecks
16
16
  module InMemoryOrdering
17
17
  module_function
18
18
 
19
- # order_by IS A RUNTIME VALUE (an HTTP query param, in the
19
+ # order_by is a runtime value (an HTTP query param, in the
20
20
  # console's case), not framework-authored bluebook source — see
21
21
  # postgres.rb's own all for the full reasoning. Whitelisted the
22
22
  # identical way before FieldPath.dig ever runs.
@@ -46,7 +46,7 @@ module Hecks
46
46
  vo = aggregate.value_object(attribute.type)
47
47
  return field.to_s unless vo
48
48
 
49
- # Numeric member first, then the SOLE attribute whatever it is
49
+ # Numeric member first, then the sole attribute whatever it is
50
50
  # named (single-attribute value objects strictly answer `.value`
51
51
  # — the same generalization `SqlQueryBuilder#query_expression`
52
52
  # makes for the column side, kept in lockstep so Memory and SQL
@@ -3,31 +3,56 @@ require "json"
3
3
  module Hecks
4
4
  module Adapters
5
5
  class Lambda
6
- # THE THIN AWS CLIENT — shared by this adapter's own read methods
6
+ # **The thin AWS client** — shared by this adapter's own read methods
7
7
  # (Lambda#find/#all/#count/#query) and Runtime::RemoteDispatcher's
8
8
  # write methods. One Lambda invoke, one JSON round trip, nothing
9
9
  # domain-specific: neither caller needs to know an AWS SDK is
10
10
  # involved at all.
11
11
  #
12
- # FUNCTION NAME IS COMPUTED, NOT PASSED — `"hecks-#{domain}"`,
12
+ # Function name is computed unless it is named — `"hecks-#{domain}"`,
13
13
  # lowercased, matches bin/project_deploy's own `stack_name`
14
14
  # exactly (bin/project_deploy: `stack_name = "hecks-#{domain_name}"`,
15
15
  # `domain_name = File.basename(domain)`). `domain` here is the
16
- # bluebook's OWN declared name (`Embryonaut`, not the directory);
16
+ # bluebook's own declared name (`Embryonaut`, not the directory);
17
17
  # today's real corpus has directory name == declared name
18
18
  # lowercased for every domain that deploys, so `.downcase` alone
19
19
  # reproduces the same string bin/project_deploy computes from the
20
- # directory — this is a real, load-bearing assumption, not a
21
- # coincidence to lean on silently forever if that ever stops
22
- # holding.
20
+ # directory.
21
+ #
22
+ # That assumption is not always true, and when it breaks nothing
23
+ # about it is recoverable from here. `bin/project_deploy` honours
24
+ # a `.world`'s own `stack_prefix`/`stack_name` — settings that
25
+ # exist precisely so a domain whose AWS identity predates a rename
26
+ # keeps targeting the stack that is actually live rather than
27
+ # standing up a second, empty one beside it. A real one:
28
+ # embryonautfoundersapp deploys as `hecksagain-embryonaut`, and no
29
+ # value of `domain` (or of `DOMAIN_NAME`, the deployed-Lambda
30
+ # override callers already pass) can make this computation produce
31
+ # a name with no dash after "hecks". Every Ruby-side read and
32
+ # dispatch for that app has therefore been invoking a function
33
+ # that does not exist — found live, and visible in the deployed
34
+ # journal having never received a single row.
35
+ #
36
+ # So the name can be named, in the one place the rest of this
37
+ # deployment is already described: the `.world`'s own
38
+ # `persisted_by("Lambda")`/`dispatched_by("Lambda")` block, beside
39
+ # `region`. Given, it wins outright; absent, the computation above
40
+ # is unchanged, which is every domain whose stack name was never
41
+ # pinned.
23
42
  class Client
24
- def initialize(domain:, region:)
43
+ def initialize(domain:, region:, function: nil)
25
44
  require "aws-sdk-lambda"
26
- @function_name = "hecks-#{domain.to_s.downcase}"
45
+ @function_name = function.to_s.empty? ? "hecks-#{domain.to_s.downcase}" : function.to_s
27
46
  @client = Aws::Lambda::Client.new(region: region)
28
47
  end
29
48
 
30
- # THE WHOLE DOMAIN, EVERY TIMEmatches dispatch::read's own
49
+ # The function this client actually invokes — read by
50
+ # `Runtime::WiringError` messages and worth asserting on
51
+ # directly, since "which function did we call" is precisely the
52
+ # thing that used to be unanswerable from outside.
53
+ attr_reader :function_name
54
+
55
+ # **The whole domain, every time** — matches dispatch::read's own
31
56
  # rehydrate-the-full-journal design (Phase 1, rust/host). No
32
57
  # caching here, deliberately not even per-request: Lambda#all
33
58
  # used to memoize this across calls, which silently served
@@ -6,8 +6,8 @@ require_relative "../../runtime/instance"
6
6
 
7
7
  module Hecks
8
8
  module Adapters
9
- # THE READ-SIDE HALF OF LAMBDA ROUTING — `persisted_by("Lambda")`'s
10
- # own adapter, resolved through the SAME `Ports::Persistence`
9
+ # **The read-side half of lambda routing** — `persisted_by("Lambda")`'s
10
+ # own adapter, resolved through the same `Ports::Persistence`
11
11
  # machinery `persisted_by("Postgres")`/`persisted_by("Memory")`
12
12
  # already use (`Runtime::Registry#repository`, `RepositoryFactory
13
13
  # .build`) — no new framework plumbing, just a new adapter class at
@@ -16,7 +16,7 @@ module Hecks
16
16
  # compose through one shared `Client` per domain rather than each
17
17
  # inventing its own AWS wiring.
18
18
  #
19
- # READ-ONLY, DELIBERATELY — `append`/`project` raise rather than
19
+ # **Read-only, deliberately** — `append`/`project` raise rather than
20
20
  # silently no-op. A write reaching this class would mean
21
21
  # `Runtime::CommandInterpreter` ran locally against a Lambda-routed
22
22
  # domain, which is exactly the bypass `Runtime::RemoteDispatcher`
@@ -39,46 +39,44 @@ module Hecks
39
39
  else
40
40
  aggregate.name
41
41
  end
42
- region =
43
- if settings.key?(:region)
44
- settings[:region]
45
- elsif settings.key?("region")
46
- settings["region"]
47
- else
48
- "us-east-1"
49
- end
50
- # TWO DIFFERENT "domain"s, deliberately not conflated: `domain`
51
- # (this aggregate's OWN bluebook name — "Identity", "Governance")
42
+ region = setting(settings, :region, "us-east-1")
43
+ # Named, when this deployment's function isn't `hecks-<domain>`
44
+ # — `Client`'s own comment has the real case that needs it.
45
+ # Absent (every domain whose stack name was never pinned), the
46
+ # computation below is exactly as it was.
47
+ function = setting(settings, :function, nil)
48
+ # Two different "domain"s, deliberately not conflated: `domain`
49
+ # (this aggregate's own bluebook name — "Identity", "Governance")
52
50
  # only ever prefixes the instances lookup, since that's how
53
51
  # rust/host's own `Store::instances()` keys every record
54
52
  # (`registry.rb`'s own `"#{a[:domain_name]}::#{a[:name]}#"`
55
53
  # dump format, unchanged by which chapter attached it). The
56
- # FUNCTION to actually call is a different question: Governance
57
- # and Identity aggregates are compiled into the ATTACHING
54
+ # function to actually call is a different question: Governance
55
+ # and Identity aggregates are compiled into the attaching
58
56
  # domain's own Lambda (one merged `Store` per target — Phase 0's
59
57
  # framework-bluebook work), never a function of their own, so
60
58
  # `settings[:domain]` is the wrong signal for `Client.new`.
61
59
  # `root` is the boot's own directory (`Registry#root`, shared by
62
- # EVERY bluebook in one registry regardless of which one
60
+ # every bluebook in one registry regardless of which one
63
61
  # attached it) — `File.basename(root)` reproduces the exact
64
62
  # same string `bin/project_deploy`'s own `stack_name` computes
65
- # from the domain PATH, so the two can never name two
66
- # different functions for the same deploy... on a LOCAL boot,
63
+ # from the domain path, so the two can never name two
64
+ # different functions for the same deploy... on a local boot,
67
65
  # where `root` is a real project directory. Inside the deployed
68
- # Lambda itself `root` is ALWAYS `/var/task` (every Lambda's own
66
+ # Lambda itself `root` is always `/var/task` (every Lambda's own
69
67
  # fixed code root, regardless of domain) — `File.basename` gives
70
68
  # "task", not the domain name, and invokes the wrong function
71
69
  # entirely. A real, live AccessDeniedException on
72
70
  # "hecks-task" caught this: invisible through every earlier
73
71
  # phase's own verify step, all run from a local boot, until
74
- # WebFunction became the first Ruby process to EVER make this
72
+ # WebFunction became the first Ruby process to ever make this
75
73
  # exact call from inside a deployed Lambda. `DOMAIN_NAME` (set
76
74
  # by bin/project_deploy's own WebFunction Environment) is the
77
75
  # real, unambiguous signal in that specific context; the
78
76
  # root-basename heuristic stays as the local-boot fallback,
79
77
  # unchanged.
80
78
  function_domain = ENV["DOMAIN_NAME"] || (root ? File.basename(root) : domain)
81
- @client = Client.new(domain: function_domain, region: region)
79
+ @client = Client.new(domain: function_domain, region: region, function: function)
82
80
  @prefix = "#{domain}::#{aggregate.hecks_name}#"
83
81
  end
84
82
 
@@ -105,18 +103,29 @@ module Hecks
105
103
  # 1 — there is no local write-ahead log for `recover!` to replay),
106
104
  # `append`/`project` raise rather than silently no-op.
107
105
 
106
+ # A `.world` block's settings arrive symbol-keyed from the DSL and
107
+ # string-keyed from a round-tripped export, so every read has to
108
+ # accept both — one helper rather than the same five lines per
109
+ # key.
110
+ def setting(settings, key, fallback)
111
+ return settings[key] if settings.key?(key)
112
+ return settings[key.to_s] if settings.key?(key.to_s)
113
+
114
+ fallback
115
+ end
116
+
108
117
  private
109
118
 
110
- # RE-FETCHED EVERY CALL, deliberately NOT memoized across them —
119
+ # Re-fetched every call, deliberately not memoized across them —
111
120
  # this adapter itself is long-lived (one instance per aggregate,
112
- # held by the registry `RUNTIME = Hecks.boot(...)` builds ONCE
121
+ # held by the registry `RUNTIME = Hecks.boot(...)` builds once
113
122
  # per Lambda web process — WebFunction's own top-level constant,
114
123
  # reused warm across every HTTP request that process serves, not
115
124
  # rebuilt per request the way a memoize-for-one-request comment
116
125
  # here used to assume). A real, live bug caught this: a mutation
117
126
  # dispatched fine (RemoteDispatcher always calls the dispatch
118
- # Lambda fresh) and the very next `.all` on the SAME warm
119
- # container kept returning the state from BEFORE that mutation,
127
+ # Lambda fresh) and the very next `.all` on the same warm
128
+ # container kept returning the state from before that mutation,
120
129
  # forever, until the container cold-started — memoizing here
121
130
  # made every write invisible to every read on a warm container.
122
131
  # Keyed by bare id (the part after "Domain::Aggregate#"), not the
@@ -130,15 +139,12 @@ module Hecks
130
139
  end.to_h
131
140
  end
132
141
 
133
- # Lambda's own JSON response is already Ruby-decoded with STRING
134
- # keys (plain `JSON.parse`, no `symbolize_names:`) — every other
135
- # adapter's own `instance(row)` builder (Postgres, Sqlite) hands
136
- # `Runtime::Instance.new` a SYMBOL-keyed state hash instead. A
137
- # round trip through `JSON.generate`/`JSON.parse` is simpler and
138
- # safer than a hand-rolled deep-symbolize helper for what is not
139
- # a hot path.
142
+ # Lambda's own JSON response is already Ruby-decoded with string
143
+ # keys (plain `JSON.parse`, no `symbolize_names:`) — decoded through
144
+ # the state codec (PR A3), the same IR-driven spelling every other
145
+ # adapter's read produces.
140
146
  def build_instance(id, state)
141
- Runtime::Instance.new(aggregate: @aggregate, id: id, state: JSON.parse(JSON.generate(state), symbolize_names: true))
147
+ Runtime::Instance.new(aggregate: @aggregate, id: id, state: Ports::Persistence::StateCodec.decode(@aggregate, state))
142
148
  end
143
149
  end
144
150
  end
@@ -5,13 +5,13 @@ require_relative "../../runtime/instance"
5
5
 
6
6
  module Hecks
7
7
  module Adapters
8
- # A BROWSER-HOSTED DOMAIN'S OWN DECLARED INTENT — not a second Memory
8
+ # **A browser-hosted domain's own declared intent** — not a second Memory
9
9
  # wearing a different name. Ruby has no way to reach a real browser's
10
10
  # `window.localStorage` at all (it is per-tab, per-origin, JS-only,
11
11
  # unreachable over any network the way D1's own REST API is) — so
12
12
  # this Ruby-side adapter is honestly a stand-in: in-process, ephemeral,
13
13
  # mechanically identical to Memory. What earns it a name of its own
14
- # is what it DECLARES, not what it happens to do in Ruby: `persisted_by
14
+ # is what it declares, not what it happens to do in Ruby: `persisted_by
15
15
  # "LocalStorage"` says "this domain expects real, durable, single-
16
16
  # device storage the moment it's actually running where it's meant to
17
17
  # run" — the same distinction Heki (real local durability) already
@@ -20,18 +20,18 @@ module Hecks
20
20
  # model_check` all get a domain that boots and behaves correctly
21
21
  # against this adapter; only a real browser gets the real durability.
22
22
  #
23
- # THE REAL BROWSER HALF lives outside this file entirely: `rust/web`'s
23
+ # The real browser half lives outside this file entirely: `rust/web`'s
24
24
  # `dispatch(json)` (docs/implemented/decisions/0015) takes an optional
25
25
  # `"seed"` (the exact `"instances"` shape it also answers with) plus
26
26
  # `"steps"` — a host rehydrates from a prior snapshot and replays only
27
27
  # the new command(s), rather than the whole history every call. A
28
28
  # page bound to this adapter is expected to hold that snapshot in
29
29
  # `window.localStorage` itself (get on load, set after every
30
- # `dispatch`) — the seed/instances round trip IS the adapter, once
30
+ # `dispatch`) — the seed/instances round trip is the adapter, once
31
31
  # you're in the one runtime that can actually reach the storage this
32
32
  # name promises.
33
33
  class LocalStorage
34
- # TENANT-CAPABLE TRIVIALLY, same reasoning as Memory's own — a
34
+ # Tenant-capable trivially, same reasoning as Memory's own — a
35
35
  # browser tab is exactly one origin, exactly one user; there is no
36
36
  # second tenant this in-process Hash could ever confuse a first
37
37
  # one with.
@@ -54,7 +54,7 @@ module Hecks
54
54
  InMemoryOrdering.ordered(@records.values, aggregate: @aggregate, order_by: order_by, direction: direction)
55
55
  end
56
56
 
57
- # THE DECISION THE GUIDE ASKS FOR, MADE EXPLICITLY: no compiled
57
+ # The decision the guide asks for, made explicitly: no compiled
58
58
  # dialect of its own, same as Heki/Memory — a personal-scale local
59
59
  # store answering by walking `all` is correct on day one, and
60
60
  # nothing about a browser tab's own data volume asks for pushdown.
@@ -62,21 +62,24 @@ module Hecks
62
62
  Ports::Query::InMemory.execute(all, specification, args, registry: context[:registry])
63
63
  end
64
64
 
65
+ # Through the state codec, the same as Memory (see its `append`).
65
66
  def append(entry)
66
- @entries << entry
67
+ copied = Ports::Persistence::Entry.new(operation: entry.operation, id: entry.id,
68
+ state: copy(entry.state), mirrors: entry.mirrors)
69
+ @entries << copied
67
70
  entry
68
71
  end
69
72
 
70
73
  def project(entry)
71
74
  if entry.save?
72
- @records[entry.id] = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state.dup)
75
+ @records[entry.id] = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: copy(entry.state))
73
76
  else
74
77
  @records.delete(entry.id)
75
78
  end
76
79
  end
77
80
 
78
81
  def save(instance)
79
- entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
82
+ entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: copy(instance.state))
80
83
  append(entry)
81
84
  project(entry)
82
85
  end
@@ -108,7 +111,11 @@ module Hecks
108
111
  self
109
112
  end
110
113
 
111
- # NOT lineage_capable? — deliberately absent, the same trade Heki
114
+ private
115
+
116
+ def copy(state) = Ports::Persistence::StateCodec.copy(@aggregate, state)
117
+
118
+ # Not lineage_capable? — deliberately absent, the same trade Heki
112
119
  # makes and states plainly (writing-an-adapter.md's own section on
113
120
  # it): a domain bound here has no edge for its own shape to travel
114
121
  # across if it ever changes; that must be hand-migrated, or the