@@ -237,9 +237,10 @@ func LoadWithIncludes(fs fsys.FS, path string, extraIncludes ...string) (*City,
237237 // Inject system pack includes into Workspace.Includes. These are
238238 // appended AFTER user includes so user packs override system pack
239239 // fallbacks via the normal dedup/fallback resolution.
240- // Skip packs already reachable from user includes (avoids duplicate
241- // agent errors when a user pack transitively includes a system pack).
242- existingPacks := resolvedPackNames (root .Workspace .Includes , fs , cityRoot )
240+ // Skip packs already reachable from user includes or top-level imports
241+ // (avoids duplicate agent errors when a user pack transitively includes
242+ // a system pack).
243+ existingPacks := resolvedPackNames (root .Workspace .Includes , root .Imports , fs , cityRoot )
243244 for _ , inc := range packIncludes {
244245 name := readPackNameFromDir (inc )
245246 if name != "" && existingPacks [name ] {
@@ -831,23 +832,30 @@ func trackWorkspace(prov *Provenance, meta toml.MetaData, source string) {
831832}
832833
833834// resolvedPackNames collects pack names that are reachable from a set of
834- // include paths (including transitive includes in pack.toml). Used to
835- // skip system pack injection when a pack is already included by the user.
836- func resolvedPackNames (includes []string , sysFS fsys.FS , cityRoot string ) map [string ]bool {
837- names := make (map [string ]bool , len (includes ))
838- var visit func (ref string )
839- visit = func (ref string ) {
840- dir := resolveConfigPath (ref , cityRoot , cityRoot )
841- // Try resolving as a pack directory.
842- packPath := filepath .Join (dir , packFile )
843- data , err := sysFS .ReadFile (packPath )
835+ // top-level include paths and imports. It walks both legacy [pack].includes
836+ // and V2 [imports] transitively so builtin system-pack injection can be
837+ // skipped when a user pack already brings the same pack into the city
838+ // closure.
839+ func resolvedPackNames (includes []string , imports map [string ]Import , sysFS fsys.FS , cityRoot string ) map [string ]bool {
840+ names := make (map [string ]bool , len (includes )+ len (imports ))
841+ seenDirs := make (map [string ]bool )
842+
843+ var visit func (ref , declDir string )
844+ visit = func (ref , declDir string ) {
845+ dir , err := resolvePackRef (ref , declDir , cityRoot )
844846 if err != nil {
845- // Maybe it's a remote ref.
846- if resolved , rErr := resolvePackRef ( ref , cityRoot , cityRoot ); rErr == nil {
847- dir = resolved
848- data , err = sysFS . ReadFile ( filepath . Join ( dir , packFile ))
849- }
847+ return
848+ }
849+ absDir , absErr := filepath . Abs ( dir )
850+ if absErr != nil {
851+ absDir = dir
850852 }
853+ if seenDirs [absDir ] {
854+ return
855+ }
856+ seenDirs [absDir ] = true
857+
858+ data , err := sysFS .ReadFile (filepath .Join (dir , packFile ))
851859 if err != nil {
852860 return
853861 }
@@ -856,20 +864,26 @@ func resolvedPackNames(includes []string, sysFS fsys.FS, cityRoot string) map[st
856864 Name string `toml:"name"`
857865 Includes []string `toml:"includes"`
858866 } `toml:"pack"`
867+ Imports map [string ]Import `toml:"imports"`
859868 }
860869 if _ , decErr := toml .Decode (string (data ), & pc ); decErr != nil || pc .Pack .Name == "" {
861870 return
862871 }
863- if names [pc .Pack .Name ] {
864- return
865- }
872+
866873 names [pc .Pack .Name ] = true
867874 for _ , sub := range pc .Pack .Includes {
868- visit (resolveConfigPath (sub , dir , cityRoot ))
875+ visit (sub , dir )
876+ }
877+ for _ , imp := range pc .Imports {
878+ visit (imp .Source , dir )
869879 }
870880 }
881+
871882 for _ , inc := range includes {
872- visit (inc )
883+ visit (inc , cityRoot )
884+ }
885+ for _ , imp := range imports {
886+ visit (imp .Source , cityRoot )
873887 }
874888 return names
875889}
0 commit comments