hecks 1.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 830e92d9660dc86cc7de1af49d85fd7ce5ab611e07341dd11f4633274b920fda
4
- data.tar.gz: 1c70b3c2994fd520a604ebd8c8cc8a53e7531e3cb5ba1dd59c6fd751b9b9dd99
3
+ metadata.gz: 29c54ce86e36189a63578e970929e42c213d5812282c5cf5ca6e56820eb02938
4
+ data.tar.gz: 1d564ee387fb38c619fbdfef209bcc757570a492e70dac16f1d5fb80889c37e8
5
5
  SHA512:
6
- metadata.gz: 3aeb0240903bed58fbd5684c23ee2069e9b3021aef07082d11e26ffa5052cebccf42d78e3aff243ba192183460056113e0510c7667306d1ed2cde3048ad118c0
7
- data.tar.gz: e104c2252f0dbbb1ba5ea97a0f6f56f890cab76e12ff86f246605003738228b94062e317f16db1ebfb376753536d8309cedff86e00275781497232de67b17e1e
6
+ metadata.gz: 24794f4aaa4f5d2268f27e063b79a23b61c7dffe7b4a87b4b58f83db73188d57716fd734d695082bdf11d78d3101fc41640a45f76510fe7cc45fd9c3cd9347a8
7
+ data.tar.gz: 9ce51558bbe7c55e9dc451140a21d7c83b7073b8a692304579f15c76db3c56371cb90c7cac07b6c02e6eb64ddd73c27572e786ab941659d2a76e649de5d2a699
@@ -133,6 +133,111 @@ Hecks.bluebook "Deploy" do
133
133
  end
134
134
  end
135
135
 
136
+ # A sibling of `LambdaTarget`, not a shared shape with it — value objects
137
+ # are not shared across aggregates in this DSL (`Tenant`'s own
138
+ # `DomainName`/`Realm`/`Schema` below already make the same point), so
139
+ # this aggregate carries its own copies of `DomainName`/`Region`/
140
+ # `Database`/`Web` rather than reaching into `LambdaTarget`'s.
141
+ aggregate "FargateTarget" do
142
+ description "One domain's validated AWS Fargate deploy target — what deployed_to(\"AwsFargate\") must resolve to before bin/project_deploy generates anything from it."
143
+
144
+ identified_by :domain
145
+
146
+ attribute :domain, DomainName
147
+ attribute :region, Region
148
+ attribute :cpu, Cpu
149
+ attribute :memory, Memory
150
+ attribute :database, Database
151
+ attribute :web, Web
152
+ attribute :port, Port
153
+
154
+ value_object "DomainName" do
155
+ attribute :value, String
156
+ invariant("a domain is named") { !value.to_s.empty? }
157
+ end
158
+
159
+ value_object "Region" do
160
+ attribute :value, String
161
+ invariant("a region is named") { !value.to_s.empty? }
162
+ end
163
+
164
+ # A positive integer, nothing more — AWS's real cpu/memory
165
+ # combinations (256 cpu units needs 512-2048 MB of `memory`, say)
166
+ # are a pairing `ECS` itself enforces at deploy time; encoding that
167
+ # whole table here would duplicate AWS's own rules for no benefit
168
+ # this generator needs. This only catches a typo (zero, negative)
169
+ # before a single file is generated, the same floor `Memory`/`Port`
170
+ # below hold for their own ranges.
171
+ value_object "Cpu" do
172
+ attribute :value, Integer
173
+ invariant("cpu is positive") { value.positive? }
174
+ end
175
+
176
+ # Fargate's own valid range, not Lambda's 128-10240 MB —
177
+ # `LambdaTarget`'s own `Memory` value object (above) is scoped to
178
+ # that aggregate alone, so this is a distinct value object under the
179
+ # same name rather than a shared one. A plain sanity floor and
180
+ # ceiling, not AWS's own cpu-paired table (see `Cpu`'s own comment).
181
+ value_object "Memory" do
182
+ attribute :value, Integer
183
+ invariant("memory is positive") { value.positive? }
184
+ invariant("memory is at most 122880 MB") { value <= 122880 }
185
+ end
186
+
187
+ # Same shape and reasoning as `LambdaTarget`'s own `Database` —
188
+ # "Postgres"/"Aurora" provision this domain's own RDS instance;
189
+ # "Shared" borrows another already-deployed domain's instance,
190
+ # isolated by a native Postgres schema. See that value object's own
191
+ # comment for the full reasoning; it applies unchanged here.
192
+ value_object "Database" do
193
+ attribute :value, String, one_of: ["Postgres", "Aurora", "Shared"]
194
+ end
195
+
196
+ # Same shape and reasoning as `LambdaTarget`'s own `Web` — "Rust"
197
+ # means the domain's own task also serves its public web UI
198
+ # in-process, over the port this aggregate's own `Port` attribute
199
+ # declares.
200
+ value_object "Web" do
201
+ attribute :value, String, one_of: ["None", "Rust"]
202
+ end
203
+
204
+ # The container port the task listens on and the target group
205
+ # routes to — a real TCP port, not an arbitrary integer.
206
+ value_object "Port" do
207
+ attribute :value, Integer
208
+ invariant("a port is at least 1") { value >= 1 }
209
+ invariant("a port is at most 65535") { value <= 65535 }
210
+ end
211
+
212
+ # All seven required, for the identical reason `LambdaTarget.Declare`
213
+ # requires all of its own attributes — see that command's own
214
+ # comment on `normalize_args`/`coerce_declared_arguments`. This
215
+ # validates a fully-resolved target, after `bin/project_deploy`'s own
216
+ # defaulting (`cpu`/`memory`/`port` each fall back to a Ruby-side
217
+ # default before this ever dispatches), not instead of it.
218
+ command "Declare" do
219
+ goal "Validate one domain's AwsFargate deploy target before anything is generated from it"
220
+
221
+ attribute :domain, DomainName
222
+ attribute :region, Region
223
+ attribute :cpu, Cpu
224
+ attribute :memory, Memory
225
+ attribute :database, Database
226
+ attribute :web, Web
227
+ attribute :port, Port
228
+
229
+ sets :domain
230
+ sets :region
231
+ sets :cpu
232
+ sets :memory
233
+ sets :database
234
+ sets :web
235
+ sets :port
236
+
237
+ emits "FargateTargetDeclared"
238
+ end
239
+ end
240
+
136
241
  # `bin/project_tenant`'S OWN VALIDATED SHAPE — same split LambdaTarget
137
242
  # draws with `bin/project_deploy`: this aggregate validates, that
138
243
  # script GENERATES/ACTS (writes `environments/<slug>.world`, ensures
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "audience": "Deploy",
3
3
  "scopes": [
4
+ {
5
+ "scope": "deploy:fargate_target.declare",
6
+ "verb": "Deploy::FargateTarget.Declare",
7
+ "role": null
8
+ },
4
9
  {
5
10
  "scope": "deploy:lambda_target.declare",
6
11
  "verb": "Deploy::LambdaTarget.Declare",
@@ -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
@@ -34,6 +34,33 @@ module Hecks
34
34
  # already destroyed, or was never issued
35
35
  def destroy(registry, key_reference:) = adapter(registry).destroy(key_reference: key_reference)
36
36
 
37
+ # Erases one subject: destroys their key in the vault, then durably records that it
38
+ # happened — in that fixed order, so a caller cannot dispatch `Shred` before the key
39
+ # is actually gone the way calling {#destroy} and `dispatch_flat` separately would
40
+ # allow. `Privacy::SubjectKey.Shred`'s own lifecycle guard (`from: "active"`) still
41
+ # refuses a raw double-dispatch; this is what closes the gap one level up, where the
42
+ # danger isn't a duplicate `Shred` but `Shred` running *before* {#destroy} does.
43
+ #
44
+ # Idempotent: a subject with no live `SubjectKey` record, or one already shredded,
45
+ # is left alone and answered `false` — a retried erasure request never re-destroys
46
+ # an already-gone key or double-dispatches `Shred`.
47
+ #
48
+ # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
49
+ # dispatcher both the lookup query and the `Shred` dispatch run through
50
+ # @param domain [String] the subject's own domain FQN, e.g. `"Lifeadelics::Registration"`
51
+ # @param subject_id [String] the data subject to erase
52
+ # @return [Boolean] true when a live key was found and shredded by this call; false
53
+ # when no `SubjectKey` record exists for this subject, or it was already shredded
54
+ def shred!(dispatcher, domain:, subject_id:) # rubocop:disable Naming/PredicateMethod
55
+ record = dispatcher.query("Privacy::SubjectKey.ForSubject", domain: domain, subject_id: subject_id).first
56
+ return false unless record
57
+ return false if record[:status].to_s == "shredded"
58
+
59
+ destroy(dispatcher.registry, key_reference: record[:key_reference].value)
60
+ dispatcher.dispatch_flat("Privacy::SubjectKey.Shred", domain: { value: domain }, subject_id: { value: subject_id })
61
+ true
62
+ end
63
+
37
64
  # Finds the single adapter bound to this port, refusing an ambiguous wiring.
38
65
  #
39
66
  # @param registry [Runtime::Registry] the booted registry to search