hecks 3.0.4 → 3.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.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +25 -7
  3. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  5. data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
  6. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
  7. data/lib/hecks/bluebook/hexagon.rb +45 -25
  8. data/lib/hecks/bluebook/model_check.rb +7 -7
  9. data/lib/hecks/chapters.rb +31 -3
  10. data/lib/hecks/cli/check_era.rb +1 -1
  11. data/lib/hecks/cli/console.rb +24 -8
  12. data/lib/hecks/cli/deploy_template_diff.rb +1 -1
  13. data/lib/hecks/cli/doc_coverage.rb +1 -1
  14. data/lib/hecks/cli/domain_stub.rb +128 -0
  15. data/lib/hecks/cli/interview_draft.rb +246 -0
  16. data/lib/hecks/cli/model_check.rb +1 -1
  17. data/lib/hecks/cli/project_cli.rb +69 -18
  18. data/lib/hecks/cli/reference.rb +2 -1
  19. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
  20. data/lib/hecks/cli/seed_semantics_corpus.rb +38 -9
  21. data/lib/hecks/cli/spec_example.rb +1 -1
  22. data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
  23. data/lib/hecks/corpus.rb +11 -8
  24. data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
  25. data/lib/hecks/deploy/oidc.json +5 -0
  26. data/lib/hecks/doc/reference.rb +4 -3
  27. data/lib/hecks/doors/cli_door.rb +13 -13
  28. data/lib/hecks/doors/cli_runner.rb +52 -32
  29. data/lib/hecks/doors/launcher_options.rb +23 -5
  30. data/lib/hecks/doors/mcp_door.rb +40 -18
  31. data/lib/hecks/doors/mcp_door_scope.rb +244 -24
  32. data/lib/hecks/doors.rb +1 -1
  33. data/lib/hecks/embryonaut_bluebook.rb +2 -2
  34. data/lib/hecks/forms/banking_presentation.rb +1 -1
  35. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
  36. data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
  37. data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
  38. data/lib/hecks/framework.rb +1 -1
  39. data/lib/hecks/fuzzing/combination_miner.rb +1 -1
  40. data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
  41. data/lib/hecks/fuzzing/differential.rb +4 -2
  42. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  43. data/lib/hecks/fuzzing/form_census.rb +2 -2
  44. data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
  45. data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
  46. data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
  47. data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
  48. data/lib/hecks/gate/stages.yml +8 -4
  49. data/lib/hecks/grammar/grammar.hecksagon +2 -2
  50. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
  51. data/lib/hecks/hecks/adapters/host_http.rb +4 -3
  52. data/lib/hecks/hecks/adapters/local_files.rb +59 -0
  53. data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
  54. data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
  55. data/lib/hecks/hecks/codebase.bluebook +4 -3
  56. data/lib/hecks/hecks/context_map.hecksagon +2 -2
  57. data/lib/hecks/hecks/custodian.bluebook +45 -3
  58. data/lib/hecks/hecks/hecks.hecksagon +10 -4
  59. data/lib/hecks/hecks/hecks.world +7 -4
  60. data/lib/hecks/hecks/site.behaviors +17 -0
  61. data/lib/hecks/hecks/site.world +10 -0
  62. data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
  63. data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
  64. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
  65. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +2 -1
  66. data/lib/hecks/projections/deploy/box/settings.rb +248 -0
  67. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +267 -0
  68. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +70 -0
  69. data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
  70. data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +165 -0
  71. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
  72. data/lib/hecks/projections/deploy/box.rb +387 -0
  73. data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
  74. data/lib/hecks/projections/deploy/fargate.rb +1 -1
  75. data/lib/hecks/projections/deploy/lambda.rb +11 -10
  76. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
  77. data/lib/hecks/projections/diagrams.rb +2 -2
  78. data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
  79. data/lib/hecks/projections/site/edge/checks.rb +137 -0
  80. data/lib/hecks/projections/site/edge/pattern.rb +50 -0
  81. data/lib/hecks/projections/site/edge/rules.rb +128 -0
  82. data/lib/hecks/projections/site/edge.rb +136 -0
  83. data/lib/hecks/projections/site/regions.rb +44 -0
  84. data/lib/hecks/projections/site/routes_ts.rb +284 -0
  85. data/lib/hecks/projections/site/site_cdn.rb +78 -0
  86. data/lib/hecks/projections/site/table/checks.rb +124 -0
  87. data/lib/hecks/projections/site/table/row_builder.rb +155 -0
  88. data/lib/hecks/projections/site/table.rb +98 -0
  89. data/lib/hecks/projections.rb +2 -0
  90. data/lib/hecks/projector/cli_projector.rb +172 -95
  91. data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
  92. data/lib/hecks/quality_control/adapters/agent.rb +2 -2
  93. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
  94. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
  95. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
  96. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
  97. data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
  98. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
  99. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
  100. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
  101. data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
  102. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
  103. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
  104. data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
  105. data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
  106. data/lib/hecks/quality_control/quality_control.bluebook +18 -18
  107. data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
  108. data/lib/hecks/runtime/command_interpreter.rb +2 -0
  109. data/lib/hecks/runtime/interpreting.rb +21 -0
  110. data/lib/hecks/runtime/invocation.rb +5 -3
  111. data/lib/hecks/runtime/registry/verification.rb +15 -29
  112. data/lib/hecks/runtime/registry.rb +14 -20
  113. data/lib/hecks/rust_build/append_optionals.rb +79 -0
  114. data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
  115. data/lib/hecks/rust_build/domain_name.rb +26 -0
  116. data/lib/hecks/rust_build/kernel_input.rb +58 -0
  117. data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
  118. data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
  119. data/lib/hecks/rust_build/project_rust.rb +57 -24
  120. data/lib/hecks/rust_build/rust_literal.rb +33 -0
  121. data/lib/hecks/rust_build/write_if_changed.rb +86 -0
  122. data/lib/hecks/site/bluebook/site.bluebook +205 -0
  123. data/lib/hecks/site/bluebook/site.hecksagon +5 -0
  124. data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
  125. data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
  126. data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
  127. data/lib/hecks/storehouse.rb +37 -7
  128. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
  129. data/lib/hecks/three_zero/forms.yml +88 -88
  130. data/lib/hecks/tools/ci_gates.rb +1 -1
  131. data/lib/hecks/tools/deploy_recipe.rb +14 -6
  132. data/lib/hecks/tools/oidc_manifests.rb +2 -1
  133. data/lib/hecks/tools/sequence_script.rb +2 -2
  134. data/lib/hecks/tools/site_routes.rb +152 -0
  135. data/lib/hecks/tools/tenant_provisioning.rb +1 -1
  136. data/lib/hecks/tools.rb +1 -0
  137. data/lib/hecks/version.rb +1 -1
  138. data/rust/build/src/cargo_sync.rs +1 -1
  139. data/rust/build/src/pipeline.rs +23 -16
  140. data/rust/build/src/resolve.rs +39 -6
  141. data/rust/host/HECKS_RELEASE +1 -1
  142. data/rust/host/src/api.rs +1 -1
  143. data/rust/host/src/dispatch.rs +87 -7
  144. data/rust/host/src/needs.rs +111 -0
  145. data/rust/host/src/web/newsletter/template.rs +69 -0
  146. data/rust/host/src/web/newsletter/tests.rs +43 -0
  147. data/rust/host/src/web/newsletter.rs +6 -1
  148. data/rust/host/src/web/newsletter_send.rs +1 -1
  149. data/rust/parser/src/keywords.rs +7 -6
  150. data/rust/parser/src/main.rs +4 -3
  151. data/rust/parser/src/parse/chapter.rs +1 -1
  152. data/rust/parser/src/parse/hecksagon.rs +25 -10
  153. data/rust/project/mutations.rb +4 -30
  154. data/rust/project/naming.rb +8 -28
  155. data/rust/project/write_if_changed.rb +4 -80
  156. data/rust/project_rust_pipeline.rb +16 -12
  157. data/rust/src/kernel/cli.rs +2 -0
  158. data/rust/src/kernel/needs.rs +74 -1
  159. metadata +39 -2
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Site
6
+ class Table
7
+ # One row with its defaults filled in.
8
+ Row = Struct.new(:path, :kind, :render, :auth, :verbs, :cache, :origin, :source,
9
+ :indexable, :switch, :off, :preview, :label, :seo, :redirect_to, :aliases,
10
+ :nav_group, :nav_order, :mobile_order, :footer_column, :footer_order,
11
+ :admin_key, :admin_order, :compress, :alb_rule, :cdn, keyword_init: true)
12
+
13
+ # Turns a declared member into a `Row`: checks its fields and their types, fills the
14
+ # defaults, and checks each closed-set value against the Site chapter's vocabulary.
15
+ class RowBuilder
16
+ # The fields a row may carry, with the Ruby class each takes (`:bool` is true or false).
17
+ FIELDS = { path: String, kind: String, render: String, auth: String, methods: String,
18
+ cache: String, origin: String, source: String, indexable: :bool, switch: String,
19
+ off: :bool, preview: String, label: String, seo: String, redirect_to: String,
20
+ aliases: String, nav_group: String, nav_order: Integer, mobile_order: Integer,
21
+ footer_column: String, footer_order: Integer, admin_key: String,
22
+ admin_order: Integer, compress: :bool, alb_rule: String, cdn: :bool }.freeze
23
+
24
+ # The value objects of the Site chapter's `Route` aggregate whose members are the closed
25
+ # sets, by the row field each constrains.
26
+ VOCABULARY = { kind: "Kind", render: "Render", auth: "Auth", cache: "CacheClass",
27
+ origin: "Origin", preview: "Preview" }.freeze
28
+
29
+ # A source that names a command or query, whose forms-scheme URL is the route's path.
30
+ DERIVED_SOURCE = /\A(command|query):([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)\z/
31
+ NAMED_SOURCE = /\A(global|collection):[a-z0-9_-]+\z/
32
+
33
+ # @param vocabulary [Hash{Symbol => Array<String>}] the closed sets
34
+ # @param registry [Runtime::Registry, nil] where a `command:` or `query:` is looked up
35
+ # @param problems [Array<String>] collects each problem found, one line each
36
+ def initialize(vocabulary:, registry:, problems:)
37
+ @vocabulary = vocabulary
38
+ @registry = registry
39
+ @problems = problems
40
+ end
41
+
42
+ # @param member [Hash{Symbol => Object}] the declared fields of one row
43
+ # @param index [Integer] the row's position, for a message about a row with no path
44
+ # @return [Row] the row with its defaults filled in; its problems are in `problems`
45
+ def call(member, index)
46
+ label = member[:path] || "row #{index + 1}"
47
+ fields = checked_fields(member, label)
48
+ source = fields[:source] || "none"
49
+ path = fields[:path] || derived_path(source)
50
+ label = path || label
51
+ check_source(label, source)
52
+ verbs = fields.delete(:methods)
53
+ row = Row.new(**fields, verbs: verbs, path: path, source: source,
54
+ kind: fields[:kind] || default_kind(source), auth: fields[:auth] || "public",
55
+ origin: fields[:origin] || default_origin(source))
56
+ fill_defaults(row)
57
+ check_values(row, label)
58
+ problem(label, "needs a path") if row.path.nil?
59
+ row
60
+ end
61
+
62
+ private
63
+
64
+ def checked_fields(member, label)
65
+ member.each_key do |key|
66
+ problem(label, "has no field #{key}; fields are #{FIELDS.keys.join(', ')}") unless FIELDS.key?(key)
67
+ end
68
+ known = member.slice(*FIELDS.keys)
69
+ known.each { |key, value| check_type(label, key, value) }
70
+ known
71
+ end
72
+
73
+ def check_type(label, key, value)
74
+ expected = FIELDS.fetch(key)
75
+ return if expected == :bool ? [true, false].include?(value) : value.is_a?(expected)
76
+
77
+ problem(label, "has #{key} #{value.inspect}; #{key} is #{expected == :bool ? 'true or false' : "a #{expected}"}")
78
+ end
79
+
80
+ def default_kind(source) = source.match?(DERIVED_SOURCE) ? "endpoint" : "page"
81
+
82
+ def default_origin(source) = source.match?(DERIVED_SOURCE) ? "domain" : "website"
83
+
84
+ # A `command:` or `query:` source leaves the path to the forms scheme,
85
+ # `/Chapter/Aggregate/Verb`.
86
+ def derived_path(source)
87
+ match = DERIVED_SOURCE.match(source) or return nil
88
+ "/#{match[2]}/#{match[3]}/#{match[4]}"
89
+ end
90
+
91
+ def fill_defaults(row)
92
+ row.off = false if row.off.nil?
93
+ row.compress = true if row.compress.nil?
94
+ row.cdn = true if row.cdn.nil?
95
+ row.render ||= row.kind == "page" ? "prerender" : "ssr"
96
+ row.verbs = list(row.verbs || (row.source.start_with?("command:") ? "POST" : "GET"))
97
+ row.aliases = list(row.aliases)
98
+ row.cache ||= default_cache(row)
99
+ row.indexable = indexable_by_default?(row) if row.indexable.nil?
100
+ fill_slots(row)
101
+ end
102
+
103
+ def fill_slots(row)
104
+ row.preview ||= "public"
105
+ row.switch ||= ""
106
+ row.footer_order ||= 0 if row.footer_column
107
+ row.admin_order ||= 0 if row.admin_key
108
+ end
109
+
110
+ def list(text) = text.to_s.split(",").map(&:strip).reject(&:empty?)
111
+
112
+ def default_cache(row)
113
+ return "no_store" if row.auth != "public" || row.kind == "endpoint"
114
+
115
+ row.origin == "assets" ? "immutable" : "page"
116
+ end
117
+
118
+ def indexable_by_default?(row) = row.kind == "page" && row.auth == "public" && !row.off
119
+
120
+ def check_values(row, label)
121
+ VOCABULARY.each_key do |field|
122
+ next if @vocabulary.fetch(field).include?(row[field])
123
+
124
+ problem(label, "has #{field} #{row[field].inspect}; #{field} is one of #{@vocabulary.fetch(field).join(', ')}")
125
+ end
126
+ row.verbs.each do |verb|
127
+ next if @vocabulary.fetch(:http_method).include?(verb)
128
+
129
+ problem(label, "has method #{verb.inspect}; methods are #{@vocabulary.fetch(:http_method).join(', ')}")
130
+ end
131
+ end
132
+
133
+ def check_source(label, source)
134
+ match = DERIVED_SOURCE.match(source)
135
+ unless match || source == "none" || source.match?(NAMED_SOURCE)
136
+ problem(label, "has source #{source.inspect}; use none, global:<slug>, collection:<name>, " \
137
+ "command:<Chapter>.<Aggregate>.<Command> or query:<Chapter>.<Aggregate>.<Query>")
138
+ end
139
+ return unless match && @registry
140
+ return if verb_exists?(*match.captures)
141
+
142
+ problem(label, "names #{source}, which no attached chapter declares")
143
+ end
144
+
145
+ def verb_exists?(word, chapter, aggregate, name)
146
+ found = @registry.bluebook(chapter)&.aggregate(aggregate) or return false
147
+ (word == "command" ? found.commands : found.queries).any? { |verb| verb.hecks_name == name }
148
+ end
149
+
150
+ def problem(label, text) = @problems << "#{label} #{text}"
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "table/row_builder"
4
+ require_relative "table/checks"
5
+
6
+ module Hecks
7
+ module Projections
8
+ module Site
9
+ # A client's route table, read from its chapter and checked against the Site chapter's
10
+ # vocabulary.
11
+ #
12
+ # The rows are the `member` lines of a `value_object "Route"` in any chapter of the project
13
+ # (the Site chapter's own `Route` aggregate names the fields and the closed sets). This class
14
+ # reads them, fills each row's defaults (the cache class from the auth and kind, the render
15
+ # from the kind, the path from a `command:` or `query:` source), and refuses a table that
16
+ # contradicts itself, naming every problem at once.
17
+ class Table
18
+ # A table the projection refuses; the message lists each problem on its own line.
19
+ class Invalid < ArgumentError; end
20
+
21
+ # The name of the value object a client declares its rows in.
22
+ ROW_OBJECT = "Route"
23
+
24
+ # @return [Array<Row>] the rows, in the order the client declared them
25
+ attr_reader :rows
26
+
27
+ # Finds the one chapter of a project that declares the route table.
28
+ #
29
+ # @param registry [Runtime::Registry] the registry the project booted into
30
+ # @return [Bluebook::Chapter] the chapter holding the `Route` value object with member rows
31
+ # @raise [Invalid] when no chapter, or more than one, declares one
32
+ def self.chapter(registry)
33
+ holders = registry.bluebooks.values.reject { |chapter| chapter.name == "Site" }
34
+ .select { |chapter| rows_of(chapter).any? }
35
+ raise Invalid, "no chapter declares a value_object \"#{ROW_OBJECT}\" with member rows" if holders.empty?
36
+ return holders.first if holders.one?
37
+
38
+ raise Invalid, "#{holders.size} chapters declare a value_object \"#{ROW_OBJECT}\": " \
39
+ "#{holders.map(&:name).join(', ')}; a project has one route table"
40
+ end
41
+
42
+ # Reads and checks one chapter's route table.
43
+ #
44
+ # @param chapter [Bluebook::Chapter] the chapter that declares the rows
45
+ # @param registry [Runtime::Registry] the registry the project booted into: it holds the
46
+ # Site chapter and, for a `command:` or `query:` source, the command or query
47
+ # @return [Table] the checked table
48
+ # @raise [Invalid] when the Site chapter is not attached or a row is refused
49
+ def self.read(chapter, registry:)
50
+ new(rows_of(chapter), vocabulary: vocabulary(registry), registry: registry)
51
+ end
52
+
53
+ # @param registry [Runtime::Registry] a registry holding the Site chapter
54
+ # @return [Hash{Symbol => Array<String>}] each constrained field to its allowed values, and
55
+ # `:http_method` to the verbs a route may answer
56
+ # @raise [Invalid] when the registry does not hold the Site chapter
57
+ def self.vocabulary(registry)
58
+ site = registry.bluebook("Site") or
59
+ raise Invalid, "the project does not attach the Site chapter (attaches \"Site\")"
60
+ route = site.aggregate("Route")
61
+ members = lambda do |name|
62
+ route.value_objects.find { |object| object.hecks_name == name }.members.map { |member| member.fetch(:value) }
63
+ end
64
+ RowBuilder::VOCABULARY.transform_values(&members).merge(http_method: members.call("HttpMethod"))
65
+ end
66
+
67
+ # @param chapter [Bluebook::Chapter] a chapter to search
68
+ # @param object [String] the name of the value object whose rows are wanted
69
+ # @return [Array<Hash{Symbol => Object}>] the member rows of its value objects of that name
70
+ def self.rows_of(chapter, object = ROW_OBJECT)
71
+ chapter.aggregates.flat_map(&:value_objects)
72
+ .select { |candidate| candidate.hecks_name == object }.flat_map(&:members)
73
+ end
74
+
75
+ # @param members [Array<Hash{Symbol => Object}>] the declared member rows
76
+ # @param vocabulary [Hash{Symbol => Array<String>}] the closed sets
77
+ # @param registry [Runtime::Registry, nil] where a `command:` or `query:` source is looked
78
+ # up; nil skips that check
79
+ # @raise [Invalid] when a row is refused
80
+ def initialize(members, vocabulary:, registry: nil)
81
+ problems = []
82
+ builder = RowBuilder.new(vocabulary: vocabulary, registry: registry, problems: problems)
83
+ @rows = members.each_with_index.map { |member, index| builder.call(member, index) }
84
+ Checks.new(@rows, problems).call
85
+ return if problems.empty?
86
+
87
+ raise Invalid, "the route table is refused:\n#{problems.map { |line| " - #{line}" }.join("\n")}"
88
+ end
89
+
90
+ # The navigation slots a row sits in.
91
+ #
92
+ # @param row [Row] a row
93
+ # @return [Array<String>] some of `desktop`, `mobile`, `footer`, `admin`
94
+ def navigation(row) = Checks.navigation(row)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -22,5 +22,7 @@ require_relative "projections/glossary"
22
22
  require_relative "projections/deploy/shared"
23
23
  require_relative "projections/deploy/lambda"
24
24
  require_relative "projections/deploy/fargate"
25
+ require_relative "projections/deploy/box"
25
26
  require_relative "projections/deploy/smoke"
26
27
  require_relative "projections/deploy/template_diff"
28
+ require_relative "projections/site/routes_ts"