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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29c54ce86e36189a63578e970929e42c213d5812282c5cf5ca6e56820eb02938
4
- data.tar.gz: 1d564ee387fb38c619fbdfef209bcc757570a492e70dac16f1d5fb80889c37e8
3
+ metadata.gz: 8261eb6f8b8b490fda7cc10deed5d1668e677031b03d67c89ca2c706cb90140a
4
+ data.tar.gz: 005ef58bad7618ab6085b22c9483465d2b58af9a9c61db7a365e70db81f4a5d6
5
5
  SHA512:
6
- metadata.gz: 24794f4aaa4f5d2268f27e063b79a23b61c7dffe7b4a87b4b58f83db73188d57716fd734d695082bdf11d78d3101fc41640a45f76510fe7cc45fd9c3cd9347a8
7
- data.tar.gz: 9ce51558bbe7c55e9dc451140a21d7c83b7073b8a692304579f15c76db3c56371cb90c7cac07b6c02e6eb64ddd73c27572e786ab941659d2a76e649de5d2a699
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
- "Identity::ExternalIdentifier.ResolvedBy",
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
- def initialize(domain:, binds: [], subscriptions: [], framework_members: [], vendored_bluebooks: [])
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" => 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")
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 = {}.freeze
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 { |stem| modules.include?(stem) && !generated?(stem, root: root) }
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.uniq
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(File.dirname(hecksagon_path), "hecks_fuzz_postgres_era.world")
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.uniq
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(File.dirname(hecksagon_path), "hecks_fuzz_postgres_era.world")
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
- @backfills.each { |backfill| state[backfill.name] = backfill.default unless state.key?(backfill.name) }
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