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.
- checksums.yaml +4 -4
- data/exe/hecks +24 -12
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
- data/lib/hecks/bluebook/hexagon.rb +45 -25
- data/lib/hecks/bluebook/model_check.rb +7 -7
- data/lib/hecks/chapters.rb +32 -3
- data/lib/hecks/cli/check_era.rb +1 -1
- data/lib/hecks/cli/deploy_template_diff.rb +1 -1
- data/lib/hecks/cli/doc_coverage.rb +1 -1
- data/lib/hecks/cli/domain_stub.rb +128 -0
- data/lib/hecks/cli/domain_writer.rb +39 -0
- data/lib/hecks/cli/interview_agent.rb +59 -0
- data/lib/hecks/cli/interview_draft.rb +246 -0
- data/lib/hecks/cli/interview_run.rb +87 -0
- data/lib/hecks/cli/interview_session.rb +299 -0
- data/lib/hecks/cli/model_check.rb +1 -1
- data/lib/hecks/cli/project_cli.rb +51 -29
- data/lib/hecks/cli/reference.rb +2 -1
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
- data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
- data/lib/hecks/cli/spec_example.rb +1 -1
- data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
- data/lib/hecks/corpus.rb +12 -8
- data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +4 -3
- data/lib/hecks/doors/cli_door.rb +13 -13
- data/lib/hecks/doors/cli_runner.rb +53 -33
- data/lib/hecks/doors/launcher_options.rb +5 -5
- data/lib/hecks/doors/mcp_door.rb +40 -18
- data/lib/hecks/doors/mcp_door_scope.rb +244 -24
- data/lib/hecks/doors/usage_cache.rb +123 -0
- data/lib/hecks/doors.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -2
- data/lib/hecks/forms/banking_presentation.rb +1 -1
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
- data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
- data/lib/hecks/framework.rb +1 -1
- data/lib/hecks/fuzzing/combination_miner.rb +1 -1
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
- data/lib/hecks/fuzzing/differential.rb +4 -2
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/form_census.rb +2 -2
- data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
- data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
- data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
- data/lib/hecks/gate/stages.yml +5 -5
- data/lib/hecks/grammar/grammar.hecksagon +2 -2
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
- data/lib/hecks/hecks/adapters/finding_github.adapter +7 -0
- data/lib/hecks/hecks/adapters/finding_github.rb +114 -0
- data/lib/hecks/hecks/adapters/host_http.rb +4 -3
- data/lib/hecks/hecks/adapters/local_files.rb +59 -0
- data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
- data/lib/hecks/hecks/adapters/terminal.rb +15 -0
- data/lib/hecks/hecks/codebase.bluebook +4 -3
- data/lib/hecks/hecks/context_map.hecksagon +2 -2
- data/lib/hecks/hecks/custodian.bluebook +81 -3
- data/lib/hecks/hecks/hecks.bluebook +18 -0
- data/lib/hecks/hecks/hecks.hecksagon +16 -4
- data/lib/hecks/hecks/hecks.world +7 -6
- data/lib/hecks/hecks/site.behaviors +17 -0
- data/lib/hecks/hecks/site.world +10 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
- data/lib/hecks/projections/deploy/box/settings.rb +324 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +268 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +68 -0
- data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
- data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +181 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +49 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
- data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +91 -0
- data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +58 -0
- data/lib/hecks/projections/deploy/box.rb +553 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
- data/lib/hecks/projections/deploy/fargate.rb +1 -1
- data/lib/hecks/projections/deploy/lambda.rb +11 -10
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
- data/lib/hecks/projections/diagrams.rb +2 -2
- data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
- data/lib/hecks/projections/site/edge/checks.rb +137 -0
- data/lib/hecks/projections/site/edge/pattern.rb +50 -0
- data/lib/hecks/projections/site/edge/rules.rb +128 -0
- data/lib/hecks/projections/site/edge.rb +136 -0
- data/lib/hecks/projections/site/regions.rb +44 -0
- data/lib/hecks/projections/site/routes_ts.rb +284 -0
- data/lib/hecks/projections/site/site_cdn.rb +78 -0
- data/lib/hecks/projections/site/table/checks.rb +124 -0
- data/lib/hecks/projections/site/table/row_builder.rb +155 -0
- data/lib/hecks/projections/site/table.rb +98 -0
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/cli_projector.rb +153 -111
- data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
- data/lib/hecks/quality_control/adapters/agent.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
- data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
- data/lib/hecks/quality_control/quality_control.bluebook +18 -18
- data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
- data/lib/hecks/runtime/loader.rb +31 -8
- data/lib/hecks/runtime/registry/verification.rb +15 -29
- data/lib/hecks/runtime/registry.rb +14 -20
- data/lib/hecks/rust_build/append_optionals.rb +79 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
- data/lib/hecks/rust_build/domain_name.rb +26 -0
- data/lib/hecks/rust_build/kernel_input.rb +58 -0
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
- data/lib/hecks/rust_build/project_rust.rb +57 -24
- data/lib/hecks/rust_build/rust_literal.rb +33 -0
- data/lib/hecks/rust_build/write_if_changed.rb +86 -0
- data/lib/hecks/site/bluebook/site.bluebook +205 -0
- data/lib/hecks/site/bluebook/site.hecksagon +5 -0
- data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
- data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
- data/lib/hecks/storehouse.rb +37 -7
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
- data/lib/hecks/three_zero/forms.yml +88 -88
- data/lib/hecks/tickets/bluebook/tickets.bluebook +331 -0
- data/lib/hecks/tickets/bluebook/tickets.hecksagon +11 -0
- data/lib/hecks/tickets/bluebook/tickets.ports.hecksagon +26 -0
- data/lib/hecks/tools/ci_gates.rb +1 -1
- data/lib/hecks/tools/deploy_recipe.rb +14 -6
- data/lib/hecks/tools/oidc_manifests.rb +2 -1
- data/lib/hecks/tools/sequence_script.rb +2 -2
- data/lib/hecks/tools/site_routes.rb +152 -0
- data/lib/hecks/tools/tenant_provisioning.rb +1 -1
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +1 -0
- data/rust/build/src/cargo_sync.rs +1 -1
- data/rust/build/src/pipeline.rs +23 -16
- data/rust/build/src/resolve.rs +39 -6
- data/rust/codegen/src/json_codec.rs +3 -0
- data/rust/codegen/src/naming.rs +4 -0
- data/rust/codegen/src/types.rs +45 -0
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +87 -7
- data/rust/host/src/needs.rs +111 -0
- data/rust/parser/src/keywords.rs +7 -6
- data/rust/parser/src/main.rs +4 -3
- data/rust/parser/src/parse/chapter.rs +1 -1
- data/rust/parser/src/parse/hecksagon.rs +25 -10
- data/rust/project/mutations.rb +4 -30
- data/rust/project/naming.rb +8 -28
- data/rust/project/write_if_changed.rb +4 -80
- data/rust/project_rust_pipeline.rb +16 -12
- data/rust/src/kernel/cli.rs +2 -0
- data/rust/src/kernel/needs.rs +74 -1
- 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
|
-
# `
|
|
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
|
|
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
|
-
|
|
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]
|
|
153
|
-
# @param
|
|
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
|
|
157
|
-
def launcher(path, name, program, executable: nil, legacy: 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(
|
|
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(
|
|
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
|
|
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
|
|
193
|
-
# #{where} <
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
274
|
-
# person is at it, so the
|
|
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("
|
|
277
|
-
|
|
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
|
|
282
|
-
def memory_default(
|
|
283
|
-
return "" if
|
|
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
|
-
|
|
305
|
+
commands.each { |command| plain!("memory command", command, VERB) }
|
|
286
306
|
|
|
287
307
|
<<~RUBY
|
|
288
308
|
|
|
289
|
-
# These
|
|
290
|
-
#
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
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 { |
|
|
323
|
+
legacy.each { |command| plain!("legacy command", command, VERB) }
|
|
302
324
|
|
|
303
325
|
<<~RUBY
|
|
304
326
|
|
data/lib/hecks/cli/reference.rb
CHANGED
|
@@ -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
|
|
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
|
|
@@ -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
|
|
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
|
|
17
|
-
# SEED=refusal_kind_lifecycle hecks seed_semantics_corpus
|
|
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
|
|
35
|
-
|
|
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
|
|
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 = {
|
|
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 `
|
|
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, "
|
|
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
|
|
238
|
-
"
|
|
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
|
-
# ` (
|
|
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
|
|