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
|
@@ -1,983 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
require "json"
|
|
3
|
-
|
|
4
|
-
module RustProjection
|
|
5
|
-
# Generates one domain's worth of Rust types, Fielded impls and commands into
|
|
6
|
-
# `mod_dir`, plus `metadata.rs` (embedded IR JSON for runtime introspection) and
|
|
7
|
-
# `registry.rs` (the JSON command router `kernel::cli.rs` dispatches through).
|
|
8
|
-
module DomainGenerator
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
# One manifest row per generate/skip decision, written to manifest.json
|
|
12
|
-
# alongside ir.json so hecks rust_coverage never has to re-derive "did this
|
|
13
|
-
# generate?" by grepping generated Rust source.
|
|
14
|
-
#
|
|
15
|
-
# gap_class distinguishes a "whole_kind" gap (this construct kind has no
|
|
16
|
-
# code path here at all, for any domain) from a "per_instance" one (the
|
|
17
|
-
# kind generates in general, but this declared instance failed one of
|
|
18
|
-
# this generator's own per-instance checks). construct names the family
|
|
19
|
-
# that forced a gap; gap_class and construct must travel together, since
|
|
20
|
-
# downstream readers (the differential fuzzer, hecks rust_coverage's
|
|
21
|
-
# allowlist) match gaps by these fields, never by reason prose.
|
|
22
|
-
def manifest_entry(kind:, id:, generated:, reason: nil, gap_class: nil, routed: nil, construct: nil)
|
|
23
|
-
if (generated == false || routed == false) && gap_class.nil?
|
|
24
|
-
raise ArgumentError, "manifest entry #{kind} #{id} records a gap with no gap_class"
|
|
25
|
-
end
|
|
26
|
-
if gap_class && construct.to_s.empty?
|
|
27
|
-
raise ArgumentError, "manifest entry #{kind} #{id} declares gap_class #{gap_class.inspect} with no construct — " \
|
|
28
|
-
"every recorded gap names the construct family that forced it"
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
entry = { kind: kind, id: id, generated: generated }
|
|
32
|
-
entry[:routed] = routed unless routed.nil?
|
|
33
|
-
entry[:gap_class] = gap_class if gap_class
|
|
34
|
-
entry[:construct] = construct.to_s if gap_class
|
|
35
|
-
entry[:reason] = reason.to_s if reason
|
|
36
|
-
entry
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Entity-scoped queries have no generated code path (the query loop in
|
|
40
|
-
# `call` walks aggregate[:queries] only). Recorded here as the
|
|
41
|
-
# whole-kind gap they are, recursively, one entry per declared entity
|
|
42
|
-
# query at any depth.
|
|
43
|
-
def entity_query_entries(owner_id, entities)
|
|
44
|
-
entities.flat_map do |entity|
|
|
45
|
-
entity_id = "#{owner_id}.#{entity[:name]}"
|
|
46
|
-
own = Array(entity[:queries]).map do |query|
|
|
47
|
-
manifest_entry(kind: "query", id: "#{entity_id}.#{query[:name]}", generated: false, gap_class: "whole_kind",
|
|
48
|
-
construct: "entity_query",
|
|
49
|
-
reason: "an entity-scoped query has no generated code path — only an aggregate's own " \
|
|
50
|
-
"declared queries reach the QUERIES table")
|
|
51
|
-
end
|
|
52
|
-
own + entity_query_entries(entity_id, Array(entity[:entities]))
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def lifecycle_extra_field(node)
|
|
57
|
-
return [] unless node[:lifecycle]
|
|
58
|
-
|
|
59
|
-
field = Projector.rust_ident_field(node[:lifecycle][:field])
|
|
60
|
-
[[Projector.rust_field(node[:lifecycle][:field]), "crate::kernel::Json::Str(self.#{field}.clone())"]]
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# For each Reference<X> command attribute, resolves X against every
|
|
64
|
-
# aggregate this domain declares, not just the ones already written
|
|
65
|
-
# when this command's file is reached — matching Ruby's lazy
|
|
66
|
-
# attribute-type resolution, which already allows forward references
|
|
67
|
-
# across aggregates in one file.
|
|
68
|
-
def reference_checks(command, aggregates_by_name, unsupported_names)
|
|
69
|
-
command[:attributes].filter_map do |attr|
|
|
70
|
-
target_name = Projector.reference_target(attr[:type])
|
|
71
|
-
next unless target_name
|
|
72
|
-
|
|
73
|
-
target = aggregates_by_name[target_name]
|
|
74
|
-
next unless target
|
|
75
|
-
next if unsupported_names.include?(target_name)
|
|
76
|
-
|
|
77
|
-
{
|
|
78
|
-
field: attr[:name],
|
|
79
|
-
optional: attr[:optional],
|
|
80
|
-
target_mod: target[:name].downcase,
|
|
81
|
-
target_name: target[:name],
|
|
82
|
-
heads: target[:identified_by].map { |path| path.split(".").first }.join(", "),
|
|
83
|
-
}
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Catches a reference field redeclared on the command under a plain
|
|
88
|
-
# value object (the attribute isn't reference?-true, so reference_checks
|
|
89
|
-
# above finds nothing to check) by checking against the aggregate's own
|
|
90
|
-
# Reference<X> field of the same name instead. Reuses the same
|
|
91
|
-
# pre-dispatch, args-based check shape as reference_checks, since a bare
|
|
92
|
-
# `sets :field` mutation (no `to:`, `append:`, etc.) copies its source
|
|
93
|
-
# argument straight into the settled state, so the value is already in
|
|
94
|
-
# `args` before dispatch runs (ADR 0037). Only covers a bare :set
|
|
95
|
-
# mutation on a scalar or single-attribute value object; a `has_many`
|
|
96
|
-
# relationship and entity-level recursion are left unchecked.
|
|
97
|
-
def state_reference_checks(aggregate, command, aggregates_by_name, unsupported_names, value_objects_by_name)
|
|
98
|
-
aggregate[:attributes].filter_map do |attr|
|
|
99
|
-
target_name = Projector.reference_target(attr[:type])
|
|
100
|
-
next unless target_name
|
|
101
|
-
|
|
102
|
-
mutation = command[:mutations].find do |m|
|
|
103
|
-
m[:op].to_s == "set" && m[:target].to_s == attr[:name].to_s && m[:source][:kind] == "argument"
|
|
104
|
-
end
|
|
105
|
-
next unless mutation
|
|
106
|
-
|
|
107
|
-
source_attr = command[:attributes].find { |a| a[:name].to_s == mutation[:source][:name].to_s }
|
|
108
|
-
next unless source_attr
|
|
109
|
-
next if Projector.reference_target(source_attr[:type])
|
|
110
|
-
|
|
111
|
-
# A has_many field needs one check_reference per element
|
|
112
|
-
# (reference_check_list) rather than the scalar accessor, since
|
|
113
|
-
# dotting `.value` into the whole Vec wouldn't compile.
|
|
114
|
-
list_item = nil
|
|
115
|
-
if attr[:list]
|
|
116
|
-
list_item = list_reference_check_item(source_attr, value_objects_by_name)
|
|
117
|
-
next unless list_item
|
|
118
|
-
|
|
119
|
-
accessor = source_attr[:name]
|
|
120
|
-
else
|
|
121
|
-
accessor = state_reference_check_accessor(source_attr, value_objects_by_name)
|
|
122
|
-
next unless accessor
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
target = aggregates_by_name[target_name]
|
|
126
|
-
next unless target
|
|
127
|
-
next if unsupported_names.include?(target_name)
|
|
128
|
-
|
|
129
|
-
{
|
|
130
|
-
field: accessor,
|
|
131
|
-
optional: source_attr[:optional],
|
|
132
|
-
list_item: list_item,
|
|
133
|
-
target_mod: target[:name].downcase,
|
|
134
|
-
target_name: target[:name],
|
|
135
|
-
heads: target[:identified_by].map { |path| path.split(".").first }.join(", "),
|
|
136
|
-
}
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# The per-element key expression reference_check_list checks: bare
|
|
141
|
-
# `item` for a list_of(String), or `&item.<field>` for a list of
|
|
142
|
-
# single-String-attribute value objects. nil for any other element
|
|
143
|
-
# shape — left unchecked, same as state_reference_check_accessor's own
|
|
144
|
-
# gaps.
|
|
145
|
-
def list_reference_check_item(source_attr, value_objects_by_name)
|
|
146
|
-
return nil unless source_attr[:list]
|
|
147
|
-
return "item" if source_attr[:type].to_s == "String"
|
|
148
|
-
|
|
149
|
-
vo = value_objects_by_name[source_attr[:type]]
|
|
150
|
-
return nil unless vo && vo[:attributes].size == 1
|
|
151
|
-
return nil unless vo[:attributes].first[:type].to_s == "String"
|
|
152
|
-
|
|
153
|
-
"&item.#{Projector.rust_ident_field(vo[:attributes].first[:name])}"
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# The source argument's own field-access expression, tacked onto
|
|
157
|
-
# `args.` by emit_reference_check. A plain scalar argument needs no
|
|
158
|
-
# accessor (nil means "use the field name as-is"). A single-attribute
|
|
159
|
-
# value object needs `.field` appended, but only for a required
|
|
160
|
-
# argument — check_reference's optional template assumes
|
|
161
|
-
# Option<String>, not Option<Struct>. A multi-attribute value object
|
|
162
|
-
# has no single field to dot into. nil for both: a deliberately narrow
|
|
163
|
-
# gap the real corpus doesn't exercise.
|
|
164
|
-
def state_reference_check_accessor(source_attr, value_objects_by_name)
|
|
165
|
-
return source_attr[:name] if Projector.effective_scalar_type(source_attr[:type])
|
|
166
|
-
|
|
167
|
-
vo = value_objects_by_name[source_attr[:type]]
|
|
168
|
-
return nil unless vo
|
|
169
|
-
return nil if source_attr[:optional]
|
|
170
|
-
return nil unless vo[:attributes].size == 1
|
|
171
|
-
|
|
172
|
-
"#{source_attr[:name]}.#{Projector.rust_ident_field(vo[:attributes].first[:name])}"
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# An aggregate's tenant field is whichever field one of its own queries
|
|
176
|
-
# names via `authorize policy, tenant: :field` — reused here to name a
|
|
177
|
-
# field on the aggregate itself for the write side. nil when no query
|
|
178
|
-
# declares one.
|
|
179
|
-
def tenant_field_for(aggregate)
|
|
180
|
-
query = Array(aggregate[:queries]).find { |q| q[:authorization] && q[:authorization][:tenant] }
|
|
181
|
-
query && query[:authorization][:tenant]
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# The write-side mirror of the query-side tenant boundary (ADR 0037):
|
|
185
|
-
# reuses the same pre-dispatch, args-based check shape as
|
|
186
|
-
# reference_checks/state_reference_checks above, since a bare `sets
|
|
187
|
-
# :field` mutation copies its source argument straight into settled
|
|
188
|
-
# state before dispatch runs. Fires only when both sides declare a
|
|
189
|
-
# tenant field and it's reachable off this command's args in the same
|
|
190
|
-
# narrow accessor shape state_reference_check_accessor already
|
|
191
|
-
# establishes; every other case bails to `[]`, never raises.
|
|
192
|
-
def tenant_boundary_checks(aggregate, command, aggregates_by_name, unsupported_names, value_objects_by_name)
|
|
193
|
-
own_tenant_field = tenant_field_for(aggregate)
|
|
194
|
-
return [] unless own_tenant_field
|
|
195
|
-
|
|
196
|
-
own_tenant_attr = command[:attributes].find { |a| a[:name].to_s == own_tenant_field.to_s }
|
|
197
|
-
return [] unless own_tenant_attr
|
|
198
|
-
|
|
199
|
-
own_accessor = state_reference_check_accessor(own_tenant_attr, value_objects_by_name)
|
|
200
|
-
return [] unless own_accessor
|
|
201
|
-
|
|
202
|
-
command[:attributes].filter_map do |attr|
|
|
203
|
-
target_name = Projector.reference_target(attr[:type])
|
|
204
|
-
next unless target_name
|
|
205
|
-
|
|
206
|
-
target = aggregates_by_name[target_name]
|
|
207
|
-
next unless target
|
|
208
|
-
next if unsupported_names.include?(target_name)
|
|
209
|
-
|
|
210
|
-
target_tenant_field = tenant_field_for(target)
|
|
211
|
-
next unless target_tenant_field
|
|
212
|
-
|
|
213
|
-
target_tenant_attr = target[:attributes].find { |a| a[:name].to_s == target_tenant_field.to_s }
|
|
214
|
-
next unless target_tenant_attr
|
|
215
|
-
|
|
216
|
-
target_accessor = state_reference_check_accessor(target_tenant_attr, value_objects_by_name)
|
|
217
|
-
next unless target_accessor
|
|
218
|
-
|
|
219
|
-
{
|
|
220
|
-
reference_field: attr[:name],
|
|
221
|
-
target_mod: target[:name].downcase,
|
|
222
|
-
target_name: target[:name],
|
|
223
|
-
aggregate_name: aggregate[:name],
|
|
224
|
-
own_tenant_field: own_tenant_field,
|
|
225
|
-
own_accessor: own_accessor,
|
|
226
|
-
target_tenant_field: target_tenant_field,
|
|
227
|
-
target_accessor: target_accessor,
|
|
228
|
-
}
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# Writes one chapter's generated Rust (per-aggregate files, registry.rs,
|
|
233
|
-
# metadata.rs, mod.rs) into mod_dir, and returns its aggregates, queries
|
|
234
|
-
# and read_models so a multi-chapter caller can merge them.
|
|
235
|
-
def call(ir, source_label, mod_dir, mod_name, merged_module: true)
|
|
236
|
-
# Validated before any side effects (mkdir_p included) so every name
|
|
237
|
-
# collision in one domain is reported at once, rather than after some
|
|
238
|
-
# aggregates already had files written.
|
|
239
|
-
refusal = Projector.reserved_name_refusal(source_label, mod_name, ir[:aggregates].map { |a| a[:name] })
|
|
240
|
-
raise refusal if refusal
|
|
241
|
-
|
|
242
|
-
unsafe = Projector.unsafe_name_refusal(source_label, ir)
|
|
243
|
-
raise unsafe if unsafe
|
|
244
|
-
|
|
245
|
-
FileUtils.mkdir_p(mod_dir)
|
|
246
|
-
domain_name = ir[:name]
|
|
247
|
-
generated_aggregates = []
|
|
248
|
-
registry_aggregates = []
|
|
249
|
-
# See manifest_entry's own header. Written to manifest.json at the
|
|
250
|
-
# end of this method, alongside the ir.json sidecar.
|
|
251
|
-
manifest = []
|
|
252
|
-
# Accumulated the same way registry_aggregates is, and returned so a
|
|
253
|
-
# multi-chapter caller can union query defs across chapters too.
|
|
254
|
-
query_defs = []
|
|
255
|
-
aggregates_by_name = ir[:aggregates].to_h { |a| [a[:name], a] }
|
|
256
|
-
unsupported_names = ir[:aggregates].select do |a|
|
|
257
|
-
vo_by_name = a[:value_objects].to_h { |vo| [vo[:name], vo] }
|
|
258
|
-
Projector.unsupported_attribute_types(a, vo_by_name).any?
|
|
259
|
-
end.map { |a| a[:name] }
|
|
260
|
-
# A command attribute can reference any value object the domain
|
|
261
|
-
# declares, not just one its own aggregate also declares.
|
|
262
|
-
# cross_aggregate_vo_imports uses this map to emit the `use` line
|
|
263
|
-
# that makes the identifier resolve; codegen call sites still emit
|
|
264
|
-
# the same bare type name.
|
|
265
|
-
domain_value_object_owner = ir[:aggregates].each_with_object({}) do |a, index|
|
|
266
|
-
a[:value_objects].each { |vo| index[vo[:name]] = a[:name] }
|
|
267
|
-
end
|
|
268
|
-
# Same domain-wide reach as above: bridging a cross-aggregate command
|
|
269
|
-
# argument's type into its target field needs the actual definition,
|
|
270
|
-
# not just which aggregate owns it. Merged into each aggregate's own
|
|
271
|
-
# local map below, with the local map winning any name collision.
|
|
272
|
-
domain_value_objects_by_name = ir[:aggregates].flat_map { |a| a[:value_objects] }.to_h { |vo| [vo[:name], vo] }
|
|
273
|
-
ir[:aggregates].each do |aggregate|
|
|
274
|
-
value_objects_by_name = domain_value_objects_by_name.merge(aggregate[:value_objects].to_h { |vo| [vo[:name], vo] })
|
|
275
|
-
# Must run before anything below reads attr[:optional] on an
|
|
276
|
-
# entity/value-object field, since those reads need the derived
|
|
277
|
-
# fact, not just what the domain author wrote by hand.
|
|
278
|
-
Projector.mark_append_optional_fields!(aggregate, value_objects_by_name)
|
|
279
|
-
Projector.derive_reverses_mutations!(aggregate)
|
|
280
|
-
|
|
281
|
-
unsupported = Projector.unsupported_attribute_types(aggregate, value_objects_by_name)
|
|
282
|
-
if unsupported.any?
|
|
283
|
-
aggregate_reason = "attribute type(s) #{unsupported.join(', ')} not generated yet " \
|
|
284
|
-
"(a bare, non-list entity-typed attribute isn't resolved to a Rust type)"
|
|
285
|
-
puts "skipping #{domain_name}::#{aggregate[:name]}: #{aggregate_reason}"
|
|
286
|
-
manifest << manifest_entry(kind: "aggregate", id: "#{domain_name}::#{aggregate[:name]}", generated: false,
|
|
287
|
-
gap_class: "per_instance", construct: "attribute_type", reason: aggregate_reason)
|
|
288
|
-
# Every command/entity/port-op this aggregate owns is recorded
|
|
289
|
-
# too, tracing back to the same root cause, rather than silently
|
|
290
|
-
# vanishing from the manifest with the aggregate.
|
|
291
|
-
aggregate[:commands].each do |command|
|
|
292
|
-
manifest << manifest_entry(kind: "command", id: "#{domain_name}::#{aggregate[:name]}.#{command[:name]}",
|
|
293
|
-
generated: false, gap_class: "per_instance",
|
|
294
|
-
construct: "owning_aggregate", reason: "owning aggregate not generated: #{aggregate_reason}")
|
|
295
|
-
end
|
|
296
|
-
aggregate[:entities].each do |entity|
|
|
297
|
-
manifest << manifest_entry(kind: "entity", id: "#{domain_name}::#{aggregate[:name]}.#{entity[:name]}",
|
|
298
|
-
generated: false, gap_class: "per_instance",
|
|
299
|
-
construct: "owning_aggregate", reason: "owning aggregate not generated: #{aggregate_reason}")
|
|
300
|
-
entity[:commands].each do |command|
|
|
301
|
-
manifest << manifest_entry(kind: "entity_command",
|
|
302
|
-
id: "#{domain_name}::#{aggregate[:name]}.#{entity[:name]}.#{command[:name]}",
|
|
303
|
-
generated: false, gap_class: "per_instance",
|
|
304
|
-
construct: "owning_aggregate", reason: "owning aggregate not generated: #{aggregate_reason}")
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
aggregate[:ports].each do |port|
|
|
308
|
-
port[:operations].each do |operation|
|
|
309
|
-
manifest << manifest_entry(kind: "port_operation",
|
|
310
|
-
id: "#{domain_name}::#{aggregate[:name]}.#{port[:name]}.#{operation[:name]}",
|
|
311
|
-
generated: false, gap_class: "per_instance",
|
|
312
|
-
construct: "owning_aggregate", reason: "owning aggregate not generated: #{aggregate_reason}")
|
|
313
|
-
end
|
|
314
|
-
end
|
|
315
|
-
next
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
manifest << manifest_entry(kind: "aggregate", id: "#{domain_name}::#{aggregate[:name]}", generated: true)
|
|
319
|
-
generated_aggregates << aggregate
|
|
320
|
-
can_route = Projector.extract_id_supported?(aggregate)
|
|
321
|
-
registry_commands = []
|
|
322
|
-
entity_commands = []
|
|
323
|
-
nested_entity_commands = []
|
|
324
|
-
port_operations = []
|
|
325
|
-
record_name = Projector.rust_ident(aggregate[:name])
|
|
326
|
-
|
|
327
|
-
path = File.join(mod_dir, "#{aggregate[:name].downcase}.rs")
|
|
328
|
-
wrote = WriteIfChanged.block(path) do |f|
|
|
329
|
-
f.puts "// GENERATED by hecks project_rust from #{source_label}'s canonical IR."
|
|
330
|
-
f.puts "// Do not hand-edit — re-run hecks project_rust instead."
|
|
331
|
-
f.puts "#![allow(dead_code, unused_variables)]"
|
|
332
|
-
f.puts 'use crate::kernel::Expr;'
|
|
333
|
-
Projector.cross_aggregate_vo_imports(aggregate, domain_value_object_owner, mod_name).each { |line| f.puts line }
|
|
334
|
-
f.puts
|
|
335
|
-
|
|
336
|
-
aggregate[:value_objects].each do |vo|
|
|
337
|
-
f.puts Projector.emit_value_object(vo, value_objects_by_name, aggregates_by_name)
|
|
338
|
-
f.puts
|
|
339
|
-
if vo[:closed_set] && vo[:attributes].size == 1
|
|
340
|
-
# A single-field closed set collapses to a Rust enum
|
|
341
|
-
# (emit_value_object's own branch), which needs the
|
|
342
|
-
# tag<->member codec.
|
|
343
|
-
f.puts Projector.emit_closed_set_codec(vo)
|
|
344
|
-
elsif vo[:closed_set]
|
|
345
|
-
# A multi-field closed set is a data table whose String
|
|
346
|
-
# fields are &'static str, not String — from_json can only
|
|
347
|
-
# select one of the table's fixed rows, not construct one.
|
|
348
|
-
f.puts Projector.emit_closed_set_table_codec(vo)
|
|
349
|
-
else
|
|
350
|
-
# A value object gets the same unknown-key refusal a
|
|
351
|
-
# command's args struct gets, with no extra allowed keys —
|
|
352
|
-
# without it, a mistyped nested VO field silently falls
|
|
353
|
-
# through to any declared default instead of being refused.
|
|
354
|
-
name = Projector.rust_ident(vo[:name])
|
|
355
|
-
f.puts Projector.emit_to_json_flat(name, vo[:attributes], value_objects_by_name)
|
|
356
|
-
f.puts
|
|
357
|
-
f.puts Projector.emit_from_json_flat(name, vo[:attributes], value_objects_by_name, unknown_argument_allowlist: [])
|
|
358
|
-
end
|
|
359
|
-
f.puts
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
aggregate[:entities].each do |entity|
|
|
363
|
-
entity_verb = "#{domain_name}::#{aggregate[:name]}.#{entity[:name]}"
|
|
364
|
-
manifest << manifest_entry(kind: "entity", id: entity_verb, generated: true)
|
|
365
|
-
f.puts Projector.emit_entity(entity, value_objects_by_name)
|
|
366
|
-
f.puts
|
|
367
|
-
entity_name = Projector.rust_ident(entity[:name])
|
|
368
|
-
f.puts Projector.emit_to_json_flat(entity_name, entity[:attributes], value_objects_by_name, extra_fields: lifecycle_extra_field(entity))
|
|
369
|
-
f.puts
|
|
370
|
-
f.puts Projector.emit_from_json_state(entity_name, entity[:attributes], value_objects_by_name, extra_fields: lifecycle_extra_field(entity))
|
|
371
|
-
f.puts
|
|
372
|
-
|
|
373
|
-
# An entity nested inside this one (one level past what this
|
|
374
|
-
# loop otherwise reaches). Its struct and JSON codec are needed
|
|
375
|
-
# the moment a sibling field references it as a Vec<...>
|
|
376
|
-
# element type. Its own commands route only via `to: {
|
|
377
|
-
# entities: [...] }` addressing (ADR 0026); a third level is
|
|
378
|
-
# deliberately not generalized.
|
|
379
|
-
#
|
|
380
|
-
# entity_can_route (used again in the nested-entities loop
|
|
381
|
-
# below) gates whether this entity's identity shape supports
|
|
382
|
-
# extract_id/extract_wants at all; nested_can_route is the same
|
|
383
|
-
# check one hop deeper. Both true is what unrouted_supported
|
|
384
|
-
# (below) gates.
|
|
385
|
-
entity_can_route = Projector.extract_id_supported?(entity)
|
|
386
|
-
|
|
387
|
-
entity[:entities].each do |nested|
|
|
388
|
-
nested_verb = "#{entity_verb}.#{nested[:name]}"
|
|
389
|
-
manifest << manifest_entry(kind: "entity", id: nested_verb, generated: true)
|
|
390
|
-
f.puts Projector.emit_entity(nested, value_objects_by_name)
|
|
391
|
-
f.puts
|
|
392
|
-
nested_rust_name = Projector.rust_ident(nested[:name])
|
|
393
|
-
f.puts Projector.emit_to_json_flat(nested_rust_name, nested[:attributes], value_objects_by_name, extra_fields: lifecycle_extra_field(nested))
|
|
394
|
-
f.puts
|
|
395
|
-
f.puts Projector.emit_from_json_state(nested_rust_name, nested[:attributes], value_objects_by_name, extra_fields: lifecycle_extra_field(nested))
|
|
396
|
-
f.puts
|
|
397
|
-
# identity() is what a routed dispatch needs off a
|
|
398
|
-
# doubly-nested element, emitted unconditionally the same
|
|
399
|
-
# way entity's own always is.
|
|
400
|
-
f.puts Projector.emit_self_identity(nested)
|
|
401
|
-
f.puts
|
|
402
|
-
|
|
403
|
-
# extract_id/extract_wants for flat-args addressing at this
|
|
404
|
-
# depth, gated on both hops' identity shape supporting it
|
|
405
|
-
# (entity_can_route && nested_can_route) since a flat
|
|
406
|
-
# dispatch must also resolve the first hop's id.
|
|
407
|
-
nested_can_route = Projector.extract_id_supported?(nested)
|
|
408
|
-
unrouted_supported = entity_can_route && nested_can_route
|
|
409
|
-
if unrouted_supported
|
|
410
|
-
f.puts Projector.emit_extract_id(nested)
|
|
411
|
-
f.puts
|
|
412
|
-
# The addressing sibling generated alongside extract_id: a
|
|
413
|
-
# present-but-blank value is a valid non-matching
|
|
414
|
-
# component here, never a refusal.
|
|
415
|
-
f.puts Projector.emit_extract_id_lenient(nested)
|
|
416
|
-
f.puts
|
|
417
|
-
f.puts Projector.emit_extract_wants(nested)
|
|
418
|
-
f.puts
|
|
419
|
-
else
|
|
420
|
-
reason = entity_can_route ? "identity #{nested[:identified_by].inspect} isn't a shape extract_id resolves yet (json_codec.rb)" : "entity #{entity[:name]}'s own identity isn't extract_id-supported either"
|
|
421
|
-
puts "skipping #{nested_verb}'s flat-args fallback: #{reason} — routed (`to:`) addressing still works"
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
nested[:commands].each do |command|
|
|
425
|
-
nested_command_verb = "#{nested_verb}.#{command[:name]}"
|
|
426
|
-
reason = Projector.entity_command_skip_reason(command, nested, value_objects_by_name)
|
|
427
|
-
if reason
|
|
428
|
-
puts "skipping #{nested_command_verb}: #{reason}"
|
|
429
|
-
manifest << manifest_entry(kind: "entity_command", id: nested_command_verb, generated: false,
|
|
430
|
-
gap_class: "per_instance", construct: reason.construct, reason: reason)
|
|
431
|
-
next
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
f.puts Projector.emit_nested_entity_command(command, nested, entity, aggregate, domain_name, value_objects_by_name, aggregates_by_name,
|
|
435
|
-
process_managers: ir[:process_managers])
|
|
436
|
-
f.puts
|
|
437
|
-
|
|
438
|
-
manifest << manifest_entry(kind: "entity_command", id: nested_command_verb, generated: true, routed: true,
|
|
439
|
-
reason: unrouted_supported ? "routed (`to: { entities: [...] }`) and flat-args (one identity head per hop) both supported (BUG#19)" : "routed (`to: { entities: [...] }`) only — no legacy/flat-argument fallback at this depth (identity shape isn't extract_id-supported at one or both hops; BUG#19's own gate)")
|
|
440
|
-
|
|
441
|
-
nested_entity_commands << {
|
|
442
|
-
verb: nested_command_verb,
|
|
443
|
-
name: command[:name],
|
|
444
|
-
entity_record: Projector.rust_ident(entity[:name]),
|
|
445
|
-
nested_record: nested_rust_name,
|
|
446
|
-
# Matches commands.rb's emit_nested_entity_command naming
|
|
447
|
-
# exactly: dispatch_entity_#{entity.downcase}_#{nested.downcase}_#{dispatch_fn_name(cmd)}.
|
|
448
|
-
fn: "#{entity[:name].downcase}_#{nested[:name].downcase}_#{Projector.dispatch_fn_name(Projector.rust_ident(command[:name]))}",
|
|
449
|
-
args_struct: "#{nested_rust_name}#{Projector.rust_ident(command[:name])}NestedEntityArgs",
|
|
450
|
-
reference_checks: reference_checks(command, aggregates_by_name, unsupported_names),
|
|
451
|
-
reference_specs: Projector.reference_specs(domain_name, command[:attributes]),
|
|
452
|
-
attributes: command[:attributes].map { |a| a[:name].to_s },
|
|
453
|
-
role: command[:role],
|
|
454
|
-
invariant_check_lines: Projector.invariant_checks_for(command, aggregates_by_name, value_objects_by_name),
|
|
455
|
-
entity_name: entity[:name],
|
|
456
|
-
entity_identity_reading: entity[:identified_by].join(", "),
|
|
457
|
-
nested_name: nested[:name],
|
|
458
|
-
nested_identity_reading: nested[:identified_by].join(", "),
|
|
459
|
-
# Whether the router gets a flat-args fallback branch for
|
|
460
|
-
# this command, or stays routed-only; both hops must
|
|
461
|
-
# support extract_id/extract_wants (see this loop's
|
|
462
|
-
# header above).
|
|
463
|
-
unrouted_supported: unrouted_supported,
|
|
464
|
-
# registry.rb builds kernel::ArgumentGates from this
|
|
465
|
-
# command's generated gate functions (emit_argument_gates)
|
|
466
|
-
# and the kernel calls them in vocabulary order, ahead of
|
|
467
|
-
# identity resolution.
|
|
468
|
-
}
|
|
469
|
-
end
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
# Computed once above (the nested-entities loop needs it too).
|
|
473
|
-
entity_router_reason = "identity #{entity[:identified_by].inspect} isn't a shape extract_id resolves yet (json_codec.rb)"
|
|
474
|
-
if entity_can_route
|
|
475
|
-
f.puts Projector.emit_extract_id(entity)
|
|
476
|
-
f.puts
|
|
477
|
-
# commands.rb's delegate_prelude/emit_entity_command and
|
|
478
|
-
# registry.rb's entity_arms route-less branch both address
|
|
479
|
-
# this entity by extract_id_lenient, not extract_id.
|
|
480
|
-
f.puts Projector.emit_extract_id_lenient(entity)
|
|
481
|
-
f.puts
|
|
482
|
-
f.puts Projector.emit_extract_wants(entity)
|
|
483
|
-
f.puts
|
|
484
|
-
f.puts Projector.emit_self_identity(entity)
|
|
485
|
-
f.puts
|
|
486
|
-
else
|
|
487
|
-
puts "skipping #{domain_name}::#{aggregate[:name]}.#{entity[:name]}'s JSON router entries: #{entity_router_reason}"
|
|
488
|
-
end
|
|
489
|
-
|
|
490
|
-
entity[:commands].each do |command|
|
|
491
|
-
entity_command_verb = "#{domain_name}::#{aggregate[:name]}.#{entity[:name]}.#{command[:name]}"
|
|
492
|
-
reason = Projector.entity_command_skip_reason(command, entity, value_objects_by_name)
|
|
493
|
-
if reason
|
|
494
|
-
puts "skipping #{entity_command_verb}: #{reason}"
|
|
495
|
-
manifest << manifest_entry(kind: "entity_command", id: entity_command_verb, generated: false,
|
|
496
|
-
gap_class: "per_instance", construct: reason.construct, reason: reason)
|
|
497
|
-
next
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
f.puts Projector.emit_entity_command(command, entity, aggregate, domain_name, value_objects_by_name, aggregates_by_name,
|
|
501
|
-
process_managers: ir[:process_managers])
|
|
502
|
-
f.puts
|
|
503
|
-
|
|
504
|
-
# This command's Rust function was already emitted
|
|
505
|
-
# unconditionally above; whether anything can dispatch to it
|
|
506
|
-
# depends on the entity's identity shape, not this command's
|
|
507
|
-
# own. `generated: true, routed: false` distinguishes a
|
|
508
|
-
# real-but-unreachable function from one that was never
|
|
509
|
-
# generated at all.
|
|
510
|
-
unless entity_can_route
|
|
511
|
-
manifest << manifest_entry(kind: "entity_command", id: entity_command_verb, generated: true,
|
|
512
|
-
routed: false, gap_class: "per_instance",
|
|
513
|
-
construct: "router_identity", reason: "generated as a real Rust function, but not JSON-dispatchable —#{entity_router_reason}")
|
|
514
|
-
next
|
|
515
|
-
end
|
|
516
|
-
|
|
517
|
-
manifest << manifest_entry(kind: "entity_command", id: entity_command_verb, generated: true, routed: true)
|
|
518
|
-
|
|
519
|
-
entity_commands << {
|
|
520
|
-
verb: entity_command_verb,
|
|
521
|
-
name: command[:name],
|
|
522
|
-
entity_record: entity_name,
|
|
523
|
-
# Matches commands.rb's emit_entity_command naming exactly:
|
|
524
|
-
# dispatch_entity_#{entity[:name].downcase}_#{dispatch_fn_name(cmd)}.
|
|
525
|
-
fn: "#{entity[:name].downcase}_#{Projector.dispatch_fn_name(Projector.rust_ident(command[:name]))}",
|
|
526
|
-
# EntityArgs, not Args — an aggregate command named after the
|
|
527
|
-
# entity command it delegates to would otherwise share its
|
|
528
|
-
# args struct's name.
|
|
529
|
-
args_struct: "#{entity_name}#{Projector.rust_ident(command[:name])}EntityArgs",
|
|
530
|
-
reference_checks: reference_checks(command, aggregates_by_name, unsupported_names),
|
|
531
|
-
reference_specs: Projector.reference_specs(domain_name, command[:attributes]),
|
|
532
|
-
# Declared attribute names; see the identical field on
|
|
533
|
-
# registry_commands, above.
|
|
534
|
-
attributes: command[:attributes].map { |a| a[:name].to_s },
|
|
535
|
-
role: command[:role],
|
|
536
|
-
# The same VO invariant/admits/pattern checks
|
|
537
|
-
# emit_entity_command already bakes into dispatch_entity_*,
|
|
538
|
-
# computed here too so registry.rb's router can run them
|
|
539
|
-
# before refuse_role_mismatch/resolve_references, matching
|
|
540
|
-
# Ruby's own dispatch order.
|
|
541
|
-
invariant_check_lines: Projector.invariant_checks_for(command, aggregates_by_name, value_objects_by_name),
|
|
542
|
-
# entity_name/entity_identity_reading come from the entity's
|
|
543
|
-
# own declared name, threaded through registry.rb's dispatch
|
|
544
|
-
# call the same way the parent aggregate's already do.
|
|
545
|
-
entity_name: entity[:name],
|
|
546
|
-
entity_identity_reading: entity[:identified_by].join(", "),
|
|
547
|
-
# One generated function per declared argument-gate step,
|
|
548
|
-
# called in vocabulary order by
|
|
549
|
-
# kernel::decode_aggregate_arguments.
|
|
550
|
-
}
|
|
551
|
-
end
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
# aggregate[:attributes] plus a String-typed, always-optional
|
|
555
|
-
# pseudo-attribute per `projects` field: the record's
|
|
556
|
-
# struct/Fielded/JSON shape needs to carry a seeded projection
|
|
557
|
-
# like any other attribute. A command's own Args struct never
|
|
558
|
-
# sees this merge.
|
|
559
|
-
record_attributes = aggregate[:attributes] + Projector.projected_field_pseudo_attributes(aggregate)
|
|
560
|
-
record_for_struct = aggregate.merge(attributes: record_attributes)
|
|
561
|
-
|
|
562
|
-
f.puts Projector.emit_record(record_for_struct, value_objects_by_name)
|
|
563
|
-
f.puts
|
|
564
|
-
f.puts Projector.emit_to_json_flat(record_name, record_attributes, value_objects_by_name, optional: true, extra_fields: lifecycle_extra_field(aggregate) + Projector.corrects_extra_fields(aggregate), aggregate: aggregate)
|
|
565
|
-
f.puts
|
|
566
|
-
f.puts Projector.emit_from_json_state(record_name, record_attributes, value_objects_by_name, optional: true, extra_fields: lifecycle_extra_field(aggregate) + Projector.corrects_extra_fields(aggregate), aggregate: aggregate)
|
|
567
|
-
f.puts
|
|
568
|
-
# dispatch/dispatch_entity are generic over the record type and
|
|
569
|
-
# need a trait-bound to_json, since the inherent method above
|
|
570
|
-
# can't be called on a bare generic T. Emitted only for
|
|
571
|
-
# aggregate records, never value objects/entities/Args structs.
|
|
572
|
-
f.puts <<~RUST
|
|
573
|
-
impl crate::kernel::ToJson for #{record_name} {
|
|
574
|
-
fn to_json(&self) -> crate::kernel::Json {
|
|
575
|
-
#{record_name}::to_json(self)
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
RUST
|
|
579
|
-
f.puts
|
|
580
|
-
f.puts Projector.emit_set_projected_field(aggregate)
|
|
581
|
-
f.puts
|
|
582
|
-
f.puts Projector.emit_projected_field_table(aggregate)
|
|
583
|
-
f.puts
|
|
584
|
-
acting_router_reason = "identity #{aggregate[:identified_by].inspect} isn't a shape extract_id resolves yet (json_codec.rb)"
|
|
585
|
-
if can_route
|
|
586
|
-
f.puts Projector.emit_extract_id(aggregate)
|
|
587
|
-
f.puts
|
|
588
|
-
else
|
|
589
|
-
puts "skipping #{domain_name}::#{aggregate[:name]}'s JSON router acting-command entries: #{acting_router_reason}"
|
|
590
|
-
end
|
|
591
|
-
|
|
592
|
-
f.puts Projector.emit_invariants_fn(aggregate)
|
|
593
|
-
f.puts
|
|
594
|
-
|
|
595
|
-
aggregate[:commands].each do |command|
|
|
596
|
-
command_verb = "#{domain_name}::#{aggregate[:name]}.#{command[:name]}"
|
|
597
|
-
reason = Projector.command_skip_reason(command, aggregate, value_objects_by_name)
|
|
598
|
-
if reason
|
|
599
|
-
puts "skipping #{command_verb}: #{reason}"
|
|
600
|
-
manifest << manifest_entry(kind: "command", id: command_verb, generated: false, gap_class: "per_instance", construct: reason.construct, reason: reason)
|
|
601
|
-
next
|
|
602
|
-
end
|
|
603
|
-
|
|
604
|
-
f.puts Projector.emit_command(command, aggregate, domain_name, value_objects_by_name, aggregates_by_name)
|
|
605
|
-
f.puts
|
|
606
|
-
args_struct = "#{Projector.rust_ident(command[:name])}Args"
|
|
607
|
-
# An event's payload is args.to_json() directly, matching what
|
|
608
|
-
# a policy or process-manager reaction needs to forward real
|
|
609
|
-
# data into a re-triggered command's from_json.
|
|
610
|
-
#
|
|
611
|
-
# Not sparse: true here — that would diverge this generator's
|
|
612
|
-
# output from the separate hecks-codegen reimplementation that
|
|
613
|
-
# spec/codegen_parity_spec.rb holds byte-identical to this one,
|
|
614
|
-
# across nearly every domain with an optional command attribute.
|
|
615
|
-
f.puts Projector.emit_to_json_flat(args_struct, command[:attributes], value_objects_by_name, sparse: true)
|
|
616
|
-
f.puts
|
|
617
|
-
allowlist = Projector.command_argument_allowlist(aggregate, command, ir[:process_managers])
|
|
618
|
-
f.puts Projector.emit_from_json_flat(args_struct, command[:attributes], value_objects_by_name, unknown_argument_allowlist: allowlist, command_name: command[:name].to_s, absent_argument_check: true, interleave_checks: true, aggregates_by_name: aggregates_by_name)
|
|
619
|
-
f.puts
|
|
620
|
-
# One generated function per declared argument-gate step,
|
|
621
|
-
# called in vocabulary order by
|
|
622
|
-
# kernel::decode_aggregate_arguments.
|
|
623
|
-
f.puts Projector.emit_argument_gates(args_struct, command[:name].to_s, command[:attributes], allowlist)
|
|
624
|
-
f.puts
|
|
625
|
-
|
|
626
|
-
# A creating command's identity comes from its own typed args
|
|
627
|
-
# (build_identity_expr, already emitted by emit_command), so
|
|
628
|
-
# it's routable regardless of extract_id — including when
|
|
629
|
-
# identity needs an extra `head:` parameter read off the raw
|
|
630
|
-
# JSON rather than the typed Args struct. An acting command's
|
|
631
|
-
# id comes from extract_id instead, so it's routable only
|
|
632
|
-
# when that is.
|
|
633
|
-
creates = Projector.creates_owner?(aggregate, command, value_objects_by_name)
|
|
634
|
-
# identity_components only applies to a creating command: an
|
|
635
|
-
# acting command reaches an existing record via
|
|
636
|
-
# extract_id/id_line, not by minting one, so it never needs
|
|
637
|
-
# these extra params.
|
|
638
|
-
identity_extra_params = creates ? Projector.identity_components(aggregate, command).filter_map { |c| c[:head] } : []
|
|
639
|
-
|
|
640
|
-
unless creates || can_route
|
|
641
|
-
puts "skipping #{command_verb}'s JSON router entry: #{acting_router_reason}"
|
|
642
|
-
manifest << manifest_entry(kind: "command", id: command_verb, generated: true, routed: false,
|
|
643
|
-
gap_class: "per_instance",
|
|
644
|
-
construct: "router_identity", reason: "generated as a real Rust function, but not JSON-dispatchable —#{acting_router_reason}")
|
|
645
|
-
next
|
|
646
|
-
end
|
|
647
|
-
|
|
648
|
-
manifest << manifest_entry(kind: "command", id: command_verb, generated: true, routed: true)
|
|
649
|
-
registry_commands << {
|
|
650
|
-
verb: command_verb,
|
|
651
|
-
name: command[:name],
|
|
652
|
-
fn: Projector.dispatch_fn_name(Projector.rust_ident(command[:name])),
|
|
653
|
-
args_struct: args_struct,
|
|
654
|
-
creates: creates,
|
|
655
|
-
identity_extra_params: identity_extra_params,
|
|
656
|
-
# Adds only entries reference_checks above doesn't already
|
|
657
|
-
# cover — a reference field redeclared under a plain value
|
|
658
|
-
# object, checked against the aggregate's own field of the
|
|
659
|
-
# same name (ADR 0037).
|
|
660
|
-
reference_checks: reference_checks(command, aggregates_by_name, unsupported_names) +
|
|
661
|
-
state_reference_checks(aggregate, command, aggregates_by_name, unsupported_names, value_objects_by_name),
|
|
662
|
-
# tenant_boundary_checks' own header has the full argument.
|
|
663
|
-
# Empty for every command outside tenant_ledger today — no
|
|
664
|
-
# other aggregate in the corpus declares a tenant-scoping
|
|
665
|
-
# query.
|
|
666
|
-
tenant_boundary_checks: tenant_boundary_checks(aggregate, command, aggregates_by_name, unsupported_names, value_objects_by_name),
|
|
667
|
-
reference_specs: Projector.reference_specs(domain_name, command[:attributes]),
|
|
668
|
-
# Read by reactions.rb's emit_command_attributes_table and
|
|
669
|
-
# ReactionInvocation.command_facts on the Ruby side.
|
|
670
|
-
attributes: command[:attributes].map { |a| a[:name].to_s },
|
|
671
|
-
role: command[:role],
|
|
672
|
-
# See the identical field on entity_commands, above, for the
|
|
673
|
-
# full reasoning.
|
|
674
|
-
invariant_check_lines: Projector.invariant_checks_for(command, aggregates_by_name, value_objects_by_name),
|
|
675
|
-
# registry.rb builds kernel::ArgumentGates from this
|
|
676
|
-
# command's generated gate functions (emit_argument_gates)
|
|
677
|
-
# and the kernel calls them in vocabulary order, ahead of
|
|
678
|
-
# identity resolution.
|
|
679
|
-
}
|
|
680
|
-
end
|
|
681
|
-
|
|
682
|
-
# The primary/driving half (ports.rb's own header): no Hydrate,
|
|
683
|
-
# no repo, so can_route never gates these — the operation's own
|
|
684
|
-
# reference attribute already names the record.
|
|
685
|
-
aggregate[:ports].each do |port|
|
|
686
|
-
port[:operations].each do |operation|
|
|
687
|
-
operation_verb = "#{domain_name}::#{aggregate[:name]}.#{port[:name]}.#{operation[:name]}"
|
|
688
|
-
reason = Projector.port_operation_skip_reason(operation, aggregate[:name], value_objects_by_name)
|
|
689
|
-
if reason
|
|
690
|
-
puts "skipping #{operation_verb}: #{reason}"
|
|
691
|
-
manifest << manifest_entry(kind: "port_operation", id: operation_verb, generated: false,
|
|
692
|
-
gap_class: "per_instance", construct: reason.construct, reason: reason)
|
|
693
|
-
next
|
|
694
|
-
end
|
|
695
|
-
|
|
696
|
-
f.puts Projector.emit_port_operation(operation, port[:name], aggregate[:name], domain_name, value_objects_by_name, aggregates_by_name)
|
|
697
|
-
f.puts
|
|
698
|
-
|
|
699
|
-
manifest << manifest_entry(kind: "port_operation", id: operation_verb, generated: true, routed: true)
|
|
700
|
-
operation_args_struct = "#{Projector.rust_ident(port[:name])}#{Projector.rust_ident(operation[:name])}Args"
|
|
701
|
-
# If this operation still declares a Reference-typed
|
|
702
|
-
# attribute pointing back at its own aggregate,
|
|
703
|
-
# emit_port_operation excludes it from the generated args
|
|
704
|
-
# struct (routing supplies the receiver now), so filter it
|
|
705
|
-
# out of reference checks too.
|
|
706
|
-
legacy_receiver_field = operation[:attributes]
|
|
707
|
-
.find { |attr| Projector.reference_target(attr[:type]) == aggregate[:name] }
|
|
708
|
-
&.dig(:name)
|
|
709
|
-
# Mirrors Dispatcher#port_invocation's additive branch: no
|
|
710
|
-
# Reference-typed attribute exists for a `to:`-declared
|
|
711
|
-
# operation, so the receiver comes from a plain fact
|
|
712
|
-
# attribute named for the owning aggregate's own
|
|
713
|
-
# identified_by field instead. Kept as a separate field
|
|
714
|
-
# since split_aggregate_receiver must not strip it from the
|
|
715
|
-
# payload — it's a real declared fact, not routing-only
|
|
716
|
-
# state. Uses `.split(".").first` to take just the head of
|
|
717
|
-
# identified_by, since a value-object-typed identity
|
|
718
|
-
# resolves to a dotted internal path rather than the flat
|
|
719
|
-
# name the operation's own attribute is actually named.
|
|
720
|
-
to_receiver_field = operation[:to] == aggregate[:name] ? aggregate[:identified_by]&.first&.split(".")&.first : nil
|
|
721
|
-
operation_reference_checks = reference_checks(operation, aggregates_by_name, unsupported_names)
|
|
722
|
-
.reject { |check| check[:target_name] == aggregate[:name] }
|
|
723
|
-
port_operations << {
|
|
724
|
-
verb: operation_verb,
|
|
725
|
-
name: operation[:name],
|
|
726
|
-
fn: "#{port[:name].downcase}_#{Projector.dispatch_fn_name(Projector.rust_ident(operation[:name]))}",
|
|
727
|
-
args_struct: operation_args_struct,
|
|
728
|
-
reference_checks: operation_reference_checks,
|
|
729
|
-
legacy_receiver_field: legacy_receiver_field,
|
|
730
|
-
to_receiver_field: to_receiver_field,
|
|
731
|
-
}
|
|
732
|
-
end
|
|
733
|
-
end
|
|
734
|
-
end
|
|
735
|
-
puts(wrote ? "wrote #{path}" : "#{path} unchanged")
|
|
736
|
-
|
|
737
|
-
registry_aggregates << {
|
|
738
|
-
name: aggregate[:name],
|
|
739
|
-
mod: aggregate[:name].downcase,
|
|
740
|
-
record: record_name,
|
|
741
|
-
commands: registry_commands,
|
|
742
|
-
entity_commands: entity_commands,
|
|
743
|
-
nested_entity_commands: nested_entity_commands,
|
|
744
|
-
ports: port_operations,
|
|
745
|
-
# Carried through verbatim; emit_identity_head_table only
|
|
746
|
-
# resolves the single-component case — a composite identity is
|
|
747
|
-
# a documented gap there, not silently assumed to work.
|
|
748
|
-
identified_by: aggregate[:identified_by],
|
|
749
|
-
# Name + identity paths only; a saga-dispatched entity command
|
|
750
|
-
# needs the entity's own identity, not just its parent
|
|
751
|
-
# aggregate's.
|
|
752
|
-
entities: aggregate[:entities].map { |e| { name: e[:name], identified_by: e[:identified_by] } },
|
|
753
|
-
# Which top-level generated module this aggregate's .rs file
|
|
754
|
-
# lives under, so a per-chapter registry.rs can qualify
|
|
755
|
-
# cross-file paths as crate::generated::#{chapter_mod}::...
|
|
756
|
-
# instead of a bare super::...
|
|
757
|
-
chapter_mod: mod_name,
|
|
758
|
-
# This aggregate's own declared reference_to/belongs_to
|
|
759
|
-
# attributes, for the domain-wide REFERENCE_TABLE and an acting
|
|
760
|
-
# command's owner_deref fetch — computed once here rather than
|
|
761
|
-
# re-derived per command.
|
|
762
|
-
reference_specs: Projector.reference_specs(domain_name, aggregate[:attributes]),
|
|
763
|
-
# The bluebook's declared name (e.g. "Governance", not the
|
|
764
|
-
# lowercase module), so a merged multi-chapter registry labels
|
|
765
|
-
# each record by its own chapter correctly.
|
|
766
|
-
domain_name: domain_name,
|
|
767
|
-
}
|
|
768
|
-
end
|
|
769
|
-
|
|
770
|
-
# A declared query now generates for real for the subset
|
|
771
|
-
# query_skip_reason admits (field-comparator conditions, order_by,
|
|
772
|
-
# limit, offset, a nulls override); anything else is a
|
|
773
|
-
# per-instance gap, not a whole-kind one, since the construct kind
|
|
774
|
-
# has a real code path.
|
|
775
|
-
query_aggregates_by_name = ir[:aggregates].to_h { |a| [a[:name], a] }
|
|
776
|
-
assignments_verb = Projector.provided_assignments(ir)
|
|
777
|
-
ir[:aggregates].each do |aggregate|
|
|
778
|
-
value_objects_by_name = aggregate[:value_objects].to_h { |vo| [vo[:name], vo] }
|
|
779
|
-
|
|
780
|
-
aggregate[:queries].each do |query|
|
|
781
|
-
query_verb = "#{domain_name}::#{aggregate[:name]}.#{query[:name]}"
|
|
782
|
-
reason = Projector.query_skip_reason(query, aggregate, value_objects_by_name, query_aggregates_by_name)
|
|
783
|
-
if reason
|
|
784
|
-
puts "skipping query #{query_verb}: #{reason}"
|
|
785
|
-
manifest << manifest_entry(kind: "query", id: query_verb, generated: false, gap_class: "per_instance", construct: reason.construct, reason: reason)
|
|
786
|
-
next
|
|
787
|
-
end
|
|
788
|
-
|
|
789
|
-
manifest << manifest_entry(kind: "query", id: query_verb, generated: true)
|
|
790
|
-
conditions, reference_hop_conditions = Projector.query_conditions_and_hops(domain_name, query, aggregate, query_aggregates_by_name)
|
|
791
|
-
query_defs << {
|
|
792
|
-
verb: query_verb,
|
|
793
|
-
aggregate: "#{domain_name}::#{aggregate[:name]}",
|
|
794
|
-
arg_checks: Projector.query_arg_checks(query, "crate::generated::#{mod_name}::#{aggregate[:name].downcase}",
|
|
795
|
-
value_objects_by_name),
|
|
796
|
-
conditions: conditions,
|
|
797
|
-
reference_hop_conditions: reference_hop_conditions,
|
|
798
|
-
order_by: query[:order_by] ? Projector.emit_query_order_by(query[:order_by], query[:null_semantics]) : nil,
|
|
799
|
-
offset: query[:offset] ? Projector.emit_query_offset(query[:offset]) : nil,
|
|
800
|
-
limit: query[:limit] ? Projector.emit_query_limit(query[:limit]) : nil,
|
|
801
|
-
authorization: Projector.emit_query_authorization(query[:name], query[:authorization]),
|
|
802
|
-
assignments: assignments_verb == "#{aggregate[:name]}.#{query[:name]}",
|
|
803
|
-
}
|
|
804
|
-
end
|
|
805
|
-
|
|
806
|
-
# Generated when the aggregate holds the entity in a list
|
|
807
|
-
# (kernel::named_query::run_entity). A query on an entity nested
|
|
808
|
-
# inside another entity stays a recorded gap.
|
|
809
|
-
aggregate_id = "#{domain_name}::#{aggregate[:name]}"
|
|
810
|
-
aggregate[:entities].each do |entity|
|
|
811
|
-
list_attr = aggregate[:attributes].find { |a| a[:list] && a[:type].to_s == entity[:name].to_s }
|
|
812
|
-
Array(entity[:queries]).each do |query|
|
|
813
|
-
query_verb = "#{aggregate_id}.#{entity[:name]}.#{query[:name]}"
|
|
814
|
-
reason = Projector.entity_query_skip_reason(query, entity, list_attr, value_objects_by_name)
|
|
815
|
-
if reason
|
|
816
|
-
puts "skipping query #{query_verb}: #{reason}"
|
|
817
|
-
manifest << manifest_entry(kind: "query", id: query_verb, generated: false, gap_class: "per_instance", construct: reason.construct, reason: reason)
|
|
818
|
-
next
|
|
819
|
-
end
|
|
820
|
-
|
|
821
|
-
manifest << manifest_entry(kind: "query", id: query_verb, generated: true)
|
|
822
|
-
query_defs << {
|
|
823
|
-
verb: query_verb,
|
|
824
|
-
aggregate: aggregate_id,
|
|
825
|
-
entity: { list_field: list_attr[:name].to_s, parent_key: Projector.snake(aggregate[:name]),
|
|
826
|
-
identity_keys: Array(entity[:identified_by]).map { |path| path.to_s.split(".").first } },
|
|
827
|
-
arg_checks: [],
|
|
828
|
-
conditions: Projector.query_conditions(query),
|
|
829
|
-
order_by: query[:order_by] ? Projector.emit_query_order_by(query[:order_by], query[:null_semantics]) : nil,
|
|
830
|
-
offset: query[:offset] ? Projector.emit_query_offset(query[:offset]) : nil,
|
|
831
|
-
limit: query[:limit] ? Projector.emit_query_limit(query[:limit]) : nil,
|
|
832
|
-
authorization: nil,
|
|
833
|
-
}
|
|
834
|
-
end
|
|
835
|
-
manifest.concat(entity_query_entries("#{aggregate_id}.#{entity[:name]}", Array(entity[:entities])))
|
|
836
|
-
end
|
|
837
|
-
end
|
|
838
|
-
|
|
839
|
-
# A declared report block now generates for real for the subset
|
|
840
|
-
# read_model_skip_reason admits (a root aggregate fetched by
|
|
841
|
-
# reference id, plus reference-matched sibling heads — no
|
|
842
|
-
# where/order_by/limit). Anything else is a per-instance gap, not a
|
|
843
|
-
# whole-kind one, matching the query codegen above.
|
|
844
|
-
read_model_defs = []
|
|
845
|
-
ir[:read_models].each do |read_model|
|
|
846
|
-
read_model_id = "#{domain_name}::#{read_model[:name]}"
|
|
847
|
-
reason = Projector.read_model_skip_reason(read_model, aggregates_by_name, unsupported_names)
|
|
848
|
-
if reason
|
|
849
|
-
puts "skipping read_model #{read_model_id}: #{reason}"
|
|
850
|
-
manifest << manifest_entry(kind: "read_model", id: read_model_id, generated: false, gap_class: "per_instance", construct: reason.construct, reason: reason)
|
|
851
|
-
next
|
|
852
|
-
end
|
|
853
|
-
|
|
854
|
-
manifest << manifest_entry(kind: "read_model", id: read_model_id, generated: true)
|
|
855
|
-
read_model_defs << Projector.read_model_def(domain_name, read_model, aggregates_by_name)
|
|
856
|
-
end
|
|
857
|
-
|
|
858
|
-
# A same-domain policy generates into POLICIES and dispatches
|
|
859
|
-
# locally; a cross-domain policy generates into the separate
|
|
860
|
-
# CROSS_DOMAIN_POLICIES table instead, delivered by rust/host's
|
|
861
|
-
# lambda_client.rs rather than a local dispatch_by_name call. This
|
|
862
|
-
# manifest entry can't attest that a live cross-domain reaction's
|
|
863
|
-
# target actually exists — that's an operational fact about a real
|
|
864
|
-
# deploy, not something codegen can check.
|
|
865
|
-
ir[:policies].each do |policy|
|
|
866
|
-
manifest << manifest_entry(kind: "policy", id: "#{domain_name}::#{policy[:name]}", generated: true, routed: true)
|
|
867
|
-
end
|
|
868
|
-
|
|
869
|
-
# Unlike emit_policy_table above, no process manager has a
|
|
870
|
-
# per-instance skip condition: every declared dispatch target is
|
|
871
|
-
# already fully domain-qualified on the wire.
|
|
872
|
-
ir[:process_managers].each do |pm|
|
|
873
|
-
manifest << manifest_entry(kind: "process_manager", id: "#{domain_name}::#{pm[:name]}", generated: true, routed: true)
|
|
874
|
-
end
|
|
875
|
-
|
|
876
|
-
# Which adapter an aggregate is bound to is a deployment fact, not
|
|
877
|
-
# something re-derived from the bluebook's shape. Always generated
|
|
878
|
-
# and routed: rust/host's journal read/write path is generic over
|
|
879
|
-
# storage_name, and reachable outside the WASM kernel/cli.rs match
|
|
880
|
-
# arm entirely.
|
|
881
|
-
ir.fetch(:lineage, {}).fetch(:capable_aggregates, []).each do |aggregate|
|
|
882
|
-
manifest << manifest_entry(
|
|
883
|
-
kind: "lineage_aggregate", id: "#{domain_name}::#{aggregate[:name]}", generated: true, routed: true,
|
|
884
|
-
reason: "read via rust/host's journal::read_lineage_head_all/_by_id, written via journal::" \
|
|
885
|
-
"append_lineage_mutation — both generic over storage_name (\"#{aggregate[:storage_name]}\"), " \
|
|
886
|
-
"dispatched OUTSIDE the WASM kernel/InMemoryRepository path entirely, matching Ruby's own " \
|
|
887
|
-
"CommandInterpreter routing for a Postgres-bound aggregate (rust/project.rb's own header)"
|
|
888
|
-
)
|
|
889
|
-
end
|
|
890
|
-
|
|
891
|
-
metadata_path = File.join(mod_dir, "metadata.rs")
|
|
892
|
-
wrote = WriteIfChanged.block(metadata_path) do |f|
|
|
893
|
-
f.puts "// GENERATED by hecks project_rust — #{source_label}'s own canonical IR,"
|
|
894
|
-
f.puts "// embedded for runtime self-description. Not read by any dispatch"
|
|
895
|
-
f.puts "// function in this module — introspection only."
|
|
896
|
-
f.puts "pub const IR_JSON: &str = #{Projector.rust_string_literal(JSON.pretty_generate(ir))};"
|
|
897
|
-
end
|
|
898
|
-
puts(wrote ? "wrote #{metadata_path}" : "#{metadata_path} unchanged")
|
|
899
|
-
|
|
900
|
-
# rust/host carries no path dependency on this crate — its .wasm
|
|
901
|
-
# module is an opaque, untrusted artifact loaded via wasmtime, so
|
|
902
|
-
# metadata.rs's IR_JSON is unreachable from rust/host at compile
|
|
903
|
-
# time. hecks build_wasm copies this sidecar into rust/dist/ so a
|
|
904
|
-
# Rust-native web layer can read it at runtime via HECKS_IR_PATH.
|
|
905
|
-
ir_json_path = File.join(mod_dir, "ir.json")
|
|
906
|
-
wrote = WriteIfChanged.call(ir_json_path, JSON.pretty_generate(ir))
|
|
907
|
-
puts(wrote ? "wrote #{ir_json_path}" : "#{ir_json_path} unchanged")
|
|
908
|
-
|
|
909
|
-
# Sits alongside ir.json for the same reason: one is this call's
|
|
910
|
-
# account of what it read, the other of what it did with what it
|
|
911
|
-
# read. hecks rust_coverage reads both and diffs against an
|
|
912
|
-
# allowlist; nothing in this generator reads manifest.json back.
|
|
913
|
-
manifest_path = File.join(mod_dir, "manifest.json")
|
|
914
|
-
wrote = WriteIfChanged.call(manifest_path, JSON.pretty_generate(manifest))
|
|
915
|
-
puts(wrote ? "wrote #{manifest_path}" : "#{manifest_path} unchanged")
|
|
916
|
-
|
|
917
|
-
registry_path = File.join(mod_dir, "registry.rs")
|
|
918
|
-
wrote_registry = WriteIfChanged.block(registry_path) do |f|
|
|
919
|
-
f.puts Projector.emit_registry(registry_aggregates)
|
|
920
|
-
f.puts
|
|
921
|
-
f.puts Projector.emit_reference_lookup(registry_aggregates)
|
|
922
|
-
f.puts
|
|
923
|
-
f.puts Projector.emit_policy_table(domain_name, ir[:policies], ir[:aggregates])
|
|
924
|
-
f.puts
|
|
925
|
-
f.puts Projector.emit_cross_domain_policy_table(domain_name, ir[:policies])
|
|
926
|
-
f.puts
|
|
927
|
-
f.puts Projector.emit_process_manager_table(ir[:process_managers])
|
|
928
|
-
f.puts
|
|
929
|
-
f.puts Projector.emit_reference_key_table([[domain_name, generated_aggregates.map { |a| a[:name] }]])
|
|
930
|
-
f.puts
|
|
931
|
-
f.puts Projector.emit_creates_table(registry_aggregates)
|
|
932
|
-
f.puts
|
|
933
|
-
f.puts Projector.emit_identity_head_table(registry_aggregates)
|
|
934
|
-
f.puts
|
|
935
|
-
f.puts Projector.emit_entity_identity_head_table(registry_aggregates)
|
|
936
|
-
f.puts
|
|
937
|
-
f.puts Projector.emit_command_attributes_table(registry_aggregates)
|
|
938
|
-
f.puts
|
|
939
|
-
f.puts Projector.emit_query_table(query_defs)
|
|
940
|
-
f.puts
|
|
941
|
-
f.puts Projector.emit_query_arg_check_table(query_defs)
|
|
942
|
-
f.puts
|
|
943
|
-
read_model_defs.each do |rmd|
|
|
944
|
-
next unless rmd[:group_by_fn_body]
|
|
945
|
-
|
|
946
|
-
f.puts rmd[:group_by_fn_body]
|
|
947
|
-
f.puts
|
|
948
|
-
end
|
|
949
|
-
f.puts Projector.emit_read_model_table(read_model_defs)
|
|
950
|
-
end
|
|
951
|
-
puts(wrote_registry ? "wrote #{registry_path}" : "#{registry_path} unchanged")
|
|
952
|
-
|
|
953
|
-
mod_path = File.join(mod_dir, "mod.rs")
|
|
954
|
-
# This generator never writes `pub mod merged;` itself (`hecks project_rust`
|
|
955
|
-
# appends it separately, only once a chapter gets its own
|
|
956
|
-
# merged.rs). Re-including an already-present trailer keeps
|
|
957
|
-
# WriteIfChanged's before/after comparison apples-to-apples —
|
|
958
|
-
# without it, comparing base-only content against base-plus-merged
|
|
959
|
-
# content on disk would look changed every run. Requires
|
|
960
|
-
# merged_module too, not just presence on disk: a framework chapter
|
|
961
|
-
# never regenerates merged.rs, and an orphaned trailer here would
|
|
962
|
-
# leave a dangling `pub mod merged;` in the chapter's mod.rs once
|
|
963
|
-
# pruning removes the file.
|
|
964
|
-
merged_trailer = merged_module && File.exist?(mod_path) && File.read(mod_path).include?("pub mod merged;")
|
|
965
|
-
wrote_mod = WriteIfChanged.block(mod_path) do |f|
|
|
966
|
-
f.puts "// GENERATED by hecks project_rust — re-run it to refresh this list."
|
|
967
|
-
f.puts "pub mod metadata;"
|
|
968
|
-
f.puts "pub mod registry;"
|
|
969
|
-
generated_aggregates.each { |a| f.puts "pub mod #{a[:name].downcase};" }
|
|
970
|
-
f.puts "pub mod merged;" if merged_trailer
|
|
971
|
-
end
|
|
972
|
-
puts(wrote_mod ? "wrote #{mod_path}" : "#{mod_path} unchanged")
|
|
973
|
-
|
|
974
|
-
# Returned so hecks project_rust can concatenate :aggregates across
|
|
975
|
-
# every chapter a domain attaches into one merged
|
|
976
|
-
# Store/dispatch_by_name, and :queries the same way. Each aggregate
|
|
977
|
-
# already carries chapter_mod (set above); a query_def needs no
|
|
978
|
-
# chapter tag since its verb/aggregate are already fully
|
|
979
|
-
# domain-qualified.
|
|
980
|
-
{ aggregates: registry_aggregates, queries: query_defs, read_models: read_model_defs }
|
|
981
|
-
end
|
|
982
|
-
end
|
|
983
|
-
end
|