@@ -280,16 +280,77 @@ function schemaHrefFrom(fromCategory: string): (name: string) => string | null {
280280}
281281
282282
283+ /**
284+ * Every page this run publishes: `category` -> page slug -> the schemas that
285+ * page documents.
286+ *
287+ * Grouped ONCE and read twice — by §2 below, which emits the pages, and by
288+ * `sourcePathToDocsRoute`, which has to answer "is there a page for this file?"
289+ * while §2 is still part-way through the categories. Neither of the two obvious
290+ * shortcuts can answer it: asking the sink (`wasEmitted`) makes the reply depend
291+ * on which category the walk reached first, and asking the disk makes a run's
292+ * output depend on the previous run's, so a deleted page would keep resolving
293+ * until someone regenerated twice.
294+ */
295+ function groupSchemasByPage ( ) : Map < string , Map < string , Array < { name : string ; content : any } > > > {
296+ const byCategory = new Map < string , Map < string , Array < { name : string ; content : any } > > > ( ) ;
297+
298+ for ( const category of Object . keys ( CATEGORIES ) ) {
299+ const categorySchemaDir = path . join ( SCHEMA_DIR , category ) ;
300+ if ( ! fs . existsSync ( categorySchemaDir ) ) {
301+ console . log ( `Warning: Schema directory ${ categorySchemaDir } does not exist` ) ;
302+ continue ;
303+ }
304+
305+ const pages = new Map < string , Array < { name : string ; content : any } > > ( ) ;
306+ for ( const file of fs . readdirSync ( categorySchemaDir ) . filter ( f => f . endsWith ( '.json' ) ) ) {
307+ const schemaName = file . replace ( '.json' , '' ) ;
308+ const content = JSON . parse ( fs . readFileSync ( path . join ( categorySchemaDir , file ) , 'utf-8' ) ) ;
309+ // Category-scoped: the page is owned by the file in THIS category that puts
310+ // the name on its export surface — declaration or re-export. `misc` stays
311+ // the catch-all for a published schema no `.zod.ts` here accounts for
312+ // (`security/*` declares two in plain `.ts` files), and it is honest about
313+ // it: `sourcePathFor` finds no file, so the page prints no "Source:" line.
314+ const zodFile = schemaIndex . pageFor ( category , schemaName ) || 'misc' ;
315+
316+ if ( ! pages . has ( zodFile ) ) pages . set ( zodFile , [ ] ) ;
317+ pages . get ( zodFile ) ! . push ( { name : schemaName , content } ) ;
318+ }
319+
320+ byCategory . set ( category , pages ) ;
321+ }
322+
323+ return byCategory ;
324+ }
325+
283326/**
284327 * Rewrite a source path referenced from JSDoc (`../automation/sync.zod.ts`) to
285328 * the docs route that renders it. Without this the generated page links to a
286329 * path that only exists in the repo, i.e. a 404 on the site.
330+ *
331+ * Always given a path WITH a category segment: `lib/file-description.ts`
332+ * completes a same-directory spelling from its `fromCategory` before calling in,
333+ * precisely so this stays the `<category>/<file>` lookup #4696 settled on and
334+ * never has to guess which `auth.zod.ts` an author meant.
287335 */
288336function sourcePathToDocsRoute ( target : string ) : string | null {
289337 const m = target . match ( / (?: ^ | \/ ) ( [ \w - ] + ) \/ ( [ \w . - ] + ) \. z o d \. t s $ / ) ;
290338 if ( ! m ) return null ;
291339 const [ , category , zodFile ] = m ;
292340 if ( ! CATEGORIES [ category ] ) return null ;
341+ // A real category is not yet a page. This used to be the whole test, which
342+ // was survivable only because every path the old regex could match happened
343+ // to name a file with a page behind it. #6484 widened what reaches here to
344+ // include same-directory spellings, and FOUR of the nine name a neighbour
345+ // that does not exist at all — `identity/auth`, `system/audit`,
346+ // `system/compliance`, `system/masking`, all four long since removed. Under
347+ // the old test each would have become a confident link to a 404 (measured:
348+ // deleting this line puts exactly those four dead routes into the artifact).
349+ //
350+ // File existence is not the test either: seven `.zod.ts` sources publish no
351+ // page at all, their schemas being unrepresentable in JSON Schema. The test
352+ // is whether THIS run emits the page, which is what the map knows.
353+ if ( ! PAGES_BY_CATEGORY . get ( category ) ?. has ( zodFile ) ) return null ;
293354 return `/docs/references/${ category } /${ zodFile } ` ;
294355}
295356
@@ -420,7 +481,14 @@ function generateZodFileMarkdown(zodFile: string, schemas: Array<{name: string,
420481 const sourcePath = sourceRel ? path . join ( REPO_ROOT , sourceRel ) : undefined ;
421482 let fileDesc = '' ;
422483 if ( sourcePath && fs . existsSync ( sourcePath ) ) {
423- fileDesc = renderFileDescription ( fs . readFileSync ( sourcePath , 'utf-8' ) , { sourcePathToDocsRoute } ) ;
484+ // `category` is what a path written relative to the module's own
485+ // directory is relative TO — without it the renderer cannot tell which
486+ // `auth.zod.ts` a neighbour reference means, and until #6484 it was never
487+ // told, so those references shipped as plain prose.
488+ fileDesc = renderFileDescription ( fs . readFileSync ( sourcePath , 'utf-8' ) , {
489+ fromCategory : category ,
490+ sourcePathToDocsRoute,
491+ } ) ;
424492 }
425493
426494 let md = `---\n` ;
@@ -699,6 +767,15 @@ function deadDocLinks(mdx: string): string[] {
699767
700768console . log ( 'Building documentation...' ) ;
701769
770+ /**
771+ * The page inventory, built before anything is rendered.
772+ *
773+ * It has to exist before the first `renderFileDescription` call, because that
774+ * is where `sourcePathToDocsRoute` is asked whether a referenced neighbour has
775+ * a page — an answer no partially-filled sink could give.
776+ */
777+ const PAGES_BY_CATEGORY = groupSchemasByPage ( ) ;
778+
702779/** Categories that had schemas to regenerate from — drives the flush() guard. */
703780let managedCount = 0 ;
704781
@@ -734,34 +811,11 @@ if (fs.existsSync(DOCS_ROOT)) {
734811 // But verify we don't kill the manual files.
735812}
736813
737- Object . keys ( CATEGORIES ) . forEach ( category => {
738- const categorySchemaDir = path . join ( SCHEMA_DIR , category ) ;
739-
740- if ( ! fs . existsSync ( categorySchemaDir ) ) {
741- console . log ( `Warning: Schema directory ${ categorySchemaDir } does not exist` ) ;
742- return ;
743- }
744-
745- const files = fs . readdirSync ( categorySchemaDir ) . filter ( f => f . endsWith ( '.json' ) ) ;
746- const zodFileSchemas = new Map < string , Array < { name : string , content : any } > > ( ) ;
747-
748- files . forEach ( file => {
749- const schemaName = file . replace ( '.json' , '' ) ;
750- const schemaPath = path . join ( categorySchemaDir , file ) ;
751- const content = JSON . parse ( fs . readFileSync ( schemaPath , 'utf-8' ) ) ;
752- // Category-scoped: the page is owned by the file in THIS category that puts
753- // the name on its export surface — declaration or re-export. `misc` stays
754- // the catch-all for a published schema no `.zod.ts` here accounts for
755- // (`security/*` declares two in plain `.ts` files), and it is honest about
756- // it: `sourcePathFor` finds no file, so the page prints no "Source:" line.
757- const zodFile = schemaIndex . pageFor ( category , schemaName ) || 'misc' ;
758-
759- if ( ! zodFileSchemas . has ( zodFile ) ) {
760- zodFileSchemas . set ( zodFile , [ ] ) ;
761- }
762- zodFileSchemas . get ( zodFile ) ! . push ( { name : schemaName , content } ) ;
763- } ) ;
764-
814+ // The grouping is `PAGES_BY_CATEGORY`'s, not a second one computed here: the
815+ // page a schema lands on decides both what this loop writes and what
816+ // `sourcePathToDocsRoute` calls a live route, and two enumerations of that could
817+ // disagree — the same discipline §2.6 already applies to the root index.
818+ PAGES_BY_CATEGORY . forEach ( ( zodFileSchemas , category ) => {
765819 const categoryDir = path . join ( DOCS_ROOT , category ) ;
766820
767821 // Generate file
0 commit comments