hecks 1.3.0 → 1.5.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 (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -20,56 +20,81 @@ module Hecks
20
20
  UNSET = Object.new.freeze
21
21
  private_constant :UNSET
22
22
 
23
+ # Returns the attributes declared so far, minting the accumulating list on first use.
24
+ #
25
+ # @return [Array<Bluebook::Attribute>] the attributes declared so far, in declaration order
23
26
  def attributes = @attributes ||= []
24
27
 
25
28
  # Value objects synthesised from inline closed sets, collected here and
26
29
  # installed by whoever owns value objects (the aggregate).
30
+ #
31
+ # @return [Array<Bluebook::ValueObject>] the value objects synthesised so far
27
32
  def closed_sets = @closed_sets ||= []
28
33
 
29
- # `admits:` names a closed set that is ALREADY DECLARED elsewhere —
34
+ # Declares one field on the owning construct, with its type and, optionally, a default,
35
+ # an optionality flag, a pattern, a closed-set reference, or an inline closed set.
36
+ #
37
+ # `admits:` names a closed set that is already declared elsewhere —
30
38
  #
31
39
  # attribute :op, String, admits: "Vocabulary::QueryComparator"
32
40
  #
33
- # which is the difference between it and `one_of`: `one_of` SYNTHESISES
41
+ # which is the difference between it and `one_of`: `one_of` synthesises
34
42
  # a fresh value object named for the attribute, so it can only ever name
35
43
  # something new. `admits` points at a set the language already holds, so
36
44
  # the same set can be named from many places without being written twice.
37
45
  #
38
- # QUALIFIED, because a closed set is a value object INSIDE an aggregate
46
+ # Qualified, because a closed set is a value object inside an aggregate
39
47
  # and `reference_to` reaches heads only — so this is text, checked where
40
48
  # it is read rather than by reference resolution.
41
49
  #
42
- # AND WRITTEN AS TEXT, not as the constant path `Vocabulary::QueryComparator`
50
+ # And written as text, not as the constant path `Vocabulary::QueryComparator`
43
51
  # it reads like. The constant spelling was tried — `ConstShim` returning
44
52
  # a Module so Ruby's `::` reaches a second `const_missing` — and it
45
53
  # cannot hold: `Facade::Surface` installs every aggregate name as a
46
- # TOP-LEVEL constant, so the moment any facade exists, `Vocabulary`
54
+ # top-level constant, so the moment any facade exists, `Vocabulary`
47
55
  # resolves to that module and the shim is never asked. A spelling that
48
56
  # works only until a facade is built is worse than a quoted one.
49
57
  #
50
- # THE TYPE POSITION TAKES A BARE CONSTANT, ALWAYS REQUIRED (ADR 0025,
58
+ # The type position takes a bare constant, always required (ADR 0025,
51
59
  # "Attributes") — omitting it (a mint default of String) and quoting
52
60
  # it as text both refuse now. Neither had a real reason left: no
53
61
  # corpus attribute ever omitted the type, and `ConstShim#const_missing`
54
62
  # (S0b) already resolves a bare, not-yet-declared constant to the
55
- # SAME forward reference the quoted form existed for — a bareword
63
+ # same forward reference the quoted form existed for — a bareword
56
64
  # `Name` reaches a value object named "Name" declared later in the
57
- # same block exactly as `"Name"` used to, `spell`'s own `to_s`
58
- # renders either one identically. Neither form appears in any frozen
59
- # era text (checked directly), so both are refused unconditionally —
60
- # nothing for `MetaValidator.shadow_parsing?` to answer for.
61
- # RENAMED FROM `attribute` — item #13's full metaprogrammed
62
- # dispatch (slice 3, whole-project table-unification survey).
63
- # The word `attribute` itself is no longer a real method any
64
- # builder answers directly: every (context, word) Keyword row
65
- # for it carries `calls: "attribute_impl"`, and `GenericDispatch`
66
- # forwards the whole call here untouched — this method's own
67
- # body is exactly what `attribute` always was, unchanged, just
68
- # reached generically now rather than by Ruby's own direct
69
- # method lookup. `attribute_collector_spec.rb` (`AttributeCollector
70
- # has no method without a test` — dsl_coverage_spec.rb) and the
71
- # bootstrap fallback (`GenericDispatch::BOOTSTRAP_CALLS_FALLBACK`) both
72
- # name this same string; they must never drift apart.
65
+ # same block, exactly what the quoted form `"Name"` would have
66
+ # reached — `spell`'s own `to_s` renders either one identically.
67
+ # Neither form appears in any frozen era text (checked directly), so
68
+ # both are refused unconditionally — nothing for
69
+ # `MetaValidator.shadow_parsing?` to answer for.
70
+ #
71
+ # The word `attribute` itself is not a real method any builder answers
72
+ # directly: every (context, word) Keyword row for it carries
73
+ # `calls: "attribute_impl"`, and `GenericDispatch` forwards the whole
74
+ # call here untouched. `attribute_collector_spec.rb`
75
+ # (`AttributeCollector has no method without a test` —
76
+ # dsl_coverage_spec.rb) and the bootstrap fallback
77
+ # (`GenericDispatch::BOOTSTRAP_CALLS_FALLBACK`) both name this same
78
+ # string; they must never drift apart.
79
+ #
80
+ # @param name [Symbol] the attribute's name
81
+ # @param type [Module, Symbol, ListOf, OneOf] the attribute's type: a bare constant
82
+ # (resolved by `ConstShim`), or the `ListOf`/`OneOf` wrapper `list_of`/`one_of` return
83
+ # when called in this same type position; quoted text is refused
84
+ # @param default [Object, nil] the value an omitted attribute defaults to; not type-checked
85
+ # here
86
+ # @param optional [Boolean] whether the attribute may be omitted entirely
87
+ # @param pattern [Regexp, String, nil] a pattern the attribute's value must match; refused
88
+ # if it uses a construct `PatternSubset` disallows
89
+ # @param admits [String, nil] the qualified name of an already-declared closed set this
90
+ # attribute's value may come from, such as `"Vocabulary::QueryComparator"`
91
+ # @param one_of [Array<String, Symbol>, nil] permitted values for a field-shaped closed
92
+ # set, declared inline via `one_of: [...]`; meaningful only inside a `value_object`
93
+ # (`ValueObjectBuilder` overrides `install_inline_closed_set`) — refused everywhere else
94
+ # @return [void]
95
+ # @raise [Bluebook::DSL::Malformed] if `name` is already declared, `type` is omitted or
96
+ # quoted text, `pattern` uses a disallowed construct, or `one_of:` is given outside a
97
+ # `value_object`
73
98
  def attribute_impl(name, type = UNSET, default: nil, optional: false, pattern: nil,
74
99
  admits: nil, one_of: nil)
75
100
  # moved to the language: FieldName invariant, on Root.Attribute
@@ -109,20 +134,25 @@ module Hecks
109
134
  install_inline_closed_set(name, one_of) if one_of
110
135
  end
111
136
 
112
- # RENAMED FROM `list_of` — item #13's full metaprogrammed
113
- # dispatch (slice 5). Called in an attribute's own TYPE
114
- # position (`attribute :x, list_of(Y)`), never through a `def
115
- # list_of` any ONE builder answers as its own word — reached
116
- # via `WordGate#word_gate_dispatch`'s new "Type"-context
117
- # fallback, the same one `one_of_impl` below uses. Bootstrap-
118
- # reachable (every core chapter's own list-typed attributes use
119
- # it), so `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK` carries a
120
- # SINGLE `["Type", "list_of"]` entry rather than one per calling
121
- # context — the bootstrap branch checks that key too now, same
122
- # reasoning as the ordinary fallback.
137
+ # Wraps a type so `attribute_impl` records it as a list-valued attribute.
138
+ #
139
+ # Called in an attribute's own type position (`attribute :x,
140
+ # list_of(Y)`), never through a `def list_of` any one builder
141
+ # answers as its own word — reached via
142
+ # `WordGate#word_gate_dispatch`'s "Type"-context fallback, the same
143
+ # one `one_of_impl` below uses. Bootstrap-reachable (every core
144
+ # chapter's own list-typed attributes use it), so
145
+ # `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK` carries a single
146
+ # `["Type", "list_of"]` entry rather than one per calling context —
147
+ # the bootstrap branch checks that key too, same reasoning as the
148
+ # ordinary fallback.
149
+ #
150
+ # @param type [Module, Symbol] the list's element type, a bare constant
151
+ # @return [Bluebook::DSL::AttributeCollector::ListOf] the wrapper `attribute_impl` reads
152
+ # to mark the attribute list-valued
123
153
  def list_of_impl(type) = ListOf.new(type)
124
154
 
125
- # `reference_to Account` MINTS `:account` — no `_id` — the default
155
+ # `reference_to Account` mints `:account` — no `_id` — the default
126
156
  # every `reference_to`-shaped word (`AggregateBuilder`,
127
157
  # `EntityBuilder`, `CommandBuilder#cross_reference`,
128
158
  # `QueryBuilder`, `PortOperationBuilder`, all `include
@@ -132,13 +162,13 @@ module Hecks
132
162
  # `.`; deriving the hop name from `account_id` is what made an
133
163
  # explicit operator impossible before).
134
164
  #
135
- # LEGACY UNDER SHADOW-PARSING (S0a's own bridge): frozen era text
136
- # was minted under the OLD default, and `EraGuard.shadow_parse`
137
- # reconstructs a held aggregate's shape to DIFF against the
138
- # current one — reading that text through the NEW default would
139
- # silently reconstruct the WRONG historical name, not merely
165
+ # Legacy under shadow-parsing (S0a's own bridge): frozen era text
166
+ # was minted under the old default, and `EraGuard.shadow_parse`
167
+ # reconstructs a held aggregate's shape to diff against the
168
+ # current one — reading that text through the new default would
169
+ # silently reconstruct the wrong historical name, not merely
140
170
  # refuse a spelling the way S1's `identified_by` legacy forms do.
141
- # This is a mint DEFAULT changing, not a syntax being removed, so
171
+ # This is a mint default changing, not a syntax being removed, so
142
172
  # there is nothing to refuse here — only a fork in what gets
143
173
  # minted when `as:` is omitted.
144
174
  private def default_reference_name(target)
@@ -146,25 +176,31 @@ module Hecks
146
176
  :"#{Naming.snake(target)}#{suffix}"
147
177
  end
148
178
 
149
- # A closed set declared INLINE on the attribute:
179
+ # A closed set declared inline on the attribute:
150
180
  #
151
181
  # attribute :status, one_of("open", "shut")
152
182
  #
153
183
  # Desugars to a value object named for the attribute, so it goes through
154
184
  # exactly the machinery a hand-written one_of does — and so the
155
- # attribute's type is still a DECLARED value object, which is now a
185
+ # attribute's type is still a declared value object, which is now a
156
186
  # structural rule rather than a predicate.
157
187
  #
158
- # An earlier reading of this spelling parsed it and threw the values
159
- # away: the attribute became a plain String and the closed set meant
160
- # nothing, in a construct that looked supported. The desugaring is
161
- # pinned now — the same bluebook must always yield the same IR.
162
- # RENAMED FROM `one_of` — item #13's full metaprogrammed dispatch
163
- # (slice 5), same reasoning as list_of_impl above. SAME NAME as
188
+ # Wraps a list of permitted values so `attribute_impl` synthesises a closed-set value
189
+ # object for them.
190
+ #
191
+ # The desugaring is pinned — the same bluebook must always yield the
192
+ # same IR, so a plain String attribute silently discarding the
193
+ # values is not an outcome this spelling can produce.
194
+ #
195
+ # Reached the same way `list_of_impl` above is. Same name as
164
196
  # `ValueObjectBuilder#one_of_impl`'s own override on purpose —
165
197
  # that method's own `super(*values)` call (the no-block, bare
166
- # type-position case) resolves by METHOD NAME up the ancestor
198
+ # type-position case) resolves by method name up the ancestor
167
199
  # chain, and renaming only one side would silently break it.
200
+ #
201
+ # @param values [Array<String, Symbol>] the permitted values
202
+ # @return [Bluebook::DSL::AttributeCollector::OneOf] the wrapper `attribute_impl` reads to
203
+ # synthesise the closed-set value object
168
204
  def one_of_impl(*values) = OneOf.new(values)
169
205
 
170
206
  private
@@ -180,11 +216,11 @@ module Hecks
180
216
  )
181
217
  end
182
218
 
183
- # `one_of:` NAMES A CLOSED SET ON THE FIELD ITSELF (ADR 0025,
219
+ # `one_of:` names a closed set on the field itself (ADR 0025,
184
220
  # "Attributes") — joining `pattern:`/`admits:` where value
185
221
  # constraints already live, for the one context where it means
186
222
  # anything: a `value_object` block, where it replaces the old
187
- # `one_of do member ... end` wrapper for a SINGLE-FIELD set (a
223
+ # `one_of do member ... end` wrapper for a single-field set (a
188
224
  # multi-field set still writes bare `member` lines, unwrapped).
189
225
  # `ValueObjectBuilder` overrides this; every other includer
190
226
  # (Aggregate/Entity/Command/Query/PortOperation) inherits this
@@ -198,15 +234,15 @@ module Hecks
198
234
  "with the type-position one_of(...) instead"
199
235
  end
200
236
 
201
- # A NAME DECLARED TWICE ON THE SAME OWNER IS TWO ATTRIBUTES SHARING
202
- # ONE NAME, and nothing downstream disambiguates them — every
237
+ # A name declared twice on the same owner is two attributes sharing
238
+ # one name, and nothing downstream disambiguates them — every
203
239
  # reader that walks `attributes` looking for one by name
204
240
  # (`seal_mutation_targets`, `seal_query_field`, `projects`'s own
205
241
  # local check, `Instance#[]`, ...) uses `Array#find`/`any?`, which
206
242
  # silently answers whichever declaration happens to come first and
207
- # discards the second. Used to boot clean and stay that way : both
208
- # declarations survived into the IR, one of them permanently
209
- # unreachable by name. Refused HERE, at the one place every owner
243
+ # discards the second — left unrefused, both declarations would
244
+ # survive into the IR, one of them permanently unreachable by name.
245
+ # Refused here, at the one place every owner
210
246
  # (Aggregate/Entity/Command/Query/PortOperation/ValueObject, each
211
247
  # `include AttributeCollector`) mints an attribute through, rather
212
248
  # than taught to each of those readers individually.
@@ -216,7 +252,7 @@ module Hecks
216
252
  raise Malformed, "#{name} is declared twice — an attribute name is declared once, not twice"
217
253
  end
218
254
 
219
- # A pattern is refused AT DECLARATION, not when a value first meets it :
255
+ # A pattern is refused at declaration, not when a value first meets it :
220
256
  # a regex whose meaning depends on which engine reads it is a defect in
221
257
  # the bluebook, and a bluebook that loads is one whose patterns carry
222
258
  # one meaning. PatternSubset says which those are, and why each is refused.
@@ -246,9 +282,9 @@ module Hecks
246
282
  def resolve_identity_field!(field, value_objects, context_name)
247
283
  attr = attributes.find { |a| a.name == field }
248
284
 
249
- # `:id` OR AN `_id`-SUFFIXED NAME WITH NO MATCHING ATTRIBUTE is
250
- # not a typo to refuse — it is the language's own WALK-PARENT/
251
- # FALLBACK-IDENTITY convention. The `_id` suffix is the meta-
285
+ # `:id` or an `_id`-suffixed name with no matching attribute is
286
+ # not a typo to refuse — it is the language's own walk-parent/
287
+ # fallback-identity convention. The `_id` suffix is the meta-
252
288
  # domain's own (the `Command`/`Entity`/`Aggregate` etc. records
253
289
  # identify by `owner_id`/`bluebook_id`/`aggregate_id`, supplied
254
290
  # by the judge's replay rather than declared locally — see
@@ -285,26 +321,26 @@ module Hecks
285
321
  "#{context_name}.identified_by #{target} mints :#{field}, but that attribute is already declared"
286
322
  end
287
323
 
288
- # `Attribute.new` DIRECTLY, not the public `attribute(...)` DSL
289
- # entry — `target` is `Naming.demodulise`'d TEXT, not a bareword
324
+ # `Attribute.new` directly, not the public `attribute(...)` DSL
325
+ # entry — `target` is `Naming.demodulise`'d text, not a bareword
290
326
  # the bluebook author typed (the type position's own quoted-text
291
327
  # refusal is about DSL source, not internal minting), and `vo`
292
- # ITSELF can't be passed either: it is the real, already-built
328
+ # itself can't be passed either: it is the real, already-built
293
329
  # `ValueObject` subclass, permanently anonymous from Ruby's own
294
330
  # `to_s` (only `hecks_name` carries its name) — `Attribute#spell`
295
331
  # would demodulise it to "#<Class:0x...>", not "PizzaName".
296
332
  attributes << Attribute.new(name: field, type: target)
297
- # MOVED to `insert_at` — the attribute count AT THE MOMENT
333
+ # Moved to `insert_at` — the attribute count at the moment
298
334
  # `identified_by` was actually called, captured by the caller —
299
335
  # not left where `attribute` just appended it. Resolution happens
300
- # at BUILD time, after every other attribute in the block has
301
- # already run, so appending would put the identity field LAST
336
+ # at build time, after every other attribute in the block has
337
+ # already run, so appending would put the identity field last
302
338
  # regardless of where `identified_by` was actually written.
303
339
  # Most real bluebooks write it first (insert_at 0); one (a
304
340
  # ScheduledPayment corpus member) writes it after a reference_to
305
- # and an attribute — this matches either, and whatever a person
306
- # hand-writing `attribute field, Type` at that exact point,
307
- # the way this used to be required, would have produced.
341
+ # and an attribute — this matches either, producing the same
342
+ # result a person hand-writing `attribute field, Type` at that
343
+ # exact point would.
308
344
  attributes.insert(insert_at, attributes.pop)
309
345
  vo.attributes.flat_map do |attribute|
310
346
  identity_paths_for_attribute(attribute, value_objects, context_name,
@@ -332,13 +368,14 @@ module Hecks
332
368
  raise Malformed, "#{context_name}'s identity value objects form a cycle: #{cycle}"
333
369
  end
334
370
 
335
- # A BARE FIELD DERIVES ONE SCALAR. `identified_by :ref` (or one leg
371
+ # **A bare field derives one scalar**. `identified_by :ref` (or one leg
336
372
  # of a compound `identified_by :a, :b`) names a single field, and
337
- # deriving ITS path only makes sense while every value object along
373
+ # deriving its path only makes sense while every value object along
338
374
  # the way wraps exactly one field itself — the same "single-field
339
- # value object" ADR 0025 names this shape after. A multi-field
340
- # value object here used to expand silently into every one of its
341
- # own fields, minting an unannounced compound key nothing declared.
375
+ # value object" ADR 0025 names this shape after. Left unrefused, a
376
+ # multi-field value object here would expand silently into every
377
+ # one of its own fields, minting an unannounced compound key
378
+ # nothing declared.
342
379
  if nested.attributes.size != 1
343
380
  candidates = nested.attributes.map(&:name).join(", ")
344
381
  raise Malformed,
@@ -7,6 +7,13 @@ module Hecks
7
7
  # `Payment.persisted_by "Postgres"` into a queued `Bind`, and `#port`
8
8
  # is the one real method, for `Aggregate.port("Name") do ... end`.
9
9
  class BindingProxy
10
+ # Mints the stand-in module a bare domain constant resolves to inside a `.hecksagon` block.
11
+ #
12
+ # @param domain [Symbol, String] the domain name, the first segment of `Domain::Aggregate`
13
+ # @param collector [Array<Bluebook::Bind>] the list every bind made through the module's
14
+ # proxies is appended to
15
+ # @return [Module] an anonymous module whose `const_missing` answers a `BindingProxy`
16
+ # for `"Domain::Aggregate"`
10
17
  def self.namespace(domain, collector)
11
18
  Module.new do
12
19
  define_singleton_method(:const_missing) do |aggregate|
@@ -15,17 +22,30 @@ module Hecks
15
22
  end
16
23
  end
17
24
 
25
+ # @param fqn [String] the aggregate's qualified name, `"Domain::Aggregate"`
26
+ # @param collector [Array<Bluebook::Bind>] the list each bind made on this proxy is
27
+ # appended to
18
28
  def initialize(fqn, collector)
19
29
  @fqn = fqn
20
30
  @collector = collector
21
31
  end
22
32
 
23
- # THE AGGREGATE-SCOPED PORT — `Payments::Payment.port("Gateway") do
33
+ # Declares a port on this aggregate and attaches it to the already-registered bluebook.
34
+ #
35
+ # **The aggregate-scoped port** — `Payments::Payment.port("Gateway") do
24
36
  # ... end`, the same receiver a plain bind like `.persisted_by(...)`
25
37
  # already reaches, because a port belongs to exactly one aggregate
26
- # the same way a bind does. A REAL method, not method_missing : its
38
+ # the same way a bind does. A real method, not method_missing : its
27
39
  # shape (a name and a block building operations) has nothing to do
28
40
  # with `Bind`, so it does not belong in that generic verb path.
41
+ #
42
+ # @param name [String] the port's name, such as `"Gateway"`
43
+ # @yield the port body, evaluated against a `DomainPortBuilder`: either `verb`/`signal`
44
+ # or `operation`/`tells`/`asks` blocks
45
+ # @return [Bluebook::DSL::BindingProxy] this proxy, so further binds can chain
46
+ # @raise [Bluebook::DSL::Malformed] if the current registry holds no such aggregate, or
47
+ # the body declares both a verb and operations, neither, or an operation the port
48
+ # grammar refuses
29
49
  def port(name, &block)
30
50
  domain, aggregate_name = @fqn.split("::")
31
51
  aggregate_ir = Hecks.current_registry.bluebook(domain)&.aggregate(aggregate_name) or
@@ -57,6 +77,16 @@ module Hecks
57
77
  end
58
78
 
59
79
  def method_missing(verb, *args, **kwargs, &block)
80
+ # A BARE CALL — no args, no kwargs, no block — starts (or
81
+ # continues, on `AttributePath` itself below) a Privacy
82
+ # marking chain: `Registration.attendee.medications.has_phi(
83
+ # readable_by: "Privacy officer")`. No existing real
84
+ # `.hecksagon` bind is ever called this way (verified by
85
+ # grep before adding this branch — `persisted_by`/
86
+ # `opened_by`/every other bind always takes at least one
87
+ # arg), so this cannot collide with recording a `Bind`.
88
+ return AttributePath.new(@fqn, [verb.to_s]) if args.empty? && kwargs.empty? && !block
89
+
60
90
  @collector << Bind.new(
61
91
  aggregate: @fqn,
62
92
  verb: verb.to_s,
@@ -71,6 +101,55 @@ module Hecks
71
101
 
72
102
  def to_s = @fqn
73
103
  end
104
+
105
+ # What a Privacy-marking chain resolves to after its first bare segment —
106
+ # `Registration.attendee` returns one of these, `.medications` returns another
107
+ # (one segment longer), and a terminal `has_<category>(readable_by:)` records the
108
+ # marking and ends the chain. See `BindingProxy#method_missing`'s own header for why
109
+ # a bare call is unambiguously the start of one of these, never a `Bind`.
110
+ class AttributePath
111
+ # @param fqn [String] the aggregate's own qualified name, `"Domain::Aggregate"`
112
+ # @param path [Array<String>] every segment named so far, e.g. `["attendee"]`
113
+ def initialize(fqn, path)
114
+ @fqn = fqn
115
+ @path = path
116
+ end
117
+
118
+ # @param verb [Symbol] `has_<category>` to record the marking and end the chain;
119
+ # any other bare name to extend the path one segment further
120
+ # @param readable_by [String] required only for a `has_<category>` call — the
121
+ # Governance role a read must hold to see this field unredacted
122
+ # @return [Bluebook::DSL::AttributePath, nil] a longer chain for a plain segment;
123
+ # `nil` (nothing further to chain) for a `has_<category>` call
124
+ # @raise [Malformed] if a `has_<category>` call omits `readable_by:`, or any call
125
+ # carries positional args or a block (neither shape this chain supports)
126
+ def method_missing(verb, *args, readable_by: nil, **kwargs, &block)
127
+ name = verb.to_s
128
+ return record_marking(name.delete_prefix("has_"), readable_by) if name.start_with?("has_")
129
+
130
+ if !args.empty? || !kwargs.empty? || block
131
+ raise Malformed, "#{@fqn}.#{@path.join('.')}.#{name} — an attribute path chain takes no " \
132
+ "arguments except a terminal has_<category>(readable_by:)"
133
+ end
134
+
135
+ AttributePath.new(@fqn, @path + [name])
136
+ end
137
+
138
+ def respond_to_missing?(_name, _include_private = false) = true
139
+
140
+ def to_s = "#{@fqn}.#{@path.join('.')}"
141
+
142
+ private
143
+
144
+ def record_marking(category, readable_by)
145
+ raise Malformed, "#{self}.has_#{category} needs readable_by: (the Governance role a read must hold)" unless readable_by
146
+
147
+ Hecks.current_registry.add_pending_privacy_marking(
148
+ domain: @fqn, attribute_path: @path.join("."), category: category, readable_by: readable_by
149
+ )
150
+ nil
151
+ end
152
+ end
74
153
  end
75
154
  end
76
155
  end