hecks 3.2.1 → 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 +1 -1
- 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/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_scope.rb +2 -1
- 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/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/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/site_toolchain.rb +67 -0
- 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 +213 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
- data/lib/hecks/language/oidc.json +5 -0
- 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 +17 -4
- 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/smoke-after-deploy.sh.tmpl +271 -0
- data/lib/hecks/projections/deploy/box.rb +15 -2
- 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/live_edge.rb +162 -0
- 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 +40 -18
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
- data/lib/hecks/rust_build/project_rust.rb +3 -10
- data/lib/hecks/rust_build.rb +1 -0
- data/lib/hecks/site/bluebook/site.bluebook +53 -1
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
- data/lib/hecks/tools/gate.rb +1 -1
- data/lib/hecks/tools/rust_comment_style.rb +1 -3
- 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 +151 -0
- data/qa/settings.yml +1 -2
- data/rust/build/src/lineage_pass.rs +30 -15
- data/rust/build/src/main.rs +4 -2
- data/rust/build/src/pipeline.rs +8 -19
- data/rust/codegen/src/bridging.rs +7 -7
- data/rust/codegen/src/json.rs +214 -23
- data/rust/codegen/src/literal.rs +6 -3
- data/rust/codegen/src/main.rs +20 -9
- data/rust/codegen/src/naming.rs +4 -2
- data/rust/codegen/src/optional_pass.rs +226 -0
- data/rust/{build → codegen}/src/sidecars.rs +9 -5
- 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 +99 -2
- 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/kernel/cli.rs +16 -6
- data/rust/src/kernel/needs.rs +104 -9
- metadata +22 -8
- 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
|
@@ -1,44 +1,26 @@
|
|
|
1
1
|
require "yaml"
|
|
2
|
+
require "hecks/vocabulary"
|
|
2
3
|
|
|
3
4
|
module Hecks
|
|
4
5
|
module Fuzzing
|
|
5
6
|
# Loads and validates `qa/settings.yml`, the hecks_qa dials.
|
|
6
7
|
#
|
|
7
8
|
# Fails loud: a missing key, unknown key or wrong-typed value raises at load time.
|
|
8
|
-
#
|
|
9
|
+
# What each dial is and takes is declared as a `QaDial` row in the Vocabulary chapter.
|
|
9
10
|
class QaSettings
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
widening_tiers: Array,
|
|
16
|
-
sweep_max_parallel: Integer,
|
|
17
|
-
liveness_fallback_seconds: Integer,
|
|
18
|
-
draft_only: [TrueClass, FalseClass],
|
|
19
|
-
auto_merge: [TrueClass, FalseClass],
|
|
20
|
-
branch_prefix: String,
|
|
21
|
-
adversarial_fraction: Numeric,
|
|
22
|
-
guided_generation: [TrueClass, FalseClass],
|
|
23
|
-
corpus_splice_probability: Numeric,
|
|
24
|
-
favor_rare_verbs: Integer,
|
|
25
|
-
self_consistency_checks: [TrueClass, FalseClass],
|
|
26
|
-
shrink_budget: Integer,
|
|
27
|
-
yield_weight_seconds: Integer,
|
|
28
|
-
yield_decay_percent: Integer,
|
|
29
|
-
rotation_stale_floor_seconds: Integer,
|
|
30
|
-
persistence_parity_seed_cap: Integer,
|
|
31
|
-
concurrency_seed_cap: Integer,
|
|
32
|
-
adapter_parity_pairs: Hash,
|
|
33
|
-
modes: Hash,
|
|
34
|
-
role_draw_probability: Numeric,
|
|
35
|
-
dry_run_fraction: Numeric,
|
|
36
|
-
generated_domains_per_tick: Integer,
|
|
37
|
-
generated_domains_rust: [TrueClass, FalseClass],
|
|
38
|
-
generated_domain_seeds: Integer,
|
|
39
|
-
structural_refusal_boundary: Array
|
|
11
|
+
# The classes a value of each `type` of a `QaDial` row must be an instance of. Booleans list
|
|
12
|
+
# both TrueClass and FalseClass; Numeric admits `0` as well as `0.0`.
|
|
13
|
+
TYPE_CLASSES = {
|
|
14
|
+
"Integer" => [Integer], "Numeric" => [Numeric], "Boolean" => [TrueClass, FalseClass],
|
|
15
|
+
"String" => [String], "Array" => [Array], "Hash" => [Hash]
|
|
40
16
|
}.freeze
|
|
41
17
|
|
|
18
|
+
# Accessor name to the classes a valid value must be an instance of, one entry per `QaDial`
|
|
19
|
+
# row of the Vocabulary chapter, where each dial is declared with its type and its meaning.
|
|
20
|
+
EXPECTED_TYPES = Hecks::Vocabulary.rows("QaDial").to_h do |dial|
|
|
21
|
+
[dial.fetch("name").to_sym, TYPE_CLASSES.fetch(dial.fetch("type"))]
|
|
22
|
+
end.freeze
|
|
23
|
+
|
|
42
24
|
attr_reader(*EXPECTED_TYPES.keys)
|
|
43
25
|
|
|
44
26
|
# The one real file, resolved from this file's `__dir__` rather than the bluebook's: the
|
|
@@ -68,8 +50,7 @@ module Hecks
|
|
|
68
50
|
end
|
|
69
51
|
end
|
|
70
52
|
|
|
71
|
-
# @param raw [Hash] parsed YAML settings keyed by symbol, one entry per
|
|
72
|
-
# `EXPECTED_TYPES`
|
|
53
|
+
# @param raw [Hash] parsed YAML settings keyed by symbol, one entry per `QaDial` row
|
|
73
54
|
# @param path [String] path to the settings file, used only in error messages
|
|
74
55
|
# @raise [ArgumentError] if `raw` is missing a required key, declares an unknown
|
|
75
56
|
# key, or gives a value the wrong type for its dial
|
|
@@ -81,7 +62,7 @@ module Hecks
|
|
|
81
62
|
if extra.any?
|
|
82
63
|
raise ArgumentError,
|
|
83
64
|
"#{path} declares unknown key(s) #{extra.sort.join(', ')} — " \
|
|
84
|
-
"
|
|
65
|
+
"no QaDial row of the Vocabulary chapter declares them"
|
|
85
66
|
end
|
|
86
67
|
|
|
87
68
|
EXPECTED_TYPES.each do |key, expected|
|
data/lib/hecks/gate/stages.yml
CHANGED
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
#
|
|
10
10
|
# `env` is set only where the caller has not set it: a push check has no use for the Hecks
|
|
11
11
|
# domain's database, so it runs on Memory, as a pull-request run does in CI.
|
|
12
|
+
#
|
|
13
|
+
# A check a second stage repeats is written once, under an anchor (`&name`), and aliased
|
|
14
|
+
# (`*name`) where the other stage takes it, so the two stages cannot drift apart.
|
|
15
|
+
#
|
|
16
|
+
# The `ci` stage is the checks the CI workflow `ci-checks.yml` holds. Each of its steps runs one
|
|
17
|
+
# job's checks with `hecks gate_run.gate stage=ci only=<ids> --wait`, so the commands live here and
|
|
18
|
+
# the workflow keeps only which job runs which ids and what each step is called;
|
|
19
|
+
# spec/gate_ci_stage_spec.rb pins that every check is run by exactly one step. Select checks
|
|
20
|
+
# with `only=`: several of them write generated files and compare, so the stage is not meant to
|
|
21
|
+
# run whole in one checkout.
|
|
12
22
|
pre_push:
|
|
13
23
|
env:
|
|
14
24
|
HECKS_ENVIRONMENT: memory
|
|
@@ -17,7 +27,8 @@ pre_push:
|
|
|
17
27
|
title: rspec (parallel)
|
|
18
28
|
run: [bundle, exec, parallel_rspec, spec, -n, "{workers}"]
|
|
19
29
|
blocked: the suite is red.
|
|
20
|
-
-
|
|
30
|
+
- &check_fuzzing
|
|
31
|
+
id: fuzzing
|
|
21
32
|
title: spec/fuzzing, which the parallel run above excludes
|
|
22
33
|
run: [bundle, exec, rspec, spec/fuzzing, --tag, fuzzing]
|
|
23
34
|
blocked: spec/fuzzing is red.
|
|
@@ -25,31 +36,125 @@ pre_push:
|
|
|
25
36
|
title: adapter agreement, one declared query and every engine giving the same answer
|
|
26
37
|
run: [bundle, exec, rspec, spec/adapters/query_agreement_spec.rb, --tag, io]
|
|
27
38
|
blocked: the engines disagree about a declared query; one of them is wrong, and a self-consistent adapter spec cannot tell which.
|
|
28
|
-
-
|
|
39
|
+
- &check_engine_agreement
|
|
40
|
+
id: engine_agreement
|
|
29
41
|
title: engine agreement, every declared comparator on both engines
|
|
30
42
|
run: [bundle, exec, exe/hecks, conformance_run.check_engine_agreement, --wait]
|
|
31
43
|
blocked: a comparator is missing a shared case or a cross-engine spec.
|
|
32
|
-
-
|
|
44
|
+
- &check_model_check
|
|
45
|
+
id: model_check
|
|
33
46
|
title: model check, unreachable states, dead transitions and orphaned handlers
|
|
34
47
|
run: [bundle, exec, exe/hecks, model_check, --wait]
|
|
35
48
|
blocked: the model checker found something.
|
|
36
|
-
-
|
|
49
|
+
- &check_doc_coverage
|
|
50
|
+
id: doc_coverage
|
|
37
51
|
title: docs coverage, words with no running example
|
|
38
52
|
run: [bundle, exec, exe/hecks, conformance_run.measure_doc_coverage, --wait]
|
|
39
53
|
blocked: the language ships something undocumented; write the missing sections and examples.
|
|
40
|
-
-
|
|
54
|
+
- &check_rubocop
|
|
55
|
+
id: rubocop
|
|
41
56
|
title: rubocop, the static style and correctness gate
|
|
42
57
|
run: [bundle, exec, rubocop, -c, .rubocop.yml]
|
|
43
58
|
blocked: rubocop found an offense.
|
|
44
|
-
-
|
|
59
|
+
- &check_codegen_drift
|
|
60
|
+
id: codegen_drift
|
|
45
61
|
title: codegen drift, the committed rust/src/generated/ must match a fresh regeneration
|
|
46
62
|
run: [bundle, exec, exe/hecks, regeneration_run.regenerate_corpus, --check, --wait]
|
|
47
63
|
blocked: regenerating hits a diff, so the committed Rust is stale; run `hecks regenerate_corpus --confirm` and commit the result.
|
|
48
|
-
-
|
|
64
|
+
- &check_comment_style
|
|
65
|
+
id: comment_style
|
|
49
66
|
title: comment style, every comment under lib/hecks against docs/COMMENT_STYLE_GUIDE.md
|
|
50
67
|
run: [bundle, exec, exe/hecks, style_run.check_comments, paths=lib/hecks, --wait]
|
|
51
68
|
blocked: a comment breaks the style guide, such as a line past 100 characters; `hecks fix_comments` rewrites the mechanical ones, and the rest are shortened by hand. CI runs this same check.
|
|
52
|
-
-
|
|
69
|
+
- &check_comment_blocks
|
|
70
|
+
id: comment_blocks
|
|
53
71
|
title: comment blocks, no run of comment lines past ADR 0075's cap
|
|
54
72
|
run: [bundle, exec, exe/hecks, style_run.check_comments, "paths=lib,spec,examples", only=long_block, --wait]
|
|
55
73
|
blocked: a comment block ran past the cap; shorten it.
|
|
74
|
+
|
|
75
|
+
# The checks `.githooks/post-commit` runs after each commit. A red one is reported and never
|
|
76
|
+
# blocks the commit; pre_push runs it again as the backstop.
|
|
77
|
+
post_commit:
|
|
78
|
+
env:
|
|
79
|
+
HECKS_ENVIRONMENT: memory
|
|
80
|
+
checks:
|
|
81
|
+
- *check_fuzzing
|
|
82
|
+
|
|
83
|
+
# The checks `.github/workflows/ci-checks.yml` runs. It sets no `env`: the job's journal choice
|
|
84
|
+
# (the `hecks-environment` action) reaches every check, as it reached each command before.
|
|
85
|
+
ci:
|
|
86
|
+
checks:
|
|
87
|
+
- *check_model_check
|
|
88
|
+
- *check_engine_agreement
|
|
89
|
+
- *check_doc_coverage
|
|
90
|
+
- *check_rubocop
|
|
91
|
+
- *check_comment_style
|
|
92
|
+
- *check_comment_blocks
|
|
93
|
+
- *check_codegen_drift
|
|
94
|
+
- id: lint_deploy_recipes
|
|
95
|
+
title: generated deploy recipes, a command with no checked exit status or no echo
|
|
96
|
+
run: [bundle, exec, exe/hecks, deploy, makefile_check.lint, --wait]
|
|
97
|
+
blocked: a generated deploy Makefile has an unchecked command, or real AWS or database work with no echo telling the person running it what is about to happen.
|
|
98
|
+
- id: fuzz
|
|
99
|
+
title: fuzz, every example domain on the Memory adapter
|
|
100
|
+
run: [bundle, exec, exe/hecks, fuzz_run.fuzz, --wait]
|
|
101
|
+
blocked: a property, an interpreter crash or a determinism check failed on a replayed sequence; the seed is in the output.
|
|
102
|
+
- id: fuzz_sqlite
|
|
103
|
+
title: fuzz on a real on-disk SQLite database
|
|
104
|
+
run: [bundle, exec, exe/hecks, fuzz_run.fuzz, adapter=sqlite, seeds=6, --wait]
|
|
105
|
+
blocked: the SQLite adapter broke a property the Memory adapter held; seeds are 1..N, so the run repeats.
|
|
106
|
+
- id: fuzz_postgres
|
|
107
|
+
title: fuzz on real Postgres, one worker (every ephemeral boot recreates the same scratch schema)
|
|
108
|
+
run: [bundle, exec, exe/hecks, fuzz_run.fuzz, adapter=postgres, seeds=3, steps=20, --wait]
|
|
109
|
+
blocked: the Postgres adapter broke a property the Memory adapter held; it needs PGHOST, PGUSER and PGPASSWORD.
|
|
110
|
+
- id: vocabulary_drift
|
|
111
|
+
title: vocabulary, the reserved-name tables regenerated must match the committed ones
|
|
112
|
+
run:
|
|
113
|
+
- sh
|
|
114
|
+
- -ec
|
|
115
|
+
- |
|
|
116
|
+
bundle exec exe/hecks language_run.project_vocabulary --confirm --wait
|
|
117
|
+
bundle exec exe/hecks language_run.project_reserved_names --confirm --wait
|
|
118
|
+
git diff --exit-code -- lib/hecks/vocabulary.rb rust/codegen/src/reserved_names.rs rust/build/src/reserved_names.rs
|
|
119
|
+
blocked: a generated vocabulary file is stale; the check rewrote it, so commit the result.
|
|
120
|
+
- id: ci_gates
|
|
121
|
+
title: ci gates, the CiGate rows must match the marked regions of the workflows
|
|
122
|
+
run: [bundle, exec, exe/hecks, regeneration_run.project_ci_gates, --wait]
|
|
123
|
+
blocked: a CiGate row changed with no projection, or a region was edited by hand; run `hecks regeneration_run.project_ci_gates --confirm`.
|
|
124
|
+
- id: tools_doc
|
|
125
|
+
title: tools doc, docs/tools.md must match the RetiredScript rows
|
|
126
|
+
run: [bundle, exec, exe/hecks, regeneration_run.project_tools_doc, --wait]
|
|
127
|
+
blocked: a command's arguments changed with no regenerated doc; run `hecks regeneration_run.project_tools_doc --confirm`.
|
|
128
|
+
- id: rust_coverage
|
|
129
|
+
title: rust coverage, every generated module
|
|
130
|
+
run: [bundle, exec, exe/hecks, query, corpus_run.corpus_rust_coverage, --wait]
|
|
131
|
+
blocked: a construct a domain declares has no generated Rust, or an entry on RUST_COVERAGE_PENDING is now fixed and must be deleted.
|
|
132
|
+
- id: rust_coverage_codegen
|
|
133
|
+
title: rust coverage against hecks-codegen's own manifest, pizzas governance identity banking meta
|
|
134
|
+
run:
|
|
135
|
+
- sh
|
|
136
|
+
- -ec
|
|
137
|
+
- |
|
|
138
|
+
for module in pizzas governance identity banking meta; do
|
|
139
|
+
bundle exec exe/hecks query build.rust_coverage module_name=$module codegen=rust --wait
|
|
140
|
+
done
|
|
141
|
+
blocked: hecks-codegen generated nothing for a construct a domain declares.
|
|
142
|
+
- id: kernel_coverage
|
|
143
|
+
title: rust kernel coverage, every capability variant has a kernel file
|
|
144
|
+
run: [bundle, exec, exe/hecks, kernel_run.measure_kernel_coverage, --wait]
|
|
145
|
+
blocked: a capability's enum variant has no rust/src/kernel file behind it.
|
|
146
|
+
- id: rust_vocabulary_drift
|
|
147
|
+
title: rust vocabulary, the kernel vocab tables regenerated must match the committed ones
|
|
148
|
+
run:
|
|
149
|
+
- sh
|
|
150
|
+
- -ec
|
|
151
|
+
- |
|
|
152
|
+
bundle exec exe/hecks language_run.project_rust_vocabulary --confirm --wait
|
|
153
|
+
git diff --exit-code -- rust/src/kernel/vocab/
|
|
154
|
+
untracked="$(git status --porcelain --untracked-files=all -- rust/src/kernel/vocab/)"
|
|
155
|
+
if [ -n "$untracked" ]; then
|
|
156
|
+
echo "rust/src/kernel/vocab has uncommitted generated files:" >&2
|
|
157
|
+
echo "$untracked" >&2
|
|
158
|
+
exit 1
|
|
159
|
+
fi
|
|
160
|
+
blocked: the generated kernel vocab tables are stale or untracked; the check rewrote them, so commit the result.
|
|
@@ -16,7 +16,7 @@ module Hecks
|
|
|
16
16
|
# files reports drift and writes only when it is confirmed.
|
|
17
17
|
module Regeneration
|
|
18
18
|
# Every operation this family carries out.
|
|
19
|
-
OPERATIONS = %w[regenerate_corpus project_ci_gates decide_ci_gate].freeze
|
|
19
|
+
OPERATIONS = %w[regenerate_corpus project_ci_gates project_tools_doc decide_ci_gate].freeze
|
|
20
20
|
|
|
21
21
|
# How many lines of a script's output a refusal keeps.
|
|
22
22
|
KEPT_LINES = 60
|
|
@@ -37,6 +37,7 @@ module Hecks
|
|
|
37
37
|
check = flag.call(:check) || !flag.call(:confirm)
|
|
38
38
|
child = RubyChild.new(tree)
|
|
39
39
|
return project_ci_gates(child, check) if operation == "project_ci_gates"
|
|
40
|
+
return project_tools_doc(child, check) if operation == "project_tools_doc"
|
|
40
41
|
return decide_ci_gate(child, held) if operation == "decide_ci_gate"
|
|
41
42
|
|
|
42
43
|
result = child.capture("regen_codegen_domains", *("--check" if check))
|
|
@@ -53,6 +54,14 @@ module Hecks
|
|
|
53
54
|
child.answer("project_ci_gates", *("--check" if check))
|
|
54
55
|
end
|
|
55
56
|
|
|
57
|
+
# @param child [RubyChild] the checkout's tool runner
|
|
58
|
+
# @param check [Boolean] whether to only compare
|
|
59
|
+
# @return [String] what the tool printed
|
|
60
|
+
# @raise [ConsoleCapture::Failure] with the stale sections, when a check finds drift
|
|
61
|
+
def project_tools_doc(child, check)
|
|
62
|
+
child.answer("project_tools_doc", *("--check" if check))
|
|
63
|
+
end
|
|
64
|
+
|
|
56
65
|
# @param child [RubyChild] the checkout's tool runner
|
|
57
66
|
# @param held [Hash] the record's fields: `gate`, a `CiGate` row's name
|
|
58
67
|
# @return [String] `touched=true` or `touched=false`
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../doors/json_door"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
# Gives a booted domain its first administrator through the chapter that provides
|
|
8
|
+
# `"membership"`, the way a project's hand-written bootstrap script does.
|
|
9
|
+
#
|
|
10
|
+
# The chapter names its own verbs (`provides "membership", admit:, grant:, people:`), so no
|
|
11
|
+
# aggregate is recognised by name. Both dispatches run as a caller that asserts the role the
|
|
12
|
+
# command is gated to and binds no `actor_id`: Governance checks an identified caller against
|
|
13
|
+
# its role assignments, which a domain with no administrator cannot have yet, and checks an
|
|
14
|
+
# unidentified one by comparing the role named (ADR 0025). That is the bootstrap step, and it is
|
|
15
|
+
# refused once anyone holds an administrator role, so it cannot add a second.
|
|
16
|
+
class FirstAdmin
|
|
17
|
+
# The roles that count as an administrator besides the one the grant command is gated to.
|
|
18
|
+
OWNER_ROLES = %w[Owner].freeze
|
|
19
|
+
|
|
20
|
+
# What a bootstrap did.
|
|
21
|
+
#
|
|
22
|
+
# @!attribute [r] email
|
|
23
|
+
# @return [String] who was granted access
|
|
24
|
+
# @!attribute [r] role
|
|
25
|
+
# @return [String] the role they now hold
|
|
26
|
+
# @!attribute [r] admitted
|
|
27
|
+
# @return [Boolean] whether the person had to be admitted first
|
|
28
|
+
Result = Struct.new(:email, :role, :admitted, keyword_init: true) do
|
|
29
|
+
# @return [String] the sentence the verb answers with
|
|
30
|
+
def to_s = "Granted #{role} access to #{email}#{' (admitted first)' if admitted}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param runtime [Hecks::Runtime] the booted domain
|
|
34
|
+
def initialize(runtime)
|
|
35
|
+
@runtime = runtime
|
|
36
|
+
@registry = runtime.registry
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Admits the person if they are not yet, then grants them a role.
|
|
40
|
+
#
|
|
41
|
+
# @param email [String] the person's email, the membership aggregate's identity
|
|
42
|
+
# @param name [String, nil] their name; the part of the email before the `@` when nil
|
|
43
|
+
# @param role [String, nil] the role to grant; the one the grant command is gated to when nil
|
|
44
|
+
# @return [Result] what was done
|
|
45
|
+
# @raise [Runtime::NotFound] when the domain has no membership chapter, the email is not an
|
|
46
|
+
# email, or an administrator already exists
|
|
47
|
+
def call(email:, name: nil, role: nil)
|
|
48
|
+
raise Runtime::NotFound, "#{email.inspect} is not an email address" unless email.to_s.match?(/\A[^\s@]+@[^\s@]+\z/)
|
|
49
|
+
|
|
50
|
+
provider = membership_provider
|
|
51
|
+
verbs = verbs_of(provider)
|
|
52
|
+
gate = gated_role(provider, verbs.fetch(:grant))
|
|
53
|
+
people = rows(verbs.fetch(:people))
|
|
54
|
+
refuse_if_administered!(people, [gate, *OWNER_ROLES].uniq)
|
|
55
|
+
|
|
56
|
+
admitted = people.none? { |person| person.dig(:email, :value) == email }
|
|
57
|
+
admit(provider, verbs.fetch(:admit), email, name || email[/\A[^@]+/]) if admitted
|
|
58
|
+
grant(provider, verbs.fetch(:grant), email, role || gate)
|
|
59
|
+
Result.new(email: email, role: role || gate, admitted: admitted)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def membership_provider
|
|
65
|
+
domain = @registry.bluebooks.values.first&.name
|
|
66
|
+
@registry.membership_provider_for(domain) or
|
|
67
|
+
raise Runtime::NotFound, "this domain attaches no chapter that provides \"membership\""
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def verbs_of(provider)
|
|
71
|
+
%i[admit grant people].to_h { |key| [key, provider.provided_verb(Bluebook::Capabilities::MEMBERSHIP, key)] }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def rows(verb) = @runtime.query(verb).map { |row| Doors::JsonDoor.materialize(row) }
|
|
75
|
+
|
|
76
|
+
# The role a verb's command declares, which the dispatch must assert to be let through.
|
|
77
|
+
def gated_role(provider, verb)
|
|
78
|
+
aggregate, command = verb.split("::").last.split(".")
|
|
79
|
+
found = provider.aggregates.find { |candidate| candidate.hecks_name == aggregate }
|
|
80
|
+
&.commands&.find { |candidate| candidate.hecks_name == command }
|
|
81
|
+
found&.role&.to_s or raise Runtime::NotFound, "#{verb} declares no role to bootstrap as"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def refuse_if_administered!(people, roles)
|
|
85
|
+
held = people.select { |person| roles.map(&:downcase).include?(person.dig(:role, :value).to_s.downcase) }
|
|
86
|
+
return if held.empty?
|
|
87
|
+
|
|
88
|
+
who = held.map { |person| person.dig(:email, :value) }.join(", ")
|
|
89
|
+
raise Runtime::NotFound, "an administrator already exists (#{who}); grant roles through the " \
|
|
90
|
+
"domain's own #{roles.first} access instead"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def admit(provider, verb, email, name)
|
|
94
|
+
as(provider, verb) do
|
|
95
|
+
@runtime.dispatch(verb, with: { email: { value: email }, name: { value: name } })
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def grant(provider, verb, email, role)
|
|
100
|
+
as(provider, verb) { @runtime.dispatch(verb, to: email, with: { role: { value: role } }) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def as(provider, verb, &)
|
|
104
|
+
Hecks.as_caller(role: gated_role(provider, verb), &)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -60,6 +60,55 @@ module Hecks
|
|
|
60
60
|
{ report: { value: out.string } }
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
# Checks a project's vendored packages against their `bluebook.lock` files and describes
|
|
64
|
+
# them as the manifest a client image carries, through `Hecks::EmbryonautBluebook::Manifest`.
|
|
65
|
+
#
|
|
66
|
+
# The manifest records the project's own commit and whether its tree has uncommitted changes,
|
|
67
|
+
# read from the repository the project stands in (`unknown` outside one).
|
|
68
|
+
#
|
|
69
|
+
# @param held [Hash] the `Package` query's arguments: `root` (the project; the current
|
|
70
|
+
# directory when absent)
|
|
71
|
+
# @return [Hash{Symbol => String}] `text:` the manifest as JSON
|
|
72
|
+
# @raise [Runtime::NotFound] naming each package whose files disagree with its lock
|
|
73
|
+
def verify(**held)
|
|
74
|
+
root = File.expand_path(plain(held[:root]) || Dir.pwd)
|
|
75
|
+
raise Runtime::NotFound, "#{root} is not a directory" unless File.directory?(root)
|
|
76
|
+
|
|
77
|
+
{ text: EmbryonautBluebook::Manifest.new(root, built_from: built_from(root)).to_json_text }
|
|
78
|
+
rescue EmbryonautBluebook::Manifest::Mismatch => e
|
|
79
|
+
raise Runtime::NotFound, e.message
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Judges every package of a bluebook registry against its latest release, through
|
|
83
|
+
# `Hecks::EmbryonautBluebook::Registry#check`.
|
|
84
|
+
#
|
|
85
|
+
# @param held [Hash] the `Registry` query's arguments: `root` (the registry; the current
|
|
86
|
+
# directory when absent)
|
|
87
|
+
# @return [Hash{Symbol => String}] `text:` the notes and `versions ok`
|
|
88
|
+
# @raise [Runtime::NotFound] naming each package at fault, or when `root` is no repository
|
|
89
|
+
def check(**held)
|
|
90
|
+
report = registry(held).check
|
|
91
|
+
raise Runtime::NotFound, report.to_s unless report.ok?
|
|
92
|
+
|
|
93
|
+
{ text: report.to_s }
|
|
94
|
+
rescue Vendoring::Error => e
|
|
95
|
+
raise Runtime::NotFound, e.message
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Tags a package of a bluebook registry at the version its `bluebook.yml` carries, through
|
|
99
|
+
# `Hecks::EmbryonautBluebook::Registry#release`. The tag is made in the local repository and
|
|
100
|
+
# never pushed.
|
|
101
|
+
#
|
|
102
|
+
# @param held [Hash] the `Registry` record: `package`, and `root` (the current directory when
|
|
103
|
+
# absent)
|
|
104
|
+
# @return [Hash{Symbol => Hash}] `report:` the tag made and the command that publishes it
|
|
105
|
+
# @raise [ConsoleCapture::Failure] when the package breaks a release rule
|
|
106
|
+
def tag(**held)
|
|
107
|
+
{ report: { value: registry(held).release(plain(held[:package])).to_s } }
|
|
108
|
+
rescue Vendoring::Error => e
|
|
109
|
+
raise ConsoleCapture::Failure, e.message
|
|
110
|
+
end
|
|
111
|
+
|
|
63
112
|
# Who the repository at a directory commits as: its configured name and email.
|
|
64
113
|
#
|
|
65
114
|
# @param chdir [String, nil] a directory inside the repository; the current one when nil
|
|
@@ -87,6 +136,16 @@ module Hecks
|
|
|
87
136
|
|
|
88
137
|
private
|
|
89
138
|
|
|
139
|
+
def registry(held)
|
|
140
|
+
EmbryonautBluebook::Registry.new(File.expand_path(plain(held[:root]) || Dir.pwd))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def built_from(root)
|
|
144
|
+
head = capture("rev-parse", "HEAD", chdir: root)
|
|
145
|
+
status = capture("status", "--porcelain", "--", ".", chdir: root)
|
|
146
|
+
{ "commit" => head.ok? ? head.out.strip : "unknown", "dirty" => status.ok? && !status.out.strip.empty? }
|
|
147
|
+
end
|
|
148
|
+
|
|
90
149
|
def plain(argument) = argument.is_a?(Hash) ? argument[:value] : argument
|
|
91
150
|
|
|
92
151
|
# A source that is not a repository is worded here; the vendoring's own refusal for it is a
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "first_admin"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
# What the `DomainRuntime` port's adapter does when an `Operation` asks it to give a domain its
|
|
8
|
+
# first administrator.
|
|
9
|
+
module InProcessAccess
|
|
10
|
+
# Boots the domain and grants the first administrator through its membership chapter.
|
|
11
|
+
#
|
|
12
|
+
# @param held [Hash] the `Operation` record: `subject` (the domain), `email`, and optionally
|
|
13
|
+
# `name` and `role`
|
|
14
|
+
# @return [Hash{Symbol => Hash}] `output:` the sentence saying who now holds which role
|
|
15
|
+
# @raise [Runtime::NotFound] if the domain cannot be found or has no membership chapter, or an
|
|
16
|
+
# administrator already exists
|
|
17
|
+
def bootstrap(**held)
|
|
18
|
+
result = FirstAdmin.new(boot(held[:subject])).call(
|
|
19
|
+
email: plain(held[:email]), name: plain(held[:name]), role: plain(held[:role])
|
|
20
|
+
)
|
|
21
|
+
output(result.to_s)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -9,6 +9,7 @@ require_relative "../../cli/stores"
|
|
|
9
9
|
require_relative "../../cli/shape"
|
|
10
10
|
require_relative "../../cli/history"
|
|
11
11
|
require_relative "in_process_operations"
|
|
12
|
+
require_relative "in_process_access"
|
|
12
13
|
|
|
13
14
|
module Hecks
|
|
14
15
|
module Adapters
|
|
@@ -25,6 +26,7 @@ module Hecks
|
|
|
25
26
|
# a projection refresh, a behaviors run, a smoke test and a bounded follow.
|
|
26
27
|
class InProcessBoot
|
|
27
28
|
include InProcessOperations
|
|
29
|
+
include InProcessAccess
|
|
28
30
|
|
|
29
31
|
# Accepts the arguments every driven adapter is built with and keeps none of them.
|
|
30
32
|
#
|
|
@@ -19,9 +19,9 @@ module Hecks
|
|
|
19
19
|
# minutes, so it starts as a child through `ProcessPool`.
|
|
20
20
|
class RustToolchain
|
|
21
21
|
# The `Hecks::RustBuild` tool each ask runs.
|
|
22
|
-
TOOLS = { generate: "project_rust", wasm: "project_wasm",
|
|
23
|
-
|
|
24
|
-
coverage: "rust_coverage" }.freeze
|
|
22
|
+
TOOLS = { generate: "project_rust", wasm: "project_wasm", host: "project_host",
|
|
23
|
+
browser_wasm: "project_wasm_browser", conform: "rust_conformance",
|
|
24
|
+
replay: "rust_conformance_fuzz", coverage: "rust_coverage" }.freeze
|
|
25
25
|
|
|
26
26
|
# The load path the benchmark child runs with: this checkout's or gem's `lib/`.
|
|
27
27
|
LIB = File.expand_path("../../..", __dir__)
|
|
@@ -73,6 +73,21 @@ module Hecks
|
|
|
73
73
|
child(:wasm, [plain(held[:domain])])
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
# Generates the domain, compiles it to a `wasm32-wasip1` module, builds the host binary from
|
|
77
|
+
# the workspace and stages the three files in a directory an image copies from.
|
|
78
|
+
#
|
|
79
|
+
# @param held [Hash] the `Build` record: `domain`, `target` (the machine's own when absent)
|
|
80
|
+
# and `stage_dir` (`.hecks/host/<target>/` when absent)
|
|
81
|
+
# @return [Hash{Symbol => Hash}] `output:` where the files were staged
|
|
82
|
+
# @raise [ConsoleCapture::Failure] when the toolchain or the target is missing, or a step
|
|
83
|
+
# fails
|
|
84
|
+
def host(**held)
|
|
85
|
+
argv = [plain(held[:domain])]
|
|
86
|
+
argv << "--target=#{plain(held[:target])}" if plain(held[:target])
|
|
87
|
+
argv << "--stage=#{plain(held[:stage_dir])}" if plain(held[:stage_dir])
|
|
88
|
+
child(:host, argv)
|
|
89
|
+
end
|
|
90
|
+
|
|
76
91
|
# Generates the domain, then compiles it to an ES module for a browser through wasm-bindgen.
|
|
77
92
|
#
|
|
78
93
|
# @param held [Hash] the `Build` record: `domain`
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
3
4
|
require_relative "console_capture"
|
|
5
|
+
require_relative "shell"
|
|
4
6
|
require_relative "codebase/tree"
|
|
5
7
|
require_relative "codebase/ruby_child"
|
|
8
|
+
require "hecks/tools"
|
|
9
|
+
require "hecks/tools/site_routes"
|
|
10
|
+
require "hecks/projections/site/live_edge"
|
|
6
11
|
|
|
7
12
|
module Hecks
|
|
8
13
|
module Adapters
|
|
@@ -44,8 +49,70 @@ module Hecks
|
|
|
44
49
|
{ output: { value: result.out } }
|
|
45
50
|
end
|
|
46
51
|
|
|
52
|
+
# Compares the CloudFront behaviours a project's edge generates with a live distribution's.
|
|
53
|
+
# The live configuration is read from a saved `get-distribution-config` answer (`live`) or
|
|
54
|
+
# fetched with that one read-only call (`distribution`, the distribution's id); nothing is
|
|
55
|
+
# changed on either side.
|
|
56
|
+
#
|
|
57
|
+
# @param held [Hash] the `SiteProjection` record: `domain`, and `live` or `distribution`,
|
|
58
|
+
# with `expect_new` (path patterns, comma separated) and `refs` (`!Ref Name=id` words,
|
|
59
|
+
# comma separated) when set
|
|
60
|
+
# @return [Hash{Symbol => Hash}] `output:` the report of a distribution that matches
|
|
61
|
+
# @raise [ConsoleCapture::Failure] when the project has no edge, the live configuration
|
|
62
|
+
# cannot be read, or the two differ; the message is the report
|
|
63
|
+
def compare(**held)
|
|
64
|
+
edge = edge_of(located(:domain, held[:domain]))
|
|
65
|
+
live = live_configuration(held)
|
|
66
|
+
live_edge = Projections::Site::LiveEdge
|
|
67
|
+
comparison = live_edge.new(edge, live, expect_new: words(held[:expect_new]),
|
|
68
|
+
refs: live_edge.refs_from(plain(held[:refs]))).call
|
|
69
|
+
raise ConsoleCapture::Failure, comparison.to_s unless comparison.clean?
|
|
70
|
+
|
|
71
|
+
{ output: { value: comparison.to_s } }
|
|
72
|
+
end
|
|
73
|
+
|
|
47
74
|
private
|
|
48
75
|
|
|
76
|
+
# The project's checked edge, read the way `project_site` reads it.
|
|
77
|
+
def edge_of(root)
|
|
78
|
+
registry = nil
|
|
79
|
+
outcome = ConsoleCapture.capture { registry = Tools::SiteRoutes.registry_for(root) }
|
|
80
|
+
raise ConsoleCapture::Failure, outcome.output.strip unless outcome.ok?
|
|
81
|
+
|
|
82
|
+
site = Projections::Site
|
|
83
|
+
chapter = site::Table.chapter(registry)
|
|
84
|
+
table = site::Table.read(chapter, registry: registry)
|
|
85
|
+
site::Edge.read(chapter, table: table, vocabulary: site::Table.vocabulary(registry)) ||
|
|
86
|
+
raise(ConsoleCapture::Failure, "#{root} declares no Edge rows")
|
|
87
|
+
rescue Projections::Site::Table::Invalid => e
|
|
88
|
+
raise ConsoleCapture::Failure, e.message
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def live_configuration(held)
|
|
92
|
+
file = plain(held[:live])
|
|
93
|
+
id = plain(held[:distribution])
|
|
94
|
+
text = file ? read_saved(file) : fetch(id)
|
|
95
|
+
JSON.parse(text)
|
|
96
|
+
rescue JSON::ParserError => e
|
|
97
|
+
raise ConsoleCapture::Failure, "the live configuration is not JSON: #{e.message.lines.first.strip}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def read_saved(path)
|
|
101
|
+
File.read(File.expand_path(path))
|
|
102
|
+
rescue SystemCallError => e
|
|
103
|
+
raise ConsoleCapture::Failure, "cannot read the live configuration: #{e.message}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The one AWS call: reading a distribution's configuration.
|
|
107
|
+
def fetch(id)
|
|
108
|
+
result = Shell.new.capture("aws", "cloudfront", "get-distribution-config", "--id", id.to_s)
|
|
109
|
+
return result.out if result.ok?
|
|
110
|
+
|
|
111
|
+
raise ConsoleCapture::Failure, "aws cloudfront get-distribution-config failed: #{message_of(result)}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def words(argument) = plain(argument).to_s.split(",").map(&:strip).reject(&:empty?)
|
|
115
|
+
|
|
49
116
|
def message_of(result)
|
|
50
117
|
text = [result.err, result.out].map(&:strip).reject(&:empty?).join("\n")
|
|
51
118
|
text.empty? ? "the tool ended with status #{result.status.exitstatus}" : text
|
|
@@ -85,6 +85,12 @@ Hecks.behaviors "Codebase" do
|
|
|
85
85
|
expect refused: "must match"
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
test "ProjectToolsDoc refuses a run that is not named" do
|
|
89
|
+
tests "ProjectToolsDoc", on: "RegenerationRun"
|
|
90
|
+
input run: { value: "" }
|
|
91
|
+
expect refused: "must match"
|
|
92
|
+
end
|
|
93
|
+
|
|
88
94
|
test "DecideCiGate refuses a run that is not named" do
|
|
89
95
|
tests "DecideCiGate", on: "RegenerationRun"
|
|
90
96
|
input run: { value: "" }, gate: { value: "runtime_changed" }
|
|
@@ -1027,6 +1027,22 @@ Hecks.bluebook "Hecks" do
|
|
|
1027
1027
|
emits ProjectCiGatesRequested
|
|
1028
1028
|
end
|
|
1029
1029
|
|
|
1030
|
+
command "ProjectToolsDoc" do
|
|
1031
|
+
role "Maintainer"
|
|
1032
|
+
goal "Write the launcher forms of docs/tools.md from the RetiredScript rows, or with check only compare them"
|
|
1033
|
+
|
|
1034
|
+
attribute :check, Switch, optional: true
|
|
1035
|
+
attribute :run, RunKey
|
|
1036
|
+
attribute :confirm, Switch, optional: true
|
|
1037
|
+
|
|
1038
|
+
sets :check
|
|
1039
|
+
sets :run
|
|
1040
|
+
sets :confirm
|
|
1041
|
+
sets :operation, to: "project_tools_doc"
|
|
1042
|
+
|
|
1043
|
+
emits ProjectToolsDocRequested
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1030
1046
|
command "DecideCiGate" do
|
|
1031
1047
|
role "Maintainer"
|
|
1032
1048
|
goal "Say whether this run's change touches what a CiGate row guards, as the gated job's detector"
|
|
@@ -1105,6 +1121,11 @@ Hecks.bluebook "Hecks" do
|
|
|
1105
1121
|
trigger RegenerationRun::SourceTree::Examine, with: { run: :run }
|
|
1106
1122
|
end
|
|
1107
1123
|
|
|
1124
|
+
policy "ExamineWhenProjectToolsDocRequested" do
|
|
1125
|
+
on "RegenerationRun.ProjectToolsDocRequested"
|
|
1126
|
+
trigger RegenerationRun::SourceTree::Examine, with: { run: :run }
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1108
1129
|
policy "ExamineWhenDecideCiGateRequested" do
|
|
1109
1130
|
on "RegenerationRun.DecideCiGateRequested"
|
|
1110
1131
|
trigger RegenerationRun::SourceTree::Examine, with: { run: :run }
|