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
@@ -11,6 +11,12 @@ module Hecks
11
11
  # do (facade/surface/aggregate_door.rb) — so they exist for every
12
12
  # aggregate whether or not its bluebook declared a query at all.
13
13
  module RecordRenderer
14
+ # Renders the index page listing every record of one aggregate.
15
+ #
16
+ # @param registry [Runtime::Registry] the booted registry holding the domain
17
+ # @param domain [String, Symbol] the domain name
18
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose records are listed
19
+ # @return [String] the index page's rendered HTML
14
20
  def self.index(registry:, domain:, aggregate:)
15
21
  instances = registry.repository(domain, aggregate).all
16
22
  <<~HTML
@@ -23,6 +29,12 @@ module Hecks
23
29
  HTML
24
30
  end
25
31
 
32
+ # Renders the "+ Command" buttons for every creating command.
33
+ #
34
+ # @param domain [String, Symbol] the domain name
35
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose creating commands
36
+ # are linked
37
+ # @return [String] HTML action buttons, or `""` if the aggregate declares none
26
38
  def self.creating_links(domain, aggregate)
27
39
  creators = aggregate.commands.select(&:creates?)
28
40
  return "" if creators.empty?
@@ -34,6 +46,15 @@ module Hecks
34
46
  %(<div class="actions">#{links.join}</div>)
35
47
  end
36
48
 
49
+ # Renders one record's state page: its current fields, plus the commands and
50
+ # queries it can dispatch next.
51
+ #
52
+ # @param registry [Runtime::Registry] the booted registry holding the domain
53
+ # @param domain [String, Symbol] the domain name
54
+ # @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
55
+ # @param id [String, Object] the record's identity
56
+ # @return [String, nil] the record page's rendered HTML, or nil if no record
57
+ # has that id
37
58
  def self.show(registry:, domain:, aggregate:, id:)
38
59
  instance = registry.repository(domain, aggregate).find(id)
39
60
  return nil unless instance
@@ -50,6 +71,10 @@ module Hecks
50
71
  HTML
51
72
  end
52
73
 
74
+ # Renders a record's state as an HTML field table.
75
+ #
76
+ # @param state [Hash{Symbol => Object}] the record's stored attribute values
77
+ # @return [String] an HTML table of the state's fields, values escaped
53
78
  def self.state_table(state)
54
79
  rows = state.map do |key, value|
55
80
  "<tr><th>#{Escape.html(Humanize.label(key.to_s))}</th><td>#{Escape.html(render_value(value))}</td></tr>"
@@ -60,6 +85,10 @@ module Hecks
60
85
  # `Value.materialize` first — a stored field is a `Runtime::Value`
61
86
  # wherever its attribute is a value object, not a plain Hash (see
62
87
  # record_table.rb's own note on the same read).
88
+ #
89
+ # @param value [Object] a stored field's value, possibly a `Runtime::Value`
90
+ # @return [String] the value rendered as text: a Hash as `"key: value, ..."`
91
+ # pairs, an Array `;`-joined, anything else its own `to_s`
63
92
  def self.render_value(value)
64
93
  case (value = Runtime::Value.materialize(value))
65
94
  when Hash then value.map { |k, v| "#{k}: #{render_value(v)}" }.join(", ")
@@ -68,17 +97,25 @@ module Hecks
68
97
  end
69
98
  end
70
99
 
71
- # Every non-creating command, EXCEPT a lifecycle transition that does
100
+ # Every non-creating command, except a lifecycle transition that does
72
101
  # not apply from the record's current state — the same rule
73
102
  # `Rules#admissible_transition` enforces at dispatch, read here so a
74
103
  # link that would only refuse is never offered in the first place.
104
+ #
105
+ # @param domain [String, Symbol] the domain name
106
+ # @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
107
+ # @param id [String, Object] the record's identity
108
+ # @param current_state [String, nil] the record's current lifecycle state, or
109
+ # nil if the aggregate has no lifecycle field or it is unset
110
+ # @return [String] an HTML list of admissible command links, or a message
111
+ # when no command currently applies
75
112
  def self.command_links(domain, aggregate, id, current_state)
76
113
  commands = aggregate.commands.reject(&:creates?).select { |cmd| applies?(aggregate, cmd, current_state) }
77
114
  return "<p><em>No commands act on an existing #{Escape.html(aggregate.hecks_name)}.</em></p>" if commands.empty?
78
115
 
79
116
  items = commands.map do |cmd|
80
117
  # L12 — the id is free-form (S3): percent-encoded as the query
81
- # VALUE (a raw `&` here would smuggle a second bogus query
118
+ # value (a raw `&` here would smuggle a second bogus query
82
119
  # parameter), then the assembled href is attribute-escaped as
83
120
  # usual.
84
121
  href = "/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html?to=#{Escape.url(id)}"
@@ -88,6 +125,14 @@ module Hecks
88
125
  %(<ul class="verb-list">#{items.join}</ul>)
89
126
  end
90
127
 
128
+ # Whether the aggregate's lifecycle admits dispatching `command` from
129
+ # `current_state`.
130
+ #
131
+ # @param aggregate [Bluebook::Aggregate] the aggregate the command belongs to
132
+ # @param command [Bluebook::Command] the command to check
133
+ # @param current_state [String, nil] the record's current lifecycle state
134
+ # @return [Boolean] true if the aggregate declares no lifecycle transitions for
135
+ # this command, or one of them admits `current_state`
91
136
  def self.applies?(aggregate, command, current_state)
92
137
  transitions = aggregate.lifecycle&.transitions_for(command.hecks_name) || []
93
138
  return true if transitions.empty?
@@ -95,6 +140,12 @@ module Hecks
95
140
  transitions.any? { |t| t.from.nil? || Array(t.from).map(&:to_s).include?(current_state.to_s) }
96
141
  end
97
142
 
143
+ # Renders the "Queries" link list for an aggregate's own queries.
144
+ #
145
+ # @param domain [String, Symbol] the domain name
146
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose queries are linked
147
+ # @return [String] an HTML section listing every query, or `""` if the
148
+ # aggregate declares none
98
149
  def self.query_links(domain, aggregate)
99
150
  return "" if aggregate.queries.empty?
100
151
 
@@ -105,6 +156,13 @@ module Hecks
105
156
  %(<h2>Queries</h2><ul class="verb-list">#{items.join}</ul>)
106
157
  end
107
158
 
159
+ # Renders the full command-and-query index for an aggregate, independent of
160
+ # any one record's state.
161
+ #
162
+ # @param domain [String, Symbol] the domain name
163
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose verbs are listed
164
+ # @return [String] an HTML section listing every non-creating command and every
165
+ # query, or `""` if the aggregate declares neither
108
166
  def self.verb_list(domain, aggregate)
109
167
  commands = aggregate.commands.reject(&:creates?)
110
168
  return "" if commands.empty? && aggregate.queries.empty?
@@ -5,7 +5,7 @@ module Hecks
5
5
  module Forms
6
6
  # The one shape every renderer in this directory reads a record as —
7
7
  # `id` plus its state hash. `Runtime::Instance` already answers both,
8
- # so `repository.all`'s own records pass straight through; a QUERY's
8
+ # so `repository.all`'s own records pass straight through; a query's
9
9
  # answer does not (`QueryInterpreter#call` flattens `{id:}.merge(state)`
10
10
  # into one hash with no method to call — see
11
11
  # docs/command-form-and-query-form-bluebook.md's note on why), so
@@ -19,12 +19,26 @@ module Hecks
19
19
  # query's own results (query_form_renderer.rb) — the same records
20
20
  # read the same columns either way.
21
21
  module RecordTable
22
+ # Picks which fields to show as columns: identity, lifecycle state, then scalar
23
+ # attributes, capped at seven.
24
+ #
25
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose records will be shown
26
+ # @return [Array<Symbol>] up to 7 unique field names, in that order; excludes
27
+ # reference and list attributes
22
28
  def self.columns(aggregate)
23
29
  lifecycle = aggregate.lifecycle&.field
24
30
  scalars = aggregate.attributes.reject { |a| a.reference? || a.list? }.map(&:name)
25
31
  [*aggregate.identity_heads, lifecycle, *scalars].compact.uniq.first(7)
26
32
  end
27
33
 
34
+ # Renders a list of records as an HTML table, one row per record.
35
+ #
36
+ # @param aggregate [Bluebook::Aggregate] the aggregate the records belong to
37
+ # @param instances [Array<Runtime::Instance, Forms::Record>] the records to render;
38
+ # anything answering `#id`/`#state` works
39
+ # @param domain [String] the owning chapter's name, for building each row's link
40
+ # @return [String] the table markup; `"<p><em>No records.</em></p>"` when
41
+ # `instances` is empty
28
42
  def self.render(aggregate, instances, domain:)
29
43
  cols = columns(aggregate)
30
44
  head = (["id"] + cols).map { |name| "<th>#{Escape.html(Humanize.label(name.to_s))}</th>" }.join
@@ -39,6 +53,13 @@ module Hecks
39
53
  HTML
40
54
  end
41
55
 
56
+ # Renders one record's own table row, linked to its show page.
57
+ #
58
+ # @param instance [Runtime::Instance, Forms::Record] the record to render
59
+ # @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
60
+ # @param cols [Array<Symbol>] the column field names, as `columns` returns them
61
+ # @param domain [String] the owning chapter's name, for building the row's link
62
+ # @return [String] the `<tr>` markup
42
63
  def self.row(instance, aggregate, cols, domain)
43
64
  cells = cols.map { |name| "<td>#{Escape.html(cell(instance, name))}</td>" }.join
44
65
  # L12 — the id is free-form (S3): percent-encoded as the path
@@ -50,6 +71,12 @@ module Hecks
50
71
  "<tr><td><a href=\"#{Escape.attr(href)}\">#{Escape.html(instance.id)}</a></td>#{cells}</tr>"
51
72
  end
52
73
 
74
+ # Reads one field's value for display, unwrapped to a single cell value.
75
+ #
76
+ # @param instance [Runtime::Instance, Forms::Record] the record to read
77
+ # @param name [Symbol] the field name to read
78
+ # @return [Object] the field's own value; for a Hash-shaped value object, its
79
+ # first member's value; `""` for `nil`
53
80
  def self.cell(instance, name)
54
81
  # `state` holds `Runtime::Value` wherever an attribute is a value
55
82
  # object, not a plain Hash — `.materialize` is the runtime's own
@@ -7,12 +7,27 @@ module Hecks
7
7
  # resolve against the same repository the same way, and neither word
8
8
  # owns this more than the other.
9
9
  module ReferenceOptions
10
+ # Collects every `:reference` field's own dropdown options out of a resolved field
11
+ # tree.
12
+ #
13
+ # @param registry [Runtime::Registry] the booted registry to read repositories from
14
+ # @param domain [String] the owning chapter's name
15
+ # @param fields [Array<Forms::Field>] the resolved field tree to search
16
+ # @return [Hash{String => Array<Array(String, String)>, nil}] one entry per
17
+ # `:reference` field found, keyed by its `path`; `nil` when the field's target
18
+ # could not be resolved or its options could not be read
10
19
  def self.collect(registry, domain, fields)
11
20
  targets = {}
12
21
  walk(fields) { |field| targets[field.path] = field.target_aggregate if field.kind == :reference }
13
22
  targets.transform_values { |aggregate| aggregate && options_for(registry, domain, aggregate) }
14
23
  end
15
24
 
25
+ # Visits every field in a tree, depth-first, including each `:list`/`:group`
26
+ # field's own children.
27
+ #
28
+ # @param fields [Array<Forms::Field>] the field tree to walk
29
+ # @yieldparam field [Forms::Field] each field visited, parent before children
30
+ # @return [void]
16
31
  def self.walk(fields, &block)
17
32
  fields.each do |field|
18
33
  block.call(field)
@@ -20,6 +35,15 @@ module Hecks
20
35
  end
21
36
  end
22
37
 
38
+ # Reads up to 200 records' ids as `<select>` options, for one reference field's
39
+ # target aggregate.
40
+ #
41
+ # @param registry [Runtime::Registry] the booted registry to read the repository from
42
+ # @param domain [String] the owning chapter's name
43
+ # @param aggregate [Bluebook::Aggregate] the referenced aggregate to list
44
+ # @return [Array<Array(String, String)>, nil] up to 200 `[id, id]` pairs; `nil` when
45
+ # the repository cannot be resolved (a wiring gap degrades to a plain text id
46
+ # input, not a 500)
23
47
  def self.options_for(registry, domain, aggregate)
24
48
  registry.repository(domain, aggregate).all.first(200).map { |instance| [instance.id, instance.id] }
25
49
  rescue StandardError
@@ -1,11 +1,11 @@
1
1
  module Hecks
2
2
  module Forms
3
- # THE VALUE-OBJECT CLASSIFICATION every consumer of a resolved VO
3
+ # The value-object classification every consumer of a resolved VO
4
4
  # shape needs, spelled once — `field_shape.rb` (a command's own
5
5
  # form), `ui_schema.rb` (embryonaut_console's table/detail-panel
6
6
  # renderer, a separate app, not this repo), and
7
7
  # `adapters/driven/sql_query_builder.rb` (a query's own ORDER BY/
8
- # WHERE compiler) each grew their own copy of "is this VO money-
8
+ # where compiler) each grew their own copy of "is this VO money-
9
9
  # shaped," "does it have exactly one attribute, worth unwrapping,"
10
10
  # and "which member of it is numeric" — the same three questions,
11
11
  # answered identically by definition (a VO's shape doesn't change
@@ -18,15 +18,21 @@ module Hecks
18
18
  # every money-shaped VO in the corpus spells it exactly this way,
19
19
  # the two-attribute convention the language itself never enforces
20
20
  # but every real chapter follows.
21
+ # @param value_object [Bluebook::ValueObject] the value object to classify
22
+ # @return [Boolean] true if its attributes are exactly `cents` and `currency`
21
23
  def money?(value_object)
22
24
  value_object.attributes.map { |a| a.name.to_s }.sort == %w[cents currency]
23
25
  end
24
26
 
25
- # A VO with exactly one attribute is a NAME for a scalar, not a
27
+ # A VO with exactly one attribute is a name for a scalar, not a
26
28
  # genuine group (EmailAddress{address}, CustomerNumber{value}) —
27
29
  # [[feedback_name_the_scalar_field]]'s own reasoning, shared here
28
30
  # rather than re-decided per caller. Returns the sole attribute,
29
31
  # or nil for anything else.
32
+ #
33
+ # @param value_object [Bluebook::ValueObject] the value object to inspect
34
+ # @return [Bluebook::Attribute, nil] its one declared attribute, or nil if
35
+ # it declares zero or more than one
30
36
  def sole_attribute(value_object)
31
37
  return nil unless value_object.attributes.size == 1
32
38
 
@@ -38,6 +44,10 @@ module Hecks
38
44
  # money-shaped (money's own two members are handled by `money?`
39
45
  # instead, since which one governs ordering is a money-specific
40
46
  # decision, not a general "pick the first number" one).
47
+ #
48
+ # @param value_object [Bluebook::ValueObject] the value object to inspect
49
+ # @return [Bluebook::Attribute, nil] its first Integer- or Float-typed
50
+ # attribute, or nil if it declares none
41
51
  def numeric_member(value_object)
42
52
  value_object.attributes.find { |a| %w[Integer Float].include?(a.type.to_s) }
43
53
  end
data/lib/hecks/forms.rb CHANGED
@@ -2,7 +2,7 @@
2
2
  # `query_form.bluebook` will eventually become — see docs/
3
3
  # command-form-and-query-form-bluebook.md), the IR->HTML renderers, and
4
4
  # the Rack app that content-negotiates between them and a plain JSON
5
- # reading of the same dispatch. NOT required by `require "hecks"`
5
+ # reading of the same dispatch. Not required by `require "hecks"`
6
6
  # itself — a project that never boots this file never pays for `rack`,
7
7
  # the same lazy-dependency discipline the Gemfile's own comment already
8
8
  # holds `pg`/`oauth2`/`aws-sdk-lambda` to.
@@ -19,7 +19,7 @@ module Hecks
19
19
  # `expose` deliberately does and does not cover.
20
20
  module Forms
21
21
  # `expose`'s own declaration — "which chapters does this app expose" —
22
- # kept OUTSIDE the `Hecks.*` collector convention (`Hecks.bluebook`,
22
+ # kept outside the `Hecks.*` collector convention (`Hecks.bluebook`,
23
23
  # `Hecks.hecksagon`, ...) and outside `Runtime::Registry` entirely, on
24
24
  # purpose: a real language word goes through `syntax.bluebook` and
25
25
  # `MetaValidator` (see docs/implemented/guides/extending-hecks.md, "a new word is a
@@ -30,29 +30,49 @@ module Hecks
30
30
  # see docs/command-form-and-query-form-bluebook.md, "why this isn't
31
31
  # syntax.bluebook yet".
32
32
  #
33
- # ONE `expose` GRANTS A WHOLE CHAPTER, not a command or a query
33
+ # One `expose` grants a whole chapter, not a command or a query
34
34
  # individually — the future `command_form.bluebook`/`query_form.bluebook`
35
- # words are PER-DECLARATION (one command, one query, its own form/view,
35
+ # words are per-declaration (one command, one query, its own form/view,
36
36
  # possibly its own overrides), which `expose` doesn't do today and was
37
37
  # never trying to; it's the coarse "turn this chapter's forms on at
38
38
  # all" switch those finer words will eventually sit inside.
39
39
  class Config
40
40
  attr_reader :name, :exposes
41
41
 
42
+ # @param name [String, Symbol] the app name this configuration is registered under;
43
+ # stored as a String
42
44
  def initialize(name)
43
45
  @name = name.to_s
44
46
  @exposes = []
45
47
  end
46
48
 
49
+ # Switches on the forms of one whole chapter for this app; called inside the
50
+ # `Forms.configure` block.
51
+ #
52
+ # @param chapter_name [String, Symbol] name of the chapter (domain) to expose, such as
53
+ # `"Banking"`; stored as a String
54
+ # @return [Array<String>] every chapter name exposed so far, in declaration order
47
55
  def expose(chapter_name) = @exposes << chapter_name.to_s
48
56
  end
49
57
 
58
+ # Declares an app and the chapters it exposes, replacing any earlier configuration
59
+ # registered under the same name.
60
+ #
61
+ # @param name [String, Symbol] the app name `App.for` later looks the configuration up by
62
+ # @yield evaluated with `instance_eval` against the new `Forms::Config`, so a bare
63
+ # `expose "Banking"` inside the block reaches `Config#expose`; optional
64
+ # @return [Forms::Config] the configuration just registered
50
65
  def self.configure(name, &block)
51
66
  config = Config.new(name)
52
67
  config.instance_eval(&block) if block
53
68
  (@configs ||= {})[config.name] = config
54
69
  end
55
70
 
71
+ # Looks up the configuration an earlier `Forms.configure` registered.
72
+ #
73
+ # @param name [String, Symbol] the app name given to `Forms.configure`
74
+ # @return [Forms::Config, nil] the app's configuration, or nil when no app of that name
75
+ # has been configured
56
76
  def self.config(name) = (@configs || {})[name.to_s]
57
77
  end
58
78
  end
data/lib/hecks/fqn.rb CHANGED
@@ -8,20 +8,52 @@ module Hecks
8
8
 
9
9
  attr_reader :realm, :domain, :version, :aggregate, :verb, :kind
10
10
 
11
+ # Builds the FQN of one command on an aggregate.
12
+ #
13
+ # @param realm [String, nil] deployment identity from the world, or nil for an
14
+ # unrealmed address
15
+ # @param domain [String] the domain name
16
+ # @param aggregate [String] the aggregate name the command belongs to
17
+ # @param command [String] the command's PascalCase verb
18
+ # @param version [String, nil] the pinned domain version, or nil for the
19
+ # world's configured latest alias
20
+ # @return [Fqn] the command address
21
+ # @raise [Fqn::Invalid] if any segment is empty or contains a separator, or if
22
+ # `command` is not a valid PascalCase command name
11
23
  def self.command(realm:, domain:, aggregate:, command:, version: nil)
12
24
  new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: command, kind: :command)
13
25
  end
14
26
 
27
+ # Builds the FQN of one query, either on an aggregate or domain-level.
28
+ #
29
+ # @param realm [String, nil] deployment identity from the world, or nil for an
30
+ # unrealmed address
31
+ # @param domain [String] the domain name
32
+ # @param query [String] the query's snake_case verb
33
+ # @param aggregate [String, nil] the aggregate the query belongs to, or nil for
34
+ # a domain-level read model
35
+ # @param version [String, nil] the pinned domain version, or nil for the
36
+ # world's configured latest alias
37
+ # @return [Fqn] the query address
38
+ # @raise [Fqn::Invalid] if any segment is empty or contains a separator, or if
39
+ # `query` is not a valid snake_case query name
15
40
  def self.query(realm:, domain:, query:, aggregate: nil, version: nil)
16
41
  new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: query, kind: :query)
17
42
  end
18
43
 
19
- # ONE ORDER-DEPENDENT PARSE PIPELINE: split -> shape-validate -> dispatch
44
+ # One order-dependent parse pipeline: split -> shape-validate -> dispatch
20
45
  # on segment count -> split domain/version -> classify kind -> cross-
21
46
  # field validate -> construct. Each step consumes locals (segments, verb,
22
47
  # kind) the step before it derived; splitting would mean threading all of
23
48
  # them back out as parameters/returns between new methods, for no
24
49
  # readability gain over reading the pipeline top to bottom once.
50
+ #
51
+ # @param text [String] a `Realm::Domain::Aggregate.verb` address, with domain
52
+ # optionally `@version`-pinned and aggregate optional for a domain-level query
53
+ # @return [Fqn] the parsed address
54
+ # @raise [Fqn::Invalid] if `text` is not shaped like a FQN, its verb is neither
55
+ # PascalCase nor snake_case, its domain version is malformed, or it names a
56
+ # domain-level command
25
57
  # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
26
58
  def self.parse(text)
27
59
  head, separator, verb = text.to_s.rpartition(".")
@@ -53,9 +85,28 @@ module Hecks
53
85
  new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: verb, kind: kind)
54
86
  end
55
87
 
88
+ # Whether `name` is a valid command verb: PascalCase.
89
+ #
90
+ # @param name [String, Symbol, #to_s] the candidate verb
91
+ # @return [Boolean] true if `name` matches the PascalCase command shape
56
92
  def self.command_name?(name) = /\A[A-Z][A-Za-z0-9]*\z/.match?(name.to_s)
93
+
94
+ # Whether `name` is a valid query verb: snake_case.
95
+ #
96
+ # @param name [String, Symbol, #to_s] the candidate verb
97
+ # @return [Boolean] true if `name` matches the snake_case query shape
57
98
  def self.query_name?(name) = /\A[a-z][a-z0-9_]*\z/.match?(name.to_s)
58
99
 
100
+ # @param realm [String, nil] deployment identity from the world, or nil for an
101
+ # unrealmed address
102
+ # @param domain [String] the domain name
103
+ # @param aggregate [String, nil] the aggregate name, or nil for a domain-level query
104
+ # @param verb [String] the command or query verb
105
+ # @param kind [Symbol, String] `:command` or `:query`
106
+ # @param version [String, nil] the pinned domain version, or nil for the
107
+ # world's configured latest alias
108
+ # @raise [Fqn::Invalid] if any segment is empty or contains a separator, or if
109
+ # `verb` does not match the shape required by `kind`
59
110
  def initialize(realm:, domain:, aggregate:, verb:, kind:, version: nil)
60
111
  @realm = realm && segment(realm, "realm")
61
112
  @domain = segment(domain, "domain")
@@ -68,7 +119,14 @@ module Hecks
68
119
  raise Invalid, "#{@kind} FQN has an invalid verb #{@verb.inspect}" unless valid
69
120
  end
70
121
 
122
+ # Whether this address names a command.
123
+ #
124
+ # @return [Boolean] true if this address's kind is `:command`
71
125
  def command? = @kind == :command
126
+
127
+ # Whether this address names a query.
128
+ #
129
+ # @return [Boolean] true if this address's kind is `:query`
72
130
  def query? = @kind == :query
73
131
 
74
132
  def to_s
@@ -2,6 +2,15 @@ Hecks.bluebook "Governance" do
2
2
  vision "Organizational authority is explicit, reviewable, and historically explainable."
3
3
  supporting
4
4
 
5
+ # WHAT THIS CHAPTER ANSWERS FOR EVERY DOMAIN THAT ATTACHES IT. The role
6
+ # check at dispatch, boot's refusal of an ungoverned `role`, the
7
+ # authorization adapter and the fuzzer's grant steering all read these
8
+ # three verbs from here, rather than recognising "Governance" by name.
9
+ provides "authorization",
10
+ assignments: "RoleAssignment.AssignmentsForActor",
11
+ grant: "RoleAssignment.Assign",
12
+ transitions: "RoleTransition.Allowed"
13
+
5
14
  aggregate "RoleAssignment" do
6
15
  description "One actor holding one role, over one scope, from one point in time — revoked by setting when it ended, never deleted."
7
16
 
@@ -0,0 +1,155 @@
1
+ Hecks.bluebook "Privacy" do
2
+ vision "Which fields of a domain's own aggregates are sensitive is a fact about that domain, declared apart from it — the domain itself never spells out that one of its own attributes is PII or PHI."
3
+ supporting
4
+
5
+ # A DOMAIN ATTACHING THIS NEVER MARKS ITSELF — `Mark`'s `role
6
+ # "Deployment"` matches every other wiring-time command in this
7
+ # codebase (`Hecksagon::UsesFramework`, `Tenancy::Tenant.Register`):
8
+ # something outside the domain's own bluebook decides a field is
9
+ # sensitive, the same way persistence binds are a wiring decision
10
+ # and not baked into an aggregate's own declaration.
11
+ aggregate "Marking" do
12
+ description "One attribute of one domain's own aggregate, flagged sensitive — and the role required to read it unredacted."
13
+
14
+ attribute :domain, MarkingDomain
15
+ attribute :attribute_path, AttributePath
16
+ attribute :category, Category
17
+ attribute :readable_by, RoleName
18
+
19
+ identified_by :domain, :attribute_path
20
+
21
+ value_object "MarkingDomain" do
22
+ attribute :value, String
23
+ invariant("a marking names the domain it applies to") { !value.to_s.empty? }
24
+ end
25
+
26
+ # DOTTED, same convention Forms' own `Field#path` and a query's
27
+ # cross-object `where` clause already use — `"attendee.medications"`,
28
+ # not a bare leaf name, so a marking can reach into a nested value
29
+ # object rather than only a top-level attribute.
30
+ value_object "AttributePath" do
31
+ attribute :value, String
32
+ invariant("a marking names an attribute path") { !value.to_s.empty? }
33
+ end
34
+
35
+ # OPEN-ENDED ON PURPOSE — "pii", "phi", whatever else a consuming
36
+ # domain finds itself needing, none of it enumerated here. This
37
+ # bluebook is the mechanism (mark, gate, redact); it does not also
38
+ # own the vocabulary of sensitivity categories that exist in the
39
+ # world.
40
+ value_object "Category" do
41
+ attribute :value, String
42
+ invariant("a marking is categorized") { !value.to_s.empty? }
43
+ end
44
+
45
+ value_object "RoleName" do
46
+ attribute :value, String
47
+ invariant("reading a marked field unredacted requires a named role") { !value.to_s.empty? }
48
+ end
49
+
50
+ command "Mark" do
51
+ role "Deployment"
52
+ goal "Flag one attribute of a domain's own aggregate as sensitive, gating who may read it unredacted"
53
+
54
+ attribute :domain, MarkingDomain
55
+ attribute :attribute_path, AttributePath
56
+ attribute :category, Category
57
+ attribute :readable_by, RoleName
58
+
59
+ emits Marked
60
+ end
61
+
62
+ command "Unmark" do
63
+ role "Deployment"
64
+ goal "Remove a sensitivity flag from an attribute"
65
+
66
+ reference_to Marking
67
+
68
+ emits Unmarked
69
+ end
70
+
71
+ query "ForDomain" do
72
+ description "Every marking declared for one domain, read by whatever gates or redacts that domain's own records."
73
+ attribute :domain, MarkingDomain
74
+ where(domain: :domain)
75
+ end
76
+ end
77
+
78
+ # **Cryptoshredding** — a right-to-erasure request satisfied without
79
+ # rewriting or deleting a single event. `key_reference` is an opaque
80
+ # handle a caller obtains from `Ports::KeyVault.issue` before
81
+ # dispatching `Issue`, the same "nothing is minted inside a bluebook"
82
+ # convention `Identity::Register`'s own `identity_id` and
83
+ # `Delivery::Record`'s own `message_id` already hold to (the
84
+ # expression sublanguage cannot call a port at all). The key material
85
+ # itself never appears here or on any emitted event — only the fact
86
+ # that a reference exists, and the fact that it was destroyed.
87
+ aggregate "SubjectKey" do
88
+ description "One data subject's own encryption key, from the moment it exists to the moment it is destroyed."
89
+
90
+ attribute :domain, KeyDomain
91
+ attribute :subject_id, SubjectId
92
+ attribute :key_reference, KeyReference
93
+
94
+ identified_by :domain, :subject_id
95
+
96
+ value_object "KeyDomain" do
97
+ attribute :value, String
98
+ invariant("a key names the domain its subject belongs to") { !value.to_s.empty? }
99
+ end
100
+
101
+ value_object "SubjectId" do
102
+ attribute :value, String
103
+ invariant("a key names the subject it was issued for") { !value.to_s.empty? }
104
+ end
105
+
106
+ value_object "KeyReference" do
107
+ attribute :value, String
108
+ invariant("a key carries the vault's own opaque reference to it") { !value.to_s.empty? }
109
+ end
110
+
111
+ # `Shred`'s own guard against running twice — the same mechanism
112
+ # `Compliance::PrivacyReview`'s `Clear`/`Escalate` already use rather
113
+ # than an optional timestamp plus a hand-written `given`.
114
+ lifecycle :status, default: "active" do
115
+ transition "Shred" => "shredded", from: "active"
116
+ end
117
+
118
+ command "Issue" do
119
+ role "System"
120
+ goal "Record that a subject's data is now encrypted under a fresh, dedicated key"
121
+
122
+ attribute :domain, KeyDomain
123
+ attribute :subject_id, SubjectId
124
+ attribute :key_reference, KeyReference
125
+
126
+ sets :domain
127
+ sets :subject_id
128
+ sets :key_reference
129
+
130
+ emits SubjectKeyIssued
131
+ end
132
+
133
+ # **Records destruction, never performs it** — the actual
134
+ # `Ports::KeyVault.destroy` call happens in driving-side code before
135
+ # this is ever dispatched, the same boundary `send_issue.rb` draws
136
+ # around `Delivery::Record` for an already-sent email. This
137
+ # command's own job is to make an already-irreversible fact
138
+ # durable, not to cause it.
139
+ command "Shred" do
140
+ role "Privacy officer"
141
+ goal "Mark this subject's key destroyed, so its ciphertext is recorded as permanently unrecoverable"
142
+
143
+ reference_to SubjectKey
144
+
145
+ emits SubjectKeyShredded
146
+ end
147
+
148
+ query "ForSubject" do
149
+ description "One subject's key record, checked before any attempt to decrypt their sensitive fields."
150
+ attribute :domain, KeyDomain
151
+ attribute :subject_id, SubjectId
152
+ where(domain: :domain, subject_id: :subject_id)
153
+ end
154
+ end
155
+ end
@@ -30,6 +30,21 @@
30
30
  "scope": "compliance:box_surrender_review.open",
31
31
  "verb": "Compliance::BoxSurrenderReview.Open",
32
32
  "role": "System"
33
+ },
34
+ {
35
+ "scope": "compliance:privacy_review.clear",
36
+ "verb": "Compliance::PrivacyReview.Clear",
37
+ "role": "Compliance officer"
38
+ },
39
+ {
40
+ "scope": "compliance:privacy_review.escalate",
41
+ "verb": "Compliance::PrivacyReview.Escalate",
42
+ "role": "Compliance officer"
43
+ },
44
+ {
45
+ "scope": "compliance:privacy_review.open",
46
+ "verb": "Compliance::PrivacyReview.Open",
47
+ "role": "System"
33
48
  }
34
49
  ],
35
50
  "roles": [