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
@@ -0,0 +1,299 @@
1
+ require_relative "interview_draft"
2
+
3
+ module Hecks
4
+ module CLI
5
+ # One interview, held at a terminal (ADR 0088).
6
+ #
7
+ # hecks drives: it asks the agent for a question, shows it, reads the answer the developer typed
8
+ # back from the expert, records it, asks the agent what the answer names as findings, and puts
9
+ # each to the developer to accept or reject. The agent never dispatches anything. Every SME
10
+ # command is dispatched here, so the domain's own rules are the guard on the interview.
11
+ #
12
+ # With no agent (`--no-ai`), or for a turn the agent could not answer, a plain prompt stands in:
13
+ # a fixed question, and findings typed by the developer.
14
+ class InterviewSession
15
+ NOTICE = "Your answers are sent to a model through your own `claude` login. " \
16
+ "Run with --no-ai to leave it out.".freeze
17
+
18
+ FALLBACK_QUESTIONS = [
19
+ "What is the main thing this business keeps track of?",
20
+ "What happens to it, from the beginning to the end?",
21
+ "What must never happen?",
22
+ "Is there anything else I should know?"
23
+ ].freeze
24
+
25
+ TASK = "You interview a subject matter expert, through the developer who sits beside them, about the " \
26
+ "business domain called %<subject>s, so the developer can model it. Ask one plain-language " \
27
+ "question at a time about what the business keeps track of, what happens to it, and what must " \
28
+ "never happen. Use the expert's own words. Do not assume what kind of business it is, or an " \
29
+ "industry, from the name of the domain: learn it from what the expert says. When you interpret " \
30
+ "an answer, propose findings only with the verbs listed under `verbs`, using the argument " \
31
+ "names given there, and nothing else.".freeze
32
+
33
+ VERBS = {
34
+ "SME::Interview.ProposeThing" => {
35
+ "meaning" => "a thing the business keeps track of, and the field that identifies one of it",
36
+ "arguments" => %w[name identifier]
37
+ },
38
+ "SME::Interview.ProposeAction" => {
39
+ "meaning" => "something that happens to a thing, and the event it announces, in the past tense",
40
+ "arguments" => %w[name thing event creates]
41
+ },
42
+ "SME::Interview.ProposeRule" => {
43
+ "meaning" => "a rule the expert stated, in their own words",
44
+ "arguments" => %w[statement]
45
+ }
46
+ }.freeze
47
+
48
+ # What each finding verb needs, and the SME commands that carry it out and decide it.
49
+ KINDS = {
50
+ "SME::Interview.ProposeThing" => { kind: "thing", fields: %w[name identifier], propose: :propose_thing!,
51
+ entity: "ThingFinding", accept: "AcceptThing", reject: "RejectThing" },
52
+ "SME::Interview.ProposeAction" => { kind: "action", fields: %w[name thing event creates],
53
+ propose: :propose_action!, entity: "ActionFinding",
54
+ accept: "AcceptAction", reject: "RejectAction" },
55
+ "SME::Interview.ProposeRule" => { kind: "rule", fields: %w[statement], propose: :propose_rule!,
56
+ entity: "RuleFinding", accept: "AcceptRule", reject: "RejectRule" }
57
+ }.freeze
58
+
59
+ TRUE_WORDS = %w[true yes y 1].freeze
60
+
61
+ Result = Struct.new(:status, :interview, keyword_init: true)
62
+
63
+ # @param runtime [Runtime] the booted SME chapter
64
+ # @param agent [#question, #proposals, nil] the interviewer; nil runs the plain prompts
65
+ # @param input [#gets] where the developer types
66
+ # @param output [#puts, #print] where the session speaks
67
+ # @param reference [String] the interview's reference, such as `INT-1`
68
+ # @param subject [String] the domain being discovered, as its bluebook will spell it
69
+ # @param expert [String] who is being interviewed
70
+ def initialize(runtime:, agent:, input:, output:, reference:, subject:, expert:)
71
+ @runtime = runtime
72
+ @agent = agent
73
+ @input = input
74
+ @output = output
75
+ @reference = reference
76
+ @subject = subject
77
+ @expert = expert
78
+ @asked = []
79
+ @number = 0
80
+ @suggested = false
81
+ end
82
+
83
+ # Holds the interview until the developer finishes or quits.
84
+ #
85
+ # @return [Result] `concluded` with the plain interview (see `InterviewDraft`), or `cancelled`
86
+ def call
87
+ ::Interview.plan!(reference: @reference, subject: @subject, expert: @expert).begin!
88
+ say("#{NOTICE}\n") if @agent
89
+ say("Type done when there is enough to start, or quit to stop and write nothing.\n")
90
+ turn until finished?
91
+ outcome
92
+ end
93
+
94
+ private
95
+
96
+ def outcome
97
+ return Result.new(status: :cancelled) if @cancelled
98
+
99
+ Result.new(status: :concluded, interview: InterviewDraft.from_record(current))
100
+ end
101
+
102
+ def finished? = @finished
103
+
104
+ def current = ::Interview.find(@reference)
105
+
106
+ def turn
107
+ question = next_question
108
+ say("\n#{question}")
109
+ answer = ask("> ")
110
+ return end_of_input if answer.nil?
111
+
112
+ case answer.strip.downcase
113
+ when "" then say("Say what the expert said, or type done.")
114
+ when "done" then conclude
115
+ when "quit" then cancel
116
+ else answered(question, answer.strip)
117
+ end
118
+ end
119
+
120
+ def answered(question, answer)
121
+ current.record!(question: question, answer: answer)
122
+ @asked << question
123
+ findings_from(answer)
124
+ suggest_enough
125
+ end
126
+
127
+ def next_question
128
+ asked = @asked.dup
129
+ proposed = @agent && ask_agent { @agent.question(state: state, asked: asked) }
130
+ text = proposed&.text
131
+ return text if text && !@asked.include?(text)
132
+
133
+ FALLBACK_QUESTIONS.find { |q| !@asked.include?(q) } || FALLBACK_QUESTIONS.last
134
+ end
135
+
136
+ def findings_from(answer)
137
+ proposals = @agent && ask_agent { @agent.proposals(prose: answer, state: state) }
138
+ return manual_findings unless proposals
139
+
140
+ proposals.each { |proposal| review(proposal) }
141
+ end
142
+
143
+ # Runs one call to the agent; a turn it cannot answer is said aloud, and a plain one follows.
144
+ def ask_agent
145
+ yield
146
+ rescue Ports::Agent::Unavailable, Ports::Agent::ValidationError => e
147
+ say("The AI could not answer (#{e.message.lines.first.to_s.strip}). Using a plain prompt for this turn.")
148
+ nil
149
+ end
150
+
151
+ def review(proposal)
152
+ config = KINDS[proposal.verb]
153
+ return say("Ignored a proposal for #{proposal.verb}: not a finding I know.") unless config
154
+
155
+ fields = fields_of(proposal, config)
156
+ missing = config[:fields].reject { |f| f == "creates" || fields[f].to_s.strip != "" }
157
+ return say("Ignored a #{config[:kind]} proposal with no #{missing.join(', ')}.") unless missing.empty?
158
+
159
+ number = propose(config, fields) or return
160
+ say(" Proposed #{describe(config, fields)}")
161
+ say(" because: #{proposal.rationale}")
162
+ decide(config, number, accept?(" Accept? [Y/n] "))
163
+ end
164
+
165
+ def fields_of(proposal, config)
166
+ rows = proposal.arguments.to_h { |row| [row[:name].to_s, row[:value]] }
167
+ fields = rows.slice(*config[:fields])
168
+ fields["creates"] = TRUE_WORDS.include?(fields["creates"].to_s.downcase) if fields.key?("creates")
169
+ fields
170
+ end
171
+
172
+ def propose(config, fields)
173
+ number = (@number += 1)
174
+ args = fields.transform_keys(&:to_sym).merge(number: number, source: current.exchanges.size)
175
+ current.public_send(config[:propose], **args)
176
+ number
177
+ rescue StandardError => e
178
+ say(" Could not keep that finding: #{reason(e)}")
179
+ nil
180
+ end
181
+
182
+ def decide(config, number, accepted)
183
+ verb = accepted ? config[:accept] : config[:reject]
184
+ @runtime.dispatch_flat("SME::Interview.#{config[:entity]}.#{verb}",
185
+ reference: { value: @reference }, number: { value: number })
186
+ say(accepted ? " Accepted." : " Rejected.")
187
+ rescue StandardError => e
188
+ say(" Could not record the decision: #{reason(e)}")
189
+ end
190
+
191
+ def describe(config, fields)
192
+ case config[:kind]
193
+ when "thing" then "thing: #{fields['name']}, identified by #{fields['identifier']}"
194
+ when "action" then "action: #{fields['name']} on #{fields['thing']}, announcing #{fields['event']}" \
195
+ "#{', creating it' if fields['creates']}"
196
+ else "rule: #{fields['statement']}"
197
+ end
198
+ end
199
+
200
+ # The plain prompt: findings typed by the developer, accepted as they are entered.
201
+ def manual_findings
202
+ loop do
203
+ kind = ask("Add a finding from that answer? thing, action or rule (enter to skip): ")
204
+ kind = kind.to_s.strip.downcase
205
+ break if kind.empty?
206
+
207
+ typed = typed_finding(kind) or next say(" I only know thing, action and rule.")
208
+ config = KINDS["SME::Interview.Propose#{kind.capitalize}"]
209
+ number = propose(config, typed) or next
210
+ decide(config, number, true)
211
+ end
212
+ end
213
+
214
+ def typed_finding(kind)
215
+ case kind
216
+ when "thing" then named("Name" => "name", "Identified by" => "identifier")
217
+ when "action" then typed_action
218
+ when "rule" then named("The rule, in the expert's words" => "statement")
219
+ end
220
+ end
221
+
222
+ def typed_action
223
+ fields = named("Name" => "name", "On which thing" => "thing", "It announces (event)" => "event") or return nil
224
+ fields.merge("creates" => TRUE_WORDS.include?(ask(" Does it create the thing? [y/N] ").to_s.strip.downcase))
225
+ end
226
+
227
+ def named(prompts)
228
+ fields = prompts.to_h { |label, key| [key, ask(" #{label}: ").to_s.strip] }
229
+ fields.values.all? { |v| !v.empty? } ? fields : nil
230
+ end
231
+
232
+ def suggest_enough
233
+ return if @suggested || !enough?
234
+
235
+ @suggested = true
236
+ say("\nI think we have enough to start. Type done to finish, or keep answering.")
237
+ end
238
+
239
+ def enough?
240
+ plain = InterviewDraft.from_record(current)
241
+ InterviewDraft.accepted(plain, :things).any? && InterviewDraft.accepted(plain, :actions).any?
242
+ end
243
+
244
+ def conclude
245
+ current.conclude!
246
+ @finished = true
247
+ rescue StandardError => e
248
+ say("Not finished yet: #{reason(e)}.")
249
+ end
250
+
251
+ def cancel
252
+ current.cancel!
253
+ @cancelled = @finished = true
254
+ say("Stopped. Nothing was written.")
255
+ end
256
+
257
+ def end_of_input
258
+ conclude
259
+ cancel unless @finished
260
+ end
261
+
262
+ def accept?(label)
263
+ !%w[n no].include?(ask(label).to_s.strip.downcase)
264
+ end
265
+
266
+ def state
267
+ plain = InterviewDraft.from_record(current)
268
+ { task: format(TASK, subject: @subject), subject: @subject, expert: @expert, verbs: VERBS,
269
+ exchanges: plain[:exchanges].each_with_index.map { |e, i| e.slice(:question, :answer).merge(number: i + 1) },
270
+ accepted: accepted_state(plain), gaps: gaps(plain) }
271
+ end
272
+
273
+ def accepted_state(plain)
274
+ { things: InterviewDraft.accepted(plain, :things).map { |f| f.slice(:name, :identifier) },
275
+ actions: InterviewDraft.accepted(plain, :actions).map { |f| f.slice(:name, :thing, :event, :creates) },
276
+ rules: InterviewDraft.accepted(plain, :rules).map { |f| f.slice(:statement) } }
277
+ end
278
+
279
+ # What the interview has not yet pinned down, in words a model can act on.
280
+ def gaps(plain)
281
+ things = InterviewDraft.accepted(plain, :things).map { |t| t[:name].to_s }
282
+ actions = InterviewDraft.accepted(plain, :actions)
283
+ gaps = things.reject { |t| actions.any? { |a| a[:thing] == t } }.map { |t| "nothing is yet said to happen to #{t}" }
284
+ gaps += things.reject { |t| actions.any? { |a| a[:thing] == t && a[:creates] } }.map { |t| "nothing yet creates #{t}" }
285
+ unplaced = actions.reject { |a| things.include?(a[:thing].to_s) }
286
+ gaps + unplaced.map { |a| "#{a[:name]} names #{a[:thing]}, not yet a thing" }
287
+ end
288
+
289
+ def reason(error) = error.message.sub(/\A[A-Z]\w* refused\s+[—-]\s+/, "").strip
290
+
291
+ def ask(label)
292
+ @output.print(label)
293
+ @input.gets&.chomp
294
+ end
295
+
296
+ def say(text) = @output.puts(text)
297
+ end
298
+ end
299
+ 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,10 @@ 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",
35
+ tickets: "lib/hecks/tickets/bluebook/*.bluebook",
34
36
  tenancy: "lib/hecks/tenancy/bluebook/*.bluebook",
37
+ sme: "lib/hecks/sme/bluebook/*.bluebook",
35
38
  fixture: "spec/fixtures/**/*.bluebook"
36
39
  }.freeze
37
40
 
@@ -232,14 +235,14 @@ module Hecks
232
235
  Elsewhere = Struct.new(:check, :destination, :names, :why)
233
236
 
234
237
  RUST_ELSEWHERE = {
235
- "meta" => Elsewhere.new(:named_in, "spec/codegen_parity_spec.rb", "bluebook_language",
238
+ "meta" => Elsewhere.new(:named_in, "lib/hecks/tools/regeneration_run.rb", "rust/src/generated",
236
239
  "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")
240
+ "hecks project_rust run rewrites it, so the drift check diffs it with the rest " \
241
+ "of rust/src/generated, and there is no directory to fuzz")
239
242
  }.freeze
240
243
 
241
- # Every place a Rust-facing domain's own `uses_framework`/
242
- # `uses_embryonaut_bluebook` attachment could be declared.
244
+ # Every place a Rust-facing domain's own `attaches` (or the deprecated `uses_framework` /
245
+ # `uses_embryonaut_bluebook`) attachment could be declared.
243
246
  #
244
247
  # @param root [String] repository root to search under
245
248
  # @return [String] every reachable hecksagon file's own text, joined by newlines
@@ -307,10 +310,11 @@ module Hecks
307
310
 
308
311
  # The stamp hecks project_rust writes into metadata.rs, e.g.
309
312
  # `examples/pizzas` or `the self-hosted language (...)`, with any
310
- # ` (uses_framework "X")`/` (uses_embryonaut_bluebook "X")` suffix stripped.
313
+ # ` (attaches "X")`/` (attaches "x", from: :vendor)` suffix stripped (the deprecated
314
+ # `uses_framework` / `uses_embryonaut_bluebook` spellings too).
311
315
  SOURCE_STAMP = /
312
316
  GENERATED\ by\ hecks\ project_rust\ —\ (.+?)
313
- (?:\ \((?:uses_framework|uses_embryonaut_bluebook)\ "\w+"\))?
317
+ (?:\ \((?:attaches|uses_framework|uses_embryonaut_bluebook)\ "\w+"(?:,\ from:\ :vendor)?\))?
314
318
  's\ own\ canonical\ IR,
315
319
  /x
316
320