hecks 3.2.0 → 3.3.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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/bluebook/assembly/contracts.rb +2 -1
  3. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  4. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
  5. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
  6. data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
  7. data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
  8. data/lib/hecks/bluebook/query.rb +8 -5
  9. data/lib/hecks/cli/check_era.rb +3 -39
  10. data/lib/hecks/cli/interview_draft.rb +86 -31
  11. data/lib/hecks/cli/interview_session.rb +77 -26
  12. data/lib/hecks/cli/interview_shape.rb +179 -0
  13. data/lib/hecks/doors/cli_runner.rb +14 -1
  14. data/lib/hecks/doors/launcher_options.rb +13 -3
  15. data/lib/hecks/doors/mcp_door.rb +16 -2
  16. data/lib/hecks/doors/mcp_door_scope.rb +45 -5
  17. data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
  18. data/lib/hecks/doors.rb +1 -1
  19. data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
  20. data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
  21. data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
  22. data/lib/hecks/embryonaut_bluebook.rb +2 -0
  23. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
  24. data/lib/hecks/fuzzing/qa_settings.rb +15 -34
  25. data/lib/hecks/gate/stages.yml +113 -8
  26. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
  27. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
  28. data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
  29. data/lib/hecks/hecks/adapters/git.rb +59 -0
  30. data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
  31. data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
  32. data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
  33. data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
  34. data/lib/hecks/hecks/adapters/site_toolchain.rb +88 -11
  35. data/lib/hecks/hecks/codebase.behaviors +6 -0
  36. data/lib/hecks/hecks/codebase.bluebook +21 -0
  37. data/lib/hecks/hecks/custodian.behaviors +6 -0
  38. data/lib/hecks/hecks/custodian.bluebook +217 -0
  39. data/lib/hecks/hecks/hecks.hecksagon +24 -0
  40. data/lib/hecks/hecks/hecks.world +8 -3
  41. data/lib/hecks/hecks/site.world +5 -2
  42. data/lib/hecks/language/bluebook/query.bluebook +25 -0
  43. data/lib/hecks/language/bluebook/vocabulary.bluebook +220 -7
  44. data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
  45. data/lib/hecks/language/oidc.json +5 -0
  46. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
  47. data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
  48. data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
  49. data/lib/hecks/projections/deploy/box/settings.rb +40 -6
  50. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
  51. data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
  52. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
  53. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
  54. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
  55. data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
  56. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
  57. data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
  58. data/lib/hecks/projections/deploy/box.rb +28 -4
  59. data/lib/hecks/projections/deploy/fargate.rb +3 -3
  60. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  61. data/lib/hecks/projections/model/deviations.rb +1 -1
  62. data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
  63. data/lib/hecks/projections/site/edge/checks.rb +31 -5
  64. data/lib/hecks/projections/site/edge.rb +25 -11
  65. data/lib/hecks/projections/site/live_edge.rb +162 -0
  66. data/lib/hecks/projections/site/routes_ts.rb +50 -27
  67. data/lib/hecks/projections/site/site_cdn.rb +7 -4
  68. data/lib/hecks/projections/site/table/checks.rb +53 -7
  69. data/lib/hecks/projections/site/table/link_builder.rb +64 -0
  70. data/lib/hecks/projections/site/table/row_builder.rb +21 -6
  71. data/lib/hecks/projections/site/table.rb +16 -5
  72. data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
  73. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
  74. data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
  75. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
  76. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
  77. data/lib/hecks/quality_control/quality_control.bluebook +76 -124
  78. data/lib/hecks/runtime/interpreting.rb +3 -22
  79. data/lib/hecks/runtime/needs.rb +48 -0
  80. data/lib/hecks/runtime/query_interpreter.rb +3 -0
  81. data/lib/hecks/runtime/registry/verification.rb +3 -3
  82. data/lib/hecks/rust_build/host.rb +147 -0
  83. data/lib/hecks/rust_build/kernel_input.rb +51 -17
  84. data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
  85. data/lib/hecks/rust_build/project_rust.rb +7 -140
  86. data/lib/hecks/rust_build.rb +1 -0
  87. data/lib/hecks/site/bluebook/site.bluebook +80 -14
  88. data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
  89. data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
  90. data/lib/hecks/storehouse.rb +35 -1
  91. data/lib/hecks/tools/comment_style.rb +4 -2
  92. data/lib/hecks/tools/gate.rb +1 -1
  93. data/lib/hecks/tools/rust_comment_style.rb +2 -4
  94. data/lib/hecks/tools/site_routes.rb +69 -28
  95. data/lib/hecks/tools/tools_doc.rb +204 -0
  96. data/lib/hecks/tools.rb +1 -0
  97. data/lib/hecks/version.rb +1 -1
  98. data/lib/hecks/vocabulary.rb +152 -1
  99. data/lib/hecks.rb +1 -0
  100. data/qa/settings.yml +1 -2
  101. data/rust/build/src/lineage_pass.rs +31 -16
  102. data/rust/build/src/main.rs +4 -2
  103. data/rust/build/src/pipeline.rs +9 -20
  104. data/rust/codegen/src/attr.rs +1 -1
  105. data/rust/codegen/src/bridging.rs +8 -8
  106. data/rust/codegen/src/commands.rs +1 -1
  107. data/rust/codegen/src/constraints.rs +1 -1
  108. data/rust/codegen/src/dependency_planning.rs +1 -1
  109. data/rust/codegen/src/domain_generator.rs +2 -2
  110. data/rust/codegen/src/exemplar.rs +1 -1
  111. data/rust/codegen/src/expr_emitter.rs +1 -1
  112. data/rust/codegen/src/fielded.rs +1 -1
  113. data/rust/codegen/src/json.rs +214 -23
  114. data/rust/codegen/src/json_codec.rs +1 -1
  115. data/rust/codegen/src/literal.rs +6 -3
  116. data/rust/codegen/src/main.rs +23 -13
  117. data/rust/codegen/src/mutations.rs +1 -1
  118. data/rust/codegen/src/naming.rs +5 -3
  119. data/rust/codegen/src/optional_pass.rs +226 -0
  120. data/rust/codegen/src/ports.rs +1 -1
  121. data/rust/codegen/src/queries.rs +3 -3
  122. data/rust/codegen/src/reactions.rs +1 -1
  123. data/rust/codegen/src/read_models.rs +2 -2
  124. data/rust/codegen/src/reference_specs.rs +1 -1
  125. data/rust/codegen/src/registry.rs +4 -4
  126. data/rust/codegen/src/shared.rs +1 -1
  127. data/rust/{build → codegen}/src/sidecars.rs +9 -5
  128. data/rust/codegen/src/skip_reason.rs +1 -1
  129. data/rust/codegen/src/types.rs +4 -4
  130. data/rust/host/HECKS_RELEASE +1 -1
  131. data/rust/host/src/api.rs +1 -1
  132. data/rust/host/src/dispatch.rs +5 -0
  133. data/rust/host/src/needs.rs +193 -29
  134. data/rust/host/src/resend.rs +4 -4
  135. data/rust/host/src/ui_schema.rs +1 -1
  136. data/rust/parser/src/emit.rs +12 -0
  137. data/rust/parser/src/ir.rs +2 -0
  138. data/rust/parser/src/keywords.rs +2 -0
  139. data/rust/parser/src/main.rs +1 -0
  140. data/rust/parser/src/parse/command.rs +2 -44
  141. data/rust/parser/src/parse/mod.rs +1 -0
  142. data/rust/parser/src/parse/needs.rs +60 -0
  143. data/rust/parser/src/parse/query.rs +3 -0
  144. data/rust/src/exemplar/commands.rs +1 -1
  145. data/rust/src/exemplar/constraints.rs +1 -1
  146. data/rust/src/exemplar/fielded.rs +1 -1
  147. data/rust/src/exemplar/json.rs +1 -1
  148. data/rust/src/exemplar/mod.rs +1 -1
  149. data/rust/src/exemplar/mutations.rs +1 -1
  150. data/rust/src/exemplar/queries.rs +1 -1
  151. data/rust/src/exemplar/reactions.rs +1 -1
  152. data/rust/src/exemplar/read_models.rs +1 -1
  153. data/rust/src/exemplar/registry.rs +1 -1
  154. data/rust/src/exemplar/types.rs +1 -1
  155. data/rust/src/kernel/cli.rs +18 -8
  156. data/rust/src/kernel/needs.rs +104 -9
  157. data/rust/src/kernel/read_model.rs +1 -1
  158. data/rust/src/lib.rs +1 -1
  159. metadata +24 -30
  160. data/lib/hecks/rust_build/append_optionals.rb +0 -79
  161. data/lib/hecks/rust_build/rust_literal.rb +0 -33
  162. data/lib/hecks/three_zero/forms.yml +0 -127
  163. data/lib/hecks/three_zero.rb +0 -12
  164. data/rust/build/src/json.rs +0 -421
  165. data/rust/build/src/optional_pass.rs +0 -150
  166. data/rust/project/bridging.rb +0 -257
  167. data/rust/project/commands.rb +0 -877
  168. data/rust/project/constraints.rb +0 -86
  169. data/rust/project/dependency_planning.rb +0 -178
  170. data/rust/project/domain_generator.rb +0 -983
  171. data/rust/project/exemplar.rb +0 -148
  172. data/rust/project/expr_emitter.rb +0 -72
  173. data/rust/project/fielded.rb +0 -141
  174. data/rust/project/json_codec.rb +0 -641
  175. data/rust/project/mutations.rb +0 -695
  176. data/rust/project/naming.rb +0 -223
  177. data/rust/project/ports.rb +0 -65
  178. data/rust/project/queries.rb +0 -493
  179. data/rust/project/reactions.rb +0 -317
  180. data/rust/project/read_models.rb +0 -690
  181. data/rust/project/reference_specs.rb +0 -27
  182. data/rust/project/registry.rb +0 -396
  183. data/rust/project/skip_reason.rb +0 -29
  184. data/rust/project/types.rb +0 -225
  185. data/rust/project/write_if_changed.rb +0 -7
  186. data/rust/project.rb +0 -30
  187. data/rust/project_rust_pipeline.rb +0 -391
@@ -11,9 +11,10 @@ module Hecks
11
11
  # on it: the distribution's cache behaviours and the load balancer's listener rules.
12
12
  #
13
13
  # The blocks are returned by name, `"behaviors"` and `"listener_rules"`, as text from the
14
- # left margin. `Regions` puts each between the markers of the same name in the project's
15
- # template, at the markers' indentation. The text is a pure function of the table and the
16
- # edge rows.
14
+ # left margin; a project whose `Edge` row says `alb: false` has no load balancer and gets
15
+ # `"behaviors"` alone. `Regions` puts each between the markers of the same name in the
16
+ # project's template, at the markers' indentation. The text is a pure function of the table
17
+ # and the edge rows.
17
18
  module SiteCdn
18
19
  extend Projector::Target
19
20
 
@@ -25,7 +26,7 @@ module Hecks
25
26
  # @param options [Hash{Symbol => Object}] `:registry` (Runtime::Registry) the project booted
26
27
  # into; or `:table` (Table) and `:edge` (Edge), already read
27
28
  # @return [Hash{String => String}] each region's name to its text; empty when the chapter
28
- # declares no edge
29
+ # declares no edge, and without `"listener_rules"` when the edge has no load balancer
29
30
  # @raise [Table::Invalid] when the table or the edge is refused
30
31
  def call(bluebook:, options: {})
31
32
  registry = options[:registry]
@@ -33,6 +34,8 @@ module Hecks
33
34
  edge = options[:edge] || Edge.read(bluebook, table: table, vocabulary: Table.vocabulary(registry))
34
35
  return {} unless edge
35
36
 
37
+ return { "behaviors" => behaviors(edge) } unless edge.alb?
38
+
36
39
  { "behaviors" => behaviors(edge), "listener_rules" => listener_rules(edge) }
37
40
  end
38
41
 
@@ -1,19 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../edge/pattern"
4
+
3
5
  module Hecks
4
6
  module Projections
5
7
  module Site
6
8
  class Table
7
9
  # What no single row can show, and what a row contradicts in itself once its defaults are
8
- # filled: a path declared twice, a switch the rows disagree on, an off route that sits in a
9
- # navigation, an indexable route that is not public.
10
+ # filled: a path declared twice, a switch the rows disagree on, a navigation entry that
11
+ # cannot be linked to, an indexable route that is not public, a public route beneath a
12
+ # prefix that is not.
10
13
  class Checks
11
14
  SWITCH_ID = /\A[a-z][a-z0-9_]*\z/
15
+ FRAGMENT = /\A[A-Za-z0-9][\w:.-]*\z/
12
16
  PUBLIC_MENUS = %w[desktop mobile footer].freeze
13
17
 
14
18
  # The navigation slots a row sits in.
15
19
  #
16
- # @param row [Row] a row
20
+ # @param row [Row, Link] a row or link
17
21
  # @return [Array<String>] some of `desktop`, `mobile`, `footer`, `admin`
18
22
  def self.navigation(row)
19
23
  { "desktop" => row.nav_order, "mobile" => row.mobile_order, "footer" => row.footer_column,
@@ -22,8 +26,10 @@ module Hecks
22
26
 
23
27
  # @param rows [Array<Row>] the table's rows, defaults filled in
24
28
  # @param problems [Array<String>] collects each problem found, one line each
25
- def initialize(rows, problems)
29
+ # @param links [Array<Link>] the table's extra navigation entries
30
+ def initialize(rows, problems, links: [])
26
31
  @rows = rows
32
+ @links = links
27
33
  @problems = problems
28
34
  end
29
35
 
@@ -32,7 +38,8 @@ module Hecks
32
38
  check_duplicate_paths
33
39
  check_switches
34
40
  @rows.each { |row| check_row(row) }
35
- check_unique(@rows.filter_map(&:admin_key), "admin key")
41
+ @links.each { |link| check_link(link) }
42
+ check_unique((@rows + @links).filter_map(&:admin_key), "admin key")
36
43
  check_unique(@rows.map(&:source).grep(/\Aglobal:/), "source")
37
44
  end
38
45
 
@@ -63,6 +70,7 @@ module Hecks
63
70
  check_redirect(row, path)
64
71
  check_navigation(row, path)
65
72
  check_indexing(row, path)
73
+ check_beneath_prefix(row, path)
66
74
  return unless row.preview == "draft" && row.source == "none"
67
75
 
68
76
  problem(path, "previews as a draft but has source none; a draft preview needs a global: or collection: source")
@@ -90,13 +98,51 @@ module Hecks
90
98
  slots = self.class.navigation(row)
91
99
  return if slots.empty?
92
100
 
93
- problem(path, "is off and sits in the #{slots.join(' and ')} navigation") if row.off
94
- problem(path, "sits in the navigation but is a #{row.kind}; only a page does") unless row.kind == "page"
101
+ unless row.verbs.include?("GET")
102
+ problem(path, "sits in the navigation but answers #{row.verbs.join(',')}; a link is a GET")
103
+ end
95
104
  problem(path, "sits in the navigation and has a parameter") if path.match?(/[:*]/)
96
105
  problem(path, "sits in the navigation and has no label") if row.label.to_s.empty?
106
+ check_heading(row, path)
97
107
  check_menu_audience(row, path, slots)
98
108
  end
99
109
 
110
+ def check_heading(item, path)
111
+ return unless item.mobile_heading
112
+ return problem(path, "has a mobile_heading but no mobile_order") if item.mobile_order.nil?
113
+
114
+ problem(path, "has an empty mobile_heading") if item.mobile_heading.strip.empty?
115
+ end
116
+
117
+ def check_link(link)
118
+ path = "NavLink #{link.path}"
119
+ slots = self.class.navigation(link)
120
+ problem(path, "sits in no navigation; give it nav_order, mobile_order, footer_column or admin_key") if slots.empty?
121
+ problem(path, "has a parameter; a link is to one page") if link.path.to_s.match?(/[:*]/)
122
+ problem(path, "has no label") if link.label.to_s.empty?
123
+ if link.fragment && !link.fragment.match?(FRAGMENT)
124
+ problem(path, "has fragment #{link.fragment.inspect}; a fragment is an element id, written without #")
125
+ end
126
+ check_heading(link, path)
127
+ check_menu_audience(link, path, slots)
128
+ end
129
+
130
+ # A public route under a prefix that is not public (the sign-in page beside the admin
131
+ # pages) inherits nothing from the prefix, so the row has to say what it is: its cache
132
+ # class is declared, not derived from its auth.
133
+ def check_beneath_prefix(row, path)
134
+ return if row.auth != "public" || row.explicit_cache || row.path.nil?
135
+
136
+ pattern = Edge::Pattern.edge(row.path)
137
+ prefix = @rows.find do |other|
138
+ other.auth != "public" && other.path && Edge::Pattern.strictly_covers?(Edge::Pattern.edge(other.path), pattern)
139
+ end
140
+ return unless prefix
141
+
142
+ problem(path, "is public but sits beneath #{prefix.path}, which is #{prefix.auth}; " \
143
+ "name its cache class (cache: \"no_store\" for a sign-in page) to say that is meant")
144
+ end
145
+
100
146
  def check_menu_audience(row, path, slots)
101
147
  public_slots = slots & PUBLIC_MENUS
102
148
  if row.auth != "public" && public_slots.any?
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Site
6
+ class Table
7
+ # One extra navigation entry that points at a route the table declares, optionally at a
8
+ # fragment of its page. `off` and `switch` are copied from the route it points at.
9
+ Link = Struct.new(:path, :fragment, :label, :nav_group, :nav_order, :mobile_order, :mobile_heading,
10
+ :footer_column, :footer_order, :admin_key, :admin_order, :off, :switch, :auth,
11
+ keyword_init: true)
12
+
13
+ # Turns a declared `NavLink` member into a `Link`: checks its fields and their types, and
14
+ # copies the facts it shares with the route it points at.
15
+ class LinkBuilder
16
+ # The fields a link may carry, with the Ruby class each takes.
17
+ FIELDS = { path: String, fragment: String, label: String, nav_group: String, nav_order: Integer,
18
+ mobile_order: Integer, mobile_heading: String, footer_column: String,
19
+ footer_order: Integer, admin_key: String, admin_order: Integer }.freeze
20
+
21
+ # @param rows [Array<Row>] the table's rows, which a link points at
22
+ # @param problems [Array<String>] collects each problem found, one line each
23
+ def initialize(rows, problems)
24
+ @rows = rows
25
+ @problems = problems
26
+ end
27
+
28
+ # @param member [Hash{Symbol => Object}] the declared fields of one link
29
+ # @param index [Integer] the link's position, for a message about a link with no path
30
+ # @return [Link] the link; its problems are in `problems`
31
+ def call(member, index)
32
+ label = "NavLink #{member[:path] || "row #{index + 1}"}"
33
+ fields = checked_fields(member, label)
34
+ target = @rows.find { |row| row.path == fields[:path] && row.verbs.include?("GET") }
35
+ problem(label, "points at no route that answers GET; declare the route first") unless target
36
+ link = Link.new(**fields, off: target ? target.off : false, switch: target ? target.switch : "",
37
+ auth: target ? target.auth : "public")
38
+ link.footer_order ||= 0 if link.footer_column
39
+ link.admin_order ||= 0 if link.admin_key
40
+ link
41
+ end
42
+
43
+ private
44
+
45
+ def checked_fields(member, label)
46
+ member.each_key do |key|
47
+ problem(label, "has no field #{key}; fields are #{FIELDS.keys.join(', ')}") unless FIELDS.key?(key)
48
+ end
49
+ known = member.slice(*FIELDS.keys)
50
+ known.each do |key, value|
51
+ next if value.is_a?(FIELDS.fetch(key))
52
+
53
+ problem(label, "has #{key} #{value.inspect}; #{key} is a #{FIELDS.fetch(key)}")
54
+ end
55
+ problem(label, "needs a path") unless known[:path]
56
+ known
57
+ end
58
+
59
+ def problem(label, text) = @problems << "#{label} #{text}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -8,7 +8,8 @@ module Hecks
8
8
  Row = Struct.new(:path, :kind, :render, :auth, :verbs, :cache, :origin, :source,
9
9
  :indexable, :switch, :off, :preview, :label, :seo, :redirect_to, :aliases,
10
10
  :nav_group, :nav_order, :mobile_order, :footer_column, :footer_order,
11
- :admin_key, :admin_order, :compress, :alb_rule, :cdn, keyword_init: true)
11
+ :admin_key, :admin_order, :compress, :alb_rule, :cdn,
12
+ :seo_title, :edge_verbs, :mobile_heading, :explicit_cache, keyword_init: true)
12
13
 
13
14
  # Turns a declared member into a `Row`: checks its fields and their types, fills the
14
15
  # defaults, and checks each closed-set value against the Site chapter's vocabulary.
@@ -19,7 +20,8 @@ module Hecks
19
20
  off: :bool, preview: String, label: String, seo: String, redirect_to: String,
20
21
  aliases: String, nav_group: String, nav_order: Integer, mobile_order: Integer,
21
22
  footer_column: String, footer_order: Integer, admin_key: String,
22
- admin_order: Integer, compress: :bool, alb_rule: String, cdn: :bool }.freeze
23
+ admin_order: Integer, compress: :bool, alb_rule: String, cdn: :bool,
24
+ seo_title: String, edge_methods: String, mobile_heading: String }.freeze
23
25
 
24
26
  # The value objects of the Site chapter's `Route` aggregate whose members are the closed
25
27
  # sets, by the row field each constrains.
@@ -50,7 +52,9 @@ module Hecks
50
52
  label = path || label
51
53
  check_source(label, source)
52
54
  verbs = fields.delete(:methods)
53
- row = Row.new(**fields, verbs: verbs, path: path, source: source,
55
+ edge_verbs = fields.delete(:edge_methods)
56
+ row = Row.new(**fields, verbs: verbs, edge_verbs: edge_verbs, path: path, source: source,
57
+ explicit_cache: !fields[:cache].nil?,
54
58
  kind: fields[:kind] || default_kind(source), auth: fields[:auth] || "public",
55
59
  origin: fields[:origin] || default_origin(source))
56
60
  fill_defaults(row)
@@ -94,6 +98,7 @@ module Hecks
94
98
  row.cdn = true if row.cdn.nil?
95
99
  row.render ||= row.kind == "page" ? "prerender" : "ssr"
96
100
  row.verbs = list(row.verbs || (row.source.start_with?("command:") ? "POST" : "GET"))
101
+ row.edge_verbs = row.edge_verbs.nil? ? row.verbs : list(row.edge_verbs)
97
102
  row.aliases = list(row.aliases)
98
103
  row.cache ||= default_cache(row)
99
104
  row.indexable = indexable_by_default?(row) if row.indexable.nil?
@@ -123,11 +128,21 @@ module Hecks
123
128
 
124
129
  problem(label, "has #{field} #{row[field].inspect}; #{field} is one of #{@vocabulary.fetch(field).join(', ')}")
125
130
  end
126
- row.verbs.each do |verb|
127
- next if @vocabulary.fetch(:http_method).include?(verb)
131
+ check_verbs(row, label)
132
+ end
133
+
134
+ def check_verbs(row, label)
135
+ { "method" => row.verbs, "edge method" => row.edge_verbs }.each do |what, verbs|
136
+ verbs.each do |verb|
137
+ next if @vocabulary.fetch(:http_method).include?(verb)
128
138
 
129
- problem(label, "has method #{verb.inspect}; methods are #{@vocabulary.fetch(:http_method).join(', ')}")
139
+ problem(label, "has #{what} #{verb.inspect}; methods are #{@vocabulary.fetch(:http_method).join(', ')}")
140
+ end
130
141
  end
142
+ return if (row.verbs - row.edge_verbs).empty?
143
+
144
+ problem(label, "has edge_methods #{row.edge_verbs.join(',')}, which leave out its methods " \
145
+ "#{(row.verbs - row.edge_verbs).join(',')}; the edge must allow every verb the route answers")
131
146
  end
132
147
 
133
148
  def check_source(label, source)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "table/row_builder"
4
+ require_relative "table/link_builder"
4
5
  require_relative "table/checks"
5
6
 
6
7
  module Hecks
@@ -21,9 +22,15 @@ module Hecks
21
22
  # The name of the value object a client declares its rows in.
22
23
  ROW_OBJECT = "Route"
23
24
 
25
+ # The name of the value object a client declares its extra navigation entries in.
26
+ LINK_OBJECT = "NavLink"
27
+
24
28
  # @return [Array<Row>] the rows, in the order the client declared them
25
29
  attr_reader :rows
26
30
 
31
+ # @return [Array<Link>] the extra navigation entries, in the order the client declared them
32
+ attr_reader :links
33
+
27
34
  # Finds the one chapter of a project that declares the route table.
28
35
  #
29
36
  # @param registry [Runtime::Registry] the registry the project booted into
@@ -47,7 +54,8 @@ module Hecks
47
54
  # @return [Table] the checked table
48
55
  # @raise [Invalid] when the Site chapter is not attached or a row is refused
49
56
  def self.read(chapter, registry:)
50
- new(rows_of(chapter), vocabulary: vocabulary(registry), registry: registry)
57
+ new(rows_of(chapter), vocabulary: vocabulary(registry), registry: registry,
58
+ links: rows_of(chapter, LINK_OBJECT))
51
59
  end
52
60
 
53
61
  # @param registry [Runtime::Registry] a registry holding the Site chapter
@@ -76,20 +84,23 @@ module Hecks
76
84
  # @param vocabulary [Hash{Symbol => Array<String>}] the closed sets
77
85
  # @param registry [Runtime::Registry, nil] where a `command:` or `query:` source is looked
78
86
  # up; nil skips that check
87
+ # @param links [Array<Hash{Symbol => Object}>] the declared `NavLink` rows
79
88
  # @raise [Invalid] when a row is refused
80
- def initialize(members, vocabulary:, registry: nil)
89
+ def initialize(members, vocabulary:, registry: nil, links: [])
81
90
  problems = []
82
91
  builder = RowBuilder.new(vocabulary: vocabulary, registry: registry, problems: problems)
83
92
  @rows = members.each_with_index.map { |member, index| builder.call(member, index) }
84
- Checks.new(@rows, problems).call
93
+ link_builder = LinkBuilder.new(@rows, problems)
94
+ @links = links.each_with_index.map { |member, index| link_builder.call(member, index) }
95
+ Checks.new(@rows, problems, links: @links).call
85
96
  return if problems.empty?
86
97
 
87
98
  raise Invalid, "the route table is refused:\n#{problems.map { |line| " - #{line}" }.join("\n")}"
88
99
  end
89
100
 
90
- # The navigation slots a row sits in.
101
+ # The navigation slots a row or link sits in.
91
102
  #
92
- # @param row [Row] a row
103
+ # @param row [Row, Link] a row or link
93
104
  # @return [Array<String>] some of `desktop`, `mobile`, `footer`, `admin`
94
105
  def navigation(row) = Checks.navigation(row)
95
106
  end
@@ -64,20 +64,6 @@ module Hecks
64
64
  raise Refusal, "the working tree at #{@repo_dir} is dirty — commit or discard first:\n#{dirty}"
65
65
  end
66
66
 
67
- # Refuses one more pull request than the day's cap allows; a cap of zero is no cap.
68
- #
69
- # @param opened_today [Integer] how many PRs the ledger recorded since local midnight
70
- # @param cap [Integer] `PR_CAP_PER_DAY`
71
- # @return [void]
72
- # @raise [Refusal] when `opened_today` has reached a positive `cap`
73
- def assert_under_daily_cap!(opened_today:, cap:)
74
- return unless cap.positive? && opened_today >= cap
75
-
76
- raise Refusal, "#{opened_today} PR(s) already opened since local midnight, and " \
77
- "QualityControlDials::PR_CAP_PER_DAY is #{cap} — leave this as an open Bug/Angle " \
78
- "and open it tomorrow, or raise the dial in qa/settings.yml"
79
- end
80
-
81
67
  # Refuses a fix commit that is not part of the branch being opened.
82
68
  #
83
69
  # @param commit [String] the commit the ledger says fixes the bug
@@ -26,7 +26,7 @@ module Hecks
26
26
  SKIP_DIR_BASENAMES = %w[.git .hg .svn vendor node_modules .bundle tmp log coverage .yardoc .ruby-lsp
27
27
  .parked bower_components .claude dist build].freeze
28
28
 
29
- # Word-boundary matched so "hecksagain" (a real, separate gem) and "hecks_site" never
29
+ # Word-boundary matched so a near-miss gem name (a separate gem) and "hecks_site" never
30
30
  # false-positive as a hecks dependency.
31
31
  GEMFILE_PATTERN = /gem\s*\(?\s*["']hecks["']/
32
32
  GEMFILE_LOCK_PATTERN = /^\s+hecks\s+\(/
@@ -15,9 +15,10 @@ module Hecks
15
15
  # take the record and the `GitPr` adapter agrees. The command's `given`s are the rules, asked
16
16
  # as a dry run before anything is opened: the branch starts with the practice's prefix; with
17
17
  # `--bug`, the bug is `fixed`; with `--angle`, it is `investigating`. The adapter answers the
18
- # rest: the tree is clean, `PR_CAP_PER_DAY` (0 = uncapped) is unmet since local midnight, and
19
- # the bug's commit is an ancestor of `HEAD`. It is idempotent. `QA_REPO_DIR` picks the checkout
20
- # `git` runs in, so a spec can use a throwaway repo and a fake `gh`.
18
+ # rest: the tree is clean and the bug's commit is an ancestor of `HEAD`. The per-day cap
19
+ # (`PR_CAP_PER_DAY`, 0 = uncapped) is `DailyQuota.Take`'s own `given`: asked as a dry run with
20
+ # the others, and taken for real once the PR is recorded. It is idempotent. `QA_REPO_DIR`
21
+ # picks the checkout `git` runs in, so a spec can use a throwaway repo and a fake `gh`.
21
22
  class QaOpenPr
22
23
  EXIT_OK = 0
23
24
 
@@ -147,7 +148,10 @@ module Hecks
147
148
  "reads as picked up before something is built from it)"
148
149
  when "the branch is one this practice recognises as its own"
149
150
  " (the branch is #{@branch.inspect}; a PR this practice opens is on a branch it can recognise " \
150
- "as its own, QualityControlDials::BRANCH_PREFIX)"
151
+ "as its own)"
152
+ when "the day's cap is not spent"
153
+ " (QualityControlDials::PR_CAP_PER_DAY is #{dial(:PR_CAP_PER_DAY, 0)} — leave this as an open " \
154
+ "Bug/Angle and open it tomorrow, or raise the dial in qa/settings.yml)"
151
155
  else ""
152
156
  end
153
157
  end
@@ -184,15 +188,23 @@ module Hecks
184
188
  check_ancestry
185
189
  end
186
190
 
191
+ # The record of the UTC day the clock reads: `DailyQuota.Today` fills the day from the clock.
192
+ def todays_quota = query("DailyQuota.Today").first
193
+
194
+ # A day with no record has spent nothing, so only an opened day can refuse.
187
195
  def check_daily_cap
188
196
  cap = dial(:PR_CAP_PER_DAY, 0)
189
- return unless cap.positive?
197
+ quota = todays_quota
198
+ return unless cap.positive? && quota
199
+
200
+ refuse_unless_ledger_takes("QualityControl::DailyQuota.Take", today: quota[:today], cap: { value: cap })
201
+ end
190
202
 
191
- today = Time.now
192
- midnight = Time.new(today.year, today.month, today.day).to_i
193
- opened_today = query("Patch.OpenedSince", since: { value: midnight }).size +
194
- query("Improvement.OpenedSince", since: { value: midnight }).size
195
- refusing { @git_pr.assert_under_daily_cap!(opened_today: opened_today, cap: cap) }
203
+ # Counts the PR just recorded against the day, opening the day's record on its first.
204
+ def take_daily_slot
205
+ row = todays_quota
206
+ quota = row ? ::QualityControl::DailyQuota.find(row[:today][:value]) : ::QualityControl::DailyQuota.open!
207
+ quota.take!(cap: { value: dial(:PR_CAP_PER_DAY, 0) })
196
208
  end
197
209
 
198
210
  def check_ancestry
@@ -271,6 +283,7 @@ module Hecks
271
283
  branch: { value: view[:headRefName] }, commit: { value: sha }, title: { value: view[:title] },
272
284
  now: { value: now }
273
285
  )
286
+ take_daily_slot
274
287
  puts "recorded Patch ##{number} for #{@bug.id} at #{sha[0, 7]}"
275
288
  end
276
289
 
@@ -282,12 +295,15 @@ module Hecks
282
295
  end
283
296
 
284
297
  # A run that stopped between `open!` and `land!` leaves the record `opened`: finish it.
285
- existing || ::QualityControl::Improvement.open!(
286
- **(@angle ? { angle: @angle.id } : {}),
287
- number: { value: number }, url: { value: view[:url] },
288
- branch: { value: view[:headRefName] }, title: { value: view[:title] },
289
- now: { value: now }
290
- )
298
+ unless existing
299
+ ::QualityControl::Improvement.open!(
300
+ **(@angle ? { angle: @angle.id } : {}),
301
+ number: { value: number }, url: { value: view[:url] },
302
+ branch: { value: view[:headRefName] }, title: { value: view[:title] },
303
+ now: { value: now }
304
+ )
305
+ take_daily_slot
306
+ end
291
307
  ::QualityControl::Improvement.find(number).land!(number: { value: number }, commit: { value: sha })
292
308
  puts "recorded Improvement ##{number}#{" for #{@angle.id}" if @angle}, landed at #{sha[0, 7]}"
293
309
  end
@@ -54,13 +54,13 @@ module Hecks
54
54
  "cannot see, because there is no second engine to disagree with. " \
55
55
  "spec/rust_conformance_fuzz_spec.rb's own DOMAINS list wires up only pizzas and " \
56
56
  "banking for CI-gated differential fuzzing. rust/Cargo.toml already declares " \
57
- "compiled-binary features for compliance and roster (plus embryonaut and meta) that " \
57
+ "compiled-binary features for compliance and roster (plus meta) that " \
58
58
  "sit unused for this purpose; examples/chess and examples/directory have no Cargo " \
59
59
  "feature at all — `hecks project_rust` has never regenerated a binary for either. " \
60
60
  "Widening either list would let differential fuzzing catch a whole bug class " \
61
61
  "Ruby-only fuzzing cannot, on domains the practice already owns.",
62
62
  citation: "BUG#1/#3/#4 (engine-divergence bugs); spec/rust_conformance_fuzz_spec.rb DOMAINS " \
63
- "(pizzas/banking only); rust/Cargo.toml [features] (compliance/roster/embryonaut/" \
63
+ "(pizzas/banking only); rust/Cargo.toml [features] (compliance/roster/" \
64
64
  "meta have binaries and are not wired in; chess/directory have no feature at all)",
65
65
  proposer: "Claude QA"
66
66
  },
@@ -12,8 +12,7 @@ module Hecks
12
12
  # rotation `Target`, printing each one's inferred capabilities. It is idempotent: references
13
13
  # already on file are skipped whatever their status (re-`Identify` refuses).
14
14
  #
15
- # It skips `examples/embryonaut` (an external product) and `lib/hecks/framework` (no standalone
16
- # Rust binary) on purpose.
15
+ # It skips `lib/hecks/framework` (no standalone Rust binary) on purpose.
17
16
  class QaSeedTargets
18
17
  # Identifies the targets the ledger lacks.
19
18
  #