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.
Files changed (111) 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 +1 -1
  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/interview_draft.rb +86 -31
  10. data/lib/hecks/cli/interview_session.rb +77 -26
  11. data/lib/hecks/cli/interview_shape.rb +179 -0
  12. data/lib/hecks/doors/cli_runner.rb +14 -1
  13. data/lib/hecks/doors/launcher_options.rb +13 -3
  14. data/lib/hecks/doors/mcp_door_scope.rb +2 -1
  15. data/lib/hecks/doors.rb +1 -1
  16. data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
  17. data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
  18. data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
  19. data/lib/hecks/embryonaut_bluebook.rb +2 -0
  20. data/lib/hecks/fuzzing/qa_settings.rb +15 -34
  21. data/lib/hecks/gate/stages.yml +113 -8
  22. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
  23. data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
  24. data/lib/hecks/hecks/adapters/git.rb +59 -0
  25. data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
  26. data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
  27. data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
  28. data/lib/hecks/hecks/adapters/site_toolchain.rb +67 -0
  29. data/lib/hecks/hecks/codebase.behaviors +6 -0
  30. data/lib/hecks/hecks/codebase.bluebook +21 -0
  31. data/lib/hecks/hecks/custodian.behaviors +6 -0
  32. data/lib/hecks/hecks/custodian.bluebook +217 -0
  33. data/lib/hecks/hecks/hecks.hecksagon +24 -0
  34. data/lib/hecks/hecks/hecks.world +8 -3
  35. data/lib/hecks/hecks/site.world +5 -2
  36. data/lib/hecks/language/bluebook/query.bluebook +25 -0
  37. data/lib/hecks/language/bluebook/vocabulary.bluebook +213 -0
  38. data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
  39. data/lib/hecks/language/oidc.json +5 -0
  40. data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
  41. data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
  42. data/lib/hecks/projections/deploy/box/settings.rb +17 -4
  43. data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
  44. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
  45. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
  46. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
  47. data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
  48. data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
  49. data/lib/hecks/projections/deploy/box.rb +15 -2
  50. data/lib/hecks/projections/deploy/fargate.rb +3 -3
  51. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  52. data/lib/hecks/projections/model/deviations.rb +1 -1
  53. data/lib/hecks/projections/site/live_edge.rb +162 -0
  54. data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
  55. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
  56. data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
  57. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
  58. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
  59. data/lib/hecks/quality_control/quality_control.bluebook +76 -124
  60. data/lib/hecks/runtime/interpreting.rb +3 -22
  61. data/lib/hecks/runtime/needs.rb +48 -0
  62. data/lib/hecks/runtime/query_interpreter.rb +3 -0
  63. data/lib/hecks/runtime/registry/verification.rb +3 -3
  64. data/lib/hecks/rust_build/host.rb +147 -0
  65. data/lib/hecks/rust_build/kernel_input.rb +40 -18
  66. data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
  67. data/lib/hecks/rust_build/project_rust.rb +3 -10
  68. data/lib/hecks/rust_build.rb +1 -0
  69. data/lib/hecks/site/bluebook/site.bluebook +53 -1
  70. data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
  71. data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
  72. data/lib/hecks/tools/gate.rb +1 -1
  73. data/lib/hecks/tools/rust_comment_style.rb +1 -3
  74. data/lib/hecks/tools/tools_doc.rb +204 -0
  75. data/lib/hecks/tools.rb +1 -0
  76. data/lib/hecks/version.rb +1 -1
  77. data/lib/hecks/vocabulary.rb +151 -0
  78. data/qa/settings.yml +1 -2
  79. data/rust/build/src/lineage_pass.rs +30 -15
  80. data/rust/build/src/main.rs +4 -2
  81. data/rust/build/src/pipeline.rs +8 -19
  82. data/rust/codegen/src/bridging.rs +7 -7
  83. data/rust/codegen/src/json.rs +214 -23
  84. data/rust/codegen/src/literal.rs +6 -3
  85. data/rust/codegen/src/main.rs +20 -9
  86. data/rust/codegen/src/naming.rs +4 -2
  87. data/rust/codegen/src/optional_pass.rs +226 -0
  88. data/rust/{build → codegen}/src/sidecars.rs +9 -5
  89. data/rust/host/HECKS_RELEASE +1 -1
  90. data/rust/host/src/api.rs +1 -1
  91. data/rust/host/src/dispatch.rs +5 -0
  92. data/rust/host/src/needs.rs +99 -2
  93. data/rust/host/src/resend.rs +4 -4
  94. data/rust/host/src/ui_schema.rs +1 -1
  95. data/rust/parser/src/emit.rs +12 -0
  96. data/rust/parser/src/ir.rs +2 -0
  97. data/rust/parser/src/keywords.rs +2 -0
  98. data/rust/parser/src/main.rs +1 -0
  99. data/rust/parser/src/parse/command.rs +2 -44
  100. data/rust/parser/src/parse/mod.rs +1 -0
  101. data/rust/parser/src/parse/needs.rs +60 -0
  102. data/rust/parser/src/parse/query.rs +3 -0
  103. data/rust/src/kernel/cli.rs +16 -6
  104. data/rust/src/kernel/needs.rs +104 -9
  105. metadata +22 -8
  106. data/lib/hecks/rust_build/append_optionals.rb +0 -79
  107. data/lib/hecks/rust_build/rust_literal.rb +0 -33
  108. data/lib/hecks/three_zero/forms.yml +0 -127
  109. data/lib/hecks/three_zero.rb +0 -12
  110. data/rust/build/src/json.rs +0 -421
  111. 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
- # The meaning of each dial is documented on `QualityControlDials` in the bluebook.
9
+ # What each dial is and takes is declared as a `QaDial` row in the Vocabulary chapter.
9
10
  class QaSettings
10
- # Accessor name to the class (or classes) a valid value must be an instance of.
11
- # Booleans list both TrueClass and FalseClass; Numeric admits `0` as well as `0.0`.
12
- EXPECTED_TYPES = {
13
- cadence_seconds: Integer,
14
- pr_cap_per_day: Integer,
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 dial in
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
- "Hecks::Fuzzing::QaSettings::EXPECTED_TYPES doesn't recognise them"
65
+ "no QaDial row of the Vocabulary chapter declares them"
85
66
  end
86
67
 
87
68
  EXPECTED_TYPES.each do |key, expected|
@@ -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
- - id: fuzzing
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
- - id: engine_agreement
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
- - id: model_check
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
- - id: doc_coverage
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
- - id: rubocop
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
- - id: codegen_drift
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
- - id: comment_style
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
- - id: comment_blocks
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", browser_wasm: "project_wasm_browser",
23
- conform: "rust_conformance", replay: "rust_conformance_fuzz",
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 }