@metaobjectsdev/metadata 1.0.3 → 1.0.4-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/core/relationship/derive-m2m-fields.d.ts +3 -1
  2. package/dist/core/relationship/derive-m2m-fields.d.ts.map +1 -1
  3. package/dist/core/relationship/derive-m2m-fields.js +110 -15
  4. package/dist/core/relationship/derive-m2m-fields.js.map +1 -1
  5. package/dist/core/relationship/find-reference.d.ts +21 -5
  6. package/dist/core/relationship/find-reference.d.ts.map +1 -1
  7. package/dist/core/relationship/find-reference.js +29 -10
  8. package/dist/core/relationship/find-reference.js.map +1 -1
  9. package/dist/core/relationship/relationship-definition.embedded.js +8 -8
  10. package/dist/core/relationship/relationship-definition.embedded.js.map +1 -1
  11. package/dist/core/relationship/resolve-relationship-reference.d.ts +27 -0
  12. package/dist/core/relationship/resolve-relationship-reference.d.ts.map +1 -0
  13. package/dist/core/relationship/resolve-relationship-reference.js +84 -0
  14. package/dist/core/relationship/resolve-relationship-reference.js.map +1 -0
  15. package/dist/errors.d.ts +1 -1
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +10 -0
  18. package/dist/errors.js.map +1 -1
  19. package/dist/index.d.ts +2 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/library/embedded-library.generated.d.ts +2 -0
  24. package/dist/library/embedded-library.generated.d.ts.map +1 -1
  25. package/dist/library/embedded-library.generated.js +11 -1
  26. package/dist/library/embedded-library.generated.js.map +1 -1
  27. package/dist/library/index.d.ts +2 -0
  28. package/dist/library/index.d.ts.map +1 -0
  29. package/dist/library/index.js +24 -0
  30. package/dist/library/index.js.map +1 -0
  31. package/dist/library/library-sources.d.ts +99 -6
  32. package/dist/library/library-sources.d.ts.map +1 -1
  33. package/dist/library/library-sources.js +165 -43
  34. package/dist/library/library-sources.js.map +1 -1
  35. package/dist/loader/meta-data-loader.d.ts.map +1 -1
  36. package/dist/loader/meta-data-loader.js +6 -1
  37. package/dist/loader/meta-data-loader.js.map +1 -1
  38. package/dist/loader/validation-passes.d.ts +2 -0
  39. package/dist/loader/validation-passes.d.ts.map +1 -1
  40. package/dist/loader/validation-passes.js +162 -22
  41. package/dist/loader/validation-passes.js.map +1 -1
  42. package/dist/parser-core.d.ts.map +1 -1
  43. package/dist/parser-core.js +13 -2
  44. package/dist/parser-core.js.map +1 -1
  45. package/dist/vocabulary-rewrite-yaml.d.ts +2 -0
  46. package/dist/vocabulary-rewrite-yaml.d.ts.map +1 -0
  47. package/dist/vocabulary-rewrite-yaml.js +22 -0
  48. package/dist/vocabulary-rewrite-yaml.js.map +1 -0
  49. package/package.json +7 -7
  50. package/src/core/relationship/derive-m2m-fields.ts +115 -15
  51. package/src/core/relationship/find-reference.ts +34 -11
  52. package/src/core/relationship/relationship-definition.embedded.ts +8 -8
  53. package/src/core/relationship/resolve-relationship-reference.ts +99 -0
  54. package/src/errors.ts +10 -0
  55. package/src/index.ts +6 -1
  56. package/src/library/embedded-library.generated.ts +12 -1
  57. package/src/library/index.ts +23 -0
  58. package/src/library/library-sources.ts +197 -47
  59. package/src/loader/meta-data-loader.ts +7 -1
  60. package/src/loader/validation-passes.ts +181 -23
  61. package/src/parser-core.ts +15 -2
  62. package/src/vocabulary-rewrite-yaml.ts +21 -0
@@ -25,7 +25,7 @@ import { ParseError } from "../errors.js";
25
25
  import type { LoaderWarning } from "../source.js";
26
26
  import { codeSource, resolvedSource } from "../source.js";
27
27
  import { parseJson } from "../parser-json.js";
28
- import { validateDataGridSortFields, validateFilterableHasIndex, validateFilterableHasSupportedOps, validateSortableHasSupportedSubtype, validateOriginPaths, validateDerivedFieldProvidability, validateDataGridFilterValues, validateFieldObjectStorage, validateFieldMap, validateTemplatePayloadRefs, validateFieldDefaults, validateRelationships, validateIndexLookupFields, validateProjectionFilter, validateRetiredRequirementLinks } from "./validation-passes.js";
28
+ import { validateDataGridSortFields, validateFilterableHasIndex, validateFilterableHasSupportedOps, validateSortableHasSupportedSubtype, validateOriginPaths, validateDerivedFieldProvidability, validateDataGridFilterValues, validateFieldObjectStorage, validateFieldMap, validateTemplatePayloadRefs, validateFieldDefaults, validateRelationships, validateOneSideReferenceResolution, validateIndexLookupFields, validateProjectionFilter, validateRetiredRequirementLinks } from "./validation-passes.js";
29
29
  import { runRegisteredValidation } from "./validation-registry.js";
30
30
  import { validateSourceRoles } from "../persistence/source/validate-source-roles.js";
31
31
  import { validateSourceEscapes } from "../persistence/source/validate-source-escapes.js";
@@ -652,6 +652,12 @@ export class MetaDataLoader {
652
652
  // are invalid on a 1:N relationship.
653
653
  errors.push(...validateRelationships(root));
654
654
 
655
+ // Rule (e) — #368: a `@cardinality: one` relationship must resolve to
656
+ // exactly one identity.reference when its declaring entity holds more
657
+ // than one onto the same target; an unresolvable case is a load error
658
+ // naming the candidates (ADR-0029 §5).
659
+ errors.push(...validateOneSideReferenceResolution(root));
660
+
655
661
  // index.lookup @fields resolution — each index.lookup must name ≥1 field,
656
662
  // and every field must exist in the entity's effective (resolved) field set
657
663
  // (ADR-0039: resolving accessor, so inherited fields via extends are visible).
@@ -129,6 +129,11 @@ import {
129
129
  CARDINALITY_ONE,
130
130
  CARDINALITY_MANY,
131
131
  } from "../core/relationship/relationship-constants.js";
132
+ import {
133
+ referenceCandidatesFor,
134
+ resolveRelationshipReference,
135
+ } from "../core/relationship/resolve-relationship-reference.js";
136
+ import type { MetaRoot } from "../shared/meta-root.js";
132
137
  import { stripPackage } from "../naming.js";
133
138
  import {
134
139
  FILTER_COMPOSE_OR,
@@ -1938,11 +1943,29 @@ export function validateDataGridFilterValues(root: MetaData): ParseError[] {
1938
1943
  // (c) When @through is present: the named entity must exist and declare exactly
1939
1944
  // two identity.reference children; @sourceRefField (if present) must match
1940
1945
  // one of those references' FK fields → ERR_INVALID_RELATIONSHIP.
1941
- // (d) @through / @sourceRefField / @symmetric are invalid on a non-M:N
1942
- // relationship (@cardinality != "many", or no @through) → ERR_INVALID_RELATIONSHIP.
1946
+ // (d) @through / @symmetric are invalid on a non-M:N relationship
1947
+ // (@cardinality != "many", or no @through) → ERR_INVALID_RELATIONSHIP.
1948
+ // @sourceRefField is also invalid there, EXCEPT on @cardinality: "one"
1949
+ // (#368: it then names which of several identity.reference nodes onto
1950
+ // the same target the relationship navigates).
1943
1951
  //
1944
- // Own-relationships only: a relationship is validated on the entity that declares
1945
- // it (matching the own-attrs policy of the other passes).
1952
+ // ADR-0039: resolving, not own-only (#368 fix round 3) every rule above
1953
+ // validates a property of the relationship's OWN declaration (its attrs,
1954
+ // plus for rule (c) the @through target resolved against the root), so a
1955
+ // relationship inherited via extends must be validated wherever it's
1956
+ // visible, or a child entity that only SEES the relationship through
1957
+ // inheritance could carry a violation no pass ever examines. That makes an
1958
+ // inherited, UNMODIFIED relationship visited once per inheriting entity —
1959
+ // checked once below (keyed on the relationship node's own identity), not
1960
+ // once per entity, using the DECLARING entity (rel.parent) rather than
1961
+ // whichever entity's effective view got there first for every piece of
1962
+ // context a rule reads (message text, @through's package per ADR-0042, rule
1963
+ // (a)'s self-join comparison). That keeps each check's result independent of
1964
+ // which entity triggered it, which is what makes "checked once" both
1965
+ // sufficient and correct. An override replaces the node in place
1966
+ // (MetaData._effectiveChildren), so it is never the same object as what it
1967
+ // overrides and is never skipped against it — a genuinely different
1968
+ // declaration is always independently checked.
1946
1969
  // ---------------------------------------------------------------------------
1947
1970
 
1948
1971
  // The junction's reference view: the validator and the runtime/codegen FK
@@ -1970,14 +1993,36 @@ function _countJunctionReferences(junction: MetaData): number {
1970
1993
 
1971
1994
  export function validateRelationships(root: MetaData): ParseError[] {
1972
1995
  const errors: ParseError[] = [];
1996
+ // #368 fix round 3 — the outer loop below is now resolving (obj.relationships()),
1997
+ // so a relationship inherited unmodified by N entities is reached N times. Its
1998
+ // own attrs never change based on who inherits it, so re-validating it more
1999
+ // than once would report the identical finding N times — pure noise. Checked
2000
+ // is keyed on the relationship NODE's own object identity: MetaData._effectiveChildren
2001
+ // reuses the super's child object in place for an unmodified inherited child,
2002
+ // so the same physical declaration IS the same object everywhere it's visible,
2003
+ // while an override replaces it with a genuinely different object (correctly
2004
+ // NOT skipped — a distinct declaration is a distinct finding).
2005
+ const checked = new Set<MetaData>();
1973
2006
  // ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
1974
2007
  for (const obj of root.children().filter((c) => c.type === TYPE_OBJECT)) {
1975
- // ADR-0042a bare @through resolves in the declaring entity's package.
1976
- const referrerPkg = obj.package ?? obj.fileDefaultPackage ?? "";
1977
- // ADR-0039: own a relationship is validated on the entity that DECLARES it
1978
- // (the M:N slim-vocabulary rules apply to own-declared relationships; its
1979
- // inheritable attrs are read resolving below).
1980
- for (const rel of obj.ownChildren().filter((c) => c.type === TYPE_RELATIONSHIP)) {
2008
+ // ADR-0039: resolving see the header comment above: rule (d) (like rule
2009
+ // (e)) must see a relationship inherited via extends, not just this
2010
+ // entity's own declarations. `checked` absorbs the resulting revisits.
2011
+ for (const rel of (obj as MetaObject).relationships()) {
2012
+ if (checked.has(rel)) continue;
2013
+ checked.add(rel);
2014
+
2015
+ // Every rule below validates a property of the relationship's OWN
2016
+ // declaration (its attrs, plus for rule (c) the @through target), so
2017
+ // context — the entity name in messages, the package a bare @through
2018
+ // resolves in (ADR-0042), and rule (a)'s self-join comparison — is
2019
+ // always the entity that DECLARES `rel` (rel.parent), never `obj` (the
2020
+ // entity whose effective view happened to reach it first). This keeps
2021
+ // the check's result independent of iteration order/inheritance depth,
2022
+ // which is what makes checking each node exactly once correct.
2023
+ const declaringEntity = rel.parent ?? obj;
2024
+ const referrerPkg = declaringEntity.package ?? declaringEntity.fileDefaultPackage ?? "";
2025
+
1981
2026
  // ADR-0039: resolving — a relationship may inherit its M:N attrs via extends.
1982
2027
  const through = rel.attr(RELATIONSHIP_ATTR_THROUGH);
1983
2028
  const sourceRefField = rel.attr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
@@ -1999,16 +2044,23 @@ export function validateRelationships(root: MetaData): ParseError[] {
1999
2044
  if (hasThrough) {
2000
2045
  errors.push(
2001
2046
  new ParseError(
2002
- `relationship "${obj.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_THROUGH} but is not a M:N ` +
2047
+ `relationship "${declaringEntity.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_THROUGH} but is not a M:N ` +
2003
2048
  `relationship (requires @${RELATIONSHIP_ATTR_CARDINALITY}: "${CARDINALITY_MANY}").`,
2004
2049
  { code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
2005
2050
  ),
2006
2051
  );
2007
2052
  }
2008
- if (hasSourceRefField) {
2053
+ // #368: @sourceRefField also disambiguates a `@cardinality: one`
2054
+ // relationship when the entity holds more than one identity.reference
2055
+ // onto the same target. Only the M:N *junction* reading is rejected
2056
+ // here; rule (e) — validateOneSideReferenceResolution, below in this
2057
+ // file — checks that it names a real local reference.
2058
+ if (hasSourceRefField && cardinality !== CARDINALITY_ONE) {
2009
2059
  errors.push(
2010
2060
  new ParseError(
2011
- `relationship "${obj.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.`,
2012
2064
  { code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
2013
2065
  ),
2014
2066
  );
@@ -2016,7 +2068,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
2016
2068
  if (symmetric) {
2017
2069
  errors.push(
2018
2070
  new ParseError(
2019
- `relationship "${obj.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SYMMETRIC} but is not a M:N relationship.`,
2071
+ `relationship "${declaringEntity.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SYMMETRIC} but is not a M:N relationship.`,
2020
2072
  { code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
2021
2073
  ),
2022
2074
  );
@@ -2028,7 +2080,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
2028
2080
  if (symmetric && hasSourceRefField) {
2029
2081
  errors.push(
2030
2082
  new ParseError(
2031
- `relationship "${obj.name}.${rel.name}" sets both @${RELATIONSHIP_ATTR_SYMMETRIC} and ` +
2083
+ `relationship "${declaringEntity.name}.${rel.name}" sets both @${RELATIONSHIP_ATTR_SYMMETRIC} and ` +
2032
2084
  `@${RELATIONSHIP_ATTR_SOURCE_REF_FIELD}; they are mutually exclusive.`,
2033
2085
  { code: "ERR_BAD_ATTR_VALUE", source: rel.source },
2034
2086
  ),
@@ -2040,12 +2092,13 @@ export function validateRelationships(root: MetaData): ParseError[] {
2040
2092
  // in this package is self, but an FQN "other::Widget" (a different same-short-
2041
2093
  // name entity) is NOT (comparing stripped short names would misclassify it).
2042
2094
  const isSelfJoin =
2043
- typeof objectRef === "string" && resolveObjectRef(root, objectRef, referrerPkg).node === obj;
2095
+ typeof objectRef === "string" &&
2096
+ resolveObjectRef(root, objectRef, referrerPkg).node === declaringEntity;
2044
2097
  if (symmetric && !isSelfJoin) {
2045
2098
  errors.push(
2046
2099
  new ParseError(
2047
- `relationship "${obj.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SYMMETRIC} but @${RELATIONSHIP_ATTR_OBJECT_REF} ` +
2048
- `"${String(objectRef)}" is not the declaring entity "${obj.name}"; @${RELATIONSHIP_ATTR_SYMMETRIC} is self-join-only.`,
2100
+ `relationship "${declaringEntity.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SYMMETRIC} but @${RELATIONSHIP_ATTR_OBJECT_REF} ` +
2101
+ `"${String(objectRef)}" is not the declaring entity "${declaringEntity.name}"; @${RELATIONSHIP_ATTR_SYMMETRIC} is self-join-only.`,
2049
2102
  { code: "ERR_BAD_ATTR_VALUE", source: rel.source },
2050
2103
  ),
2051
2104
  );
@@ -2056,8 +2109,8 @@ export function validateRelationships(root: MetaData): ParseError[] {
2056
2109
  if (!junction) {
2057
2110
  errors.push(
2058
2111
  new ParseError(
2059
- `relationship "${obj.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" does not resolve to an entity.${didYouMeanHint(root, String(through))}`,
2060
- { code: "ERR_INVALID_RELATIONSHIP", source: resolvedSource(rel.source, `${obj.fqn()}::${rel.name}`, String(through)) },
2112
+ `relationship "${declaringEntity.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" does not resolve to an entity.${didYouMeanHint(root, String(through))}`,
2113
+ { code: "ERR_INVALID_RELATIONSHIP", source: resolvedSource(rel.source, `${declaringEntity.fqn()}::${rel.name}`, String(through)) },
2061
2114
  ),
2062
2115
  );
2063
2116
  continue;
@@ -2069,7 +2122,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
2069
2122
  if (junction.subType !== OBJECT_SUBTYPE_ENTITY) {
2070
2123
  errors.push(
2071
2124
  new ParseError(
2072
- `relationship "${obj.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" resolves to ` +
2125
+ `relationship "${declaringEntity.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" resolves to ` +
2073
2126
  `${junction.type}.${junction.subType}, not an entity — a junction is a persisted join table ` +
2074
2127
  `and must be object.entity.`,
2075
2128
  { code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
@@ -2081,7 +2134,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
2081
2134
  if (refCount !== 2) {
2082
2135
  errors.push(
2083
2136
  new ParseError(
2084
- `relationship "${obj.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" must declare exactly two ` +
2137
+ `relationship "${declaringEntity.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" must declare exactly two ` +
2085
2138
  `identity.reference children (one per FK side); found ${refCount}.`,
2086
2139
  { code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
2087
2140
  ),
@@ -2094,7 +2147,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
2094
2147
  if (!fkFields.includes(sourceRefField as string)) {
2095
2148
  errors.push(
2096
2149
  new ParseError(
2097
- `relationship "${obj.name}.${rel.name}" @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} "${sourceRefField}" does not match ` +
2150
+ `relationship "${declaringEntity.name}.${rel.name}" @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} "${sourceRefField}" does not match ` +
2098
2151
  `any identity.reference FK field on junction "${through}". Available: ${fkFields.join(", ") || "(none)"}.`,
2099
2152
  { code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
2100
2153
  ),
@@ -2106,6 +2159,111 @@ export function validateRelationships(root: MetaData): ParseError[] {
2106
2159
  return errors;
2107
2160
  }
2108
2161
 
2162
+ // ---------------------------------------------------------------------------
2163
+ // Rule (e) — #368: a `@cardinality: one` relationship must resolve to exactly
2164
+ // one identity.reference. Two references onto the same target are
2165
+ // indistinguishable from the relationship's @objectRef alone, so the resolver
2166
+ // would silently emit the first one's FK column. ADR-0029 §5: a second path
2167
+ // is a load error naming the candidates.
2168
+ //
2169
+ // Registered alongside validateRelationships (the M:N slim-vocabulary pass,
2170
+ // above) — same deferred-resolution timing (after all files load + extends
2171
+ // resolution).
2172
+ //
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
2180
+ // change meaning depending on who inherits the relationship. Rule (e)
2181
+ // instead validates whether THIS entity's reference set resolves the
2182
+ // relationship uniquely, which is a property of the EFFECTIVE entity, not of
2183
+ // wherever the relationship happens to be declared. A child entity that
2184
+ // extends a clean parent and adds a second identity.reference onto the same
2185
+ // target makes an INHERITED relationship ambiguous on the child even though
2186
+ // the parent (and the relationship's own declaration) are untouched — own-
2187
+ // scoping this pass would leave that case unchecked, and codegen/runtime
2188
+ // (which resolve against the effective entity) would silently drop the
2189
+ // relation (#368 fix round 2). If a parent and a child are both genuinely
2190
+ // ambiguous, both are reported — two entities are broken, not one error
2191
+ // duplicated.
2192
+ // ---------------------------------------------------------------------------
2193
+
2194
+ export function validateOneSideReferenceResolution(root: MetaRoot): ParseError[] {
2195
+ const errors: ParseError[] = [];
2196
+ for (const obj of root.objects()) {
2197
+ // ADR-0039: resolving — see the scope note above: rule (e) checks THIS
2198
+ // entity's effective reference set against every relationship it can see,
2199
+ // including one only inherited via extends.
2200
+ for (const rel of obj.relationships()) {
2201
+ // ADR-0039: resolving — @cardinality/@objectRef may be inherited via extends.
2202
+ if (rel.attr(RELATIONSHIP_ATTR_CARDINALITY) !== CARDINALITY_ONE) continue;
2203
+ const objectRef = rel.attr(RELATIONSHIP_ATTR_OBJECT_REF);
2204
+ if (typeof objectRef !== "string" || objectRef === "") continue;
2205
+
2206
+ const candidates = referenceCandidatesFor(obj, objectRef);
2207
+
2208
+ const sourceRefField = rel.attr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
2209
+ const declared = typeof sourceRefField === "string" && sourceRefField !== ""
2210
+ ? sourceRefField
2211
+ : undefined;
2212
+
2213
+ if (declared !== undefined) {
2214
+ // A declared @sourceRefField short-circuits the ladder at ANY
2215
+ // candidate count — checked independently of resolveRelationshipReference,
2216
+ // whose step 1 ("exactly one candidate -> that one") would otherwise
2217
+ // silently return the lone candidate even when it disagrees with the
2218
+ // declared field. The author named a specific FK; it must exist,
2219
+ // whether there are zero, one, or many candidates.
2220
+ const matchesDeclared = candidates.some((c) => c.fields[0] === declared);
2221
+ if (matchesDeclared) continue;
2222
+ errors.push(
2223
+ new ParseError(
2224
+ `relationship "${obj.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} ` +
2225
+ `"${declared}", which names no identity.reference targeting "${objectRef}". ` +
2226
+ `Candidates: ${formatReferenceCandidates(candidates)}.`,
2227
+ { code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
2228
+ ),
2229
+ );
2230
+ continue;
2231
+ }
2232
+
2233
+ // No @sourceRefField declared: ambiguity only exists with 2+ candidates —
2234
+ // resolveRelationshipReference's name-pairing step (ladder step 3) decides.
2235
+ if (candidates.length <= 1) continue;
2236
+ const resolved = resolveRelationshipReference(obj, rel.name, objectRef);
2237
+ if (resolved) continue;
2238
+
2239
+ errors.push(
2240
+ new ParseError(
2241
+ `relationship "${obj.name}.${rel.name}" is ambiguous: "${obj.name}" declares ` +
2242
+ `${candidates.length} identity.reference nodes targeting "${objectRef}" and the ` +
2243
+ `relationship name does not pair with exactly one. Candidates: ${formatReferenceCandidates(candidates)}. ` +
2244
+ `Set @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} to the FK field this relationship navigates.`,
2245
+ { code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
2246
+ ),
2247
+ );
2248
+ }
2249
+ }
2250
+ return errors;
2251
+ }
2252
+
2253
+ /**
2254
+ * Render a candidate reference as `name(fkField)`, or `name(fieldA, fieldB)`
2255
+ * for a composite reference — so two composite references sharing a first
2256
+ * column (e.g. both starting `tenantId`) still print distinguishably.
2257
+ *
2258
+ * NOTE: this is display only. Matching (both here and in
2259
+ * resolveRelationshipReference) still keys on `fields[0]` alone — a
2260
+ * composite reference cannot actually be disambiguated by @sourceRefField.
2261
+ * That's a documented limitation, not fixed by this rendering change.
2262
+ */
2263
+ function formatReferenceCandidates(candidates: readonly MetaReferenceIdentity[]): string {
2264
+ return candidates.map((c) => `${c.name}(${c.fields.join(", ")})`).join(", ");
2265
+ }
2266
+
2109
2267
  // NOTE: identity.reference @references resolution moved to the validation registry
2110
2268
  // (defaultValidationRegistry → a declarative reference descriptor with dottedFieldPath).
2111
2269
 
@@ -945,7 +945,18 @@ function parseNodeInto(
945
945
  // "merged"` envelope. Last-writer-wins is preserved for non-conflicting
946
946
  // cases (one side unset, same value, etc.) — those carry through to the
947
947
  // existing applyInlineAttrsAndUnknownKeys logic below.
948
- if (fr5cActive && preMergeAttrSnapshot !== undefined) {
948
+ //
949
+ // FR-043 Amendment 2 — `overlay: true` LICENSES the override. The conflict error
950
+ // exists to catch two files that collided without knowing about each other; the flag
951
+ // is the author saying "I know about the other declaration and I mean to change it".
952
+ // The loader already treats the flag specially (find-or-throw versus create-or-find),
953
+ // so honouring it here makes it mean ONE thing rather than two. An unmarked
954
+ // redeclaration still merges and still errors, which is the case FR5c was written for.
955
+ if (
956
+ fr5cActive &&
957
+ preMergeAttrSnapshot !== undefined &&
958
+ nodeData[RESERVED_KEY_OVERLAY] !== true
959
+ ) {
949
960
  detectAttrMergeConflicts(
950
961
  target,
951
962
  nodeData,
@@ -1037,7 +1048,9 @@ function parseNodeInto(
1037
1048
  * non-empty value. If so, emit ERR_MERGE_CONFLICT with a `format: "merged"`
1038
1049
  * envelope naming both contributors. The merge itself proceeds (existing
1039
1050
  * last-writer-wins) so the loader sees one canonical tree; the error
1040
- * surfaces the conflict so a consumer can fix the metadata. */
1051
+ * surfaces the conflict so a consumer can fix the metadata.
1052
+ *
1053
+ * NOT called for an `overlay: true` declaration — see the call site. */
1041
1054
  function detectAttrMergeConflicts(
1042
1055
  target: MetaData,
1043
1056
  nodeData: Record<string, unknown>,
@@ -0,0 +1,21 @@
1
+ // @metaobjectsdev/metadata/vocabulary-rewrite-yaml — the YAML arm of `meta upgrade`.
2
+ //
3
+ // The implementation stays under `core/`, beside the canonical-JSON rewriter it mirrors.
4
+ // This file exists so the SUBPATH NAME mirrors the layout under `src/`, which is what
5
+ // `tsconfig.scripts.json`'s `@metaobjectsdev/metadata/*` → `src/*` mapping requires.
6
+ //
7
+ // Nesting is NOT the problem — the `*` matches across `/`, which is why
8
+ // `@metaobjectsdev/codegen-ts/templates/entity-file` resolves to
9
+ // `src/templates/entity-file.ts` perfectly well. The problem was a subpath NAMED
10
+ // `vocabulary-rewrite-yaml` whose source sat at `src/core/vocabulary-rewrite-yaml.ts`:
11
+ // the substitution yields `src/vocabulary-rewrite-yaml`, and that is not where the module
12
+ // is. Renaming the export to `./core/vocabulary-rewrite-yaml` would also have satisfied
13
+ // the rule and was the cheaper edit, but this package is published and that subpath is
14
+ // public API, so the name stays and the layout moves to meet it.
15
+ //
16
+ // When the mapping misses, tsc falls through to `node_modules` and reads `dist/` — present
17
+ // for anyone who has built, absent on a fresh CI checkout where the `gates` lane runs
18
+ // `bun install` and never builds. `./library` shipped that way and took the lane down;
19
+ // this one had not been imported from anywhere `scripts/` typechecks, so it was latent.
20
+ // `cli/test/subpath-resolves-under-scripts-paths.test.ts` now gates the class.
21
+ export { rewriteYamlDocument, type YamlRewriteResult } from "./core/vocabulary-rewrite-yaml.js";