hecks 1.5.1 → 2.1.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 +48 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +24 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -1
- data/lib/hecks/bluebook/hexagon.rb +18 -3
- data/lib/hecks/corpus.rb +120 -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 +76 -0
- data/lib/hecks/router/namespace_installer.rb +10 -0
- data/lib/hecks/runtime/registry/verification.rb +116 -2
- data/lib/hecks/runtime/registry.rb +119 -3
- data/lib/hecks/version.rb +1 -1
- metadata +2 -2
|
@@ -17,15 +17,20 @@ 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!
|
|
27
|
+
refuse_unresolved_port_operations!
|
|
26
28
|
|
|
27
29
|
@hecksagons.each_value do |hexagon|
|
|
28
30
|
refuse_ungoverned_roles!(hexagon)
|
|
31
|
+
refuse_unwired_framework_members!(hexagon)
|
|
32
|
+
refuse_unwired_vendored_bluebooks!(hexagon)
|
|
33
|
+
refuse_bounded_without_acl!(hexagon)
|
|
29
34
|
|
|
30
35
|
hexagon.binds.each do |bind|
|
|
31
36
|
# A domain-level default (§0) — `persisted_by "Heki"` bare,
|
|
@@ -261,6 +266,115 @@ module Hecks
|
|
|
261
266
|
"without that it is silent decoration, the exact defect this refusal exists to catch"
|
|
262
267
|
end
|
|
263
268
|
|
|
269
|
+
# A vendored embryonaut bluebook is a bounded context, not a pile of
|
|
270
|
+
# types the consumer inlines. `uses_embryonaut_bluebook` only loads
|
|
271
|
+
# the package's `.bluebook` files — persistence, Governance, and the
|
|
272
|
+
# anti-corruption layer (`translates` field mapping) live on a named
|
|
273
|
+
# sibling hecksagon the CONSUMER must declare. Without that hexagon
|
|
274
|
+
# the package has no wiring of its own, and cross-context field
|
|
275
|
+
# mapping has nowhere to be written. Breaking in 2.0: attaching a
|
|
276
|
+
# vendored chapter without `Hecks.hecksagon "PackageName"` refuses
|
|
277
|
+
# at boot rather than silently sharing the consumer's hexagon.
|
|
278
|
+
def refuse_unwired_vendored_bluebooks!(hexagon)
|
|
279
|
+
Array(hexagon.vendored_bluebooks).each do |package|
|
|
280
|
+
chapter_name = Naming.pascal(package)
|
|
281
|
+
next if hecksagon(chapter_name)
|
|
282
|
+
|
|
283
|
+
raise WiringError,
|
|
284
|
+
"#{hexagon.domain} attaches vendored bluebook #{package.inspect} " \
|
|
285
|
+
"(bounded context #{chapter_name}) but never declared " \
|
|
286
|
+
"Hecks.hecksagon #{chapter_name.inspect} — put that sibling " \
|
|
287
|
+
"(and any `translates` ACL) in context_map.hecksagon; " \
|
|
288
|
+
"same-name blocks merge, order-independent."
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Same gate as vendored packages: `uses_framework` loads a bounded
|
|
293
|
+
# context, and the consumer must declare the sibling hecksagon that
|
|
294
|
+
# is its ACL. Governance/Identity/Privacy already follow this in
|
|
295
|
+
# every real consumer; boot now refuses a silent miss.
|
|
296
|
+
def refuse_unwired_framework_members!(hexagon)
|
|
297
|
+
Array(hexagon.framework_members).each do |member|
|
|
298
|
+
next if hecksagon(member)
|
|
299
|
+
|
|
300
|
+
raise WiringError,
|
|
301
|
+
"#{hexagon.domain} attaches framework member #{member.inspect} " \
|
|
302
|
+
"(bounded context) but never declared Hecks.hecksagon " \
|
|
303
|
+
"#{member.inspect} — put that sibling (and any `translates` " \
|
|
304
|
+
"ACL) in context_map.hecksagon; same-name blocks merge, " \
|
|
305
|
+
"order-independent."
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# An explicit `bounded` mark on a consumer chapter always needs an
|
|
310
|
+
# ACL — at least one `translates` block. Any field can be mapped;
|
|
311
|
+
# the BC does not list which. `uses_framework` / `uses_embryonaut_bluebook`
|
|
312
|
+
# mark the ATTACHED chapter bounded (module wrap, no Object shortcut)
|
|
313
|
+
# and require the sibling hecksagon above; they do not require a
|
|
314
|
+
# `translates` on that sibling unless the consumer also wrote `bounded`.
|
|
315
|
+
# rust/host Google sign-in reads `ir.json`'s `membership` and
|
|
316
|
+
# `identity` keys (Exporter.membership / Exporter.identity) —
|
|
317
|
+
# never a deploy-time env var. Membership without Identity is
|
|
318
|
+
# the exact gap that produced a live `google_unlinked` after a
|
|
319
|
+
# successful Google handshake: provision cannot Register/Link.
|
|
320
|
+
# Refuse at Ruby boot (and project_rust, which exports the same
|
|
321
|
+
# pair) so a missing sibling cannot ship.
|
|
322
|
+
def refuse_membership_without_identity!
|
|
323
|
+
return unless @bluebooks.values.any? { |chapter| chapter.provides?(Bluebook::Capabilities::MEMBERSHIP) }
|
|
324
|
+
return if @bluebooks.values.any? { |chapter| chapter.provides?(Bluebook::Capabilities::IDENTITY) }
|
|
325
|
+
|
|
326
|
+
raise WiringError,
|
|
327
|
+
"a chapter that provides \"membership\" is loaded, but none provides " \
|
|
328
|
+
"\"identity\" — rust/host Google sign-in cannot register or link an " \
|
|
329
|
+
"identity from the hecksagon/world. Attach Identity (`uses_framework " \
|
|
330
|
+
"\"Identity\"` plus a sibling Hecks.hecksagon \"Identity\") so the " \
|
|
331
|
+
"identity verbs are exported onto ir.json, not guessed at deploy."
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# A `provides` row whose contract kind is `:port_operation` names an
|
|
335
|
+
# operation the chapter's hecksagon declares, and the hecksagon
|
|
336
|
+
# attaches after the chapter is built, so this is the first point the
|
|
337
|
+
# operation can be checked. Without it, a host reading the declared
|
|
338
|
+
# verb would dispatch to an operation nothing declares.
|
|
339
|
+
def refuse_unresolved_port_operations!
|
|
340
|
+
@bluebooks.each_value do |chapter|
|
|
341
|
+
chapter.provides.each do |row|
|
|
342
|
+
next unless Bluebook::Capabilities::CONTRACTS.dig(row.capability, row.key.to_sym) == :port_operation
|
|
343
|
+
next if port_operation_declared?(chapter, row.verb)
|
|
344
|
+
|
|
345
|
+
raise WiringError,
|
|
346
|
+
"#{chapter.name} provides #{row.capability.inspect} #{row.key}: #{row.verb.inspect}, " \
|
|
347
|
+
"but its hecksagon declares no such port operation — declare it with " \
|
|
348
|
+
"`#{chapter.name}::Aggregate.port \"Port\" do operation \"Operation\" ... end`."
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Says whether a chapter declares the port operation an
|
|
354
|
+
# `"Aggregate.Port.Operation"` verb names.
|
|
355
|
+
#
|
|
356
|
+
# @param chapter [Bluebook::Chapter] the chapter whose aggregates' ports are searched
|
|
357
|
+
# @param verb [String] the verb, spelled `"Aggregate.Port.Operation"`
|
|
358
|
+
# @return [Boolean] whether that aggregate has that port with that operation
|
|
359
|
+
def port_operation_declared?(chapter, verb)
|
|
360
|
+
aggregate_name, port_name, operation_name = verb.split(".", 3)
|
|
361
|
+
ports = chapter.aggregate(aggregate_name)&.ports || []
|
|
362
|
+
ports.any? { |port| port.name == port_name && port.operations.any? { |op| op.hecks_name == operation_name } }
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def refuse_bounded_without_acl!(hexagon)
|
|
366
|
+
return unless hexagon.bounded?
|
|
367
|
+
return if hexagon.translates.any?
|
|
368
|
+
|
|
369
|
+
raise WiringError,
|
|
370
|
+
"#{hexagon.domain} is marked bounded but never declared a " \
|
|
371
|
+
"translates ACL — a bounded chapter wraps in its own module and " \
|
|
372
|
+
"cross-context field mapping lives on the hecksagon, not in " \
|
|
373
|
+
"rust/host and not as a field list on the bluebook. " \
|
|
374
|
+
"Add `translates \"Name\" do on Foreign::Event; trigger Local::Command, " \
|
|
375
|
+
"with: { ... } end` (any field) or drop `bounded`."
|
|
376
|
+
end
|
|
377
|
+
|
|
264
378
|
# The suggestion, derived from whichever framework members actually
|
|
265
379
|
# declare `provides "authorization"` — never a hardcoded name.
|
|
266
380
|
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,91 @@ 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
|
+
vendored_provider_for(domain, Bluebook::Capabilities::MEMBERSHIP)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# The chapter that answers the guest newsletter signup for `domain` —
|
|
382
|
+
# the domain's own chapter, a framework member, or a vendored
|
|
383
|
+
# embryonaut bluebook it attaches, that declares `provides
|
|
384
|
+
# "newsletter"`. Nil when none does. Newsletter is recognised by
|
|
385
|
+
# what it declares (Subscribe/AddName/Confirm/Unsubscribe), so a
|
|
386
|
+
# chapter that declares the same thing is recognised the same way.
|
|
387
|
+
#
|
|
388
|
+
# @param domain [String, Symbol] the domain whose newsletter chapter is being resolved
|
|
389
|
+
# @return [Bluebook::Chapter, nil] the chapter that answers `domain`'s newsletter
|
|
390
|
+
# signup, or nil if none does
|
|
391
|
+
def newsletter_provider_for(domain)
|
|
392
|
+
vendored_provider_for(domain, Bluebook::Capabilities::NEWSLETTER)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# The chapter that provides `capability` for `domain`: the domain's
|
|
396
|
+
# own chapter, any framework member its hecksagon attaches, or any
|
|
397
|
+
# vendored embryonaut bluebook it attaches, that declares it. Falls
|
|
398
|
+
# back to any loaded chapter that does, because a consuming domain
|
|
399
|
+
# often wires a vendored chapter as its own `Hecks.hecksagon "Name"`
|
|
400
|
+
# (so that chapter can attach Governance on that named hexagon)
|
|
401
|
+
# rather than via `uses_embryonaut_bluebook` on itself — the chapter
|
|
402
|
+
# is loaded, it just isn't listed on the consumer's own hexagon.
|
|
403
|
+
#
|
|
404
|
+
# @param domain [String, Symbol] the domain the provider is resolved for
|
|
405
|
+
# @param capability [String] the capability's name, such as
|
|
406
|
+
# `Bluebook::Capabilities::MEMBERSHIP`
|
|
407
|
+
# @return [Bluebook::Chapter, nil] the providing chapter, or nil if none loaded does
|
|
408
|
+
def vendored_provider_for(domain, capability)
|
|
409
|
+
hexagon = hecksagon(domain)
|
|
410
|
+
vendored = Array(hexagon&.vendored_bluebooks).map { |name| Naming.pascal(name) }
|
|
411
|
+
names = [domain.to_s, *Array(hexagon&.framework_members), *vendored]
|
|
412
|
+
attached = names.filter_map { |name| bluebook(name) }
|
|
413
|
+
.find { |chapter| chapter.provides?(capability) }
|
|
414
|
+
return attached if attached
|
|
415
|
+
|
|
416
|
+
@bluebooks.values.find { |chapter| chapter.provides?(capability) }
|
|
417
|
+
end
|
|
418
|
+
|
|
311
419
|
# Resolves and memoizes `aggregate`'s authoritative repository.
|
|
312
420
|
#
|
|
313
421
|
# @param domain [String, Symbol] name of the domain `aggregate` belongs to
|
|
@@ -437,12 +545,20 @@ module Hecks
|
|
|
437
545
|
# @return [Bluebook::Hecksagon] a new wiring with every list-shaped fact
|
|
438
546
|
# concatenated, `base` then `overlay`
|
|
439
547
|
def merge_hecksagons(base, overlay)
|
|
548
|
+
# Same-name blocks concatenate regardless of which file they came
|
|
549
|
+
# from (`lifeadelics.hecksagon`, `context_map.hecksagon`, an
|
|
550
|
+
# environment overlay). Order-independent: list facts uniq, so
|
|
551
|
+
# loading context_map before or after the domain file is the same
|
|
552
|
+
# merged hecksagon. Binds stay concatenated (BindingPolicy still
|
|
553
|
+
# refuses a genuine double-bind).
|
|
440
554
|
Bluebook::Hecksagon.new(
|
|
441
555
|
domain: base.domain,
|
|
442
556
|
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
|
|
557
|
+
subscriptions: (base.subscriptions + overlay.subscriptions).uniq,
|
|
558
|
+
framework_members: (base.framework_members + overlay.framework_members).uniq,
|
|
559
|
+
vendored_bluebooks: (base.vendored_bluebooks + overlay.vendored_bluebooks).uniq,
|
|
560
|
+
bounded: base.bounded? || overlay.bounded?,
|
|
561
|
+
translates: (base.translates + overlay.translates).uniq
|
|
446
562
|
)
|
|
447
563
|
end
|
|
448
564
|
|
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: 1.
|
|
4
|
+
version: 2.1.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-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: prism
|