hecks 3.2.0 → 3.2.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/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
- data/lib/hecks/cli/check_era.rb +3 -39
- data/lib/hecks/doors/mcp_door.rb +16 -2
- data/lib/hecks/doors/mcp_door_scope.rb +43 -4
- data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
- data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
- data/lib/hecks/hecks/adapters/site_toolchain.rb +21 -11
- data/lib/hecks/language/bluebook/vocabulary.bluebook +7 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
- data/lib/hecks/projections/deploy/box/settings.rb +23 -2
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
- data/lib/hecks/projections/deploy/box.rb +13 -2
- data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
- data/lib/hecks/projections/site/edge/checks.rb +31 -5
- data/lib/hecks/projections/site/edge.rb +25 -11
- data/lib/hecks/projections/site/routes_ts.rb +50 -27
- data/lib/hecks/projections/site/site_cdn.rb +7 -4
- data/lib/hecks/projections/site/table/checks.rb +53 -7
- data/lib/hecks/projections/site/table/link_builder.rb +64 -0
- data/lib/hecks/projections/site/table/row_builder.rb +21 -6
- data/lib/hecks/projections/site/table.rb +16 -5
- data/lib/hecks/rust_build/kernel_input.rb +18 -6
- data/lib/hecks/rust_build/project_rust.rb +6 -132
- data/lib/hecks/site/bluebook/site.bluebook +27 -13
- data/lib/hecks/storehouse.rb +35 -1
- data/lib/hecks/tools/comment_style.rb +4 -2
- data/lib/hecks/tools/rust_comment_style.rb +1 -1
- data/lib/hecks/tools/site_routes.rb +69 -28
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -1
- data/lib/hecks.rb +1 -0
- data/rust/build/src/lineage_pass.rs +1 -1
- data/rust/build/src/pipeline.rs +1 -1
- data/rust/codegen/src/attr.rs +1 -1
- data/rust/codegen/src/bridging.rs +1 -1
- data/rust/codegen/src/commands.rs +1 -1
- data/rust/codegen/src/constraints.rs +1 -1
- data/rust/codegen/src/dependency_planning.rs +1 -1
- data/rust/codegen/src/domain_generator.rs +2 -2
- data/rust/codegen/src/exemplar.rs +1 -1
- data/rust/codegen/src/expr_emitter.rs +1 -1
- data/rust/codegen/src/fielded.rs +1 -1
- data/rust/codegen/src/json_codec.rs +1 -1
- data/rust/codegen/src/main.rs +3 -4
- data/rust/codegen/src/mutations.rs +1 -1
- data/rust/codegen/src/naming.rs +1 -1
- data/rust/codegen/src/ports.rs +1 -1
- data/rust/codegen/src/queries.rs +3 -3
- data/rust/codegen/src/reactions.rs +1 -1
- data/rust/codegen/src/read_models.rs +2 -2
- data/rust/codegen/src/reference_specs.rs +1 -1
- data/rust/codegen/src/registry.rs +4 -4
- data/rust/codegen/src/shared.rs +1 -1
- data/rust/codegen/src/skip_reason.rs +1 -1
- data/rust/codegen/src/types.rs +4 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/needs.rs +94 -27
- data/rust/src/exemplar/commands.rs +1 -1
- data/rust/src/exemplar/constraints.rs +1 -1
- data/rust/src/exemplar/fielded.rs +1 -1
- data/rust/src/exemplar/json.rs +1 -1
- data/rust/src/exemplar/mod.rs +1 -1
- data/rust/src/exemplar/mutations.rs +1 -1
- data/rust/src/exemplar/queries.rs +1 -1
- data/rust/src/exemplar/reactions.rs +1 -1
- data/rust/src/exemplar/read_models.rs +1 -1
- data/rust/src/exemplar/registry.rs +1 -1
- data/rust/src/exemplar/types.rs +1 -1
- data/rust/src/kernel/cli.rs +2 -2
- data/rust/src/kernel/read_model.rs +1 -1
- data/rust/src/lib.rs +1 -1
- metadata +3 -23
- data/rust/project/bridging.rb +0 -257
- data/rust/project/commands.rb +0 -877
- data/rust/project/constraints.rb +0 -86
- data/rust/project/dependency_planning.rb +0 -178
- data/rust/project/domain_generator.rb +0 -983
- data/rust/project/exemplar.rb +0 -148
- data/rust/project/expr_emitter.rb +0 -72
- data/rust/project/fielded.rb +0 -141
- data/rust/project/json_codec.rb +0 -641
- data/rust/project/mutations.rb +0 -695
- data/rust/project/naming.rb +0 -223
- data/rust/project/ports.rb +0 -65
- data/rust/project/queries.rb +0 -493
- data/rust/project/reactions.rb +0 -317
- data/rust/project/read_models.rb +0 -690
- data/rust/project/reference_specs.rb +0 -27
- data/rust/project/registry.rb +0 -396
- data/rust/project/skip_reason.rb +0 -29
- data/rust/project/types.rb +0 -225
- data/rust/project/write_if_changed.rb +0 -7
- data/rust/project.rb +0 -30
- data/rust/project_rust_pipeline.rb +0 -391
|
@@ -30,7 +30,8 @@ module Hecks
|
|
|
30
30
|
# @return [String] the input as JSON, ready for the binary's stdin
|
|
31
31
|
def json(domain_path, steps) = JSON.generate(build(domain_path, steps))
|
|
32
32
|
|
|
33
|
-
# Each command's declared defaults, read from the domain's
|
|
33
|
+
# Each command's declared defaults, entity commands included, read from the domain's
|
|
34
|
+
# generated IR. An entity's commands sit under `Domain::Aggregate.Entity[.Entity].Command`.
|
|
34
35
|
#
|
|
35
36
|
# @param domain_path [String] the domain's directory
|
|
36
37
|
# @return [Hash{String => Hash{String => Object}}] empty when the IR is absent or has none
|
|
@@ -38,14 +39,25 @@ module Hecks
|
|
|
38
39
|
ir = generated_ir(domain_path) or return {}
|
|
39
40
|
domain = ir["name"].to_s
|
|
40
41
|
Array(ir["aggregates"]).each_with_object({}) do |aggregate, table|
|
|
41
|
-
|
|
42
|
-
held = Array(command["attributes"]).reject { |attribute| attribute["default"].nil? }
|
|
43
|
-
.to_h { |attribute| [attribute["name"], attribute["default"]] }
|
|
44
|
-
table["#{domain}::#{aggregate['name']}.#{command['name']}"] = held unless held.empty?
|
|
45
|
-
end
|
|
42
|
+
collect_defaults(aggregate, "#{domain}::#{aggregate['name']}", table)
|
|
46
43
|
end
|
|
47
44
|
end
|
|
48
45
|
|
|
46
|
+
# Adds the defaults of `node`'s commands, then of each entity nested in it, under `prefix`.
|
|
47
|
+
#
|
|
48
|
+
# @param node [Hash] an aggregate or entity of the IR
|
|
49
|
+
# @param prefix [String] the verb path down to `node`
|
|
50
|
+
# @param table [Hash] filled in place
|
|
51
|
+
# @return [void]
|
|
52
|
+
def collect_defaults(node, prefix, table)
|
|
53
|
+
Array(node["commands"]).each do |command|
|
|
54
|
+
held = Array(command["attributes"]).reject { |attribute| attribute["default"].nil? }
|
|
55
|
+
.to_h { |attribute| [attribute["name"], attribute["default"]] }
|
|
56
|
+
table["#{prefix}.#{command['name']}"] = held unless held.empty?
|
|
57
|
+
end
|
|
58
|
+
Array(node["entities"]).each { |entity| collect_defaults(entity, "#{prefix}.#{entity['name']}", table) }
|
|
59
|
+
end
|
|
60
|
+
|
|
49
61
|
# @param domain_path [String] the domain's directory
|
|
50
62
|
# @return [Hash, nil] the parsed generated IR, or nil when the domain has none
|
|
51
63
|
def generated_ir(domain_path)
|
|
@@ -17,9 +17,8 @@ module Hecks
|
|
|
17
17
|
# ProjectRust.call(["path/to/domain"])
|
|
18
18
|
#
|
|
19
19
|
# `hecks-codegen` is the generator (ADR 0086): this class builds the IR from the live registry
|
|
20
|
-
# and `CodegenRun` runs the binary on it.
|
|
21
|
-
#
|
|
22
|
-
# `rust/project_rust_pipeline.rb` instead, with no Ruby load of the domain.
|
|
20
|
+
# and `CodegenRun` runs the binary on it. The Ruby-free path is `hecks-build`, which feeds the
|
|
21
|
+
# same binary from `hecks-parse`.
|
|
23
22
|
#
|
|
24
23
|
# The era plugin is required unconditionally so the generator works for any domain, lineage
|
|
25
24
|
# capable or not; `pg` stays lazy inside it, so nothing here opens a database connection.
|
|
@@ -46,13 +45,7 @@ module Hecks
|
|
|
46
45
|
# @return [Integer] the exit status
|
|
47
46
|
def call
|
|
48
47
|
validate_name!
|
|
49
|
-
|
|
50
|
-
require_relative "../../../rust/project"
|
|
51
|
-
require_relative "../../../rust/project_rust_pipeline"
|
|
52
|
-
RustProjectPipeline.call(@domain)
|
|
53
|
-
else
|
|
54
|
-
generate
|
|
55
|
-
end
|
|
48
|
+
generate
|
|
56
49
|
0
|
|
57
50
|
end
|
|
58
51
|
|
|
@@ -78,18 +71,11 @@ module Hecks
|
|
|
78
71
|
FileUtils.mkdir_p(@out_root)
|
|
79
72
|
build_target_ir
|
|
80
73
|
FileUtils.rm_rf(File.join(@out_root, "active"))
|
|
81
|
-
|
|
74
|
+
generate_with_codegen
|
|
82
75
|
write_root_mod
|
|
83
76
|
sync_cargo_features
|
|
84
77
|
end
|
|
85
78
|
|
|
86
|
-
# The rollback path (ADR 0086): the Ruby generator in `rust/project`, until it is deleted.
|
|
87
|
-
def generate_with_ruby
|
|
88
|
-
require_relative "../../../rust/project"
|
|
89
|
-
write_meta
|
|
90
|
-
write_target
|
|
91
|
-
end
|
|
92
|
-
|
|
93
79
|
# `hecks-codegen` is the generator; this process only builds the IR it reads from the live
|
|
94
80
|
# registry, so a domain's persistence, seams, translations and source text reach `ir.json`.
|
|
95
81
|
def generate_with_codegen
|
|
@@ -105,8 +91,8 @@ module Hecks
|
|
|
105
91
|
).call
|
|
106
92
|
end
|
|
107
93
|
|
|
108
|
-
# The IR as
|
|
109
|
-
#
|
|
94
|
+
# The IR as the generator reads it: string-keyed through JSON, with the append-optional
|
|
95
|
+
# fields marked, since `ir.json` records them.
|
|
110
96
|
def prepared_ir(tree, shaped: true)
|
|
111
97
|
AppendOptionals.mark(shaped ? json_shaped(tree) : tree)
|
|
112
98
|
end
|
|
@@ -186,78 +172,6 @@ module Hecks
|
|
|
186
172
|
)
|
|
187
173
|
end
|
|
188
174
|
|
|
189
|
-
# The self-hosted language goes through the same codegen as the target, read from the
|
|
190
|
-
# grammar registry `MetaValidator` boots with.
|
|
191
|
-
def write_meta
|
|
192
|
-
meta_ir = json_shaped(
|
|
193
|
-
Hecks::Projector::Exporter.call(Hecks::Bluebook::MetaValidator.grammar_registry).fetch("Bluebook")
|
|
194
|
-
)
|
|
195
|
-
dir = File.join(@out_root, "meta")
|
|
196
|
-
# Tracked per directory: several domains coexist on disk, and `pop_and_prune` deletes only
|
|
197
|
-
# the files this run never touched. `active` is a set of feature re-exports in the root
|
|
198
|
-
# `mod.rs`, never a directory.
|
|
199
|
-
WriteIfChanged.push_directory(dir)
|
|
200
|
-
result = RustProjection::DomainGenerator.call(
|
|
201
|
-
meta_ir, "the self-hosted language (lib/hecks/language/bluebook)", dir, "meta"
|
|
202
|
-
)
|
|
203
|
-
write_merged(File.join(dir, "merged.rs"), result[:aggregates], result[:queries], result[:read_models],
|
|
204
|
-
policies: RustProjection::Projector.emit_policy_table(meta_ir[:name], meta_ir[:policies],
|
|
205
|
-
meta_ir[:aggregates]),
|
|
206
|
-
cross: RustProjection::Projector.emit_cross_domain_policy_table(meta_ir[:name],
|
|
207
|
-
meta_ir[:policies]),
|
|
208
|
-
managers: meta_ir[:process_managers],
|
|
209
|
-
keys: [[meta_ir[:name], result[:aggregates].map { |a| a[:name] }]])
|
|
210
|
-
append_merged_mod(File.join(dir, "mod.rs"))
|
|
211
|
-
WriteIfChanged.pop_and_prune(dir)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def write_target
|
|
215
|
-
dir = File.join(@out_root, @mod_name)
|
|
216
|
-
WriteIfChanged.push_directory(dir)
|
|
217
|
-
result = RustProjection::DomainGenerator.call(@ir, @domain, dir, @mod_name)
|
|
218
|
-
chapters = write_chapters
|
|
219
|
-
aggregates = result[:aggregates] + chapters[:aggregates]
|
|
220
|
-
write_merged(File.join(dir, "merged.rs"), aggregates, result[:queries] + chapters[:queries],
|
|
221
|
-
result[:read_models] + chapters[:read_models],
|
|
222
|
-
policies: RustProjection::Projector.emit_merged_policy_table(chapters[:policy_sources]),
|
|
223
|
-
cross: RustProjection::Projector.emit_merged_cross_domain_policy_table(chapters[:policy_sources]),
|
|
224
|
-
managers: @ir[:process_managers] + chapters[:process_managers],
|
|
225
|
-
keys: reference_keys(result[:aggregates], chapters))
|
|
226
|
-
append_merged_mod(File.join(dir, "mod.rs"))
|
|
227
|
-
WriteIfChanged.pop_and_prune(dir)
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
# Every other bluebook the target's `attaches` calls pulled into the registry, each
|
|
231
|
-
# generated into its own module. A framework chapter is a function of its bluebook alone, so
|
|
232
|
-
# regenerating one is safe whichever domain last touched it.
|
|
233
|
-
def write_chapters
|
|
234
|
-
totals = { aggregates: [], queries: [], read_models: [], process_managers: [],
|
|
235
|
-
policy_sources: [{ domain_name: @domain_name, policies: @ir[:policies],
|
|
236
|
-
aggregates: @ir[:aggregates] }],
|
|
237
|
-
mod_names: { @mod_name => @domain_name } }
|
|
238
|
-
vendored = @registry.hecksagons.values.flat_map(&:vendored_packages)
|
|
239
|
-
(@registry.bluebooks.keys - [@domain_name]).each { |chapter| write_chapter(chapter, vendored, totals) }
|
|
240
|
-
totals
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def write_chapter(chapter_name, vendored, totals)
|
|
244
|
-
mod_name = chapter_name.downcase
|
|
245
|
-
totals[:mod_names][mod_name] = chapter_name
|
|
246
|
-
chapter_ir = json_shaped(Hecks::Projector::Exporter.call(@registry).fetch(chapter_name))
|
|
247
|
-
result = nil
|
|
248
|
-
WriteIfChanged.track_directory(File.join(@out_root, mod_name)) do
|
|
249
|
-
result = RustProjection::DomainGenerator.call(chapter_ir, "#{@domain} (#{attachment(chapter_name, vendored)})",
|
|
250
|
-
File.join(@out_root, mod_name), mod_name,
|
|
251
|
-
merged_module: false)
|
|
252
|
-
end
|
|
253
|
-
totals[:aggregates].concat(result[:aggregates])
|
|
254
|
-
totals[:queries].concat(result[:queries])
|
|
255
|
-
totals[:read_models].concat(result[:read_models])
|
|
256
|
-
totals[:process_managers].concat(chapter_ir[:process_managers])
|
|
257
|
-
totals[:policy_sources] << { domain_name: chapter_name, policies: chapter_ir[:policies],
|
|
258
|
-
aggregates: chapter_ir[:aggregates] }
|
|
259
|
-
end
|
|
260
|
-
|
|
261
175
|
# How the target attached a chapter, across every hecksagon the registry loaded: a chapter a
|
|
262
176
|
# sibling hecksagon vendors in is labelled as vendored.
|
|
263
177
|
def attachment(chapter_name, vendored)
|
|
@@ -265,46 +179,6 @@ module Hecks
|
|
|
265
179
|
name ? "attaches #{name.inspect}, from: :vendor" : "attaches #{chapter_name.inspect}"
|
|
266
180
|
end
|
|
267
181
|
|
|
268
|
-
def reference_keys(target_aggregates, chapters)
|
|
269
|
-
chapters[:mod_names].map do |mod_name, chapter_name|
|
|
270
|
-
own = mod_name == @mod_name ? target_aggregates : chapters[:aggregates].select { |a| a[:chapter_mod] == mod_name }
|
|
271
|
-
[chapter_name, own.map { |a| a[:name] }]
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
# Writes a module's `merged.rs`: the registry and every dispatch table over its aggregates.
|
|
276
|
-
def write_merged(path, aggregates, queries, read_models, policies:, cross:, managers:, keys:)
|
|
277
|
-
projector = RustProjection::Projector
|
|
278
|
-
wrote = WriteIfChanged.block(path) do |file|
|
|
279
|
-
[projector.emit_registry(aggregates), projector.emit_reference_lookup(aggregates), policies, cross,
|
|
280
|
-
projector.emit_process_manager_table(managers), projector.emit_reference_key_table(keys),
|
|
281
|
-
projector.emit_creates_table(aggregates), projector.emit_identity_head_table(aggregates),
|
|
282
|
-
projector.emit_entity_identity_head_table(aggregates),
|
|
283
|
-
projector.emit_command_attributes_table(aggregates), projector.emit_query_table(queries),
|
|
284
|
-
projector.emit_query_arg_check_table(queries)].each do |table|
|
|
285
|
-
file.puts table
|
|
286
|
-
file.puts
|
|
287
|
-
end
|
|
288
|
-
# A read model's `group_by` function is written before the table names it.
|
|
289
|
-
read_models.each do |model|
|
|
290
|
-
next unless model[:group_by_fn_body]
|
|
291
|
-
|
|
292
|
-
file.puts model[:group_by_fn_body]
|
|
293
|
-
file.puts
|
|
294
|
-
end
|
|
295
|
-
file.puts projector.emit_read_model_table(read_models)
|
|
296
|
-
end
|
|
297
|
-
puts(wrote ? "wrote #{path}" : "#{path} unchanged")
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
# Appends the `pub mod merged;` line once; appending it again would grow the file and bump
|
|
301
|
-
# its mtime on every run.
|
|
302
|
-
def append_merged_mod(path)
|
|
303
|
-
return if File.exist?(path) && File.read(path).include?("pub mod merged;")
|
|
304
|
-
|
|
305
|
-
File.open(path, "a") { |file| file.puts "pub mod merged;" }
|
|
306
|
-
end
|
|
307
|
-
|
|
308
182
|
# `src/generated/mod.rs`: every domain ever generated, scanned off disk. A directory is a
|
|
309
183
|
# selectable domain exactly when it carries its own `merged.rs`; a framework chapter has
|
|
310
184
|
# none and is always compiled in.
|
|
@@ -95,12 +95,14 @@ Hecks.bluebook "Site" do
|
|
|
95
95
|
aggregate "SiteProjection" do
|
|
96
96
|
description "One projection of a site's route table into its routes.ts, or the check that the file on disk is current."
|
|
97
97
|
|
|
98
|
-
attribute :domain,
|
|
99
|
-
attribute :run,
|
|
100
|
-
attribute :out,
|
|
101
|
-
attribute :
|
|
102
|
-
attribute :
|
|
103
|
-
attribute :
|
|
98
|
+
attribute :domain, DomainPath
|
|
99
|
+
attribute :run, RunKey
|
|
100
|
+
attribute :out, Location, optional: true
|
|
101
|
+
attribute :template, Location, optional: true
|
|
102
|
+
attribute :extension, Extension, optional: true
|
|
103
|
+
attribute :check, Switch, optional: true
|
|
104
|
+
attribute :output, Report, optional: true
|
|
105
|
+
attribute :refusal, Report, optional: true
|
|
104
106
|
identified_by :run
|
|
105
107
|
|
|
106
108
|
value_object "RunKey" do
|
|
@@ -116,11 +118,18 @@ Hecks.bluebook "Site" do
|
|
|
116
118
|
invariant("a project is named by its path") { !value.to_s.empty? }
|
|
117
119
|
end
|
|
118
120
|
|
|
119
|
-
#
|
|
121
|
+
# A directory or file anywhere on disk, relative to where the command runs: where routes.ts is
|
|
122
|
+
# written when not in the project's generated/ directory, or the template to rewrite in place.
|
|
120
123
|
value_object "Location" do
|
|
121
124
|
attribute :value, String, pattern: '[^\t\n\r]'
|
|
122
125
|
end
|
|
123
126
|
|
|
127
|
+
# The file extension of the generated module: `ts` (routes.ts), or `mts` (routes.mts), which
|
|
128
|
+
# Node loads as an ES module even in a package whose "type" is "commonjs".
|
|
129
|
+
value_object "Extension" do
|
|
130
|
+
attribute :value, String, one_of: ["ts", "mts"]
|
|
131
|
+
end
|
|
132
|
+
|
|
124
133
|
value_object "Switch" do
|
|
125
134
|
attribute :value, Boolean
|
|
126
135
|
end
|
|
@@ -134,19 +143,24 @@ Hecks.bluebook "Site" do
|
|
|
134
143
|
transition "Fault" => "faulted", from: "requested"
|
|
135
144
|
end
|
|
136
145
|
|
|
137
|
-
# `hecks site site_projection.project_site <project> [out=<dir>] [--check]`.
|
|
138
|
-
# routes.ts that differs from the table is faulted, naming the
|
|
146
|
+
# `hecks site site_projection.project_site <project> [out=<dir>] [template=<file>] [extension=ts|mts] [--check]`.
|
|
147
|
+
# With `check` nothing is written: a routes.ts that differs from the table is faulted, naming the
|
|
148
|
+
# file, so a CI job fails on drift.
|
|
139
149
|
command "ProjectSite" do
|
|
140
150
|
goal "Project a site's route table into routes.ts, or with check only compare it with the file on disk"
|
|
141
151
|
|
|
142
|
-
attribute :domain,
|
|
143
|
-
attribute :run,
|
|
144
|
-
attribute :out,
|
|
145
|
-
attribute :
|
|
152
|
+
attribute :domain, DomainPath
|
|
153
|
+
attribute :run, RunKey
|
|
154
|
+
attribute :out, Location, optional: true
|
|
155
|
+
attribute :template, Location, optional: true
|
|
156
|
+
attribute :extension, Extension, optional: true
|
|
157
|
+
attribute :check, Switch, optional: true
|
|
146
158
|
|
|
147
159
|
sets :domain
|
|
148
160
|
sets :run
|
|
149
161
|
sets :out
|
|
162
|
+
sets :template
|
|
163
|
+
sets :extension
|
|
150
164
|
sets :check
|
|
151
165
|
|
|
152
166
|
emits "SiteProjectionRequested"
|
data/lib/hecks/storehouse.rb
CHANGED
|
@@ -230,10 +230,44 @@ module Hecks
|
|
|
230
230
|
result = runtime.dispatch_flat(spec[:command], envelope)
|
|
231
231
|
ok(summary: summary,
|
|
232
232
|
id: result.id,
|
|
233
|
-
state:
|
|
233
|
+
state: settled_state(runtime, spec, result),
|
|
234
234
|
events: result.events.map { |event| { name: event.name, payload: Doors::JsonDoor.materialize(event.payload) } })
|
|
235
235
|
end
|
|
236
236
|
|
|
237
|
+
# The record as its repository holds it once every reaction has run, which is the state the
|
|
238
|
+
# dispatch resulted in; the handle's own state is the record as the command left it, so a run
|
|
239
|
+
# record that a reaction completes would otherwise read as `requested`. Falls back to the
|
|
240
|
+
# handle's state for a command that belongs to no top-level aggregate or whose record cannot be
|
|
241
|
+
# read.
|
|
242
|
+
# :nodoc:
|
|
243
|
+
def settled_state(runtime, spec, result)
|
|
244
|
+
return if result.state.nil?
|
|
245
|
+
|
|
246
|
+
bluebook = bluebook_for(runtime)
|
|
247
|
+
head = spec[:command].split("::", 2).last
|
|
248
|
+
aggregate = bluebook.aggregates.find { |candidate| candidate.hecks_name == head.split(".").first } if
|
|
249
|
+
head.count(".") == 1
|
|
250
|
+
record = aggregate && runtime.registry.repository(bluebook.name, aggregate)&.find(result.id)&.to_h
|
|
251
|
+
Doors::JsonDoor.materialize(record || result.state)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# What a caller needs to call each of the named commands: its qualified name, the role it
|
|
255
|
+
# declares, what it does, and its argument names, a trailing `*` marking a required one.
|
|
256
|
+
#
|
|
257
|
+
# @param runtime [Runtime::Dispatcher] the booted domain
|
|
258
|
+
# @param names [Array<String>] short or qualified command names
|
|
259
|
+
# @return [Array<String>] one line per name that resolves, in order
|
|
260
|
+
def command_guide(runtime, names)
|
|
261
|
+
cli = Projector.call(:cli, bluebook: bluebook_for(runtime), options: { program: "mcp" })
|
|
262
|
+
names.filter_map do |name|
|
|
263
|
+
spec = resolve!(cli, name.to_s, asking: false)
|
|
264
|
+
arguments = spec[:arguments].map { |arg| arg[:path].split(".").first + (arg[:required] ? "*" : "") }
|
|
265
|
+
"#{name} (role #{spec[:role] || 'none'}): #{spec[:summary]}. Arguments: #{arguments.uniq.join(', ')}"
|
|
266
|
+
rescue Runtime::NotFound
|
|
267
|
+
nil
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
237
271
|
# :nodoc:
|
|
238
272
|
def dry_run_outcome(runtime, spec, envelope, summary:)
|
|
239
273
|
flat = flatten_legacy(envelope, spec[:receiver], spec[:legacy_receiver])
|
|
@@ -946,10 +946,12 @@ module Hecks
|
|
|
946
946
|
$CHILD_STATUS.success? ? out : nil
|
|
947
947
|
end
|
|
948
948
|
|
|
949
|
-
# `.rb` files, plus extensionless scripts with a Ruby shebang.
|
|
949
|
+
# `.rb` files, plus extensionless scripts with a Ruby shebang. The first line is read as
|
|
950
|
+
# bytes, so an extensionless binary (a compiled test fixture's output) is passed over
|
|
951
|
+
# rather than refused for not being UTF-8.
|
|
950
952
|
def ruby_files(dir)
|
|
951
953
|
scripts = Dir[File.join(dir, "**", "*")].select do |path|
|
|
952
|
-
File.file?(path) && File.extname(path).empty? && File.open(path, &:gets).to_s.match?(/\A#!.*ruby/)
|
|
954
|
+
File.file?(path) && File.extname(path).empty? && File.open(path, "rb", &:gets).to_s.match?(/\A#!.*ruby/n)
|
|
953
955
|
end
|
|
954
956
|
Dir[File.join(dir, "**", "*.rb")] + scripts
|
|
955
957
|
end
|
|
@@ -30,7 +30,7 @@ module Hecks
|
|
|
30
30
|
DOC_LINE = %r{\A\s*(?:///(?!/)|//!)}
|
|
31
31
|
|
|
32
32
|
# `// TMPL:name BEGIN`/`END` sentinels are parsed verbatim by
|
|
33
|
-
# `rust/
|
|
33
|
+
# `rust/codegen/src/exemplar.rs`; every prose rule skips them outright.
|
|
34
34
|
TMPL_MARKER = /\bTMPL:\S+\s+(?:BEGIN|END)\b/i
|
|
35
35
|
MOD_DOC_LINE = %r{\A\s*//!}
|
|
36
36
|
ATTR_LINE = /\A\s*#\[/
|
|
@@ -14,41 +14,47 @@ module Hecks
|
|
|
14
14
|
# it declares an edge, into the behaviours and listener rules of its template
|
|
15
15
|
# (`Projections::Site::SiteCdn`).
|
|
16
16
|
#
|
|
17
|
-
# hecks site site_projection.project_site [<project>] [--out=<dir>] [--
|
|
17
|
+
# hecks site site_projection.project_site [<project>] [--out=<dir>] [--template=<file>]
|
|
18
|
+
# [--extension=ts|mts] [--check]
|
|
18
19
|
#
|
|
19
|
-
# `<project>`
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# names each file that differs from the table.
|
|
20
|
+
# `<project>` (default: the working directory) holds `bluebook/` and the `vendor/` it attaches
|
|
21
|
+
# from. `routes.ts` goes to `<project>/generated` or to `--out`, anywhere. The template is the
|
|
22
|
+
# `Edge` row's, relative to the project, or `--template`, anywhere; it is rewritten in place
|
|
23
|
+
# between its `BEGIN`/`END GENERATED site_cdn` markers. With `--out` alone, `--out` receives a
|
|
24
|
+
# copy of it instead. `--extension=mts` names the module `routes.mts`; `--check` writes nothing.
|
|
25
25
|
module SiteRoutes
|
|
26
26
|
# Where the files go, relative to the project, when `--out` names no directory.
|
|
27
27
|
DEFAULT_OUT = "generated"
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# The extensions the module may take; the first is the default.
|
|
30
|
+
EXTENSIONS = %w[ts mts].freeze
|
|
31
|
+
|
|
32
|
+
USAGE = "usage: hecks site site_projection.project_site [<project>] [--out=<dir>] " \
|
|
33
|
+
"[--template=<file>] [--extension=#{EXTENSIONS.join('|')}] [--check]".freeze
|
|
30
34
|
|
|
31
35
|
module_function
|
|
32
36
|
|
|
33
37
|
# Projects the route table, writes what differs and prints a line for each file written.
|
|
34
38
|
#
|
|
35
39
|
# @param argv [Array<String>] the project directory, then the flags
|
|
36
|
-
# @param root [String] the
|
|
40
|
+
# @param root [String] the project used when `argv` names none; the working directory
|
|
37
41
|
# @return [Integer] 0, or 1 when `--check` finds a file out of date
|
|
38
|
-
# @raise [SystemExit] with the reason on stderr when the project declares no route table
|
|
39
|
-
#
|
|
40
|
-
def main(argv, root:
|
|
42
|
+
# @raise [SystemExit] with the reason on stderr when the project declares no route table, the
|
|
43
|
+
# table is refused, or a flag is refused
|
|
44
|
+
def main(argv, root: Dir.pwd)
|
|
41
45
|
argv = argv.dup
|
|
42
|
-
out = nil
|
|
46
|
+
out = template = extension = nil
|
|
43
47
|
check = false
|
|
44
48
|
OptionParser.new do |parser|
|
|
45
49
|
parser.on("--out=DIR") { |value| out = value }
|
|
50
|
+
parser.on("--template=FILE") { |value| template = value }
|
|
51
|
+
parser.on("--extension=EXT") { |value| extension = value }
|
|
46
52
|
parser.on("--check") { check = true }
|
|
47
53
|
end.parse!(argv)
|
|
48
54
|
project = argv.empty? ? root : File.expand_path(argv.shift)
|
|
49
55
|
abort USAGE unless argv.empty?
|
|
50
56
|
|
|
51
|
-
files = projection(project, out: out)
|
|
57
|
+
files = projection(project, out: out, template: template, extension: extension)
|
|
52
58
|
stale = files.reject { |path, text| File.file?(path) && File.read(path) == text }.keys
|
|
53
59
|
return report(stale, project, check) if check
|
|
54
60
|
|
|
@@ -63,16 +69,26 @@ module Hecks
|
|
|
63
69
|
|
|
64
70
|
# @param root [String] the project directory
|
|
65
71
|
# @param out [String, nil] the directory to write to; `generated/` of the project when nil
|
|
72
|
+
# @param template [String, nil] the template to rewrite in place, wherever it lies; the
|
|
73
|
+
# `Edge` row's template, relative to the project, when nil
|
|
74
|
+
# @param extension [String, nil] `ts` (the default) or `mts`, the module's file extension
|
|
66
75
|
# @return [Hash{String => String}] each file's absolute path to the text it should hold
|
|
67
|
-
# @raise [SystemExit] when the project declares no route table
|
|
68
|
-
|
|
76
|
+
# @raise [SystemExit] when the project declares no route table, the table is refused, or a
|
|
77
|
+
# path or the extension is refused
|
|
78
|
+
def projection(root, out: nil, template: nil, extension: nil)
|
|
79
|
+
extension ||= EXTENSIONS.first
|
|
80
|
+
unless EXTENSIONS.include?(extension)
|
|
81
|
+
abort "project_site: --extension is one of #{EXTENSIONS.join(', ')}, not #{extension.inspect}"
|
|
82
|
+
end
|
|
83
|
+
dir = out ? File.expand_path(out) : File.join(root, DEFAULT_OUT)
|
|
84
|
+
abort "project_site: --out #{dir} is a file; it names the directory routes.#{extension} goes in" if File.file?(dir)
|
|
85
|
+
|
|
69
86
|
registry = registry_for(root)
|
|
70
87
|
chapter = Projections::Site::Table.chapter(registry)
|
|
71
88
|
table = Projections::Site::Table.read(chapter, registry: registry)
|
|
72
89
|
files = Projector.call(:site_routes_ts, bluebook: chapter, options: { table: table })
|
|
73
|
-
|
|
74
|
-
written
|
|
75
|
-
written.merge(template_files(root, out, chapter, table, registry))
|
|
90
|
+
written = files.to_h { |name, text| [File.join(dir, name.sub(/\.ts\z/, ".#{extension}")), text] }
|
|
91
|
+
written.merge(template_files(root, out, chapter, table, registry, template: template))
|
|
76
92
|
rescue Projections::Site::Table::Invalid => e
|
|
77
93
|
abort "project_site: #{e.message}"
|
|
78
94
|
end
|
|
@@ -84,25 +100,50 @@ module Hecks
|
|
|
84
100
|
# @param chapter [Bluebook::Chapter] the chapter that declares the route table
|
|
85
101
|
# @param table [Projections::Site::Table] the checked table
|
|
86
102
|
# @param registry [Hecks::Runtime::Registry] the registry the project booted into
|
|
103
|
+
# @param template [String, nil] a template named on the command line, rewritten in place
|
|
87
104
|
# @return [Hash{String => String}] the template's path to its text; empty with no edge
|
|
88
|
-
# @raise [SystemExit] when
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
# @raise [SystemExit] when a template is named and the project declares no edge, the
|
|
106
|
+
# template is missing or lacks a region it needs, or holds a region an edge without a load
|
|
107
|
+
# balancer does not use
|
|
108
|
+
def template_files(root, out, chapter, table, registry, template: nil)
|
|
109
|
+
edge = Projections::Site::Edge.read(chapter, table: table, template: template,
|
|
110
|
+
vocabulary: Projections::Site::Table.vocabulary(registry))
|
|
111
|
+
unless edge
|
|
112
|
+
abort "project_site: --template names #{template}, but the project declares no Edge rows" if template
|
|
113
|
+
return {}
|
|
114
|
+
end
|
|
92
115
|
|
|
93
116
|
regions = Projector.call(:site_cdn, bluebook: chapter, options: { table: table, edge: edge })
|
|
94
|
-
relative = edge.setting.template
|
|
95
|
-
source = File.join(root, relative)
|
|
96
|
-
|
|
117
|
+
relative = template || edge.setting.template
|
|
118
|
+
source = template ? File.expand_path(template) : File.join(root, relative)
|
|
119
|
+
unless File.file?(source)
|
|
120
|
+
abort "project_site: the template #{relative} does not exist#{" in #{root}" unless template}"
|
|
121
|
+
end
|
|
97
122
|
|
|
98
|
-
text =
|
|
123
|
+
text = rewritten(File.read(source), regions, relative, edge)
|
|
124
|
+
in_place = template || out.nil?
|
|
125
|
+
{ (in_place ? source : File.join(File.expand_path(out), relative)) => text }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @param text [String] the template
|
|
129
|
+
# @param regions [Hash{String => String}] each region's name to the block that goes in it
|
|
130
|
+
# @param relative [String] the template's name, for a message
|
|
131
|
+
# @param edge [Projections::Site::Edge] the checked edge
|
|
132
|
+
# @return [String] the template with each region rewritten
|
|
133
|
+
# @raise [SystemExit] when a region is missing, or a listener_rules region is left behind
|
|
134
|
+
# by an edge with no load balancer
|
|
135
|
+
def rewritten(text, regions, relative, edge)
|
|
136
|
+
if !edge.alb? && Projections::Site::Regions.region?(text, "listener_rules")
|
|
137
|
+
abort "project_site: #{relative} has a BEGIN/END GENERATED site_cdn listener_rules region, " \
|
|
138
|
+
"and the Edge row says alb: false; remove the region"
|
|
139
|
+
end
|
|
140
|
+
regions.reduce(text) do |current, (name, block)|
|
|
99
141
|
unless Projections::Site::Regions.region?(current, name)
|
|
100
142
|
abort "project_site: #{relative} has no BEGIN/END GENERATED site_cdn #{name} region"
|
|
101
143
|
end
|
|
102
144
|
|
|
103
145
|
Projections::Site::Regions.replace(current, name, block)
|
|
104
146
|
end
|
|
105
|
-
{ (out ? File.join(File.expand_path(out), relative) : source) => text }
|
|
106
147
|
end
|
|
107
148
|
|
|
108
149
|
# Loads the project's chapters the way a deploy does: the bluebooks, then the hecksagons that
|
data/lib/hecks/version.rb
CHANGED
data/lib/hecks/vocabulary.rb
CHANGED
|
@@ -51,7 +51,7 @@ module Hecks
|
|
|
51
51
|
].freeze,
|
|
52
52
|
"CiGate" => [
|
|
53
53
|
{"name"=>"runtime_changed", "workflow"=>"ci.yml", "mode"=>"touches", "pattern"=>"^lib/hecks/runtime/", "push"=>"skip", "label"=>"does this change touch lib/hecks/runtime/**?"}.freeze,
|
|
54
|
-
{"name"=>"postgres_io_relevant_changed", "workflow"=>"ci-postgres-io-parallel.yml", "mode"=>"skips_unless", "pattern"=>"^(docs/|editors/|release/|deploy/|\\.claude/|\\.githooks/|rust/(parser|codegen|host|build|lsp|web|tests
|
|
54
|
+
{"name"=>"postgres_io_relevant_changed", "workflow"=>"ci-postgres-io-parallel.yml", "mode"=>"skips_unless", "pattern"=>"^(docs/|editors/|release/|deploy/|\\.claude/|\\.githooks/|rust/(parser|codegen|host|build|lsp|web|tests)/|\\.rubocop\\.yml$|\\.rubocop_todo\\.yml$|\\.mcp\\.json$|\\.rspec-local\\.example$|README\\.md$|CHANGELOG\\.md$|CONTRIBUTING\\.md$|SECURITY\\.md$|LICENSE$|\\.gitignore$)", "push"=>"before_sha", "label"=>"does this change touch anything rspec_postgres_io_parallel covers?"}.freeze
|
|
55
55
|
].freeze,
|
|
56
56
|
"Comparison" => [
|
|
57
57
|
{"symbol"=>">=", "compares_less_than"=>"true", "compares_equal"=>"false", "negated"=>"true"}.freeze,
|
data/lib/hecks.rb
CHANGED
|
@@ -34,6 +34,7 @@ require_relative "hecks/doors/cli_runner"
|
|
|
34
34
|
require_relative "hecks/storehouse"
|
|
35
35
|
require_relative "hecks/mcp_stdio_guard"
|
|
36
36
|
require_relative "hecks/doors/mcp_door_scope"
|
|
37
|
+
require_relative "hecks/doors/mcp_guide_cache"
|
|
37
38
|
require_relative "hecks/framework"
|
|
38
39
|
require_relative "hecks/vendoring"
|
|
39
40
|
require_relative "hecks/embryonaut_bluebook"
|
data/rust/build/src/pipeline.rs
CHANGED
data/rust/codegen/src/attr.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Port of `
|
|
1
|
+
//! Port of the retired Ruby generator's `dependency_planning.rb`, an independent re-derivation of the
|
|
2
2
|
//! Analyzer's `complete_state? && state_independent?` predicate. Mirrors the Ruby file.
|
|
3
3
|
|
|
4
4
|
use crate::json::Json;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Port of `
|
|
1
|
+
//! Port of the retired Ruby generator's `domain_generator.rb`'s `DomainGenerator.call`: the per-aggregate
|
|
2
2
|
//! `.rs` files plus `registry.rs`. `metadata.rs` and `ir.json` are not emitted here.
|
|
3
3
|
|
|
4
4
|
use crate::exemplar::Exemplar;
|
|
@@ -1181,7 +1181,7 @@ pub fn generate(
|
|
|
1181
1181
|
None,
|
|
1182
1182
|
None,
|
|
1183
1183
|
Some(format!(
|
|
1184
|
-
"read via rust/host's journal::read_lineage_head_all/_by_id, written via journal::append_lineage_mutation — both generic over storage_name (\"{}\"), dispatched OUTSIDE the WASM kernel/InMemoryRepository path entirely, matching Ruby's own CommandInterpreter routing for a Postgres-bound aggregate (rust/
|
|
1184
|
+
"read via rust/host's journal::read_lineage_head_all/_by_id, written via journal::append_lineage_mutation — both generic over storage_name (\"{}\"), dispatched OUTSIDE the WASM kernel/InMemoryRepository path entirely, matching Ruby's own CommandInterpreter routing for a Postgres-bound aggregate (rust/codegen/src/domain_generator.rs)",
|
|
1185
1185
|
lineage_aggregate.get("storage_name").map(Json::to_s).unwrap_or_default()
|
|
1186
1186
|
)),
|
|
1187
1187
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Port of `
|
|
1
|
+
//! Port of the retired Ruby generator's `exemplar.rb`: loads the template shapes in `rust/src/exemplar/*.rs`.
|
|
2
2
|
//! Both codegens read those files at runtime, so the shapes cannot drift.
|
|
3
3
|
use std::collections::HashMap;
|
|
4
4
|
use std::path::PathBuf;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Port of `
|
|
1
|
+
//! Port of the retired Ruby generator's `expr_emitter.rb`: turns each IR rule's `ast` tree into `Expr` source.
|
|
2
2
|
//! A literal-array `include?` never reaches here; `AstJson` rewrites it to an or of equalities.
|
|
3
3
|
|
|
4
4
|
use crate::json::Json;
|