@@ -442,13 +442,11 @@ describe('renderFileDescription — #6136: the bare-path rewriter skips formed l
442442 // skipping paths, and a rewriter that stopped doing its job would pass the
443443 // two cases above for the wrong reason.
444444 //
445- // Spelled WITHOUT a `../` prefix on purpose. A bare path that carries one
446- // is mis-linked by a defect this PR does not touch — the rewriter's leading
447- // `\b` cannot match at the `.` of `../`, so the prefix is left outside the
448- // link (`../../[system/cache.zod.ts](route)`, live on `api/http-cache` and
449- // `system/cache`). That is a different input shape from #6136 (no `{@link }`
450- // is involved) and it is filed separately; asserting the broken spelling
451- // here would ratify it, so this case steers around it the way #5059's did.
445+ // Spelled WITHOUT a `../` prefix on purpose: when this was written the
446+ // prefixed spelling was mis-linked by a defect #6136 did not touch, and
447+ // asserting the broken output here would have ratified it, so the case
448+ // steered around it the way #5059's did. #6229 has since fixed it — the
449+ // prefixed spellings are pinned in their own block below, correctly.
452450 const source = [
453451 '/**' ,
454452 ' * The connector lives in integration/connector.zod.ts today.' ,
@@ -463,6 +461,98 @@ describe('renderFileDescription — #6136: the bare-path rewriter skips formed l
463461 } ) ;
464462} ) ;
465463
464+ /**
465+ * #6229 — a `../` prefix belongs INSIDE the link, not beside it.
466+ *
467+ * The rewriter opened with `\b((?:\.\./)?…)`. A word boundary needs a word
468+ * character on one side and every character of `../` is a non-word one, so the
469+ * `\b` could never match at the `.`: the match began at the first path segment
470+ * and the prefix was stranded next to the link it belongs to, published as
471+ * `See also: ../../[system/cache.zod.ts](route)`.
472+ *
473+ * Measured rather than assumed: the prefix group was DEAD for every realistic
474+ * input, not capped at one level as first recorded. `../x/y.zod.ts` lost its
475+ * prefix exactly like `../../x/y.zod.ts` did, and the only spelling that ever
476+ * reached the group was `x../y/z.zod.ts` — a word character before the dots,
477+ * which nobody writes. So the two halves of the fix are not independent: `?`
478+ * to `*` alone is a no-op on a group that is never reached, and moving the
479+ * `\b` alone still strands the outer level of a `../../`. Both cases below
480+ * therefore pin a spelling that a one-sided fix leaves red.
481+ *
482+ * No `{@link }` appears anywhere here — this link is produced entirely by the
483+ * bare-path step, which is why the shape survived #6136.
484+ */
485+ describe ( 'renderFileDescription — #6229: a bare path keeps its `../` prefix inside the link' , ( ) => {
486+ const ctx = {
487+ // Mirrors `build-docs.ts`'s `sourcePathToDocsRoute`: `$`-anchored with a
488+ // `(?:^|/)` head, so a `../` prefix on the way IN already resolves to the
489+ // same page. The defect was never in route resolution — only in how much
490+ // of the path the rewriter handed it.
491+ sourcePathToDocsRoute : ( t : string ) => {
492+ const m = / (?: ^ | \/ ) ( s y s t e m | a p i ) \/ ( [ \w - ] + ) \. z o d \. t s $ / . exec ( t ) ;
493+ return m ? `/docs/references/${ m [ 1 ] } /${ m [ 2 ] } ` : null ;
494+ } ,
495+ } ;
496+
497+ const describedBy = ( line : string ) =>
498+ renderFileDescription ( [ '/**' , ` * ${ line } ` , ' */' , '' , "import { z } from 'zod';" , '' ] . join ( '\n' ) , ctx ) ;
499+
500+ it ( 'keeps a two-level `../../` prefix inside the link' , ( ) => {
501+ // `packages/spec/src/api/http-cache.zod.ts:35` verbatim — the exact input
502+ // behind `content/docs/references/api/http-cache.mdx`, which published
503+ // `See also: ../../[system/cache.zod.ts](/docs/references/system/cache) …`.
504+ expect ( describedBy ( '@see ../../system/cache.zod.ts for application-level caching' ) ) . toBe (
505+ 'See also: [../../system/cache.zod.ts](/docs/references/system/cache) for application-level caching' ,
506+ ) ;
507+ } ) ;
508+
509+ it ( 'keeps the `../../` prefix inside the link on the second published page' , ( ) => {
510+ // `packages/spec/src/system/cache.zod.ts:28` verbatim — the other half of
511+ // the pair, so neither page can regress on its own.
512+ expect ( describedBy ( '@see ../../api/http-cache.zod.ts for HTTP-level caching' ) ) . toBe (
513+ 'See also: [../../api/http-cache.zod.ts](/docs/references/api/http-cache) for HTTP-level caching' ,
514+ ) ;
515+ } ) ;
516+
517+ it ( 'keeps a single-level `../` prefix inside the link' , ( ) => {
518+ // NOT a case that already worked before #6229 — see the block comment. It
519+ // is pinned because it is the spelling the rest of the corpus uses inside
520+ // `{@link }` tags, so a bare one is a matter of time.
521+ expect ( describedBy ( 'The application cache lives in ../system/cache.zod.ts today.' ) ) . toBe (
522+ 'The application cache lives in [../system/cache.zod.ts](/docs/references/system/cache) today.' ,
523+ ) ;
524+ } ) ;
525+
526+ it ( 'keeps an arbitrarily deep prefix inside the link' , ( ) => {
527+ // `*`, not a second `?`: the depth is whatever the author wrote.
528+ expect ( describedBy ( 'Declared in ../../../system/cache.zod.ts for the record.' ) ) . toBe (
529+ 'Declared in [../../../system/cache.zod.ts](/docs/references/system/cache) for the record.' ,
530+ ) ;
531+ } ) ;
532+
533+ it ( 'still links an unprefixed path — the fix must not narrow the common case' , ( ) => {
534+ expect ( describedBy ( 'The application cache lives in system/cache.zod.ts today.' ) ) . toBe (
535+ 'The application cache lives in [system/cache.zod.ts](/docs/references/system/cache) today.' ,
536+ ) ;
537+ } ) ;
538+
539+ it ( 'prints an unroutable prefixed path as code, prefix included' , ( ) => {
540+ // The null-route fallback has to carry the prefix too, or the page would
541+ // show `../../` beside a code span the way it used to show it beside a link.
542+ expect ( describedBy ( 'Declared in ../../nowhere/absent.zod.ts for now.' ) ) . toBe (
543+ 'Declared in `../../nowhere/absent.zod.ts` for now.' ,
544+ ) ;
545+ } ) ;
546+
547+ it ( 'still refuses to start mid-word' , ( ) => {
548+ // The `\b` moved, it did not go away: `xsystem/…` is one token, so the
549+ // rewriter must not carve a link out of its tail.
550+ expect ( describedBy ( 'Declared in xsystem/cache.zod.ts for now.' ) ) . toBe (
551+ 'Declared in `xsystem/cache.zod.ts` for now.' ,
552+ ) ;
553+ } ) ;
554+ } ) ;
555+
466556/**
467557 * The corpus half: re-derive the verdict from the real sources, so the six
468558 * pages the issue measured cannot silently re-acquire a wrong opening, and so a
@@ -652,6 +742,23 @@ describe('corpus — every rendered description is well-formed markdown', () =>
652742 expect ( offenders ) . toEqual ( [ ] ) ;
653743 } ) ;
654744
745+ it ( 'never strands a `../` prefix outside the link it belongs to (#6229)' , ( ) => {
746+ // The corpus half of the unit block above, and the assertion behind the
747+ // issue's own acceptance grep (`\.\./\[` over `content/docs/references/`,
748+ // which this re-derives from source instead of from the artifact). The
749+ // published shape was `See also: ../../[system/cache.zod.ts](route)` on
750+ // `api/http-cache` and `system/cache`: the rewriter began matching at the
751+ // first path SEGMENT, so the prefix stayed behind as bare text beside the
752+ // construct that names it. The code-span fallback lost it the same way
753+ // (`../../` + a backtick), so both closers are checked.
754+ const offenders : string [ ] = [ ] ;
755+ for ( const { rel, out } of described ) {
756+ const stranded = out . match ( / (?: \. \. \/ ) + [ [ ` ] / ) ;
757+ if ( stranded ) offenders . push ( `${ rel } : ${ stranded [ 0 ] } ` ) ;
758+ }
759+ expect ( offenders ) . toEqual ( [ ] ) ;
760+ } ) ;
761+
655762 it ( 'keeps a description for every source that had one — #6134 selection is untouched' , ( ) => {
656763 // The rendering fix must not remove a page's opening paragraph; that is
657764 // #5059's acceptance criterion and it still binds. 185 sources carry a
0 commit comments