@@ -88,7 +88,7 @@ static int RunGen(string[] rest)
8888
8989 // Rung 1 (explicit positional) is honored as-is; an omitted metadataDir
9090 // falls back to the port-neutral .metaobjects/config.json ladder.
91- metadataDir = ResolveMetadataDirOrExit ( metadataDir ) ;
91+ var resolvedMeta = ResolveMetadataDirOrExit ( metadataDir ) ;
9292
9393 // Advisory: nudge a re-scaffold if the copied-in agent context predates this build.
9494 // Never throws, never changes the exit code (a missing/corrupt manifest is ignored).
@@ -97,7 +97,16 @@ static int RunGen(string[] rest)
9797 var generatorNames = generatorsCsv
9898 ? . Split ( ',' , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ;
9999
100- var outcome = GenCommand . Run ( metadataDir , outDir , ns , emitAbstractShapes , generatorNames , templateRoot , templateSpecPath ) ;
100+ // A ladder-resolved (non-null Files) source loads via the already-resolved,
101+ // `_pending`-excluded file list (MetaDataLoader.FromUris) — never a second
102+ // FromDirectory walk of resolvedMeta.Directory, which would both duplicate
103+ // the walk ResolveMetadataDirOrExit already did AND silently include `_pending`.
104+ var outcome = resolvedMeta . Files is { } files
105+ ? GenCommand . Run (
106+ MetaObjects . Loader . MetaDataLoader . FromUris ( files . Select ( f => new Uri ( f ) ) . ToList ( ) ) ,
107+ outDir , ns , emitAbstractShapes , generatorNames , templateRoot , templateSpecPath )
108+ : GenCommand . Run (
109+ resolvedMeta . Directory , outDir , ns , emitAbstractShapes , generatorNames , templateRoot , templateSpecPath ) ;
101110 if ( ! outcome . Ok )
102111 {
103112 foreach ( var e in outcome . LoadErrors ) Console . Error . WriteLine ( $ " load error: { e } ") ;
@@ -141,13 +150,20 @@ static int RunDocs(string[] rest)
141150
142151 // Rung 1 (explicit positional) is honored as-is; an omitted metadataDir
143152 // falls back to the port-neutral .metaobjects/config.json ladder.
144- metadataDir = ResolveMetadataDirOrExit ( metadataDir ) ;
153+ var resolvedMeta = ResolveMetadataDirOrExit ( metadataDir ) ;
145154
146155 // Default the project label to the input directory's leaf name (cosmetic — surfaces
147156 // in the AGENT-API header). Trailing-separator-safe.
148- project ??= new DirectoryInfo ( Path . TrimEndingDirectorySeparator ( Path . GetFullPath ( metadataDir ) ) ) . Name ;
149-
150- var outcome = DocsCommand . Run ( metadataDir , outDir , project , ns , modelBaseUrl : modelBaseUrl ) ;
157+ project ??= new DirectoryInfo ( Path . TrimEndingDirectorySeparator ( Path . GetFullPath ( resolvedMeta . Directory ) ) ) . Name ;
158+
159+ // See the identical comment in RunGen above: a ladder-resolved source loads
160+ // via its already-resolved, `_pending`-excluded file list, never a second
161+ // (unfiltered) directory walk.
162+ var outcome = resolvedMeta . Files is { } files
163+ ? DocsCommand . Run (
164+ MetaObjects . Loader . MetaDataLoader . FromUris ( files . Select ( f => new Uri ( f ) ) . ToList ( ) ) ,
165+ outDir , project , ns , modelBaseUrl : modelBaseUrl )
166+ : DocsCommand . Run ( resolvedMeta . Directory , outDir , project , ns , modelBaseUrl : modelBaseUrl ) ;
151167 if ( ! outcome . Ok )
152168 {
153169 foreach ( var e in outcome . LoadErrors ) Console . Error . WriteLine ( $ " load error: { e } ") ;
@@ -174,13 +190,27 @@ static int RunDocs(string[] rest)
174190// docs, verify) so an omitted positional argument is never a hard requirement
175191// wherever a project's config can name the location instead.
176192//
177- // Never returns null: either hands back a real directory, or prints a
178- // diagnostic and terminates the process — callers may treat the result as
179- // always-present and keep their existing (now-unreachable-when-omitted)
180- // null checks for the OTHER positional/option they still require.
181- static string ResolveMetadataDirOrExit ( string ? metadataDir )
193+ // The metadata-location ladder's result: always a directory (explicit-arg
194+ // back-compat, and cosmetic labeling even on the ladder path), and — when
195+ // resolution went through the .metaobjects/config.json ladder rather than an
196+ // explicit CLI argument — the ladder's OWN already-resolved, `_pending`-draft-
197+ // excluded file list too. A caller with a non-null Files must load via
198+ // MetaDataLoader.FromUris(Files) rather than FromDirectory(Directory): the
199+ // latter would both re-walk a tree this function already walked once (via
200+ // SourceResolver) AND silently lose the `_pending` exclusion, since
201+ // DirectorySource.Options.ExcludePending defaults to false at the loader
202+ // level (SourceResolver is the one place that turns it on). Declared at file
203+ // scope below the entry point (top-level-statement files require type
204+ // declarations to follow every top-level statement / local function).
205+
206+ // Never exits without a usable result: either hands back a real directory
207+ // (+ file list, when ladder-resolved), or prints a diagnostic and terminates
208+ // the process — callers may treat the result as always-present and keep
209+ // their existing (now-unreachable-when-omitted) null checks for the OTHER
210+ // positional/option they still require.
211+ static ResolvedMetadata ResolveMetadataDirOrExit ( string ? metadataDir )
182212{
183- if ( metadataDir is not null ) return metadataDir ;
213+ if ( metadataDir is not null ) return new ResolvedMetadata ( metadataDir , null ) ;
184214
185215 var cwd = Directory . GetCurrentDirectory ( ) ;
186216 try
@@ -190,11 +220,13 @@ static string ResolveMetadataDirOrExit(string? metadataDir)
190220
191221 if ( specs . Count == 0 )
192222 {
193- // No declared sources — validate + apply the DEFAULT directory through
223+ // No declared sources — resolve + apply the DEFAULT directory through
194224 // the same ladder the shared conformance corpus gates (raises
195- // ERR_COLLECTION_NOT_FOUND when the default is also absent).
196- _ = MetaObjects . Config . SourceResolver . ResolveCollection ( cwd ) ;
197- return Path . Combine ( cwd , MetaObjects . Config . NeutralConfig . DefaultMetadataDir ) ;
225+ // ERR_COLLECTION_NOT_FOUND when the default is also absent). The
226+ // returned file list IS the load — no second walk needed.
227+ var defaultFiles = MetaObjects . Config . SourceResolver . ResolveCollection ( cwd ) ;
228+ return new ResolvedMetadata (
229+ Path . Combine ( cwd , MetaObjects . Config . NeutralConfig . DefaultMetadataDir ) , defaultFiles ) ;
198230 }
199231
200232 if ( specs . Count > 1 )
@@ -213,9 +245,9 @@ static string ResolveMetadataDirOrExit(string? metadataDir)
213245
214246 // Exactly one declared source. Resolve + validate it through the same
215247 // kind/existence checks ResolveSources applies (ERR_SOURCE_KIND_UNSUPPORTED /
216- // ERR_SOURCE_UNRESOLVED), then hand the loader that spec's OWN root — never
217- // the default directory name, which this project may not even have .
218- MetaObjects . Config . SourceResolver . ResolveSources ( cwd , specs ) ;
248+ // ERR_SOURCE_UNRESOLVED) — its return value IS the (already `_pending`-
249+ // excluded) file list to load, not just a validation signal to discard .
250+ var files = MetaObjects . Config . SourceResolver . ResolveSources ( cwd , specs ) ;
219251 var rawPath = specs [ 0 ] [ "path" ] ; // guaranteed present: ResolveSources above
220252 // would already have thrown otherwise.
221253 var resolved = Path . IsPathRooted ( rawPath ) ? rawPath : Path . GetFullPath ( Path . Combine ( cwd , rawPath ) ) ;
@@ -235,7 +267,7 @@ static string ResolveMetadataDirOrExit(string? metadataDir)
235267 throw new InvalidOperationException ( "unreachable" ) ;
236268 }
237269
238- return resolved ;
270+ return new ResolvedMetadata ( resolved , files ) ;
239271 }
240272 catch ( MetaObjects . MetaModelException e )
241273 {
@@ -306,7 +338,7 @@ static int RunVerify(string[] rest)
306338
307339 // Rung 1 (explicit positional) is honored as-is; an omitted metadataDir
308340 // falls back to the port-neutral .metaobjects/config.json ladder.
309- metadataDir = ResolveMetadataDirOrExit ( metadataDir ) ;
341+ var resolvedMeta = ResolveMetadataDirOrExit ( metadataDir ) ;
310342
311343 // The templates gate needs a root. Bare verify (defaults to templates) and an
312344 // explicit --templates both require it; surface a clear usage error if absent.
@@ -328,7 +360,11 @@ static int RunVerify(string[] rest)
328360
329361 var opts = new VerifyCommand . Options
330362 {
331- MetadataDir = metadataDir ,
363+ MetadataDir = resolvedMeta . Directory ,
364+ // A ladder-resolved source loads via this already-resolved,
365+ // `_pending`-excluded file list (see VerifyCommand.LoadMetadata) — never a
366+ // second (unfiltered) directory walk of MetadataDir.
367+ MetadataFiles = resolvedMeta . Files ,
332368 TemplatesRoot = templatesRoot ,
333369 OutDir = outDir ,
334370 Namespace = ns ,
@@ -390,3 +426,6 @@ static int RunVerify(string[] rest)
390426
391427 return result . ExitCode ;
392428}
429+
430+ // See the doc comment on ResolveMetadataDirOrExit above.
431+ readonly record struct ResolvedMetadata ( string Directory , IReadOnlyList < string > ? Files ) ;
0 commit comments