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
@@ -1,225 +0,0 @@
1
- module RustProjection
2
- module Projector
3
- module_function
4
-
5
- # `check_invariants` mirroring `Value.build` (runtime/value/coercion.rb). `offered` sorts only
6
- # the top-level fields, matching Ruby's `canonical_fields`.
7
- def emit_check_invariants(vo, value_objects_by_name, aggregates_by_name)
8
- name = rust_ident(vo[:name])
9
- type_name = vo[:name].to_s
10
-
11
- # Ruby's order (`Value.build`, coercion.rb): nested value objects, then field
12
- # constraints, then invariants last, so a failed pattern is a `TypeMismatch`.
13
- body = []
14
-
15
- vo[:attributes].each do |attr|
16
- next if SCALAR.key?(attr[:type])
17
-
18
- nested = value_objects_by_name[attr[:type]]
19
- next unless nested && !nested[:closed_set]
20
-
21
- field = rust_ident_field(attr[:name])
22
- body << (attr[:list] ? " for item in &self.#{field} { item.check_invariants()?; }" : " self.#{field}.check_invariants()?;")
23
- end
24
-
25
- # `pattern:`/`admits:` declared on the value object's own fields; command-argument
26
- # usage-level `admits:` is handled in commands.rb.
27
- vo[:attributes].each do |attr|
28
- next if attr[:list]
29
-
30
- field = "self.#{rust_ident_field(attr[:name])}"
31
- admits_line = emit_admits_check(field, attr, aggregates_by_name, value_objects_by_name)
32
- body << " #{admits_line}" if admits_line
33
- pattern_line = emit_pattern_check(field, attr, name, value_objects_by_name)
34
- body << " #{pattern_line}" if pattern_line
35
- end
36
-
37
- body += vo[:invariants].map do |inv|
38
- expr = ExprEmitter.emit_ast(inv[:ast])
39
- <<~RUST.rstrip
40
- {
41
- let ctx = crate::kernel::EvalContext { args: &crate::kernel::NoFields, instance: self };
42
- if !crate::kernel::interpret(&#{expr}, &ctx)?.truthy() {
43
- let mut offered = self.to_json();
44
- if let crate::kernel::Json::Object(fields) = &mut offered {
45
- fields.sort_by(|a, b| a.0.cmp(&b.0));
46
- }
47
- let offered = offered.to_json_string();
48
- return Err(crate::kernel::Refusal::InvariantViolation(crate::kernel::refusal_wording::InvariantViolationValueObjectInvariantArgs {
49
- name: #{type_name.inspect},
50
- description: #{rust_string_literal(inv[:description])},
51
- offered: offered.as_str(),
52
- }.render_args()));
53
- }
54
- }
55
- RUST
56
- end
57
-
58
- <<~RUST
59
- impl #{name} {
60
- pub fn check_invariants(&self) -> Result<(), crate::kernel::Refusal> {
61
- #{body.join("\n")}
62
- Ok(())
63
- }
64
- }
65
- RUST
66
- end
67
-
68
- # A multi-field closed set: a plain struct plus a `pub const` array of members.
69
- # No `Fielded` or `check_invariants`.
70
- def emit_closed_set_table(vo)
71
- name = rust_ident(vo[:name])
72
-
73
- field_subs_list = vo[:attributes].map do |attr|
74
- type = rust_type(attr[:type], list: attr[:list])
75
- type = "&'static str" if type == "String" # static data — a borrowed literal, not an owned String
76
- { "TmplFieldType" => type, "tmpl_field" => rust_ident_field(attr[:name]) }
77
- end
78
- struct_part = Exemplar.compose("plain_struct", { "TmplType" => name }, field_id: "struct_field", field_subs_list: field_subs_list)
79
-
80
- # Rows carry only the fields they set; missing ones fall back to `default:` or "".
81
- member_literals = vo[:members].map do |row|
82
- present = row.to_h { |field_name, value| [field_name.to_s, value] }
83
- fields = vo[:attributes].map do |attr|
84
- field_name = attr[:name].to_s
85
- raw = present.key?(field_name) ? present[field_name] : (attr[:default] || "")
86
- literal = case attr[:type]
87
- when "Integer" then raw.to_i.to_s
88
- when "Float" then "#{raw.to_f}f64"
89
- else rust_string_literal(raw.to_s) # String, or an unrecognized type — treated as text, not silently dropped
90
- end
91
- Exemplar.render("closed_set_table_row_field", "tmpl_field" => rust_ident_field(attr[:name]), "tmpl_value_placeholder()" => literal)
92
- end.join(", ")
93
- " #{name} { #{fields} },"
94
- end
95
-
96
- "#{struct_part}\n\npub const #{screaming_snake(vo[:name])}: &[#{name}] = &[\n#{member_literals.join("\n")}\n];"
97
- end
98
-
99
- def emit_value_object(vo, value_objects_by_name, aggregates_by_name)
100
- name = rust_ident(vo[:name])
101
-
102
- if vo[:closed_set]
103
- # One field is an enum tag. Several are a data table: members can share any
104
- # single field (Keyword rows share `word`), so no one field can be the tag.
105
- return emit_closed_set_table(vo) if vo[:attributes].size > 1
106
-
107
- variants = vo[:members].map { |row| closed_set_variant(row) }
108
- enum_part = Exemplar.compose(
109
- "closed_set_enum",
110
- { "TmplKind" => name },
111
- field_id: "closed_set_enum:VARIANT",
112
- field_subs_list: variants.map { |v| { "TmplMemberA" => v } }
113
- )
114
- # Consumer of `fielded_capable_nested?` (naming.rb).
115
- return "#{enum_part}\n\n#{emit_closed_set_fielded_impl(vo)}"
116
- end
117
-
118
- field_subs_list = vo[:attributes].map do |attr|
119
- type = rust_type(attr[:type], list: attr[:list])
120
- type = "Option<#{type}>" if attr[:optional]
121
- { "TmplFieldType" => type, "tmpl_field" => rust_ident_field(attr[:name]) }
122
- end
123
- struct_part = Exemplar.compose("plain_struct", { "TmplType" => name }, field_id: "struct_field", field_subs_list: field_subs_list)
124
-
125
- [
126
- struct_part,
127
- emit_fielded_flat(name, vo[:attributes], value_objects_by_name,
128
- entity_names: aggregates_by_name.values.flat_map { |a| a[:entities].map { |e| e[:name] } }),
129
- emit_check_invariants(vo, value_objects_by_name, aggregates_by_name),
130
- ].join("\n\n")
131
- end
132
-
133
- def unsupported_attribute_types(aggregate, value_objects_by_name)
134
- entity_names = aggregate[:entities].map { |e| e[:name] }
135
- aggregate[:attributes]
136
- .reject do |attr|
137
- effective_scalar_type(attr[:type]) || value_objects_by_name.key?(attr[:type]) ||
138
- (attr[:list] && entity_names.include?(attr[:type]))
139
- end
140
- .map { |attr| attr[:type] }
141
- .uniq
142
- end
143
-
144
- # An entity as a struct with a `Fielded` impl and no `check_invariants`.
145
- # Commands addressing one element by identity are not generated; this only enables `append`.
146
- def emit_entity(entity, value_objects_by_name)
147
- name = rust_ident(entity[:name])
148
- field_subs_list = entity[:attributes].map do |attr|
149
- type = rust_type(attr[:type], list: attr[:list])
150
- # `optional: true` becomes `Option<T>`; `command_skip_reason` (bridging.rb) skips
151
- # optional sources feeding a non-optional field.
152
- type = "Option<#{type}>" if attr[:optional]
153
- { "TmplFieldType" => type, "tmpl_field" => rust_ident_field(attr[:name]) }
154
- end
155
- field_subs_list << { "TmplFieldType" => "String", "tmpl_field" => rust_ident_field(entity[:lifecycle][:field]) } if entity[:lifecycle]
156
- struct_part = Exemplar.compose("plain_struct", { "TmplType" => name }, field_id: "struct_field", field_subs_list: field_subs_list)
157
-
158
- # The entity's TransitionCheck reads the lifecycle field generically, in the flat shape.
159
- lifecycle_arm =
160
- if entity[:lifecycle]
161
- field = rust_field(entity[:lifecycle][:field])
162
- ident = rust_ident_field(entity[:lifecycle][:field])
163
- [%( "#{field}" => Some(Field::Value(Value::Str(self.#{ident}.clone()))),)]
164
- else
165
- []
166
- end
167
- "#{struct_part}\n\n#{emit_fielded_flat(name, entity[:attributes], value_objects_by_name, extra_arms: lifecycle_arm)}"
168
- end
169
-
170
- def emit_record(aggregate, value_objects_by_name)
171
- name = rust_ident(aggregate[:name])
172
- field_subs_list = aggregate[:attributes].map do |attr|
173
- # Optional because a creating command may not set every field. A list field stays a
174
- # `Vec<T>` unless `list_attr_creation_optional?`.
175
- type = rust_type(attr[:type], list: attr[:list])
176
- type = "Option<#{type}>" if !attr[:list] || list_attr_creation_optional?(aggregate, attr[:name], value_objects_by_name)
177
- { "TmplFieldType" => type, "tmpl_field" => rust_ident_field(attr[:name]) }
178
- end
179
- field_subs_list << { "TmplFieldType" => "String", "tmpl_field" => rust_ident_field(aggregate[:lifecycle][:field]) } if aggregate[:lifecycle]
180
- # One plain `bool` field per event a command `corrects` (see `corrects_flag_field`).
181
- correctable_event_names(aggregate).each { |ev| field_subs_list << { "TmplFieldType" => "bool", "tmpl_field" => corrects_flag_field(ev) } }
182
- struct_part = Exemplar.compose("plain_struct", { "TmplType" => name }, field_id: "struct_field", field_subs_list: field_subs_list)
183
-
184
- "#{struct_part}\n\n#{emit_fielded_record(aggregate, value_objects_by_name)}"
185
- end
186
-
187
- # `projects` fields become always-optional String attributes. The caller merges them onto
188
- # `aggregate[:attributes]` for record emission only, never for command Args or from_json_flat.
189
- def projected_field_pseudo_attributes(aggregate)
190
- (aggregate[:projected_fields] || []).map do |field|
191
- { name: field[:name], type: "String", list: false, optional: true }
192
- end
193
- end
194
-
195
- # `impl SetProjectedField`: one arm per `projects` field, empty match when there are none,
196
- # so dispatch can require the bound unconditionally.
197
- def emit_set_projected_field(aggregate)
198
- name = rust_ident(aggregate[:name])
199
- arms = (aggregate[:projected_fields] || []).map do |field|
200
- ident = rust_ident_field(field[:name])
201
- " #{field[:name].inspect} => self.#{ident} = value,"
202
- end
203
- <<~RUST
204
- impl crate::kernel::SetProjectedField for #{name} {
205
- fn set_projected_field(&mut self, name: &'static str, value: Option<String>) {
206
- match name {
207
- #{arms.join("\n")}
208
- _ => {}
209
- }
210
- }
211
- }
212
- RUST
213
- end
214
-
215
- # `<NAME>_PROJECTED_FIELDS`: one `ProjectedFieldSpec` row per `projects` field, empty if none.
216
- def emit_projected_field_table(aggregate)
217
- name = screaming_snake(aggregate[:name])
218
- rows = (aggregate[:projected_fields] || []).map do |field|
219
- " crate::kernel::ProjectedFieldSpec { field: #{field[:name].inspect}, reference: #{field[:reference].inspect}, " \
220
- "remote_field: #{field[:remote_field].inspect} },"
221
- end
222
- "pub static #{name}_PROJECTED_FIELDS: &[crate::kernel::ProjectedFieldSpec] = &[\n#{rows.join("\n")}\n];\n"
223
- end
224
- end
225
- end
@@ -1,7 +0,0 @@
1
- require "hecks/rust_build/write_if_changed"
2
-
3
- module RustProjection
4
- # The write-if-unchanged helper moved to `Hecks::RustBuild::WriteIfChanged`, which the
5
- # codegen-backed `hecks project_rust` also uses; this name stays until this generator is deleted.
6
- WriteIfChanged = Hecks::RustBuild::WriteIfChanged
7
- end
data/rust/project.rb DELETED
@@ -1,30 +0,0 @@
1
- # Build-time generator: turns one domain's canonical IR into Rust source under rust/src/generated/.
2
- # The compiled binary never parses or interprets bluebooks.
3
- #
4
- # hecks project_rust <domain>
5
- #
6
- # The IR arrives JSON-shaped, so mutation ops, targets and attribute names are Strings; compare
7
- # them with `.to_s`. Commands the kernel cannot express are skipped loudly, by name and reason.
8
- module RustProjection
9
- end
10
-
11
- require_relative "project/write_if_changed"
12
- require_relative "project/skip_reason"
13
- require_relative "project/exemplar"
14
- require_relative "project/expr_emitter"
15
- require_relative "project/naming"
16
- require_relative "project/reference_specs"
17
- require_relative "project/constraints"
18
- require_relative "project/fielded"
19
- require_relative "project/json_codec"
20
- require_relative "project/types"
21
- require_relative "project/bridging"
22
- require_relative "project/mutations"
23
- require_relative "project/dependency_planning"
24
- require_relative "project/commands"
25
- require_relative "project/ports"
26
- require_relative "project/registry"
27
- require_relative "project/reactions"
28
- require_relative "project/queries"
29
- require_relative "project/read_models"
30
- require_relative "project/domain_generator"
@@ -1,391 +0,0 @@
1
- require "json"
2
- require "fileutils"
3
- require "open3"
4
- require "tmpdir"
5
-
6
- # Opt-in, all-Rust equivalent of hecks project_rust's default path, selected
7
- # when HECKS_PARSER=rust and HECKS_CODEGEN=rust; never boots a bluebook DSL.
8
- module RustProjectPipeline
9
- ROOT = File.expand_path("..", __dir__).freeze
10
- PARSER_DIR = File.join(ROOT, "rust/parser").freeze
11
- CODEGEN_DIR = File.join(ROOT, "rust/codegen").freeze
12
- PARSER_BIN = File.join(PARSER_DIR, "target/debug/hecks-parse").freeze
13
- CODEGEN_BIN = File.join(CODEGEN_DIR, "target/debug/hecks-codegen").freeze
14
-
15
- module_function
16
-
17
- def call(domain)
18
- ensure_binaries_built!
19
-
20
- target_mod_name = File.basename(domain)
21
- # Becomes a directory name, Rust module identifier, and Cargo feature
22
- # name below (see Projector.valid_domain_mod_name?).
23
- unless RustProjection::Projector.valid_domain_mod_name?(target_mod_name)
24
- abort "hecks project_rust (Rust path): domain name #{target_mod_name.inspect} (from #{domain.inspect}) can't be " \
25
- "used as-is — it has to double as a Rust module identifier and a Cargo feature name, and this one is " \
26
- "either not a plain lowercase identifier, is a Rust keyword, or collides with a reserved Cargo.toml " \
27
- "key (#{RustProjection::Projector::CARGO_RESERVED_DOMAIN_NAMES.join(', ')}). Rename the domain directory."
28
- end
29
- bluebook_paths = Dir.glob(File.join(domain, "bluebook", "*.bluebook")).sort
30
- abort "hecks project_rust (Rust path): #{domain} has no .bluebook files" if bluebook_paths.empty?
31
- hecksagon_path = File.join(domain, "bluebook", "#{target_mod_name}.hecksagon")
32
- hecksagon_path = nil unless File.exist?(hecksagon_path)
33
-
34
- target_chapter_name = header_chapter_name(bluebook_paths.first)
35
- target_bluebooks = bluebook_paths.select { |path| header_chapter_name(path) == target_chapter_name }
36
-
37
- gem_chapter_names, vendored_package_names =
38
- if hecksagon_path
39
- resolved = JSON.parse(run_capture!(PARSER_BIN, "resolve", "--chapter", target_chapter_name, hecksagon_path))
40
- [resolved.fetch("gem_chapters"), resolved.fetch("vendored_packages")]
41
- else
42
- [[], []]
43
- end
44
-
45
- target_files = target_bluebooks + [hecksagon_path].compact
46
- target_ir_text = derive_append_optionals(run_capture!(PARSER_BIN, "chapter", "--chapter", target_chapter_name, *target_files))
47
- # Only the target gets a lineage sidecar — matches the default path,
48
- # which never sets one on a framework chapter either.
49
- target_ir_text = derive_lineage(target_ir_text, hecksagon_path, sibling_world_path(domain, target_mod_name))
50
-
51
- # Mirrors hecks project_rust's own Framework.load! resolution. A framework
52
- # member has no .hecksagon of its own, only its .bluebook.
53
- # A gem chapter that is not a framework member (the language, Tenancy, Deploy) is loaded by
54
- # the Ruby runtime alone, so it is checked against the one table and skipped here.
55
- chapters = gem_chapter_names.filter_map do |fw_name|
56
- next if Hecks::Chapters.index.key?(fw_name) && !Hecks::Framework.members.key?(fw_name)
57
-
58
- fw_path = Hecks::Framework.members.fetch(fw_name) do
59
- abort "hecks project_rust (Rust path): attaches #{fw_name.inspect} names no chapter the gem carries — known: #{Hecks::Chapters.table.keys.sort.join(', ')}"
60
- end
61
- fw_chapter_name = header_chapter_name(fw_path)
62
- unless fw_chapter_name == fw_name
63
- abort "hecks project_rust (Rust path): #{fw_path} declares chapter #{fw_chapter_name.inspect}, but attaches named #{fw_name.inspect}"
64
- end
65
- fw_ir_text = derive_append_optionals(run_capture!(PARSER_BIN, "chapter", "--chapter", fw_chapter_name, fw_path))
66
- {
67
- mod_name: fw_name.downcase,
68
- source_label: "#{domain} (attaches #{fw_name.inspect})",
69
- ir_text: fw_ir_text
70
- }
71
- end
72
-
73
- # Mirrors EmbryonautBluebook.load!: every .bluebook file directly under
74
- # vendor/embryonaut_bluebooks/<name>/bluebook/, sorted. A vendored
75
- # package has no .hecksagon of its own either, just its .bluebook(s).
76
- chapters += vendored_package_names.map do |pkg_name|
77
- unless pkg_name.to_s.match?(Hecks::EmbryonautBluebook::PACKAGE_NAME)
78
- abort "hecks project_rust (Rust path): attaches #{pkg_name.inspect}, from: :vendor is not a package " \
79
- "name — it must match [a-z][a-z0-9_]* (the name `hecks vendor` accepts)"
80
- end
81
- pkg_dir = File.join(domain, "vendor", "embryonaut_bluebooks", pkg_name.to_s, "bluebook")
82
- pkg_files = Dir.glob(File.join(pkg_dir, "*.bluebook")).sort
83
- if pkg_files.empty?
84
- abort "hecks project_rust (Rust path): attaches #{pkg_name.inspect}, from: :vendor names no vendored " \
85
- "bluebook at #{pkg_dir} — run hecks vendor #{pkg_name}"
86
- end
87
- pkg_chapter_name = header_chapter_name(pkg_files.first)
88
- expected_chapter_name = Hecks::Naming.pascal(pkg_name.to_s)
89
- unless pkg_chapter_name == expected_chapter_name
90
- abort "hecks project_rust (Rust path): #{pkg_files.first} declares chapter #{pkg_chapter_name.inspect}, but " \
91
- "attaches #{pkg_name.inspect}, from: :vendor expects #{expected_chapter_name.inspect}"
92
- end
93
- pkg_bluebooks = pkg_files.select { |path| header_chapter_name(path) == pkg_chapter_name }
94
- pkg_ir_text = derive_append_optionals(run_capture!(PARSER_BIN, "chapter", "--chapter", pkg_chapter_name, *pkg_bluebooks))
95
- {
96
- # Derived from the declared chapter name, not pkg_name.downcase
97
- # directly — the two diverge once a package name has an underscore.
98
- mod_name: expected_chapter_name.downcase,
99
- source_label: "#{domain} (attaches #{pkg_name.inspect}, from: :vendor)",
100
- ir_text: pkg_ir_text
101
- }
102
- end
103
-
104
- # Framework- and embryonaut_bluebook-derived chapters can collide on
105
- # mod_name (chapters here is a plain Array, not a Hash), so guard
106
- # explicitly against two entries overwriting each other's sidecars.
107
- chapters.group_by { |c| c[:mod_name] }.each_value do |group|
108
- next if group.size == 1
109
-
110
- abort "hecks project_rust (Rust path): #{group.map { |c| c[:source_label] }.join(' and ')} both resolve to the " \
111
- "same generated module #{group.first[:mod_name].inspect} — attach #{target_chapter_name} to at most one " \
112
- "of them"
113
- end
114
-
115
- # The self-hosted grammar's own "Bluebook" chapter, read without
116
- # booting anything.
117
- meta_files = Hecks::Bluebook::MetaValidator::GRAMMAR_FILES
118
- meta_ir_text = derive_append_optionals(run_capture!(PARSER_BIN, "chapter", "--chapter", "Bluebook", *meta_files))
119
-
120
- # `HECKS_RUST_DIR` picks the crate to project into, as it does for hecks project_rust's
121
- # Ruby path; unset, it is this repository's own `rust/`.
122
- rust_dir = ENV.fetch("HECKS_RUST_DIR", File.expand_path("../rust", __dir__))
123
- out_root = File.join(rust_dir, "src/generated")
124
- FileUtils.mkdir_p(out_root)
125
- # Only this run's own directories are cleared — other domains coexist
126
- # on disk.
127
- FileUtils.rm_rf(File.join(out_root, "meta"))
128
- FileUtils.rm_rf(File.join(out_root, "active"))
129
- FileUtils.rm_rf(File.join(out_root, target_mod_name))
130
- chapters.each { |c| FileUtils.rm_rf(File.join(out_root, c[:mod_name])) }
131
-
132
- Dir.mktmpdir("hecks-codegen-full") do |tmp|
133
- meta_ir_path = File.join(tmp, "meta_ir.json")
134
- File.write(meta_ir_path, meta_ir_text)
135
- meta_source_label = "the self-hosted language (lib/hecks/language/bluebook)"
136
- run!(CODEGEN_BIN, "full", out_root, "meta", meta_source_label, meta_ir_path)
137
- write_sidecars!(File.join(out_root, "meta"), meta_ir_text, meta_source_label)
138
-
139
- target_ir_path = File.join(tmp, "target_ir.json")
140
- File.write(target_ir_path, target_ir_text)
141
-
142
- codegen_args = [out_root, target_mod_name, domain, target_ir_path]
143
- chapters.each_with_index do |c, i|
144
- ir_path = File.join(tmp, "chapter_#{i}_ir.json")
145
- File.write(ir_path, c[:ir_text])
146
- codegen_args.concat([c[:mod_name], c[:source_label], ir_path])
147
- end
148
- run!(CODEGEN_BIN, "full", *codegen_args)
149
- end
150
-
151
- write_sidecars!(File.join(out_root, target_mod_name), target_ir_text, domain)
152
- chapters.each { |c| write_sidecars!(File.join(out_root, c[:mod_name]), c[:ir_text], c[:source_label]) }
153
-
154
- sync_mod_and_cargo!(out_root, target_mod_name)
155
- end
156
-
157
- # Applies the same append-optional-field derivation domain_generator.rb
158
- # applies in Ruby — hecks-parse's own ir.json has not had it applied yet.
159
- def derive_append_optionals(ir_text)
160
- ir = JSON.parse(ir_text, symbolize_names: true)
161
- ir[:aggregates].each do |aggregate|
162
- value_objects_by_name = aggregate[:value_objects].to_h { |vo| [vo[:name], vo] }
163
- RustProjection::Projector.mark_append_optional_fields!(aggregate, value_objects_by_name)
164
- end
165
- JSON.pretty_generate(ir)
166
- end
167
-
168
- # Text-based equivalent of Exporter.lineage — no Runtime::Registry boot.
169
- def derive_lineage(ir_text, hecksagon_path, world_path = nil)
170
- ir = JSON.parse(ir_text, symbolize_names: true)
171
- binds = hecksagon_path ? persistence_binds(hecksagon_path) : {}
172
- fallback = (world_path && default_adapter_name(world_path, ir[:name])) || "Memory"
173
- capable_adapters = lineage_capable_adapter_names
174
-
175
- capable = ir[:aggregates].select do |aggregate|
176
- adapter_name = binds.fetch(aggregate[:name], fallback)
177
- capable_adapters.include?(adapter_name)
178
- end
179
-
180
- ir[:lineage] = {
181
- capable_aggregates: capable.map { |aggregate| { name: aggregate[:name], storage_name: Hecks::Naming.snake(aggregate[:name]) } }
182
- }
183
- JSON.pretty_generate(ir)
184
- end
185
-
186
- # Matches only role-less persisted_by binds — the sole authoritative kind.
187
- def persistence_binds(hecksagon_path)
188
- text = File.read(hecksagon_path)
189
- binds = {}
190
- text.each_line do |line|
191
- next if line =~ /\brole:/
192
- next unless line =~ /(?:\w+(?:::\w+)*::)?(\w+)\.persisted_by\(\s*"([^"]+)"/
193
-
194
- binds[Regexp.last_match(1)] = Regexp.last_match(2)
195
- end
196
- binds
197
- end
198
-
199
- # The target's own `.world` file, if it has one — named for the domain the
200
- # same way its `.hecksagon` is.
201
- def sibling_world_path(domain, target_mod_name)
202
- path = File.join(domain, "bluebook", "#{target_mod_name}.world")
203
- path if File.exist?(path)
204
- end
205
-
206
- # A .world file may hold several worlds (one per chapter) — only lines
207
- # inside the target chapter's own Hecks.world block count, and the last
208
- # default_adapter there wins, matching the Ruby builder.
209
- def default_adapter_name(world_path, chapter)
210
- in_target_world = false
211
- found = nil
212
- File.foreach(world_path) do |line|
213
- if (opened = line[/\AHecks\.world[^"]*"([^"]*)"/, 1])
214
- in_target_world = opened == chapter
215
- elsif in_target_world && (name = line[/\A\s*default_adapter\s+"([^"]*)"/, 1])
216
- found = name
217
- end
218
- end
219
- found
220
- end
221
-
222
- # Read off adapter source directly, so a new lineage-capable adapter
223
- # needs no change here.
224
- def lineage_capable_adapter_names
225
- Dir[File.join(ROOT, "lib/hecks/adapters/driven/*.rb")].filter_map do |path|
226
- text = File.read(path)
227
- next unless text.match?(/lineage_capable\?\s*=\s*true/)
228
-
229
- text[/^\s*class\s+(\w+)/, 1]
230
- end.compact
231
- end
232
-
233
- # The declared chapter name off a .bluebook file's own header line.
234
- def header_chapter_name(path)
235
- line = File.foreach(path).find { |l| l =~ /\A\s*Hecks\.bluebook\s+"([^"]+)"/ }
236
- (line && Regexp.last_match(1)) or abort "hecks project_rust (Rust path): #{path} has no 'Hecks.bluebook \"Name\"' header"
237
- end
238
-
239
- # Writes ir.json byte-identical to hecks-parse's own output — never
240
- # reparsed or reformatted, to avoid a non-byte-exact diff for no reason.
241
- def write_sidecars!(mod_dir, ir_text, source_label)
242
- FileUtils.mkdir_p(mod_dir)
243
-
244
- ir_json_path = File.join(mod_dir, "ir.json")
245
- File.write(ir_json_path, ir_text)
246
- puts "wrote #{ir_json_path}"
247
-
248
- metadata_path = File.join(mod_dir, "metadata.rs")
249
- File.open(metadata_path, "w") do |f|
250
- f.puts "// GENERATED by hecks project_rust — #{source_label}'s own canonical IR,"
251
- f.puts "// embedded for runtime self-description. Not read by any dispatch"
252
- f.puts "// function in this module — introspection only."
253
- f.puts "pub const IR_JSON: &str = #{RustProjection::Projector.rust_string_literal(ir_text)};"
254
- end
255
- puts "wrote #{metadata_path}"
256
- end
257
-
258
- def ensure_binaries_built!
259
- build!(PARSER_DIR)
260
- build!(CODEGEN_DIR)
261
- end
262
-
263
- def build!(crate_dir)
264
- ok = system("cargo", "build", chdir: crate_dir, out: $stdout, err: $stderr)
265
- ok or abort "hecks project_rust (Rust path): cargo build failed in #{crate_dir} — run it there directly to see why"
266
- end
267
-
268
- def run_capture!(*cmd)
269
- out, err, status = Open3.capture3(*cmd)
270
- status.success? or abort "hecks project_rust (Rust path): #{cmd.join(' ')} failed:\n#{err}"
271
- out
272
- end
273
-
274
- def run!(*cmd)
275
- ok = system(*cmd)
276
- ok or abort "hecks project_rust (Rust path): #{cmd.join(' ')} failed"
277
- end
278
-
279
- # Duplicates hecks project_rust's own default-path tail (root mod.rs +
280
- # Cargo.toml feature sync) rather than extracting a shared method, so
281
- # the default path's own body stays byte-for-byte untouched.
282
- def sync_mod_and_cargo!(out_root, target_mod_name)
283
- all_dirs = Dir.children(out_root).select { |name| File.directory?(File.join(out_root, name)) }.sort
284
- domains = all_dirs.select { |name| File.exist?(File.join(out_root, name, "merged.rs")) }
285
-
286
- root_mod_path = File.join(out_root, "mod.rs")
287
- File.open(root_mod_path, "w") do |f|
288
- f.puts "// GENERATED by hecks project_rust — re-run it to refresh this list."
289
- f.puts "//"
290
- f.puts "// BUG#25 — every DOMAIN (an entry in `domains`, below: a directory"
291
- f.puts "// carrying its own merged.rs and therefore its own Cargo feature) is"
292
- f.puts "// declared behind that SAME feature, so a broken or absent domain's"
293
- f.puts "// generated code is never even compiled into a build that selects a"
294
- f.puts "// DIFFERENT domain — `cargo build --features waybill` used to pull in"
295
- f.puts "// every OTHER domain's `pub mod` unconditionally, so one domain's own"
296
- f.puts "// non-compiling generated code (a `has_many` field, before this fix)"
297
- f.puts "// broke every feature's build, not just its own. A shared FRAMEWORK"
298
- f.puts "// chapter (governance/identity — no merged.rs of its own, so absent"
299
- f.puts "// from `domains`) stays unconditional: it has no single feature of its"
300
- f.puts "// own to gate behind, and is compiled in by whichever domain(s) attach"
301
- f.puts "// it via `attaches`."
302
- all_dirs.each do |name|
303
- f.puts "#[cfg(feature = #{name.inspect})]" if domains.include?(name)
304
- f.puts "pub mod #{name};"
305
- end
306
- f.puts
307
- f.puts "// `active` is whichever ONE domain's own merged Store/dispatch table"
308
- f.puts "// is selected by Cargo feature (rust/Cargo.toml, kept in sync with this"
309
- f.puts "// list by hecks project_rust) — kernel/cli.rs imports"
310
- f.puts "// `crate::generated::active::{dispatch_by_name, Store, ...}` unchanged"
311
- f.puts "// no matter which domain that resolves to."
312
- f.puts "//"
313
- f.puts "// DOMAIN FEATURES ARE MUTUALLY EXCLUSIVE (R5) — Cargo has no native"
314
- f.puts "// concept of that, and `default = [#{target_mod_name.inspect}]` in"
315
- f.puts "// Cargo.toml stays enabled unless a build passes"
316
- f.puts "// `--no-default-features`, so a plain `cargo build --features banking`"
317
- f.puts "// used to enable BOTH the default domain's feature and banking's,"
318
- f.puts "// producing two `pub use ... as active;` imports and a hard E0252."
319
- f.puts "// Fixed two ways below, entirely mechanically from `domains`/"
320
- f.puts "// `target_mod_name` (this run's new default) — never hand-maintained:"
321
- f.puts "//"
322
- f.puts "// 1. THE DEFAULT DOMAIN'S OWN re-export backs off whenever any OTHER"
323
- f.puts "// domain feature is also enabled, so it never contends for `active`"
324
- f.puts "// by accident — `cargo build --features banking` (still implicitly"
325
- f.puts "// carrying the default feature) now resolves to banking alone,"
326
- f.puts "// same as if `--no-default-features` had been passed explicitly."
327
- f.puts "// 2. Two NON-default domain features enabled TOGETHER (a genuine,"
328
- f.puts "// not-accidental conflict — nothing makes that combination"
329
- f.puts "// meaningful) still fail the build, but with an explicit"
330
- f.puts "// `compile_error!` naming both features instead of a cryptic"
331
- f.puts "// \"defined multiple times\"."
332
- domains.each do |name|
333
- other_domains = domains - [name]
334
- if name == target_mod_name && !other_domains.empty?
335
- guard = other_domains.map { |o| "feature = #{o.inspect}" }.join(", ")
336
- f.puts "#[cfg(all(feature = #{name.inspect}, not(any(#{guard}))))]"
337
- else
338
- f.puts "#[cfg(feature = #{name.inspect})]"
339
- end
340
- f.puts "pub use #{name}::merged as active;"
341
- end
342
- non_default_domains = domains - [target_mod_name]
343
- if non_default_domains.size > 1
344
- f.puts
345
- f.puts "// Explicit conflict guard — see point 2 above. Only fires when TWO"
346
- f.puts "// non-default domain features are both turned on; the default"
347
- f.puts "// domain's own feature never reaches here (point 1 already excludes"
348
- f.puts "// it whenever any other domain feature is present)."
349
- non_default_domains.combination(2).each do |a, b|
350
- f.puts "#[cfg(all(feature = #{a.inspect}, feature = #{b.inspect}))]"
351
- f.puts "compile_error!(\"domain features are mutually exclusive — enable only one of: #{domains.join(', ')} (both #{a} and #{b} are enabled)\");"
352
- end
353
- end
354
- end
355
- puts "wrote #{root_mod_path}"
356
-
357
- # The crate's own Cargo.toml, two levels above src/generated — the one `HECKS_RUST_DIR` chose.
358
- cargo_toml_path = File.expand_path("../../Cargo.toml", out_root)
359
- cargo_toml = File.read(cargo_toml_path)
360
- unless cargo_toml.include?("[features]")
361
- cargo_toml = cargo_toml.sub(/\A/, <<~TOML)
362
- # ONE FEATURE PER GENERATED DOMAIN — selects which domain's own
363
- # generated::<domain>::merged module `generated::active` re-exports
364
- # (rust/src/generated/mod.rs). Kept in sync by hecks project_rust: a
365
- # feature is added here whenever a new domain is generated, never
366
- # removed (the domain's own generated/<name>/ directory is the
367
- # source of truth for whether it still exists). `default` tracks
368
- # whichever domain was generated MOST RECENTLY, so a bare
369
- # `cargo build`/`cargo test` keeps behaving exactly as it always
370
- # has; any other still-generated domain stays reachable via
371
- # `--no-default-features --features <name>`.
372
- [features]
373
- default = []
374
-
375
- TOML
376
- end
377
- # Scoped to the [features] table only — checking against the whole
378
- # file false-positives against [package]/[lib] keys sharing a word
379
- # with a domain name (version, edition, name, path, ...).
380
- features_table = cargo_toml[/^\[features\](?:\n(?!\[).*)*$/] || ""
381
- domains.each do |name|
382
- next if features_table =~ /^#{Regexp.escape(name)}\s*=/
383
-
384
- cargo_toml = cargo_toml.sub(/^default\s*=.*$/) { "#{Regexp.last_match(0)}\n#{name} = []" }
385
- features_table = cargo_toml[/^\[features\](?:\n(?!\[).*)*$/] || ""
386
- end
387
- cargo_toml = cargo_toml.sub(/^default\s*=.*$/, "default = [#{target_mod_name.inspect}]")
388
- File.write(cargo_toml_path, cargo_toml)
389
- puts "wrote #{cargo_toml_path} (default feature: #{target_mod_name})"
390
- end
391
- end