@@ -287,42 +287,38 @@ private static JsonObject serializeBody(MetaData node, boolean effective, String
287287 body .addProperty (KEY_NAME , shortName );
288288 }
289289
290- // 2. package — emit only where introduced (differs from parent's package) .
290+ // 2. package — emit only where the TS oracle sets `model. package` .
291291 //
292292 // Special case: MetaRoot's full name IS the package (e.g. "acme::commerce").
293293 // For all other nodes, getPackage() gives the package prefix of the qualified name.
294294 //
295- // WHY "differs from parent" rather than a naive "emit if non-empty":
296- // The TS oracle uses model.package, which is the *authored* package — undefined
297- // when the author did not write a package key, so "emit if present" works there.
298- // Java's getPackage() returns the *effective* (inherited) package: every node in
299- // an "acme::commerce" tree would return "acme::commerce" even when they never
300- // authored it. Emitting unconditionally would wrongly inject a "package" key on
301- // every node. Emitting only when the value differs from the parent's resolved
302- // package approximates "authored here for the first time."
295+ // The TS oracle emits `package` iff `model.package` is set, which happens for:
296+ // (a) the root node (its package is always present);
297+ // (b) a node that explicitly authored a `package` key on its body;
298+ // (c) a MetaField whose parent is NOT a MetaObject (field-at-root /
299+ // abstract shared field) — TS's package-inheritance rule sets
300+ // `model.package` from the file context for these.
303301 //
304- // Known imprecision (Task 2 follow-up): a child that redundantly re-authors the
305- // exact same package as its parent would be suppressed here but emitted by TS.
306- // Tracking the truly-authored package requires a separate flag on MetaData that
307- // does not exist yet; that is the Task 2 work.
302+ // We must NOT use "differs from parent package" as a trigger: a single
303+ // loader builds ONE MetaRoot for a multi-file load, so children merged
304+ // from a file with a different package would spuriously emit a `package`
305+ // key that TS never emits (it threads authored-package, not effective
306+ // package). Effective package is recovered structurally on reload from
307+ // the parent context, so suppressing it here is byte-correct and
308+ // round-trips.
308309 String nodePackage = resolveNodePackage (node );
309310 if (nodePackage != null && !nodePackage .isEmpty ()) {
310- boolean differsFromParent = (parentPackage == null ) || !nodePackage .equals (parentPackage );
311- // Cross-port: abstract field-type nodes declared at root level
312- // (e.g. an abstract field.enum bound as a shared type) always emit
313- // their package, even when it equals the root's package — they are
314- // addressable library entries that need a stable qualifier so other
315- // entities can reference them via `extends`. Objects / templates /
316- // layouts at root do not need the redundant emission.
317- boolean rootAbstractFieldType = (node .getParent () instanceof MetaRoot )
318- && (node instanceof com .metaobjects .field .MetaField )
319- && getIsAbstractValue (node );
311+ boolean isRoot = node instanceof MetaRoot ;
320312 // Cross-port byte-parity: when the author explicitly wrote a
321- // `package` key on this node's body (even one that happens to
322- // equal the parent's package), round-trip it on the way out.
313+ // `package` key on this node's body, round-trip it on the way out.
323314 // CanonicalJsonParser tracks this via MetaData.isPackageAuthored().
324315 boolean explicitlyAuthored = node .isPackageAuthored ();
325- if (differsFromParent || rootAbstractFieldType || explicitlyAuthored ) {
316+ // TS package-inheritance rule (c): a MetaField NOT directly inside a
317+ // MetaObject (declared at root, or in another non-object container)
318+ // carries its own package so it stays addressable via `extends`.
319+ boolean fieldOutsideObject = (node instanceof com .metaobjects .field .MetaField )
320+ && !(node .getParent () instanceof com .metaobjects .object .MetaObject );
321+ if (isRoot || explicitlyAuthored || fieldOutsideObject ) {
326322 body .addProperty (KEY_PACKAGE , nodePackage );
327323 }
328324 }
@@ -438,47 +434,111 @@ private static List<MetaData> collectOwnStructuralChildren(MetaData node) {
438434 }
439435
440436 /**
441- * Returns structural children (own + inherited from super chain) de-duplicated by
442- * type+name, preserving own children first.
437+ * Returns the effective structural children of {@code node} (own + inherited
438+ * via the super chain), in the cross-port canonical order.
439+ *
440+ * <p>Mirrors the TS oracle's {@code _effectiveChildren} exactly: start from
441+ * the super's effective children, then for each own child either OVERRIDE
442+ * the matching (type, name) super child in place (preserving the super's
443+ * position) or APPEND it at the end when it introduces a new member. This
444+ * places inherited members first, with newly-introduced own members last —
445+ * the byte-order the {@code expected-effective.json} fixtures assert.</p>
446+ *
447+ * <p>Java's {@code getChildren(MetaData.class, true)} de-duplicates by
448+ * type+name but lists own children first, which diverges from the oracle —
449+ * hence this explicit recursive merge.</p>
443450 */
444451 private static List <MetaData > collectEffectiveStructuralChildren (MetaData node ) {
445- // getChildren(MetaData.class, true) walks the super chain but de-duplicates by type-name key.
446- List <MetaData > all = node .getChildren (MetaData .class , true );
447- List <MetaData > result = new ArrayList <>();
448- for (MetaData child : all ) {
449- if (!(child instanceof MetaAttribute )) {
450- result .add (child );
452+ return effectiveChildrenMerged (node , new java .util .IdentityHashMap <>());
453+ }
454+
455+ private static List <MetaData > effectiveChildrenMerged (MetaData node ,
456+ java .util .Map <MetaData , Boolean > visited ) {
457+ MetaData superData = node .hasSuperData () ? node .getSuperData () : null ;
458+
459+ if (superData == null || visited .containsKey (superData )) {
460+ return collectOwnStructuralChildren (node );
461+ }
462+ visited .put (superData , Boolean .TRUE );
463+
464+ // Start from the super's effective children (a fresh, mutable copy).
465+ List <MetaData > result = new ArrayList <>(effectiveChildrenMerged (superData , visited ));
466+
467+ List <MetaData > appendQueue = new ArrayList <>();
468+ for (MetaData ownChild : collectOwnStructuralChildren (node )) {
469+ int idx = -1 ;
470+ for (int i = 0 ; i < result .size (); i ++) {
471+ MetaData sc = result .get (i );
472+ if (sc .getType ().equals (ownChild .getType ())
473+ && canonicalMatchName (sc ).equals (canonicalMatchName (ownChild ))) {
474+ idx = i ;
475+ break ;
476+ }
477+ }
478+ if (idx != -1 ) {
479+ result .set (idx , ownChild ); // in-place override, super's position kept
480+ } else {
481+ appendQueue .add (ownChild );
451482 }
452483 }
484+ result .addAll (appendQueue );
453485 return result ;
454486 }
455487
488+ /**
489+ * The name used for cross-port effective-override matching. Mirrors the TS
490+ * oracle, whose auto-named nodes carry {@code name === ""}: an auto-generated
491+ * name (e.g. a {@code source.rdb}'s {@code rdb1}) collapses to the empty
492+ * string so an own auto-named node overrides its inherited counterpart at the
493+ * super's position — exactly as TS matches {@code (type, "")} against
494+ * {@code (type, "")}. Java assigns real {@code rdbN} auto-names (which the
495+ * serializer suppresses), so without this collapse the per-package counter
496+ * would make Product's {@code rdb1} and ProductSummary's {@code rdb2} fail to
497+ * match and the inherited source would wrongly duplicate.
498+ */
499+ private static String canonicalMatchName (MetaData node ) {
500+ if (isAutoGeneratedName (node )) {
501+ return "" ;
502+ }
503+ String s = node .getShortName ();
504+ return s == null ? "" : s ;
505+ }
506+
456507 // ---------------------------------------------------------------------------
457508 // Helpers — package resolution
458509 // ---------------------------------------------------------------------------
459510
460511 /**
461512 * Returns the "authoring package" of a node — the package context an author
462- * would use to write its {@code extends} ref. For root-level nodes this is
463- * the root's own package; for child nodes it walks up to the nearest
464- * MetaObject (entity / value) and returns that ancestor's package. Used by
465- * extends-ref serialization so a field inside an entity in package X
466- * references a sibling in X by short name (not by FQN).
513+ * would use to write its {@code extends} ref. For a node that is itself a
514+ * top-level type (a child of the MetaRoot — an object / template / abstract
515+ * field), this is the node's OWN package. For a node nested inside an entity
516+ * it walks up to the nearest MetaObject and returns that ancestor's package.
517+ * Used by extends-ref serialization so a sibling in the same package is
518+ * referenced by short name (not by FQN).
519+ *
520+ * <p>Cross-port correctness for the flattened multi-file case: a single
521+ * loader builds ONE MetaRoot whose name is just the first input file's
522+ * package. The other files contribute children in OTHER packages. The
523+ * root's own package is therefore NOT the authoring context for those
524+ * children — each node's own {@link MetaData#getPackage()} (the effective
525+ * package of its FQN) is. So we never fall back to the root's package; we
526+ * fall back to the node's own package.</p>
467527 */
468528 private static String authoringPackageFor (MetaData node ) {
469529 MetaData current = node ;
470530 while (current != null ) {
471- if (current instanceof MetaRoot ) {
472- return resolveNodePackage (current );
473- }
474531 MetaData parent = current .getParent ();
475- if (parent == null ) break ;
532+ if (parent == null || parent instanceof MetaRoot ) {
533+ // `current` is a top-level type — its own package IS the
534+ // authoring context (not the shared root's package).
535+ return current .getPackage ();
536+ }
476537 if (parent instanceof com .metaobjects .object .MetaObject ) {
477538 // current is a child of an entity → authoring context = entity's package
478539 return parent .getPackage ();
479540 }
480- // Non-MetaObject parent (or MetaRoot): keep walking up; MetaRoot is
481- // caught on the next iteration's top-of-loop check.
541+ // Non-MetaObject parent: keep walking up.
482542 current = parent ;
483543 }
484544 return node .getPackage ();
0 commit comments