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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8261eb6f8b8b490fda7cc10deed5d1668e677031b03d67c89ca2c706cb90140a
|
|
4
|
+
data.tar.gz: 005ef58bad7618ab6085b22c9483465d2b58af9a9c61db7a365e70db81f4a5d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 926f57e7d5a1af30ec918269460bc04a20c25dd1feaaf8da910fe0a0ea241f7b4622feec775dfa8390bba16253b6d4b18130b0ea5a0e544259cae70cbd961cbe
|
|
7
|
+
data.tar.gz: aca0677bd624c1a61d9f6d3dbe6f0e74991d690557d4abed0abf4a3e4a240931434a375b633ba7f424f04c3c36a2454811608ca2894a99d12cbdc2460876ca1b
|
|
@@ -24,8 +24,14 @@ module Hecks
|
|
|
24
24
|
# @param subject [String] the OIDC subject the issuer vouches for, compared as a String
|
|
25
25
|
# @return [String, nil] the linked identity's id, or nil if nothing has linked this pair
|
|
26
26
|
def resolve(registry, issuer:, subject:)
|
|
27
|
+
# The resolve verb this registry's declared identity provider names
|
|
28
|
+
# (`provides "identity", resolve:`), never a hard-coded chapter —
|
|
29
|
+
# same declared-not-named shape GovernanceAuthorization already holds.
|
|
30
|
+
provider = registry.bluebooks.values.find { |chapter| chapter.provides?(::Hecks::Bluebook::Capabilities::IDENTITY) }
|
|
31
|
+
verb = provider&.provided_verb(::Hecks::Bluebook::Capabilities::IDENTITY, :resolve) ||
|
|
32
|
+
"Identity::ExternalIdentifier.ResolvedBy"
|
|
27
33
|
rows = Runtime::Dispatcher.new(registry).query(
|
|
28
|
-
|
|
34
|
+
verb,
|
|
29
35
|
issuer: { value: issuer.to_s }, subject: { value: subject.to_s }
|
|
30
36
|
)
|
|
31
37
|
|
|
@@ -11,6 +11,17 @@ module Hecks
|
|
|
11
11
|
# key => :command | :query the kind of verb that key must name
|
|
12
12
|
module Capabilities
|
|
13
13
|
AUTHORIZATION = "authorization".freeze
|
|
14
|
+
# Who may sign in, and with what role — rust/host's Google-OAuth
|
|
15
|
+
# provision/member_rows resolve this instead of an env var naming
|
|
16
|
+
# the aggregate (HECKS_MEMBERSHIP_AGGREGATE). Same declared-
|
|
17
|
+
# not-named shape AUTHORIZATION already is for Governance.
|
|
18
|
+
MEMBERSHIP = "membership".freeze
|
|
19
|
+
# A stable organizational identity, independent of how it was
|
|
20
|
+
# authenticated — recognised by declaration, not the literal name
|
|
21
|
+
# "Identity". Same declared-not-named shape AUTHORIZATION already
|
|
22
|
+
# is. rust/host does not build this chapter's payloads; any field
|
|
23
|
+
# mapping lives on the consuming hecksagon's `translates` ACL.
|
|
24
|
+
IDENTITY = "identity".freeze
|
|
14
25
|
|
|
15
26
|
CONTRACTS = {
|
|
16
27
|
AUTHORIZATION => {
|
|
@@ -20,6 +31,22 @@ module Hecks
|
|
|
20
31
|
grant: :command,
|
|
21
32
|
# every grant of one role acting as another
|
|
22
33
|
transitions: :query
|
|
34
|
+
}.freeze,
|
|
35
|
+
MEMBERSHIP => {
|
|
36
|
+
# recognize a person who may eventually sign in
|
|
37
|
+
admit: :command,
|
|
38
|
+
# grant an admitted person a role (the access-grant half)
|
|
39
|
+
grant: :command,
|
|
40
|
+
# every admitted person, for the admin listing
|
|
41
|
+
people: :query
|
|
42
|
+
}.freeze,
|
|
43
|
+
IDENTITY => {
|
|
44
|
+
# mint a stable identity, independent of how it authenticated
|
|
45
|
+
register: :command,
|
|
46
|
+
# associate an (issuer, subject) pair with that identity
|
|
47
|
+
link: :command,
|
|
48
|
+
# look up the identity an authenticated pair resolves to
|
|
49
|
+
resolve: :query
|
|
23
50
|
}.freeze
|
|
24
51
|
}.freeze
|
|
25
52
|
end
|
|
@@ -27,6 +27,20 @@ module Hecks
|
|
|
27
27
|
@subscriptions = []
|
|
28
28
|
@framework_members = []
|
|
29
29
|
@vendored_bluebooks = []
|
|
30
|
+
@bounded = false
|
|
31
|
+
@translates = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Marks THIS chapter as a bounded context — a consumer-owned chapter
|
|
35
|
+
# that `uses_framework` / `uses_embryonaut_bluebook` will not load.
|
|
36
|
+
# Framework and vendored packages get the mark automatically from
|
|
37
|
+
# those words; they never write `bounded` in their own bluebooks.
|
|
38
|
+
# A bounded chapter wraps in its own module (no Object shortcut)
|
|
39
|
+
# and must declare at least one `translates` ACL or boot refuses.
|
|
40
|
+
#
|
|
41
|
+
# @return [Boolean] true
|
|
42
|
+
def bounded
|
|
43
|
+
@bounded = true
|
|
30
44
|
end
|
|
31
45
|
|
|
32
46
|
# Subscribes this hecksagon to an event it takes from outside the domain's own bluebook.
|
|
@@ -54,6 +68,11 @@ module Hecks
|
|
|
54
68
|
def uses_framework(name)
|
|
55
69
|
@framework_members << name.to_s
|
|
56
70
|
Hecks::Framework.load!(name)
|
|
71
|
+
# Automatic bounded mark — the framework member is a bounded
|
|
72
|
+
# context; the consumer's sibling `Hecks.hecksagon "Name"` is
|
|
73
|
+
# the anti-corruption layer. Not written in the framework
|
|
74
|
+
# bluebook itself.
|
|
75
|
+
Hecks.current_registry&.mark_bounded(name.to_s)
|
|
57
76
|
end
|
|
58
77
|
|
|
59
78
|
# Attaches a vendored embryonaut bluebook to this domain and loads its files into the
|
|
@@ -83,6 +102,10 @@ module Hecks
|
|
|
83
102
|
def uses_embryonaut_bluebook(name)
|
|
84
103
|
@vendored_bluebooks << name.to_s
|
|
85
104
|
Hecks::EmbryonautBluebook.load!(name)
|
|
105
|
+
# Automatic bounded mark — same as `uses_framework`. The
|
|
106
|
+
# package's directory name Pascal-cases to the chapter
|
|
107
|
+
# (`"membership"` → `Membership`).
|
|
108
|
+
Hecks.current_registry&.mark_bounded(Hecks::Naming.pascal(name.to_s))
|
|
86
109
|
end
|
|
87
110
|
|
|
88
111
|
# Declares a port at the hecksagon's root and attaches it to the registered bluebook.
|
|
@@ -173,6 +196,7 @@ module Hecks
|
|
|
173
196
|
resolver = ->(const) { ConstShim::ScopedConstant.for(const) }
|
|
174
197
|
built = ConstShim.with(resolver) { PolicyBuilder.build(name, &block) }
|
|
175
198
|
|
|
199
|
+
@translates << name.to_s
|
|
176
200
|
bluebook_ir.add_policy(built)
|
|
177
201
|
end
|
|
178
202
|
|
|
@@ -197,7 +221,8 @@ module Hecks
|
|
|
197
221
|
# with any other block declared for the same domain
|
|
198
222
|
def build
|
|
199
223
|
Hecksagon.new(domain: @domain, binds: @binds, subscriptions: @subscriptions,
|
|
200
|
-
framework_members: @framework_members, vendored_bluebooks: @vendored_bluebooks
|
|
224
|
+
framework_members: @framework_members, vendored_bluebooks: @vendored_bluebooks,
|
|
225
|
+
bounded: @bounded, translates: @translates)
|
|
201
226
|
end
|
|
202
227
|
|
|
203
228
|
# Records any verb the grammar does not own as a domain-wide bind to the named adapter.
|
|
@@ -51,10 +51,13 @@ module Hecks
|
|
|
51
51
|
binds: many(:binds),
|
|
52
52
|
subscriptions: -> { subscriptions.map(&:to_s) },
|
|
53
53
|
framework_members: -> { framework_members.map(&:to_s) },
|
|
54
|
-
vendored_bluebooks: -> { vendored_bluebooks.map(&:to_s) }
|
|
54
|
+
vendored_bluebooks: -> { vendored_bluebooks.map(&:to_s) },
|
|
55
|
+
bounded: :bounded,
|
|
56
|
+
translates: -> { translates.map(&:to_s) }
|
|
55
57
|
)
|
|
56
58
|
|
|
57
|
-
attr_reader :domain, :binds, :subscriptions, :framework_members, :vendored_bluebooks
|
|
59
|
+
attr_reader :domain, :binds, :subscriptions, :framework_members, :vendored_bluebooks,
|
|
60
|
+
:translates
|
|
58
61
|
|
|
59
62
|
# @param domain [String, Symbol] the domain this hecksagon wires
|
|
60
63
|
# @param binds [Array<Bluebook::Bind>] the declared adapter binds
|
|
@@ -64,13 +67,25 @@ module Hecks
|
|
|
64
67
|
# (`Governance`, `Identity`, ...) this domain attaches
|
|
65
68
|
# @param vendored_bluebooks [Array<String, Symbol>] the vendored embryonaut
|
|
66
69
|
# bluebook package names this domain attaches
|
|
67
|
-
|
|
70
|
+
# @param bounded [Boolean] whether this chapter is an explicit bounded context
|
|
71
|
+
# (consumer-owned; `uses_framework` / `uses_embryonaut_bluebook` mark
|
|
72
|
+
# attached chapters bounded on the registry instead)
|
|
73
|
+
# @param translates [Array<String>] names of `translates` ACL blocks declared here
|
|
74
|
+
def initialize(domain:, binds: [], subscriptions: [], framework_members: [],
|
|
75
|
+
vendored_bluebooks: [], bounded: false, translates: [])
|
|
68
76
|
@domain = domain.to_s
|
|
69
77
|
@binds = binds
|
|
70
78
|
@subscriptions = subscriptions
|
|
71
79
|
@framework_members = framework_members
|
|
72
80
|
@vendored_bluebooks = vendored_bluebooks
|
|
81
|
+
@bounded = bounded ? true : false
|
|
82
|
+
@translates = Array(translates).map(&:to_s)
|
|
73
83
|
end
|
|
84
|
+
|
|
85
|
+
# Says whether this hecksagon marked its own chapter `bounded`.
|
|
86
|
+
#
|
|
87
|
+
# @return [Boolean] whether `bounded` was declared on this block
|
|
88
|
+
def bounded? = @bounded
|
|
74
89
|
end
|
|
75
90
|
|
|
76
91
|
# The built form of a `.world` file, produced by `DSL::WorldBuilder` —
|
data/lib/hecks/corpus.rb
CHANGED
|
@@ -265,20 +265,115 @@ module Hecks
|
|
|
265
265
|
Elsewhere = Struct.new(:check, :destination, :names, :why)
|
|
266
266
|
|
|
267
267
|
RUST_ELSEWHERE = {
|
|
268
|
-
"meta"
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
"embryonaut"
|
|
273
|
-
|
|
274
|
-
|
|
268
|
+
"meta" => Elsewhere.new(:named_in, "spec/codegen_parity_spec.rb", "bluebook_language",
|
|
269
|
+
"the self-hosted grammar (lib/hecks/language), not a domain directory — every " \
|
|
270
|
+
"bin/project_rust run rewrites it (so the drift check diffs it), codegen parity " \
|
|
271
|
+
"checks it as bluebook_language, and there is no directory to fuzz"),
|
|
272
|
+
"embryonaut" => Elsewhere.new(:external, "~/Projects/embryonautfoundersapp", "embryonaut",
|
|
273
|
+
"an external product's domain — its bluebook, regeneration and parity are owed " \
|
|
274
|
+
"by its own repo; here bin/rust_coverage checks only the committed snapshot"),
|
|
275
|
+
# Lifeadelics — same external-product shape as "embryonaut" above,
|
|
276
|
+
# first generated 2026-09-19 fixing a live era-shape-drift outage.
|
|
277
|
+
# Its directory used to be named "domain" (a generic, collision-
|
|
278
|
+
# prone Cargo feature/module name — the chapter name is, and
|
|
279
|
+
# always was, "Lifeadelics"), so the generated module and Cargo
|
|
280
|
+
# feature were "domain" too, read off `metadata.rs`'s own stamp
|
|
281
|
+
# the same way `generated_source` reads any other module's. Fixed
|
|
282
|
+
# 2026-09-20 by renaming the directory itself
|
|
283
|
+
# (~/Projects/lifeadelics/domain -> ~/Projects/lifeadelics/
|
|
284
|
+
# lifeadelics) — the generated module and Cargo feature are
|
|
285
|
+
# "lifeadelics" now, matching every other domain's own convention.
|
|
286
|
+
#
|
|
287
|
+
# Lifeadelics also attaches `accounts`/`newsletter`/`payments`
|
|
288
|
+
# (its own vendored embryonaut_bluebooks packages, see
|
|
289
|
+
# RUST_EXTERNAL_VENDORED_CHAPTERS below) and `Privacy` (an in-repo
|
|
290
|
+
# framework chapter, lib/hecks/framework/bluebook/privacy.bluebook,
|
|
291
|
+
# generated into Rust for the first time by any domain here).
|
|
292
|
+
# `rust_side_chapters` only ever looks for the attaching domain
|
|
293
|
+
# among `rust_domains` — an in-repo directory — so it can attribute
|
|
294
|
+
# neither module to lifeadelics on its own; `rust_attachment_
|
|
295
|
+
# hecksagon_text`, below, reads every `:external` domain's own
|
|
296
|
+
# hecksagon files too, the same way it reads an in-repo domain's,
|
|
297
|
+
# so `Privacy`'s own bucket membership (already correct — it is a
|
|
298
|
+
# real in-repo framework member with no merged.rs) can still be
|
|
299
|
+
# proven attached.
|
|
300
|
+
"lifeadelics" => Elsewhere.new(:external, "~/Projects/lifeadelics", "lifeadelics",
|
|
301
|
+
"an external product's domain — its bluebook, regeneration and parity are owed " \
|
|
302
|
+
"by its own repo; here bin/rust_coverage checks only the committed snapshot")
|
|
275
303
|
}.freeze
|
|
276
304
|
|
|
305
|
+
# Vendored embryonaut_bluebooks packages attached only by an external
|
|
306
|
+
# RUST_ELSEWHERE domain — `members(:vendored)` can't find them
|
|
307
|
+
# itself; that glob only reaches `examples/*/vendor/
|
|
308
|
+
# embryonaut_bluebooks/*`, never an external checkout. Declared once,
|
|
309
|
+
# by hand, the same manual-commit contract "embryonaut" itself
|
|
310
|
+
# already carries: stem => the RUST_ELSEWHERE feature that attaches
|
|
311
|
+
# it. spec/corpus_rust_spec.rb checks each is really attached, the
|
|
312
|
+
# same as an in-repo vendored chapter (see
|
|
313
|
+
# `rust_external_vendored_domain_dir`, below).
|
|
314
|
+
RUST_EXTERNAL_VENDORED_CHAPTERS = {
|
|
315
|
+
"accounts" => "lifeadelics",
|
|
316
|
+
"newsletter" => "lifeadelics",
|
|
317
|
+
"payments" => "lifeadelics",
|
|
318
|
+
"membership" => "lifeadelics"
|
|
319
|
+
}.freeze
|
|
320
|
+
|
|
321
|
+
# Every external vendored chapter's own stem.
|
|
322
|
+
#
|
|
323
|
+
# @return [Array<String>] stems declared in RUST_EXTERNAL_VENDORED_CHAPTERS
|
|
324
|
+
def rust_external_vendored_chapters
|
|
325
|
+
RUST_EXTERNAL_VENDORED_CHAPTERS.keys
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Where an external vendored chapter's own bluebook lives — somewhere
|
|
329
|
+
# under `<destination>/**/vendor/embryonaut_bluebooks/<stem>`, the
|
|
330
|
+
# same layout `EmbryonautBluebook.load!` resolves for an in-repo
|
|
331
|
+
# vendored member, one level further out. Globbed rather than joined
|
|
332
|
+
# directly: `RUST_ELSEWHERE`'s own `destination` names the external
|
|
333
|
+
# product's checkout root, not necessarily the exact directory its
|
|
334
|
+
# own bluebook lives under (confirmed live against lifeadelics's own
|
|
335
|
+
# checkout: `~/Projects/lifeadelics/lifeadelics/vendor/
|
|
336
|
+
# embryonaut_bluebooks/accounts/bluebook`, one level below
|
|
337
|
+
# `~/Projects/lifeadelics` itself).
|
|
338
|
+
#
|
|
339
|
+
# @param stem [String] an external vendored chapter's stem
|
|
340
|
+
# @return [String, nil] the vendored member's own directory, or nil when none is found
|
|
341
|
+
def rust_external_vendored_domain_dir(stem)
|
|
342
|
+
feature = RUST_EXTERNAL_VENDORED_CHAPTERS.fetch(stem)
|
|
343
|
+
destination = File.expand_path(RUST_ELSEWHERE.fetch(feature).destination)
|
|
344
|
+
Dir.glob(File.join(destination, "**", "vendor", "embryonaut_bluebooks", stem)).first
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Every place a Rust-facing domain's own `uses_framework`/
|
|
348
|
+
# `uses_embryonaut_bluebook` attachment could be declared — every
|
|
349
|
+
# in-repo Rust domain's own hecksagon files, plus each `:external`
|
|
350
|
+
# RUST_ELSEWHERE domain's own (its `destination`, expanded, is a
|
|
351
|
+
# real checkout on this machine, read the same way an in-repo
|
|
352
|
+
# domain's own hecksagon files already are).
|
|
353
|
+
#
|
|
354
|
+
# @param root [String] repository root to search under
|
|
355
|
+
# @return [String] every reachable hecksagon file's own text, joined by newlines
|
|
356
|
+
def rust_attachment_hecksagon_text(root: ROOT)
|
|
357
|
+
in_repo = rust_domains(root: root).flat_map { |domain| Dir.glob(File.join(domain.dir, "**", "*.hecksagon")) }
|
|
358
|
+
external = RUST_ELSEWHERE.values.select { |route| route.check == :external }
|
|
359
|
+
.flat_map { |route| Dir.glob(File.join(File.expand_path(route.destination), "**", "*.hecksagon")) }
|
|
360
|
+
(in_repo + external).map { |path| File.read(path) }.join("\n")
|
|
361
|
+
end
|
|
362
|
+
|
|
277
363
|
# **Shrink-only**. A generated module `bin/rust_coverage` still reports a
|
|
278
364
|
# gap for. `bin/corpus --rust-coverage` requires each of these to
|
|
279
365
|
# still fail, so an entry that starts passing breaks the build until
|
|
280
366
|
# it is deleted here.
|
|
281
|
-
RUST_COVERAGE_PENDING = {
|
|
367
|
+
RUST_COVERAGE_PENDING = {
|
|
368
|
+
"accounts" => "query Listing declares no where clause at all (\"every account, alphabetically by " \
|
|
369
|
+
"email\") — rust/project/queries.rb's own no_wheres skip refuses to generate an " \
|
|
370
|
+
"unfiltered per_instance Listing (\"nothing for filter_entries to bake in\"); a " \
|
|
371
|
+
"structural Rust codegen limitation, not a bug in this vendored package",
|
|
372
|
+
"newsletter" => "same no_wheres gap as accounts, on all 3 of its own Listing queries " \
|
|
373
|
+
"(Delivery/Issue/Subscriber) — see accounts' entry above",
|
|
374
|
+
"lifeadelics" => "same no_wheres gap as accounts, on Registration.Listing — see accounts' entry above",
|
|
375
|
+
"membership" => "same no_wheres gap as accounts, on Person.All — see accounts' entry above"
|
|
376
|
+
}.freeze
|
|
282
377
|
|
|
283
378
|
# The Cargo `[features]` table's raw text.
|
|
284
379
|
#
|
|
@@ -396,7 +491,26 @@ module Hecks
|
|
|
396
491
|
def rust_side_chapters(kind, root: ROOT)
|
|
397
492
|
modules = generated_modules(root: root)
|
|
398
493
|
members(kind, root: root).map(&:stem)
|
|
399
|
-
.select
|
|
494
|
+
.select do |stem|
|
|
495
|
+
module_name = Naming.pascal(stem).downcase
|
|
496
|
+
modules.include?(module_name) && !generated?(module_name, root: root)
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# The generated module name a `:framework`/`:vendored` stem writes
|
|
501
|
+
# under — `Naming.pascal(stem).downcase` (`bin/project_rust`'s own
|
|
502
|
+
# convention, see `rust_side_chapters`'s header); a chapter whose
|
|
503
|
+
# stem holds an underscore ("console_settings") writes to a
|
|
504
|
+
# different, underscore-free module name ("consolesettings"), so a
|
|
505
|
+
# bucket-membership check against `generated_modules` (a directory
|
|
506
|
+
# name) needs this mapping rather than the raw stem
|
|
507
|
+
# `rust_framework_chapters`/`rust_vendored_chapters` still return
|
|
508
|
+
# (every other caller wants the stem, to build a bluebook file path).
|
|
509
|
+
#
|
|
510
|
+
# @param stem [String] a `:framework`/`:vendored` corpus member's stem
|
|
511
|
+
# @return [String] the generated module's own directory name
|
|
512
|
+
def rust_side_module_name(stem)
|
|
513
|
+
Naming.pascal(stem).downcase
|
|
400
514
|
end
|
|
401
515
|
|
|
402
516
|
def rust_framework_chapters(root: ROOT)
|
|
@@ -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
|
|
|
@@ -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
|