hecks 3.6.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/bluebook/meta_validator/modes.rb +14 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot/disk_cache.rb +28 -3
- data/lib/hecks/bluebook/meta_validator/verdict_cache/prebuilt.rb +44 -0
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +3 -17
- data/lib/hecks/bluebook/meta_validator.rb +4 -2
- data/lib/hecks/cache_dir.rb +10 -0
- data/lib/hecks/corpus/routes.rb +1 -1
- data/lib/hecks/deploy/bluebook/deploy.bluebook +109 -0
- data/lib/hecks/deploy/oidc.json +10 -0
- data/lib/hecks/doors/usage_cache.rb +19 -9
- data/lib/hecks/fuzzing/coverage_campaign.rb +42 -17
- data/lib/hecks/fuzzing/failure_triage.rb +80 -0
- data/lib/hecks/fuzzing/form_census/chapter_forms.rb +38 -0
- data/lib/hecks/fuzzing/form_census/reference_forms.rb +49 -0
- data/lib/hecks/fuzzing/form_census.rb +28 -52
- data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +22 -7
- data/lib/hecks/fuzzing/isolated_boot/scratch_schema.rb +27 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +6 -2
- data/lib/hecks/fuzzing/mutation/checks.rb +76 -0
- data/lib/hecks/fuzzing/mutation/finders.rb +136 -0
- data/lib/hecks/fuzzing/mutation/judge.rb +94 -0
- data/lib/hecks/fuzzing/mutation/observation.rb +38 -0
- data/lib/hecks/fuzzing/mutation/operators.rb +52 -0
- data/lib/hecks/fuzzing/mutation/report.rb +78 -0
- data/lib/hecks/fuzzing/mutation/run.rb +99 -0
- data/lib/hecks/fuzzing/mutation/selection.rb +32 -0
- data/lib/hecks/fuzzing/mutation/site.rb +21 -0
- data/lib/hecks/fuzzing/mutation/source_map.rb +59 -0
- data/lib/hecks/fuzzing/mutation.rb +12 -0
- data/lib/hecks/fuzzing/properties/catalog.rb +10 -59
- data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +63 -0
- data/lib/hecks/fuzzing/properties/read_model_names.rb +50 -0
- data/lib/hecks/fuzzing/properties/read_models.rb +138 -0
- data/lib/hecks/fuzzing/properties/role_checks.rb +50 -0
- data/lib/hecks/fuzzing/properties.rb +7 -0
- data/lib/hecks/fuzzing/replay/observation.rb +1 -1
- data/lib/hecks/fuzzing/replay/role_check.rb +49 -0
- data/lib/hecks/fuzzing/replay/session.rb +22 -6
- data/lib/hecks/fuzzing/replay.rb +1 -0
- data/lib/hecks/fuzzing/runtime_coverage.rb +70 -0
- data/lib/hecks/fuzzing/shrinker/value_simplification.rb +64 -0
- data/lib/hecks/fuzzing/shrinker.rb +3 -0
- data/lib/hecks/fuzzing.rb +3 -0
- data/lib/hecks/gate/stages.yml +12 -2
- data/lib/hecks/hecks/adapters/deploy_toolchain/answers.rb +2 -1
- data/lib/hecks/hecks/adapters/github_rulesets.rb +7 -5
- data/lib/hecks/hecks/adapters/process_pool.rb +26 -4
- data/lib/hecks/hecks/adapters/site_toolchain/prober.rb +64 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +41 -5
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +37 -0
- data/lib/hecks/hecks/hecks.hecksagon +6 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -1
- data/lib/hecks/projections/deploy/box/roll_scripts.rb +6 -4
- data/lib/hecks/projections/deploy/box/settings/patterns.rb +4 -0
- data/lib/hecks/projections/deploy/box/settings/types.rb +7 -3
- data/lib/hecks/projections/deploy/box/settings.rb +18 -0
- data/lib/hecks/projections/deploy/box/shared_database.rb +69 -0
- data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +6 -7
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/box/templates/provision-database.sh.tmpl +97 -0
- data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +4 -2
- data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +2 -1
- data/lib/hecks/projections/deploy/box.rb +14 -2
- data/lib/hecks/projections/deploy/shared_database_instance/templates/Makefile.tmpl +11 -0
- data/lib/hecks/projections/deploy/shared_database_instance/templates/README.md.tmpl +25 -0
- data/lib/hecks/projections/deploy/shared_database_instance/templates/rds.yaml.tmpl +180 -0
- data/lib/hecks/projections/deploy/shared_database_instance.rb +106 -0
- data/lib/hecks/projections/deploy/vercel/settings.rb +83 -0
- data/lib/hecks/projections/deploy/vercel.rb +134 -0
- data/lib/hecks/projections/site/probe.rb +131 -0
- data/lib/hecks/projections/site/site_host/dockerfile.rb +1 -1
- data/lib/hecks/projections/site/site_host/templates.rb +12 -4
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +9 -2
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +1 -0
- data/lib/hecks/quality_control/cli/qa_sweep/seed_loop.rb +10 -2
- data/lib/hecks/runtime/value/validation.rb +34 -3
- data/lib/hecks/rust_build/native_build.rb +29 -1
- data/lib/hecks/rust_build/wasm.rb +16 -3
- data/lib/hecks/site/bluebook/site.bluebook +31 -0
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/tools/deploy_recipe/generation.rb +59 -0
- data/lib/hecks/tools/deploy_recipe/targets.rb +56 -2
- data/lib/hecks/tools/deploy_recipe/worlds.rb +2 -1
- data/lib/hecks/tools/deploy_recipe.rb +20 -10
- data/lib/hecks/tools/fuzz_sweep/options.rb +21 -10
- data/lib/hecks/tools/fuzz_sweep/reporting.rb +25 -3
- data/lib/hecks/tools/fuzz_sweep.rb +26 -9
- data/lib/hecks/tools/lanes/files.rb +2 -4
- data/lib/hecks/tools/lanes/live.rb +3 -4
- data/lib/hecks/tools/lanes/ruleset.rb +23 -1
- data/lib/hecks/tools/lanes.rb +12 -1
- data/lib/hecks/tools/mutation_run.rb +73 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +3 -1
- data/prebuilt/4e6c3cd1f578a38732ea5454dae52562d03d8e9886216077a5c34cbf589f3463.marshal +0 -0
- data/prebuilt/911cdcc56b5a094ed26583a4c76401dba6455b1f434722ed22b850bc1b1bdd28.marshal +0 -0
- data/prebuilt/verdicts-3230a251570aa03666708d16f92e646feab6a3f8cc2a3540c017d748e41d41a1.json +1 -0
- data/qa/settings.yml +1 -0
- data/rust/codegen/src/fielded.rs +3 -3
- data/rust/codegen/src/json_codec.rs +2 -2
- data/rust/codegen/src/naming.rs +20 -0
- data/rust/host/Cargo.toml +6 -2
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +28 -5
- data/rust/host/src/auth.rs +11 -1
- data/rust/host/src/boundary_fuzz/api.rs +260 -0
- data/rust/host/src/boundary_fuzz/auth.rs +119 -0
- data/rust/host/src/boundary_fuzz/expr_json.rs +158 -0
- data/rust/host/src/boundary_fuzz/ir.rs +79 -0
- data/rust/host/src/boundary_fuzz/server.rs +227 -0
- data/rust/host/src/boundary_fuzz/web.rs +125 -0
- data/rust/host/src/commerce_ir.rs +459 -0
- data/rust/host/src/extension.rs +100 -0
- data/rust/host/src/fuzz_support.rs +165 -0
- data/rust/host/src/ir.rs +4 -448
- data/rust/host/src/journal.rs +1 -1
- data/rust/host/src/lib.rs +39 -0
- data/rust/host/src/main.rs +10 -44
- data/rust/host/src/mint.rs +1 -1
- data/rust/host/src/payments/tests.rs +8 -8
- data/rust/host/src/payments.rs +10 -10
- data/rust/host/src/rate_limit.rs +27 -41
- data/rust/host/src/secrets.rs +44 -0
- data/rust/host/src/server.rs +6 -1
- data/rust/host/src/web/commerce.rs +113 -0
- data/rust/host/src/web/newsletter.rs +2 -1
- data/rust/host/src/web/newsletter_send.rs +1 -1
- data/rust/host/src/web/registration_receipt.rs +2 -2
- data/rust/host/src/web/registrations.rs +49 -49
- data/rust/host/src/web/resend_webhook.rs +299 -0
- data/rust/host/src/web.rs +55 -49
- data/rust/parser/fuzz/.gitignore +4 -0
- data/rust/parser/fuzz/Cargo.lock +89 -0
- data/rust/parser/fuzz/Cargo.toml +27 -0
- data/rust/parser/fuzz/README.md +19 -0
- data/rust/parser/fuzz/fuzz_targets/chapter.rs +50 -0
- data/rust/parser/fuzz/fuzz_targets/resolve.rs +35 -0
- data/rust/parser/src/parse/command.rs +14 -2
- data/rust/parser/src/parse/mod.rs +25 -0
- data/rust/parser/src/ruby_value.rs +95 -6
- data/rust/src/kernel/json.rs +34 -3
- metadata +58 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1f5088f868a4fb798d8295bdf93ad836ac63abeeac372d19ea486ed445587944
|
|
4
|
+
data.tar.gz: 526e4623eeb9947b2ccf74cf8599ee8f0820b677335c36f33905f20695fdb0bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6a604f67014c7c7136749ad1d2d661652083a5ab03274faa5531a221f0e6a27e81b4be0e0a464ec5d7253eac7ebc33aae342308fd58a724cc65d90f77d5a68c
|
|
7
|
+
data.tar.gz: c087ba04b615b664d1895fc040c75c4a906b330d080e24e360f9086b2893250f71cf7970d77253064e4ec91d1be12962497fce4097bcb8df66f45cbc1e31effe
|
|
@@ -118,6 +118,20 @@ module Hecks
|
|
|
118
118
|
@shadow_parsing = previous
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
# Wraps `block` with `shadow_parsing?` false, restoring it afterward. The language's own
|
|
122
|
+
# grammar is built once per process and memoized, so it cannot take the mode of whichever
|
|
123
|
+
# caller happens to ask for it first.
|
|
124
|
+
#
|
|
125
|
+
# @yield the one-time grammar build, with `shadow_parsing?` false throughout
|
|
126
|
+
# @return [Object] the block's own return value
|
|
127
|
+
def while_not_shadow_parsing
|
|
128
|
+
previous = @shadow_parsing
|
|
129
|
+
@shadow_parsing = false
|
|
130
|
+
yield
|
|
131
|
+
ensure
|
|
132
|
+
@shadow_parsing = previous
|
|
133
|
+
end
|
|
134
|
+
|
|
121
135
|
# Wraps `block` with `disabled?` forced false, restoring it afterward.
|
|
122
136
|
# Only `grammar_registry`'s one-time fixpoint build uses this.
|
|
123
137
|
#
|
|
@@ -4,6 +4,10 @@ module Hecks
|
|
|
4
4
|
module SyntaxBoot
|
|
5
5
|
# The grammar table's memo keys and its cross-process copy on disk.
|
|
6
6
|
module DiskCache
|
|
7
|
+
# How long an entry nobody has read stays before the next write sweeps it away. A read
|
|
8
|
+
# marks an entry as used, so only the tables of other code age out.
|
|
9
|
+
UNREAD_KEEP_SECONDS = 24 * 60 * 60
|
|
10
|
+
|
|
7
11
|
# `equal?`, not `==` — chapter identity, not value equality, is the
|
|
8
12
|
# fact this cache key tracks.
|
|
9
13
|
def same_chapters?(cached, current)
|
|
@@ -23,18 +27,29 @@ module Hecks
|
|
|
23
27
|
def disk_cache_enabled? = ENV["HECKS_SYNTAX_BOOT_CACHE"] != "off"
|
|
24
28
|
|
|
25
29
|
# Fails toward a real boot, never toward a wrong table: a missing
|
|
26
|
-
# file, a corrupt blob, or a permission error just misses the cache.
|
|
30
|
+
# file, a corrupt blob, or a permission error just misses the cache. A table
|
|
31
|
+
# the user has not cached is looked for among the ones the gem ships.
|
|
27
32
|
def read_disk_cache(chapters)
|
|
28
33
|
return nil unless disk_cache_enabled?
|
|
29
34
|
|
|
30
35
|
path = disk_cache_path(chapters)
|
|
31
|
-
return
|
|
36
|
+
return load_table(path, touch: true) if File.exist?(path)
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
shipped = Hecks::CacheDir.prebuilt(File.basename(path))
|
|
39
|
+
File.exist?(shipped) ? load_table(shipped) : nil
|
|
34
40
|
rescue StandardError
|
|
35
41
|
nil
|
|
36
42
|
end
|
|
37
43
|
|
|
44
|
+
# @param path [String] a table file
|
|
45
|
+
# @param touch [Boolean] whether to mark it as used (a shipped file is read-only)
|
|
46
|
+
# @return [Hash, nil] the table
|
|
47
|
+
def load_table(path, touch: false)
|
|
48
|
+
table = Marshal.load(File.binread(path)) # rubocop:disable Security/MarshalLoad -- own cache or the gem's own file, never external input
|
|
49
|
+
FileUtils.touch(path) if touch
|
|
50
|
+
table
|
|
51
|
+
end
|
|
52
|
+
|
|
38
53
|
# Writes to a PID-suffixed temp file and renames it into place, so a
|
|
39
54
|
# concurrent writer's rename can only overwrite with identical
|
|
40
55
|
# content, never leave a torn file for a concurrent reader.
|
|
@@ -46,10 +61,20 @@ module Hecks
|
|
|
46
61
|
tmp_path = "#{path}.#{Process.pid}.tmp"
|
|
47
62
|
File.binwrite(tmp_path, Marshal.dump(result))
|
|
48
63
|
File.rename(tmp_path, path)
|
|
64
|
+
prune_disk_cache
|
|
49
65
|
rescue StandardError
|
|
50
66
|
nil
|
|
51
67
|
end
|
|
52
68
|
|
|
69
|
+
# Removes the tables no process has read for `UNREAD_KEEP_SECONDS`.
|
|
70
|
+
def prune_disk_cache
|
|
71
|
+
Dir.glob(File.join(cache_dir, "*.marshal")).each do |file|
|
|
72
|
+
File.delete(file) if Time.now - File.mtime(file) > UNREAD_KEEP_SECONDS
|
|
73
|
+
rescue SystemCallError
|
|
74
|
+
next
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
53
78
|
def disk_cache_path(chapters)
|
|
54
79
|
File.join(cache_dir, "#{disk_cache_key(chapters)}.marshal")
|
|
55
80
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Bluebook
|
|
3
|
+
module MetaValidator
|
|
4
|
+
module VerdictCache
|
|
5
|
+
# The verdict file the process reads (its own, else the one a packaged gem ships) and the
|
|
6
|
+
# Ruby series that file is valid for.
|
|
7
|
+
module Prebuilt
|
|
8
|
+
# The Ruby the judging code ran on, to the minor version: a patch release changes neither
|
|
9
|
+
# a verdict nor the encoding of one, so a cache built on one patch serves the others.
|
|
10
|
+
#
|
|
11
|
+
# @return [String] such as `ruby-3.3`
|
|
12
|
+
def ruby_series = "#{RUBY_ENGINE}-#{RUBY_VERSION[/\A\d+\.\d+/]}"
|
|
13
|
+
|
|
14
|
+
# The file the gem ships for the current code's verdicts, when it ships one.
|
|
15
|
+
#
|
|
16
|
+
# @return [String] an absolute path
|
|
17
|
+
def prebuilt_path = Hecks::CacheDir.prebuilt(File.basename(path))
|
|
18
|
+
|
|
19
|
+
# Reads and decodes the current code's file: the user's own, else the gem's prebuilt one.
|
|
20
|
+
#
|
|
21
|
+
# @return [Hash{String => Hash}] entries, or `{}` on any problem
|
|
22
|
+
def read
|
|
23
|
+
own = path
|
|
24
|
+
return read_file(own) if File.file?(own) && trusted?(own)
|
|
25
|
+
|
|
26
|
+
File.file?(prebuilt_path) ? read_file(prebuilt_path) : {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param file [String] a verdict file already trusted
|
|
30
|
+
# @return [Hash{String => Hash}] its entries, or `{}` on any problem
|
|
31
|
+
def read_file(file)
|
|
32
|
+
doc = JSON.parse(File.binread(file), max_nesting: false)
|
|
33
|
+
return {} unless doc.is_a?(Hash) && doc["format"] == FORMAT
|
|
34
|
+
|
|
35
|
+
decoded = decode(doc["entries"])
|
|
36
|
+
well_formed?(decoded) ? decoded : {}
|
|
37
|
+
rescue StandardError
|
|
38
|
+
{}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require_relative "../../cache_dir"
|
|
5
5
|
require_relative "../../version"
|
|
6
6
|
require_relative "verdict_cache/tagging"
|
|
7
|
+
require_relative "verdict_cache/prebuilt"
|
|
7
8
|
|
|
8
9
|
module Hecks
|
|
9
10
|
module Bluebook
|
|
@@ -22,6 +23,7 @@ module Hecks
|
|
|
22
23
|
# VerdictCache.flush # also runs at process exit
|
|
23
24
|
module VerdictCache
|
|
24
25
|
extend Tagging
|
|
26
|
+
extend Prebuilt
|
|
25
27
|
|
|
26
28
|
module_function
|
|
27
29
|
|
|
@@ -70,7 +72,7 @@ module Hecks
|
|
|
70
72
|
# file's relative path and bytes, in sorted order
|
|
71
73
|
def digest_of(lib)
|
|
72
74
|
state = Digest::SHA256.new
|
|
73
|
-
state << "#{FORMAT}\0#{
|
|
75
|
+
state << "#{FORMAT}\0#{ruby_series}\0#{Hecks::VERSION}\0"
|
|
74
76
|
judging_files(lib).each do |file|
|
|
75
77
|
state << file.delete_prefix(lib) << "\0" << File.binread(file) << "\0"
|
|
76
78
|
end
|
|
@@ -146,22 +148,6 @@ module Hecks
|
|
|
146
148
|
# @return [Hash{String => Hash}] every verdict this process knows
|
|
147
149
|
def entries = @entries ||= {}
|
|
148
150
|
|
|
149
|
-
# Reads and decodes the current code's file.
|
|
150
|
-
#
|
|
151
|
-
# @return [Hash{String => Hash}] entries, or `{}` on any problem
|
|
152
|
-
def read
|
|
153
|
-
file = path
|
|
154
|
-
return {} unless File.file?(file) && trusted?(file)
|
|
155
|
-
|
|
156
|
-
doc = JSON.parse(File.binread(file), max_nesting: false)
|
|
157
|
-
return {} unless doc.is_a?(Hash) && doc["format"] == FORMAT
|
|
158
|
-
|
|
159
|
-
decoded = decode(doc["entries"])
|
|
160
|
-
well_formed?(decoded) ? decoded : {}
|
|
161
|
-
rescue StandardError
|
|
162
|
-
{}
|
|
163
|
-
end
|
|
164
|
-
|
|
165
151
|
# Whether `file` and its directory belong to this user and nobody else
|
|
166
152
|
# can write them.
|
|
167
153
|
#
|
|
@@ -168,12 +168,14 @@ module Hecks
|
|
|
168
168
|
end
|
|
169
169
|
|
|
170
170
|
# The booted, self-judged language registry every ordinary bluebook is
|
|
171
|
-
# judged against — built once per process and memoized
|
|
171
|
+
# judged against — built once per process and memoized, with shadow parsing off: a caller
|
|
172
|
+
# inside `while_shadow_parsing` that is first to ask would otherwise memoize a grammar
|
|
173
|
+
# parsed in that mode, or fail to load it.
|
|
172
174
|
#
|
|
173
175
|
# @return [Runtime::Registry] the booted, fixpoint-judged language
|
|
174
176
|
# registry
|
|
175
177
|
def self.grammar_registry
|
|
176
|
-
@grammar_registry ||=
|
|
178
|
+
@grammar_registry ||= while_not_shadow_parsing do
|
|
177
179
|
registry = load_grammar_into(Runtime::Registry.new)
|
|
178
180
|
# Assigned before the fixpoint judge below, since judging re-enters
|
|
179
181
|
# grammar_registry (via fresh_runtime/Plan.for) and a bare `||=`
|
data/lib/hecks/cache_dir.rb
CHANGED
|
@@ -20,6 +20,16 @@ module Hecks
|
|
|
20
20
|
File.join(root, name)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# The directory the gem ships prebuilt cache files in, beside `lib/` so the digest of `lib/`
|
|
24
|
+
# that names a cache file never covers the files built from it. It exists only in a packaged
|
|
25
|
+
# gem, and it is part of the gem's own code, so it needs no ownership check.
|
|
26
|
+
#
|
|
27
|
+
# @param name [String] the file name inside it
|
|
28
|
+
# @return [String] the absolute path of `name` under the gem's `prebuilt/`
|
|
29
|
+
def prebuilt(name)
|
|
30
|
+
File.join(File.expand_path("../..", __dir__), "prebuilt", name)
|
|
31
|
+
end
|
|
32
|
+
|
|
23
33
|
# The cache root, resolved once per process.
|
|
24
34
|
#
|
|
25
35
|
# @return [String] the absolute path of the directory every `path` lives under
|
data/lib/hecks/corpus/routes.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Hecks
|
|
|
27
27
|
# hecks fuzz sweeps it too, once Fuzzing::Replay coerces value-object args
|
|
28
28
|
# before recomputing givens — today it doesn't, so this given reads as
|
|
29
29
|
# wrongly admitted.
|
|
30
|
-
Route.new(%r{\Aspec/fixtures/rust_host/}, :named_in, "rust/host/src/web.rs", "checkout_fixture",
|
|
30
|
+
Route.new(%r{\Aspec/fixtures/rust_host/}, :named_in, "rust/host/src/web/commerce.rs", "checkout_fixture",
|
|
31
31
|
"the Rust host's checkout fixture, pinned by its web and /api tests")
|
|
32
32
|
].freeze
|
|
33
33
|
end
|
|
@@ -274,6 +274,115 @@ Hecks.bluebook "Deploy" do
|
|
|
274
274
|
end
|
|
275
275
|
end
|
|
276
276
|
|
|
277
|
+
aggregate "VercelTarget" do
|
|
278
|
+
description "One domain's validated Vercel deploy target — what deployed_to(\"Vercel\") must resolve to before hecks deploy project generates a Vercel function's configuration from it"
|
|
279
|
+
|
|
280
|
+
identified_by :domain
|
|
281
|
+
|
|
282
|
+
attribute :domain, DomainName
|
|
283
|
+
attribute :region, Region
|
|
284
|
+
attribute :memory, Memory
|
|
285
|
+
attribute :max_duration, MaxDuration
|
|
286
|
+
|
|
287
|
+
value_object "DomainName" do
|
|
288
|
+
attribute :value, String
|
|
289
|
+
invariant("a domain is named") { !value.to_s.empty? }
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# A Vercel region id such as iad1 or fra1: three letters and a digit. Vercel refuses an
|
|
293
|
+
# unknown id at deploy; this checks the shape only.
|
|
294
|
+
value_object "Region" do
|
|
295
|
+
attribute :value, String
|
|
296
|
+
invariant("a region is a Vercel id such as iad1") { value.to_s.match?(/\A[a-z]{3}[0-9]\z/) }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Function memory in MB. Vercel sells 128 MB up to a plan's ceiling; the ceiling is the
|
|
300
|
+
# plan's to enforce.
|
|
301
|
+
value_object "Memory" do
|
|
302
|
+
attribute :value, Integer
|
|
303
|
+
invariant("memory is at least 128 MB") { value >= 128 }
|
|
304
|
+
invariant("memory is at most 10240 MB") { value <= 10240 }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Seconds a single invocation may run. 800 is the longest any plan allows.
|
|
308
|
+
value_object "MaxDuration" do
|
|
309
|
+
attribute :value, Integer
|
|
310
|
+
invariant("a duration is at least 1 second") { value >= 1 }
|
|
311
|
+
invariant("a duration is at most 800 seconds") { value <= 800 }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# All four required for the same reason as `BoxTarget.Declare`; this validates a target
|
|
315
|
+
# already resolved by `hecks deploy project`'s defaults.
|
|
316
|
+
command "Declare" do
|
|
317
|
+
goal "Validate one domain's Vercel deploy target before anything is generated from it"
|
|
318
|
+
|
|
319
|
+
attribute :domain, DomainName
|
|
320
|
+
attribute :region, Region
|
|
321
|
+
attribute :memory, Memory
|
|
322
|
+
attribute :max_duration, MaxDuration
|
|
323
|
+
|
|
324
|
+
sets :domain
|
|
325
|
+
sets :region
|
|
326
|
+
sets :memory
|
|
327
|
+
sets :max_duration
|
|
328
|
+
|
|
329
|
+
emits "VercelTargetDeclared"
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
aggregate "SharedDatabaseTarget" do
|
|
334
|
+
description "One platform's validated shared database instance — what deployed_to(\"AwsSharedDatabase\") must resolve to before hecks deploy project generates the RDS stack that several client sites each keep a database on."
|
|
335
|
+
|
|
336
|
+
identified_by :domain
|
|
337
|
+
|
|
338
|
+
attribute :domain, DomainName
|
|
339
|
+
attribute :region, Region
|
|
340
|
+
attribute :database_class, DatabaseClass
|
|
341
|
+
attribute :storage_gb, StorageGb
|
|
342
|
+
|
|
343
|
+
value_object "DomainName" do
|
|
344
|
+
attribute :value, String
|
|
345
|
+
invariant("a domain is named") { !value.to_s.empty? }
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
value_object "Region" do
|
|
349
|
+
attribute :value, String
|
|
350
|
+
invariant("a region is named") { !value.to_s.empty? }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# An RDS instance class such as db.t4g.small.
|
|
354
|
+
value_object "DatabaseClass" do
|
|
355
|
+
attribute :value, String
|
|
356
|
+
invariant("a database class is db.family.size, such as db.t4g.small") { value.to_s.match?(/\Adb\.[a-z][a-z0-9-]*\.[a-z0-9]+\z/) }
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Every client's database lives on this disk, so it is sized for all of them; 20 GB is
|
|
360
|
+
# RDS's gp3 floor.
|
|
361
|
+
value_object "StorageGb" do
|
|
362
|
+
attribute :value, Integer
|
|
363
|
+
invariant("storage is at least 20 GB") { value >= 20 }
|
|
364
|
+
invariant("storage is at most 65536 GB") { value <= 65536 }
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# All four required for the same reason as `BoxTarget.Declare`; this validates a target
|
|
368
|
+
# already resolved by `hecks deploy project`'s defaults.
|
|
369
|
+
command "Declare" do
|
|
370
|
+
goal "Validate one platform's shared database target before anything is generated from it"
|
|
371
|
+
|
|
372
|
+
attribute :domain, DomainName
|
|
373
|
+
attribute :region, Region
|
|
374
|
+
attribute :database_class, DatabaseClass
|
|
375
|
+
attribute :storage_gb, StorageGb
|
|
376
|
+
|
|
377
|
+
sets :domain
|
|
378
|
+
sets :region
|
|
379
|
+
sets :database_class
|
|
380
|
+
sets :storage_gb
|
|
381
|
+
|
|
382
|
+
emits "SharedDatabaseTargetDeclared"
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
277
386
|
# The validated shape of `hecks deploy tenant.provision`, which does the generating and provisioning.
|
|
278
387
|
# Not a saga: each of its steps is idempotent and local, so there is nothing to resume.
|
|
279
388
|
aggregate "Tenant" do
|
data/lib/hecks/deploy/oidc.json
CHANGED
|
@@ -301,6 +301,11 @@
|
|
|
301
301
|
"verb": "Deploy::ServiceRoll.Verify",
|
|
302
302
|
"role": null
|
|
303
303
|
},
|
|
304
|
+
{
|
|
305
|
+
"scope": "deploy:shared_database_target.declare",
|
|
306
|
+
"verb": "Deploy::SharedDatabaseTarget.Declare",
|
|
307
|
+
"role": null
|
|
308
|
+
},
|
|
304
309
|
{
|
|
305
310
|
"scope": "deploy:smoke_run.flag",
|
|
306
311
|
"verb": "Deploy::SmokeRun.Flag",
|
|
@@ -355,6 +360,11 @@
|
|
|
355
360
|
"scope": "deploy:tenant.reprovision",
|
|
356
361
|
"verb": "Deploy::Tenant.Reprovision",
|
|
357
362
|
"role": null
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"scope": "deploy:vercel_target.declare",
|
|
366
|
+
"verb": "Deploy::VercelTarget.Declare",
|
|
367
|
+
"role": null
|
|
358
368
|
}
|
|
359
369
|
],
|
|
360
370
|
"roles": [
|
|
@@ -8,8 +8,9 @@ module Hecks
|
|
|
8
8
|
#
|
|
9
9
|
# The help is a pure function of the domain's declarations and of the gem that projects them,
|
|
10
10
|
# and reading those is most of what a launcher costs. The answer is kept in a file named for a
|
|
11
|
-
# digest of everything it depends on (the domain's declaration files, the gem's
|
|
12
|
-
# the hecks and Ruby versions, the environment overlay, the command line and the
|
|
11
|
+
# digest of everything it depends on (the domain's declaration files, the gem's judging code and
|
|
12
|
+
# doors, the hecks and Ruby versions, the environment overlay, the command line and the
|
|
13
|
+
# program name),
|
|
13
14
|
# so any edit to one of them is simply a different entry: nothing is ever stale, only unused.
|
|
14
15
|
# Only an answer that ended well is kept, and any trouble with the cache itself is ignored
|
|
15
16
|
# and the help is worked out as if it did not exist.
|
|
@@ -71,15 +72,20 @@ module Hecks
|
|
|
71
72
|
# @return [String, nil] nil when the digest cannot be taken
|
|
72
73
|
def entry_path(domain, argv, program, audience = "")
|
|
73
74
|
digest = Digest::SHA256.new
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
end
|
|
77
|
-
[domain, library].each { |root| fingerprint(root, digest) }
|
|
75
|
+
key_parts(program, audience, argv).each { |part| digest << part.to_s << "\0" }
|
|
76
|
+
fingerprint_code(domain, digest)
|
|
78
77
|
File.join(directory, "usage-#{digest.hexdigest}.json")
|
|
79
78
|
rescue SystemCallError
|
|
80
79
|
nil
|
|
81
80
|
end
|
|
82
81
|
|
|
82
|
+
# @return [Array<String>] what the answer depends on besides the files: the gem and Ruby
|
|
83
|
+
# series, the environment overlay, the program and audience and the command line
|
|
84
|
+
def key_parts(program, audience, argv)
|
|
85
|
+
[Hecks::VERSION, Hecks::Bluebook::MetaValidator::VerdictCache.ruby_series,
|
|
86
|
+
ENV["HECKS_ENVIRONMENT"].to_s, program, audience, argv.join("\0")]
|
|
87
|
+
end
|
|
88
|
+
|
|
83
89
|
# Feeds the digest every declaration file under `root`: its path below `root` and its bytes,
|
|
84
90
|
# so a touch or a checkout elsewhere keeps the entry and an edit does not.
|
|
85
91
|
def fingerprint(root, digest)
|
|
@@ -88,9 +94,13 @@ module Hecks
|
|
|
88
94
|
end
|
|
89
95
|
end
|
|
90
96
|
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
# Feeds the digest what shapes a help text: the domain's declarations, the judging trees of
|
|
98
|
+
# the gem (the same set the verdict cache keys on) and the doors that render the text. An
|
|
99
|
+
# edit to the gem's projections, deploy or release code cannot change a help line.
|
|
100
|
+
def fingerprint_code(domain, digest)
|
|
101
|
+
fingerprint(domain, digest)
|
|
102
|
+
digest << Hecks::Bluebook::MetaValidator::VerdictCache.code_digest << "\0"
|
|
103
|
+
fingerprint(__dir__, digest)
|
|
94
104
|
end
|
|
95
105
|
|
|
96
106
|
# Reads the remembered answer and marks the entry as used, so a sweep keeps what is read.
|
|
@@ -26,6 +26,7 @@ module Hecks
|
|
|
26
26
|
@max_prefix_depth = max_prefix_depth
|
|
27
27
|
@corpus = []
|
|
28
28
|
@seen = Set.new
|
|
29
|
+
@runtime_seen = Set.new
|
|
29
30
|
@verb_hits = Hash.new(0)
|
|
30
31
|
@declared_verbs = Set.new
|
|
31
32
|
@seeds = @spliced = @seeds_with_new = 0
|
|
@@ -43,34 +44,39 @@ module Hecks
|
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
# Folds one seed's trace into the running coverage; admits a corpus entry if it reached
|
|
46
|
-
# a new tuple.
|
|
47
|
+
# a new tuple, or, when `runtime` is given, a new runtime line or branch.
|
|
47
48
|
#
|
|
48
49
|
# @param seed [Integer] the seed the trace was generated from
|
|
49
50
|
# @param plan [Fuzzing::CoverageCampaign::Plan] the plan that seed ran with
|
|
50
51
|
# @param trace [Fuzzing::SequenceGenerator::Trace] from `SequenceGenerator.trace`
|
|
52
|
+
# @param runtime [Enumerable<String>, nil] keys from `RuntimeCoverage.measure`; nil when
|
|
53
|
+
# runtime feedback is off, which leaves admission to tuples alone
|
|
51
54
|
# @return [void]
|
|
52
|
-
def record(seed, plan, trace)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
new_at = trace.coverage.filter_map do |index, tuple|
|
|
58
|
-
@verb_hits[tuple.split(" | ").first] += 1
|
|
59
|
-
index if @seen.add?(tuple)
|
|
60
|
-
end
|
|
61
|
-
return if new_at.empty?
|
|
55
|
+
def record(seed, plan, trace, runtime: nil)
|
|
56
|
+
tally(plan, trace)
|
|
57
|
+
new_at = new_tuple_steps(trace)
|
|
58
|
+
fresh_runtime = new_runtime?(runtime)
|
|
59
|
+
return if new_at.empty? && !fresh_runtime
|
|
62
60
|
|
|
63
61
|
@seeds_with_new += 1
|
|
64
|
-
|
|
62
|
+
# A tuple cuts the entry at the step that reached it; a runtime key is not tied to a step,
|
|
63
|
+
# so a seed that reached only new runtime keeps its whole sequence.
|
|
64
|
+
admit(seed, plan, new_at.empty? ? [trace.steps.size, 1].max : new_at.max + 1)
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# The number of distinct runtime lines and branches reached so far; zero while runtime
|
|
68
|
+
# feedback is off.
|
|
69
|
+
def runtime_seen = @runtime_seen.size
|
|
70
|
+
|
|
67
71
|
# The number of distinct coverage tuples reached so far.
|
|
68
72
|
def tuples_seen = @seen.size
|
|
69
73
|
|
|
70
74
|
# One line of running totals: tuples seen, seeds run, seeds with new coverage, splices.
|
|
71
75
|
def summary
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
runtime = @runtime_seen.empty? ? "" : " and #{@runtime_seen.size} runtime line/branch key(s)"
|
|
77
|
+
"coverage: #{@seen.size} distinct (verb, kind, state, mutation, outcome) tuple(s)#{runtime} over " \
|
|
78
|
+
"#{@seeds} seed(s); #{@seeds_with_new} seed(s) reached something new; " \
|
|
79
|
+
"#{@spliced} spliced from a corpus of #{@corpus.size}"
|
|
74
80
|
end
|
|
75
81
|
|
|
76
82
|
# Serializes the accumulated corpus and coverage knowledge, JSON-safe, so a later process can
|
|
@@ -78,10 +84,12 @@ module Hecks
|
|
|
78
84
|
# favor count, corpus and prefix-depth bounds — are configuration a caller supplies fresh each
|
|
79
85
|
# time, not state, so they are not part of this.
|
|
80
86
|
#
|
|
81
|
-
# @return [Hash] with string keys "corpus", "seen", "
|
|
87
|
+
# @return [Hash] with string keys "corpus", "seen", "runtime_seen", "verb_hits" and
|
|
88
|
+
# "declared_verbs"
|
|
82
89
|
def to_h
|
|
83
90
|
{ "corpus" => @corpus.map { |entry| { "spec" => entry[:spec], "attempts" => entry[:attempts] } },
|
|
84
91
|
"seen" => @seen.to_a,
|
|
92
|
+
"runtime_seen" => @runtime_seen.to_a.sort,
|
|
85
93
|
"verb_hits" => @verb_hits.dup,
|
|
86
94
|
"declared_verbs" => @declared_verbs.to_a }
|
|
87
95
|
end
|
|
@@ -94,9 +102,8 @@ module Hecks
|
|
|
94
102
|
def restore!(state)
|
|
95
103
|
state = state.transform_keys(&:to_s)
|
|
96
104
|
@corpus = Array(state["corpus"]).map { |entry| { spec: entry["spec"], attempts: entry["attempts"] } }
|
|
97
|
-
@seen = Set.new(Array(state[
|
|
105
|
+
@seen, @runtime_seen, @declared_verbs = %w[seen runtime_seen declared_verbs].map { |key| Set.new(Array(state[key])) }
|
|
98
106
|
@verb_hits = Hash.new(0).merge(state["verb_hits"] || {})
|
|
99
|
-
@declared_verbs = Set.new(Array(state["declared_verbs"]))
|
|
100
107
|
end
|
|
101
108
|
|
|
102
109
|
# Builds a campaign that already knows what a prior campaign's `#to_h` reached, configured
|
|
@@ -116,6 +123,24 @@ module Hecks
|
|
|
116
123
|
|
|
117
124
|
private
|
|
118
125
|
|
|
126
|
+
def tally(plan, trace)
|
|
127
|
+
@seeds += 1
|
|
128
|
+
@spliced += 1 if plan.spliced?
|
|
129
|
+
@declared_verbs.merge(trace.verbs)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The step indexes whose tuples nothing had reached before; each tuple's verb counts as a hit.
|
|
133
|
+
def new_tuple_steps(trace)
|
|
134
|
+
trace.coverage.filter_map do |index, tuple|
|
|
135
|
+
@verb_hits[tuple.split(" | ").first] += 1
|
|
136
|
+
index if @seen.add?(tuple)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def new_runtime?(runtime)
|
|
141
|
+
runtime.to_a.map { |key| @runtime_seen.add?(key) }.any?
|
|
142
|
+
end
|
|
143
|
+
|
|
119
144
|
def splice_prefix(random)
|
|
120
145
|
entry = @corpus[random.rand(@corpus.size)]
|
|
121
146
|
entry[:spec].merge("steps" => random.rand(1..entry[:attempts]))
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Fuzzing
|
|
7
|
+
# Tells one fuzz finding from another and keeps the smallest repro of each.
|
|
8
|
+
#
|
|
9
|
+
# A finding's identity is its property (or exception class), its message with the minted
|
|
10
|
+
# parts normalized away, and the shape of its shrunk steps (the verbs, in order). Two seeds
|
|
11
|
+
# that break the same property the same way share an identity however their ids differ.
|
|
12
|
+
module FailureTriage
|
|
13
|
+
# Directory, under the checkout, where minimized repros are kept.
|
|
14
|
+
REGRESSION_DIR = "spec/corpus/regressions".freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# @param property [String] the property name or exception class the finding opens with
|
|
19
|
+
# @param message [String, nil] the finding's message
|
|
20
|
+
# @param steps [Array<Hash>] the shrunk steps that reproduce it
|
|
21
|
+
# @return [String] a stable twelve-character identity
|
|
22
|
+
def signature(property, message, steps)
|
|
23
|
+
shape = steps.map { |step| step.key?("verb") ? step["verb"] : step[:verb] }
|
|
24
|
+
Digest::SHA256.hexdigest([property, normalize(message), shape].to_json)[0, 12]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Strips the parts a run mints (ids, hex, numbers, quoted literals) so two runs of one
|
|
28
|
+
# defect read the same.
|
|
29
|
+
#
|
|
30
|
+
# @param message [String, nil]
|
|
31
|
+
# @return [String]
|
|
32
|
+
def normalize(message)
|
|
33
|
+
message.to_s
|
|
34
|
+
.gsub(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, "<uuid>")
|
|
35
|
+
.gsub(/\b0x[0-9a-f]+\b|\b[0-9a-f]{16,}\b/i, "<hex>")
|
|
36
|
+
.gsub(/(["']).*?\1/, "<str>")
|
|
37
|
+
.gsub(/-?\d+(?:\.\d+)?/, "<n>")
|
|
38
|
+
.squeeze(" ").strip
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Collapses findings to one per identity, keeping the smallest sequence of each.
|
|
42
|
+
#
|
|
43
|
+
# @param failures [Array<Hash>] findings carrying `:signature`, `:message` and `:steps`
|
|
44
|
+
# @return [Array<Hash>] one finding per distinct identity, each with `:triage` (the
|
|
45
|
+
# identity) and `:duplicates` (how many findings it stands for)
|
|
46
|
+
def dedupe(failures)
|
|
47
|
+
failures.group_by { |failure| signature(failure[:signature], failure[:message], failure[:steps]) }
|
|
48
|
+
.map do |identity, group|
|
|
49
|
+
group.min_by { |failure| failure[:steps].length }.merge(triage: identity, duplicates: group.length)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Writes a minimized repro under `<root>/spec/corpus/regressions/<domain>/`, in the
|
|
54
|
+
# `{name, note, steps}` shape `hecks run` replays. An existing file is never touched: the
|
|
55
|
+
# identity is in the file name, so a finding already kept is left as it was.
|
|
56
|
+
#
|
|
57
|
+
# @param root [String] the checkout
|
|
58
|
+
# @param domain_name [String] the domain's basename
|
|
59
|
+
# @param finding [Hash] a `dedupe` result carrying `:triage`, `:steps`, `:seed`, `:message`
|
|
60
|
+
# @return [String, nil] the path written, or nil when that finding was already kept
|
|
61
|
+
def persist(root, domain_name, finding)
|
|
62
|
+
dir = File.join(root, REGRESSION_DIR, domain_name)
|
|
63
|
+
path = File.join(dir, "#{finding.fetch(:triage)}.json")
|
|
64
|
+
return nil if File.exist?(path)
|
|
65
|
+
|
|
66
|
+
FileUtils.mkdir_p(dir)
|
|
67
|
+
File.write(path, JSON.pretty_generate(document(domain_name, finding)), mode: "wx")
|
|
68
|
+
path
|
|
69
|
+
rescue Errno::EEXIST
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def document(domain_name, finding)
|
|
74
|
+
{ name: "#{domain_name}-regression-#{finding.fetch(:triage)}",
|
|
75
|
+
note: "hecks fuzz found #{finding[:signature]} at seed #{finding[:seed]}: #{finding[:message]}",
|
|
76
|
+
steps: finding.fetch(:steps) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
module FormCensus
|
|
4
|
+
# The chapter-level constructs an aggregate takes part in, folded onto its own hash so the
|
|
5
|
+
# per-aggregate forms can read them.
|
|
6
|
+
#
|
|
7
|
+
# Policies and read models live on the chapter, not the aggregate: an `across` policy is
|
|
8
|
+
# attributed to the aggregate whose event it answers, and a read model to each aggregate it
|
|
9
|
+
# takes a head from.
|
|
10
|
+
module ChapterForms
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# @param aggregate [Hash] one aggregate of an exported chapter
|
|
14
|
+
# @param chapter_ir [Hash] the exported chapter it belongs to
|
|
15
|
+
# @return [Hash] the aggregate with `across_policies` and `multi_head_read_models` added
|
|
16
|
+
def onto(aggregate, chapter_ir)
|
|
17
|
+
aggregate.merge("across_policies" => across_policies(aggregate, chapter_ir),
|
|
18
|
+
"multi_head_read_models" => multi_head_read_models(aggregate, chapter_ir))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Policies with a target domain whose triggering event is qualified by this aggregate.
|
|
22
|
+
def across_policies(aggregate, chapter_ir)
|
|
23
|
+
(chapter_ir["policies"] || []).select do |policy|
|
|
24
|
+
!policy["target_domain"].to_s.empty? && policy["on_event"].to_s.split(".", 2).first == aggregate["name"]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Read models composing two or more aggregate heads, one of them this aggregate.
|
|
29
|
+
def multi_head_read_models(aggregate, chapter_ir)
|
|
30
|
+
(chapter_ir["read_models"] || []).select do |read_model|
|
|
31
|
+
heads = read_model["aggregate_heads"] || []
|
|
32
|
+
heads.size >= 2 && heads.any? { |head| head["aggregate"] == aggregate["name"] }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|