hecks 1.4.0 → 1.5.1

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 (275) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
  112. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  113. data/lib/hecks/deploy/oidc.json +5 -0
  114. data/lib/hecks/doc/reference.rb +185 -16
  115. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  116. data/lib/hecks/facade/handle.rb +76 -3
  117. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  118. data/lib/hecks/forms/field_shape.rb +3 -0
  119. data/lib/hecks/forms/page.rb +14 -0
  120. data/lib/hecks/forms/port_argument.rb +12 -0
  121. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  122. data/lib/hecks/forms/record_renderer.rb +58 -0
  123. data/lib/hecks/forms/record_table.rb +27 -0
  124. data/lib/hecks/forms/reference_options.rb +24 -0
  125. data/lib/hecks/forms/value_object_shape.rb +10 -0
  126. data/lib/hecks/fqn.rb +58 -0
  127. data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
  128. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  129. data/lib/hecks/framework/oidc.json +15 -0
  130. data/lib/hecks/framework.rb +43 -20
  131. data/lib/hecks/freezer.rb +17 -1
  132. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  133. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  134. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  135. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  136. data/lib/hecks/fuzzing/differential.rb +34 -0
  137. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  138. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  139. data/lib/hecks/fuzzing/form_census.rb +86 -0
  140. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  141. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  142. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  143. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  144. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  145. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  146. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  147. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  148. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  149. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  150. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  151. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  152. data/lib/hecks/fuzzing/properties.rb +24 -15
  153. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  154. data/lib/hecks/fuzzing/replay.rb +137 -29
  155. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  156. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  157. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  158. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  159. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  160. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  161. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  162. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  163. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  164. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  165. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  166. data/lib/hecks/grammar/evolve.rb +178 -2
  167. data/lib/hecks/grammar.rb +46 -0
  168. data/lib/hecks/ir.rb +38 -7
  169. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  170. data/lib/hecks/literal.rb +32 -0
  171. data/lib/hecks/naming.rb +88 -7
  172. data/lib/hecks/ports/access_control.rb +5 -10
  173. data/lib/hecks/ports/authorization.rb +3 -6
  174. data/lib/hecks/ports/identity_assignment.rb +1 -2
  175. data/lib/hecks/ports/identity_resolution.rb +1 -2
  176. data/lib/hecks/ports/key_vault.port +6 -0
  177. data/lib/hecks/ports/key_vault.rb +85 -0
  178. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  179. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  180. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  182. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  183. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  184. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  185. data/lib/hecks/ports/query/in_memory.rb +39 -6
  186. data/lib/hecks/ports/query/ordering.rb +15 -0
  187. data/lib/hecks/ports.rb +1 -0
  188. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  189. data/lib/hecks/projections/deploy/fargate.rb +666 -0
  190. data/lib/hecks/projections/deploy/lambda.rb +2423 -0
  191. data/lib/hecks/projections/deploy/shared.rb +624 -0
  192. data/lib/hecks/projections/diagrams.rb +243 -7
  193. data/lib/hecks/projections/glossary/html.rb +88 -0
  194. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  195. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  196. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  197. data/lib/hecks/projections/glossary.rb +161 -19
  198. data/lib/hecks/projections/model/deviations.rb +44 -0
  199. data/lib/hecks/projections/model.rb +51 -1
  200. data/lib/hecks/projections/oidc.rb +40 -3
  201. data/lib/hecks/projections/parser_table.rb +49 -8
  202. data/lib/hecks/projections/reference.rb +12 -0
  203. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  204. data/lib/hecks/projections/shape.rb +8 -0
  205. data/lib/hecks/projections/statements.rb +63 -16
  206. data/lib/hecks/projections/vocabulary.rb +17 -0
  207. data/lib/hecks/projections.rb +3 -0
  208. data/lib/hecks/projector/cli_projector.rb +218 -10
  209. data/lib/hecks/projector/docs_projector.rb +145 -19
  210. data/lib/hecks/projector/exporter.rb +65 -11
  211. data/lib/hecks/projector/ir_projector.rb +6 -0
  212. data/lib/hecks/projector/narrate_projector.rb +136 -15
  213. data/lib/hecks/projector/target.rb +47 -10
  214. data/lib/hecks/projector.rb +101 -19
  215. data/lib/hecks/query_ir.rb +47 -0
  216. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  217. data/lib/hecks/rendering.rb +6 -0
  218. data/lib/hecks/router/namespace_installer.rb +13 -0
  219. data/lib/hecks/router.rb +55 -0
  220. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  221. data/lib/hecks/runtime/boot_gates.rb +18 -0
  222. data/lib/hecks/runtime/caller.rb +32 -0
  223. data/lib/hecks/runtime/capability_graph.rb +11 -0
  224. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  225. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  226. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  227. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  228. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  229. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  230. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  231. data/lib/hecks/runtime/dispatcher.rb +28 -50
  232. data/lib/hecks/runtime/entity_element.rb +161 -8
  233. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  234. data/lib/hecks/runtime/errors.rb +18 -4
  235. data/lib/hecks/runtime/event.rb +10 -5
  236. data/lib/hecks/runtime/identity.rb +71 -3
  237. data/lib/hecks/runtime/instance.rb +67 -7
  238. data/lib/hecks/runtime/interpreting.rb +13 -5
  239. data/lib/hecks/runtime/invocation.rb +118 -36
  240. data/lib/hecks/runtime/loader.rb +94 -8
  241. data/lib/hecks/runtime/outbox.rb +145 -7
  242. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  243. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  244. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  245. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  246. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  247. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  248. data/lib/hecks/runtime/reference_hop.rb +42 -0
  249. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  250. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  251. data/lib/hecks/runtime/registry/verification.rb +119 -4
  252. data/lib/hecks/runtime/registry.rb +157 -4
  253. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  254. data/lib/hecks/runtime/routing.rb +27 -2
  255. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  256. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  257. data/lib/hecks/runtime/tenant_check.rb +26 -6
  258. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  259. data/lib/hecks/runtime/value/coercion.rb +255 -33
  260. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  261. data/lib/hecks/runtime/value.rb +50 -0
  262. data/lib/hecks/runtime.rb +32 -0
  263. data/lib/hecks/storehouse.rb +305 -9
  264. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  265. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  266. data/lib/hecks/version.rb +1 -1
  267. data/lib/hecks.rb +79 -1
  268. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  269. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  270. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  271. metadata +14 -5
  272. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  273. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  274. data/lib/hecks/deprecation.rb +0 -95
  275. data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
@@ -0,0 +1,221 @@
1
+ Hecks.bluebook "Compliance" do
2
+ vision "Something elsewhere already acted to contain a risk; this domain tracks the human review that decides what happens next."
3
+ supporting
4
+
5
+ # LIVES HERE (lib/hecks/framework/bluebook/), NOT under some
6
+ # domain's own examples/ directory, but that's a statement about
7
+ # SOURCE SHARING, not about how it's consumed — unlike Governance/
8
+ # Identity (which every real attaching domain only ever wants IN-
9
+ # PROCESS), Compliance genuinely supports BOTH modes from the same
10
+ # source: a domain that wants Review handled locally can
11
+ # `uses_framework "Compliance"` exactly like those two; a domain that
12
+ # wants it as a real, separately-deployed service — Banking's own
13
+ # choice, examples/compliance/ — reaches it through `across
14
+ # "Compliance"` on a policy instead, genuine cross-Lambda delivery.
15
+ # Nothing about this bluebook's own declarations changes between the
16
+ # two; only whether the CONSUMER merges it in or deploys it apart.
17
+ #
18
+ # examples/compliance/bluebook/compliance.bluebook is a SYMLINK back
19
+ # to this file (not the other way around, as it briefly was in
20
+ # 1.5.0) — a symlink pointing outside lib/ never survives `gem
21
+ # build` (RubyGems drops it, warning "not supported on all
22
+ # platforms"), so the real content has to live in the tree the
23
+ # gemspec actually packages.
24
+ #
25
+ # CONTAIN FIRST, INVESTIGATE SECOND — the real pattern this domain
26
+ # exists for. Banking's own "Compliance officer" role already acts
27
+ # directly and synchronously in three places (Customer.Suspend/
28
+ # Reinstate, Account.Freeze/Unfreeze, OnboardingCase.Clear/Decline) —
29
+ # this domain is deliberately NOT a fourth. By the time either
30
+ # aggregate below is opened, the containing action (a freeze, a box
31
+ # surrender) has ALREADY happened, unilaterally, elsewhere; this
32
+ # domain never asks permission, it is TOLD a fact and gives the same
33
+ # officer a place to record what they decided about it afterward.
34
+ #
35
+ # TWO AGGREGATES, NOT ONE — found live, designing this: Banking's own
36
+ # `ReviewOnFreeze`/`ReviewOnBoxSurrender` policies forward their
37
+ # triggering event's payload VERBATIM (neither declares a `with:`
38
+ # projection, which policies have gained since), and the two events
39
+ # carry genuinely different fields (`AccountFrozen`: `number`;
40
+ # `BoxSurrendered`: `branch_code` + `box_number`). One shared
41
+ # aggregate would need an artificial union of both shapes — and while
42
+ # `with:` could now reshape either side into it, that would be
43
+ # inventing a sameness the domain does not have. Two small aggregates,
44
+ # each identified by its own subject's own natural key, need neither.
45
+
46
+ aggregate "AccountFreezeReview" do
47
+ description "The compliance review a frozen account gets, opened the moment Banking::Account.Freeze already happened."
48
+
49
+ attribute :number, AccountNumber
50
+
51
+ identified_by :number
52
+
53
+ value_object "AccountNumber" do
54
+ attribute :value, String
55
+ invariant("an account number is present") { !value.to_s.empty? }
56
+ end
57
+
58
+ lifecycle :status, default: "open" do
59
+ transition "Clear" => "cleared", from: "open"
60
+ transition "Escalate" => "escalated", from: "open"
61
+ end
62
+
63
+ # SYSTEM-TRIGGERED, matching `AccountFrozen`'s own payload shape
64
+ # field for field — `Banking::Account.AccountFrozen`'s `number` is
65
+ # the ONLY field this ever receives (ReviewOnFreeze's own `on
66
+ # "Account.AccountFrozen"`), so this declares exactly that and
67
+ # nothing more; an unrecognized field would refuse at the JSON
68
+ # boundary rather than silently drop it.
69
+ command "Open" do
70
+ role "System"
71
+ goal "Register that a frozen account needs compliance review"
72
+
73
+ sets :number
74
+
75
+ emits AccountFreezeReviewOpened
76
+ end
77
+
78
+ command "Clear" do
79
+ role "Compliance officer"
80
+ goal "Confirm the freeze was warranted, or resolve it, with nothing further to escalate"
81
+
82
+ reference_to AccountFreezeReview
83
+
84
+ emits AccountFreezeReviewCleared
85
+ end
86
+
87
+ command "Escalate" do
88
+ role "Compliance officer"
89
+ goal "Send a genuine finding on to whatever handles it next — a formal filing, a longer hold, a referral"
90
+
91
+ reference_to AccountFreezeReview
92
+
93
+ emits AccountFreezeReviewEscalated
94
+ end
95
+ end
96
+
97
+ aggregate "BoxSurrenderReview" do
98
+ description "The compliance review a surrendered safe deposit box gets — audit/escheatment implications, not a fraud contain the way a freeze is, but the same after-the-fact review shape."
99
+
100
+ attribute :branch_code, BranchCode
101
+ attribute :box_number, BoxNumber
102
+
103
+ identified_by :branch_code, :box_number
104
+
105
+ value_object "BranchCode" do
106
+ attribute :value, String
107
+ invariant("a branch is coded") { !value.to_s.empty? }
108
+ end
109
+
110
+ value_object "BoxNumber" do
111
+ attribute :value, Integer
112
+ invariant("a box is numbered from one") { value.positive? }
113
+ end
114
+
115
+ lifecycle :status, default: "open" do
116
+ transition "Clear" => "cleared", from: "open"
117
+ transition "Escalate" => "escalated", from: "open"
118
+ end
119
+
120
+ # Matches `BoxSurrendered`'s own payload field for field
121
+ # (`ReviewOnBoxSurrender`'s own `on SafeDepositBox::BoxSurrendered`) — the same
122
+ # discipline `AccountFreezeReview.Open` holds itself to, above.
123
+ command "Open" do
124
+ role "System"
125
+ goal "Register that a surrendered box needs compliance review"
126
+
127
+ sets :branch_code
128
+ sets :box_number
129
+
130
+ emits BoxSurrenderReviewOpened
131
+ end
132
+
133
+ command "Clear" do
134
+ role "Compliance officer"
135
+ goal "Confirm the surrender needs no further compliance action"
136
+
137
+ reference_to BoxSurrenderReview
138
+
139
+ emits BoxSurrenderReviewCleared
140
+ end
141
+
142
+ command "Escalate" do
143
+ role "Compliance officer"
144
+ goal "Send a genuine finding on to whatever handles it next"
145
+
146
+ reference_to BoxSurrenderReview
147
+
148
+ emits BoxSurrenderReviewEscalated
149
+ end
150
+ end
151
+
152
+ # A THIRD SHAPE, NOT A UNION OF THE OTHER TWO — the same restraint the
153
+ # header above holds Banking's own two events to. `Privacy::Marking`'s
154
+ # own `Marked` event carries `domain`/`attribute_path`, matching
155
+ # neither `AccountFrozen` (`number`) nor `BoxSurrendered`
156
+ # (`branch_code`/`box_number`), so this gets its own aggregate rather
157
+ # than an artificial union.
158
+ #
159
+ # REACHED VIA `translates`, NOT a `policy` this bluebook declares —
160
+ # which foreign domain's event a consumer conforms to is a wiring
161
+ # decision (see `HecksagonBuilder#translates`'s own header), so the
162
+ # reaction lives in whichever `.hecksagon` attaches both Privacy and
163
+ # Compliance, not here.
164
+ aggregate "PrivacyReview" do
165
+ description "The compliance review a newly marked sensitive field gets, opened the moment Privacy::Marking.Mark already happened."
166
+
167
+ attribute :domain, Domain
168
+ attribute :attribute_path, AttributePath
169
+
170
+ identified_by :domain, :attribute_path
171
+
172
+ value_object "Domain" do
173
+ attribute :value, String
174
+ invariant("a review names the domain whose attribute was marked") { !value.to_s.empty? }
175
+ end
176
+
177
+ value_object "AttributePath" do
178
+ attribute :value, String
179
+ invariant("a review names the attribute that was marked") { !value.to_s.empty? }
180
+ end
181
+
182
+ lifecycle :status, default: "open" do
183
+ transition "Clear" => "cleared", from: "open"
184
+ transition "Escalate" => "escalated", from: "open"
185
+ end
186
+
187
+ # SYSTEM-TRIGGERED, matching only `Privacy::Marking`'s own identity
188
+ # fields — the `translates` block that reacts to `Marked` forwards
189
+ # just `domain`/`attribute_path`, the same restraint
190
+ # `AccountFreezeReview.Open`/`BoxSurrenderReview.Open` already hold
191
+ # themselves to above; `category`/`readable_by` stay on the
192
+ # marking itself, not duplicated here.
193
+ command "Open" do
194
+ role "System"
195
+ goal "Register that a newly marked sensitive field needs compliance review"
196
+
197
+ sets :domain
198
+ sets :attribute_path
199
+
200
+ emits PrivacyReviewOpened
201
+ end
202
+
203
+ command "Clear" do
204
+ role "Compliance officer"
205
+ goal "Confirm the marking was warranted, or resolve it, with nothing further to escalate"
206
+
207
+ reference_to PrivacyReview
208
+
209
+ emits PrivacyReviewCleared
210
+ end
211
+
212
+ command "Escalate" do
213
+ role "Compliance officer"
214
+ goal "Send a genuine finding on to whatever handles it next"
215
+
216
+ reference_to PrivacyReview
217
+
218
+ emits PrivacyReviewEscalated
219
+ end
220
+ end
221
+ end
@@ -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": [
@@ -6,6 +6,8 @@ module Hecks
6
6
  # domain-agnostic chapters no single example owns, shared by reference
7
7
  # rather than copied into every domain that wants one.
8
8
  #
9
+ # ## Where it lives
10
+ #
9
11
  # Under `lib/`, not a top-level sibling — a real consumer (embryonaut)
10
12
  # vendors only `lib/` (`bin/vendor-hecks`, and this gem's own
11
13
  # `hecks.gemspec`, both glob `lib/**/*`), so a `framework/`
@@ -19,6 +21,8 @@ module Hecks
19
21
  # non-Ruby data a module owns lives inside `lib/` with the code that
20
22
  # reads it, not beside it.
21
23
  #
24
+ # ## Derived, not hand-listed
25
+ #
22
26
  # Derived from the directory, not hand-listed a second time — the same
23
27
  # reasoning `Assembly::CONTRACTS` gives for reading the language's own
24
28
  # fields from a table instead of restating them: a member added here
@@ -27,19 +31,13 @@ module Hecks
27
31
  # stale in silence. `spec/corpus_spec.rb`'s own `FRAMEWORK_MEMBERS`
28
32
  # glob is the precedent this mirrors.
29
33
  #
34
+ # ## Naming
35
+ #
30
36
  # Named by file stem, capitalized — `governance.bluebook` holds
31
37
  # `Hecks.bluebook "Governance"`, the same one-to-one spelling every
32
38
  # other chapter in this codebase already keeps between its filename
33
39
  # and its declared name.
34
40
  module Framework
35
- ROOT = File.expand_path("framework/bluebook", __dir__).freeze
36
-
37
- def self.members
38
- Dir.glob(File.join(ROOT, "*.bluebook")).to_h do |path|
39
- [Naming.pascal(File.basename(path, ".bluebook")), path]
40
- end
41
- end
42
-
43
41
  # Loaded from its own real path, always — never a copy. A domain
44
42
  # booted through `Fuzzing::IsolatedBoot`'s tmp-directory copy still
45
43
  # reaches the same `framework/bluebook/governance.bluebook` this
@@ -64,28 +62,36 @@ module Hecks
64
62
  # everywhere else too. See `examples/banking/bluebook/banking.hecksagon`
65
63
  # for the pattern — a real `.hecksagon` file can hold more than one
66
64
  # `Hecks.hecksagon` call, one per domain it wires.
67
- # Idempotent, per registry — `uses_framework` is now called more than
68
- # once for the same member within a single boot (S8: a domain
69
- # attaching Governance for its own role check, plus a framework
70
- # sibling attaching it too, e.g. `banking.hecksagon`'s Identity
71
- # block). `Kernel.load` always re-executes the file, unlike
72
- # `require`, so a second call would re-run `Hecks.bluebook
73
- # "Governance"` a second time — and the self-hosting meta-domain
74
- # records every declaration as a real dispatched command against its
75
- # own ledger, so a second `Declare` for the same aggregate is a real
76
- # `AlreadyExists`, not a no-op. Skipped once the member's bluebook is
77
- # already registered in this registry — the same chapter, not merely
78
- # a same-named one from a stale prior boot.
65
+ ROOT = File.expand_path("framework/bluebook", __dir__).freeze
66
+
67
+ # Every framework member available to attach, by declared name.
68
+ #
69
+ # @return [Hash{String => String}] each member's capitalized bluebook name,
70
+ # mapped to the absolute path of its `.bluebook` file
71
+ def self.members
72
+ Dir.glob(File.join(ROOT, "*.bluebook")).to_h do |path|
73
+ [Naming.pascal(File.basename(path, ".bluebook")), path]
74
+ end
75
+ end
76
+
79
77
  # Every member whose own bluebook declares `provides capability` —
80
78
  # read off the member's real IR (each loaded into a scratch registry,
81
79
  # never the caller's), not off its name. Used where a check needs to
82
80
  # say which member would satisfy it (`refuse_ungoverned_roles!`'s own
83
81
  # suggestion, `Fuzzing::TargetCapabilities`).
82
+ #
83
+ # @param capability [String, Symbol] the capability name to look for
84
+ # @return [Array<String>] the names of every framework member that provides it,
85
+ # sorted
84
86
  def self.providers_of(capability)
85
87
  members.keys.select { |name| chapter(name).provides?(capability) }.sort
86
88
  end
87
89
 
88
90
  # One member's chapter, built in isolation.
91
+ #
92
+ # @param name [String, Symbol] the member's name, such as `"Governance"`
93
+ # @return [Bluebook::Chapter] the member's chapter, loaded into a scratch registry
94
+ # @raise [Runtime::WiringError] if no framework member has that name
89
95
  def self.chapter(name)
90
96
  path = members.fetch(name.to_s) do
91
97
  raise Runtime::WiringError,
@@ -107,6 +113,23 @@ module Hecks
107
113
  registry.bluebook(name.to_s)
108
114
  end
109
115
 
116
+ # Idempotent, per registry — `uses_framework` is now called more than
117
+ # once for the same member within a single boot (S8: a domain
118
+ # attaching Governance for its own role check, plus a framework
119
+ # sibling attaching it too, e.g. `banking.hecksagon`'s Identity
120
+ # block). `Kernel.load` always re-executes the file, unlike
121
+ # `require`, so a second call would re-run `Hecks.bluebook
122
+ # "Governance"` a second time — and the self-hosting meta-domain
123
+ # records every declaration as a real dispatched command against its
124
+ # own ledger, so a second `Declare` for the same aggregate is a real
125
+ # `AlreadyExists`, not a no-op. Skipped once the member's bluebook is
126
+ # already registered in this registry — the same chapter, not merely
127
+ # a same-named one from a stale prior boot.
128
+ #
129
+ # @param name [String, Symbol] the member's name, such as `"Governance"`
130
+ # @return [Boolean, nil] true when this call loaded the member's bluebook, nil when
131
+ # the current registry already held it
132
+ # @raise [Runtime::WiringError] if no framework member has that name
110
133
  def self.load!(name)
111
134
  path = members.fetch(name.to_s) do
112
135
  raise Runtime::WiringError,
data/lib/hecks/freezer.rb CHANGED
@@ -8,7 +8,7 @@ module Hecks
8
8
  # Hash a field holds stays mutable, so a caller reaches straight
9
9
  # through and edits in place. Each fix looked complete and none was.
10
10
  #
11
- # What should be frozen, and why it is not everything:
11
+ # ## What should be frozen, and why it is not everything
12
12
  #
13
13
  # a value object has no identity to change over. `with` already
14
14
  # answers a new one rather than mutating, so freezing it through is
@@ -33,6 +33,10 @@ module Hecks
33
33
  # Numbers, symbols, nil and booleans are already immediate or frozen;
34
34
  # a Value froze itself when it was built. What is left is the mutable
35
35
  # trio, and each has to be walked rather than topped.
36
+ #
37
+ # @param held [Object] any domain value
38
+ # @return [Object] `held`, recursively frozen if it is a Hash, Array, or
39
+ # String; returned unchanged otherwise
36
40
  def deep(held)
37
41
  case held
38
42
  when Hash then held.each_value { |inner| deep(inner) }.freeze
@@ -45,11 +49,20 @@ module Hecks
45
49
  # The question a gate asks, rather than the act. Answers false for the
46
50
  # first thing that is reachable and mutable, which is what makes a
47
51
  # failure message worth reading.
52
+ #
53
+ # @param held [Object] any domain value
54
+ # @return [Boolean] true if `held` and everything reachable from it is frozen
48
55
  def deeply_frozen?(held) = unfrozen_within(held).nil?
49
56
 
50
57
  # The path to the first mutable thing reachable from `held`, or nil.
51
58
  # A path rather than a boolean because "something in this event is
52
59
  # mutable" is not an actionable sentence.
60
+ #
61
+ # @param held [Object] any domain value
62
+ # @param path [Array<String>] the owner path accumulated by the recursive
63
+ # walk so far; callers pass nothing and get the default
64
+ # @return [String, nil] the dotted path (Hash keys, Array indices, or
65
+ # `"(the value itself)"`) to the first mutable value found, or nil if none
53
66
  def unfrozen_within(held, path = [])
54
67
  return (path.empty? ? "(the value itself)" : path.join(".")) unless immune?(held) || held.frozen?
55
68
 
@@ -62,6 +75,9 @@ module Hecks
62
75
  # Immediates are frozen in every Ruby that matters, but asking
63
76
  # `frozen?` of them and trusting the answer has bitten enough people
64
77
  # that it is worth being explicit.
78
+ #
79
+ # @param held [Object] any domain value
80
+ # @return [Boolean] true if `held` is nil, true, false, a Numeric, or a Symbol
65
81
  def immune?(held) = held.nil? || held == true || held == false || held.is_a?(Numeric) || held.is_a?(Symbol)
66
82
  end
67
83
  end