@@ -42,7 +42,7 @@ import {
4242 FIELD_ATTR_COLUMN ,
4343 OBJECT_PROJECTION_ATTR_FILTER ,
4444 findReferenceBetween ,
45- stripPackage ,
45+ resolveObjectRef ,
4646 type AggregateFunction ,
4747} from "@metaobjectsdev/metadata" ;
4848import { type MetaData , type MetaField , type MetaRoot , MetaObject } from "@metaobjectsdev/metadata" ;
@@ -290,11 +290,34 @@ export function projectionViewName(
290290export function refNamedOwner ( node : MetaData , root : MetaRoot ) : MetaObject | undefined {
291291 const ref = ( node as { superRef ?: string } ) . superRef ;
292292 if ( ref === undefined ) return undefined ;
293- const lastSep = ref . lastIndexOf ( "::" ) ;
294- const tail = lastSep === - 1 ? ref : ref . slice ( lastSep + 2 ) ;
295- const dot = tail . indexOf ( "." ) ;
293+ // The entity portion is everything before the FIRST "." (packages use "::", the
294+ // member path uses ".").
295+ const dot = ref . indexOf ( "." ) ;
296296 if ( dot <= 0 ) return undefined ;
297- return root . findObject ( tail . slice ( 0 , dot ) ) ?? undefined ;
297+ const entityRef = ref . slice ( 0 , dot ) ;
298+ // An FQN `extends` binds the EXACT package's entity (#244); a bare ref keeps the prior
299+ // findObject behavior (a projection extends its base by bare name in the same package).
300+ if ( entityRef . includes ( "::" ) ) return resolveEntityRef ( root , entityRef , "" ) ;
301+ return root . findObject ( entityRef ) ?? undefined ;
302+ }
303+
304+ /** Effective package of an object, taken from its resolution key ("<pkg>::<Name>"). */
305+ function packageOf ( obj : MetaData ) : string {
306+ const key = obj . resolutionKey ( ) ;
307+ const i = key . lastIndexOf ( "::" ) ;
308+ return i >= 0 ? key . slice ( 0 , i ) : "" ;
309+ }
310+
311+ /**
312+ * Resolve an object reference (bare or fully-qualified) to its MetaObject, package-aware
313+ * per the ADR-0042 contract (`resolveObjectRef`): an FQN binds exactly, a bare ref binds
314+ * the referrer's package then a root-level object. Replaces the load-order-dependent
315+ * `root.findObject(stripPackage(ref))` the view-spec used to do, which discarded the
316+ * package qualifier and let a same-bare-named entity in another package win (#244).
317+ */
318+ function resolveEntityRef ( root : MetaRoot , ref : string , referrerPkg : string ) : MetaObject | undefined {
319+ const node = resolveObjectRef ( root , ref , referrerPkg ) . node ;
320+ return node instanceof MetaObject ? node : undefined ;
298321}
299322
300323function baseEntityFor (
@@ -370,20 +393,20 @@ function primaryKeyColumn(entity: MetaObject, ctx: ExtractContext): string | und
370393 * `@of` to name the aggregated entity, so it is derived from the last `@via` hop.
371394 * Returns undefined if any hop fails to resolve (a prior loader error already fired).
372395 */
373- function viaTerminalEntity ( via : string , root : MetaRoot ) : string | undefined {
396+ function viaTerminalEntity ( via : string , root : MetaRoot , referrerPkg : string ) : string | undefined {
374397 const segments = via . split ( "." ) ;
375398 const rawEntity = segments [ 0 ] ;
376399 if ( ! rawEntity ) return undefined ;
377- let currentObj = root . findObject ( stripPackage ( rawEntity ) ) ;
400+ let currentObj = resolveEntityRef ( root , rawEntity , referrerPkg ) ;
378401 if ( ! currentObj ) return undefined ;
379- let terminal = currentObj . name ;
402+ let terminal = currentObj . resolutionKey ( ) ;
380403 for ( const relName of segments . slice ( 1 ) ) {
381404 const resolved = resolveHop ( currentObj , relName ) ;
382405 if ( ! resolved ) return undefined ;
383- const target = root . findObject ( stripPackage ( resolved . targetName ) ) ;
406+ const target = resolveEntityRef ( root , resolved . targetName , packageOf ( currentObj ) ) ;
384407 if ( ! target ) return undefined ;
385408 currentObj = target ;
386- terminal = target . name ;
409+ terminal = target . resolutionKey ( ) ;
387410 }
388411 return terminal ;
389412}
@@ -474,7 +497,11 @@ function resolveExprNode(
474497}
475498
476499function shortAliasFor ( entityName : string , used : Set < string > ) : string {
477- const base = ( entityName [ 0 ] ?? "x" ) . toLowerCase ( ) ;
500+ // Derive from the SHORT name — an entity ref may now be a resolutionKey ("pkg::Name",
501+ // #244); the alias must stay the first letter of the entity, so existing single-package
502+ // view SQL is byte-identical (a changed alias would churn `verify --db` fingerprints).
503+ const short = entityName . includes ( "::" ) ? entityName . slice ( entityName . lastIndexOf ( "::" ) + 2 ) : entityName ;
504+ const base = ( short [ 0 ] ?? "x" ) . toLowerCase ( ) ;
478505 if ( ! used . has ( base ) ) { used . add ( base ) ; return base ; }
479506 let i = 0 ;
480507 let candidate : string ;
@@ -517,6 +544,9 @@ function buildJoinTree(
517544 ctx : ExtractContext ,
518545) : JoinTree {
519546 const allPaths : Path [ ] = [ ] ;
547+ // Referrer package for resolving a bare @via head: the origins are authored on the
548+ // projection, so bare refs bind package-locally to it (FQN refs ignore this).
549+ const projPkg = packageOf ( projection ) ;
520550
521551 // ADR-0039: own — a projection's DECLARED field set IS the exposure (FR-024/
522552 // ADR-0028, inclusive + fail-closed); iterate the projection's own fields and
@@ -546,10 +576,10 @@ function buildJoinTree(
546576 const rawEntity = segments [ 0 ] ;
547577 const relSegments = segments . slice ( 1 ) ;
548578 if ( ! rawEntity ) continue ;
549- // @via may be package-qualified ("pkg::Entity.rel"); the joinTree + findObject
550- // key on bare names ( so they line up with bare passthrough @from lookups).
551- const entityName = stripPackage ( rawEntity ) ;
552- let currentObj = root . findObject ( entityName ) ;
579+ // @via may be package-qualified ("pkg::Entity.rel"). Resolve package-aware and key
580+ // the joinTree on resolutionKey() (FQN) so a same- bare-named entity in another
581+ // package can't win — the passthrough @from lookups key on the same FQN (#244).
582+ let currentObj = resolveEntityRef ( root , rawEntity , projPkg ) ;
553583 if ( ! currentObj ) continue ;
554584
555585 const path : Path = [ ] ;
@@ -560,11 +590,10 @@ function buildJoinTree(
560590 const resolved = resolveHop ( currentObj , relName ) ;
561591 if ( ! resolved ) break ;
562592 const { targetName, cardinality } = resolved ;
563- // @objectRef /@references may be package-qualified ("pkg::Entity") — the directory
564- // loader qualifies a same-package ref even when authored bare — but findObject
565- // keys on the BARE name (like the @via/@of/@from paths above). Strip first, or
566- // the join (and every aggregate traversing it) is silently dropped.
567- const target = root . findObject ( stripPackage ( targetName ) ) ;
593+ // @objectRef /@references may be package-qualified ("pkg::Entity"); resolve it
594+ // package-aware relative to the hop's source entity (the loader qualifies a
595+ // same-package ref even when authored bare), so the join binds the exact target.
596+ const target = resolveEntityRef ( root , targetName , packageOf ( currentObj ) ) ;
568597 if ( ! target ) break ;
569598
570599 const ref = findReferenceBetween ( currentObj as MetaObject , target ) ;
@@ -608,7 +637,7 @@ function buildJoinTree(
608637 pkColumn : joinColumnFor ( pkHolder , resolvedPkField , ctx ) ,
609638 referenceHolder,
610639 joinType,
611- targetEntity : stripPackage ( targetName ) ,
640+ targetEntity : target . resolutionKey ( ) ,
612641 } ) ;
613642 currentObj = target ;
614643 }
@@ -646,7 +675,7 @@ function buildJoinTree(
646675 }
647676
648677 return {
649- baseEntity : base . name ,
678+ baseEntity : base . resolutionKey ( ) ,
650679 baseAlias,
651680 joins : Array . from ( trieRoot . children . values ( ) ) . map ( toJoinNode ) ,
652681 } ;
@@ -683,6 +712,8 @@ function buildSelectSpec(
683712 usedAliases : Set < string > ,
684713) : SelectSpec {
685714 const columns : SelectColumn [ ] = [ ] ;
715+ // Referrer package for resolving bare @from/@of/@via entity refs (FQN refs ignore it).
716+ const projPkg = packageOf ( projection ) ;
686717
687718 // FR-024 (ADR-0028): the projection's DECLARED field set IS the exposure —
688719 // the inclusive list, fail-closed by construction. The pre-FR-024 loop that
@@ -721,13 +752,13 @@ function buildSelectSpec(
721752 const from = origin . ownAttr ( ORIGIN_PASSTHROUGH_ATTR_FROM ) as string ;
722753 const dotIdx = from . indexOf ( "." ) ;
723754 if ( dotIdx < 1 ) continue ;
724- // @from is package-qualified ("pkg::Entity.field"); the joinTree + findObject
725- // key on the BARE entity name, so strip the package before resolving.
726- const entityName = stripPackage ( from . slice ( 0 , dotIdx ) ) ;
755+ // @from may be package-qualified ("pkg::Entity.field"). Resolve package-aware and
756+ // key the alias lookup on the SAME resolutionKey() the joinTree stored (#244).
727757 const fieldName = from . slice ( dotIdx + 1 ) ;
728- const targetEntity = root . findObject ( entityName ) ;
729- const sourceAlias = findAliasInTree ( joinTree , entityName ) ;
730- if ( ! targetEntity || sourceAlias === undefined ) continue ;
758+ const targetEntity = resolveEntityRef ( root , from . slice ( 0 , dotIdx ) , projPkg ) ;
759+ if ( ! targetEntity ) continue ;
760+ const sourceAlias = findAliasInTree ( joinTree , targetEntity . resolutionKey ( ) ) ;
761+ if ( sourceAlias === undefined ) continue ;
731762 // EFFECTIVE fields — the source column may be inherited via `extends`
732763 // (e.g. created_at/updated_at/created_by from an audited base), which
733764 // ownChildren() would miss.
@@ -751,9 +782,9 @@ function buildSelectSpec(
751782 // hop, and @filter is the (required) quantified predicate.
752783 const via = origin . ownAttr ( ORIGIN_AGGREGATE_ATTR_VIA ) as string | undefined ;
753784 if ( ! via ) continue ;
754- const relatedName = viaTerminalEntity ( via , root ) ;
785+ const relatedName = viaTerminalEntity ( via , root , projPkg ) ;
755786 if ( ! relatedName ) continue ;
756- const relatedEntity = root . findObject ( relatedName ) ;
787+ const relatedEntity = resolveEntityRef ( root , relatedName , projPkg ) ;
757788 const sourceAlias = findAliasInTree ( joinTree , relatedName ) ;
758789 if ( ! relatedEntity || sourceAlias === undefined ) continue ;
759790 const joinedPk = primaryKeyColumn ( relatedEntity , ctx ) ;
@@ -779,11 +810,11 @@ function buildSelectSpec(
779810 if ( ! of_ ) continue ;
780811 const dotIdx = of_ . indexOf ( "." ) ;
781812 if ( dotIdx < 1 ) continue ;
782- const entityName = stripPackage ( of_ . slice ( 0 , dotIdx ) ) ;
783813 const fieldName = of_ . slice ( dotIdx + 1 ) ;
784- const targetEntity = root . findObject ( entityName ) ;
785- const sourceAlias = findAliasInTree ( joinTree , entityName ) ;
786- if ( ! targetEntity || sourceAlias === undefined ) continue ;
814+ const targetEntity = resolveEntityRef ( root , of_ . slice ( 0 , dotIdx ) , projPkg ) ;
815+ if ( ! targetEntity ) continue ;
816+ const sourceAlias = findAliasInTree ( joinTree , targetEntity . resolutionKey ( ) ) ;
817+ if ( sourceAlias === undefined ) continue ;
787818 const targetField = targetEntity . fields ( ) . find ( ( f ) => f . name === fieldName ) ;
788819 if ( ! targetField ) continue ;
789820
@@ -837,9 +868,8 @@ function buildSelectSpec(
837868 if ( ! of_ ) continue ;
838869 const dotIdx = of_ . indexOf ( "." ) ;
839870 if ( dotIdx < 1 ) continue ;
840- const childName = stripPackage ( of_ . slice ( 0 , dotIdx ) ) ;
841871 const ofFieldName = of_ . slice ( dotIdx + 1 ) ;
842- const childEntity = root . findObject ( childName ) ;
872+ const childEntity = resolveEntityRef ( root , of_ . slice ( 0 , dotIdx ) , projPkg ) ;
843873 if ( ! childEntity ) continue ;
844874 const ofField = childEntity . fields ( ) . find ( ( f ) => f . name === ofFieldName ) ;
845875 if ( ! ofField ) continue ;
@@ -867,9 +897,10 @@ function buildSelectSpec(
867897 : undefined ;
868898 columns . push ( {
869899 kind : "first" ,
900+ // FQN so it lines up with the resolutionKey()-keyed joinTables in build-projection-views (#244).
870901 fieldName : field . name ,
871902 dbColAlias : dbCol ,
872- childEntity : childEntity . name ,
903+ childEntity : childEntity . resolutionKey ( ) ,
873904 childAlias,
874905 sourceColumn : sourceColumnNameFor ( ofField , ctx ) ,
875906 referenceHolder,
0 commit comments