Skip to content

Commit 53edf3f

Browse files
dmealingclaude
andcommitted
fix(loader): #368 — rule (d)'s message names both legal homes; correct rule (e)'s scope note
Two small cross-port corrections, identical in TypeScript, Python, C# and Java. M7 — rule (d) rejected @sourceRefField with "but is not a M:N relationship.", which reads as "this attribute is never allowed here" now that @Cardinality: "one" is a legal home for it (the whole point of this branch). The message now names both: "...but is neither a M:N relationship (requires @through with @Cardinality: "many") nor a @Cardinality: "one" relationship." Wording is byte-identical across the four ports. M6 — rule (e)'s header comment still justified itself against a rule (d) that was "own-scoped" ("so own-scoping there is correct"). Rule (d) stopped being own-scoped in this same branch: it walks the effective relationship set and dedupes by node identity, reporting against the declaring entity. The comment now says what actually differs — the SUBJECT of each pass, not which relationships it can see. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfd8nat1WcSpXffd8YetiC
1 parent 6b47b8d commit 53edf3f

5 files changed

Lines changed: 49 additions & 17 deletions

File tree

‎server/csharp/MetaObjects/Loader/ValidationPasses.cs‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3202,7 +3202,9 @@ public static IReadOnlyList<MetaError> ValidateRelationships(MetaData root)
32023202
if (hasSourceRefField && !isCardinalityOne)
32033203
{
32043204
errors.Add(new MetaError(
3205-
$"relationship \"{declaringEntity.Name}.{rel.Name}\" sets @{RELATIONSHIP_ATTR_SOURCE_REF_FIELD} but is not a M:N relationship.",
3205+
$"relationship \"{declaringEntity.Name}.{rel.Name}\" sets @{RELATIONSHIP_ATTR_SOURCE_REF_FIELD} but is neither a M:N " +
3206+
$"relationship (requires @{RELATIONSHIP_ATTR_THROUGH} with @{RELATIONSHIP_ATTR_CARDINALITY}: \"{CARDINALITY_MANY}\") " +
3207+
$"nor a @{RELATIONSHIP_ATTR_CARDINALITY}: \"{CARDINALITY_ONE}\" relationship.",
32063208
ErrorCode.ERR_INVALID_RELATIONSHIP,
32073209
Envelope: rel.Source));
32083210
}
@@ -3306,9 +3308,13 @@ public static IReadOnlyList<MetaError> ValidateRelationships(MetaData root)
33063308
// above) — same deferred-resolution timing (after all files load + extends
33073309
// resolution).
33083310
//
3309-
// Scope differs deliberately from rule (d): rule (d) validates attrs that
3310-
// travel with the relationship's OWN declaration (@through/@symmetric/
3311-
// @sourceRefField), so own-scoping there is correct — those attrs don't
3311+
// Scope differs deliberately from rule (d) — in SUBJECT, not in which
3312+
// relationships each pass walks (both walk the EFFECTIVE set; rule (d) is
3313+
// no longer own-scoped, or an M:N declaration reached only via extends
3314+
// would go unchecked). Rule (d) validates attrs that travel with the
3315+
// relationship's OWN declaration (@through/@symmetric/@sourceRefField), so
3316+
// it checks each declaration EXACTLY ONCE — deduped by node identity, and
3317+
// reported against the entity that declares it — because those attrs don't
33123318
// change meaning depending on who inherits the relationship. Rule (e)
33133319
// instead validates whether THIS entity's reference set resolves the
33143320
// relationship uniquely, which is a property of the EFFECTIVE entity, not of

‎server/java/metadata/src/main/java/com/metaobjects/loader/ValidationPhase.java‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1819,7 +1819,12 @@ private static void validateRelationshipM2MNode(MetaRoot root, MetaObject obj,
18191819
ErrorMessageConstants.ERR_INVALID_RELATIONSHIP
18201820
+ ": relationship \"" + declaringEntity.getShortName() + "." + rel.getShortName()
18211821
+ "\" sets @" + MetaRelationship.ATTR_SOURCE_REF_FIELD
1822-
+ " but is not a M:N relationship.",
1822+
+ " but is neither a M:N relationship (requires @"
1823+
+ MetaRelationship.ATTR_THROUGH + " with @"
1824+
+ MetaRelationship.ATTR_CARDINALITY + ": \""
1825+
+ MetaRelationship.CARDINALITY_MANY + "\") nor a @"
1826+
+ MetaRelationship.ATTR_CARDINALITY + ": \""
1827+
+ MetaRelationship.CARDINALITY_ONE + "\" relationship.",
18231828
ErrorCode.ERR_INVALID_RELATIONSHIP, rel.getSource()));
18241829
}
18251830
if (symmetric) {
@@ -1926,9 +1931,13 @@ private static void validateRelationshipM2MNode(MetaRoot root, MetaObject obj,
19261931
// above) — same deferred-resolution timing (after all files load + extends
19271932
// resolution).
19281933
//
1929-
// Scope differs deliberately from rule (d): rule (d) validates attrs that
1930-
// travel with the relationship's OWN declaration (@through/@symmetric/
1931-
// @sourceRefField), so own-scoping there is correct — those attrs don't
1934+
// Scope differs deliberately from rule (d) — in SUBJECT, not in which
1935+
// relationships each pass walks (both walk the EFFECTIVE set; rule (d) is
1936+
// no longer own-scoped, or an M:N declaration reached only via extends
1937+
// would go unchecked). Rule (d) validates attrs that travel with the
1938+
// relationship's OWN declaration (@through/@symmetric/@sourceRefField), so
1939+
// it checks each declaration EXACTLY ONCE — deduped by node identity, and
1940+
// reported against the entity that declares it — because those attrs don't
19321941
// change meaning depending on who inherits the relationship. Rule (e)
19331942
// instead validates whether THIS entity's reference set resolves the
19341943
// relationship uniquely, which is a property of the EFFECTIVE entity, not of

‎server/java/metadata/src/test/java/com/metaobjects/relationship/Issue368RelationshipReferenceValidationTest.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,11 @@ public void sourceRefFieldOnAbsentCardinalityStillErrors() {
224224
assertHasError(outcome, ErrorCode.ERR_INVALID_RELATIONSHIP);
225225
assertTrue(outcome.joinedMessages(), outcome.joinedMessages().contains("Week.program"));
226226
assertTrue(outcome.joinedMessages(), outcome.joinedMessages()
227-
.contains("sets @sourceRefField but is not a M:N relationship"));
227+
.contains("sets @sourceRefField but is neither a M:N relationship"));
228+
// The message names BOTH legal homes for the attribute -- M:N and
229+
// @cardinality: "one" -- so it can't read as "never allowed here".
230+
assertTrue(outcome.joinedMessages(), outcome.joinedMessages()
231+
.contains("nor a @cardinality: \"one\" relationship"));
228232
}
229233

230234
@Test

‎server/python/src/metaobjects/loader/validation_passes.py‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2698,7 +2698,10 @@ def _validate_relationships(root: MetaData, errors: list[MetaError]) -> None:
26982698
if has_source_ref_field and not is_cardinality_one:
26992699
errors.append(MetaError(
27002700
f'relationship "{declaring_entity.name}.{rel.name}" sets '
2701-
f'@{RELATIONSHIP_ATTR_SOURCE_REF_FIELD} but is not a M:N relationship.',
2701+
f'@{RELATIONSHIP_ATTR_SOURCE_REF_FIELD} but is neither a M:N '
2702+
f'relationship (requires @{RELATIONSHIP_ATTR_THROUGH} with '
2703+
f'@{RELATIONSHIP_ATTR_CARDINALITY}: "{CARDINALITY_MANY}") nor a '
2704+
f'@{RELATIONSHIP_ATTR_CARDINALITY}: "{CARDINALITY_ONE}" relationship.',
27022705
ErrorCode.ERR_INVALID_RELATIONSHIP,
27032706
envelope=rel.source,
27042707
))
@@ -2807,9 +2810,13 @@ def _validate_relationships(root: MetaData, errors: list[MetaError]) -> None:
28072810
# above) — same deferred-resolution timing (after all files load + extends
28082811
# resolution).
28092812
#
2810-
# Scope differs deliberately from rule (d): rule (d) validates attrs that
2811-
# travel with the relationship's OWN declaration (@through/@symmetric/
2812-
# @sourceRefField), so own-scoping there is correct — those attrs don't
2813+
# Scope differs deliberately from rule (d) — in SUBJECT, not in which
2814+
# relationships each pass walks (both walk the EFFECTIVE set; rule (d) is
2815+
# no longer own-scoped, or an M:N declaration reached only via extends
2816+
# would go unchecked). Rule (d) validates attrs that travel with the
2817+
# relationship's OWN declaration (@through/@symmetric/@sourceRefField), so
2818+
# it checks each declaration EXACTLY ONCE — deduped by node identity, and
2819+
# reported against the entity that declares it — because those attrs don't
28132820
# change meaning depending on who inherits the relationship. Rule (e)
28142821
# instead validates whether THIS entity's reference set resolves the
28152822
# relationship uniquely, which is a property of the EFFECTIVE entity, not of

‎server/typescript/packages/metadata/src/loader/validation-passes.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2058,7 +2058,9 @@ export function validateRelationships(root: MetaData): ParseError[] {
20582058
if (hasSourceRefField && cardinality !== CARDINALITY_ONE) {
20592059
errors.push(
20602060
new ParseError(
2061-
`relationship "${declaringEntity.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} but is not a M:N relationship.`,
2061+
`relationship "${declaringEntity.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} but is neither a M:N ` +
2062+
`relationship (requires @${RELATIONSHIP_ATTR_THROUGH} with @${RELATIONSHIP_ATTR_CARDINALITY}: "${CARDINALITY_MANY}") ` +
2063+
`nor a @${RELATIONSHIP_ATTR_CARDINALITY}: "${CARDINALITY_ONE}" relationship.`,
20622064
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
20632065
),
20642066
);
@@ -2168,9 +2170,13 @@ export function validateRelationships(root: MetaData): ParseError[] {
21682170
// above) — same deferred-resolution timing (after all files load + extends
21692171
// resolution).
21702172
//
2171-
// Scope differs deliberately from rule (d): rule (d) validates attrs that
2172-
// travel with the relationship's OWN declaration (@through/@symmetric/
2173-
// @sourceRefField), so own-scoping there is correct — those attrs don't
2173+
// Scope differs deliberately from rule (d) — in SUBJECT, not in which
2174+
// relationships each pass walks (both walk the EFFECTIVE set; rule (d) is
2175+
// no longer own-scoped, or an M:N declaration reached only via extends
2176+
// would go unchecked). Rule (d) validates attrs that travel with the
2177+
// relationship's OWN declaration (@through/@symmetric/@sourceRefField), so
2178+
// it checks each declaration EXACTLY ONCE — deduped by node identity, and
2179+
// reported against the entity that declares it — because those attrs don't
21742180
// change meaning depending on who inherits the relationship. Rule (e)
21752181
// instead validates whether THIS entity's reference set resolves the
21762182
// relationship uniquely, which is a property of the EFFECTIVE entity, not of

0 commit comments

Comments
 (0)