hecks 1.5.0 → 2.0.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.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/identity_registry.rb +7 -1
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -1
- data/lib/hecks/bluebook/hexagon.rb +18 -3
- data/lib/hecks/corpus.rb +123 -9
- data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +11 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +9 -2
- data/lib/hecks/fuzzing/isolated_boot.rb +12 -2
- data/lib/hecks/language/bluebook/bluebook.bluebook +10 -1
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -0
- data/lib/hecks/ports/key_vault.rb +27 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +32 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +41 -0
- data/lib/hecks/projections/deploy/fargate.rb +913 -0
- data/lib/hecks/projections/deploy/lambda.rb +2440 -0
- data/lib/hecks/projections/deploy/shared.rb +624 -0
- data/lib/hecks/projections.rb +3 -0
- data/lib/hecks/projector/exporter.rb +48 -0
- data/lib/hecks/projector/target.rb +18 -5
- data/lib/hecks/projector.rb +31 -17
- data/lib/hecks/router/namespace_installer.rb +10 -0
- data/lib/hecks/runtime/registry/verification.rb +84 -2
- data/lib/hecks/runtime/registry.rb +94 -3
- data/lib/hecks/version.rb +1 -1
- metadata +5 -2
- 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
|
|
@@ -9,6 +9,17 @@ Hecks.bluebook "Identity" do
|
|
|
9
9
|
# identity provider is.
|
|
10
10
|
generic
|
|
11
11
|
|
|
12
|
+
# WHAT THIS CHAPTER ANSWERS FOR EVERY DOMAIN THAT ATTACHES IT. Same
|
|
13
|
+
# declared-not-named shape Governance's own `provides "authorization"`
|
|
14
|
+
# already is. rust/host does not build this chapter's payloads — any
|
|
15
|
+
# field mapping lives on the consuming hecksagon's `translates` ACL
|
|
16
|
+
# (any field; this chapter does not list which). Link's reference is
|
|
17
|
+
# `identity`, not `identity_id` and not `to:`.
|
|
18
|
+
provides "identity",
|
|
19
|
+
register: "Identity.Register",
|
|
20
|
+
link: "ExternalIdentifier.Link",
|
|
21
|
+
resolve: "ExternalIdentifier.ResolvedBy"
|
|
22
|
+
|
|
12
23
|
aggregate "Identity" do
|
|
13
24
|
description "A stable organizational identity, independent of how it was authenticated."
|
|
14
25
|
|
|
@@ -435,11 +435,18 @@ module Hecks
|
|
|
435
435
|
# @param schema [String] the PostgresEra schema name to write into the `.world` file
|
|
436
436
|
# @return [void]
|
|
437
437
|
def write_postgres_era_world!(copy, database:, schema:)
|
|
438
|
+
# Same merge IsolatedBoot.rebind_to_postgres_era! now does — one
|
|
439
|
+
# world file per directory, every hecksagon name in that directory.
|
|
440
|
+
worlds_by_dir = Hash.new { |h, k| h[k] = [] }
|
|
438
441
|
Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |hecksagon_path|
|
|
439
|
-
names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
|
|
442
|
+
names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
|
|
443
|
+
worlds_by_dir[File.dirname(hecksagon_path)].concat(names)
|
|
444
|
+
end
|
|
445
|
+
worlds_by_dir.each do |dir, names|
|
|
446
|
+
names = names.uniq
|
|
440
447
|
next if names.empty?
|
|
441
448
|
|
|
442
|
-
world_path = File.join(
|
|
449
|
+
world_path = File.join(dir, "hecks_fuzz_postgres_era.world")
|
|
443
450
|
File.write(world_path, names.map do |name|
|
|
444
451
|
<<~WORLD
|
|
445
452
|
Hecks.world "#{name}" do
|
|
@@ -404,11 +404,21 @@ module Hecks
|
|
|
404
404
|
# database it is about to throw away; the one-line warning
|
|
405
405
|
# PostgresEra prints per boot under the opt-in is the honest
|
|
406
406
|
# price. Inert on a machine whose ambient user is ordinary.
|
|
407
|
+
# One world file per directory, every hecksagon name in that
|
|
408
|
+
# directory — not one write per *.hecksagon file. context_map.hecksagon
|
|
409
|
+
# sits beside the domain file; a second File.write to the same
|
|
410
|
+
# hecks_fuzz_postgres_era.world would drop the first file's names
|
|
411
|
+
# (found live: ConcurrentDispatchUnboundFixture + Governance).
|
|
412
|
+
worlds_by_dir = Hash.new { |h, k| h[k] = [] }
|
|
407
413
|
Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |hecksagon_path|
|
|
408
|
-
names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
|
|
414
|
+
names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
|
|
415
|
+
worlds_by_dir[File.dirname(hecksagon_path)].concat(names)
|
|
416
|
+
end
|
|
417
|
+
worlds_by_dir.each do |dir, names|
|
|
418
|
+
names = names.uniq
|
|
409
419
|
next if names.empty?
|
|
410
420
|
|
|
411
|
-
world_path = File.join(
|
|
421
|
+
world_path = File.join(dir, "hecks_fuzz_postgres_era.world")
|
|
412
422
|
File.write(world_path, names.map do |name|
|
|
413
423
|
<<~WORLD
|
|
414
424
|
Hecks.world "#{name}" do
|
|
@@ -181,7 +181,11 @@ Hecks.bluebook "Bluebook", version: "1" do
|
|
|
181
181
|
|
|
182
182
|
# ONE ROW OF ONE CAPABILITY — `provides "authorization", assignments:
|
|
183
183
|
# ..., grant: ..., transitions: ...` offers three, one per key, in the
|
|
184
|
-
# order they were written.
|
|
184
|
+
# order they were written. Membership (`admit`/`grant`/`people`) and
|
|
185
|
+
# identity (`register`/`link`/`resolve`) use the same word with their
|
|
186
|
+
# own named keys; the argument table admits every key any capability
|
|
187
|
+
# declares, and `Capabilities::CONTRACTS` holds which keys a given
|
|
188
|
+
# capability must name.
|
|
185
189
|
command "Provide" do
|
|
186
190
|
role "Language"
|
|
187
191
|
goal "Declare one verb this chapter answers a capability with"
|
|
@@ -287,6 +291,11 @@ Hecks.bluebook "Bluebook", version: "1" do
|
|
|
287
291
|
member keyword: "provides", context: "Bluebook", at: "", named: "assignments", kind: "text", required: "false", fills: "provides"
|
|
288
292
|
member keyword: "provides", context: "Bluebook", at: "", named: "grant", kind: "text", required: "false", fills: "provides"
|
|
289
293
|
member keyword: "provides", context: "Bluebook", at: "", named: "transitions", kind: "text", required: "false", fills: "provides"
|
|
294
|
+
member keyword: "provides", context: "Bluebook", at: "", named: "admit", kind: "text", required: "false", fills: "provides"
|
|
295
|
+
member keyword: "provides", context: "Bluebook", at: "", named: "people", kind: "text", required: "false", fills: "provides"
|
|
296
|
+
member keyword: "provides", context: "Bluebook", at: "", named: "register", kind: "text", required: "false", fills: "provides"
|
|
297
|
+
member keyword: "provides", context: "Bluebook", at: "", named: "link", kind: "text", required: "false", fills: "provides"
|
|
298
|
+
member keyword: "provides", context: "Bluebook", at: "", named: "resolve", kind: "text", required: "false", fills: "provides"
|
|
290
299
|
member keyword: "aggregate", context: "Bluebook", at: "1", named: "", kind: "text", required: "true", fills: "name"
|
|
291
300
|
member keyword: "read_model", context: "Bluebook", at: "1", named: "", kind: "text", required: "true", fills: "name"
|
|
292
301
|
member keyword: "policy", context: "Bluebook", at: "1", named: "", kind: "text", required: "true", fills: "name"
|
|
@@ -110,6 +110,12 @@ Hecks.bluebook "Hecksagon" do
|
|
|
110
110
|
# the same shape `subscribe`/`uses_framework` already are, reached
|
|
111
111
|
# by Ruby's own method lookup before `word_gate` ever runs.
|
|
112
112
|
member word: "translates", context: "Hecksagon", body: "keywords", inner: "Policy", opens: "Policy", fills: ""
|
|
113
|
+
# A CONSUMER-OWNED bounded context — framework/vendored packages
|
|
114
|
+
# get this mark automatically from uses_framework /
|
|
115
|
+
# uses_embryonaut_bluebook and never write it in their own files.
|
|
116
|
+
# No `calls:` — `HecksagonBuilder#bounded` is an ordinary method,
|
|
117
|
+
# same shape `subscribe` already is.
|
|
118
|
+
member word: "bounded", context: "Hecksagon", body: "none", inner: "", opens: "", fills: ""
|
|
113
119
|
|
|
114
120
|
end
|
|
115
121
|
|
|
@@ -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
|
|
@@ -155,8 +155,13 @@ module Hecks
|
|
|
155
155
|
@drops.each { |name| apply_drop(state, name) }
|
|
156
156
|
# Last, and only where nothing already answered — a backfill
|
|
157
157
|
# fills the gap a rename/move/convert left untouched, never
|
|
158
|
-
# overwrites a value that already made it across.
|
|
159
|
-
|
|
158
|
+
# overwrites a value that already made it across. Dotted-path
|
|
159
|
+
# aware, same as `apply_drop` above (`apply_backfill`'s own
|
|
160
|
+
# header) — a bare name was the only shape this ever needed
|
|
161
|
+
# until a value object gained new required members with zero
|
|
162
|
+
# source data of their own (found live: lifeadelics' Attendee
|
|
163
|
+
# redesign, commit 4326dcd).
|
|
164
|
+
@backfills.each { |backfill| apply_backfill(state, backfill) }
|
|
160
165
|
Entry.new(operation: entry.operation, id: entry.id, state: state, mirrors: entry.mirrors)
|
|
161
166
|
end
|
|
162
167
|
|
|
@@ -312,6 +317,31 @@ module Hecks
|
|
|
312
317
|
end
|
|
313
318
|
end
|
|
314
319
|
|
|
320
|
+
# Applies one backfill, dotted-path aware — `apply_drop`'s own
|
|
321
|
+
# mirror on the addition side. A bare name sets a plain top-level
|
|
322
|
+
# key, unchanged from before; a dotted name (`"attendee.
|
|
323
|
+
# first_name"`) reaches into an existing value-object member the
|
|
324
|
+
# same way `apply_drop`/`hecks_tr_insert` (rule_compiler.rb's own
|
|
325
|
+
# SQL-compiled twin, kept in step with this) already do — creating
|
|
326
|
+
# the container Hash if an old record somehow lacks it entirely,
|
|
327
|
+
# never overwriting a value already there at either level.
|
|
328
|
+
#
|
|
329
|
+
# @param state [Hash] the entry's own state Hash, mutated in place
|
|
330
|
+
# @param backfill [Bluebook::TranslationBackfill] the backfill rule to apply
|
|
331
|
+
# @return [void]
|
|
332
|
+
def apply_backfill(state, backfill)
|
|
333
|
+
name = backfill.name.to_s
|
|
334
|
+
top, member = name.split(".", 2)
|
|
335
|
+
top = top.to_sym
|
|
336
|
+
|
|
337
|
+
if member
|
|
338
|
+
nested = (state[top] ||= {})
|
|
339
|
+
nested[member] = backfill.default unless nested.key?(member)
|
|
340
|
+
else
|
|
341
|
+
state[top] = backfill.default unless state.key?(top)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
315
345
|
# A dotted path's first segment is a top-level (symbol) key; a
|
|
316
346
|
# second segment reaches into a value-object member by its string
|
|
317
347
|
# key — the spelling a raw stored row carries. Translation runs on
|
|
@@ -54,6 +54,21 @@ module Hecks
|
|
|
54
54
|
declared.computes.each do |compute|
|
|
55
55
|
expression = compile_compute(expression, compute)
|
|
56
56
|
end
|
|
57
|
+
# Backfills last, same order `Lineage#translate` already applies
|
|
58
|
+
# in-process (that method's own comment: "only where nothing
|
|
59
|
+
# already answered") — now compiled here too, closing the gap
|
|
60
|
+
# this rule kind otherwise leaves: `rust/host`'s boot-time mint
|
|
61
|
+
# audit reads this exact compiled expression, never
|
|
62
|
+
# `Lineage#translate`, so it cannot see a backfilled value unless
|
|
63
|
+
# backfills compile to SQL like every other rule kind here. A
|
|
64
|
+
# real, live gap for any new required value-object member with no
|
|
65
|
+
# source data at all — `compute` cannot fill it either, since its
|
|
66
|
+
# own guard requires a real, already-present field to consume
|
|
67
|
+
# (found live: lifeadelics' Attendee redesign, commit 4326dcd,
|
|
68
|
+
# needed exactly this and had nothing that worked).
|
|
69
|
+
declared.backfills.each do |backfill|
|
|
70
|
+
expression = compile_backfill(expression, backfill)
|
|
71
|
+
end
|
|
57
72
|
expression
|
|
58
73
|
end
|
|
59
74
|
|
|
@@ -124,6 +139,32 @@ module Hecks
|
|
|
124
139
|
"LATERAL (SELECT (__s ->> #{text_literal(from)}) AS #{quote(from)}) __fields)"
|
|
125
140
|
end
|
|
126
141
|
|
|
142
|
+
# A newly added, required attribute with no source at all — the
|
|
143
|
+
# addition-side sibling of `compile_compute`'s own header, but with
|
|
144
|
+
# no field to consume: `hecks_tr_extract`'s own `.present` flag
|
|
145
|
+
# (already installed for `hecks_tr_drop`/`hecks_tr_move`/
|
|
146
|
+
# `hecks_tr_convert` — no new database function needed) answers
|
|
147
|
+
# "is this path — bare or a dotted value-object member alike —
|
|
148
|
+
# already there," and `hecks_tr_insert` (already merge-safe: it
|
|
149
|
+
# creates a missing intermediate container without disturbing any
|
|
150
|
+
# sibling member already in it) fills it only when it is not, the
|
|
151
|
+
# exact "never overwrites a value already there" rule `Lineage#
|
|
152
|
+
# translate`'s own in-process backfill already holds itself to.
|
|
153
|
+
#
|
|
154
|
+
# @param expression [String] the SQL expression built so far by `compile_rules`, read as
|
|
155
|
+
# `__s` inside this backfill's own check
|
|
156
|
+
# @param backfill [Bluebook::TranslationBackfill] the declared backfill rule
|
|
157
|
+
# @return [String] `expression` wrapped so the backfill's default lands at its declared
|
|
158
|
+
# path when nothing is there yet, unchanged otherwise
|
|
159
|
+
def compile_backfill(expression, backfill)
|
|
160
|
+
name = backfill.name.to_s
|
|
161
|
+
default_json = JSON.generate(backfill.default)
|
|
162
|
+
"(SELECT CASE WHEN (hecks_tr_extract(__s, #{path_literal(name)})).present THEN __s " \
|
|
163
|
+
"ELSE hecks_tr_insert(__s, #{path_literal(name)}, #{text_literal(default_json)}::jsonb, " \
|
|
164
|
+
"#{text_literal("backfill #{name}")}) END " \
|
|
165
|
+
"FROM (SELECT (#{expression}) AS __s) __outer)"
|
|
166
|
+
end
|
|
167
|
+
|
|
127
168
|
# `PG::Connection.quote_ident` needs the `pg` gem loaded, not
|
|
128
169
|
# connected — required here, lazily, the same "a domain that
|
|
129
170
|
# never wires PostgresEra should never need the gem" reasoning
|