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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
  3. data/lib/hecks/cli/check_era.rb +3 -39
  4. data/lib/hecks/doors/mcp_door.rb +16 -2
  5. data/lib/hecks/doors/mcp_door_scope.rb +43 -4
  6. data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
  7. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
  8. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
  9. data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
  10. data/lib/hecks/hecks/adapters/site_toolchain.rb +21 -11
  11. data/lib/hecks/language/bluebook/vocabulary.bluebook +7 -7
  12. data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
  13. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
  14. data/lib/hecks/projections/deploy/box/settings.rb +23 -2
  15. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
  16. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
  17. data/lib/hecks/projections/deploy/box.rb +13 -2
  18. data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
  19. data/lib/hecks/projections/site/edge/checks.rb +31 -5
  20. data/lib/hecks/projections/site/edge.rb +25 -11
  21. data/lib/hecks/projections/site/routes_ts.rb +50 -27
  22. data/lib/hecks/projections/site/site_cdn.rb +7 -4
  23. data/lib/hecks/projections/site/table/checks.rb +53 -7
  24. data/lib/hecks/projections/site/table/link_builder.rb +64 -0
  25. data/lib/hecks/projections/site/table/row_builder.rb +21 -6
  26. data/lib/hecks/projections/site/table.rb +16 -5
  27. data/lib/hecks/rust_build/kernel_input.rb +18 -6
  28. data/lib/hecks/rust_build/project_rust.rb +6 -132
  29. data/lib/hecks/site/bluebook/site.bluebook +27 -13
  30. data/lib/hecks/storehouse.rb +35 -1
  31. data/lib/hecks/tools/comment_style.rb +4 -2
  32. data/lib/hecks/tools/rust_comment_style.rb +1 -1
  33. data/lib/hecks/tools/site_routes.rb +69 -28
  34. data/lib/hecks/version.rb +1 -1
  35. data/lib/hecks/vocabulary.rb +1 -1
  36. data/lib/hecks.rb +1 -0
  37. data/rust/build/src/lineage_pass.rs +1 -1
  38. data/rust/build/src/pipeline.rs +1 -1
  39. data/rust/codegen/src/attr.rs +1 -1
  40. data/rust/codegen/src/bridging.rs +1 -1
  41. data/rust/codegen/src/commands.rs +1 -1
  42. data/rust/codegen/src/constraints.rs +1 -1
  43. data/rust/codegen/src/dependency_planning.rs +1 -1
  44. data/rust/codegen/src/domain_generator.rs +2 -2
  45. data/rust/codegen/src/exemplar.rs +1 -1
  46. data/rust/codegen/src/expr_emitter.rs +1 -1
  47. data/rust/codegen/src/fielded.rs +1 -1
  48. data/rust/codegen/src/json_codec.rs +1 -1
  49. data/rust/codegen/src/main.rs +3 -4
  50. data/rust/codegen/src/mutations.rs +1 -1
  51. data/rust/codegen/src/naming.rs +1 -1
  52. data/rust/codegen/src/ports.rs +1 -1
  53. data/rust/codegen/src/queries.rs +3 -3
  54. data/rust/codegen/src/reactions.rs +1 -1
  55. data/rust/codegen/src/read_models.rs +2 -2
  56. data/rust/codegen/src/reference_specs.rs +1 -1
  57. data/rust/codegen/src/registry.rs +4 -4
  58. data/rust/codegen/src/shared.rs +1 -1
  59. data/rust/codegen/src/skip_reason.rs +1 -1
  60. data/rust/codegen/src/types.rs +4 -4
  61. data/rust/host/HECKS_RELEASE +1 -1
  62. data/rust/host/src/needs.rs +94 -27
  63. data/rust/src/exemplar/commands.rs +1 -1
  64. data/rust/src/exemplar/constraints.rs +1 -1
  65. data/rust/src/exemplar/fielded.rs +1 -1
  66. data/rust/src/exemplar/json.rs +1 -1
  67. data/rust/src/exemplar/mod.rs +1 -1
  68. data/rust/src/exemplar/mutations.rs +1 -1
  69. data/rust/src/exemplar/queries.rs +1 -1
  70. data/rust/src/exemplar/reactions.rs +1 -1
  71. data/rust/src/exemplar/read_models.rs +1 -1
  72. data/rust/src/exemplar/registry.rs +1 -1
  73. data/rust/src/exemplar/types.rs +1 -1
  74. data/rust/src/kernel/cli.rs +2 -2
  75. data/rust/src/kernel/read_model.rs +1 -1
  76. data/rust/src/lib.rs +1 -1
  77. metadata +3 -23
  78. data/rust/project/bridging.rb +0 -257
  79. data/rust/project/commands.rb +0 -877
  80. data/rust/project/constraints.rb +0 -86
  81. data/rust/project/dependency_planning.rb +0 -178
  82. data/rust/project/domain_generator.rb +0 -983
  83. data/rust/project/exemplar.rb +0 -148
  84. data/rust/project/expr_emitter.rb +0 -72
  85. data/rust/project/fielded.rb +0 -141
  86. data/rust/project/json_codec.rb +0 -641
  87. data/rust/project/mutations.rb +0 -695
  88. data/rust/project/naming.rb +0 -223
  89. data/rust/project/ports.rb +0 -65
  90. data/rust/project/queries.rb +0 -493
  91. data/rust/project/reactions.rb +0 -317
  92. data/rust/project/read_models.rb +0 -690
  93. data/rust/project/reference_specs.rb +0 -27
  94. data/rust/project/registry.rb +0 -396
  95. data/rust/project/skip_reason.rb +0 -29
  96. data/rust/project/types.rb +0 -225
  97. data/rust/project/write_if_changed.rb +0 -7
  98. data/rust/project.rb +0 -30
  99. data/rust/project_rust_pipeline.rb +0 -391
@@ -1,317 +0,0 @@
1
- module RustProjection
2
- module Projector
3
- module_function
4
-
5
- # Re-derives event_qualifier/event_name from on_event the same way
6
- # Policy#event_qualifier/#event_name do — the wire only carries on_event whole.
7
- def policy_event_qualifier(on_event)
8
- text = on_event.to_s
9
- text.include?(".") ? text.split(".", 2).first : nil
10
- end
11
-
12
- def policy_event_name(on_event)
13
- text = on_event.to_s
14
- text.include?(".") ? text.split(".", 2).last : text
15
- end
16
-
17
- # Emits PolicyRule rows for policies targeting domain_name only; a policy
18
- # targeting another domain routes through emit_cross_domain_policy_table.
19
- def emit_policy_table(domain_name, policies, aggregates = [])
20
- fns = where_fns(policies)
21
- rows = local_policy_rows(domain_name, policies, aggregates)
22
-
23
- Exemplar.render(
24
- "policy_table",
25
- 'crate::kernel::PolicyRule { policy_name: "tmpl_policy_name", event_name: "tmpl_event_name", event_qualifier: None, target_verb: "tmpl_target_verb", for_each: None, for_each_key: None, with_spec: &[], where_expr: None },' => rows.join("\n")
26
- ).then { |table| fns.empty? ? table : "#{table}\n\n#{fns.join("\n\n")}" }
27
- end
28
-
29
- # Merges policy rows across the target domain and every attached chapter;
30
- # each source is qualified against its own domain_name/aggregates.
31
- def emit_merged_policy_table(sources)
32
- rows = sources.flat_map { |source| local_policy_rows(source[:domain_name], source[:policies], source[:aggregates]) }
33
- fns = sources.flat_map { |source| where_fns(source[:policies]) }
34
-
35
- Exemplar.render(
36
- "policy_table",
37
- 'crate::kernel::PolicyRule { policy_name: "tmpl_policy_name", event_name: "tmpl_event_name", event_qualifier: None, target_verb: "tmpl_target_verb", for_each: None, for_each_key: None, with_spec: &[], where_expr: None },' => rows.join("\n")
38
- ).then { |table| fns.empty? ? table : "#{table}\n\n#{fns.join("\n\n")}" }
39
- end
40
-
41
- def local_policy_rows(domain_name, policies, aggregates = [])
42
- policies.filter_map do |policy|
43
- target_domain = policy[:target_domain] || domain_name
44
- next nil unless target_domain == domain_name
45
-
46
- event_name = policy_event_name(policy[:on_event])
47
- qualifier = policy_event_qualifier(policy[:on_event])
48
- qualifier_expr = qualifier ? "Some(#{qualifier.inspect})" : "None"
49
- target_verb = "#{target_domain}::#{policy[:trigger_command]}"
50
-
51
- # policy_name lets orchestrate.rs's reaction_log/saga_log name the
52
- # policy that fired, matching what CrossDomainPolicyRule already carries.
53
- " crate::kernel::PolicyRule { policy_name: #{policy[:name].to_s.inspect}, event_name: #{event_name.inspect}, " \
54
- "event_qualifier: #{qualifier_expr}, target_verb: #{target_verb.inspect}, " \
55
- "for_each: #{fan_out_verb_expr(domain_name, policy)}, " \
56
- "for_each_key: #{fan_out_key_expr(domain_name, policy, aggregates)}, " \
57
- "with_spec: #{with_spec_expr(policy)}, where_expr: #{where_expr(policy)} },"
58
- end
59
- end
60
-
61
- # where_expr is a generated function, not a literal: PolicyRule is a const
62
- # row but Expr owns boxes, so a where { ... } predicate compiles to a callback.
63
- def where_fn_name(policy) = "where_#{dispatch_fn_name(rust_ident(policy[:name].to_s))}"
64
-
65
- def where_expr(policy)
66
- policy[:where].to_s.empty? ? "None" : "Some(#{where_fn_name(policy)})"
67
- end
68
-
69
- def where_fns(policies)
70
- policies.reject { |policy| policy[:where].to_s.empty? }.map do |policy|
71
- "fn #{where_fn_name(policy)}() -> crate::kernel::Expr {\n" \
72
- " use crate::kernel::Expr;\n" \
73
- " #{ExprEmitter.emit_ast(policy[:where_ast])}\n" \
74
- "}"
75
- end
76
- end
77
-
78
- # Resolves a bare "Aggregate.query" for_each spelling against the policy's
79
- # own domain here, so the kernel's own lookup stays a plain table hit.
80
- def fan_out_verb_expr(domain_name, policy)
81
- for_each = policy[:for_each].to_s
82
- return "None" if for_each.empty?
83
-
84
- "Some(#{(for_each.include?("::") ? for_each : "#{domain_name}::#{for_each}").inspect})"
85
- end
86
-
87
- # Addressing key: a command declared on the aggregate it references uses
88
- # its bare reference key; one merely holding a reference uses that attribute.
89
- def fan_out_key_expr(domain_name, policy, aggregates)
90
- for_each = policy[:for_each].to_s
91
- return "None" if for_each.empty?
92
-
93
- path, = for_each.split(".", 2)
94
- row_aggregate = path.to_s.include?("::") ? path.split("::", 2).last : path
95
- command = target_command_for(policy, aggregates)
96
- key = command && addressing_key_for(command, row_aggregate)
97
- key ? "Some(#{key.to_s.inspect})" : "None"
98
- end
99
-
100
- def target_command_for(policy, aggregates)
101
- aggregate_name, command_name = policy[:trigger_command].to_s.split(".", 2)
102
- aggregate = Array(aggregates).find { |candidate| candidate[:name].to_s == aggregate_name.to_s }
103
- Array(aggregate && aggregate[:commands]).find { |candidate| candidate[:name].to_s == command_name.to_s }
104
- end
105
-
106
- def addressing_key_for(command, aggregate_name)
107
- return snake_case(aggregate_name) if command[:references].to_s == aggregate_name.to_s
108
-
109
- held = Array(command[:attributes]).find { |attribute| attribute[:type].to_s == "Reference<#{aggregate_name}>" }
110
- held && held[:name]
111
- end
112
-
113
- def snake_case(name)
114
- name.to_s.gsub(/([a-z\d])([A-Z])/, '\\1_\\2').gsub(/([A-Z]+)([A-Z][a-z])/, '\\1_\\2').downcase
115
- end
116
-
117
- # Each binding rides the wire already rendered (Literal::render, so a
118
- # Symbol keeps its leading colon and stays distinct from a same-spelled string).
119
- def with_spec_expr(policy)
120
- pairs = Array(policy[:with_spec])
121
- return "&[]" if pairs.empty?
122
-
123
- "&[#{pairs.map { |key, value| "(#{key.to_s.inspect}, #{value.to_s.inspect})" }.join(', ')}]"
124
- end
125
-
126
- # target_verb is always fully qualified ("Domain::Aggregate.Command") —
127
- # the same form dispatch_by_name and every cross-Lambda caller expect.
128
- def cross_domain_policy_rows(domain_name, policies)
129
- policies.filter_map do |policy|
130
- target_domain = policy[:target_domain] || domain_name
131
- next nil if target_domain == domain_name
132
-
133
- event_name = policy_event_name(policy[:on_event])
134
- qualifier = policy_event_qualifier(policy[:on_event])
135
- qualifier_expr = qualifier ? "Some(#{qualifier.inspect})" : "None"
136
- target_verb = "#{target_domain}::#{policy[:trigger_command]}"
137
-
138
- " crate::kernel::CrossDomainPolicyRule { policy_name: #{policy[:name].to_s.inspect}, event_name: #{event_name.inspect}, " \
139
- "event_qualifier: #{qualifier_expr}, target_domain: #{target_domain.inspect}, target_verb: #{target_verb.inspect}, " \
140
- "where_expr: #{where_expr(policy)} },"
141
- end
142
- end
143
-
144
- def emit_cross_domain_policy_table(domain_name, policies)
145
- rows = cross_domain_policy_rows(domain_name, policies)
146
-
147
- puts "cross-domain policy table: #{rows.size} row(s) — delivered by rust/host's lambda_client.rs, not locally dispatched" if rows.any?
148
-
149
- Exemplar.render(
150
- "cross_domain_policy_table",
151
- 'crate::kernel::CrossDomainPolicyRule { policy_name: "tmpl_policy_name", event_name: "tmpl_event_name", event_qualifier: None, target_domain: "tmpl_target_domain", target_verb: "tmpl_target_verb", where_expr: None },' =>
152
- rows.join("\n")
153
- )
154
- end
155
-
156
- # Merges cross-domain rows across every attached chapter the same way
157
- # emit_merged_policy_table merges local ones; aggregates are unused here.
158
- def emit_merged_cross_domain_policy_table(sources)
159
- rows = sources.flat_map { |source| cross_domain_policy_rows(source[:domain_name], source[:policies]) }
160
-
161
- puts "cross-domain policy table: #{rows.size} row(s) — delivered by rust/host's lambda_client.rs, not locally dispatched" if rows.any?
162
-
163
- Exemplar.render(
164
- "cross_domain_policy_table",
165
- 'crate::kernel::CrossDomainPolicyRule { policy_name: "tmpl_policy_name", event_name: "tmpl_event_name", event_qualifier: None, target_domain: "tmpl_target_domain", target_verb: "tmpl_target_verb", where_expr: None },' =>
166
- rows.join("\n")
167
- )
168
- end
169
-
170
- # Reuses Marks.read (mutations.rb's append_field_source is the same round
171
- # trip) rather than re-deriving the with: literal's wire spelling.
172
- def with_value_parsed(raw)
173
- Hecks::Bluebook::Assembly::Marks.read(raw)
174
- end
175
-
176
- # Builds a Json value expression rather than a const literal: Json holds
177
- # Vec/String, which isn't const-constructible in stable Rust.
178
- def json_literal_expr(value)
179
- case value
180
- when Hash
181
- fields = value.map { |k, v| "(#{k.to_s.inspect}.to_string(), #{json_literal_expr(v)})" }
182
- "crate::kernel::Json::Object(vec![#{fields.join(', ')}])"
183
- when String
184
- "crate::kernel::Json::Str(#{value.inspect}.to_string())"
185
- when Integer
186
- "crate::kernel::Json::int(#{value})"
187
- when Float
188
- "crate::kernel::Json::Num(#{value}f64, None)"
189
- when true, false
190
- "crate::kernel::Json::Bool(#{value})"
191
- when nil
192
- "crate::kernel::Json::Null"
193
- else
194
- raise "unsupported with: literal #{value.inspect} — json_literal_expr doesn't cover this shape"
195
- end
196
- end
197
-
198
- # A bare Symbol is a runtime WithValue::Ref; anything else is a literal,
199
- # emitted as its own fn() -> Json collected into literal_fns.
200
- def emit_with_value(raw, literal_fns)
201
- parsed = with_value_parsed(raw)
202
- return "crate::kernel::WithValue::Ref(#{parsed.to_s.inspect})" if parsed.is_a?(Symbol)
203
-
204
- fn_name = "pm_literal_#{literal_fns.size}"
205
- literal_fns << Exemplar.render(
206
- "with_value_literal_fn",
207
- "tmpl_literal_fn" => fn_name,
208
- "tmpl_body_placeholder()" => json_literal_expr(parsed)
209
- )
210
- "crate::kernel::WithValue::Literal(#{fn_name})"
211
- end
212
-
213
- # compensates recurses into itself at most one level: a compensation is
214
- # never itself compensable.
215
- def emit_dispatch_spec(spec, literal_fns)
216
- with_pairs = spec[:with_spec].map { |key, raw| "(#{key.to_s.inspect}, #{emit_with_value(raw, literal_fns)})" }
217
- compensates = spec[:compensates] ? "Some(&#{emit_dispatch_spec(spec[:compensates], literal_fns)})" : "None"
218
- "crate::kernel::DispatchSpec { command_name: #{spec[:command_name].inspect}, with: &[#{with_pairs.join(', ')}], compensates: #{compensates} }"
219
- end
220
-
221
- def emit_handler(handler, literal_fns)
222
- dispatches = handler[:dispatches].map { |d| emit_dispatch_spec(d, literal_fns) }
223
- "crate::kernel::Handler { event_type: #{handler[:event_type].inspect}, from_state: #{handler[:from_state].inspect}, to_state: #{handler[:to_state].inspect}, dispatches: &[#{dispatches.join(', ')}] }"
224
- end
225
-
226
- # No cross-domain narrowing needed here the way emit_policy_table needs
227
- # one: every dispatch's command_name is already domain-qualified on the wire.
228
- def emit_process_manager_table(process_managers)
229
- literal_fns = []
230
- pm_exprs = process_managers.map do |pm|
231
- handlers = pm[:handlers].map { |h| emit_handler(h, literal_fns) }
232
- "crate::kernel::ProcessManagerDef { name: #{pm[:name].inspect}, correlates_by: #{pm[:correlates_by].inspect}, " \
233
- "starts_on: #{pm[:starts_on].inspect}, ends_on: #{pm[:ends_on].inspect}, initial_state: #{pm[:states].first.inspect}, " \
234
- "handlers: &[#{handlers.join(', ')}] }"
235
- end
236
-
237
- Exemplar.render(
238
- "process_manager_table",
239
- "fn tmpl_literal_fns_placeholder() {}" => literal_fns.join("\n"),
240
- ' crate::kernel::ProcessManagerDef { name: "tmpl_pm_name", correlates_by: "tmpl_correlates_by", starts_on: "tmpl_starts_on", ends_on: "tmpl_ends_on", initial_state: "tmpl_initial_state", handlers: &[] },' =>
241
- pm_exprs.map { |e| " #{e}," }.join("\n")
242
- )
243
- end
244
-
245
- # Precomputes Naming.reference_key(event.aggregate) per aggregate for
246
- # kernel::orchestrate's correlation fallback, keyed by qualified aggregate name.
247
- def emit_reference_key_table(chapters)
248
- arms = chapters.flat_map do |domain_name, aggregate_names|
249
- aggregate_names.map do |name|
250
- qualified = "#{domain_name}::#{name}"
251
- key = Hecks::Naming.snake(name)
252
- " #{qualified.inspect} => Some(#{key.inspect}),"
253
- end
254
- end
255
-
256
- Exemplar.render("reference_key_table", '"tmpl_qualified" => Some("tmpl_key"),' => arms.join("\n"))
257
- end
258
-
259
- # Whether a verb's dispatch creates its own record, needed before
260
- # orchestrate.rs's routing split can promote an addressing key into to:.
261
- def emit_creates_table(aggregates)
262
- arms = aggregates.flat_map do |aggregate|
263
- (Array(aggregate[:commands]).map { |c| [c[:verb], c[:creates]] } +
264
- Array(aggregate[:entity_commands]).map { |c| [c[:verb], false] }).map do |verb, creates|
265
- " #{verb.inspect} => #{creates},"
266
- end
267
- end
268
-
269
- Exemplar.render("creates_table", '"tmpl_verb" => true,' => arms.join("\n"))
270
- end
271
-
272
- # Single-component identity only; a composite identity (more than one
273
- # component) is a known gap, skipped rather than guessed at.
274
- def emit_identity_head_table(aggregates)
275
- arms = aggregates.filter_map do |aggregate|
276
- heads = Array(aggregate[:identified_by])
277
- next if heads.size != 1
278
-
279
- head = heads.first.to_s.split(".").first
280
- qualified = "#{aggregate[:domain_name]}::#{aggregate[:name]}"
281
- " #{qualified.inspect} => Some(#{head.inspect}),"
282
- end
283
-
284
- Exemplar.render("identity_head_table", '"tmpl_qualified" => Some("tmpl_head"),' => arms.join("\n"))
285
- end
286
-
287
- # Entity-level counterpart to emit_identity_head_table, keyed by
288
- # "Domain::Aggregate.Entity"; composite identities are out of scope.
289
- def emit_entity_identity_head_table(aggregates)
290
- arms = aggregates.flat_map do |aggregate|
291
- Array(aggregate[:entities]).filter_map do |entity|
292
- heads = Array(entity[:identified_by])
293
- next if heads.size != 1
294
-
295
- head = heads.first.to_s.split(".").first
296
- qualified = "#{aggregate[:domain_name]}::#{aggregate[:name]}.#{entity[:name]}"
297
- " #{qualified.inspect} => Some(#{head.inspect}),"
298
- end
299
- end
300
-
301
- Exemplar.render("entity_identity_head_table", '"tmpl_qualified" => Some("tmpl_head"),' => arms.join("\n"))
302
- end
303
-
304
- # Declared attribute names per verb, used to filter a policy/process-
305
- # manager dispatch's with: facts down to what the target command declares.
306
- def emit_command_attributes_table(aggregates)
307
- arms = aggregates.flat_map do |aggregate|
308
- (Array(aggregate[:commands]) + Array(aggregate[:entity_commands])).map do |c|
309
- names = Array(c[:attributes]).map { |name| name.to_s.inspect }.join(", ")
310
- " #{c[:verb].inspect} => &[#{names}],"
311
- end
312
- end
313
-
314
- Exemplar.render("command_attributes_table", '"tmpl_verb" => &["tmpl_attr"],' => arms.join("\n"))
315
- end
316
- end
317
- end