@metaobjectsdev/metadata 1.0.5-rc.3 → 1.0.5-rc.6

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.
@@ -134,6 +134,10 @@ import {
134
134
  resolveRelationshipReference,
135
135
  } from "../core/relationship/resolve-relationship-reference.js";
136
136
  import type { MetaRoot } from "../shared/meta-root.js";
137
+ import type { MetaRelationship } from "../core/relationship/meta-relationship.js";
138
+ // Rule (f) runs the REAL M:N derivation so the loader and codegen cannot drift.
139
+ import { deriveM2MFields, M2MDerivationError } from "../core/relationship/derive-m2m-fields.js";
140
+ import { isReadOnlySource, isWritableSource } from "../shared/node-guards.js";
137
141
  import { stripPackage } from "../naming.js";
138
142
  import {
139
143
  FILTER_COMPOSE_OR,
@@ -2250,6 +2254,115 @@ export function validateOneSideReferenceResolution(root: MetaRoot): ParseError[]
2250
2254
  return errors;
2251
2255
  }
2252
2256
 
2257
+ // ---------------------------------------------------------------------------
2258
+ // Rule (f) — a M:N junction must PAIR: one identity.reference resolving to the
2259
+ // navigating entity, one to the @objectRef target.
2260
+ //
2261
+ // Rule (d) already checks that the junction declares exactly TWO references. It
2262
+ // never checked WHAT they point at, so a junction whose two references name some
2263
+ // other pair of entities loaded perfectly clean and then failed — differently —
2264
+ // in every port's codegen: TypeScript and C# warned and emitted no traversal
2265
+ // route (the endpoint simply 404s, with nothing in the build output a CI gate
2266
+ // would fail on), while Java, Kotlin and Python threw and failed the build. One
2267
+ // input, five contracts, and the quiet arm is the dangerous one.
2268
+ //
2269
+ // The check belongs HERE because the model is what is wrong, and because a
2270
+ // loader error is the only answer every port already agrees on. Both codegen
2271
+ // branches become unreachable for a model that loaded.
2272
+ //
2273
+ // It runs the REAL derivation (deriveM2MFields) rather than reimplementing the
2274
+ // pairing, so the loader and the FK derivation cannot drift apart — the same
2275
+ // reason _junctionReferences above shares referenceIdentities() with codegen. A
2276
+ // parallel check would be a second opinion, and the defect class being closed
2277
+ // here is precisely "two parts of the pipeline disagree".
2278
+ //
2279
+ // SCOPE mirrors codegen's own iteration exactly (relation-resolver.ts): every
2280
+ // CONCRETE, non-projection object crossed with its EFFECTIVE relationships.
2281
+ // That is not incidental —
2282
+ // * per (entity, relationship), NOT deduped by declaration like rule (d):
2283
+ // pairing is a property of the NAVIGATING entity. deriveM2MFields accepts a
2284
+ // junction reference naming either the declaring entity or the navigating
2285
+ // one, so an inherited M:N can pair from one subtype and not another, and
2286
+ // checking the declaration alone would both miss that and reject models
2287
+ // codegen accepts.
2288
+ // * abstract levels are skipped because their own declarations do not file —
2289
+ // an abstract base's FK reaches a table only through a concrete descendant,
2290
+ // whose own walk reaches this same relationship.
2291
+ // * projections are skipped because they never emit a relations() block.
2292
+ // Widening past that set would fail builds codegen never had a problem with.
2293
+ // ---------------------------------------------------------------------------
2294
+
2295
+ /** codegen-ts's isProjection, reproduced here: that module is not reachable from
2296
+ * the loader, and the two source guards it is built from live in this package.
2297
+ * ADR-0039: own — projection-ness is the entity's OWN declared source @kind. */
2298
+ function isProjectionObject(obj: MetaObject): boolean {
2299
+ const own = obj.ownChildren();
2300
+ return own.some(isReadOnlySource) && !own.some(isWritableSource);
2301
+ }
2302
+
2303
+ export function validateM2MJunctionPairing(root: MetaRoot): ParseError[] {
2304
+ const errors: ParseError[] = [];
2305
+ for (const obj of root.objects()) {
2306
+ if (obj.isAbstract === true) continue;
2307
+ if (isProjectionObject(obj)) continue;
2308
+ // ADR-0039: resolving — an M:N relationship may be reached only via extends.
2309
+ for (const rel of obj.relationships()) {
2310
+ if (rel.attr(RELATIONSHIP_ATTR_CARDINALITY) !== CARDINALITY_MANY) continue;
2311
+ if (rel.attr(RELATIONSHIP_ATTR_THROUGH) === undefined) continue;
2312
+ try {
2313
+ deriveM2MFields(rel, obj, root);
2314
+ } catch (err) {
2315
+ if (!(err instanceof M2MDerivationError)) throw err;
2316
+ // Rule (d) reports the malformed-junction cases it owns (missing
2317
+ // @through, unresolvable @through, a reference count other than two, an
2318
+ // @sourceRefField matching no FK) against the DECLARING entity, and the
2319
+ // derivation re-checks them. Reporting them again here — once per
2320
+ // concrete subtype — would bury the pairing finding under duplicates of
2321
+ // a diagnosis the author already has, so those are left to rule (d) and
2322
+ // this rule speaks only where it is the sole witness.
2323
+ if (isJunctionShapeErrorOwnedByRuleD(rel, root)) continue;
2324
+ errors.push(
2325
+ new ParseError(
2326
+ `relationship "${obj.name}.${rel.name}" cannot be resolved through its ` +
2327
+ `@${RELATIONSHIP_ATTR_THROUGH} junction: ${err.message}. Without a reference to ` +
2328
+ `"${obj.name}" the junction rows cannot be filtered to this entity, so no ` +
2329
+ `traversal is derivable.`,
2330
+ { code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
2331
+ ),
2332
+ );
2333
+ }
2334
+ }
2335
+ }
2336
+ return errors;
2337
+ }
2338
+
2339
+ /**
2340
+ * True when the derivation's failure is one rule (d) already reports — a
2341
+ * junction that is missing, unresolvable, or does not declare exactly two
2342
+ * identity.reference children. Keyed on the SHAPE rather than on the message
2343
+ * text: matching strings would make this silently stop de-duplicating the first
2344
+ * time a message is reworded, and a gate keyed to prose breaks on the rewrite.
2345
+ */
2346
+ function isJunctionShapeErrorOwnedByRuleD(rel: MetaRelationship, root: MetaRoot): boolean {
2347
+ const through = rel.attr(RELATIONSHIP_ATTR_THROUGH);
2348
+ if (typeof through !== "string" || through === "") return true;
2349
+ // Resolve exactly as rule (d) and deriveM2MFields do — ADR-0042 via the single
2350
+ // resolveObjectRef matcher, referred from the DECLARING entity's package. A
2351
+ // second resolution rule here is what made this helper disagree with the
2352
+ // derivation and emit rule (d)'s finding a second time.
2353
+ const declaring = rel.parent;
2354
+ const referrerPkg = declaring?.package ?? declaring?.fileDefaultPackage ?? "";
2355
+ const junction = _findObject(root, through, referrerPkg) as MetaObject | undefined;
2356
+ if (junction === undefined) return true;
2357
+ if (junction.subType !== OBJECT_SUBTYPE_ENTITY) return true;
2358
+ if (_countJunctionReferences(junction) !== 2) return true;
2359
+ const sourceRefField = rel.attr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
2360
+ if (typeof sourceRefField === "string" && sourceRefField !== "") {
2361
+ if (!_junctionReferenceFkFields(junction).includes(sourceRefField)) return true;
2362
+ }
2363
+ return false;
2364
+ }
2365
+
2253
2366
  /**
2254
2367
  * Render a candidate reference as `name(fkField)`, or `name(fieldA, fieldB)`
2255
2368
  * for a composite reference — so two composite references sharing a first