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
|
@@ -126,6 +126,54 @@ module Hecks
|
|
|
126
126
|
}
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
# Same seam as `authorization` — which chapter answers "who may
|
|
130
|
+
# sign in" (`Registry#membership_provider_for`), with its declared
|
|
131
|
+
# verbs qualified and the membership aggregate named off `admit`.
|
|
132
|
+
# rust/host reads this instead of HECKS_MEMBERSHIP_AGGREGATE.
|
|
133
|
+
# `{}` when nothing this domain attaches provides membership.
|
|
134
|
+
# @param registry [Runtime::Registry] the booted registry `domain_name` is loaded in
|
|
135
|
+
# @param domain_name [String] the domain to export the membership binding for
|
|
136
|
+
# @return [Hash{Symbol => String, nil}] `:provider` (name), `:admit`, `:grant`,
|
|
137
|
+
# `:people` (qualified verbs), and `:aggregate` (`:admit`'s own leading
|
|
138
|
+
# aggregate name); `{}` if nothing this domain attaches provides membership
|
|
139
|
+
def membership(registry, domain_name)
|
|
140
|
+
provider = registry.membership_provider_for(domain_name)
|
|
141
|
+
return {} unless provider
|
|
142
|
+
|
|
143
|
+
capability = Bluebook::Capabilities::MEMBERSHIP
|
|
144
|
+
admit = provider.provided_verb(capability, :admit)
|
|
145
|
+
{
|
|
146
|
+
provider: provider.name,
|
|
147
|
+
admit: admit,
|
|
148
|
+
grant: provider.provided_verb(capability, :grant),
|
|
149
|
+
people: provider.provided_verb(capability, :people),
|
|
150
|
+
aggregate: admit&.split(".")&.first
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Same seam as `authorization` — which chapter answers "who is this
|
|
155
|
+
# authenticated pair" (`Registry#identity_provider_for`), with its
|
|
156
|
+
# declared verbs qualified. rust/host reads this instead of naming
|
|
157
|
+
# Identity::Identity.Register / ExternalIdentifier.Link. `{}` when
|
|
158
|
+
# nothing this domain attaches provides identity. Breaking in 2.0:
|
|
159
|
+
# Link's reference field is `identity`, never `identity_id`.
|
|
160
|
+
# @param registry [Runtime::Registry] the booted registry `domain_name` is loaded in
|
|
161
|
+
# @param domain_name [String] the domain to export the identity binding for
|
|
162
|
+
# @return [Hash{Symbol => String, nil}] `:provider` (name), `:register`, `:link`,
|
|
163
|
+
# `:resolve` (qualified verbs); `{}` if nothing this domain attaches provides identity
|
|
164
|
+
def identity(registry, domain_name)
|
|
165
|
+
provider = registry.identity_provider_for(domain_name)
|
|
166
|
+
return {} unless provider
|
|
167
|
+
|
|
168
|
+
capability = Bluebook::Capabilities::IDENTITY
|
|
169
|
+
{
|
|
170
|
+
provider: provider.name,
|
|
171
|
+
register: provider.provided_verb(capability, :register),
|
|
172
|
+
link: provider.provided_verb(capability, :link),
|
|
173
|
+
resolve: provider.provided_verb(capability, :resolve)
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
|
|
129
177
|
# Translation IR, always as an array, with each aggregate's
|
|
130
178
|
# precompiled SQL attached (`compiled_translation_aggregate`) —
|
|
131
179
|
# this is the export a consumer embeds (`ir.json`'s `translations`
|
|
@@ -80,12 +80,17 @@ module Hecks
|
|
|
80
80
|
# chapter must declare; nil means none
|
|
81
81
|
# @param emits [Symbol] the kind of artifact `self` returns: `:artifact` (the default,
|
|
82
82
|
# a single Hash or String) or `:files` (a path => contents tree)
|
|
83
|
+
# @param needs_world [Boolean] whether `self` is an export — a target that reads a
|
|
84
|
+
# domain's `.world`/`.hecksagon` bindings, not only its declaration. `false` (the
|
|
85
|
+
# default) for an ordinary projection; `true` refuses a `Projector.call` made with
|
|
86
|
+
# no `world:`.
|
|
83
87
|
# @return [Symbol] the registered key
|
|
84
|
-
def projects_as(key, requires: nil, declares: nil, emits: :artifact)
|
|
85
|
-
@projection_emits
|
|
86
|
-
@projection_key
|
|
87
|
-
@projection_declares
|
|
88
|
-
@projection_requires
|
|
88
|
+
def projects_as(key, requires: nil, declares: nil, emits: :artifact, needs_world: false)
|
|
89
|
+
@projection_emits = emits
|
|
90
|
+
@projection_key = key.to_sym
|
|
91
|
+
@projection_declares = Array(declares)
|
|
92
|
+
@projection_requires = Array(requires)
|
|
93
|
+
@projection_needs_world = needs_world
|
|
89
94
|
Projector.register(@projection_key, self)
|
|
90
95
|
@projection_key
|
|
91
96
|
end
|
|
@@ -108,6 +113,14 @@ module Hecks
|
|
|
108
113
|
# @return [Symbol] the kind of artifact `self` returns: `:artifact` or `:files`
|
|
109
114
|
def projection_emits = @projection_emits || :artifact
|
|
110
115
|
|
|
116
|
+
# Tells whether this target is an export — one that needs a domain's
|
|
117
|
+
# `.world`/`.hecksagon` bindings, passed as `Projector.call`'s `world:`,
|
|
118
|
+
# rather than only its declaration.
|
|
119
|
+
#
|
|
120
|
+
# @return [Boolean] the value given to `projects_as`' `needs_world:`, or false
|
|
121
|
+
# before `projects_as` is called
|
|
122
|
+
def projection_needs_world? = @projection_needs_world || false
|
|
123
|
+
|
|
111
124
|
# Names the capability module(s) a construct must satisfy, defaulting
|
|
112
125
|
# to plain chapter-hood when `projects_as` named none.
|
|
113
126
|
#
|
data/lib/hecks/projector.rb
CHANGED
|
@@ -31,11 +31,13 @@ module Hecks
|
|
|
31
31
|
#
|
|
32
32
|
# an export takes a declaration and its bindings and answers
|
|
33
33
|
# something that is the domain, running elsewhere — rust/project.rb's
|
|
34
|
-
# generated crate, the WASM artifact, the
|
|
35
|
-
#
|
|
36
|
-
# projection never looks at, because a
|
|
37
|
-
# it is wired.
|
|
38
|
-
# this
|
|
34
|
+
# generated crate, the WASM artifact, the CloudFormation template
|
|
35
|
+
# `lib/hecks/projections/deploy` renders. It needs the
|
|
36
|
+
# `.world`/`.hecksagon` a projection never looks at, because a
|
|
37
|
+
# running system has to know how it is wired. A target that needs
|
|
38
|
+
# this declares `needs_world: true` (`Target#projects_as`) and reads
|
|
39
|
+
# `options.fetch(:world)`; `Projector.call`'s own `world:` keyword is
|
|
40
|
+
# what carries it, merged into `options` before the target ever runs.
|
|
39
41
|
#
|
|
40
42
|
# a state projection takes records — a domain after dispatch — and is
|
|
41
43
|
# a read-model question wearing the same word.
|
|
@@ -88,17 +90,21 @@ module Hecks
|
|
|
88
90
|
# @param bluebook [Bluebook::Behaviour::Chapter, Hecks::IR] the chapter or
|
|
89
91
|
# IR-emitting construct to project
|
|
90
92
|
# @param options [Hash] projector-specific options, passed through unchanged
|
|
93
|
+
# @param world [Bluebook::World, nil] the domain's `.world`/`.hecksagon` bindings,
|
|
94
|
+
# required by an export (`needs_world: true`); nil for an ordinary projection.
|
|
95
|
+
# Merged into `options[:world]` before the target runs — the target never
|
|
96
|
+
# receives it as a separate argument.
|
|
91
97
|
# @return [Object] whatever the projector's own `call` returns: typically a
|
|
92
98
|
# `Hash`/`String` artifact, or a `Hash{String => String}` file tree
|
|
93
99
|
# @raise [UnknownProjector] if no projector is registered under `name`
|
|
94
100
|
# @raise [WrongConstruct] if `bluebook` lacks a capability or aggregate the
|
|
95
|
-
# projector requires
|
|
96
|
-
def call(name, bluebook:, options: {})
|
|
101
|
+
# projector requires, or if the projector needs `world:` and none is given
|
|
102
|
+
def call(name, bluebook:, options: {}, world: nil)
|
|
97
103
|
projector = registry.fetch(name.to_sym) do
|
|
98
104
|
raise UnknownProjector, "no projector registered for #{name.inspect} — registered: #{registered.sort.inspect}"
|
|
99
105
|
end
|
|
100
|
-
admits!(name, projector, bluebook)
|
|
101
|
-
projector.call(bluebook: bluebook, options: options)
|
|
106
|
+
admits!(name, projector, bluebook, world)
|
|
107
|
+
projector.call(bluebook: bluebook, options: world ? options.merge(world: world) : options)
|
|
102
108
|
end
|
|
103
109
|
|
|
104
110
|
# Refuses `construct` if it lacks a capability or declared aggregate
|
|
@@ -116,13 +122,17 @@ module Hecks
|
|
|
116
122
|
# @param name [String, Symbol] the projector's registered key, used in the message
|
|
117
123
|
# when refusing
|
|
118
124
|
# @param projector [Module, Class, #call] the target being checked; consulted for
|
|
119
|
-
# `projection_requires` and `
|
|
125
|
+
# `projection_requires`, `projection_declares`, and `projection_needs_world?` when
|
|
126
|
+
# it answers them
|
|
120
127
|
# @param construct [Bluebook::Behaviour::Chapter, Hecks::IR] the chapter or
|
|
121
128
|
# IR-emitting construct offered to the projector
|
|
129
|
+
# @param world [Bluebook::World, nil] the `world:` given to `Projector.call`, checked
|
|
130
|
+
# against the target's own `needs_world:` declaration
|
|
122
131
|
# @return [void]
|
|
123
|
-
# @raise [WrongConstruct] if `construct` lacks a required capability,
|
|
124
|
-
#
|
|
125
|
-
|
|
132
|
+
# @raise [WrongConstruct] if `construct` lacks a required capability, the chapter it
|
|
133
|
+
# is declares no aggregate the projector needs, or the projector needs `world:`
|
|
134
|
+
# and `world` is nil
|
|
135
|
+
def admits!(name, projector, construct, world = nil)
|
|
126
136
|
needed = projector.respond_to?(:projection_requires) ? projector.projection_requires : []
|
|
127
137
|
missing = needed.reject { |capability| capable?(construct, capability) }
|
|
128
138
|
unless missing.empty?
|
|
@@ -133,11 +143,15 @@ module Hecks
|
|
|
133
143
|
|
|
134
144
|
declared = projector.respond_to?(:projection_declares) ? projector.projection_declares : []
|
|
135
145
|
absent = declared.reject { |named| construct.aggregate(named) }
|
|
136
|
-
|
|
146
|
+
unless absent.empty?
|
|
147
|
+
raise WrongConstruct,
|
|
148
|
+
"#{name.inspect} needs a chapter declaring #{absent.join(' and ')}; " \
|
|
149
|
+
"#{construct.name} declares no such aggregate."
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
return unless projector.respond_to?(:projection_needs_world?) && projector.projection_needs_world? && world.nil?
|
|
137
153
|
|
|
138
|
-
raise WrongConstruct,
|
|
139
|
-
"#{name.inspect} needs a chapter declaring #{absent.join(' and ')}; " \
|
|
140
|
-
"#{construct.name} declares no such aggregate."
|
|
154
|
+
raise WrongConstruct, "#{name.inspect} needs .world/.hecksagon bindings — pass world: to Projector.call."
|
|
141
155
|
end
|
|
142
156
|
|
|
143
157
|
# Tells whether `construct` has the capability `admits!` requires of it.
|
|
@@ -138,12 +138,22 @@ module Hecks
|
|
|
138
138
|
current_entries.reject { |entry| entry.fqn.aggregate.nil? }
|
|
139
139
|
.group_by { |entry| [entry.fqn.aggregate, entry.fqn.verb] }
|
|
140
140
|
.each do |(aggregate, verb), candidates|
|
|
141
|
+
# Bounded chapters wrap in their own module (`Domain::Aggregate`)
|
|
142
|
+
# so two BCs can both declare `Person` without colliding on
|
|
143
|
+
# Object::Person. Folder-spread files of the SAME chapter still
|
|
144
|
+
# get the shortcut — they are not BCs.
|
|
145
|
+
next if bounded_chapter?(candidates)
|
|
146
|
+
|
|
141
147
|
shortcut_target(aggregate).define_singleton_method(verb) do |**args|
|
|
142
148
|
installer.send(:dispatch_short, candidates, **args)
|
|
143
149
|
end
|
|
144
150
|
end
|
|
145
151
|
end
|
|
146
152
|
|
|
153
|
+
def bounded_chapter?(candidates)
|
|
154
|
+
candidates.any? { |entry| entry.dispatcher.registry.bounded?(entry.fqn.domain) }
|
|
155
|
+
end
|
|
156
|
+
|
|
147
157
|
def dispatch_short(candidates, **args)
|
|
148
158
|
if candidates.length > 1
|
|
149
159
|
shown = candidates.map { |entry| entry.fqn.to_s }.sort.join(", ")
|
|
@@ -17,15 +17,19 @@ module Hecks
|
|
|
17
17
|
# @raise [Runtime::WiringError] if a bind names an undeclared aggregate, an
|
|
18
18
|
# adapter cannot satisfy its port's verb or declared `answers`, a world
|
|
19
19
|
# setting names a field its adapter does not declare, the default adapter
|
|
20
|
-
# is unusable,
|
|
21
|
-
# attached
|
|
20
|
+
# is unusable, a command declares a role with no authorization provider
|
|
21
|
+
# attached, or a membership chapter is loaded with no identity chapter
|
|
22
22
|
def verify!
|
|
23
23
|
verify_default_adapter!
|
|
24
24
|
verify_singleton_port_answers!
|
|
25
25
|
refuse_cross_package_bluebook_merge!
|
|
26
|
+
refuse_membership_without_identity!
|
|
26
27
|
|
|
27
28
|
@hecksagons.each_value do |hexagon|
|
|
28
29
|
refuse_ungoverned_roles!(hexagon)
|
|
30
|
+
refuse_unwired_framework_members!(hexagon)
|
|
31
|
+
refuse_unwired_vendored_bluebooks!(hexagon)
|
|
32
|
+
refuse_bounded_without_acl!(hexagon)
|
|
29
33
|
|
|
30
34
|
hexagon.binds.each do |bind|
|
|
31
35
|
# A domain-level default (§0) — `persisted_by "Heki"` bare,
|
|
@@ -261,6 +265,84 @@ module Hecks
|
|
|
261
265
|
"without that it is silent decoration, the exact defect this refusal exists to catch"
|
|
262
266
|
end
|
|
263
267
|
|
|
268
|
+
# A vendored embryonaut bluebook is a bounded context, not a pile of
|
|
269
|
+
# types the consumer inlines. `uses_embryonaut_bluebook` only loads
|
|
270
|
+
# the package's `.bluebook` files — persistence, Governance, and the
|
|
271
|
+
# anti-corruption layer (`translates` field mapping) live on a named
|
|
272
|
+
# sibling hecksagon the CONSUMER must declare. Without that hexagon
|
|
273
|
+
# the package has no wiring of its own, and cross-context field
|
|
274
|
+
# mapping has nowhere to be written. Breaking in 2.0: attaching a
|
|
275
|
+
# vendored chapter without `Hecks.hecksagon "PackageName"` refuses
|
|
276
|
+
# at boot rather than silently sharing the consumer's hexagon.
|
|
277
|
+
def refuse_unwired_vendored_bluebooks!(hexagon)
|
|
278
|
+
Array(hexagon.vendored_bluebooks).each do |package|
|
|
279
|
+
chapter_name = Naming.pascal(package)
|
|
280
|
+
next if hecksagon(chapter_name)
|
|
281
|
+
|
|
282
|
+
raise WiringError,
|
|
283
|
+
"#{hexagon.domain} attaches vendored bluebook #{package.inspect} " \
|
|
284
|
+
"(bounded context #{chapter_name}) but never declared " \
|
|
285
|
+
"Hecks.hecksagon #{chapter_name.inspect} — put that sibling " \
|
|
286
|
+
"(and any `translates` ACL) in context_map.hecksagon; " \
|
|
287
|
+
"same-name blocks merge, order-independent."
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Same gate as vendored packages: `uses_framework` loads a bounded
|
|
292
|
+
# context, and the consumer must declare the sibling hecksagon that
|
|
293
|
+
# is its ACL. Governance/Identity/Privacy already follow this in
|
|
294
|
+
# every real consumer; boot now refuses a silent miss.
|
|
295
|
+
def refuse_unwired_framework_members!(hexagon)
|
|
296
|
+
Array(hexagon.framework_members).each do |member|
|
|
297
|
+
next if hecksagon(member)
|
|
298
|
+
|
|
299
|
+
raise WiringError,
|
|
300
|
+
"#{hexagon.domain} attaches framework member #{member.inspect} " \
|
|
301
|
+
"(bounded context) but never declared Hecks.hecksagon " \
|
|
302
|
+
"#{member.inspect} — put that sibling (and any `translates` " \
|
|
303
|
+
"ACL) in context_map.hecksagon; same-name blocks merge, " \
|
|
304
|
+
"order-independent."
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# An explicit `bounded` mark on a consumer chapter always needs an
|
|
309
|
+
# ACL — at least one `translates` block. Any field can be mapped;
|
|
310
|
+
# the BC does not list which. `uses_framework` / `uses_embryonaut_bluebook`
|
|
311
|
+
# mark the ATTACHED chapter bounded (module wrap, no Object shortcut)
|
|
312
|
+
# and require the sibling hecksagon above; they do not require a
|
|
313
|
+
# `translates` on that sibling unless the consumer also wrote `bounded`.
|
|
314
|
+
# rust/host Google sign-in reads `ir.json`'s `membership` and
|
|
315
|
+
# `identity` keys (Exporter.membership / Exporter.identity) —
|
|
316
|
+
# never a deploy-time env var. Membership without Identity is
|
|
317
|
+
# the exact gap that produced a live `google_unlinked` after a
|
|
318
|
+
# successful Google handshake: provision cannot Register/Link.
|
|
319
|
+
# Refuse at Ruby boot (and project_rust, which exports the same
|
|
320
|
+
# pair) so a missing sibling cannot ship.
|
|
321
|
+
def refuse_membership_without_identity!
|
|
322
|
+
return unless @bluebooks.values.any? { |chapter| chapter.provides?(Bluebook::Capabilities::MEMBERSHIP) }
|
|
323
|
+
return if @bluebooks.values.any? { |chapter| chapter.provides?(Bluebook::Capabilities::IDENTITY) }
|
|
324
|
+
|
|
325
|
+
raise WiringError,
|
|
326
|
+
"a chapter that provides \"membership\" is loaded, but none provides " \
|
|
327
|
+
"\"identity\" — rust/host Google sign-in cannot register or link an " \
|
|
328
|
+
"identity from the hecksagon/world. Attach Identity (`uses_framework " \
|
|
329
|
+
"\"Identity\"` plus a sibling Hecks.hecksagon \"Identity\") so the " \
|
|
330
|
+
"identity verbs are exported onto ir.json, not guessed at deploy."
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def refuse_bounded_without_acl!(hexagon)
|
|
334
|
+
return unless hexagon.bounded?
|
|
335
|
+
return if hexagon.translates.any?
|
|
336
|
+
|
|
337
|
+
raise WiringError,
|
|
338
|
+
"#{hexagon.domain} is marked bounded but never declared a " \
|
|
339
|
+
"translates ACL — a bounded chapter wraps in its own module and " \
|
|
340
|
+
"cross-context field mapping lives on the hecksagon, not in " \
|
|
341
|
+
"rust/host and not as a field list on the bluebook. " \
|
|
342
|
+
"Add `translates \"Name\" do on Foreign::Event; trigger Local::Command, " \
|
|
343
|
+
"with: { ... } end` (any field) or drop `bounded`."
|
|
344
|
+
end
|
|
345
|
+
|
|
264
346
|
# The suggestion, derived from whichever framework members actually
|
|
265
347
|
# declare `provides "authorization"` — never a hardcoded name.
|
|
266
348
|
def authorization_attachment_hint
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require_relative "registry/verification"
|
|
2
2
|
require_relative "registry/saga_persistence"
|
|
3
3
|
require_relative "outbox"
|
|
4
|
+
require_relative "../naming"
|
|
4
5
|
|
|
5
6
|
module Hecks
|
|
6
7
|
module Runtime
|
|
@@ -27,6 +28,7 @@ module Hecks
|
|
|
27
28
|
@bluebooks = {}
|
|
28
29
|
@bluebook_sources = {}
|
|
29
30
|
@hecksagons = {}
|
|
31
|
+
@bounded_chapters = {}
|
|
30
32
|
@ports = {}
|
|
31
33
|
@adapters = {}
|
|
32
34
|
@worlds = {}
|
|
@@ -186,8 +188,29 @@ module Hecks
|
|
|
186
188
|
def add_hecksagon(item)
|
|
187
189
|
existing = @hecksagons[item.domain]
|
|
188
190
|
@hecksagons[item.domain] = existing ? merge_hecksagons(existing, item) : item
|
|
191
|
+
mark_bounded(item.domain) if item.bounded?
|
|
189
192
|
end
|
|
190
193
|
|
|
194
|
+
# Marks `name` as a bounded context — called automatically by
|
|
195
|
+
# `uses_framework` / `uses_embryonaut_bluebook`, and by
|
|
196
|
+
# `add_hecksagon` when the block itself declared `bounded`.
|
|
197
|
+
# A bounded chapter wraps in its own module (no Object shortcut)
|
|
198
|
+
# and must have a `translates` ACL or boot refuses.
|
|
199
|
+
#
|
|
200
|
+
# @param name [String, Symbol] the chapter name to mark bounded
|
|
201
|
+
# @return [void]
|
|
202
|
+
def mark_bounded(name)
|
|
203
|
+
@bounded_chapters[name.to_s] = true
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Says whether `name` is a bounded context — attached via
|
|
207
|
+
# `uses_framework` / `uses_embryonaut_bluebook`, or a consumer
|
|
208
|
+
# chapter that declared `bounded` on its own hecksagon.
|
|
209
|
+
#
|
|
210
|
+
# @param name [String, Symbol] the chapter name to check
|
|
211
|
+
# @return [Boolean] whether that chapter is bounded
|
|
212
|
+
def bounded?(name) = @bounded_chapters[name.to_s] ? true : false
|
|
213
|
+
|
|
191
214
|
# Registers a loaded port, keyed by its own declared name.
|
|
192
215
|
#
|
|
193
216
|
# @param item [Bluebook::Port, Bluebook::DomainPort] the loaded port
|
|
@@ -308,6 +331,66 @@ module Hecks
|
|
|
308
331
|
@bluebooks.values.select { |chapter| chapter.provides?(Bluebook::Capabilities::AUTHORIZATION) }
|
|
309
332
|
end
|
|
310
333
|
|
|
334
|
+
# The chapter that answers "who is this authenticated pair" for
|
|
335
|
+
# `domain` — the domain's own chapter, or any framework member its
|
|
336
|
+
# hecksagon attaches, that declares `provides "identity"`. Nil when
|
|
337
|
+
# none does. Replaces every check for the literal name "Identity":
|
|
338
|
+
# Identity is recognised by what it declares (Register/Link/ResolvedBy),
|
|
339
|
+
# and a chapter that declares the same thing is recognised the same way.
|
|
340
|
+
#
|
|
341
|
+
# @param domain [String, Symbol] the domain whose identity chapter is being resolved
|
|
342
|
+
# @return [Bluebook::Chapter, nil] the chapter that answers `domain`'s identity
|
|
343
|
+
# questions, or nil if none does
|
|
344
|
+
def identity_provider_for(domain)
|
|
345
|
+
names = [domain.to_s, *Array(hecksagon(domain)&.framework_members)]
|
|
346
|
+
attached = names.filter_map { |name| bluebook(name) }
|
|
347
|
+
.find { |chapter| chapter.provides?(Bluebook::Capabilities::IDENTITY) }
|
|
348
|
+
return attached if attached
|
|
349
|
+
|
|
350
|
+
# Sibling hecksagons — a consuming domain often wires Identity as
|
|
351
|
+
# `Hecks.hecksagon "Identity"` (so Register can attach Governance
|
|
352
|
+
# on that named hexagon), not only via uses_framework on the
|
|
353
|
+
# consuming domain. The chapter is loaded; it just isn't listed
|
|
354
|
+
# on the consumer's own hexagon.
|
|
355
|
+
@bluebooks.values.find { |chapter| chapter.provides?(Bluebook::Capabilities::IDENTITY) }
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# The chapter that answers "who may sign in" for `domain` — the
|
|
359
|
+
# domain's own chapter, any framework member its hecksagon attaches,
|
|
360
|
+
# or any vendored embryonaut bluebook it attaches, that declares
|
|
361
|
+
# `provides "membership"`. Nil when none does. Replaces rust/host's
|
|
362
|
+
# own HECKS_MEMBERSHIP_AGGREGATE env var: Membership is recognised
|
|
363
|
+
# by what it declares (Person.Admit/GrantAccess/All), and a chapter
|
|
364
|
+
# that declares the same thing (Embryonaut::Member, say) is
|
|
365
|
+
# recognised the same way.
|
|
366
|
+
#
|
|
367
|
+
# Vendored packages are included here and not in
|
|
368
|
+
# `authorization_provider_for` because membership is a vendored
|
|
369
|
+
# embryonaut_bluebooks chapter (`uses_embryonaut_bluebook
|
|
370
|
+
# "membership"`), not a framework member shipped inside hecks.
|
|
371
|
+
# `Naming.pascal` is the same directory-to-chapter convention
|
|
372
|
+
# `EmbryonautBluebook.load!` already uses.
|
|
373
|
+
#
|
|
374
|
+
# @param domain [String, Symbol] the domain whose sign-in aggregate is being resolved
|
|
375
|
+
# @return [Bluebook::Chapter, nil] the chapter that answers `domain`'s membership
|
|
376
|
+
# questions, or nil if none does
|
|
377
|
+
def membership_provider_for(domain)
|
|
378
|
+
hexagon = hecksagon(domain)
|
|
379
|
+
vendored = Array(hexagon&.vendored_bluebooks).map { |name| Naming.pascal(name) }
|
|
380
|
+
names = [domain.to_s, *Array(hexagon&.framework_members), *vendored]
|
|
381
|
+
attached = names.filter_map { |name| bluebook(name) }
|
|
382
|
+
.find { |chapter| chapter.provides?(Bluebook::Capabilities::MEMBERSHIP) }
|
|
383
|
+
return attached if attached
|
|
384
|
+
|
|
385
|
+
# Sibling hecksagons — Lifeadelics vendors Membership as
|
|
386
|
+
# `Hecks.hecksagon "Membership"` (so Person::Admit can attach
|
|
387
|
+
# Governance on that named hexagon), not via uses_embryonaut_bluebook
|
|
388
|
+
# on the consuming domain. The chapter is loaded; it just isn't
|
|
389
|
+
# listed on Lifeadelics' own hexagon. Any loaded chapter that
|
|
390
|
+
# provides membership is the bounded-context answer.
|
|
391
|
+
@bluebooks.values.find { |chapter| chapter.provides?(Bluebook::Capabilities::MEMBERSHIP) }
|
|
392
|
+
end
|
|
393
|
+
|
|
311
394
|
# Resolves and memoizes `aggregate`'s authoritative repository.
|
|
312
395
|
#
|
|
313
396
|
# @param domain [String, Symbol] name of the domain `aggregate` belongs to
|
|
@@ -437,12 +520,20 @@ module Hecks
|
|
|
437
520
|
# @return [Bluebook::Hecksagon] a new wiring with every list-shaped fact
|
|
438
521
|
# concatenated, `base` then `overlay`
|
|
439
522
|
def merge_hecksagons(base, overlay)
|
|
523
|
+
# Same-name blocks concatenate regardless of which file they came
|
|
524
|
+
# from (`lifeadelics.hecksagon`, `context_map.hecksagon`, an
|
|
525
|
+
# environment overlay). Order-independent: list facts uniq, so
|
|
526
|
+
# loading context_map before or after the domain file is the same
|
|
527
|
+
# merged hecksagon. Binds stay concatenated (BindingPolicy still
|
|
528
|
+
# refuses a genuine double-bind).
|
|
440
529
|
Bluebook::Hecksagon.new(
|
|
441
530
|
domain: base.domain,
|
|
442
531
|
binds: base.binds + overlay.binds,
|
|
443
|
-
subscriptions: base.subscriptions + overlay.subscriptions,
|
|
444
|
-
framework_members: base.framework_members + overlay.framework_members,
|
|
445
|
-
vendored_bluebooks: base.vendored_bluebooks + overlay.vendored_bluebooks
|
|
532
|
+
subscriptions: (base.subscriptions + overlay.subscriptions).uniq,
|
|
533
|
+
framework_members: (base.framework_members + overlay.framework_members).uniq,
|
|
534
|
+
vendored_bluebooks: (base.vendored_bluebooks + overlay.vendored_bluebooks).uniq,
|
|
535
|
+
bounded: base.bounded? || overlay.bounded?,
|
|
536
|
+
translates: (base.translates + overlay.translates).uniq
|
|
446
537
|
)
|
|
447
538
|
end
|
|
448
539
|
|
data/lib/hecks/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hecks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Young
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: prism
|
|
@@ -378,6 +378,9 @@ files:
|
|
|
378
378
|
- lib/hecks/ports/query/ordering.rb
|
|
379
379
|
- lib/hecks/projections.rb
|
|
380
380
|
- lib/hecks/projections/bootstrap_table.rb
|
|
381
|
+
- lib/hecks/projections/deploy/fargate.rb
|
|
382
|
+
- lib/hecks/projections/deploy/lambda.rb
|
|
383
|
+
- lib/hecks/projections/deploy/shared.rb
|
|
381
384
|
- lib/hecks/projections/diagrams.rb
|
|
382
385
|
- lib/hecks/projections/glossary.rb
|
|
383
386
|
- lib/hecks/projections/glossary/html.rb
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
../../../../examples/compliance/bluebook/compliance.bluebook
|