hecks 1.5.1 → 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/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/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 +273 -26
- data/lib/hecks/projections/deploy/lambda.rb +21 -4
- data/lib/hecks/projector/exporter.rb +48 -0
- 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 +2 -2
|
@@ -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
|