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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 830e92d9660dc86cc7de1af49d85fd7ce5ab611e07341dd11f4633274b920fda
4
- data.tar.gz: 1c70b3c2994fd520a604ebd8c8cc8a53e7531e3cb5ba1dd59c6fd751b9b9dd99
3
+ metadata.gz: 8261eb6f8b8b490fda7cc10deed5d1668e677031b03d67c89ca2c706cb90140a
4
+ data.tar.gz: 005ef58bad7618ab6085b22c9483465d2b58af9a9c61db7a365e70db81f4a5d6
5
5
  SHA512:
6
- metadata.gz: 3aeb0240903bed58fbd5684c23ee2069e9b3021aef07082d11e26ffa5052cebccf42d78e3aff243ba192183460056113e0510c7667306d1ed2cde3048ad118c0
7
- data.tar.gz: e104c2252f0dbbb1ba5ea97a0f6f56f890cab76e12ff86f246605003738228b94062e317f16db1ebfb376753536d8309cedff86e00275781497232de67b17e1e
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)
@@ -133,6 +133,111 @@ Hecks.bluebook "Deploy" do
133
133
  end
134
134
  end
135
135
 
136
+ # A sibling of `LambdaTarget`, not a shared shape with it — value objects
137
+ # are not shared across aggregates in this DSL (`Tenant`'s own
138
+ # `DomainName`/`Realm`/`Schema` below already make the same point), so
139
+ # this aggregate carries its own copies of `DomainName`/`Region`/
140
+ # `Database`/`Web` rather than reaching into `LambdaTarget`'s.
141
+ aggregate "FargateTarget" do
142
+ description "One domain's validated AWS Fargate deploy target — what deployed_to(\"AwsFargate\") must resolve to before bin/project_deploy generates anything from it."
143
+
144
+ identified_by :domain
145
+
146
+ attribute :domain, DomainName
147
+ attribute :region, Region
148
+ attribute :cpu, Cpu
149
+ attribute :memory, Memory
150
+ attribute :database, Database
151
+ attribute :web, Web
152
+ attribute :port, Port
153
+
154
+ value_object "DomainName" do
155
+ attribute :value, String
156
+ invariant("a domain is named") { !value.to_s.empty? }
157
+ end
158
+
159
+ value_object "Region" do
160
+ attribute :value, String
161
+ invariant("a region is named") { !value.to_s.empty? }
162
+ end
163
+
164
+ # A positive integer, nothing more — AWS's real cpu/memory
165
+ # combinations (256 cpu units needs 512-2048 MB of `memory`, say)
166
+ # are a pairing `ECS` itself enforces at deploy time; encoding that
167
+ # whole table here would duplicate AWS's own rules for no benefit
168
+ # this generator needs. This only catches a typo (zero, negative)
169
+ # before a single file is generated, the same floor `Memory`/`Port`
170
+ # below hold for their own ranges.
171
+ value_object "Cpu" do
172
+ attribute :value, Integer
173
+ invariant("cpu is positive") { value.positive? }
174
+ end
175
+
176
+ # Fargate's own valid range, not Lambda's 128-10240 MB —
177
+ # `LambdaTarget`'s own `Memory` value object (above) is scoped to
178
+ # that aggregate alone, so this is a distinct value object under the
179
+ # same name rather than a shared one. A plain sanity floor and
180
+ # ceiling, not AWS's own cpu-paired table (see `Cpu`'s own comment).
181
+ value_object "Memory" do
182
+ attribute :value, Integer
183
+ invariant("memory is positive") { value.positive? }
184
+ invariant("memory is at most 122880 MB") { value <= 122880 }
185
+ end
186
+
187
+ # Same shape and reasoning as `LambdaTarget`'s own `Database` —
188
+ # "Postgres"/"Aurora" provision this domain's own RDS instance;
189
+ # "Shared" borrows another already-deployed domain's instance,
190
+ # isolated by a native Postgres schema. See that value object's own
191
+ # comment for the full reasoning; it applies unchanged here.
192
+ value_object "Database" do
193
+ attribute :value, String, one_of: ["Postgres", "Aurora", "Shared"]
194
+ end
195
+
196
+ # Same shape and reasoning as `LambdaTarget`'s own `Web` — "Rust"
197
+ # means the domain's own task also serves its public web UI
198
+ # in-process, over the port this aggregate's own `Port` attribute
199
+ # declares.
200
+ value_object "Web" do
201
+ attribute :value, String, one_of: ["None", "Rust"]
202
+ end
203
+
204
+ # The container port the task listens on and the target group
205
+ # routes to — a real TCP port, not an arbitrary integer.
206
+ value_object "Port" do
207
+ attribute :value, Integer
208
+ invariant("a port is at least 1") { value >= 1 }
209
+ invariant("a port is at most 65535") { value <= 65535 }
210
+ end
211
+
212
+ # All seven required, for the identical reason `LambdaTarget.Declare`
213
+ # requires all of its own attributes — see that command's own
214
+ # comment on `normalize_args`/`coerce_declared_arguments`. This
215
+ # validates a fully-resolved target, after `bin/project_deploy`'s own
216
+ # defaulting (`cpu`/`memory`/`port` each fall back to a Ruby-side
217
+ # default before this ever dispatches), not instead of it.
218
+ command "Declare" do
219
+ goal "Validate one domain's AwsFargate deploy target before anything is generated from it"
220
+
221
+ attribute :domain, DomainName
222
+ attribute :region, Region
223
+ attribute :cpu, Cpu
224
+ attribute :memory, Memory
225
+ attribute :database, Database
226
+ attribute :web, Web
227
+ attribute :port, Port
228
+
229
+ sets :domain
230
+ sets :region
231
+ sets :cpu
232
+ sets :memory
233
+ sets :database
234
+ sets :web
235
+ sets :port
236
+
237
+ emits "FargateTargetDeclared"
238
+ end
239
+ end
240
+
136
241
  # `bin/project_tenant`'S OWN VALIDATED SHAPE — same split LambdaTarget
137
242
  # draws with `bin/project_deploy`: this aggregate validates, that
138
243
  # script GENERATES/ACTS (writes `environments/<slug>.world`, ensures
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "audience": "Deploy",
3
3
  "scopes": [
4
+ {
5
+ "scope": "deploy:fargate_target.declare",
6
+ "verb": "Deploy::FargateTarget.Declare",
7
+ "role": null
8
+ },
4
9
  {
5
10
  "scope": "deploy:lambda_target.declare",
6
11
  "verb": "Deploy::LambdaTarget.Declare",