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.
- 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 +31 -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/interview_draft.rb +246 -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 +11 -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 +52 -32
- 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.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/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/codebase.bluebook +4 -3
- data/lib/hecks/hecks/context_map.hecksagon +2 -2
- data/lib/hecks/hecks/custodian.bluebook +45 -3
- data/lib/hecks/hecks/hecks.hecksagon +10 -4
- data/lib/hecks/hecks/hecks.world +6 -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 +248 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +267 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +70 -0
- data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
- data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +165 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
- data/lib/hecks/projections/deploy/box.rb +387 -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 +122 -104
- 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/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/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/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/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 +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
|
-
# `
|
|
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,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, "
|
|
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
|
|
238
|
-
"
|
|
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
|
-
# ` (
|
|
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
|
|