hecks 2.0.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8261eb6f8b8b490fda7cc10deed5d1668e677031b03d67c89ca2c706cb90140a
4
- data.tar.gz: 005ef58bad7618ab6085b22c9483465d2b58af9a9c61db7a365e70db81f4a5d6
3
+ metadata.gz: b7e80387e679bdc08c6a48b9c1eabefee28d7f9fd0e28a41f203e1fd9b207c34
4
+ data.tar.gz: 455038d69b310e2fc9ae72b30d811623c8c1a85a4a5da118b842b8af7fd082b0
5
5
  SHA512:
6
- metadata.gz: 926f57e7d5a1af30ec918269460bc04a20c25dd1feaaf8da910fe0a0ea241f7b4622feec775dfa8390bba16253b6d4b18130b0ea5a0e544259cae70cbd961cbe
7
- data.tar.gz: aca0677bd624c1a61d9f6d3dbe6f0e74991d690557d4abed0abf4a3e4a240931434a375b633ba7f424f04c3c36a2454811608ca2894a99d12cbdc2460876ca1b
6
+ metadata.gz: 7738a1869d51907dce7b981d7f0d3d1977f73a8baae57d947d2667155830495f9e19163a4c1fe5785e79fa262a7733c6d1d31086b6bc0ed85191bd31d6ad829b
7
+ data.tar.gz: f3506ffdc5133c083030ac87f0056a1373de5802520116956b0e30c00ea3d473e1e55f9fa62e813048e98c063aa05446a44e61ff49540dcc266e74546d6da7fa
@@ -8,7 +8,12 @@ module Hecks
8
8
  # complete and point at real verbs. `BluebookBuilder::Validation
9
9
  # #validate_provisions!` holds every `provides` to this table.
10
10
  #
11
- # key => :command | :query the kind of verb that key must name
11
+ # key => :command | :query | :port_operation the kind of verb that key must name
12
+ #
13
+ # A `:port_operation` verb is spelled `"Aggregate.Port.Operation"` and names
14
+ # an operation the chapter's hecksagon declares on one of its aggregates.
15
+ # The hecksagon attaches after the chapter is built, so the chapter itself
16
+ # only checks the spelling; `Registry#verify!` checks the operation exists.
12
17
  module Capabilities
13
18
  AUTHORIZATION = "authorization".freeze
14
19
  # Who may sign in, and with what role — rust/host's Google-OAuth
@@ -22,6 +27,11 @@ module Hecks
22
27
  # is. rust/host does not build this chapter's payloads; any field
23
28
  # mapping lives on the consuming hecksagon's `translates` ACL.
24
29
  IDENTITY = "identity".freeze
30
+ # Guest-facing newsletter signup: who subscribed, confirmed, or left —
31
+ # recognised by declaration, not the literal chapter name
32
+ # "Newsletter". rust/host's newsletter routes dispatch these verbs
33
+ # and read the subscribing aggregate's instances off `subscribe`.
34
+ NEWSLETTER = "newsletter".freeze
25
35
 
26
36
  CONTRACTS = {
27
37
  AUTHORIZATION => {
@@ -47,6 +57,16 @@ module Hecks
47
57
  link: :command,
48
58
  # look up the identity an authenticated pair resolves to
49
59
  resolve: :query
60
+ }.freeze,
61
+ NEWSLETTER => {
62
+ # a guest signs up; the aggregate it names is the subscriber
63
+ subscribe: :command,
64
+ # attach a display name to an existing subscriber
65
+ add_name: :command,
66
+ # a subscriber confirms their address from the emailed link
67
+ confirm: :command,
68
+ # a subscriber leaves from the emailed link
69
+ unsubscribe: :command
50
70
  }.freeze
51
71
  }.freeze
52
72
  end
@@ -113,12 +113,14 @@ module Hecks
113
113
  # @param capability [String] the capability the row belongs to, such as
114
114
  # `"authorization"`
115
115
  # @param row [Bluebook::Chapter::Provision] the one declared row to check
116
- # @param kind [Symbol] `:command` or `:query`, the kind of verb this row's key
117
- # must name (from `Capabilities::CONTRACTS`)
116
+ # @param kind [Symbol] `:command`, `:query` or `:port_operation`, the kind of
117
+ # verb this row's key must name (from `Capabilities::CONTRACTS`)
118
118
  # @return [void]
119
119
  # @raise [Bluebook::DSL::Malformed] if `row.verb` does not split into a real
120
120
  # aggregate and a member of the right kind that aggregate declares
121
121
  def validate_provided_verb!(bluebook, capability, row, kind)
122
+ return validate_provided_port_operation!(bluebook, capability, row) if kind == :port_operation
123
+
122
124
  aggregate_name, member = row.verb.split(".", 2)
123
125
  aggregate = bluebook.aggregate(aggregate_name)
124
126
  return if aggregate && member && provided_member_names(aggregate, kind).include?(member)
@@ -127,6 +129,26 @@ module Hecks
127
129
  "which names no #{kind} this chapter declares (spelled \"Aggregate.#{kind.capitalize}\")"
128
130
  end
129
131
 
132
+ # Checks the spelling of a `provides` verb that names a hecksagon port
133
+ # operation. The port itself is declared in the hecksagon, which attaches
134
+ # after this chapter is built, so all that can be checked here is that the
135
+ # verb has the `"Aggregate.Port.Operation"` shape and its aggregate is
136
+ # this chapter's; `Registry#verify!` checks the operation exists.
137
+ #
138
+ # @param bluebook [Bluebook::Chapter] the chapter the row was declared on
139
+ # @param capability [String] the capability the row belongs to
140
+ # @param row [Bluebook::Chapter::Provision] the one declared row to check
141
+ # @return [void]
142
+ # @raise [Bluebook::DSL::Malformed] if the verb is not `"Aggregate.Port.Operation"`
143
+ # or names an aggregate this chapter does not declare
144
+ def validate_provided_port_operation!(bluebook, capability, row)
145
+ aggregate_name, port, operation = row.verb.split(".", 3)
146
+ return if bluebook.aggregate(aggregate_name) && port && operation && !operation.include?(".")
147
+
148
+ raise Malformed, "#{bluebook.name} provides #{capability.inspect} #{row.key}: #{row.verb.inspect}, " \
149
+ "which is not spelled \"Aggregate.Port.Operation\" over an aggregate this chapter declares"
150
+ end
151
+
130
152
  # Names of the members a `provides` verb of the given kind may address.
131
153
  #
132
154
  # @param aggregate [Bluebook::Aggregate] the aggregate the verb is scoped to
data/lib/hecks/corpus.rb CHANGED
@@ -284,7 +284,7 @@ module Hecks
284
284
  # lifeadelics) — the generated module and Cargo feature are
285
285
  # "lifeadelics" now, matching every other domain's own convention.
286
286
  #
287
- # Lifeadelics also attaches `accounts`/`newsletter`/`payments`
287
+ # Lifeadelics also attaches `newsletter`/`payments`/`membership`
288
288
  # (its own vendored embryonaut_bluebooks packages, see
289
289
  # RUST_EXTERNAL_VENDORED_CHAPTERS below) and `Privacy` (an in-repo
290
290
  # framework chapter, lib/hecks/framework/bluebook/privacy.bluebook,
@@ -312,7 +312,6 @@ module Hecks
312
312
  # same as an in-repo vendored chapter (see
313
313
  # `rust_external_vendored_domain_dir`, below).
314
314
  RUST_EXTERNAL_VENDORED_CHAPTERS = {
315
- "accounts" => "lifeadelics",
316
315
  "newsletter" => "lifeadelics",
317
316
  "payments" => "lifeadelics",
318
317
  "membership" => "lifeadelics"
@@ -365,14 +364,12 @@ module Hecks
365
364
  # still fail, so an entry that starts passing breaks the build until
366
365
  # it is deleted here.
367
366
  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"
367
+ "newsletter" => "query Listing declares no where clause at all, on all 3 of its own Listing queries " \
368
+ "(Delivery/Issue/Subscriber) — rust/project/queries.rb's own no_wheres skip refuses " \
369
+ "to generate an unfiltered per_instance Listing (\"nothing for filter_entries to " \
370
+ "bake in\"); a structural Rust codegen limitation, not a bug in this vendored package",
371
+ "lifeadelics" => "same no_wheres gap as newsletter, on Registration.Listing — see newsletter's entry above",
372
+ "membership" => "same no_wheres gap as newsletter, on Person.All — see newsletter's entry above"
376
373
  }.freeze
377
374
 
378
375
  # The Cargo `[features]` table's raw text.
@@ -174,6 +174,34 @@ module Hecks
174
174
  }
175
175
  end
176
176
 
177
+ # Same seam as `membership` — which chapter answers the guest
178
+ # newsletter signup (`Registry#newsletter_provider_for`), with its
179
+ # declared verbs qualified and the subscribing aggregate named off
180
+ # `subscribe`. rust/host reads this instead of naming
181
+ # Newsletter::Subscriber.Subscribe and friends. `{}` when nothing
182
+ # this domain attaches provides newsletter.
183
+ # @param registry [Runtime::Registry] the booted registry `domain_name` is loaded in
184
+ # @param domain_name [String] the domain to export the newsletter binding for
185
+ # @return [Hash{Symbol => String, nil}] `:provider` (name), `:subscribe`, `:add_name`,
186
+ # `:confirm`, `:unsubscribe` (qualified verbs), and `:aggregate` (`:subscribe`'s own
187
+ # leading qualified aggregate name); `{}` if nothing this domain attaches provides
188
+ # newsletter
189
+ def newsletter(registry, domain_name)
190
+ provider = registry.newsletter_provider_for(domain_name)
191
+ return {} unless provider
192
+
193
+ capability = Bluebook::Capabilities::NEWSLETTER
194
+ subscribe = provider.provided_verb(capability, :subscribe)
195
+ {
196
+ provider: provider.name,
197
+ subscribe: subscribe,
198
+ add_name: provider.provided_verb(capability, :add_name),
199
+ confirm: provider.provided_verb(capability, :confirm),
200
+ unsubscribe: provider.provided_verb(capability, :unsubscribe),
201
+ aggregate: subscribe&.split(".")&.first
202
+ }
203
+ end
204
+
177
205
  # Translation IR, always as an array, with each aggregate's
178
206
  # precompiled SQL attached (`compiled_translation_aggregate`) —
179
207
  # this is the export a consumer embeds (`ir.json`'s `translations`
@@ -24,6 +24,7 @@ module Hecks
24
24
  verify_singleton_port_answers!
25
25
  refuse_cross_package_bluebook_merge!
26
26
  refuse_membership_without_identity!
27
+ refuse_unresolved_port_operations!
27
28
 
28
29
  @hecksagons.each_value do |hexagon|
29
30
  refuse_ungoverned_roles!(hexagon)
@@ -330,6 +331,37 @@ module Hecks
330
331
  "identity verbs are exported onto ir.json, not guessed at deploy."
331
332
  end
332
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
+
333
365
  def refuse_bounded_without_acl!(hexagon)
334
366
  return unless hexagon.bounded?
335
367
  return if hexagon.translates.any?
@@ -375,20 +375,45 @@ module Hecks
375
375
  # @return [Bluebook::Chapter, nil] the chapter that answers `domain`'s membership
376
376
  # questions, or nil if none does
377
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)
378
409
  hexagon = hecksagon(domain)
379
410
  vendored = Array(hexagon&.vendored_bluebooks).map { |name| Naming.pascal(name) }
380
411
  names = [domain.to_s, *Array(hexagon&.framework_members), *vendored]
381
412
  attached = names.filter_map { |name| bluebook(name) }
382
- .find { |chapter| chapter.provides?(Bluebook::Capabilities::MEMBERSHIP) }
413
+ .find { |chapter| chapter.provides?(capability) }
383
414
  return attached if attached
384
415
 
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) }
416
+ @bluebooks.values.find { |chapter| chapter.provides?(capability) }
392
417
  end
393
418
 
394
419
  # Resolves and memoizes `aggregate`'s authoritative repository.
data/lib/hecks/version.rb CHANGED
@@ -12,5 +12,5 @@ module Hecks
12
12
  # it there, or even in the consuming Gemfile, never closed the gap,
13
13
  # because gemspec evaluation happens before anything Bundler
14
14
  # resolves is actually loadable yet.
15
- VERSION = "2.0.0".freeze
15
+ VERSION = "2.1.0".freeze
16
16
  end
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: 2.0.0
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-24 00:00:00.000000000 Z
11
+ date: 2026-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prism