hecks 1.5.0 → 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.
@@ -126,6 +126,54 @@ module Hecks
126
126
  }
127
127
  end
128
128
 
129
+ # Same seam as `authorization` — which chapter answers "who may
130
+ # sign in" (`Registry#membership_provider_for`), with its declared
131
+ # verbs qualified and the membership aggregate named off `admit`.
132
+ # rust/host reads this instead of HECKS_MEMBERSHIP_AGGREGATE.
133
+ # `{}` when nothing this domain attaches provides membership.
134
+ # @param registry [Runtime::Registry] the booted registry `domain_name` is loaded in
135
+ # @param domain_name [String] the domain to export the membership binding for
136
+ # @return [Hash{Symbol => String, nil}] `:provider` (name), `:admit`, `:grant`,
137
+ # `:people` (qualified verbs), and `:aggregate` (`:admit`'s own leading
138
+ # aggregate name); `{}` if nothing this domain attaches provides membership
139
+ def membership(registry, domain_name)
140
+ provider = registry.membership_provider_for(domain_name)
141
+ return {} unless provider
142
+
143
+ capability = Bluebook::Capabilities::MEMBERSHIP
144
+ admit = provider.provided_verb(capability, :admit)
145
+ {
146
+ provider: provider.name,
147
+ admit: admit,
148
+ grant: provider.provided_verb(capability, :grant),
149
+ people: provider.provided_verb(capability, :people),
150
+ aggregate: admit&.split(".")&.first
151
+ }
152
+ end
153
+
154
+ # Same seam as `authorization` — which chapter answers "who is this
155
+ # authenticated pair" (`Registry#identity_provider_for`), with its
156
+ # declared verbs qualified. rust/host reads this instead of naming
157
+ # Identity::Identity.Register / ExternalIdentifier.Link. `{}` when
158
+ # nothing this domain attaches provides identity. Breaking in 2.0:
159
+ # Link's reference field is `identity`, never `identity_id`.
160
+ # @param registry [Runtime::Registry] the booted registry `domain_name` is loaded in
161
+ # @param domain_name [String] the domain to export the identity binding for
162
+ # @return [Hash{Symbol => String, nil}] `:provider` (name), `:register`, `:link`,
163
+ # `:resolve` (qualified verbs); `{}` if nothing this domain attaches provides identity
164
+ def identity(registry, domain_name)
165
+ provider = registry.identity_provider_for(domain_name)
166
+ return {} unless provider
167
+
168
+ capability = Bluebook::Capabilities::IDENTITY
169
+ {
170
+ provider: provider.name,
171
+ register: provider.provided_verb(capability, :register),
172
+ link: provider.provided_verb(capability, :link),
173
+ resolve: provider.provided_verb(capability, :resolve)
174
+ }
175
+ end
176
+
129
177
  # Translation IR, always as an array, with each aggregate's
130
178
  # precompiled SQL attached (`compiled_translation_aggregate`) —
131
179
  # this is the export a consumer embeds (`ir.json`'s `translations`
@@ -80,12 +80,17 @@ module Hecks
80
80
  # chapter must declare; nil means none
81
81
  # @param emits [Symbol] the kind of artifact `self` returns: `:artifact` (the default,
82
82
  # a single Hash or String) or `:files` (a path => contents tree)
83
+ # @param needs_world [Boolean] whether `self` is an export — a target that reads a
84
+ # domain's `.world`/`.hecksagon` bindings, not only its declaration. `false` (the
85
+ # default) for an ordinary projection; `true` refuses a `Projector.call` made with
86
+ # no `world:`.
83
87
  # @return [Symbol] the registered key
84
- def projects_as(key, requires: nil, declares: nil, emits: :artifact)
85
- @projection_emits = emits
86
- @projection_key = key.to_sym
87
- @projection_declares = Array(declares)
88
- @projection_requires = Array(requires)
88
+ def projects_as(key, requires: nil, declares: nil, emits: :artifact, needs_world: false)
89
+ @projection_emits = emits
90
+ @projection_key = key.to_sym
91
+ @projection_declares = Array(declares)
92
+ @projection_requires = Array(requires)
93
+ @projection_needs_world = needs_world
89
94
  Projector.register(@projection_key, self)
90
95
  @projection_key
91
96
  end
@@ -108,6 +113,14 @@ module Hecks
108
113
  # @return [Symbol] the kind of artifact `self` returns: `:artifact` or `:files`
109
114
  def projection_emits = @projection_emits || :artifact
110
115
 
116
+ # Tells whether this target is an export — one that needs a domain's
117
+ # `.world`/`.hecksagon` bindings, passed as `Projector.call`'s `world:`,
118
+ # rather than only its declaration.
119
+ #
120
+ # @return [Boolean] the value given to `projects_as`' `needs_world:`, or false
121
+ # before `projects_as` is called
122
+ def projection_needs_world? = @projection_needs_world || false
123
+
111
124
  # Names the capability module(s) a construct must satisfy, defaulting
112
125
  # to plain chapter-hood when `projects_as` named none.
113
126
  #
@@ -31,11 +31,13 @@ module Hecks
31
31
  #
32
32
  # an export takes a declaration and its bindings and answers
33
33
  # something that is the domain, running elsewhere — rust/project.rb's
34
- # generated crate, the WASM artifact, the SAM template
35
- # bin/project_deploy renders. It needs the `.world`/`.hecksagon` a
36
- # projection never looks at, because a running system has to know how
37
- # it is wired. That is the whole reason bin/project_deploy cannot use
38
- # this protocol: `call(bluebook:, options:)` has no channel for it.
34
+ # generated crate, the WASM artifact, the CloudFormation template
35
+ # `lib/hecks/projections/deploy` renders. It needs the
36
+ # `.world`/`.hecksagon` a projection never looks at, because a
37
+ # running system has to know how it is wired. A target that needs
38
+ # this declares `needs_world: true` (`Target#projects_as`) and reads
39
+ # `options.fetch(:world)`; `Projector.call`'s own `world:` keyword is
40
+ # what carries it, merged into `options` before the target ever runs.
39
41
  #
40
42
  # a state projection takes records — a domain after dispatch — and is
41
43
  # a read-model question wearing the same word.
@@ -88,17 +90,21 @@ module Hecks
88
90
  # @param bluebook [Bluebook::Behaviour::Chapter, Hecks::IR] the chapter or
89
91
  # IR-emitting construct to project
90
92
  # @param options [Hash] projector-specific options, passed through unchanged
93
+ # @param world [Bluebook::World, nil] the domain's `.world`/`.hecksagon` bindings,
94
+ # required by an export (`needs_world: true`); nil for an ordinary projection.
95
+ # Merged into `options[:world]` before the target runs — the target never
96
+ # receives it as a separate argument.
91
97
  # @return [Object] whatever the projector's own `call` returns: typically a
92
98
  # `Hash`/`String` artifact, or a `Hash{String => String}` file tree
93
99
  # @raise [UnknownProjector] if no projector is registered under `name`
94
100
  # @raise [WrongConstruct] if `bluebook` lacks a capability or aggregate the
95
- # projector requires
96
- def call(name, bluebook:, options: {})
101
+ # projector requires, or if the projector needs `world:` and none is given
102
+ def call(name, bluebook:, options: {}, world: nil)
97
103
  projector = registry.fetch(name.to_sym) do
98
104
  raise UnknownProjector, "no projector registered for #{name.inspect} — registered: #{registered.sort.inspect}"
99
105
  end
100
- admits!(name, projector, bluebook)
101
- projector.call(bluebook: bluebook, options: options)
106
+ admits!(name, projector, bluebook, world)
107
+ projector.call(bluebook: bluebook, options: world ? options.merge(world: world) : options)
102
108
  end
103
109
 
104
110
  # Refuses `construct` if it lacks a capability or declared aggregate
@@ -116,13 +122,17 @@ module Hecks
116
122
  # @param name [String, Symbol] the projector's registered key, used in the message
117
123
  # when refusing
118
124
  # @param projector [Module, Class, #call] the target being checked; consulted for
119
- # `projection_requires` and `projection_declares` when it answers them
125
+ # `projection_requires`, `projection_declares`, and `projection_needs_world?` when
126
+ # it answers them
120
127
  # @param construct [Bluebook::Behaviour::Chapter, Hecks::IR] the chapter or
121
128
  # IR-emitting construct offered to the projector
129
+ # @param world [Bluebook::World, nil] the `world:` given to `Projector.call`, checked
130
+ # against the target's own `needs_world:` declaration
122
131
  # @return [void]
123
- # @raise [WrongConstruct] if `construct` lacks a required capability, or the
124
- # chapter it is declares no aggregate the projector needs
125
- def admits!(name, projector, construct)
132
+ # @raise [WrongConstruct] if `construct` lacks a required capability, the chapter it
133
+ # is declares no aggregate the projector needs, or the projector needs `world:`
134
+ # and `world` is nil
135
+ def admits!(name, projector, construct, world = nil)
126
136
  needed = projector.respond_to?(:projection_requires) ? projector.projection_requires : []
127
137
  missing = needed.reject { |capability| capable?(construct, capability) }
128
138
  unless missing.empty?
@@ -133,11 +143,15 @@ module Hecks
133
143
 
134
144
  declared = projector.respond_to?(:projection_declares) ? projector.projection_declares : []
135
145
  absent = declared.reject { |named| construct.aggregate(named) }
136
- return if absent.empty?
146
+ unless absent.empty?
147
+ raise WrongConstruct,
148
+ "#{name.inspect} needs a chapter declaring #{absent.join(' and ')}; " \
149
+ "#{construct.name} declares no such aggregate."
150
+ end
151
+
152
+ return unless projector.respond_to?(:projection_needs_world?) && projector.projection_needs_world? && world.nil?
137
153
 
138
- raise WrongConstruct,
139
- "#{name.inspect} needs a chapter declaring #{absent.join(' and ')}; " \
140
- "#{construct.name} declares no such aggregate."
154
+ raise WrongConstruct, "#{name.inspect} needs .world/.hecksagon bindings — pass world: to Projector.call."
141
155
  end
142
156
 
143
157
  # Tells whether `construct` has the capability `admits!` requires of it.
@@ -138,12 +138,22 @@ module Hecks
138
138
  current_entries.reject { |entry| entry.fqn.aggregate.nil? }
139
139
  .group_by { |entry| [entry.fqn.aggregate, entry.fqn.verb] }
140
140
  .each do |(aggregate, verb), candidates|
141
+ # Bounded chapters wrap in their own module (`Domain::Aggregate`)
142
+ # so two BCs can both declare `Person` without colliding on
143
+ # Object::Person. Folder-spread files of the SAME chapter still
144
+ # get the shortcut — they are not BCs.
145
+ next if bounded_chapter?(candidates)
146
+
141
147
  shortcut_target(aggregate).define_singleton_method(verb) do |**args|
142
148
  installer.send(:dispatch_short, candidates, **args)
143
149
  end
144
150
  end
145
151
  end
146
152
 
153
+ def bounded_chapter?(candidates)
154
+ candidates.any? { |entry| entry.dispatcher.registry.bounded?(entry.fqn.domain) }
155
+ end
156
+
147
157
  def dispatch_short(candidates, **args)
148
158
  if candidates.length > 1
149
159
  shown = candidates.map { |entry| entry.fqn.to_s }.sort.join(", ")
@@ -17,15 +17,19 @@ module Hecks
17
17
  # @raise [Runtime::WiringError] if a bind names an undeclared aggregate, an
18
18
  # adapter cannot satisfy its port's verb or declared `answers`, a world
19
19
  # setting names a field its adapter does not declare, the default adapter
20
- # is unusable, or a command declares a role with no authorization provider
21
- # attached
20
+ # is unusable, a command declares a role with no authorization provider
21
+ # attached, or a membership chapter is loaded with no identity chapter
22
22
  def verify!
23
23
  verify_default_adapter!
24
24
  verify_singleton_port_answers!
25
25
  refuse_cross_package_bluebook_merge!
26
+ refuse_membership_without_identity!
26
27
 
27
28
  @hecksagons.each_value do |hexagon|
28
29
  refuse_ungoverned_roles!(hexagon)
30
+ refuse_unwired_framework_members!(hexagon)
31
+ refuse_unwired_vendored_bluebooks!(hexagon)
32
+ refuse_bounded_without_acl!(hexagon)
29
33
 
30
34
  hexagon.binds.each do |bind|
31
35
  # A domain-level default (§0) — `persisted_by "Heki"` bare,
@@ -261,6 +265,84 @@ module Hecks
261
265
  "without that it is silent decoration, the exact defect this refusal exists to catch"
262
266
  end
263
267
 
268
+ # A vendored embryonaut bluebook is a bounded context, not a pile of
269
+ # types the consumer inlines. `uses_embryonaut_bluebook` only loads
270
+ # the package's `.bluebook` files — persistence, Governance, and the
271
+ # anti-corruption layer (`translates` field mapping) live on a named
272
+ # sibling hecksagon the CONSUMER must declare. Without that hexagon
273
+ # the package has no wiring of its own, and cross-context field
274
+ # mapping has nowhere to be written. Breaking in 2.0: attaching a
275
+ # vendored chapter without `Hecks.hecksagon "PackageName"` refuses
276
+ # at boot rather than silently sharing the consumer's hexagon.
277
+ def refuse_unwired_vendored_bluebooks!(hexagon)
278
+ Array(hexagon.vendored_bluebooks).each do |package|
279
+ chapter_name = Naming.pascal(package)
280
+ next if hecksagon(chapter_name)
281
+
282
+ raise WiringError,
283
+ "#{hexagon.domain} attaches vendored bluebook #{package.inspect} " \
284
+ "(bounded context #{chapter_name}) but never declared " \
285
+ "Hecks.hecksagon #{chapter_name.inspect} — put that sibling " \
286
+ "(and any `translates` ACL) in context_map.hecksagon; " \
287
+ "same-name blocks merge, order-independent."
288
+ end
289
+ end
290
+
291
+ # Same gate as vendored packages: `uses_framework` loads a bounded
292
+ # context, and the consumer must declare the sibling hecksagon that
293
+ # is its ACL. Governance/Identity/Privacy already follow this in
294
+ # every real consumer; boot now refuses a silent miss.
295
+ def refuse_unwired_framework_members!(hexagon)
296
+ Array(hexagon.framework_members).each do |member|
297
+ next if hecksagon(member)
298
+
299
+ raise WiringError,
300
+ "#{hexagon.domain} attaches framework member #{member.inspect} " \
301
+ "(bounded context) but never declared Hecks.hecksagon " \
302
+ "#{member.inspect} — put that sibling (and any `translates` " \
303
+ "ACL) in context_map.hecksagon; same-name blocks merge, " \
304
+ "order-independent."
305
+ end
306
+ end
307
+
308
+ # An explicit `bounded` mark on a consumer chapter always needs an
309
+ # ACL — at least one `translates` block. Any field can be mapped;
310
+ # the BC does not list which. `uses_framework` / `uses_embryonaut_bluebook`
311
+ # mark the ATTACHED chapter bounded (module wrap, no Object shortcut)
312
+ # and require the sibling hecksagon above; they do not require a
313
+ # `translates` on that sibling unless the consumer also wrote `bounded`.
314
+ # rust/host Google sign-in reads `ir.json`'s `membership` and
315
+ # `identity` keys (Exporter.membership / Exporter.identity) —
316
+ # never a deploy-time env var. Membership without Identity is
317
+ # the exact gap that produced a live `google_unlinked` after a
318
+ # successful Google handshake: provision cannot Register/Link.
319
+ # Refuse at Ruby boot (and project_rust, which exports the same
320
+ # pair) so a missing sibling cannot ship.
321
+ def refuse_membership_without_identity!
322
+ return unless @bluebooks.values.any? { |chapter| chapter.provides?(Bluebook::Capabilities::MEMBERSHIP) }
323
+ return if @bluebooks.values.any? { |chapter| chapter.provides?(Bluebook::Capabilities::IDENTITY) }
324
+
325
+ raise WiringError,
326
+ "a chapter that provides \"membership\" is loaded, but none provides " \
327
+ "\"identity\" — rust/host Google sign-in cannot register or link an " \
328
+ "identity from the hecksagon/world. Attach Identity (`uses_framework " \
329
+ "\"Identity\"` plus a sibling Hecks.hecksagon \"Identity\") so the " \
330
+ "identity verbs are exported onto ir.json, not guessed at deploy."
331
+ end
332
+
333
+ def refuse_bounded_without_acl!(hexagon)
334
+ return unless hexagon.bounded?
335
+ return if hexagon.translates.any?
336
+
337
+ raise WiringError,
338
+ "#{hexagon.domain} is marked bounded but never declared a " \
339
+ "translates ACL — a bounded chapter wraps in its own module and " \
340
+ "cross-context field mapping lives on the hecksagon, not in " \
341
+ "rust/host and not as a field list on the bluebook. " \
342
+ "Add `translates \"Name\" do on Foreign::Event; trigger Local::Command, " \
343
+ "with: { ... } end` (any field) or drop `bounded`."
344
+ end
345
+
264
346
  # The suggestion, derived from whichever framework members actually
265
347
  # declare `provides "authorization"` — never a hardcoded name.
266
348
  def authorization_attachment_hint
@@ -1,6 +1,7 @@
1
1
  require_relative "registry/verification"
2
2
  require_relative "registry/saga_persistence"
3
3
  require_relative "outbox"
4
+ require_relative "../naming"
4
5
 
5
6
  module Hecks
6
7
  module Runtime
@@ -27,6 +28,7 @@ module Hecks
27
28
  @bluebooks = {}
28
29
  @bluebook_sources = {}
29
30
  @hecksagons = {}
31
+ @bounded_chapters = {}
30
32
  @ports = {}
31
33
  @adapters = {}
32
34
  @worlds = {}
@@ -186,8 +188,29 @@ module Hecks
186
188
  def add_hecksagon(item)
187
189
  existing = @hecksagons[item.domain]
188
190
  @hecksagons[item.domain] = existing ? merge_hecksagons(existing, item) : item
191
+ mark_bounded(item.domain) if item.bounded?
189
192
  end
190
193
 
194
+ # Marks `name` as a bounded context — called automatically by
195
+ # `uses_framework` / `uses_embryonaut_bluebook`, and by
196
+ # `add_hecksagon` when the block itself declared `bounded`.
197
+ # A bounded chapter wraps in its own module (no Object shortcut)
198
+ # and must have a `translates` ACL or boot refuses.
199
+ #
200
+ # @param name [String, Symbol] the chapter name to mark bounded
201
+ # @return [void]
202
+ def mark_bounded(name)
203
+ @bounded_chapters[name.to_s] = true
204
+ end
205
+
206
+ # Says whether `name` is a bounded context — attached via
207
+ # `uses_framework` / `uses_embryonaut_bluebook`, or a consumer
208
+ # chapter that declared `bounded` on its own hecksagon.
209
+ #
210
+ # @param name [String, Symbol] the chapter name to check
211
+ # @return [Boolean] whether that chapter is bounded
212
+ def bounded?(name) = @bounded_chapters[name.to_s] ? true : false
213
+
191
214
  # Registers a loaded port, keyed by its own declared name.
192
215
  #
193
216
  # @param item [Bluebook::Port, Bluebook::DomainPort] the loaded port
@@ -308,6 +331,66 @@ module Hecks
308
331
  @bluebooks.values.select { |chapter| chapter.provides?(Bluebook::Capabilities::AUTHORIZATION) }
309
332
  end
310
333
 
334
+ # The chapter that answers "who is this authenticated pair" for
335
+ # `domain` — the domain's own chapter, or any framework member its
336
+ # hecksagon attaches, that declares `provides "identity"`. Nil when
337
+ # none does. Replaces every check for the literal name "Identity":
338
+ # Identity is recognised by what it declares (Register/Link/ResolvedBy),
339
+ # and a chapter that declares the same thing is recognised the same way.
340
+ #
341
+ # @param domain [String, Symbol] the domain whose identity chapter is being resolved
342
+ # @return [Bluebook::Chapter, nil] the chapter that answers `domain`'s identity
343
+ # questions, or nil if none does
344
+ def identity_provider_for(domain)
345
+ names = [domain.to_s, *Array(hecksagon(domain)&.framework_members)]
346
+ attached = names.filter_map { |name| bluebook(name) }
347
+ .find { |chapter| chapter.provides?(Bluebook::Capabilities::IDENTITY) }
348
+ return attached if attached
349
+
350
+ # Sibling hecksagons — a consuming domain often wires Identity as
351
+ # `Hecks.hecksagon "Identity"` (so Register can attach Governance
352
+ # on that named hexagon), not only via uses_framework on the
353
+ # consuming domain. The chapter is loaded; it just isn't listed
354
+ # on the consumer's own hexagon.
355
+ @bluebooks.values.find { |chapter| chapter.provides?(Bluebook::Capabilities::IDENTITY) }
356
+ end
357
+
358
+ # The chapter that answers "who may sign in" for `domain` — the
359
+ # domain's own chapter, any framework member its hecksagon attaches,
360
+ # or any vendored embryonaut bluebook it attaches, that declares
361
+ # `provides "membership"`. Nil when none does. Replaces rust/host's
362
+ # own HECKS_MEMBERSHIP_AGGREGATE env var: Membership is recognised
363
+ # by what it declares (Person.Admit/GrantAccess/All), and a chapter
364
+ # that declares the same thing (Embryonaut::Member, say) is
365
+ # recognised the same way.
366
+ #
367
+ # Vendored packages are included here and not in
368
+ # `authorization_provider_for` because membership is a vendored
369
+ # embryonaut_bluebooks chapter (`uses_embryonaut_bluebook
370
+ # "membership"`), not a framework member shipped inside hecks.
371
+ # `Naming.pascal` is the same directory-to-chapter convention
372
+ # `EmbryonautBluebook.load!` already uses.
373
+ #
374
+ # @param domain [String, Symbol] the domain whose sign-in aggregate is being resolved
375
+ # @return [Bluebook::Chapter, nil] the chapter that answers `domain`'s membership
376
+ # questions, or nil if none does
377
+ def membership_provider_for(domain)
378
+ hexagon = hecksagon(domain)
379
+ vendored = Array(hexagon&.vendored_bluebooks).map { |name| Naming.pascal(name) }
380
+ names = [domain.to_s, *Array(hexagon&.framework_members), *vendored]
381
+ attached = names.filter_map { |name| bluebook(name) }
382
+ .find { |chapter| chapter.provides?(Bluebook::Capabilities::MEMBERSHIP) }
383
+ return attached if attached
384
+
385
+ # Sibling hecksagons — Lifeadelics vendors Membership as
386
+ # `Hecks.hecksagon "Membership"` (so Person::Admit can attach
387
+ # Governance on that named hexagon), not via uses_embryonaut_bluebook
388
+ # on the consuming domain. The chapter is loaded; it just isn't
389
+ # listed on Lifeadelics' own hexagon. Any loaded chapter that
390
+ # provides membership is the bounded-context answer.
391
+ @bluebooks.values.find { |chapter| chapter.provides?(Bluebook::Capabilities::MEMBERSHIP) }
392
+ end
393
+
311
394
  # Resolves and memoizes `aggregate`'s authoritative repository.
312
395
  #
313
396
  # @param domain [String, Symbol] name of the domain `aggregate` belongs to
@@ -437,12 +520,20 @@ module Hecks
437
520
  # @return [Bluebook::Hecksagon] a new wiring with every list-shaped fact
438
521
  # concatenated, `base` then `overlay`
439
522
  def merge_hecksagons(base, overlay)
523
+ # Same-name blocks concatenate regardless of which file they came
524
+ # from (`lifeadelics.hecksagon`, `context_map.hecksagon`, an
525
+ # environment overlay). Order-independent: list facts uniq, so
526
+ # loading context_map before or after the domain file is the same
527
+ # merged hecksagon. Binds stay concatenated (BindingPolicy still
528
+ # refuses a genuine double-bind).
440
529
  Bluebook::Hecksagon.new(
441
530
  domain: base.domain,
442
531
  binds: base.binds + overlay.binds,
443
- subscriptions: base.subscriptions + overlay.subscriptions,
444
- framework_members: base.framework_members + overlay.framework_members,
445
- vendored_bluebooks: base.vendored_bluebooks + overlay.vendored_bluebooks
532
+ subscriptions: (base.subscriptions + overlay.subscriptions).uniq,
533
+ framework_members: (base.framework_members + overlay.framework_members).uniq,
534
+ vendored_bluebooks: (base.vendored_bluebooks + overlay.vendored_bluebooks).uniq,
535
+ bounded: base.bounded? || overlay.bounded?,
536
+ translates: (base.translates + overlay.translates).uniq
446
537
  )
447
538
  end
448
539
 
data/lib/hecks/version.rb CHANGED
@@ -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 = "1.5.0".freeze
15
+ VERSION = "2.0.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: 1.5.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Young
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-20 00:00:00.000000000 Z
11
+ date: 2026-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prism
@@ -378,6 +378,9 @@ files:
378
378
  - lib/hecks/ports/query/ordering.rb
379
379
  - lib/hecks/projections.rb
380
380
  - lib/hecks/projections/bootstrap_table.rb
381
+ - lib/hecks/projections/deploy/fargate.rb
382
+ - lib/hecks/projections/deploy/lambda.rb
383
+ - lib/hecks/projections/deploy/shared.rb
381
384
  - lib/hecks/projections/diagrams.rb
382
385
  - lib/hecks/projections/glossary.rb
383
386
  - lib/hecks/projections/glossary/html.rb
@@ -1 +0,0 @@
1
- ../../../../examples/compliance/bluebook/compliance.bluebook