Skip to content

Commit 10b73b1

Browse files
authored
feat: align manifest proto schema with handler parameter schemas (#1840)
* feat: add aligned fields to OrganizationDefinition and MarketDataSetDefinition Add legal_name, display_name, external_reference, external_reference_type to OrganizationDefinition proto to match party.register_organization handler parameters directly. Add validation_expression and resolution_key_expression to MarketDataSetDefinition to match market_information.register_data_set handler parameters. Existing name field retained for backward compatibility. * feat: pass aligned fields through executor and simplify Starlark script Update OrganizationInput and MarketDataSetInput structs with new fields. Update extractPartyAndAccounts with fallback chain (legal_name -> name, display_name -> legal_name, external_reference -> code). Update extractMarketData to pass validation/resolution expressions. Update buildSagaInput to include all new fields in saga input map. Simplify v1.3.0.star to use direct field passthrough instead of ad-hoc translation from name/attributes. * test: add tests for aligned organization and market data set fields Test new proto fields pass through buildExecutorInput and buildSagaInput. Test backward compatibility fallback when new fields are empty. Verify validation_expression and resolution_key_expression propagation. * fix: regenerate JSON schema and frontend proto, fix gofmt Regenerate manifest.v1.schema.json to include new MarketDataSetDefinition and OrganizationDefinition fields. Regenerate frontend TypeScript proto bindings. Fix gofmt alignment in executor.go and grpc_handler_test.go. * fix: complete legal_name fallback chain to include code Add missing code fallback when both legal_name and name are empty, matching the documented fallback chain: legal_name -> name -> code. Add test for code-only organization definition. * fix: make new manifest proto fields optional to avoid required constraint New organization and market data set fields (legal_name, display_name, external_reference, external_reference_type, validation_expression, resolution_key_expression) must be optional in proto3 so they don't appear in the generated JSON schema's required arrays. This preserves backward compatibility for existing manifests that don't specify them. * fix: make legacy name field optional in OrganizationDefinition The deprecated name field should also be optional so manifests can use only legal_name and display_name without being forced to provide name. Addresses remaining CodeRabbit review feedback. * revert: keep name field non-optional to avoid proto breaking change Reverting the name field optional change since it triggers the Proto Breaking Change Detection check (implicit to explicit presence is a cardinality change). The name field was already required before this PR. Making it optional would be a separate, intentional breaking change. --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent 9140d5d commit 10b73b1

9 files changed

Lines changed: 526 additions & 169 deletions

File tree

api/jsonschema/manifest.v1.schema.json

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,14 @@
674674
"description": {
675675
"type": "string",
676676
"description": "description provides additional context about this data set."
677+
},
678+
"validationExpression": {
679+
"type": "string",
680+
"description": "validation_expression is a CEL expression evaluated against each observation before acceptance. Must return a boolean. Observations are only stored when the expression returns true. Default: \"true\" (accept all observations). Example: \"value \u003e 0 \u0026\u0026 value \u003c 1000000\""
681+
},
682+
"resolutionKeyExpression": {
683+
"type": "string",
684+
"description": "resolution_key_expression is a CEL expression that determines the resolution key for deduplication and conflict resolution of observations. Default: \"observed_at\" (one observation per timestamp). Example: \"observed_at + ':' + source_code\""
677685
}
678686
},
679687
"additionalProperties": false,
@@ -795,7 +803,7 @@
795803
},
796804
"name": {
797805
"type": "string",
798-
"description": "name is the legal or display name of the organization."
806+
"description": "name is the legacy display name of the organization. Deprecated: Use legal_name and display_name instead. Retained for backward compatibility."
799807
},
800808
"partyType": {
801809
"type": "string",
@@ -807,6 +815,22 @@
807815
},
808816
"type": "object",
809817
"description": "attributes contains additional metadata for this organization as key-value pairs. Keys and values are validated against the party type's attribute_schema if defined."
818+
},
819+
"legalName": {
820+
"type": "string",
821+
"description": "legal_name is the official legal name of the organization. Maps directly to the party.register_organization handler's legal_name parameter. If empty, falls back to the name field for backward compatibility."
822+
},
823+
"displayName": {
824+
"type": "string",
825+
"description": "display_name is the human-readable display name of the organization. Maps directly to the party.register_organization handler's display_name parameter. If empty, falls back to legal_name, then name."
826+
},
827+
"externalReference": {
828+
"type": "string",
829+
"description": "external_reference is a stable identifier for this organization in an external system. Maps directly to the party.register_organization handler's external_reference parameter. If empty, falls back to the organization code."
830+
},
831+
"externalReferenceType": {
832+
"type": "string",
833+
"description": "external_reference_type identifies the type of external reference (e.g., \"LEI\", \"NATIONAL_ID\"). Accepts stripped enum names (e.g., \"LEI\") - the handler accepts both \"LEI\" and \"EXTERNAL_REFERENCE_TYPE_LEI\" formats."
810834
}
811835
},
812836
"additionalProperties": false,

api/proto/meridian/control_plane/v1/manifest.pb.go

Lines changed: 214 additions & 112 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/proto/meridian/control_plane/v1/manifest.proto

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,18 @@ message MarketDataSetDefinition {
858858

859859
// description provides additional context about this data set.
860860
string description = 6 [(buf.validate.field).string.max_len = 1024];
861+
862+
// validation_expression is a CEL expression evaluated against each observation before acceptance.
863+
// Must return a boolean. Observations are only stored when the expression returns true.
864+
// Default: "true" (accept all observations).
865+
// Example: "value > 0 && value < 1000000"
866+
optional string validation_expression = 7 [(buf.validate.field).string.max_len = 4096];
867+
868+
// resolution_key_expression is a CEL expression that determines the resolution key
869+
// for deduplication and conflict resolution of observations.
870+
// Default: "observed_at" (one observation per timestamp).
871+
// Example: "observed_at + ':' + source_code"
872+
optional string resolution_key_expression = 8 [(buf.validate.field).string.max_len = 4096];
861873
}
862874

863875
// ========================================
@@ -876,11 +888,9 @@ message OrganizationDefinition {
876888
pattern: "^[A-Z][A-Z0-9_]*$"
877889
}];
878890

879-
// name is the legal or display name of the organization.
880-
string name = 2 [(buf.validate.field).string = {
881-
min_len: 1
882-
max_len: 255
883-
}];
891+
// name is the legacy display name of the organization.
892+
// Deprecated: Use legal_name and display_name instead. Retained for backward compatibility.
893+
string name = 2 [(buf.validate.field).string.max_len = 255];
884894

885895
// party_type is the party classification (e.g., "ORGANIZATION", "COUNTERPARTY").
886896
// Must match a party type code from the manifest's party_types or a built-in type.
@@ -893,6 +903,26 @@ message OrganizationDefinition {
893903
// attributes contains additional metadata for this organization as key-value pairs.
894904
// Keys and values are validated against the party type's attribute_schema if defined.
895905
map<string, string> attributes = 4;
906+
907+
// legal_name is the official legal name of the organization.
908+
// Maps directly to the party.register_organization handler's legal_name parameter.
909+
// If empty, falls back to the name field for backward compatibility.
910+
optional string legal_name = 5 [(buf.validate.field).string.max_len = 255];
911+
912+
// display_name is the human-readable display name of the organization.
913+
// Maps directly to the party.register_organization handler's display_name parameter.
914+
// If empty, falls back to legal_name, then name.
915+
optional string display_name = 6 [(buf.validate.field).string.max_len = 255];
916+
917+
// external_reference is a stable identifier for this organization in an external system.
918+
// Maps directly to the party.register_organization handler's external_reference parameter.
919+
// If empty, falls back to the organization code.
920+
optional string external_reference = 7 [(buf.validate.field).string.max_len = 255];
921+
922+
// external_reference_type identifies the type of external reference (e.g., "LEI", "NATIONAL_ID").
923+
// Accepts stripped enum names (e.g., "LEI") - the handler accepts both
924+
// "LEI" and "EXTERNAL_REFERENCE_TYPE_LEI" formats.
925+
optional string external_reference_type = 8 [(buf.validate.field).string.max_len = 64];
896926
}
897927

898928
// ========================================

frontend/src/api/gen/meridian/control_plane/v1/manifest_pb.ts

Lines changed: 59 additions & 2 deletions
Large diffs are not rendered by default.

services/control-plane/internal/applier/defaults/apply_manifest/v1.3.0.star

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,19 +171,19 @@ def execute_apply_manifest():
171171
})
172172

173173
# Phase 55: Register organizations with Party service
174+
# Fields are passed through directly from the manifest proto via buildSagaInput.
175+
# Fallback resolution (legal_name from name, external_reference from code) is
176+
# handled upstream in extractPartyAndAccounts, so the Starlark script receives
177+
# pre-resolved values.
174178
for org in organizations:
175179
step(name="register_organization_" + org["code"])
176-
org_name = org.get("name", org["code"])
177-
org_attrs = org.get("attributes", {})
178-
# Use external_reference from attributes if present, otherwise fall back to org code.
179-
ext_ref = org_attrs.get("external_reference", org["code"])
180180
party.register_organization(
181-
legal_name=org_name,
182-
display_name=org_name,
181+
legal_name=org.get("legal_name", org.get("name", org["code"])),
182+
display_name=org.get("display_name", org.get("legal_name", org.get("name", org["code"]))),
183183
party_type=org.get("party_type", "ORGANIZATION"),
184-
external_reference=ext_ref,
185-
external_reference_type=org.get("external_reference_type", "NATIONAL_ID"),
186-
attributes=org_attrs,
184+
external_reference=org.get("external_reference", org["code"]),
185+
external_reference_type=org.get("external_reference_type", ""),
186+
attributes=org.get("attributes", {}),
187187
)
188188
registered_organizations.append({
189189
"code": org["code"],

services/control-plane/internal/applier/executor.go

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -221,20 +221,26 @@ type MarketDataSourceInput struct {
221221

222222
// MarketDataSetInput represents a market data set to register and activate.
223223
type MarketDataSetInput struct {
224-
Code string
225-
Category string
226-
Unit string
227-
SourceCode string
228-
DisplayName string
229-
Description string
224+
Code string
225+
Category string
226+
Unit string
227+
SourceCode string
228+
DisplayName string
229+
Description string
230+
ValidationExpression string
231+
ResolutionKeyExpression string
230232
}
231233

232234
// OrganizationInput represents an organization to register.
233235
type OrganizationInput struct {
234-
Code string
235-
Name string
236-
PartyType string
237-
Attributes map[string]string
236+
Code string
237+
Name string
238+
LegalName string
239+
DisplayName string
240+
ExternalReference string
241+
ExternalReferenceType string
242+
PartyType string
243+
Attributes map[string]string
238244
}
239245

240246
// InternalAccountInput represents an internal account to initiate.
@@ -484,12 +490,14 @@ func (e *ManifestExecutor) buildSagaInput(input *ApplyManifestInput) map[string]
484490
marketDataSets := make([]interface{}, len(input.MarketDataSets))
485491
for i, ds := range input.MarketDataSets {
486492
marketDataSets[i] = map[string]interface{}{
487-
"code": ds.Code,
488-
"category": ds.Category,
489-
"unit": ds.Unit,
490-
"source_code": ds.SourceCode,
491-
"display_name": ds.DisplayName,
492-
"description": ds.Description,
493+
"code": ds.Code,
494+
"category": ds.Category,
495+
"unit": ds.Unit,
496+
"source_code": ds.SourceCode,
497+
"display_name": ds.DisplayName,
498+
"description": ds.Description,
499+
"validation_expression": ds.ValidationExpression,
500+
"resolution_key_expression": ds.ResolutionKeyExpression,
493501
}
494502
}
495503
sagaInput["market_data_sets"] = marketDataSets
@@ -515,10 +523,14 @@ func (e *ManifestExecutor) buildSagaInput(input *ApplyManifestInput) map[string]
515523
attrs[k] = v
516524
}
517525
organizations[i] = map[string]interface{}{
518-
"code": org.Code,
519-
"name": org.Name,
520-
"party_type": org.PartyType,
521-
"attributes": attrs,
526+
"code": org.Code,
527+
"name": org.Name,
528+
"legal_name": org.LegalName,
529+
"display_name": org.DisplayName,
530+
"external_reference": org.ExternalReference,
531+
"external_reference_type": org.ExternalReferenceType,
532+
"party_type": org.PartyType,
533+
"attributes": attrs,
522534
}
523535
}
524536
sagaInput["organizations"] = organizations

services/control-plane/internal/applier/executor_test.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,20 +140,26 @@ func TestBuildSagaInput_NewResourceTypes(t *testing.T) {
140140
},
141141
MarketDataSets: []MarketDataSetInput{
142142
{
143-
Code: "USD_EUR_FX",
144-
Category: "DATA_CATEGORY_FX_RATE",
145-
Unit: "USD/EUR",
146-
SourceCode: "BLOOMBERG",
147-
DisplayName: "USD/EUR Spot Rate",
148-
Description: "Spot FX rate",
143+
Code: "USD_EUR_FX",
144+
Category: "DATA_CATEGORY_FX_RATE",
145+
Unit: "USD/EUR",
146+
SourceCode: "BLOOMBERG",
147+
DisplayName: "USD/EUR Spot Rate",
148+
Description: "Spot FX rate",
149+
ValidationExpression: "value > 0",
150+
ResolutionKeyExpression: "observed_at",
149151
},
150152
},
151153
Organizations: []OrganizationInput{
152154
{
153-
Code: "ACME_ENERGY",
154-
Name: "Acme Energy Corp",
155-
PartyType: "ORGANIZATION",
156-
Attributes: map[string]string{"industry": "energy"},
155+
Code: "ACME_ENERGY",
156+
Name: "Acme Energy Corp",
157+
LegalName: "Acme Energy Corporation",
158+
DisplayName: "Acme Energy",
159+
ExternalReference: "LEI-ACME-001",
160+
ExternalReferenceType: "LEI",
161+
PartyType: "ORGANIZATION",
162+
Attributes: map[string]string{"industry": "energy"},
157163
},
158164
},
159165
InternalAccounts: []InternalAccountInput{
@@ -184,13 +190,19 @@ func TestBuildSagaInput_NewResourceTypes(t *testing.T) {
184190
require.True(t, ok)
185191
assert.Equal(t, "USD_EUR_FX", firstDS["code"])
186192
assert.Equal(t, "BLOOMBERG", firstDS["source_code"])
193+
assert.Equal(t, "value > 0", firstDS["validation_expression"])
194+
assert.Equal(t, "observed_at", firstDS["resolution_key_expression"])
187195

188196
orgs, ok := sagaInput["organizations"].([]interface{})
189197
require.True(t, ok)
190198
require.Len(t, orgs, 1)
191199
firstOrg, ok := orgs[0].(map[string]interface{})
192200
require.True(t, ok)
193201
assert.Equal(t, "ACME_ENERGY", firstOrg["code"])
202+
assert.Equal(t, "Acme Energy Corporation", firstOrg["legal_name"])
203+
assert.Equal(t, "Acme Energy", firstOrg["display_name"])
204+
assert.Equal(t, "LEI-ACME-001", firstOrg["external_reference"])
205+
assert.Equal(t, "LEI", firstOrg["external_reference_type"])
194206
attrs, ok := firstOrg["attributes"].(map[string]interface{})
195207
require.True(t, ok)
196208
assert.Equal(t, "energy", attrs["industry"])

services/control-plane/internal/applier/grpc_handler.go

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -801,24 +801,51 @@ func extractMarketData(mf *controlplanev1.Manifest, input *ApplyManifestInput) {
801801
}
802802
for _, ds := range md.GetDatasets() {
803803
input.MarketDataSets = append(input.MarketDataSets, MarketDataSetInput{
804-
Code: ds.GetCode(),
805-
Category: stripEnumPrefix(ds.GetCategory().String(), "DATA_CATEGORY_"),
806-
Unit: ds.GetUnit(),
807-
SourceCode: ds.GetSourceCode(),
808-
DisplayName: ds.GetDisplayName(),
809-
Description: ds.GetDescription(),
804+
Code: ds.GetCode(),
805+
Category: stripEnumPrefix(ds.GetCategory().String(), "DATA_CATEGORY_"),
806+
Unit: ds.GetUnit(),
807+
SourceCode: ds.GetSourceCode(),
808+
DisplayName: ds.GetDisplayName(),
809+
Description: ds.GetDescription(),
810+
ValidationExpression: ds.GetValidationExpression(),
811+
ResolutionKeyExpression: ds.GetResolutionKeyExpression(),
810812
})
811813
}
812814
}
813815

814816
// extractPartyAndAccounts converts organizations and internal accounts from the manifest proto.
815817
func extractPartyAndAccounts(mf *controlplanev1.Manifest, input *ApplyManifestInput) {
816818
for _, org := range mf.GetOrganizations() {
819+
// Resolve legal_name with fallback chain: legal_name -> name -> code
820+
legalName := org.GetLegalName()
821+
if legalName == "" {
822+
legalName = org.GetName()
823+
}
824+
if legalName == "" {
825+
legalName = org.GetCode()
826+
}
827+
828+
// Resolve display_name with fallback chain: display_name -> legal_name
829+
displayName := org.GetDisplayName()
830+
if displayName == "" {
831+
displayName = legalName
832+
}
833+
834+
// Resolve external_reference with fallback: external_reference -> code
835+
extRef := org.GetExternalReference()
836+
if extRef == "" {
837+
extRef = org.GetCode()
838+
}
839+
817840
input.Organizations = append(input.Organizations, OrganizationInput{
818-
Code: org.GetCode(),
819-
Name: org.GetName(),
820-
PartyType: org.GetPartyType(),
821-
Attributes: org.GetAttributes(),
841+
Code: org.GetCode(),
842+
Name: org.GetName(),
843+
LegalName: legalName,
844+
DisplayName: displayName,
845+
ExternalReference: extRef,
846+
ExternalReferenceType: org.GetExternalReferenceType(),
847+
PartyType: org.GetPartyType(),
848+
Attributes: org.GetAttributes(),
822849
})
823850
}
824851
for _, ia := range mf.GetInternalAccounts() {

0 commit comments

Comments
 (0)