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.
- checksums.yaml +4 -4
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -1
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
- data/lib/hecks/bluebook/query.rb +8 -5
- data/lib/hecks/cli/check_era.rb +3 -39
- data/lib/hecks/cli/interview_draft.rb +86 -31
- data/lib/hecks/cli/interview_session.rb +77 -26
- data/lib/hecks/cli/interview_shape.rb +179 -0
- data/lib/hecks/doors/cli_runner.rb +14 -1
- data/lib/hecks/doors/launcher_options.rb +13 -3
- data/lib/hecks/doors/mcp_door.rb +16 -2
- data/lib/hecks/doors/mcp_door_scope.rb +45 -5
- data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
- data/lib/hecks/doors.rb +1 -1
- data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
- data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
- data/lib/hecks/fuzzing/qa_settings.rb +15 -34
- data/lib/hecks/gate/stages.yml +113 -8
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
- data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
- data/lib/hecks/hecks/adapters/git.rb +59 -0
- data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
- data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
- data/lib/hecks/hecks/adapters/site_toolchain.rb +88 -11
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +217 -0
- data/lib/hecks/hecks/hecks.hecksagon +24 -0
- data/lib/hecks/hecks/hecks.world +8 -3
- data/lib/hecks/hecks/site.world +5 -2
- data/lib/hecks/language/bluebook/query.bluebook +25 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +220 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
- data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
- data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
- data/lib/hecks/projections/deploy/box/settings.rb +40 -6
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
- data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
- data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
- data/lib/hecks/projections/deploy/box.rb +28 -4
- data/lib/hecks/projections/deploy/fargate.rb +3 -3
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
- data/lib/hecks/projections/site/edge/checks.rb +31 -5
- data/lib/hecks/projections/site/edge.rb +25 -11
- data/lib/hecks/projections/site/live_edge.rb +162 -0
- data/lib/hecks/projections/site/routes_ts.rb +50 -27
- data/lib/hecks/projections/site/site_cdn.rb +7 -4
- data/lib/hecks/projections/site/table/checks.rb +53 -7
- data/lib/hecks/projections/site/table/link_builder.rb +64 -0
- data/lib/hecks/projections/site/table/row_builder.rb +21 -6
- data/lib/hecks/projections/site/table.rb +16 -5
- data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
- data/lib/hecks/quality_control/quality_control.bluebook +76 -124
- data/lib/hecks/runtime/interpreting.rb +3 -22
- data/lib/hecks/runtime/needs.rb +48 -0
- data/lib/hecks/runtime/query_interpreter.rb +3 -0
- data/lib/hecks/runtime/registry/verification.rb +3 -3
- data/lib/hecks/rust_build/host.rb +147 -0
- data/lib/hecks/rust_build/kernel_input.rb +51 -17
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
- data/lib/hecks/rust_build/project_rust.rb +7 -140
- data/lib/hecks/rust_build.rb +1 -0
- data/lib/hecks/site/bluebook/site.bluebook +80 -14
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
- data/lib/hecks/storehouse.rb +35 -1
- data/lib/hecks/tools/comment_style.rb +4 -2
- data/lib/hecks/tools/gate.rb +1 -1
- data/lib/hecks/tools/rust_comment_style.rb +2 -4
- data/lib/hecks/tools/site_routes.rb +69 -28
- data/lib/hecks/tools/tools_doc.rb +204 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +152 -1
- data/lib/hecks.rb +1 -0
- data/qa/settings.yml +1 -2
- data/rust/build/src/lineage_pass.rs +31 -16
- data/rust/build/src/main.rs +4 -2
- data/rust/build/src/pipeline.rs +9 -20
- data/rust/codegen/src/attr.rs +1 -1
- data/rust/codegen/src/bridging.rs +8 -8
- data/rust/codegen/src/commands.rs +1 -1
- data/rust/codegen/src/constraints.rs +1 -1
- data/rust/codegen/src/dependency_planning.rs +1 -1
- data/rust/codegen/src/domain_generator.rs +2 -2
- data/rust/codegen/src/exemplar.rs +1 -1
- data/rust/codegen/src/expr_emitter.rs +1 -1
- data/rust/codegen/src/fielded.rs +1 -1
- data/rust/codegen/src/json.rs +214 -23
- data/rust/codegen/src/json_codec.rs +1 -1
- data/rust/codegen/src/literal.rs +6 -3
- data/rust/codegen/src/main.rs +23 -13
- data/rust/codegen/src/mutations.rs +1 -1
- data/rust/codegen/src/naming.rs +5 -3
- data/rust/codegen/src/optional_pass.rs +226 -0
- data/rust/codegen/src/ports.rs +1 -1
- data/rust/codegen/src/queries.rs +3 -3
- data/rust/codegen/src/reactions.rs +1 -1
- data/rust/codegen/src/read_models.rs +2 -2
- data/rust/codegen/src/reference_specs.rs +1 -1
- data/rust/codegen/src/registry.rs +4 -4
- data/rust/codegen/src/shared.rs +1 -1
- data/rust/{build → codegen}/src/sidecars.rs +9 -5
- data/rust/codegen/src/skip_reason.rs +1 -1
- data/rust/codegen/src/types.rs +4 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +5 -0
- data/rust/host/src/needs.rs +193 -29
- data/rust/host/src/resend.rs +4 -4
- data/rust/host/src/ui_schema.rs +1 -1
- data/rust/parser/src/emit.rs +12 -0
- data/rust/parser/src/ir.rs +2 -0
- data/rust/parser/src/keywords.rs +2 -0
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/command.rs +2 -44
- data/rust/parser/src/parse/mod.rs +1 -0
- data/rust/parser/src/parse/needs.rs +60 -0
- data/rust/parser/src/parse/query.rs +3 -0
- data/rust/src/exemplar/commands.rs +1 -1
- data/rust/src/exemplar/constraints.rs +1 -1
- data/rust/src/exemplar/fielded.rs +1 -1
- data/rust/src/exemplar/json.rs +1 -1
- data/rust/src/exemplar/mod.rs +1 -1
- data/rust/src/exemplar/mutations.rs +1 -1
- data/rust/src/exemplar/queries.rs +1 -1
- data/rust/src/exemplar/reactions.rs +1 -1
- data/rust/src/exemplar/read_models.rs +1 -1
- data/rust/src/exemplar/registry.rs +1 -1
- data/rust/src/exemplar/types.rs +1 -1
- data/rust/src/kernel/cli.rs +18 -8
- data/rust/src/kernel/needs.rs +104 -9
- data/rust/src/kernel/read_model.rs +1 -1
- data/rust/src/lib.rs +1 -1
- metadata +24 -30
- data/lib/hecks/rust_build/append_optionals.rb +0 -79
- data/lib/hecks/rust_build/rust_literal.rb +0 -33
- data/lib/hecks/three_zero/forms.yml +0 -127
- data/lib/hecks/three_zero.rb +0 -12
- data/rust/build/src/json.rs +0 -421
- data/rust/build/src/optional_pass.rs +0 -150
- data/rust/project/bridging.rb +0 -257
- data/rust/project/commands.rb +0 -877
- data/rust/project/constraints.rb +0 -86
- data/rust/project/dependency_planning.rb +0 -178
- data/rust/project/domain_generator.rb +0 -983
- data/rust/project/exemplar.rb +0 -148
- data/rust/project/expr_emitter.rb +0 -72
- data/rust/project/fielded.rb +0 -141
- data/rust/project/json_codec.rb +0 -641
- data/rust/project/mutations.rb +0 -695
- data/rust/project/naming.rb +0 -223
- data/rust/project/ports.rb +0 -65
- data/rust/project/queries.rb +0 -493
- data/rust/project/reactions.rb +0 -317
- data/rust/project/read_models.rb +0 -690
- data/rust/project/reference_specs.rb +0 -27
- data/rust/project/registry.rb +0 -396
- data/rust/project/skip_reason.rb +0 -29
- data/rust/project/types.rb +0 -225
- data/rust/project/write_if_changed.rb +0 -7
- data/rust/project.rb +0 -30
- data/rust/project_rust_pipeline.rb +0 -391
data/rust/project/types.rb
DELETED
|
@@ -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
|