hecks 3.0.5 → 3.1.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 (150) 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 +31 -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/interview_draft.rb +246 -0
  15. data/lib/hecks/cli/model_check.rb +1 -1
  16. data/lib/hecks/cli/project_cli.rb +51 -29
  17. data/lib/hecks/cli/reference.rb +2 -1
  18. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
  19. data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
  20. data/lib/hecks/cli/spec_example.rb +1 -1
  21. data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
  22. data/lib/hecks/corpus.rb +11 -8
  23. data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
  24. data/lib/hecks/deploy/oidc.json +5 -0
  25. data/lib/hecks/doc/reference.rb +4 -3
  26. data/lib/hecks/doors/cli_door.rb +13 -13
  27. data/lib/hecks/doors/cli_runner.rb +52 -32
  28. data/lib/hecks/doors/launcher_options.rb +5 -5
  29. data/lib/hecks/doors/mcp_door.rb +40 -18
  30. data/lib/hecks/doors/mcp_door_scope.rb +244 -24
  31. data/lib/hecks/doors.rb +1 -1
  32. data/lib/hecks/embryonaut_bluebook.rb +2 -2
  33. data/lib/hecks/forms/banking_presentation.rb +1 -1
  34. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
  35. data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
  36. data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
  37. data/lib/hecks/framework.rb +1 -1
  38. data/lib/hecks/fuzzing/combination_miner.rb +1 -1
  39. data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
  40. data/lib/hecks/fuzzing/differential.rb +4 -2
  41. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  42. data/lib/hecks/fuzzing/form_census.rb +2 -2
  43. data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
  44. data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
  45. data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
  46. data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
  47. data/lib/hecks/gate/stages.yml +5 -5
  48. data/lib/hecks/grammar/grammar.hecksagon +2 -2
  49. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
  50. data/lib/hecks/hecks/adapters/host_http.rb +4 -3
  51. data/lib/hecks/hecks/adapters/local_files.rb +59 -0
  52. data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
  53. data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
  54. data/lib/hecks/hecks/codebase.bluebook +4 -3
  55. data/lib/hecks/hecks/context_map.hecksagon +2 -2
  56. data/lib/hecks/hecks/custodian.bluebook +45 -3
  57. data/lib/hecks/hecks/hecks.hecksagon +10 -4
  58. data/lib/hecks/hecks/hecks.world +6 -6
  59. data/lib/hecks/hecks/site.behaviors +17 -0
  60. data/lib/hecks/hecks/site.world +10 -0
  61. data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
  62. data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
  63. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
  64. data/lib/hecks/projections/deploy/box/settings.rb +248 -0
  65. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +267 -0
  66. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +70 -0
  67. data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
  68. data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +165 -0
  69. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
  70. data/lib/hecks/projections/deploy/box.rb +387 -0
  71. data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
  72. data/lib/hecks/projections/deploy/fargate.rb +1 -1
  73. data/lib/hecks/projections/deploy/lambda.rb +11 -10
  74. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
  75. data/lib/hecks/projections/diagrams.rb +2 -2
  76. data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
  77. data/lib/hecks/projections/site/edge/checks.rb +137 -0
  78. data/lib/hecks/projections/site/edge/pattern.rb +50 -0
  79. data/lib/hecks/projections/site/edge/rules.rb +128 -0
  80. data/lib/hecks/projections/site/edge.rb +136 -0
  81. data/lib/hecks/projections/site/regions.rb +44 -0
  82. data/lib/hecks/projections/site/routes_ts.rb +284 -0
  83. data/lib/hecks/projections/site/site_cdn.rb +78 -0
  84. data/lib/hecks/projections/site/table/checks.rb +124 -0
  85. data/lib/hecks/projections/site/table/row_builder.rb +155 -0
  86. data/lib/hecks/projections/site/table.rb +98 -0
  87. data/lib/hecks/projections.rb +2 -0
  88. data/lib/hecks/projector/cli_projector.rb +122 -104
  89. data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
  90. data/lib/hecks/quality_control/adapters/agent.rb +2 -2
  91. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
  92. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
  93. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
  94. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
  95. data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
  96. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
  97. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
  98. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
  99. data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
  100. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
  101. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
  102. data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
  103. data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
  104. data/lib/hecks/quality_control/quality_control.bluebook +18 -18
  105. data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
  106. data/lib/hecks/runtime/registry/verification.rb +15 -29
  107. data/lib/hecks/runtime/registry.rb +14 -20
  108. data/lib/hecks/rust_build/append_optionals.rb +79 -0
  109. data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
  110. data/lib/hecks/rust_build/domain_name.rb +26 -0
  111. data/lib/hecks/rust_build/kernel_input.rb +58 -0
  112. data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
  113. data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
  114. data/lib/hecks/rust_build/project_rust.rb +57 -24
  115. data/lib/hecks/rust_build/rust_literal.rb +33 -0
  116. data/lib/hecks/rust_build/write_if_changed.rb +86 -0
  117. data/lib/hecks/site/bluebook/site.bluebook +205 -0
  118. data/lib/hecks/site/bluebook/site.hecksagon +5 -0
  119. data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
  120. data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
  121. data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
  122. data/lib/hecks/storehouse.rb +37 -7
  123. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
  124. data/lib/hecks/three_zero/forms.yml +88 -88
  125. data/lib/hecks/tools/ci_gates.rb +1 -1
  126. data/lib/hecks/tools/deploy_recipe.rb +14 -6
  127. data/lib/hecks/tools/oidc_manifests.rb +2 -1
  128. data/lib/hecks/tools/sequence_script.rb +2 -2
  129. data/lib/hecks/tools/site_routes.rb +152 -0
  130. data/lib/hecks/tools/tenant_provisioning.rb +1 -1
  131. data/lib/hecks/tools.rb +1 -0
  132. data/lib/hecks/version.rb +1 -1
  133. data/rust/build/src/cargo_sync.rs +1 -1
  134. data/rust/build/src/pipeline.rs +23 -16
  135. data/rust/build/src/resolve.rs +39 -6
  136. data/rust/host/HECKS_RELEASE +1 -1
  137. data/rust/host/src/api.rs +1 -1
  138. data/rust/host/src/dispatch.rs +87 -7
  139. data/rust/host/src/needs.rs +111 -0
  140. data/rust/parser/src/keywords.rs +7 -6
  141. data/rust/parser/src/main.rs +4 -3
  142. data/rust/parser/src/parse/chapter.rs +1 -1
  143. data/rust/parser/src/parse/hecksagon.rs +25 -10
  144. data/rust/project/mutations.rb +4 -30
  145. data/rust/project/naming.rb +8 -28
  146. data/rust/project/write_if_changed.rb +4 -80
  147. data/rust/project_rust_pipeline.rb +16 -12
  148. data/rust/src/kernel/cli.rs +2 -0
  149. data/rust/src/kernel/needs.rs +74 -1
  150. metadata +37 -1
@@ -0,0 +1,246 @@
1
+ require_relative "../naming"
2
+ require_relative "domain_stub"
3
+
4
+ module Hecks
5
+ module CLI
6
+ # Turns an interview's accepted findings into the files of a first domain (ADR 0088).
7
+ #
8
+ # Pure: it answers the text of each file and writes none, so the command that runs an interview
9
+ # can check every target before it writes the first, and a spec can boot the result. It shares
10
+ # the files around a bluebook with `hecks init` (`DomainStub.support_files`) and renders the
11
+ # bluebook itself, from what the developer accepted, where `init` writes a fixed stub.
12
+ #
13
+ # Only `accepted` findings reach the bluebook; every finding reaches the record. A rule is never
14
+ # turned into code from prose: it is a comment citing its source, to become a `given` by hand.
15
+ module InterviewDraft
16
+ ACCEPTED = "accepted".freeze
17
+
18
+ # The pattern every generated string field carries: it refuses a blank value.
19
+ PATTERN = %q('[^ \t\n\r]').freeze
20
+
21
+ # What stands in for a creating command nothing in the interview named.
22
+ STUB_NOTE = " # TODO: no action that creates was accepted; replace Create with the real one.".freeze
23
+ private_constant :PATTERN, :STUB_NOTE
24
+
25
+ module_function
26
+
27
+ # The files a first interview writes: the bluebook, the files around it, and the record.
28
+ #
29
+ # @param interview [Hash] the interview, with at least one accepted thing
30
+ # @param adapter [String, nil] an adapter in `DomainStub::ADAPTERS`; its default when nil
31
+ # @return [Hash{String => String}] each file's path under the domain directory, and its text
32
+ # @raise [ArgumentError] when nothing was accepted, or the subject or adapter is refused
33
+ def files(interview, adapter: nil)
34
+ name = interview.fetch(:subject)
35
+ support = DomainStub.support_files(name: name, adapter: adapter)
36
+ { "bluebook/#{Naming.snake(name)}.bluebook" => bluebook(interview) }
37
+ .merge(support)
38
+ .merge(record_file(interview))
39
+ end
40
+
41
+ # What a later interview writes: its record, ending in the aggregates and commands it would
42
+ # add, for a developer to merge. An existing domain is never touched (ADR 0088).
43
+ #
44
+ # @param interview [Hash] the interview
45
+ # @return [Hash{String => String}] the one file, `interviews/<reference>.md`
46
+ def additions(interview)
47
+ record_file(interview, "\n## Proposed additions\n\nMerge by hand into the domain's bluebook.\n\n" \
48
+ "```ruby\n#{fragment(interview)}```\n")
49
+ end
50
+
51
+ # The interview's record: every exchange in order, and every finding with how it was decided.
52
+ #
53
+ # @param interview [Hash] the interview
54
+ # @return [String] Markdown
55
+ def record(interview)
56
+ lines = ["# Interview #{interview.fetch(:reference)}: #{interview.fetch(:subject)}", "",
57
+ "Expert: #{interview.fetch(:expert)}", "", "## Exchanges", ""]
58
+ interview.fetch(:exchanges).each_with_index { |ex, i| lines.concat(exchange_lines(ex, i + 1)) }
59
+ lines << "" << "## Findings" << ""
60
+ lines.concat(finding_lines(interview))
61
+ "#{lines.join("\n")}\n"
62
+ end
63
+
64
+ # Reads the plain interview out of an `Interview` record booted from the SME chapter.
65
+ #
66
+ # @param interview [Object] the booted record
67
+ # @return [Hash] the plain interview this module takes: `reference`, `subject`, `expert`,
68
+ # `exchanges` (`question`, `answer`, `topic`), and `things`, `actions` and `rules`, each a
69
+ # finding with its `number`, `source` and `status` and the fields of its kind
70
+ def from_record(interview)
71
+ { reference: plain(interview.reference), subject: plain(interview.subject), expert: plain(interview.expert),
72
+ exchanges: interview.exchanges.map { |ex| slice(ex, :question, :answer, :topic) },
73
+ things: interview.thing_findings.map { |f| slice(f, :number, :name, :identifier, :source, :status) },
74
+ actions: interview.action_findings.map { |f| slice(f, :number, :name, :thing, :event, :creates, :source, :status) },
75
+ rules: interview.rule_findings.map { |f| slice(f, :number, :statement, :source, :status) } }
76
+ end
77
+
78
+ # @api private
79
+ def record_file(interview, tail = "")
80
+ { "interviews/#{file_stem(interview.fetch(:reference))}.md" => record(interview) + tail }
81
+ end
82
+
83
+ # @api private
84
+ def file_stem(reference) = reference.to_s.gsub(/[^A-Za-z0-9._-]/, "_")
85
+
86
+ # @api private
87
+ def bluebook(interview)
88
+ name = interview.fetch(:subject)
89
+ things = accepted(interview, :things)
90
+ raise ArgumentError, "nothing was accepted: a draft needs at least one thing" if things.empty?
91
+
92
+ out = ["Hecks.bluebook #{name.inspect} do"]
93
+ out.concat(header(interview))
94
+ out << %( vision "TODO: say in one sentence what #{name} is for.")
95
+ out << " supporting" << ""
96
+ out.concat(rule_lines(interview))
97
+ out << fragment(interview).chomp << "end"
98
+ "#{out.join("\n")}\n"
99
+ end
100
+
101
+ # The aggregates, with their commands: the part a later interview offers to merge.
102
+ # @api private
103
+ def fragment(interview)
104
+ things = accepted(interview, :things)
105
+ actions = accepted(interview, :actions)
106
+ placed = things.map { |t| word(t[:name]) }
107
+ blocks = things.map { |thing| aggregate(interview, thing, actions) }
108
+ loose = actions.reject { |a| placed.include?(word(a[:thing])) }
109
+ blocks << unplaced(interview, loose) unless loose.empty?
110
+ "#{blocks.join("\n\n")}\n"
111
+ end
112
+
113
+ # @api private
114
+ def header(interview)
115
+ [" # Drafted from interview #{interview[:reference]} with #{interview[:expert]}. Each name below cites the",
116
+ " # exchange it came from; edit freely."]
117
+ end
118
+
119
+ # @api private
120
+ def rule_lines(interview)
121
+ rules = accepted(interview, :rules)
122
+ return [] if rules.empty?
123
+
124
+ [" # RULES the expert gave, for you to turn into `given` clauses:"] +
125
+ rules.map { |r| " # - #{cite(interview, r)} #{one_line(r[:statement])}" } + [""]
126
+ end
127
+
128
+ # @api private
129
+ def aggregate(interview, thing, actions)
130
+ name = word(thing[:name])
131
+ field = Naming.snake(word(thing[:identifier]))
132
+ field_type = identifier_type(name, field)
133
+ mine = actions.select { |a| word(a[:thing]) == name }
134
+ creating = mine.select { |a| a[:creates] }
135
+ rest = mine - creating
136
+ lines = [" # #{source_note(interview, thing)}".rstrip,
137
+ " aggregate #{name.inspect} do",
138
+ " description #{"TODO: describe what a #{name} is.".inspect}", "",
139
+ " identified_by :#{field}", "",
140
+ " attribute :#{field}, #{field_type}", "",
141
+ " value_object #{field_type.inspect} do",
142
+ " attribute :value, String, pattern: #{PATTERN}",
143
+ " end"]
144
+ creating = [stub_create(name)] if creating.empty?
145
+ creating.each { |action| lines.concat(creating_command(interview, action, field, field_type)) }
146
+ rest.each { |action| lines.concat(mutating_command(interview, action, name)) }
147
+ (lines << " end").join("\n")
148
+ end
149
+
150
+ # The creating command nothing in the interview named: a stub, marked for the developer.
151
+ # @api private
152
+ def stub_create(name) = { name: "Create", event: "#{name}Created", stub: true }
153
+
154
+ # @api private
155
+ def creating_command(interview, action, field, field_type)
156
+ verb = word(action[:name])
157
+ note = action[:stub] ? STUB_NOTE : " # #{source_note(interview, action)}"
158
+ ["", note, " command #{verb.inspect} do", " goal #{"TODO: say what #{verb} does".inspect}", "",
159
+ " attribute :#{field}, #{field_type}", "", " emits #{word(action[:event]).inspect}", " end"]
160
+ end
161
+
162
+ # @api private
163
+ def mutating_command(interview, action, name)
164
+ verb = word(action[:name])
165
+ ["", " # #{source_note(interview, action)}", " command #{verb.inspect} do",
166
+ " goal #{"TODO: say what #{verb} does".inspect}", "", " reference_to #{name}", "",
167
+ " emits #{word(action[:event]).inspect}", " end"]
168
+ end
169
+
170
+ # An action whose thing nobody accepted has nowhere to go; it is kept as a comment, not lost.
171
+ # @api private
172
+ def unplaced(interview, actions)
173
+ lines = [" # UNPLACED: these accepted actions name a thing that was not accepted. Place each, or drop it."]
174
+ actions.each do |action|
175
+ lines << " # - #{cite(interview, action)} #{word(action[:name])} on #{action[:thing]}, announcing #{action[:event]}"
176
+ end
177
+ lines.join("\n")
178
+ end
179
+
180
+ # @api private
181
+ def identifier_type(name, field)
182
+ type = Naming.pascal(field)
183
+ type == name ? "#{type}Id" : type
184
+ end
185
+
186
+ # @api private
187
+ def exchange_lines(exchange, number)
188
+ lines = ["#{number}. **Q:** #{one_line(exchange[:question])}", " **A:** #{one_line(exchange[:answer])}"]
189
+ topic = exchange[:topic].to_s
190
+ lines << " _Topic: #{one_line(topic)}_" unless topic.empty?
191
+ lines
192
+ end
193
+
194
+ # @api private
195
+ def finding_lines(interview)
196
+ lines = interview.fetch(:things).map do |f|
197
+ "- Thing #{f[:number]}, #{f[:status]}: **#{f[:name]}**, identified by `#{f[:identifier]}` (exchange #{f[:source]})"
198
+ end
199
+ lines += interview.fetch(:actions).map do |f|
200
+ creates = f[:creates] ? ", creates it" : ""
201
+ "- Action #{f[:number]}, #{f[:status]}: **#{f[:name]}** on #{f[:thing]}, " \
202
+ "announcing `#{f[:event]}`#{creates} (exchange #{f[:source]})"
203
+ end
204
+ lines + interview.fetch(:rules).map do |f|
205
+ "- Rule #{f[:number]}, #{f[:status]}: #{one_line(f[:statement])} (exchange #{f[:source]})"
206
+ end
207
+ end
208
+
209
+ # @api private
210
+ def accepted(interview, kind) = interview.fetch(kind).select { |finding| finding[:status] == ACCEPTED }
211
+
212
+ # A free-text name as a PascalCase word safe to write into Ruby: `lend a book` is `LendABook`.
213
+ # @api private
214
+ def word(text) = text.to_s.split(/[^A-Za-z0-9]+/).reject(&:empty?).map { |part| part.sub(/\A./, &:upcase) }.join
215
+
216
+ # @api private
217
+ def one_line(text) = text.to_s.gsub(/\s+/, " ").strip
218
+
219
+ # @api private
220
+ def cite(interview, finding) = "#{interview[:reference]} ##{finding[:source]}:"
221
+
222
+ # Where a name came from: the interview, the exchange, and a short quotation of the answer.
223
+ # @api private
224
+ def source_note(interview, finding)
225
+ exchange = interview.fetch(:exchanges)[finding[:source].to_i - 1]
226
+ note = "from #{cite(interview, finding)}"
227
+ return note unless exchange
228
+
229
+ text = one_line(exchange[:answer])
230
+ text = "#{text[0, 77]}..." if text.length > 80
231
+ "#{note} #{text.inspect}"
232
+ end
233
+
234
+ # @api private
235
+ def plain(value)
236
+ return nil if value.nil?
237
+
238
+ found = value.respond_to?(:to_h) && !value.is_a?(String) ? value.to_h : value
239
+ found.is_a?(Hash) && found.key?(:value) ? found[:value] : found
240
+ end
241
+
242
+ # @api private
243
+ def slice(record, *keys) = keys.to_h { |key| [key, plain(record[key])] }
244
+ end
245
+ end
246
+ end
@@ -88,7 +88,7 @@ module Hecks
88
88
  end
89
89
 
90
90
  # `root:` is the source's parent, so a member declaring
91
- # `uses_embryonaut_bluebook` can vendor from it.
91
+ # `attaches ... from: :vendor` can vendor from it.
92
92
  def boot(source)
93
93
  root = File.directory?(source) ? File.dirname(source) : nil
94
94
  registry = Runtime::Registry.new(root: root)
@@ -17,7 +17,7 @@ module Hecks
17
17
  # Build output and other trees under a root that hold no domain a caller means.
18
18
  IGNORED = %r{\A(rust|deploy|tmp|coverage)/}
19
19
 
20
- # What a chapter name, a domain path and a launcher verb may be made of.
20
+ # What a chapter name, a domain path and a launcher command may be made of.
21
21
  NAME = /\A[A-Za-z][A-Za-z0-9_ ]*\z/
22
22
  PATH = %r{\A[\w./-]+\z}
23
23
  VERB = /\A[A-Za-z_][A-Za-z0-9_]*\z/
@@ -40,9 +40,28 @@ module Hecks
40
40
  status.zero? ? puts(text) : abort(text)
41
41
  LAUNCHER_TAIL
42
42
 
43
+ # What a Memory command does when refused: say why, from the record's `refusal`.
44
+ MEMORY_REFUSAL = <<~'LAUNCHER_REFUSAL'.gsub(/^/, " ").freeze
45
+ if MEMORY_COMMANDS.include?(ARGV.first.to_s.chomp("!"))
46
+ why = begin
47
+ require "json"
48
+ JSON.parse(text).dig("state", "refusal", "value")
49
+ rescue JSON::ParserError
50
+ nil
51
+ end
52
+ warn(why ? why.sub(/\A(\w+::)+\w+: /, "") : reason)
53
+ else
54
+ puts text
55
+ warn reason
56
+ end
57
+ LAUNCHER_REFUSAL
58
+
43
59
  # The generator's name as an executable launcher's header states it, whoever ran it.
44
60
  GENERATOR = "hecks project_cli".freeze
45
61
 
62
+ # The launcher's test for a Memory command; a trailing `!` is not part of the name.
63
+ RUNS_ON_MEMORY = 'MEMORY_COMMANDS.include?(ARGV.first.to_s.chomp("!"))'.freeze
64
+
46
65
  module_function
47
66
 
48
67
  # Writes a launcher for each named domain, or for every domain under `root`.
@@ -103,7 +122,7 @@ module Hecks
103
122
  label = executable || "#{path}/#{snake}"
104
123
  file = File.join(root, label)
105
124
  text = launcher(path, name, program, executable: executable, legacy: setting[:legacy],
106
- memory_verbs: setting[:memory_verbs], opted: !setting.empty?)
125
+ memory_commands: setting[:memory_commands], opted: !setting.empty?)
107
126
  return skip(path, "#{label} is a directory") if File.directory?(file)
108
127
 
109
128
  return :drifted if check && !(File.file?(file) && File.read(file) == text)
@@ -149,12 +168,12 @@ module Hecks
149
168
  # @param program [String] the generator's invocation, named in the header
150
169
  # @param executable [String, nil] the file's path under the root when it is not beside the
151
170
  # domain; its program name is then that file's basename, and its header names the generator
152
- # @param legacy [Array<String>, nil] verbs an executable hands to `Hecks::CLI` first
153
- # @param memory_verbs [Array<String>, nil] verbs run on Memory unless HECKS_ENVIRONMENT is set
171
+ # @param legacy [Array<String>, nil] commands an executable hands to `Hecks::CLI` first
172
+ # @param memory_commands [Array<String>, nil] commands that default to Memory
154
173
  # @param opted [Boolean] whether the chapter's world declares a `launcher` setting
155
174
  # @return [String] the Ruby source
156
- # @raise [ArgumentError] if a name, path, executable or legacy verb is not plain
157
- def launcher(path, name, program, executable: nil, legacy: nil, memory_verbs: nil, opted: !executable.nil?)
175
+ # @raise [ArgumentError] if a name, path, executable or legacy command is not plain
176
+ def launcher(path, name, program, executable: nil, legacy: nil, memory_commands: nil, opted: !executable.nil?)
158
177
  plain!("chapter name", name, NAME)
159
178
  plain!("domain path", path, PATH)
160
179
  snake = Naming.snake(name)
@@ -175,35 +194,35 @@ module Hecks
175
194
  where = shown = "#{path}/#{snake}"
176
195
  end
177
196
 
178
- handoff = legacy_handoff(Array(legacy)) + memory_default(Array(memory_verbs)) if executable
197
+ handoff = legacy_handoff(Array(legacy)) + memory_default(Array(memory_commands)) if executable
179
198
  encoding = opted ? ENCODING : ""
180
199
  ending = opted ? OPTED_ENDING : PLAIN_ENDING
181
- ending = quiet_ending(ending) if executable && !Array(memory_verbs).empty?
200
+ ending = quiet_ending(ending) if executable && !Array(memory_commands).empty?
182
201
 
183
202
  <<~RUBY
184
203
  #!/usr/bin/env ruby
185
204
 
186
205
  # #{name}'s command line — GENERATED by #{program}. Do not edit.
187
206
  #
188
- # A launcher, not a CLI: the verbs, their arguments and their refusals are
207
+ # A launcher, not a CLI: the commands, their arguments and their refusals are
189
208
  # projected from the bluebook beside it every time this runs, so a change
190
209
  # to the chapter shows up here without re-minting anything.
191
210
  #
192
- # #{where} every verb, and every question
193
- # #{where} <verb> --help what it wants, and how it refuses
211
+ # #{where} every command, and every question
212
+ # #{where} <command> --help what it wants, and how it refuses
194
213
 
195
214
  #{encoding}$LOAD_PATH.unshift File.expand_path("#{up}lib", __dir__)
196
215
  #{handoff}
197
216
  require "hecks"
198
217
 
199
- #{entry(name, boot, shown, opted, quiet: !Array(memory_verbs).empty?)}
218
+ #{entry(name, boot, shown, opted, quiet: !Array(memory_commands).empty?)}
200
219
  #{ending}
201
220
  RUBY
202
221
  end
203
222
 
204
223
  # @api private
205
224
  # @return [String] the launcher's middle: for an opted-in launcher, usage answered from the
206
- # projection and a boot only for a line that runs a verb; for any other, the boot and
225
+ # projection and a boot only for a line that runs a command; for any other, the boot and
207
226
  # dispatch every launcher has always had, byte for byte
208
227
  def entry(name, boot, shown, opted, quiet: false)
209
228
  opted ? described_entry(name, boot, shown, quiet: quiet) : plain_entry(name, boot, shown)
@@ -213,10 +232,10 @@ module Hecks
213
232
  # @return [String] the opted-in launcher's middle
214
233
  def described_entry(name, boot, shown, quiet: false)
215
234
  started = "Hecks.boot_described(described, install_doors: false)"
216
- started = "Hecks::Doors::LauncherOptions.quietly(hold: MEMORY_VERBS.include?(ARGV.first)) { #{started} }" if quiet
235
+ started = "Hecks::Doors::LauncherOptions.quietly(hold: #{RUNS_ON_MEMORY}) { #{started} }" if quiet
217
236
  <<~RUBY.chomp
218
237
  # Usage is answered from the projected chapter alone: no adapter is bound and no
219
- # database is opened. Only a line that runs a verb boots the domain,
238
+ # database is opened. Only a line that runs a command boots the domain,
220
239
  # and it boots from what the usage check already loaded.
221
240
  domain = #{boot}
222
241
  program = "#{shown}"
@@ -270,35 +289,38 @@ module Hecks
270
289
 
271
290
  # @api private
272
291
  # @param ending [String] the launcher's closing lines
273
- # @return [String] the same lines, except that a memory verb that ended well prints nothing: a
274
- # person is at it, so the settled record is noise after the session
292
+ # @return [String] the same lines, except that a memory command prints no settled record, only
293
+ # the reason it was refused: a person is at it, so the record is noise
275
294
  def quiet_ending(ending)
276
- ending.sub("status.zero? ? puts(text)",
277
- "exit 0 if status.zero? && MEMORY_VERBS.include?(ARGV.first)\nstatus.zero? ? puts(text)")
295
+ ending.sub(" puts text\n warn reason\n", MEMORY_REFUSAL)
296
+ .sub("status.zero? ? puts(text)",
297
+ "exit 0 if status.zero? && MEMORY_COMMANDS.include?(ARGV.first.to_s.chomp(\"!\"))\nstatus.zero? ? puts(text)")
278
298
  end
279
299
 
280
300
  # @api private
281
- # @return [String] the lines that default the listed verbs to Memory, or nothing
282
- def memory_default(verbs)
283
- return "" if verbs.empty?
301
+ # @return [String] the lines that default the listed commands to Memory, or nothing
302
+ def memory_default(commands)
303
+ return "" if commands.empty?
284
304
 
285
- verbs.each { |verb| plain!("memory verb", verb, VERB) }
305
+ commands.each { |command| plain!("memory command", command, VERB) }
286
306
 
287
307
  <<~RUBY
288
308
 
289
- # These verbs keep nothing worth a database, so they run on Memory unless told otherwise, and
290
- # print nothing when they end well.
291
- MEMORY_VERBS = %w[#{verbs.join(' ')}].freeze
292
- ENV["HECKS_ENVIRONMENT"] ||= "memory" if MEMORY_VERBS.include?(ARGV.first)
309
+ # These commands keep nothing worth a database, so they run on Memory unless told otherwise.
310
+ # A person is at them: they wait for their result, say why when they are refused, and print
311
+ # no record when they end well.
312
+ MEMORY_COMMANDS = %w[#{commands.join(' ')}].freeze
313
+ ENV["HECKS_ENVIRONMENT"] ||= "memory" if MEMORY_COMMANDS.include?(ARGV.first.to_s.chomp("!"))
314
+ ARGV << "--wait" if MEMORY_COMMANDS.include?(ARGV.first.to_s.chomp("!")) && !ARGV.include?("--wait")
293
315
  RUBY
294
316
  end
295
317
 
296
318
  # @api private
297
- # @return [String] the lines that hand the legacy verbs to `Hecks::CLI`, or nothing
319
+ # @return [String] the lines that hand the legacy commands to `Hecks::CLI`, or nothing
298
320
  def legacy_handoff(legacy)
299
321
  return "" if legacy.empty?
300
322
 
301
- legacy.each { |verb| plain!("legacy verb", verb, VERB) }
323
+ legacy.each { |command| plain!("legacy command", command, VERB) }
302
324
 
303
325
  <<~RUBY
304
326
 
@@ -5,7 +5,8 @@ require_relative "../projection_files"
5
5
 
6
6
  module Hecks
7
7
  module CLI
8
- # The command behind `hecks project_reference`: regenerates `docs/implemented/reference/` from
8
+ # The command behind `hecks language_run.project_reference`: regenerates
9
+ # `docs/implemented/reference/` from
9
10
  # the language's Syntax chapter. Tables come from the declaration; prose is kept from the
10
11
  # committed pages.
11
12
  module Reference
@@ -39,7 +39,7 @@ module Hecks
39
39
  if argv.first == "--from-run"
40
40
  from_run(root, argv.fetch(1) { abort USAGE }, out)
41
41
  else
42
- from_local(root, argv.fetch(0, "6"), out)
42
+ from_local(root, argv.fetch(0) { abort USAGE }, out)
43
43
  end
44
44
  0
45
45
  end
@@ -6,15 +6,15 @@ require_relative "../fuzzing"
6
6
 
7
7
  module Hecks
8
8
  module CLI
9
- # The command behind `hecks seed_semantics_corpus`: seeds (or deliberately re-seeds) the
9
+ # The command behind `hecks test_suite_run.seed_semantics_corpus`: seeds (or re-seeds) the
10
10
  # `expect` block of corpus fixtures from the Ruby runtime. Review the result
11
11
  # against `docs/semantics/bluebook-semantics.md` before committing.
12
12
  #
13
13
  # Seeds `spec/corpus/semantics/` and the conformance corpus ({FULL_SCRIPTS} plus
14
14
  # `spec/corpus/rust_conformance/`, whose `expect` also freezes more).
15
15
  #
16
- # hecks seed_semantics_corpus # fill fixtures missing an expect
17
- # SEED=refusal_kind_lifecycle hecks seed_semantics_corpus # re-seed one, deliberately
16
+ # hecks test_suite_run.seed_semantics_corpus # fill what lacks an expect
17
+ # SEED=refusal_kind_lifecycle hecks test_suite_run.seed_semantics_corpus # re-seed one
18
18
  #
19
19
  # The seeded expectation is the definition, not a recording: both runtimes are compared to it.
20
20
  # `occurred_at` is stripped from events (C7.3/C9.1); refusals keep verb, error and kind (C8.2).
@@ -5,7 +5,7 @@ require "rspec/core"
5
5
  module Hecks
6
6
  module CLI
7
7
  # The command behind `hecks run_spec_example`: runs one spec file filtered to one example
8
- # through `RSpec::Core::Runner`, so a `hecks quality_control log --demonstration` string
8
+ # through `RSpec::Core::Runner`, so a `hecks quality_control bug.log --demonstration` string
9
9
  # needs no `-e` quoting.
10
10
  module SpecExample
11
11
  USAGE = "usage: hecks run_spec_example <spec_file> <example description substring>"
@@ -31,9 +31,8 @@ module Hecks
31
31
  USAGE = <<~TEXT
32
32
  Usage: hecks stress_concurrency [--runs N] [--parallel N] [--seed-start N]
33
33
 
34
- --runs N How many times to run EACH group below (default 30).
35
- Each run uses a different --seed (seed-start + run
36
- index).
34
+ --runs N How many times to run EACH group below. Each run uses
35
+ a different --seed (seed-start + run index).
37
36
  --parallel N How many parallel-safe-group runs to have going as
38
37
  separate OS processes AT THE SAME TIME (default: this
39
38
  machine's own core count, via Etc.nprocessors) — real
@@ -41,9 +40,12 @@ module Hecks
41
40
  seeds one after another. The Postgres-backed group
42
41
  always runs one process at a time regardless of this
43
42
  flag.
44
- --seed-start N First --seed value (default 1); runs use seed_start,
43
+ --seed-start N First --seed value; runs use seed_start,
45
44
  seed_start + 1, ... seed_start + runs - 1.
46
45
 
46
+ `hecks stress_concurrency` fills --runs and --seed-start from the defaults its
47
+ bluebook declares (`hecks stress_concurrency --help` shows them).
48
+
47
49
  Exits 0 if every run's every example passed, 1 if anything failed -
48
50
  failing runs' full output is saved under tmp/stress-failures/
49
51
  for a real repro (same seed, same command, just add CI=true).
@@ -60,10 +62,17 @@ module Hecks
60
62
  # unknown argument
61
63
  def call(argv, root:, out: $stdout)
62
64
  argv = argv.dup
63
- settings = { runs: 30, parallel: Etc.nprocessors, seed_start: 1 }
65
+ settings = { parallel: Etc.nprocessors }
64
66
  status = parse!(argv, settings, out)
65
67
  return status if status
66
68
 
69
+ missing = %i[runs seed_start].reject { |name| settings.key?(name) }
70
+ unless missing.empty?
71
+ warn "missing #{missing.map { |name| "--#{name.to_s.tr('_', '-')}" }.join(', ')}: " \
72
+ "`hecks stress_concurrency` supplies the defaults its bluebook declares (--help for usage)"
73
+ return 64
74
+ end
75
+
67
76
  results = run_all(root, settings, out)
68
77
  report(root, results, out)
69
78
  end
data/lib/hecks/corpus.rb CHANGED
@@ -17,7 +17,7 @@ module Hecks
17
17
  # The Hecks domain (ADR 0080): its chapters share one namespace and one hecksagon, so
18
18
  # they load together, as a directory, never file by file.
19
19
  hecks: "lib/hecks/hecks",
20
- # A package vendored via `uses_embryonaut_bluebook` — nested inside the
20
+ # A package vendored via `attaches ... from: :vendor` — nested inside the
21
21
  # consuming example's own checkout, not this gem's own framework/bluebook/.
22
22
  vendored: "examples/*/vendor/embryonaut_bluebooks/*"
23
23
  }.freeze
@@ -31,7 +31,9 @@ module Hecks
31
31
  qa: "lib/hecks/quality_control/*.bluebook",
32
32
  language: "lib/hecks/language/**/*.bluebook",
33
33
  deploy: "lib/hecks/deploy/bluebook/*.bluebook",
34
+ site: "lib/hecks/site/bluebook/*.bluebook",
34
35
  tenancy: "lib/hecks/tenancy/bluebook/*.bluebook",
36
+ sme: "lib/hecks/sme/bluebook/*.bluebook",
35
37
  fixture: "spec/fixtures/**/*.bluebook"
36
38
  }.freeze
37
39
 
@@ -232,14 +234,14 @@ module Hecks
232
234
  Elsewhere = Struct.new(:check, :destination, :names, :why)
233
235
 
234
236
  RUST_ELSEWHERE = {
235
- "meta" => Elsewhere.new(:named_in, "spec/codegen_parity_spec.rb", "bluebook_language",
237
+ "meta" => Elsewhere.new(:named_in, "lib/hecks/tools/regeneration_run.rb", "rust/src/generated",
236
238
  "the self-hosted grammar (lib/hecks/language), not a domain directory — every " \
237
- "hecks project_rust run rewrites it (so the drift check diffs it), codegen parity " \
238
- "checks it as bluebook_language, and there is no directory to fuzz")
239
+ "hecks project_rust run rewrites it, so the drift check diffs it with the rest " \
240
+ "of rust/src/generated, and there is no directory to fuzz")
239
241
  }.freeze
240
242
 
241
- # Every place a Rust-facing domain's own `uses_framework`/
242
- # `uses_embryonaut_bluebook` attachment could be declared.
243
+ # Every place a Rust-facing domain's own `attaches` (or the deprecated `uses_framework` /
244
+ # `uses_embryonaut_bluebook`) attachment could be declared.
243
245
  #
244
246
  # @param root [String] repository root to search under
245
247
  # @return [String] every reachable hecksagon file's own text, joined by newlines
@@ -307,10 +309,11 @@ module Hecks
307
309
 
308
310
  # The stamp hecks project_rust writes into metadata.rs, e.g.
309
311
  # `examples/pizzas` or `the self-hosted language (...)`, with any
310
- # ` (uses_framework "X")`/` (uses_embryonaut_bluebook "X")` suffix stripped.
312
+ # ` (attaches "X")`/` (attaches "x", from: :vendor)` suffix stripped (the deprecated
313
+ # `uses_framework` / `uses_embryonaut_bluebook` spellings too).
311
314
  SOURCE_STAMP = /
312
315
  GENERATED\ by\ hecks\ project_rust\ —\ (.+?)
313
- (?:\ \((?:uses_framework|uses_embryonaut_bluebook)\ "\w+"\))?
316
+ (?:\ \((?:attaches|uses_framework|uses_embryonaut_bluebook)\ "\w+"(?:,\ from:\ :vendor)?\))?
314
317
  's\ own\ canonical\ IR,
315
318
  /x
316
319