hecks 3.0.5 → 3.1.1

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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +24 -12
  3. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  5. data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
  6. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
  7. data/lib/hecks/bluebook/hexagon.rb +45 -25
  8. data/lib/hecks/bluebook/model_check.rb +7 -7
  9. data/lib/hecks/chapters.rb +32 -3
  10. data/lib/hecks/cli/check_era.rb +1 -1
  11. data/lib/hecks/cli/deploy_template_diff.rb +1 -1
  12. data/lib/hecks/cli/doc_coverage.rb +1 -1
  13. data/lib/hecks/cli/domain_stub.rb +128 -0
  14. data/lib/hecks/cli/domain_writer.rb +39 -0
  15. data/lib/hecks/cli/interview_agent.rb +59 -0
  16. data/lib/hecks/cli/interview_draft.rb +246 -0
  17. data/lib/hecks/cli/interview_run.rb +87 -0
  18. data/lib/hecks/cli/interview_session.rb +299 -0
  19. data/lib/hecks/cli/model_check.rb +1 -1
  20. data/lib/hecks/cli/project_cli.rb +51 -29
  21. data/lib/hecks/cli/reference.rb +2 -1
  22. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
  23. data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
  24. data/lib/hecks/cli/spec_example.rb +1 -1
  25. data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
  26. data/lib/hecks/corpus.rb +12 -8
  27. data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
  28. data/lib/hecks/deploy/oidc.json +5 -0
  29. data/lib/hecks/doc/reference.rb +4 -3
  30. data/lib/hecks/doors/cli_door.rb +13 -13
  31. data/lib/hecks/doors/cli_runner.rb +53 -33
  32. data/lib/hecks/doors/launcher_options.rb +5 -5
  33. data/lib/hecks/doors/mcp_door.rb +40 -18
  34. data/lib/hecks/doors/mcp_door_scope.rb +244 -24
  35. data/lib/hecks/doors/usage_cache.rb +123 -0
  36. data/lib/hecks/doors.rb +1 -1
  37. data/lib/hecks/embryonaut_bluebook.rb +2 -2
  38. data/lib/hecks/forms/banking_presentation.rb +1 -1
  39. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
  40. data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
  41. data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
  42. data/lib/hecks/framework.rb +1 -1
  43. data/lib/hecks/fuzzing/combination_miner.rb +1 -1
  44. data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
  45. data/lib/hecks/fuzzing/differential.rb +4 -2
  46. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  47. data/lib/hecks/fuzzing/form_census.rb +2 -2
  48. data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
  49. data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
  50. data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
  51. data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
  52. data/lib/hecks/gate/stages.yml +5 -5
  53. data/lib/hecks/grammar/grammar.hecksagon +2 -2
  54. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
  55. data/lib/hecks/hecks/adapters/finding_github.adapter +7 -0
  56. data/lib/hecks/hecks/adapters/finding_github.rb +114 -0
  57. data/lib/hecks/hecks/adapters/host_http.rb +4 -3
  58. data/lib/hecks/hecks/adapters/local_files.rb +59 -0
  59. data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
  60. data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
  61. data/lib/hecks/hecks/adapters/terminal.rb +15 -0
  62. data/lib/hecks/hecks/codebase.bluebook +4 -3
  63. data/lib/hecks/hecks/context_map.hecksagon +2 -2
  64. data/lib/hecks/hecks/custodian.bluebook +81 -3
  65. data/lib/hecks/hecks/hecks.bluebook +18 -0
  66. data/lib/hecks/hecks/hecks.hecksagon +16 -4
  67. data/lib/hecks/hecks/hecks.world +7 -6
  68. data/lib/hecks/hecks/site.behaviors +17 -0
  69. data/lib/hecks/hecks/site.world +10 -0
  70. data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
  71. data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
  72. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
  73. data/lib/hecks/projections/deploy/box/settings.rb +324 -0
  74. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +268 -0
  75. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +68 -0
  76. data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
  77. data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +181 -0
  78. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +49 -0
  79. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
  80. data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +91 -0
  81. data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +58 -0
  82. data/lib/hecks/projections/deploy/box.rb +553 -0
  83. data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
  84. data/lib/hecks/projections/deploy/fargate.rb +1 -1
  85. data/lib/hecks/projections/deploy/lambda.rb +11 -10
  86. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
  87. data/lib/hecks/projections/diagrams.rb +2 -2
  88. data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
  89. data/lib/hecks/projections/site/edge/checks.rb +137 -0
  90. data/lib/hecks/projections/site/edge/pattern.rb +50 -0
  91. data/lib/hecks/projections/site/edge/rules.rb +128 -0
  92. data/lib/hecks/projections/site/edge.rb +136 -0
  93. data/lib/hecks/projections/site/regions.rb +44 -0
  94. data/lib/hecks/projections/site/routes_ts.rb +284 -0
  95. data/lib/hecks/projections/site/site_cdn.rb +78 -0
  96. data/lib/hecks/projections/site/table/checks.rb +124 -0
  97. data/lib/hecks/projections/site/table/row_builder.rb +155 -0
  98. data/lib/hecks/projections/site/table.rb +98 -0
  99. data/lib/hecks/projections.rb +2 -0
  100. data/lib/hecks/projector/cli_projector.rb +153 -111
  101. data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
  102. data/lib/hecks/quality_control/adapters/agent.rb +2 -2
  103. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
  104. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
  105. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
  106. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
  107. data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
  108. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
  109. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
  110. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
  111. data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
  112. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
  113. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
  114. data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
  115. data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
  116. data/lib/hecks/quality_control/quality_control.bluebook +18 -18
  117. data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
  118. data/lib/hecks/runtime/loader.rb +31 -8
  119. data/lib/hecks/runtime/registry/verification.rb +15 -29
  120. data/lib/hecks/runtime/registry.rb +14 -20
  121. data/lib/hecks/rust_build/append_optionals.rb +79 -0
  122. data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
  123. data/lib/hecks/rust_build/domain_name.rb +26 -0
  124. data/lib/hecks/rust_build/kernel_input.rb +58 -0
  125. data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
  126. data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
  127. data/lib/hecks/rust_build/project_rust.rb +57 -24
  128. data/lib/hecks/rust_build/rust_literal.rb +33 -0
  129. data/lib/hecks/rust_build/write_if_changed.rb +86 -0
  130. data/lib/hecks/site/bluebook/site.bluebook +205 -0
  131. data/lib/hecks/site/bluebook/site.hecksagon +5 -0
  132. data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
  133. data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
  134. data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
  135. data/lib/hecks/storehouse.rb +37 -7
  136. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
  137. data/lib/hecks/three_zero/forms.yml +88 -88
  138. data/lib/hecks/tickets/bluebook/tickets.bluebook +331 -0
  139. data/lib/hecks/tickets/bluebook/tickets.hecksagon +11 -0
  140. data/lib/hecks/tickets/bluebook/tickets.ports.hecksagon +26 -0
  141. data/lib/hecks/tools/ci_gates.rb +1 -1
  142. data/lib/hecks/tools/deploy_recipe.rb +14 -6
  143. data/lib/hecks/tools/oidc_manifests.rb +2 -1
  144. data/lib/hecks/tools/sequence_script.rb +2 -2
  145. data/lib/hecks/tools/site_routes.rb +152 -0
  146. data/lib/hecks/tools/tenant_provisioning.rb +1 -1
  147. data/lib/hecks/tools.rb +1 -0
  148. data/lib/hecks/version.rb +1 -1
  149. data/lib/hecks.rb +1 -0
  150. data/rust/build/src/cargo_sync.rs +1 -1
  151. data/rust/build/src/pipeline.rs +23 -16
  152. data/rust/build/src/resolve.rs +39 -6
  153. data/rust/codegen/src/json_codec.rs +3 -0
  154. data/rust/codegen/src/naming.rs +4 -0
  155. data/rust/codegen/src/types.rs +45 -0
  156. data/rust/host/HECKS_RELEASE +1 -1
  157. data/rust/host/src/api.rs +1 -1
  158. data/rust/host/src/dispatch.rs +87 -7
  159. data/rust/host/src/needs.rs +111 -0
  160. data/rust/parser/src/keywords.rs +7 -6
  161. data/rust/parser/src/main.rs +4 -3
  162. data/rust/parser/src/parse/chapter.rs +1 -1
  163. data/rust/parser/src/parse/hecksagon.rs +25 -10
  164. data/rust/project/mutations.rb +4 -30
  165. data/rust/project/naming.rb +8 -28
  166. data/rust/project/write_if_changed.rb +4 -80
  167. data/rust/project_rust_pipeline.rb +16 -12
  168. data/rust/src/kernel/cli.rs +2 -0
  169. data/rust/src/kernel/needs.rs +74 -1
  170. metadata +51 -2
@@ -2,7 +2,7 @@ require_relative "../naming"
2
2
 
3
3
  module Hecks
4
4
  module Projector
5
- # Projects a bluebook as its own command-line surface: the verb tree, argument spec
5
+ # Projects a bluebook as its own command-line surface: the command tree, argument spec
6
6
  # and usage text. Nothing executes here; the generic runner (`hecks run`) parses against it.
7
7
  #
8
8
  # Argument types come from the declared field types, never from guessing at the
@@ -10,22 +10,22 @@ module Hecks
10
10
  module CliProjector
11
11
  module_function
12
12
 
13
- # Projects the verb and question tables and the usage text. Commands and queries are
13
+ # Projects the command and question tables and the usage text. Commands and queries are
14
14
  # separate namespaces (a chapter may declare both under one name); ask a query with `ask`.
15
15
  #
16
16
  # @param bluebook [Bluebook::Chapter] the booted domain to project
17
17
  # @param options [Hash{Symbol => Object}] `:program` (default `"hecks run"`) for the usage
18
- # text; `:verb` and `:ask` select one verb's `--help` text; `:names` maps a launcher
18
+ # text; `:command` and `:ask` select one command's `--help` text; `:names` maps a launcher
19
19
  # name to the command it stands for (`{ "mcp" => "serve_mcp" }`); `:mint_run_keys` makes a
20
- # creating verb's `run` key optional, since the launcher mints it
21
- # @return [Hash{Symbol => Object}] `:verbs`, `:questions`, `:names` (alias tables) and
20
+ # creating command's `run` key optional, since the launcher mints it
21
+ # @return [Hash{Symbol => Object}] `:commands`, `:questions`, `:names` (alias tables) and
22
22
  # `:usage` (pre-rendered help text)
23
- # @raise [Bluebook::DSL::Malformed] if two verbs project to the same command-line name
23
+ # @raise [Bluebook::DSL::Malformed] if two commands project to the same command-line name
24
24
  def call(bluebook:, options: {})
25
- verbs = {}
25
+ commands = {}
26
26
  questions = {}
27
27
 
28
- bluebook.aggregates.each { |aggregate| claim_aggregate(verbs, questions, bluebook, aggregate) }
28
+ bluebook.aggregates.each { |aggregate| claim_aggregate(commands, questions, bluebook, aggregate) }
29
29
 
30
30
  # A report belongs to the chapter, not an aggregate, so it is addressed
31
31
  # `Chapter.Report` (one dot) where a query is `Chapter::Aggregate.Query`.
@@ -33,59 +33,55 @@ module Hecks
33
33
  claim(questions, Naming.snake(model.hecks_name), report_spec(bluebook, model))
34
34
  end
35
35
 
36
- mint_run_keys(verbs) if options[:mint_run_keys]
36
+ mint_run_keys(commands) if options[:mint_run_keys]
37
37
 
38
38
  # The display name is the shortest unambiguous one; both spellings are accepted.
39
- display_names([verbs, questions], options[:names])
39
+ display_names([commands, questions], options[:names])
40
40
 
41
- { verbs: verbs, questions: questions,
42
- names: { command: aliases(verbs), question: aliases(questions) },
43
- usage: usage(bluebook, verbs, questions, options) }
41
+ { commands: commands, questions: questions,
42
+ names: { command: aliases(commands), question: aliases(questions) },
43
+ usage: usage(bluebook, commands, questions, options) }
44
44
  end
45
45
 
46
- # Claims the verbs and questions of one aggregate and its entities and ports.
46
+ # Claims the commands and questions of one aggregate and its entities and ports.
47
47
  #
48
- # @param verbs [Hash{String => Hash}] the verb map, mutated in place
48
+ # @param commands [Hash{String => Hash}] the command map, mutated in place
49
49
  # @param questions [Hash{String => Hash}] the question map, mutated in place
50
50
  # @param bluebook [Bluebook::Chapter] the booted domain
51
51
  # @param aggregate [Bluebook::Aggregate] the aggregate to claim
52
52
  # @return [void]
53
- def claim_aggregate(verbs, questions, bluebook, aggregate)
54
- aggregate.commands.each { |c| claim(verbs, name_for(aggregate, c), command_spec(bluebook, aggregate, nil, c)) }
53
+ def claim_aggregate(commands, questions, bluebook, aggregate)
54
+ aggregate.commands.each { |c| claim(commands, name_for(aggregate, c), command_spec(bluebook, aggregate, nil, c)) }
55
55
  aggregate.queries.each { |q| claim(questions, name_for(aggregate, q), query_spec(bluebook, aggregate, nil, q)) }
56
56
 
57
57
  aggregate.entities.each do |entity|
58
58
  entity.commands.each do |c|
59
- claim(verbs, name_for(aggregate, c, entity), command_spec(bluebook, aggregate, entity, c))
59
+ claim(commands, name_for(aggregate, c, entity), command_spec(bluebook, aggregate, entity, c))
60
60
  end
61
61
  entity.queries.each do |q|
62
62
  claim(questions, name_for(aggregate, q, entity), query_spec(bluebook, aggregate, entity, q))
63
63
  end
64
64
  end
65
65
 
66
- # Port operations dispatch by the same name as a command, so they are verbs too.
66
+ # Port operations dispatch by the same name as a command, so they are commands too.
67
67
  aggregate.ports.each do |port|
68
- port.operations.each { |o| claim(verbs, name_for(aggregate, o), port_spec(bluebook, aggregate, port, o)) }
68
+ port.operations.each { |o| claim(commands, name_for(aggregate, o), port_spec(bluebook, aggregate, port, o)) }
69
69
  end
70
70
  end
71
71
 
72
- # Sets `:short` on each spec: the last segment when exactly one verb ends in it,
73
- # else the full name (two aggregates both declaring `Close` keep both spellings).
72
+ # Sets `:short` on each spec to its full `aggregate.command` name: a command is always
73
+ # called with its aggregate, so adding a command elsewhere never changes what a call means.
74
74
  #
75
- # @param specs [Hash{String => Hash}] the verb or question map, mutated in place
75
+ # @param specs [Hash{String => Hash}] the command or query map, mutated in place
76
76
  # @return [void]
77
- def shorten(specs)
78
- tails = specs.keys.group_by { |name| name.split(".").last }
79
- specs.each do |name, spec|
80
- tail = name.split(".").last
81
- spec[:short] = tails[tail].length == 1 ? tail : name
82
- end
77
+ def qualify(specs)
78
+ specs.each { |name, spec| spec[:short] = name }
83
79
  end
84
80
 
85
- # Marks the `run` key of each creating verb `minted`: optional, and never filled by a bare
81
+ # Marks the `run` key of each creating command `minted`: optional, and never filled by a bare
86
82
  # word, since the launcher makes one when it is left out.
87
- def mint_run_keys(verbs)
88
- verbs.each_value do |spec|
83
+ def mint_run_keys(commands)
84
+ commands.each_value do |spec|
89
85
  next unless spec[:creates]
90
86
 
91
87
  spec[:arguments] = spec[:arguments].map do |argument|
@@ -96,14 +92,14 @@ module Hecks
96
92
  end
97
93
  end
98
94
 
99
- # Sets the display name of every spec in each map: shortened, then renamed by `names`.
95
+ # Sets the display name of every spec in each map: its full name, then renamed by `names`.
100
96
  #
101
- # @param maps [Array<Hash{String => Hash}>] the verb and question maps, mutated in place
97
+ # @param maps [Array<Hash{String => Hash}>] the command and question maps, mutated in place
102
98
  # @param names [Hash, nil] the chapter's launcher names
103
99
  # @return [void]
104
100
  def display_names(maps, names)
105
101
  maps.each do |specs|
106
- shorten(specs)
102
+ qualify(specs)
107
103
  rename(specs, names)
108
104
  end
109
105
  end
@@ -114,7 +110,7 @@ module Hecks
114
110
  # replaced keeps working (`:short_was`, read by `aliases`). A table entry naming no
115
111
  # command in `specs` belongs to the other namespace and is skipped.
116
112
  #
117
- # @param specs [Hash{String => Hash}] the verb or question map, mutated in place
113
+ # @param specs [Hash{String => Hash}] the command or question map, mutated in place
118
114
  # @param names [Hash{String, Symbol => String, Symbol}, nil] launcher name to command name
119
115
  # @return [void]
120
116
  # @raise [Bluebook::DSL::Malformed] if a launcher name is already another command's name
@@ -131,8 +127,8 @@ module Hecks
131
127
  end
132
128
  end
133
129
 
134
- # Maps every accepted spelling (full name, `:short` and a renamed command's old short
135
- # name) to the full name.
130
+ # Maps every accepted spelling (full name, `:short` and a renamed command's old name)
131
+ # to the full name.
136
132
  def aliases(specs)
137
133
  specs.each_with_object({}) do |(name, spec), map|
138
134
  map[name] = name
@@ -142,34 +138,34 @@ module Hecks
142
138
  end
143
139
 
144
140
  # Stores `spec` under `name`, refusing a second claim rather than silently
145
- # keeping whichever verb was walked first.
141
+ # keeping whichever command was walked first.
146
142
  #
147
- # @raise [Bluebook::DSL::Malformed] if `name` is already claimed by another verb
148
- def claim(verbs, name, spec)
149
- if verbs.key?(name)
143
+ # @raise [Bluebook::DSL::Malformed] if `name` is already claimed by another command
144
+ def claim(commands, name, spec)
145
+ if commands.key?(name)
150
146
  raise Bluebook::DSL::Malformed,
151
- "two verbs project to the command-line name #{name.inspect}: " \
152
- "#{verbs[name][:verb]} and #{spec[:verb]} — rename one"
147
+ "two commands project to the command-line name #{name.inspect}: " \
148
+ "#{commands[name][:command]} and #{spec[:command]} — rename one"
153
149
  end
154
150
 
155
- verbs[name] = spec
151
+ commands[name] = spec
156
152
  end
157
153
 
158
- # The dotted command-line name, `aggregate[.entity].verb`, snake-cased.
159
- def name_for(aggregate, verb, entity = nil)
154
+ # The dotted command-line name, `aggregate[.entity].command`, snake-cased.
155
+ def name_for(aggregate, command, entity = nil)
160
156
  parts = [Naming.snake(aggregate.hecks_name)]
161
157
  parts << Naming.snake(entity.hecks_name) if entity
162
- parts << Naming.snake(verb.hecks_name)
158
+ parts << Naming.snake(command.hecks_name)
163
159
  parts.join(".")
164
160
  end
165
161
 
166
- # The verb's language name, `Chapter::Aggregate[.Entity].Verb`, for help text.
167
- def fqn(bluebook, aggregate, verb, entity = nil)
162
+ # The command's language name, `Chapter::Aggregate[.Entity].Verb`, for help text.
163
+ def fqn(bluebook, aggregate, command, entity = nil)
168
164
  [bluebook.name, "::", aggregate.hecks_name, ".",
169
- entity ? "#{entity.hecks_name}." : "", verb.hecks_name].join
165
+ entity ? "#{entity.hecks_name}." : "", command.hecks_name].join
170
166
  end
171
167
 
172
- # The options that name the record a verb acts on, before its own arguments.
168
+ # The options that name the record a command acts on, before its own arguments.
173
169
  # `nil` (a creating command) takes none: there is no record yet.
174
170
  # Ports always pass `:aggregate`, since a port is declared on an aggregate.
175
171
  def receiver_options(receiver, aggregate, entity)
@@ -204,7 +200,7 @@ module Hecks
204
200
  arguments = receiver_options(receiver, aggregate, entity) + arguments
205
201
  legacy_arguments = receiver == :aggregate ? [{ path: "id", type: "String", required: true }] : []
206
202
 
207
- { verb: fqn(bluebook, aggregate, command, entity), kind: :command,
203
+ { command: fqn(bluebook, aggregate, command, entity), kind: :command,
208
204
  summary: command.goal, role: command.role, role_gated: !command.role.to_s.empty?,
209
205
  group: aggregate.hecks_name, internal: command.role.to_s == "System",
210
206
  creates: command.creates?,
@@ -214,15 +210,15 @@ module Hecks
214
210
  arguments: arguments }
215
211
  end
216
212
 
217
- # A port operation reads as a verb but reports as a boundary: it creates no
213
+ # A port operation reads as a command but reports as a boundary: it creates no
218
214
  # record, and an outbound failure is the other side's sentence, so `refusals` is empty.
219
215
  def port_spec(bluebook, aggregate, port, operation)
220
216
  arguments = receiver_options(:aggregate, aggregate, nil) +
221
217
  operation.attributes.flat_map { |a| options_for(a, aggregate, aggregate) }
222
218
 
223
219
  # `Dispatcher#dispatch` looks the head up as a port, so the wire name is
224
- # `Aggregate.Port.Operation`; the short name (see `shorten`) hides the port.
225
- { verb: [fqn(bluebook, aggregate, operation).sub(/\.[^.]+\z/, ""), port.name, operation.hecks_name].join("."),
220
+ # `Aggregate.Port.Operation`; the launcher name hides the port.
221
+ { command: [fqn(bluebook, aggregate, operation).sub(/\.[^.]+\z/, ""), port.name, operation.hecks_name].join("."),
226
222
  kind: :command, creates: false, receiver: :aggregate, refusals: [],
227
223
  # `role:` is help text only; port dispatch never reaches the role check,
228
224
  # so `role_gated` is always false here.
@@ -254,17 +250,17 @@ module Hecks
254
250
  []
255
251
  end
256
252
 
257
- { verb: "#{bluebook.name}.#{model.hecks_name}", kind: :query,
253
+ { command: "#{bluebook.name}.#{model.hecks_name}", kind: :query,
258
254
  summary: model.description, arguments: arguments }
259
255
  end
260
256
 
261
257
  # A question that only reads the aggregate's own records back: it returns no document and
262
258
  # filters on nothing but the record's identity ("how one request ended") or its lifecycle
263
259
  # status ("every request that was refused"). Each journaled run has such a pair, which a
264
- # person reads through `hecks <verb> --wait` rather than asking for by name, so the help
265
- # sets them apart with the bookkeeping verbs. A query that returns a document, or filters
260
+ # person reads through `hecks <command> --wait` rather than asking for by name, so the help
261
+ # sets them apart with the bookkeeping commands. A query that returns a document, or filters
266
262
  # on anything else, is a real question. Only an aggregate that journals its own runs (it has
267
- # system-role commands, the ones `internal` verbs are made of) has such a pair: a release's
263
+ # system-role commands, the ones `internal` commands are made of) has such a pair: a release's
268
264
  # "every version that was shipped" is a question worth asking by name.
269
265
  def bookkeeping_query?(aggregate, query)
270
266
  return false if query.returns || query.wheres.empty?
@@ -280,7 +276,7 @@ module Hecks
280
276
  attribute ? options_for(attribute, entity || aggregate, aggregate) : []
281
277
  end
282
278
 
283
- { verb: fqn(bluebook, aggregate, query, entity), kind: :query, group: aggregate.hecks_name,
279
+ { command: fqn(bluebook, aggregate, query, entity), kind: :query, group: aggregate.hecks_name,
284
280
  internal: entity.nil? && bookkeeping_query?(aggregate, query),
285
281
  summary: query.description, arguments: arguments, returns: query.returns }
286
282
  end
@@ -301,7 +297,7 @@ module Hecks
301
297
  return [reference_option(attribute)] if attribute.reference?
302
298
 
303
299
  value_object = value_object_for(attribute, holder, aggregate)
304
- return [scalar_option(path, attribute, optional)] unless value_object
300
+ return with_declared_default([scalar_option(path, attribute, optional)], attribute) unless value_object
305
301
 
306
302
  # The list flag rides on each leaf: without it a repeated flag overwrote the
307
303
  # leaf silently, and CliDoor only ever sees a path and a spec.
@@ -312,12 +308,31 @@ module Hecks
312
308
  scalar_option("#{path}.#{field.name}", field, optional || field.optional?,
313
309
  enum: closed_members(value_object, field))
314
310
  end
311
+ fields = with_declared_default(fields, attribute)
315
312
 
316
313
  return fields unless attribute.list?
317
314
 
318
315
  fields.map { |option| option.merge(list: true, note: [option[:note], "repeatable"].compact.join("; ")) }
319
316
  end
320
317
 
318
+ # Shows the default the attribute itself declares (`attribute :runs, Count, default: 30`) on
319
+ # the leaf it fills: the matching field for a hash default, the lone field for a bare one.
320
+ # An argument with a default is never required, since the runtime fills it when omitted.
321
+ #
322
+ # @param options [Array<Hash{Symbol => Object}>] the attribute's leaf option specs
323
+ # @param attribute [Object] the attribute, which may declare a default
324
+ # @return [Array<Hash{Symbol => Object}>] `options`, with the default shown where it lands
325
+ def with_declared_default(options, attribute)
326
+ declared = attribute.default if attribute.respond_to?(:default)
327
+ return options if declared.nil?
328
+
329
+ options.map do |option|
330
+ leaf = option[:path].split(".").last
331
+ value = declared.is_a?(Hash) ? declared.fetch(leaf.to_sym) { declared[leaf] } : (declared if options.one?)
332
+ value.nil? ? option : option.merge(default: value, required: false)
333
+ end
334
+ end
335
+
321
336
  def reference_option(attribute)
322
337
  { path: attribute.name.to_s, type: "String", required: !attribute.optional?,
323
338
  note: "id of a #{attribute.type.target_name}" }
@@ -353,7 +368,7 @@ module Hecks
353
368
  nil
354
369
  end
355
370
 
356
- # The conditions that refuse the verb when they hold, in the chapter's own words: a
371
+ # The conditions that refuse the command when they hold, in the chapter's own words: a
357
372
  # lifecycle state mismatch and a missing referenced record per `reference_to`.
358
373
  def refusals(command, holder)
359
374
  out = []
@@ -366,73 +381,100 @@ module Hecks
366
381
  out
367
382
  end
368
383
 
369
- # The conditions the verb needs: each `given` states what must hold, so the verb is
384
+ # The conditions the command needs: each `given` states what must hold, so the command is
370
385
  # refused unless it does.
371
386
  def requirements(command) = command.givens.map(&:description)
372
387
 
373
- # Renders the full verb/question table, or one verb's `--help` text when
374
- # `options[:verb]` names one.
375
- def usage(bluebook, verbs, questions, options)
388
+ # Renders the full command/question table, or one command's `--help` text when
389
+ # `options[:command]` names one.
390
+ def usage(bluebook, commands, questions, options)
376
391
  program = options[:program] || "hecks run"
377
- only = options[:verb]
392
+ only = options[:command]
378
393
 
379
394
  # `options[:ask]` picks the namespace when both hold the name; without it a
380
395
  # question's `--help` would print the command sharing its name.
381
396
  if only
382
- pool = options[:ask] ? questions : verbs
397
+ pool = options[:ask] ? questions : commands
383
398
  key = aliases(pool)[only] || (options[:ask] ? nil : aliases(questions)[only])
384
399
  spec = pool[key] || questions[key]
385
- return verb_help(program, spec[:short], spec, ask: options[:ask]) if spec
400
+ return command_help(program, spec[:short], spec, ask: options[:ask]) if spec
386
401
  end
387
402
 
388
- width = (verbs.values + questions.values).map { |spec| spec[:short].length }.max.to_i
389
403
  out = ["#{bluebook.name} — #{bluebook.vision}", "",
390
- " #{program} <verb> [name=value …] do something",
391
- " #{program} ask <question> [name=value …] read something", ""]
404
+ " #{program} <command>! [name=value …] do something",
405
+ " #{program} query <query> [name=value …] read something", ""]
392
406
 
393
- out << "verbs:"
394
- out.concat(listing(verbs, width) { |spec| spec[:summary] })
407
+ out << "commands:"
408
+ out.concat(listing(commands) { |spec| spec[:summary] })
395
409
  out << ""
396
- out << "questions (nothing here changes anything):"
397
- out.concat(listing(questions, width) { |spec| first_sentence(spec[:summary]) })
410
+ out << "queries (nothing here changes anything):"
411
+ out.concat(listing(questions) { |spec| first_sentence(spec[:summary]) })
398
412
  out << ""
399
- out << " #{program} <verb> --help what one verb wants, and every way it refuses"
400
- out << " a verb can always be spelled in full — #{example_qualified(verbs)}"
413
+ out << " #{program} <command> --help what one command wants, and every way it refuses"
414
+ out << " a command is called with its aggregate — #{example_qualified(commands)}"
401
415
  out.join("\n")
402
416
  end
403
417
 
404
- # The verb or question lines of the help. A domain with more than one aggregate is listed
405
- # under a heading per aggregate, so related verbs sit together; the bookkeeping a run records
406
- # about itself (`internal`: system-role commands and port operations) is set apart as names
407
- # only, since a person never types them. A single-aggregate domain keeps the plain list.
408
- def listing(specs, width)
418
+ # The command or question lines of the help. A domain with more than one aggregate is listed
419
+ # under a heading per aggregate, so related commands sit together; the heading is the prefix
420
+ # every call to them carries, so the lines under it leave it out. The bookkeeping a run
421
+ # records about itself (`internal`: system-role commands and port operations) is set apart as
422
+ # names only, since a person never types them. A single-aggregate domain keeps the plain
423
+ # list, each name in full.
424
+ def listing(specs)
409
425
  shown, internal = specs.values.partition { |spec| !spec[:internal] }
410
426
  groups = shown.group_by { |spec| spec[:group] }
427
+ grouped = groups.length > 1
428
+ named = shown.to_h { |spec| [spec, entry_name(spec, grouped)] }
429
+ width = named.values.map(&:length).max.to_i
411
430
  lines = []
412
- if groups.length > 1
431
+ if grouped
413
432
  groups.each do |group, members|
414
433
  lines << " #{heading(group)}" if group
415
- members.each { |spec| lines << " #{spec[:short].ljust(width)} #{yield(spec)}" }
434
+ members.each { |spec| lines << " #{named[spec].ljust(width)} #{yield(spec)}#{alias_note(spec)}" }
416
435
  end
417
436
  else
418
- shown.each { |spec| lines << " #{spec[:short].ljust(width)} #{yield(spec)}" }
437
+ shown.each { |spec| lines << " #{named[spec].ljust(width)} #{yield(spec)}#{alias_note(spec)}" }
419
438
  end
420
439
  lines.concat(internal_lines(internal)) unless internal.empty?
421
440
  lines
422
441
  end
423
442
 
424
- # "LanguageRun" reads "Language": the Run suffix names the journaled-command shape all of
425
- # these aggregates share, so it adds nothing under a heading ("Test suite", "Model check").
443
+ # The aggregate's own name as a heading: the prefix of every call to the lines under it.
426
444
  def heading(group)
427
- words = Naming.snake(group.sub(/Run\z/, "")).tr("_", " ")
428
- "#{words.capitalize}:"
445
+ "#{Naming.snake(group)}:"
446
+ end
447
+
448
+ # A spec's name as listed: under its aggregate's heading the aggregate prefix is left out.
449
+ # A command the chapter gives a short name (`mcp`) is listed by its real name, so the
450
+ # heading and the line still spell a call; `alias_note` says the short name.
451
+ def entry_name(spec, grouped)
452
+ name = label(spec, real: true)
453
+ return name unless grouped && spec[:group]
454
+
455
+ name.delete_prefix("#{Naming.snake(spec[:group])}.")
456
+ end
457
+
458
+ # " (also: mcp!)" for a spec the chapter gave a short name, else nothing. A short name that
459
+ # is only the command's own name (`init` for `door.init`) is already in the line.
460
+ def alias_note(spec)
461
+ return "" if spec[:short_was].nil? || spec[:short_was].split(".").last == spec[:short]
462
+
463
+ " (also: #{label(spec)})"
464
+ end
465
+
466
+ # A command is written with the `!` that marks it; a query without one.
467
+ # With `real:`, a short name the chapter gave it gives way to the name it was given for.
468
+ def label(spec, real: false)
469
+ name = real && spec.key?(:short_was) ? spec[:short_was] : spec[:short]
470
+ spec[:kind] == :command ? "#{name}!" : name
429
471
  end
430
472
 
431
- # The internal verbs as bare names, wrapped, under one line saying what they are.
473
+ # The internal commands as bare names, wrapped, under one line saying what they are.
432
474
  def internal_lines(specs)
433
475
  lines = [" internal — what a run records about itself, named here only (`--help` still works):"]
434
476
  line = " "
435
- specs.map { |spec| spec[:short] }.each do |name|
477
+ specs.map { |spec| label(spec) }.each do |name|
436
478
  if line.length + name.length + 1 > 98
437
479
  lines << line
438
480
  line = " "
@@ -442,41 +484,41 @@ module Hecks
442
484
  lines << line
443
485
  end
444
486
 
445
- # One verb whose full spelling differs from its short one, or `""` when none does.
446
- def example_qualified(verbs)
447
- name, spec = verbs.find { |key, value| key != value[:short] } || verbs.first
448
- name ? "#{spec[:short]} is also #{name}" : ""
487
+ # An example call, `aggregate.command!`, from the first command, or `""` when there is none.
488
+ def example_qualified(commands)
489
+ name = commands.keys.first
490
+ name ? "#{name}!" : ""
449
491
  end
450
492
 
451
- # The verb table wants a query description's first sentence; `--help` prints all.
493
+ # The command table wants a query description's first sentence; `--help` prints all.
452
494
  def first_sentence(text)
453
495
  text.to_s.split(/(?<=\.)\s/).first.to_s
454
496
  end
455
497
 
456
498
  # Four independent blocks, concatenated in fixed order: meta, invocation,
457
499
  # arguments, refusals.
458
- def verb_help(program, name, spec, ask: false)
459
- out = verb_help_meta_lines(name, spec)
460
- out.concat(verb_help_invocation_lines(program, name, spec, ask))
461
- out.concat(verb_help_argument_lines(spec))
462
- out.concat(verb_help_refusal_lines(spec))
500
+ def command_help(program, name, spec, ask: false)
501
+ out = command_help_meta_lines(name, spec)
502
+ out.concat(command_help_invocation_lines(program, name, spec, ask))
503
+ out.concat(command_help_argument_lines(spec))
504
+ out.concat(command_help_refusal_lines(spec))
463
505
  out.join("\n")
464
506
  end
465
507
 
466
- def verb_help_meta_lines(name, spec)
508
+ def command_help_meta_lines(name, spec)
467
509
  out = ["#{name} — #{spec[:summary]}", ""]
468
- out << "dispatches #{spec[:verb]}" if spec[:kind] == :command
469
- out << "reads #{spec[:verb]}" if spec[:kind] == :query
470
- out << "issued by #{spec[:role]}" if spec[:role]
510
+ out << "dispatches #{spec[:command]}" if spec[:kind] == :command
511
+ out << "reads #{spec[:command]}" if spec[:kind] == :query
512
+ out << "issued by #{spec[:role]}" if spec[:role]
471
513
  out
472
514
  end
473
515
 
474
- def verb_help_invocation_lines(program, name, spec, ask)
475
- invocation = ask ? "#{program} ask #{name}" : "#{program} #{name}"
516
+ def command_help_invocation_lines(program, name, spec, ask)
517
+ invocation = ask ? "#{program} query #{name}" : "#{program} #{name}!"
476
518
  ["", " #{invocation}#{spec[:arguments].map { |a| " #{a[:path]}=…" }.join}", ""]
477
519
  end
478
520
 
479
- def verb_help_argument_lines(spec)
521
+ def command_help_argument_lines(spec)
480
522
  return [] if spec[:arguments].empty?
481
523
 
482
524
  width = spec[:arguments].map { |a| a[:path].length }.max
@@ -493,7 +535,7 @@ module Hecks
493
535
  lines << ""
494
536
  end
495
537
 
496
- def verb_help_refusal_lines(spec)
538
+ def command_help_refusal_lines(spec)
497
539
  [["refused when:", spec[:refusals]], ["refused unless:", spec[:requirements]]].flat_map do |heading, items|
498
540
  Array(items).empty? ? [] : [heading, *items.map { |item| " #{item}" }, ""]
499
541
  end
@@ -1,4 +1,4 @@
1
- # Binds the `Agent` port to `Agent`; `hecks quality_control mine_combinations` asks it to write candidate domains.
1
+ # Binds the `Agent` port to `Agent`; `hecks quality_control target.mine_combinations` asks it to write candidate domains.
2
2
  # Required by name, not relative, so a copy of this directory shares the one implementation loaded.
3
3
  require "hecks/quality_control/adapters/agent"
4
4
 
@@ -6,8 +6,8 @@ require "shellwords"
6
6
  module Hecks
7
7
  module Adapters
8
8
  # The `Agent` port's adapter: hands a prompt to an agent on the command line and lets it work
9
- # in the checkout, the way `hecks quality_control mine_combinations` asks one for candidate
10
- # domains.
9
+ # in the checkout, the way `hecks quality_control target.mine_combinations` asks one for
10
+ # candidate domains.
11
11
  #
12
12
  # Not the interviewer behind the framework's `agent` port (`ClaudeCode`), which reads one
13
13
  # JSON answer back: this agent edits files, so the only answer is whether it finished.
@@ -7,14 +7,14 @@ require_relative "../../fuzzing/concurrent_dispatch"
7
7
 
8
8
  module Hecks
9
9
  module QualityControlCli
10
- # The command behind `hecks quality_control race`: one racer of
10
+ # The command behind `hecks quality_control sweep.race`: one racer of
11
11
  # `Hecks::Fuzzing::ConcurrentDispatch`, run as its own OS process.
12
12
  #
13
- # It is spawned, not forked: the caller (`hecks quality_control ask run`) holds live
13
+ # It is spawned, not forked: the caller (`hecks quality_control query sweep.run`) holds live
14
14
  # `PostgresEra` connections, and fork duplicates their file descriptors and SSL state,
15
15
  # corrupting them when a child exits.
16
16
  #
17
- # hecks quality_control race <domain-path> <database> <schema> <verb> <args-json>
17
+ # hecks quality_control sweep.race <domain-path> <database> <schema> <verb> <args-json>
18
18
  #
19
19
  # It prints one line, `succeeded`, `refused` or `crashed:<class>: <message>`, and always exits
20
20
  # 0.
@@ -5,15 +5,15 @@ require "open3"
5
5
 
6
6
  module Hecks
7
7
  module QualityControlCli
8
- # The command behind `hecks quality_control discover_external_domains`: reports sibling-repo
9
- # bluebook domains under `--projects-dir` that already depend on the hecks gem but are not
10
- # enrolled yet. It never enrolls anything itself.
8
+ # The command behind `hecks quality_control target.discover_external_domains`: reports
9
+ # sibling-repo bluebook domains under `--projects-dir` that already depend on the hecks gem but
10
+ # are not enrolled yet. It never enrolls anything itself.
11
11
  #
12
- # hecks quality_control discover_external_domains
13
- # hecks quality_control discover_external_domains --projects-dir ~/Projects
14
- # hecks quality_control discover_external_domains --max-depth 4
15
- # hecks quality_control discover_external_domains --known-path <path> ... # bypass the
16
- # ledger read
12
+ # hecks quality_control target.discover_external_domains
13
+ # hecks quality_control target.discover_external_domains --projects-dir ~/Projects
14
+ # hecks quality_control target.discover_external_domains --max-depth 4
15
+ # hecks quality_control target.discover_external_domains --known-path <path> ... # bypass
16
+ # the ledger read
17
17
  class QaDiscoverExternalDomains
18
18
  USAGE = "usage: hecks quality_control discover_external_domains [--projects-dir <path>] " \
19
19
  "[--max-depth N] [--known-path <path> ...]"
@@ -7,10 +7,10 @@ require_relative "../../fuzzing/form_census"
7
7
 
8
8
  module Hecks
9
9
  module QualityControlCli
10
- # The command behind `hecks quality_control judge_novelty`: does a new stress domain earn its
11
- # place? It is the gate before a domain becomes a `Target`.
10
+ # The command behind `hecks quality_control target.judge_novelty`: does a new stress domain earn
11
+ # its place? It is the gate before a domain becomes a `Target`.
12
12
  #
13
- # hecks quality_control judge_novelty <domain-path> [--against <path> ...]
13
+ # hecks quality_control target.judge_novelty <domain-path> [--against <path> ...]
14
14
  #
15
15
  # A domain is new when it puts two forms together on one aggregate that no existing target does
16
16
  # (`Hecks::Fuzzing::FormCensus`). It is compared against the ledger's `Target.path` rows unless
@@ -12,8 +12,8 @@ require_relative "child"
12
12
 
13
13
  module Hecks
14
14
  module QualityControlCli
15
- # The command behind `hecks quality_control check_generated_domains`: generates domains nobody
16
- # wrote and checks them the way the rotation does (`USAGE` lists the forms: generate,
15
+ # The command behind `hecks quality_control target.check_generated_domains`: generates domains
16
+ # nobody wrote and checks them the way the rotation does (`USAGE` lists the forms: generate,
17
17
  # `--blueprint`, `--source` and `--promote`).
18
18
  #
19
19
  # Exit codes: 0 every valid domain clean; 2 at least one finding; 1 operational error.
@@ -202,8 +202,8 @@ module Hecks
202
202
  ::QualityControlDials.const_get(name)
203
203
  end
204
204
 
205
- # `--from-dials` is how `hecks quality_control tick` runs this. The dials come from the text
206
- # of the bluebook before `Hecks.bluebook`, so the ledger's Postgres is not needed.
205
+ # `--from-dials` is how `hecks quality_control sweep.tick` runs this. The dials come from the
206
+ # text of the bluebook before `Hecks.bluebook`, so the ledger's Postgres is not needed.
207
207
  # `QA_GENERATED_DOMAINS_PER_TICK` overrides the count for one run.
208
208
  #
209
209
  # @return [Boolean] false when the dials turn the step off, true once they are applied
@@ -7,11 +7,11 @@ require_relative "../../ports/persistence/plugins/era"
7
7
 
8
8
  module Hecks
9
9
  module QualityControlCli
10
- # The command behind `hecks quality_control log`: logs a bug to the QualityControl ledger,
10
+ # The command behind `hecks quality_control bug.log`: logs a bug to the QualityControl ledger,
11
11
  # proven, numbered and triaged. It runs `--demonstration` first and refuses unless it exits
12
12
  # non-zero.
13
13
  #
14
- # hecks quality_control log --sweep <sweep-id> --title "…" \
14
+ # hecks quality_control bug.log --sweep <sweep-id> --title "…" \
15
15
  # --demonstration "bundle exec rspec spec/x_spec.rb -e 'the case'" \
16
16
  # --symptom "…" --expectation "…" --submitter "<who>" \
17
17
  # --triage self_contained|bigger [--tag word]… [--reproduced yes|no]