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.
- checksums.yaml +4 -4
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -1
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
- data/lib/hecks/bluebook/query.rb +8 -5
- data/lib/hecks/cli/check_era.rb +3 -39
- data/lib/hecks/cli/interview_draft.rb +86 -31
- data/lib/hecks/cli/interview_session.rb +77 -26
- data/lib/hecks/cli/interview_shape.rb +179 -0
- data/lib/hecks/doors/cli_runner.rb +14 -1
- data/lib/hecks/doors/launcher_options.rb +13 -3
- data/lib/hecks/doors/mcp_door.rb +16 -2
- data/lib/hecks/doors/mcp_door_scope.rb +45 -5
- data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
- data/lib/hecks/doors.rb +1 -1
- data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
- data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
- data/lib/hecks/fuzzing/qa_settings.rb +15 -34
- data/lib/hecks/gate/stages.yml +113 -8
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
- data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
- data/lib/hecks/hecks/adapters/git.rb +59 -0
- data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
- data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
- data/lib/hecks/hecks/adapters/site_toolchain.rb +88 -11
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +217 -0
- data/lib/hecks/hecks/hecks.hecksagon +24 -0
- data/lib/hecks/hecks/hecks.world +8 -3
- data/lib/hecks/hecks/site.world +5 -2
- data/lib/hecks/language/bluebook/query.bluebook +25 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +220 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
- data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
- data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
- data/lib/hecks/projections/deploy/box/settings.rb +40 -6
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
- data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
- data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
- data/lib/hecks/projections/deploy/box.rb +28 -4
- data/lib/hecks/projections/deploy/fargate.rb +3 -3
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
- data/lib/hecks/projections/site/edge/checks.rb +31 -5
- data/lib/hecks/projections/site/edge.rb +25 -11
- data/lib/hecks/projections/site/live_edge.rb +162 -0
- data/lib/hecks/projections/site/routes_ts.rb +50 -27
- data/lib/hecks/projections/site/site_cdn.rb +7 -4
- data/lib/hecks/projections/site/table/checks.rb +53 -7
- data/lib/hecks/projections/site/table/link_builder.rb +64 -0
- data/lib/hecks/projections/site/table/row_builder.rb +21 -6
- data/lib/hecks/projections/site/table.rb +16 -5
- data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
- data/lib/hecks/quality_control/quality_control.bluebook +76 -124
- data/lib/hecks/runtime/interpreting.rb +3 -22
- data/lib/hecks/runtime/needs.rb +48 -0
- data/lib/hecks/runtime/query_interpreter.rb +3 -0
- data/lib/hecks/runtime/registry/verification.rb +3 -3
- data/lib/hecks/rust_build/host.rb +147 -0
- data/lib/hecks/rust_build/kernel_input.rb +51 -17
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
- data/lib/hecks/rust_build/project_rust.rb +7 -140
- data/lib/hecks/rust_build.rb +1 -0
- data/lib/hecks/site/bluebook/site.bluebook +80 -14
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
- data/lib/hecks/storehouse.rb +35 -1
- data/lib/hecks/tools/comment_style.rb +4 -2
- data/lib/hecks/tools/gate.rb +1 -1
- data/lib/hecks/tools/rust_comment_style.rb +2 -4
- data/lib/hecks/tools/site_routes.rb +69 -28
- data/lib/hecks/tools/tools_doc.rb +204 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +152 -1
- data/lib/hecks.rb +1 -0
- data/qa/settings.yml +1 -2
- data/rust/build/src/lineage_pass.rs +31 -16
- data/rust/build/src/main.rs +4 -2
- data/rust/build/src/pipeline.rs +9 -20
- data/rust/codegen/src/attr.rs +1 -1
- data/rust/codegen/src/bridging.rs +8 -8
- data/rust/codegen/src/commands.rs +1 -1
- data/rust/codegen/src/constraints.rs +1 -1
- data/rust/codegen/src/dependency_planning.rs +1 -1
- data/rust/codegen/src/domain_generator.rs +2 -2
- data/rust/codegen/src/exemplar.rs +1 -1
- data/rust/codegen/src/expr_emitter.rs +1 -1
- data/rust/codegen/src/fielded.rs +1 -1
- data/rust/codegen/src/json.rs +214 -23
- data/rust/codegen/src/json_codec.rs +1 -1
- data/rust/codegen/src/literal.rs +6 -3
- data/rust/codegen/src/main.rs +23 -13
- data/rust/codegen/src/mutations.rs +1 -1
- data/rust/codegen/src/naming.rs +5 -3
- data/rust/codegen/src/optional_pass.rs +226 -0
- data/rust/codegen/src/ports.rs +1 -1
- data/rust/codegen/src/queries.rs +3 -3
- data/rust/codegen/src/reactions.rs +1 -1
- data/rust/codegen/src/read_models.rs +2 -2
- data/rust/codegen/src/reference_specs.rs +1 -1
- data/rust/codegen/src/registry.rs +4 -4
- data/rust/codegen/src/shared.rs +1 -1
- data/rust/{build → codegen}/src/sidecars.rs +9 -5
- data/rust/codegen/src/skip_reason.rs +1 -1
- data/rust/codegen/src/types.rs +4 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +5 -0
- data/rust/host/src/needs.rs +193 -29
- data/rust/host/src/resend.rs +4 -4
- data/rust/host/src/ui_schema.rs +1 -1
- data/rust/parser/src/emit.rs +12 -0
- data/rust/parser/src/ir.rs +2 -0
- data/rust/parser/src/keywords.rs +2 -0
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/command.rs +2 -44
- data/rust/parser/src/parse/mod.rs +1 -0
- data/rust/parser/src/parse/needs.rs +60 -0
- data/rust/parser/src/parse/query.rs +3 -0
- data/rust/src/exemplar/commands.rs +1 -1
- data/rust/src/exemplar/constraints.rs +1 -1
- data/rust/src/exemplar/fielded.rs +1 -1
- data/rust/src/exemplar/json.rs +1 -1
- data/rust/src/exemplar/mod.rs +1 -1
- data/rust/src/exemplar/mutations.rs +1 -1
- data/rust/src/exemplar/queries.rs +1 -1
- data/rust/src/exemplar/reactions.rs +1 -1
- data/rust/src/exemplar/read_models.rs +1 -1
- data/rust/src/exemplar/registry.rs +1 -1
- data/rust/src/exemplar/types.rs +1 -1
- data/rust/src/kernel/cli.rs +18 -8
- data/rust/src/kernel/needs.rs +104 -9
- data/rust/src/kernel/read_model.rs +1 -1
- data/rust/src/lib.rs +1 -1
- metadata +24 -30
- data/lib/hecks/rust_build/append_optionals.rb +0 -79
- data/lib/hecks/rust_build/rust_literal.rb +0 -33
- data/lib/hecks/three_zero/forms.yml +0 -127
- data/lib/hecks/three_zero.rb +0 -12
- data/rust/build/src/json.rs +0 -421
- data/rust/build/src/optional_pass.rs +0 -150
- data/rust/project/bridging.rb +0 -257
- data/rust/project/commands.rb +0 -877
- data/rust/project/constraints.rb +0 -86
- data/rust/project/dependency_planning.rb +0 -178
- data/rust/project/domain_generator.rb +0 -983
- data/rust/project/exemplar.rb +0 -148
- data/rust/project/expr_emitter.rb +0 -72
- data/rust/project/fielded.rb +0 -141
- data/rust/project/json_codec.rb +0 -641
- data/rust/project/mutations.rb +0 -695
- data/rust/project/naming.rb +0 -223
- data/rust/project/ports.rb +0 -65
- data/rust/project/queries.rb +0 -493
- data/rust/project/reactions.rb +0 -317
- data/rust/project/read_models.rb +0 -690
- data/rust/project/reference_specs.rb +0 -27
- data/rust/project/registry.rb +0 -396
- data/rust/project/skip_reason.rb +0 -29
- data/rust/project/types.rb +0 -225
- data/rust/project/write_if_changed.rb +0 -7
- data/rust/project.rb +0 -30
- 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
|
|
15
|
-
#
|
|
16
|
-
#
|
|
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,
|
|
9
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
19
|
-
#
|
|
20
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
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/
|
|
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 `
|
|
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
|
#
|