hecks 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/bluebook/assembly/contracts.rb +2 -1
  3. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  4. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
  5. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
  6. data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
  7. data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
  8. data/lib/hecks/bluebook/query.rb +8 -5
  9. data/lib/hecks/cli/check_era.rb +3 -39
  10. data/lib/hecks/cli/interview_draft.rb +86 -31
  11. data/lib/hecks/cli/interview_session.rb +77 -26
  12. data/lib/hecks/cli/interview_shape.rb +179 -0
  13. data/lib/hecks/doors/cli_runner.rb +14 -1
  14. data/lib/hecks/doors/launcher_options.rb +13 -3
  15. data/lib/hecks/doors/mcp_door.rb +16 -2
  16. data/lib/hecks/doors/mcp_door_scope.rb +45 -5
  17. data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
  18. data/lib/hecks/doors.rb +1 -1
  19. data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
  20. data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
  21. data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
  22. data/lib/hecks/embryonaut_bluebook.rb +2 -0
  23. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
  24. data/lib/hecks/fuzzing/qa_settings.rb +15 -34
  25. data/lib/hecks/gate/stages.yml +113 -8
  26. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
  27. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
  28. data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
  29. data/lib/hecks/hecks/adapters/git.rb +59 -0
  30. data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
  31. data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
  32. data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
  33. data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
  34. data/lib/hecks/hecks/adapters/site_toolchain.rb +88 -11
  35. data/lib/hecks/hecks/codebase.behaviors +6 -0
  36. data/lib/hecks/hecks/codebase.bluebook +21 -0
  37. data/lib/hecks/hecks/custodian.behaviors +6 -0
  38. data/lib/hecks/hecks/custodian.bluebook +217 -0
  39. data/lib/hecks/hecks/hecks.hecksagon +24 -0
  40. data/lib/hecks/hecks/hecks.world +8 -3
  41. data/lib/hecks/hecks/site.world +5 -2
  42. data/lib/hecks/language/bluebook/query.bluebook +25 -0
  43. data/lib/hecks/language/bluebook/vocabulary.bluebook +220 -7
  44. data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
  45. data/lib/hecks/language/oidc.json +5 -0
  46. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
  47. data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
  48. data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
  49. data/lib/hecks/projections/deploy/box/settings.rb +40 -6
  50. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
  51. data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
  52. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
  53. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
  54. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
  55. data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
  56. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
  57. data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
  58. data/lib/hecks/projections/deploy/box.rb +28 -4
  59. data/lib/hecks/projections/deploy/fargate.rb +3 -3
  60. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  61. data/lib/hecks/projections/model/deviations.rb +1 -1
  62. data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
  63. data/lib/hecks/projections/site/edge/checks.rb +31 -5
  64. data/lib/hecks/projections/site/edge.rb +25 -11
  65. data/lib/hecks/projections/site/live_edge.rb +162 -0
  66. data/lib/hecks/projections/site/routes_ts.rb +50 -27
  67. data/lib/hecks/projections/site/site_cdn.rb +7 -4
  68. data/lib/hecks/projections/site/table/checks.rb +53 -7
  69. data/lib/hecks/projections/site/table/link_builder.rb +64 -0
  70. data/lib/hecks/projections/site/table/row_builder.rb +21 -6
  71. data/lib/hecks/projections/site/table.rb +16 -5
  72. data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
  73. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
  74. data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
  75. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
  76. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
  77. data/lib/hecks/quality_control/quality_control.bluebook +76 -124
  78. data/lib/hecks/runtime/interpreting.rb +3 -22
  79. data/lib/hecks/runtime/needs.rb +48 -0
  80. data/lib/hecks/runtime/query_interpreter.rb +3 -0
  81. data/lib/hecks/runtime/registry/verification.rb +3 -3
  82. data/lib/hecks/rust_build/host.rb +147 -0
  83. data/lib/hecks/rust_build/kernel_input.rb +51 -17
  84. data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
  85. data/lib/hecks/rust_build/project_rust.rb +7 -140
  86. data/lib/hecks/rust_build.rb +1 -0
  87. data/lib/hecks/site/bluebook/site.bluebook +80 -14
  88. data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
  89. data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
  90. data/lib/hecks/storehouse.rb +35 -1
  91. data/lib/hecks/tools/comment_style.rb +4 -2
  92. data/lib/hecks/tools/gate.rb +1 -1
  93. data/lib/hecks/tools/rust_comment_style.rb +2 -4
  94. data/lib/hecks/tools/site_routes.rb +69 -28
  95. data/lib/hecks/tools/tools_doc.rb +204 -0
  96. data/lib/hecks/tools.rb +1 -0
  97. data/lib/hecks/version.rb +1 -1
  98. data/lib/hecks/vocabulary.rb +152 -1
  99. data/lib/hecks.rb +1 -0
  100. data/qa/settings.yml +1 -2
  101. data/rust/build/src/lineage_pass.rs +31 -16
  102. data/rust/build/src/main.rs +4 -2
  103. data/rust/build/src/pipeline.rs +9 -20
  104. data/rust/codegen/src/attr.rs +1 -1
  105. data/rust/codegen/src/bridging.rs +8 -8
  106. data/rust/codegen/src/commands.rs +1 -1
  107. data/rust/codegen/src/constraints.rs +1 -1
  108. data/rust/codegen/src/dependency_planning.rs +1 -1
  109. data/rust/codegen/src/domain_generator.rs +2 -2
  110. data/rust/codegen/src/exemplar.rs +1 -1
  111. data/rust/codegen/src/expr_emitter.rs +1 -1
  112. data/rust/codegen/src/fielded.rs +1 -1
  113. data/rust/codegen/src/json.rs +214 -23
  114. data/rust/codegen/src/json_codec.rs +1 -1
  115. data/rust/codegen/src/literal.rs +6 -3
  116. data/rust/codegen/src/main.rs +23 -13
  117. data/rust/codegen/src/mutations.rs +1 -1
  118. data/rust/codegen/src/naming.rs +5 -3
  119. data/rust/codegen/src/optional_pass.rs +226 -0
  120. data/rust/codegen/src/ports.rs +1 -1
  121. data/rust/codegen/src/queries.rs +3 -3
  122. data/rust/codegen/src/reactions.rs +1 -1
  123. data/rust/codegen/src/read_models.rs +2 -2
  124. data/rust/codegen/src/reference_specs.rs +1 -1
  125. data/rust/codegen/src/registry.rs +4 -4
  126. data/rust/codegen/src/shared.rs +1 -1
  127. data/rust/{build → codegen}/src/sidecars.rs +9 -5
  128. data/rust/codegen/src/skip_reason.rs +1 -1
  129. data/rust/codegen/src/types.rs +4 -4
  130. data/rust/host/HECKS_RELEASE +1 -1
  131. data/rust/host/src/api.rs +1 -1
  132. data/rust/host/src/dispatch.rs +5 -0
  133. data/rust/host/src/needs.rs +193 -29
  134. data/rust/host/src/resend.rs +4 -4
  135. data/rust/host/src/ui_schema.rs +1 -1
  136. data/rust/parser/src/emit.rs +12 -0
  137. data/rust/parser/src/ir.rs +2 -0
  138. data/rust/parser/src/keywords.rs +2 -0
  139. data/rust/parser/src/main.rs +1 -0
  140. data/rust/parser/src/parse/command.rs +2 -44
  141. data/rust/parser/src/parse/mod.rs +1 -0
  142. data/rust/parser/src/parse/needs.rs +60 -0
  143. data/rust/parser/src/parse/query.rs +3 -0
  144. data/rust/src/exemplar/commands.rs +1 -1
  145. data/rust/src/exemplar/constraints.rs +1 -1
  146. data/rust/src/exemplar/fielded.rs +1 -1
  147. data/rust/src/exemplar/json.rs +1 -1
  148. data/rust/src/exemplar/mod.rs +1 -1
  149. data/rust/src/exemplar/mutations.rs +1 -1
  150. data/rust/src/exemplar/queries.rs +1 -1
  151. data/rust/src/exemplar/reactions.rs +1 -1
  152. data/rust/src/exemplar/read_models.rs +1 -1
  153. data/rust/src/exemplar/registry.rs +1 -1
  154. data/rust/src/exemplar/types.rs +1 -1
  155. data/rust/src/kernel/cli.rs +18 -8
  156. data/rust/src/kernel/needs.rs +104 -9
  157. data/rust/src/kernel/read_model.rs +1 -1
  158. data/rust/src/lib.rs +1 -1
  159. metadata +24 -30
  160. data/lib/hecks/rust_build/append_optionals.rb +0 -79
  161. data/lib/hecks/rust_build/rust_literal.rb +0 -33
  162. data/lib/hecks/three_zero/forms.yml +0 -127
  163. data/lib/hecks/three_zero.rb +0 -12
  164. data/rust/build/src/json.rs +0 -421
  165. data/rust/build/src/optional_pass.rs +0 -150
  166. data/rust/project/bridging.rb +0 -257
  167. data/rust/project/commands.rb +0 -877
  168. data/rust/project/constraints.rb +0 -86
  169. data/rust/project/dependency_planning.rb +0 -178
  170. data/rust/project/domain_generator.rb +0 -983
  171. data/rust/project/exemplar.rb +0 -148
  172. data/rust/project/expr_emitter.rb +0 -72
  173. data/rust/project/fielded.rb +0 -141
  174. data/rust/project/json_codec.rb +0 -641
  175. data/rust/project/mutations.rb +0 -695
  176. data/rust/project/naming.rb +0 -223
  177. data/rust/project/ports.rb +0 -65
  178. data/rust/project/queries.rb +0 -493
  179. data/rust/project/reactions.rb +0 -317
  180. data/rust/project/read_models.rb +0 -690
  181. data/rust/project/reference_specs.rb +0 -27
  182. data/rust/project/registry.rb +0 -396
  183. data/rust/project/skip_reason.rb +0 -29
  184. data/rust/project/types.rb +0 -225
  185. data/rust/project/write_if_changed.rb +0 -7
  186. data/rust/project.rb +0 -30
  187. data/rust/project_rust_pipeline.rb +0 -391
@@ -3,7 +3,6 @@
3
3
  require "fileutils"
4
4
  require "json"
5
5
  require "tmpdir"
6
- require_relative "../rust_literal"
7
6
  require_relative "../write_if_changed"
8
7
 
9
8
  module Hecks
@@ -78,7 +77,6 @@ module Hecks
78
77
  def generate_meta(stage, tmp)
79
78
  ir_path = write_ir(tmp, "meta", @meta)
80
79
  run("full", stage, "meta", META_LABEL, ir_path)
81
- write_sidecars(File.join(stage, "meta"), @meta, META_LABEL)
82
80
  end
83
81
 
84
82
  def generate_target(stage, tmp)
@@ -87,9 +85,6 @@ module Hecks
87
85
  args.push(chapter.mod_name, chapter.label, write_ir(tmp, chapter.mod_name, chapter.ir))
88
86
  end
89
87
  run("full", *args)
90
- [@target, *@chapters].each do |chapter|
91
- write_sidecars(File.join(stage, chapter.mod_name), chapter.ir, chapter.label)
92
- end
93
88
  end
94
89
 
95
90
  def write_ir(tmp, name, tree)
@@ -98,19 +93,6 @@ module Hecks
98
93
  path
99
94
  end
100
95
 
101
- # `ir.json` is what `rust/host` reads at runtime, and `metadata.rs` embeds the same text;
102
- # the codegen binary writes neither.
103
- def write_sidecars(mod_dir, tree, label)
104
- text = JSON.pretty_generate(tree)
105
- File.write(File.join(mod_dir, "ir.json"), text)
106
- File.write(File.join(mod_dir, "metadata.rs"), <<~RUST)
107
- // GENERATED by hecks project_rust — #{label}'s own canonical IR,
108
- // embedded for runtime self-description. Not read by any dispatch
109
- // function in this module — introspection only.
110
- pub const IR_JSON: &str = #{RustLiteral.string(text)};
111
- RUST
112
- end
113
-
114
96
  def run(*args)
115
97
  return if system(self.class.binary(@rust_dir), *args, out: $stdout, err: $stderr)
116
98
 
@@ -5,7 +5,6 @@ require "json"
5
5
  require_relative "../../hecks"
6
6
  require_relative "../bluebook/meta_validator"
7
7
  require_relative "../ports/persistence/plugins/era"
8
- require_relative "append_optionals"
9
8
  require_relative "domain_name"
10
9
  require_relative "write_if_changed"
11
10
 
@@ -17,9 +16,8 @@ module Hecks
17
16
  # ProjectRust.call(["path/to/domain"])
18
17
  #
19
18
  # `hecks-codegen` is the generator (ADR 0086): this class builds the IR from the live registry
20
- # and `CodegenRun` runs the binary on it. `HECKS_CODEGEN=ruby` selects the Ruby generator in
21
- # `rust/project` until that is deleted; `HECKS_PARSER=rust` with `HECKS_CODEGEN=rust` runs
22
- # `rust/project_rust_pipeline.rb` instead, with no Ruby load of the domain.
19
+ # and `CodegenRun` runs the binary on it. The Ruby-free path is `hecks-build`, which feeds the
20
+ # same binary from `hecks-parse`.
23
21
  #
24
22
  # The era plugin is required unconditionally so the generator works for any domain, lineage
25
23
  # capable or not; `pg` stays lazy inside it, so nothing here opens a database connection.
@@ -46,13 +44,7 @@ module Hecks
46
44
  # @return [Integer] the exit status
47
45
  def call
48
46
  validate_name!
49
- if ENV["HECKS_PARSER"] == "rust" && ENV["HECKS_CODEGEN"] == "rust"
50
- require_relative "../../../rust/project"
51
- require_relative "../../../rust/project_rust_pipeline"
52
- RustProjectPipeline.call(@domain)
53
- else
54
- generate
55
- end
47
+ generate
56
48
  0
57
49
  end
58
50
 
@@ -78,18 +70,11 @@ module Hecks
78
70
  FileUtils.mkdir_p(@out_root)
79
71
  build_target_ir
80
72
  FileUtils.rm_rf(File.join(@out_root, "active"))
81
- ENV["HECKS_CODEGEN"] == "ruby" ? generate_with_ruby : generate_with_codegen
73
+ generate_with_codegen
82
74
  write_root_mod
83
75
  sync_cargo_features
84
76
  end
85
77
 
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
78
  # `hecks-codegen` is the generator; this process only builds the IR it reads from the live
94
79
  # registry, so a domain's persistence, seams, translations and source text reach `ir.json`.
95
80
  def generate_with_codegen
@@ -97,20 +82,14 @@ module Hecks
97
82
  vendored = @registry.hecksagons.values.flat_map(&:vendored_packages)
98
83
  chapters = (@registry.bluebooks.keys - [@domain_name]).map do |name|
99
84
  ir = Hecks::Projector::Exporter.call(@registry).fetch(name)
100
- CodegenRun::Chapter.new(name.downcase, "#{@domain} (#{attachment(name, vendored)})", prepared_ir(ir))
85
+ CodegenRun::Chapter.new(name.downcase, "#{@domain} (#{attachment(name, vendored)})", json_shaped(ir))
101
86
  end
102
87
  CodegenRun.new(
103
- rust_dir: @rust_dir, out_root: @out_root, meta: prepared_ir(meta), chapters: chapters,
104
- target: CodegenRun::Chapter.new(@mod_name, @domain, prepared_ir(@ir, shaped: false))
88
+ rust_dir: @rust_dir, out_root: @out_root, meta: json_shaped(meta), chapters: chapters,
89
+ target: CodegenRun::Chapter.new(@mod_name, @domain, @ir)
105
90
  ).call
106
91
  end
107
92
 
108
- # The IR as `rust/project` hands it to its generators: string-keyed through JSON, with the
109
- # append-optional fields marked, since `ir.json` records them.
110
- def prepared_ir(tree, shaped: true)
111
- AppendOptionals.mark(shaped ? json_shaped(tree) : tree)
112
- end
113
-
114
93
  # Round-trips a value through JSON so generators see the string-keyed shape a real `ir.json`
115
94
  # carries, never live Ruby symbols.
116
95
  def json_shaped(payload) = JSON.parse(JSON.generate(payload), symbolize_names: true)
@@ -186,78 +165,6 @@ module Hecks
186
165
  )
187
166
  end
188
167
 
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
168
  # How the target attached a chapter, across every hecksagon the registry loaded: a chapter a
262
169
  # sibling hecksagon vendors in is labelled as vendored.
263
170
  def attachment(chapter_name, vendored)
@@ -265,46 +172,6 @@ module Hecks
265
172
  name ? "attaches #{name.inspect}, from: :vendor" : "attaches #{chapter_name.inspect}"
266
173
  end
267
174
 
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
175
  # `src/generated/mod.rs`: every domain ever generated, scanned off disk. A directory is a
309
176
  # selectable domain exactly when it carries its own `merged.rs`; a framework chapter has
310
177
  # none and is always compiled in.
@@ -31,6 +31,7 @@ module Hecks
31
31
  TOOLS = {
32
32
  "project_rust" => ["rust_build/project_rust", :ProjectRust],
33
33
  "project_wasm" => ["rust_build/wasm", :Wasm],
34
+ "project_host" => ["rust_build/host", :Host],
34
35
  "project_wasm_browser" => ["rust_build/wasm_browser", :WasmBrowser],
35
36
  "rust_conformance" => ["rust_build/conformance", :Conformance],
36
37
  "rust_conformance_fuzz" => ["rust_build/conformance_fuzz", :ConformanceFuzz],
@@ -95,12 +95,18 @@ 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, DomainPath
99
- attribute :run, RunKey
100
- attribute :out, Location, optional: true
101
- attribute :check, Switch, optional: true
102
- attribute :output, Report, optional: true
103
- attribute :refusal, Report, optional: true
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 :live, Location, optional: true
105
+ attribute :distribution, DistributionId, optional: true
106
+ attribute :expect_new, PathList, optional: true
107
+ attribute :refs, PolicyRefs, optional: true
108
+ attribute :output, Report, optional: true
109
+ attribute :refusal, Report, optional: true
104
110
  identified_by :run
105
111
 
106
112
  value_object "RunKey" do
@@ -116,15 +122,37 @@ Hecks.bluebook "Site" do
116
122
  invariant("a project is named by its path") { !value.to_s.empty? }
117
123
  end
118
124
 
119
- # Where routes.ts is written, when not beside the project's bluebook.
125
+ # A directory or file anywhere on disk, relative to where the command runs: where routes.ts is
126
+ # written when not in the project's generated/ directory, or the template to rewrite in place.
120
127
  value_object "Location" do
121
128
  attribute :value, String, pattern: '[^\t\n\r]'
122
129
  end
123
130
 
131
+ # The file extension of the generated module: `ts` (routes.ts), or `mts` (routes.mts), which
132
+ # Node loads as an ES module even in a package whose "type" is "commonjs".
133
+ value_object "Extension" do
134
+ attribute :value, String, one_of: ["ts", "mts"]
135
+ end
136
+
124
137
  value_object "Switch" do
125
138
  attribute :value, Boolean
126
139
  end
127
140
 
141
+ # A CloudFront distribution's id, whose configuration `check_live` reads.
142
+ value_object "DistributionId" do
143
+ attribute :value, String, pattern: '^[A-Z0-9]+$'
144
+ end
145
+
146
+ # Path patterns, comma separated: the behaviours a change is about to add to the live distribution.
147
+ value_object "PathList" do
148
+ attribute :value, String, pattern: '[^\t\n\r]'
149
+ end
150
+
151
+ # Policy references and the ids they stand for live, as `!Ref Name=id`, comma separated.
152
+ value_object "PolicyRefs" do
153
+ attribute :value, String, pattern: '[^\t\n\r]'
154
+ end
155
+
128
156
  value_object "Report" do
129
157
  attribute :value, String
130
158
  end
@@ -134,26 +162,59 @@ Hecks.bluebook "Site" do
134
162
  transition "Fault" => "faulted", from: "requested"
135
163
  end
136
164
 
137
- # `hecks site site_projection.project_site <project> [out=<dir>] [--check]`. With `check` nothing is written: a
138
- # routes.ts that differs from the table is faulted, naming the file, so a CI job fails on drift.
165
+ # `hecks site site_projection.project_site <project> [out=<dir>] [template=<file>] [extension=ts|mts] [--check]`.
166
+ # With `check` nothing is written: a routes.ts that differs from the table is faulted, naming the
167
+ # file, so a CI job fails on drift.
139
168
  command "ProjectSite" do
140
169
  goal "Project a site's route table into routes.ts, or with check only compare it with the file on disk"
141
170
 
142
- attribute :domain, DomainPath
143
- attribute :run, RunKey
144
- attribute :out, Location, optional: true
145
- attribute :check, Switch, optional: true
171
+ attribute :domain, DomainPath
172
+ attribute :run, RunKey
173
+ attribute :out, Location, optional: true
174
+ attribute :template, Location, optional: true
175
+ attribute :extension, Extension, optional: true
176
+ attribute :check, Switch, optional: true
146
177
 
147
178
  sets :domain
148
179
  sets :run
149
180
  sets :out
181
+ sets :template
182
+ sets :extension
150
183
  sets :check
151
184
 
152
185
  emits "SiteProjectionRequested"
153
186
  end
154
187
 
188
+ # `hecks site site_projection.check_live <project> live=<saved-config.json> | distribution=<id>
189
+ # [expect_new=<paths>] [refs=<!Ref Name=id,...>]`. Compares the project's generated CloudFront
190
+ # behaviours with a live distribution's, changing neither. `live` is a saved answer of
191
+ # `aws cloudfront get-distribution-config`; `distribution` fetches it with that one read-only call.
192
+ command "CheckLive" do
193
+ goal "Compare a project's generated CloudFront behaviours with a live distribution's"
194
+
195
+ attribute :domain, DomainPath
196
+ attribute :run, RunKey
197
+ attribute :live, Location, optional: true
198
+ attribute :distribution, DistributionId, optional: true
199
+ attribute :expect_new, PathList, optional: true
200
+ attribute :refs, PolicyRefs, optional: true
201
+
202
+ given("exactly one of a saved configuration and a distribution is named: pass live= or distribution=, not both") do
203
+ (live.set? && distribution.unset?) || (live.unset? && distribution.set?)
204
+ end
205
+
206
+ sets :domain
207
+ sets :run
208
+ sets :live
209
+ sets :distribution
210
+ sets :expect_new
211
+ sets :refs
212
+
213
+ emits "LiveCheckRequested"
214
+ end
215
+
155
216
  command "Complete" do
156
- goal "Record that routes.ts was written, or found current, and where"
217
+ goal "Record that routes.ts was written, or found current, or the live edge matched, and what it said"
157
218
 
158
219
  reference_to SiteProjection
159
220
  attribute :output, Report
@@ -193,6 +254,11 @@ Hecks.bluebook "Site" do
193
254
  trigger SiteProjection::SiteToolchain::Project, with: { run: :run }
194
255
  end
195
256
 
257
+ policy "CheckWhenRequested" do
258
+ on "SiteProjection.LiveCheckRequested"
259
+ trigger SiteProjection::SiteToolchain::Compare, with: { run: :run }
260
+ end
261
+
196
262
  policy "RecordTheProjection" do
197
263
  on "SiteProjection.SiteAnswered"
198
264
  trigger SiteProjection::Complete, with: { output: :output }
@@ -11,5 +11,11 @@ Hecks.hecksagon "Site" do
11
11
  answers "SiteAnswered"
12
12
  refuses "SiteRefused"
13
13
  end
14
+
15
+ asks "Compare", to: SiteProjection do
16
+ attribute :run, RunKey
17
+ answers "SiteAnswered"
18
+ refuses "SiteRefused"
19
+ end
14
20
  end
15
21
  end
@@ -21,6 +21,8 @@ Hecks.bluebook "SME" do
21
21
  attribute :thing_findings, list_of(ThingFinding)
22
22
  attribute :action_findings, list_of(ActionFinding)
23
23
  attribute :rule_findings, list_of(RuleFinding)
24
+ attribute :field_findings, list_of(FieldFinding)
25
+ attribute :transition_findings, list_of(TransitionFinding)
24
26
 
25
27
  # Held as counts because a rule cannot look inside a list: `Conclude` needs to know that at least
26
28
  # one thing and one action were accepted, and each acceptance adds one here (see the policies).
@@ -92,6 +94,17 @@ Hecks.bluebook "SME" do
92
94
  attribute :value, Boolean
93
95
  end
94
96
 
97
+ # A list the expert gave in a breath, comma separated: the values a field may take, or the
98
+ # fields an action needs to be told. Absent when the expert gave none.
99
+ value_object "Listing" do
100
+ attribute :value, String, pattern: '[^\t\n\r]'
101
+ end
102
+
103
+ # Who may do an action, in the expert's words ("a maintainer", "QA engineer").
104
+ value_object "Actor" do
105
+ attribute :value, String, pattern: '[^\t\n\r]'
106
+ end
107
+
95
108
  # A thing the domain keeps track of, and the field that identifies one: an aggregate to be.
96
109
  entity "ThingFinding" do
97
110
  description "A thing the expert described, proposed as an aggregate, with the field that identifies one."
@@ -136,6 +149,8 @@ Hecks.bluebook "SME" do
136
149
  attribute :thing, Name
137
150
  attribute :event, Name
138
151
  attribute :creates, Creates, optional: true
152
+ attribute :takes, Listing, optional: true
153
+ attribute :by, Actor, optional: true
139
154
  attribute :source, Source
140
155
 
141
156
  lifecycle :status, default: "proposed" do
@@ -192,6 +207,77 @@ Hecks.bluebook "SME" do
192
207
  end
193
208
  end
194
209
 
210
+ # Something the expert said a thing has, and the values it may take when they listed them: an
211
+ # attribute to be, with a closed set when the expert gave one.
212
+ entity "FieldFinding" do
213
+ description "A field the expert said a thing has, with the values it may take when they listed them."
214
+
215
+ identified_by :number
216
+
217
+ attribute :number, FindingNumber
218
+ attribute :thing, Name
219
+ attribute :name, Name
220
+ attribute :values, Listing, optional: true
221
+ attribute :source, Source
222
+
223
+ lifecycle :status, default: "proposed" do
224
+ transition "AcceptField" => "accepted", from: "proposed"
225
+ transition "RejectField" => "rejected", from: "proposed"
226
+ end
227
+
228
+ command "AcceptField", from: "proposed" do
229
+ goal "Establish that the field is real"
230
+
231
+ attribute :number, FindingNumber
232
+
233
+ emits "FieldAccepted"
234
+ end
235
+
236
+ command "RejectField", from: "proposed" do
237
+ goal "Set the proposed field aside"
238
+
239
+ attribute :number, FindingNumber
240
+
241
+ emits "FieldRejected"
242
+ end
243
+ end
244
+
245
+ # The state a thing is in after an action, and the state it had to be in before: one step of a
246
+ # lifecycle to be. The step an action that creates the thing makes names the state it starts in.
247
+ entity "TransitionFinding" do
248
+ description "A change of state an action makes to a thing, and the state it must be in before."
249
+
250
+ identified_by :number
251
+
252
+ attribute :number, FindingNumber
253
+ attribute :thing, Name
254
+ attribute :action, Name
255
+ attribute :to, Name
256
+ attribute :from, Name, optional: true
257
+ attribute :source, Source
258
+
259
+ lifecycle :status, default: "proposed" do
260
+ transition "AcceptTransition" => "accepted", from: "proposed"
261
+ transition "RejectTransition" => "rejected", from: "proposed"
262
+ end
263
+
264
+ command "AcceptTransition", from: "proposed" do
265
+ goal "Establish that the change of state is real"
266
+
267
+ attribute :number, FindingNumber
268
+
269
+ emits "TransitionAccepted"
270
+ end
271
+
272
+ command "RejectTransition", from: "proposed" do
273
+ goal "Set the proposed change of state aside"
274
+
275
+ attribute :number, FindingNumber
276
+
277
+ emits "TransitionRejected"
278
+ end
279
+ end
280
+
195
281
  lifecycle :status, default: "planned" do
196
282
  transition "Begin" => "underway", from: "planned"
197
283
  transition "Conclude" => "concluded", from: "underway"
@@ -257,16 +343,57 @@ Hecks.bluebook "SME" do
257
343
  attribute :thing, Name
258
344
  attribute :event, Name
259
345
  attribute :creates, Creates, optional: true
346
+ attribute :takes, Listing, optional: true
347
+ attribute :by, Actor, optional: true
260
348
  attribute :source, Source
261
349
 
262
350
  given("findings are proposed only while the interview is underway") { status == "underway" }
263
351
  given("a finding cites an exchange that was recorded") { source.value <= exchanges.size }
264
352
 
265
- sets :action_findings, append: { number: :number, name: :name, thing: :thing, event: :event, creates: :creates, source: :source }
353
+ sets :action_findings, append: { number: :number, name: :name, thing: :thing, event: :event, creates: :creates,
354
+ takes: :takes, by: :by, source: :source }
266
355
 
267
356
  emits "ActionProposed"
268
357
  end
269
358
 
359
+ command "ProposeField" do
360
+ goal "Propose a field the expert said a thing has, and the values it may take"
361
+
362
+ reference_to Interview
363
+ attribute :number, FindingNumber
364
+ attribute :thing, Name
365
+ attribute :name, Name
366
+ attribute :values, Listing, optional: true
367
+ attribute :source, Source
368
+
369
+ given("findings are proposed only while the interview is underway") { status == "underway" }
370
+ given("a finding cites an exchange that was recorded") { source.value <= exchanges.size }
371
+
372
+ sets :field_findings, append: { number: :number, thing: :thing, name: :name, values: :values, source: :source }
373
+
374
+ emits "FieldProposed"
375
+ end
376
+
377
+ command "ProposeTransition" do
378
+ goal "Propose the change of state an action makes to a thing"
379
+
380
+ reference_to Interview
381
+ attribute :number, FindingNumber
382
+ attribute :thing, Name
383
+ attribute :action, Name
384
+ attribute :to, Name
385
+ attribute :from, Name, optional: true
386
+ attribute :source, Source
387
+
388
+ given("findings are proposed only while the interview is underway") { status == "underway" }
389
+ given("a finding cites an exchange that was recorded") { source.value <= exchanges.size }
390
+
391
+ sets :transition_findings, append: { number: :number, thing: :thing, action: :action, to: :to, from: :from,
392
+ source: :source }
393
+
394
+ emits "TransitionProposed"
395
+ end
396
+
270
397
  command "ProposeRule" do
271
398
  goal "Keep a rule in the expert's words"
272
399
 
@@ -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: result.state.nil? ? nil : Doors::JsonDoor.materialize(result.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
@@ -25,7 +25,7 @@ module Hecks
25
25
  # @param root [String] the checkout the checks run in
26
26
  # @return [Integer] 0 when every check passed, 1 when one failed, 2 on bad usage
27
27
  def main(argv, root: Tools::ROOT, **)
28
- stages = YAML.load_file(STAGES_FILE)
28
+ stages = YAML.load_file(STAGES_FILE, aliases: true)
29
29
  return list(stages) if argv == ["--list"]
30
30
 
31
31
  name = argv.find { |arg| !arg.include?("=") && !arg.start_with?("--") }