@timber-js/app 0.2.0-alpha.210 → 0.2.0-alpha.212

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/agent-skill.md +10 -5
  2. package/dist/_chunks/{actions-Rjk4htmA.js → actions-CCdnVtWm.js} +8 -6
  3. package/dist/_chunks/actions-CCdnVtWm.js.map +1 -0
  4. package/dist/_chunks/{als-registry-DaxkVjt5.js → als-registry-BZqHCtq-.js} +2 -4
  5. package/dist/_chunks/als-registry-BZqHCtq-.js.map +1 -0
  6. package/dist/_chunks/{cache-api-DGdYfNJn.js → cache-api-LA3sBpUS.js} +5 -5
  7. package/dist/_chunks/{cache-api-DGdYfNJn.js.map → cache-api-LA3sBpUS.js.map} +1 -1
  8. package/dist/_chunks/{chains-BoO51joc.js → chains-BfoPFraI.js} +5 -146
  9. package/dist/_chunks/chains-BfoPFraI.js.map +1 -0
  10. package/dist/_chunks/classify-BT66U83D.js +147 -0
  11. package/dist/_chunks/classify-BT66U83D.js.map +1 -0
  12. package/dist/_chunks/{cli-check-ajNY3B2e.js → cli-check-BfQ54-UJ.js} +3 -3
  13. package/dist/_chunks/{cli-check-ajNY3B2e.js.map → cli-check-BfQ54-UJ.js.map} +1 -1
  14. package/dist/_chunks/{cli-schema-sync-D2eI8jEg.js → cli-schema-sync-czh2dsLs.js} +2 -2
  15. package/dist/_chunks/{cli-schema-sync-D2eI8jEg.js.map → cli-schema-sync-czh2dsLs.js.map} +1 -1
  16. package/dist/_chunks/client-dep-entries-CQwpb8dI.js +66 -0
  17. package/dist/_chunks/client-dep-entries-CQwpb8dI.js.map +1 -0
  18. package/dist/_chunks/{convention-lint-DLmhGsRS.js → convention-lint-BEVW4EID.js} +3 -3
  19. package/dist/_chunks/{convention-lint-DLmhGsRS.js.map → convention-lint-BEVW4EID.js.map} +1 -1
  20. package/dist/_chunks/{dev-server-v97rQH4b.js → dev-server-FKxptbnI.js} +2 -2
  21. package/dist/_chunks/{dev-server-v97rQH4b.js.map → dev-server-FKxptbnI.js.map} +1 -1
  22. package/dist/_chunks/{json-lossy-check-CVuRs2hG.js → json-lossy-check-C8zBY2uZ.js} +2 -2
  23. package/dist/_chunks/{json-lossy-check-CVuRs2hG.js.map → json-lossy-check-C8zBY2uZ.js.map} +1 -1
  24. package/dist/_chunks/{live-graph-9cSnn_h9.js → live-graph-C_4v-fHv.js} +4 -3
  25. package/dist/_chunks/{live-graph-9cSnn_h9.js.map → live-graph-C_4v-fHv.js.map} +1 -1
  26. package/dist/_chunks/{logger-BP0LN6vP.js → logger-CbLdcy-W.js} +2 -2
  27. package/dist/_chunks/{logger-BP0LN6vP.js.map → logger-CbLdcy-W.js.map} +1 -1
  28. package/dist/_chunks/{poison-scan-vGV7Re0B.js → poison-scan-C92liMAr.js} +2 -4
  29. package/dist/_chunks/poison-scan-C92liMAr.js.map +1 -0
  30. package/dist/_chunks/{scanner-DmqdxzbW.js → scanner-CQt12vE2.js} +2 -2
  31. package/dist/_chunks/{scanner-DmqdxzbW.js.map → scanner-CQt12vE2.js.map} +1 -1
  32. package/dist/_chunks/{sizeof-BM1409x2.js → sizeof-QPE5nd3u.js} +2 -2
  33. package/dist/_chunks/{sizeof-BM1409x2.js.map → sizeof-QPE5nd3u.js.map} +1 -1
  34. package/dist/_chunks/{walkers-Czu2jXFq.js → walkers-DAT4avhZ.js} +3 -3
  35. package/dist/_chunks/{walkers-Czu2jXFq.js.map → walkers-DAT4avhZ.js.map} +1 -1
  36. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  37. package/dist/analyze/classify.d.ts +2 -0
  38. package/dist/analyze/classify.d.ts.map +1 -1
  39. package/dist/analyze/crawl-entry.js +3 -2
  40. package/dist/analyze/crawl-entry.js.map +1 -1
  41. package/dist/analyze/graph-command.js +2 -2
  42. package/dist/analyze/poison-scan.d.ts.map +1 -1
  43. package/dist/cache/index.js +2 -2
  44. package/dist/cache/stores/memory.js +1 -1
  45. package/dist/cdn/workers-cache-purge.js +1 -1
  46. package/dist/cli.js +3 -3
  47. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  48. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  49. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  50. package/dist/client/form.d.ts +17 -64
  51. package/dist/client/form.d.ts.map +1 -1
  52. package/dist/client/index.d.ts +2 -2
  53. package/dist/client/index.d.ts.map +1 -1
  54. package/dist/client/index.js +23 -51
  55. package/dist/client/index.js.map +1 -1
  56. package/dist/client/internal.js +25 -25
  57. package/dist/client/internal.js.map +1 -1
  58. package/dist/client/navigation-api.d.ts +25 -63
  59. package/dist/client/navigation-api.d.ts.map +1 -1
  60. package/dist/client/router-lifecycle.d.ts +17 -11
  61. package/dist/client/router-lifecycle.d.ts.map +1 -1
  62. package/dist/client/router-pipeline.d.ts +0 -1
  63. package/dist/client/router-pipeline.d.ts.map +1 -1
  64. package/dist/client/router-types.d.ts +14 -45
  65. package/dist/client/router-types.d.ts.map +1 -1
  66. package/dist/client/router.d.ts.map +1 -1
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +44 -7
  69. package/dist/index.js.map +1 -1
  70. package/dist/plugins/client-dep-entries.d.ts +44 -0
  71. package/dist/plugins/client-dep-entries.d.ts.map +1 -0
  72. package/dist/plugins/content.d.ts.map +1 -1
  73. package/dist/plugins/entries.d.ts +6 -0
  74. package/dist/plugins/entries.d.ts.map +1 -1
  75. package/dist/plugins/shims.d.ts.map +1 -1
  76. package/dist/routing/index.js +2 -2
  77. package/dist/rsc-runtime/rsc.d.ts +1 -1
  78. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  79. package/dist/server/action-client.d.ts +9 -5
  80. package/dist/server/action-client.d.ts.map +1 -1
  81. package/dist/server/action-handler.d.ts +27 -8
  82. package/dist/server/action-handler.d.ts.map +1 -1
  83. package/dist/server/als-registry.d.ts +22 -2
  84. package/dist/server/als-registry.d.ts.map +1 -1
  85. package/dist/server/client-error-message.d.ts +12 -0
  86. package/dist/server/client-error-message.d.ts.map +1 -0
  87. package/dist/server/form-state-embed.d.ts +32 -0
  88. package/dist/server/form-state-embed.d.ts.map +1 -0
  89. package/dist/server/index.d.ts +0 -2
  90. package/dist/server/index.d.ts.map +1 -1
  91. package/dist/server/index.js +7 -40
  92. package/dist/server/index.js.map +1 -1
  93. package/dist/server/internal.js +10 -6
  94. package/dist/server/internal.js.map +1 -1
  95. package/dist/server/logger.d.ts +1 -0
  96. package/dist/server/logger.d.ts.map +1 -1
  97. package/dist/server/pipeline.d.ts +20 -6
  98. package/dist/server/pipeline.d.ts.map +1 -1
  99. package/dist/server/request-context.d.ts +27 -2
  100. package/dist/server/request-context.d.ts.map +1 -1
  101. package/dist/server/route-element-builder.d.ts.map +1 -1
  102. package/dist/server/rsc-entry/action-dispatcher.d.ts +6 -5
  103. package/dist/server/rsc-entry/action-dispatcher.d.ts.map +1 -1
  104. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  105. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  106. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  107. package/dist/server/ssr-bridge-types.d.ts +8 -0
  108. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  109. package/dist/server/ssr-entry.d.ts.map +1 -1
  110. package/dist/server/ssr-render.d.ts +3 -0
  111. package/dist/server/ssr-render.d.ts.map +1 -1
  112. package/docs/api/31-api-client.mdx +9 -3
  113. package/docs/learn/08-forms-and-actions.mdx +21 -28
  114. package/package.json +1 -1
  115. package/src/analyze/classify.ts +1 -1
  116. package/src/analyze/poison-scan.ts +1 -4
  117. package/src/client/browser-entry/action-dispatch.ts +74 -18
  118. package/src/client/browser-entry/hydrate.ts +18 -0
  119. package/src/client/browser-entry/router-init.ts +4 -18
  120. package/src/client/form.tsx +33 -98
  121. package/src/client/index.ts +2 -2
  122. package/src/client/navigation-api.ts +47 -173
  123. package/src/client/navigation-transition.ts +2 -2
  124. package/src/client/router-lifecycle.ts +40 -20
  125. package/src/client/router-pipeline.ts +3 -9
  126. package/src/client/router-types.ts +14 -49
  127. package/src/client/router.ts +38 -58
  128. package/src/index.ts +21 -2
  129. package/src/plugins/client-dep-entries.ts +78 -0
  130. package/src/plugins/content.ts +26 -1
  131. package/src/plugins/entries.ts +7 -0
  132. package/src/plugins/shims.ts +0 -2
  133. package/src/rsc-runtime/rsc.ts +3 -0
  134. package/src/rsc-runtime/vendor-types.d.ts +14 -0
  135. package/src/server/action-client.ts +18 -20
  136. package/src/server/action-handler.ts +133 -95
  137. package/src/server/als-registry.ts +23 -9
  138. package/src/server/client-error-message.ts +18 -0
  139. package/src/server/form-state-embed.ts +63 -0
  140. package/src/server/index.ts +0 -4
  141. package/src/server/logger.ts +6 -1
  142. package/src/server/pipeline.ts +27 -8
  143. package/src/server/request-context.ts +39 -2
  144. package/src/server/route-element-builder.ts +12 -1
  145. package/src/server/rsc-entry/action-dispatcher.ts +37 -34
  146. package/src/server/rsc-entry/error-renderer.ts +2 -1
  147. package/src/server/rsc-entry/rsc-stream.ts +20 -11
  148. package/src/server/rsc-entry/ssr-renderer.ts +14 -1
  149. package/src/server/ssr-bridge-types.ts +9 -0
  150. package/src/server/ssr-entry.ts +1 -0
  151. package/src/server/ssr-render.ts +5 -0
  152. package/dist/_chunks/actions-Rjk4htmA.js.map +0 -1
  153. package/dist/_chunks/als-registry-DaxkVjt5.js.map +0 -1
  154. package/dist/_chunks/chains-BoO51joc.js.map +0 -1
  155. package/dist/_chunks/poison-scan-vGV7Re0B.js.map +0 -1
  156. package/dist/server/form-flash.d.ts +0 -78
  157. package/dist/server/form-flash.d.ts.map +0 -1
  158. package/src/server/form-flash.ts +0 -89
@@ -1 +1 @@
1
- {"version":3,"file":"scanner-DmqdxzbW.js","names":[],"sources":["../../src/routing/collision-probe.ts","../../src/routing/interception-overlap.ts","../../src/routing/interception.ts","../../src/routing/collision-spaces.ts","../../src/routing/slot-placement.ts","../../src/routing/file-cache.ts","../../src/routing/export-detect.ts","../../src/routing/scanner.ts"],"sourcesContent":["/**\n * The order-dependence oracle shared by every route collision validator.\n *\n * A \"collision space\" is a set of pages that compete for URLs with each other:\n * the main tree, each parallel slot, each intercepting subtree. Two pages in\n * one space that answer the same URL are ambiguous — `matchNode` resolves the\n * tie by whichever `children` array lists first, so which page renders is\n * decided by filesystem enumeration order and renaming a route group can flip\n * it. Every space rejects that shape, and they all reject it the same way,\n * through this module.\n *\n * **The check asks the real matcher rather than modelling it.** For each leaf\n * we build a URL it matches, then run `matchUrlParts` twice — once on the\n * space, once on a clone with every `children` array reversed — and compare the\n * winners. A URL answered by two or more candidates in the same priority tier\n * resolves to whichever comes first in `children`, so reversing the arrays\n * flips it; a URL with a single winner, or one settled by a higher tier, is\n * unaffected. Order-dependence is therefore detected exactly, with no second\n * copy of the precedence rules to drift.\n *\n * That drift is why this is written as an oracle. The interception validator's\n * first version normalized each pattern into a \"match shape\" and compared\n * shapes, which meant reimplementing `matchNode`'s tiers, its group flattening,\n * and its zero-length optional-catch-all handling. Five review rounds found\n * five divergences between the two — param spelling, a zero-length catch-all,\n * the bracket tokens colliding with legal static directory names, a page\n * settling a grouped catch-all, and a page settling a *direct* one (which the\n * model could not express at all, because `matchNode` checks every route-group\n * page before any direct optional catch-all). Each fix was locally right and\n * the next divergence surfaced anyway. The model was the defect.\n *\n * **Every claim made here is existential** — \"here is a URL these two contend\n * for\" — which is what a finite set of probes can actually establish. The\n * reverse, \"this page can never win any URL\", is universal, and a probe's\n * silence is not evidence for it: a leaf that loses every probe may still own\n * URLs no probe visited. An earlier revision reported that as an\n * unreachable-page error and produced two false build errors in one review\n * round. Probe inadequacy must only ever cost a missed collision, never a\n * rejected app — so nothing here may report a leaf as unreachable.\n *\n * See design/07-routing.md §\"Comparing Routes by the URLs They Match\".\n */\n\nimport type { SegmentNode } from './types.ts';\nimport { effectiveUrlSegment } from './segment-classify.ts';\n// The validators ask the real matcher which page answers a URL rather than\n// modelling its precedence rules a second time. `tree-match.ts` is a pure\n// walker over `SegmentNode` with no request-time state, and\n// `server/chain-url-parts.ts` already imports the other direction.\nimport { matchUrlParts } from '../server/tree-match.ts';\n\n/** One page-or-route-bearing node in a collision space. */\nexport interface ProbeLeaf {\n /** The file that serves it — what the error message names. */\n filePath: string;\n /**\n * The nodes from the space root down to this leaf that contribute URL parts\n * — route groups excluded, since they are transparent for matching. Empty\n * for the space root's own page: `matchUrlParts` matches parts against the\n * root's *children*, so the root itself consumes nothing.\n */\n chain: SegmentNode[];\n}\n\n/** Two files in one space that a single URL resolves to order-dependently. */\nexport interface ProbedCollision {\n /** The contended URL, written for a human (see `describeProbeUrl`). */\n url: string;\n /** The file that wins in filesystem order. */\n first: string;\n /** The file that wins when every `children` array is reversed. */\n second: string;\n}\n\n/**\n * Find a URL in this collision space whose winner depends on `children` order,\n * or null if every probed URL resolves the same way in both orderings.\n *\n * `spaceRoot` is the node `matchUrlParts` starts from — the app root, a slot\n * node, or an intercepting directory. `base` is the URL that root sits at, used\n * only to render the reported URL.\n */\nexport function findOrderDependentCollision(\n spaceRoot: SegmentNode,\n leaves: Iterable<ProbeLeaf>,\n base: string\n): ProbedCollision | null {\n const reversed = withReversedChildren(spaceRoot);\n const longTail = longTailLength(spaceRoot) + 1;\n // Computed once for the space, not per leaf: it bounds the second candidate\n // wave, which only runs for a leaf the first wave never reached.\n const minLength = longestStaticName(spaceRoot);\n\n // Each probe is checked as it is generated, and the first flipped winner\n // returns immediately. Accumulating every probe first and scanning afterwards\n // reports the same collision — this walks the same probes in the same order —\n // but it pays for the whole space before saying anything, and the leaves that\n // cost the most are exactly the ones a found collision makes irrelevant.\n //\n // In a space of N equivalent dynamic siblings, only the first and last can\n // win a probe (one per ordering), so every middle leaf is unreachable and\n // exhausts the entire candidate alphabet looking for a value that reaches it\n // — 300 such siblings took ~2.5s to produce a build error the very first\n // probe already proved (codex, on PR #1002). A valid space of the same size\n // scans in ~35ms, so the cost was confined to the error path, but a slow\n // error is still a bad error.\n //\n // Deduped so a space with many leaves does not re-probe one URL, and so the\n // collision reported does not depend on which leaf generated a shared probe.\n const seen = new Set<string>();\n for (const leaf of leaves) {\n for (const { parts, display } of probeUrlsForLeaf(\n spaceRoot,\n reversed,\n leaf,\n longTail,\n minLength\n )) {\n const key = parts.join('/');\n if (seen.has(key)) continue;\n seen.add(key);\n\n const first = matchedLeafFile(spaceRoot, parts);\n const second = matchedLeafFile(reversed, parts);\n if (!first || !second || first === second) continue;\n return { url: describeProbeUrl(base, display), first, second };\n }\n }\n\n return null;\n}\n\n/** Join a probe's display parts onto its space's base. */\nfunction describeProbeUrl(base: string, display: string[]): string {\n if (display.length === 0) return base || '/';\n return `${base === '/' ? '' : base}/${display.join('/')}`;\n}\n\n/**\n * The file `matchUrlParts` resolves for a URL, or null if nothing does.\n *\n * A `route.ts` counts: `isTerminal` accepts either, so a route handler is a\n * legitimate winner of the walk and an ambiguity between a page and a route\n * handler is as real as one between two pages.\n */\nfunction matchedLeafFile(node: SegmentNode, parts: string[]): string | null {\n const match = matchUrlParts(node, parts);\n if (!match) return null;\n const leaf = match.chain[match.chain.length - 1];\n return leaf.page?.filePath ?? leaf.route?.filePath ?? null;\n}\n\n/**\n * A generated URL, and how to show it to a human.\n *\n * `display` is built alongside `parts` rather than derived from it by\n * substitution: a *static* segment can legitimately be named the same as the\n * probe value, and replacing every occurrence of that string rewrote the\n * literal directory to `<any>` — reporting `/<any>/<any>` for a collision under\n * a static `atimber-probea`, which claims a URL position is dynamic when the\n * author wrote it out (codex, on PR #1002). Only the segment that generated a\n * value knows whether it stood for one.\n */\ninterface ProbeUrl {\n parts: string[];\n display: string[];\n /**\n * Whether any part of this URL stood in for a value. When nothing did, every\n * candidate generates the identical URL and the search can stop after one.\n */\n dynamic: boolean;\n}\n\n/**\n * The values a probe's dynamic parts are tried with, in order.\n *\n * A probe stands in for \"any value\" at a dynamic position, so it is only useful\n * if it reaches the leaf it was built for. A *static* sibling's literal name or\n * a more specific *affixed* sibling's pattern can claim it instead, and that\n * higher-priority segment then wins in both orderings — a genuine tie\n * underneath goes unreported, which is safe but is the whole thing these\n * validators are for.\n *\n * Which values are claimable is a question about `matchNode`'s precedence, so\n * `probeUrlsForLeaf` asks `matchUrlParts` rather than deciding it here. This\n * only has to offer enough shapes for the search to find one that works: an\n * affixed sibling constrains the head of the value, its tail, or both, so head\n * and tail are varied over an alphanumeric alphabet. The head varies fastest\n * because a prefix is the more common affix. Exhausting the list is a missed\n * collision like any other inadequate probe, so this is a search and not a\n * proof — nothing downstream may treat it as one.\n *\n * Exported so a test that has to *name* a capturing static sibling can derive\n * the name from this list rather than restate it — a fixture spelling the value\n * out by hand stops being a capture the moment the list changes, and passes.\n *\n * Three revisions of a hand-built value each dodged one more capture and were\n * each followed by another (avoid static names; avoid affix boundary\n * characters; avoid re-affixed forms of static names — then a strictly more\n * specific affixed sibling, TIM-1313). Each rule was modelling affix precedence\n * by hand, which is what the match-shape model was deleted for.\n */\nexport function* probeCandidates(minLength = 0): Generator<string> {\n const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';\n const base = 'timber-probe';\n\n function* wave(core: string): Generator<string> {\n for (const tail of alphabet) {\n for (const head of alphabet) yield `${head}${core}${tail}`;\n }\n }\n\n yield* wave(base);\n\n // A second wave, longer than every static segment name in the space.\n //\n // A static sibling claims a probe only by **string equality**, so a value\n // longer than every static name in the space cannot be claimed by one — that\n // is a fact about lengths, not a model of `matchNode`'s precedence, which is\n // what makes it safe to state as an invariant where \"pick an unclaimable\n // value\" was not. An app holding a static directory for all 1296 first-wave\n // candidates claimed every probe and hid a real collision (codex, on\n // PR #1002); it cannot do the same here, because the length is derived from\n // its own longest static and adding a longer one only pushes this further out.\n //\n // Affix capture is not addressed by length and is not addressable this way:\n // an affixed pattern matches infinitely many values, so escaping one is still\n // the head/tail search below. The first wave is byte-identical to what it was\n // before this existed, so nothing that already found a value changes.\n const padded = base.padEnd(Math.max(base.length, minLength), 'x');\n if (padded !== base) yield* wave(padded);\n}\n\n/**\n * The longest static segment name anywhere in the space.\n *\n * Read through `effectiveUrlSegment` so an intercepting directory counts as the\n * segment it intercepts, and only static kinds count: a dynamic sibling claims\n * a probe by pattern rather than by equality, which no length can escape.\n */\nfunction longestStaticName(node: SegmentNode): number {\n let longest = 0;\n for (const child of node.children) {\n const seg = effectiveUrlSegment(child);\n if (seg.segmentType === 'static') longest = Math.max(longest, seg.segmentName.length);\n longest = Math.max(longest, longestStaticName(child));\n }\n return longest;\n}\n\n/**\n * The probe URLs for one leaf, built from the first candidate value that\n * actually reaches it.\n *\n * \"Reaches it\" is `matchUrlParts` resolving one of the leaf's own URLs to the\n * leaf's file, in either ordering — either is enough, because a leaf that wins\n * only in the reversed ordering is precisely the contention being looked for.\n * A catch-all leaf may be reached by its long probe and not its short one (at\n * one part it loses to a bare dynamic sibling deterministically), so any one of\n * the generated URLs landing is enough to accept the value.\n *\n * A leaf that no candidate reaches is shadowed at its position for every value\n * tried — most often because it sits below a catch-all, which `matchNode`\n * accepts only as a terminal and never descends past. Its probes are still\n * emitted, from the first candidate: a probe that misses its own leaf is never\n * *wrong* — it is a real URL, and a winner that differs between the two\n * orderings is a real ambiguity whichever leaf it belongs to. Nothing here may\n * report the leaf as unreachable; that is the universal claim these validators\n * do not make.\n */\nfunction probeUrlsForLeaf(\n root: SegmentNode,\n reversed: SegmentNode,\n leaf: ProbeLeaf,\n longTail: number,\n minLength: number\n): ProbeUrl[] {\n let fallback: ProbeUrl[] | null = null;\n\n for (const value of probeCandidates(minLength)) {\n const urls = probeUrlsFor(leaf.chain, value, longTail);\n const reaches = urls.some(\n ({ parts }) =>\n matchedLeafFile(root, parts) === leaf.filePath ||\n matchedLeafFile(reversed, parts) === leaf.filePath\n );\n if (reaches) return urls;\n fallback ??= urls;\n\n // A leaf with no dynamic part in its chain matches exactly one URL, so the\n // candidate value never appears in it and every further candidate would\n // generate the identical probe. Searching on would cost the whole alphabet\n // per statically-shadowed leaf — and in the main tree those are ordinary\n // (anything below a catch-all) rather than the pathology they are inside an\n // intercepting subtree. Stopping is not a weaker check: the URLs are the\n // same ones, and they are still probed via `fallback`.\n if (!urls.some(({ dynamic }) => dynamic)) break;\n }\n\n // `probeCandidates` is non-empty, so this is only reached with a fallback.\n return fallback ?? [];\n}\n\n/**\n * The URL part one segment contributes to a probe.\n *\n * A dynamic segment stands for any single value, so it contributes the\n * candidate value — re-affixed, since an affixed segment only matches parts\n * that carry its prefix and suffix, so a probe without them could not reach it.\n * A catch-all stands for one or more parts, and one is enough to reach the leaf.\n *\n * The re-affixed part is the reason a value cannot be checked once for the\n * whole space: it exposes the value to every affixed sibling that *extends*\n * this segment's affixes, which differs per position. `probeUrlsForLeaf` asks\n * the matcher whether the assembled URL reaches its leaf, and moves to the next\n * candidate when it does not.\n *\n * Segments are read through `effectiveUrlSegment` so an intercepting directory\n * contributes the part the segment it intercepts would.\n */\nfunction probePartFor(node: SegmentNode, value: string): ProbePart {\n const seg = effectiveUrlSegment(node);\n switch (seg.segmentType) {\n case 'dynamic': {\n const prefix = seg.paramPrefix ?? '';\n const suffix = seg.paramSuffix ?? '';\n // The affixes are kept in the shown form too: `x-<any>` says which URLs\n // are contended for, where a bare `<any>` would claim every value at that\n // position.\n return {\n part: `${prefix}${value}${suffix}`,\n shown: `${prefix}<any>${suffix}`,\n dynamic: true,\n };\n }\n case 'catch-all':\n case 'optional-catch-all':\n return { part: value, shown: '<any>', dynamic: true };\n default:\n return { part: seg.segmentName, shown: seg.segmentName, dynamic: false };\n }\n}\n\n/** One URL part of a probe: what to send, what to show, and whether it stands for a value. */\ninterface ProbePart {\n part: string;\n shown: string;\n dynamic: boolean;\n}\n\n/**\n * The URLs that reach a leaf.\n *\n * An **optional catch-all also matches zero parts**, so it answers the URL its\n * parent sits at as well as longer ones. Both are probed: whether the shorter\n * one is actually *its* to answer is `matchUrlParts`' business, not something\n * to decide here — a page above it, or in a route group beside it, may take\n * that URL deterministically, and the probe will simply report the same winner\n * in both orderings.\n */\nfunction probeUrlsFor(chain: SegmentNode[], value: string, longTail: number): ProbeUrl[] {\n const probeParts = chain.map((node) => probePartFor(node, value));\n const url = (ps: ProbePart[]): ProbeUrl => ({\n parts: ps.map((p) => p.part),\n display: ps.map((p) => p.shown),\n dynamic: ps.some((p) => p.dynamic),\n });\n\n // The kind that decides the probe lengths is the last part-CONTRIBUTING\n // segment's, which is why it is read off the chain rather than off the leaf\n // node. An empty chain is the space root's own page: the walk matches parts\n // against that node's children, so the node's own segment consumes nothing\n // and the only URL that reaches the page is the empty one — even when the\n // root is itself a catch-all (`(.)[...rest]`).\n const leaf = chain[chain.length - 1];\n const kind = leaf && effectiveUrlSegment(leaf).segmentType;\n const isCatchAll = kind === 'catch-all' || kind === 'optional-catch-all';\n if (!isCatchAll) return [url(probeParts)];\n\n // `matchNode` accepts a catch-all only as a terminal, so a catch-all that\n // *is* the leaf owns the whole tail of the probe and it can be re-sized\n // freely. (A catch-all higher in the chain has descendants nothing can\n // reach; those leaves are the statically-shadowed case above.)\n const head = probeParts.slice(0, -1);\n const urls = [url(probeParts)];\n\n // A LONG tail as well as a short one. At one part a catch-all loses to any\n // bare dynamic sibling, deterministically — so a one-part probe reports that\n // sibling in both orderings and two catch-alls competing at every longer URL\n // go unnoticed. `longTail` is past every finite branch in the space, where\n // no fixed-length competitor can still be matching.\n if (longTail > 1) {\n // Every part is the candidate value verbatim. Decorating them to differ\n // (`${value}-0`) puts them back inside patterns like `[value]-0/[...rest]`,\n // which outranks a catch-all — the trailing counter reintroduced exactly\n // the affix capture the value is being searched for (codex, on PR #1000).\n // The parts need not differ from each other; a catch-all consumes them all.\n const tail: ProbePart[] = Array.from({ length: longTail }, () => ({\n part: value,\n shown: '<any>',\n dynamic: true,\n }));\n urls.push(url([...head, ...tail]));\n }\n\n // An optional catch-all also matches zero parts.\n if (kind === 'optional-catch-all') urls.push(url(head));\n\n return urls;\n}\n\n/**\n * One more than the longest fixed-length path in the space.\n *\n * A probe of this length cannot be claimed by any branch that consumes a fixed\n * number of URL parts, so whatever matches it is a catch-all — which is\n * exactly the competition a short probe hides.\n */\nfunction longTailLength(node: SegmentNode): number {\n let deepest = 0;\n for (const child of node.children) {\n const own = child.segmentType === 'group' ? 0 : 1;\n deepest = Math.max(deepest, own + longTailLength(child));\n }\n return deepest;\n}\n\n/**\n * The subtree with every `children` array reversed, top to bottom.\n *\n * Nodes are shallow-copied, so files and slots are shared with the original —\n * only the ordering `matchNode` reads is different. Slots are not reversed\n * because `matchNode` never walks them; each slot is validated as its own\n * collision space, with its own reversed clone.\n */\nfunction withReversedChildren(node: SegmentNode): SegmentNode {\n return { ...node, children: [...node.children].reverse().map(withReversedChildren) };\n}\n","/**\n * Pattern intersection for intercepted URL patterns.\n *\n * Checks whether two intercepted URL patterns can both match the same concrete\n * URL. Used by the cross-mode claim check (TIM-1319) to detect when a\n * children-path interceptor and a slot interceptor from the same owner overlap.\n *\n * This is not the order-dependence oracle (`findOrderDependentCollision`) the\n * within-mode collision checks use. `findInterceptionMatch` picks the mode by\n * first-match on `pathnameMatchesPattern`, which has no priority tiers, so a\n * static `/feed/foo` and a dynamic `/feed/[id]` both match `/feed/foo` even\n * though `matchUrlParts` would resolve the static deterministically. The\n * intersection test catches that: it walks both patterns segment by segment and\n * asks whether any value satisfies both.\n *\n * This is not a model of `matchNode` precedence — that decides *which*\n * candidate wins when several match one URL. This decides the prior question:\n * *do* they both match one URL at all? The two questions are independent, and\n * conflating them is what caused TIM-1308's five-round drift.\n */\n\nimport { classifyUrlSegment } from './segment-classify.ts';\n\n/**\n * Check whether two intercepted URL patterns can both match the same concrete\n * URL. Returns a display string for the overlapping URL, or null.\n *\n * Walks both patterns segment by segment:\n * - Two statics: must be identical.\n * - Static + dynamic: static must satisfy the dynamic's affix constraints.\n * - Two dynamics: affix constraints must be compatible.\n * - A catch-all at either side: compatible with everything remaining.\n *\n * If the patterns differ in length, they can only overlap if the longer one's\n * extra part is an optional catch-all (which also matches zero segments).\n */\nexport function interceptedPatternsOverlap(p1: string, p2: string): string | null {\n if (p1 === p2) return p1;\n\n const parts1 = p1 === '/' ? [] : p1.slice(1).split('/');\n const parts2 = p2 === '/' ? [] : p2.slice(1).split('/');\n const display: string[] = [];\n let i = 0;\n\n while (i < parts1.length && i < parts2.length) {\n const seg1 = classifyUrlSegment(parts1[i]);\n const seg2 = classifyUrlSegment(parts2[i]);\n\n if (\n seg1.kind === 'catch-all' ||\n seg1.kind === 'optional-catch-all' ||\n seg2.kind === 'catch-all' ||\n seg2.kind === 'optional-catch-all'\n ) {\n display.push('<any>');\n return '/' + display.join('/');\n }\n\n const part = segmentsOverlap(seg1, seg2);\n if (part === null) return null;\n display.push(part);\n i++;\n }\n\n const remaining = i < parts1.length ? parts1 : i < parts2.length ? parts2 : null;\n if (remaining) {\n const next = classifyUrlSegment(remaining[i]);\n if (next.kind !== 'optional-catch-all') return null;\n }\n\n return display.length === 0 ? '/' : '/' + display.join('/');\n}\n\n/**\n * Can some concrete value satisfy both classified URL segments?\n *\n * An affixed dynamic (`a-[id].png`) constrains both the prefix and suffix of\n * the value, so a static segment must satisfy those constraints and two\n * dynamics must have compatible affixes — prefixes where one extends the other,\n * and likewise for suffixes. Without this check, `/feed/photo.jpg` beside\n * `/feed/asset-[id].png` would be falsely rejected.\n */\nfunction segmentsOverlap(\n seg1: ReturnType<typeof classifyUrlSegment>,\n seg2: ReturnType<typeof classifyUrlSegment>\n): string | null {\n if (seg1.kind === 'static' && seg2.kind === 'static') {\n return seg1.value === seg2.value ? seg1.value : null;\n }\n\n if (seg1.kind === 'static' && seg2.kind === 'dynamic') {\n return staticMatchesAffix(seg1.value, seg2) ? seg1.value : null;\n }\n if (seg2.kind === 'static' && seg1.kind === 'dynamic') {\n return staticMatchesAffix(seg2.value, seg1) ? seg2.value : null;\n }\n\n if (seg1.kind === 'dynamic' && seg2.kind === 'dynamic') {\n return dynamicAffixesCompatible(seg1, seg2) ? '<any>' : null;\n }\n\n return '<any>';\n}\n\n/**\n * Does a static value satisfy an affixed dynamic's prefix, suffix, and\n * non-empty capture constraints?\n *\n * Mirrors the check `pathnameMatchesPattern` applies: the value must start with\n * the prefix, end with the suffix, and be strictly longer than their combined\n * length (a capture of zero characters is not a match).\n */\nfunction staticMatchesAffix(value: string, dynamic: { prefix?: string; suffix?: string }): boolean {\n const prefix = dynamic.prefix ?? '';\n const suffix = dynamic.suffix ?? '';\n if (!prefix && !suffix) return true;\n if (prefix && !value.startsWith(prefix)) return false;\n if (suffix && !value.endsWith(suffix)) return false;\n return value.length > prefix.length + suffix.length;\n}\n\n/**\n * Can some value simultaneously satisfy two affixed dynamics?\n *\n * Two prefix constraints are compatible when one extends the other (a value\n * starting with the longer one also starts with the shorter). Suffixes are\n * analogous. Without affixes on either side, every value works.\n */\nfunction dynamicAffixesCompatible(\n d1: { prefix?: string; suffix?: string },\n d2: { prefix?: string; suffix?: string }\n): boolean {\n const p1 = d1.prefix ?? '',\n s1 = d1.suffix ?? '';\n const p2 = d2.prefix ?? '',\n s2 = d2.suffix ?? '';\n\n if (p1 && p2 && !p1.startsWith(p2) && !p2.startsWith(p1)) return false;\n if (s1 && s2 && !s1.endsWith(s2) && !s2.endsWith(s1)) return false;\n return true;\n}\n","/**\n * Intercepting route utilities.\n *\n * Computes rewrite rules from the route tree that enable intercepting routes\n * to conditionally render when navigating via client-side (soft) navigation.\n *\n * The mechanism: at build time, each intercepting route directory generates a\n * conditional rewrite. On soft navigation, the client sends an `X-Timber-URL`\n * header with the current pathname. The server checks if any rewrite's source\n * (the intercepted URL) matches the target pathname AND the header matches\n * the intercepting route's parent URL. If both match, the intercepting route\n * renders instead of the normal route.\n *\n * On hard navigation (no header), no rewrite matches, and the normal route\n * renders.\n *\n * See design/07-routing.md §\"Intercepting Routes\"\n */\n\nimport type { SegmentNode, InterceptionMarker, RouteFile } from './types.ts';\nimport { computeSegmentTreePaths, treePathDepth } from './segment-keys.ts';\nimport { classifyUrlSegment, effectiveUrlSegment } from './segment-classify.ts';\n// Every collision space — the main tree, each slot, each intercepting subtree\n// — decides order-dependence with the same oracle, so the spaces cannot drift\n// on what counts as two pages answering one URL. See TIM-1308.\nimport { findOrderDependentCollision, type ProbeLeaf } from './collision-probe.ts';\nimport { interceptedPatternsOverlap } from './interception-overlap.ts';\n\n/** Fields shared by both interception shapes. */\ninterface InterceptionRewriteBase {\n /**\n * The URL pattern that this rewrite intercepts (the target of navigation).\n * E.g., \"/photo/[id]\" for a (.)photo/[id] interception.\n */\n interceptedPattern: string;\n /**\n * The tree path of the segment that OWNS the slot holding this\n * intercepting route (`computeSegmentTreePaths`, e.g. \"/feed\",\n * \"/(browse)\", \"/(browse)/feed\", or \"/\" at the app root).\n *\n * The rewrite only applies when the source route — the page the user is\n * navigating FROM — passes through that directory, because only then\n * does the slot holding the intercepting page exist in the rendered\n * tree. The server checks this by matching X-Timber-URL and testing its\n * segment chain's tree paths for this value.\n *\n * Neither of the identities already in the codebase works here:\n *\n * - A **URL prefix** cannot express it. Route groups contribute nothing\n * to `urlPath`, so `(browse)/@detail/(...)x` and a genuinely\n * root-level `@modal/(...)x` both have prefix \"/\" while meaning very\n * different things.\n * - A **state-tree key** (`computeSegmentKeys`) cannot either. It\n * qualifies a group with its ancestry but resets to `urlPath` at the\n * next URL-visible segment, so `(browse)/feed` and `(landing)/feed`\n * both key as \"/feed\" — and a navigation inside `(landing)/feed/…`\n * would reach `(browse)/feed`'s slot.\n *\n * Tree paths never reset, so two directories that render different\n * layouts never share one. \"/\" as a scope means the app root, which\n * every route passes through — preserving the intentional\n * match-from-anywhere behavior of a root-level `(...)` interception\n * (TIM-1006) without an exemption. See TIM-1279.\n */\n interceptingScope: string;\n}\n\n/**\n * A conditional rewrite rule generated from an intercepting route.\n *\n * `mode` records which of the two shapes the directory expresses, because the\n * two render through entirely different mechanisms and the fields they need\n * are not the same. It is a union rather than a flag so a reader cannot\n * re-match `interceptingPrefix` on the shape that has no such pathname.\n *\n * See design/07-routing.md §\"Intercepting Routes\".\n */\nexport type InterceptionRewrite = SlotInterceptionRewrite | ChildrenInterceptionRewrite;\n\n/**\n * A **modal overlay** interception — the intercepting directory is a direct\n * child of a parallel slot (`feed/@modal/(.)photo/[id]`).\n *\n * The server re-matches the *source* route and the slot resolver finds the\n * intercepting child inside the slot, so the intercepting page layers over\n * the page the user was already on.\n */\nexport interface SlotInterceptionRewrite extends InterceptionRewriteBase {\n mode: 'slot';\n /**\n * The pathname the server re-matches when this rewrite applies — the\n * intercepting slot owner's URL path. E.g., \"/feed\" for a (.)photo/[id]\n * inside /feed/@modal/.\n *\n * This is a build-time constant, never a client-supplied value: the\n * X-Timber-URL header selects a rewrite but never becomes the rendered\n * pathname.\n */\n interceptingPrefix: string;\n}\n\n/**\n * A **sibling layout swap** interception — the intercepting directory is a\n * direct child of an ordinary segment or route group\n * (`(browse)/(...)[artistSlug]/…`).\n *\n * There is no overlay, no slot and no `default.tsx`: the intercepting page\n * *replaces* `children` for the owner's layout chain. Nothing is re-matched\n * either. The server resolves the owner by `interceptingScope` — a tree path,\n * which identifies exactly one directory — and builds the rendered chain as\n * `[…owner ancestors, interceptingNode, …its matched descendants]`. That is an\n * ordinary segment chain, so `route-element-builder.ts` renders it with no\n * knowledge that interception happened.\n *\n * Resolving the owner from the tree rather than from a URL is what makes this\n * shape work where the modal shape cannot: `interceptingPrefix` is\n * URL-derived, and a route group contributes nothing to `urlPath`, so an owner\n * like `app/(browse)/` serializes to \"/\" and re-matching it lands in whichever\n * group owns the canonical page. A tree path never collapses (TIM-1280).\n */\nexport interface ChildrenInterceptionRewrite extends InterceptionRewriteBase {\n mode: 'children';\n}\n\n/**\n * A set of sibling interceptors that compete for one URL position.\n *\n * `base` is the intercepted base they share — the URL their own segments sit\n * directly under — and `anchorIndex` is how many URL parts that base spans,\n * which is the index in a target pathname their segments are matched at.\n * Deeper anchors are more local and are tried first.\n */\nexport interface InterceptorAnchorGroup<TFile> {\n base: string;\n anchorIndex: number;\n interceptors: SegmentNode<TFile>[];\n}\n\n/**\n * Group an owner's intercepting children by the URL position they contend for.\n *\n * This is the definition of *which interceptors compete*, and it has exactly\n * one caller shape on each side of the build: `findInterceptingMatch` resolves\n * a group with one `matchUrlParts` walk, and\n * `assertNoInterceptedPatternCollisions` validates the same group as one\n * collision space. Written twice, the two would be free to disagree about what\n * competes — and a resolver that walks a wider set than the validator checked\n * is precisely the order-dependence the validator exists to reject (TIM-1310).\n *\n * Contention is keyed by the computed intercepted **base**, not by the marker:\n * under an owner at `/feed`, `(..)` and `(...)` both base at `/` and are both\n * matched at index 0, so they are one group. Different bases are different\n * positions — a `(.)` interceptor can never claim a target that belongs to a\n * `(..)` one beside it, because the base must also match the target's leading\n * parts.\n *\n * The base is a URL *pattern*, so its length is its part count only because\n * every pattern part stands for exactly one URL part. A catch-all in the base\n * breaks that, and `assertInterceptedBaseIsCountable` rejects the shape at\n * build time.\n *\n * A node missing either marker field is skipped, which `classifySegment` never\n * produces — it sets both whenever it reports `'intercepting'`. Stating the\n * filter here rather than defaulting the marker is what makes the validator and\n * the resolver see the same set: such a node renders nothing either way, and a\n * validator more permissive than the resolver would be checking a candidate\n * that never competes while the resolver walks one nobody checked.\n *\n * `children` is read directly rather than flattened through route groups,\n * matching both resolvers: the slot resolver reads `slotNode.children`, and an\n * interceptor deeper inside a slot subtree is rejected by\n * `validateInterceptionPlacement`. On the children path an interceptor under a\n * route group is legal and the *group* is its owner, so it is grouped with that\n * group's other interceptors — which is again exactly what the resolver walks,\n * since it resolves the owner by tree path.\n */\nexport function groupByInterceptedAnchor<TFile>(\n ownerUrlPath: string,\n children: readonly SegmentNode<TFile>[]\n): InterceptorAnchorGroup<TFile>[] {\n const byBase = new Map<string, InterceptorAnchorGroup<TFile>>();\n\n for (const child of children) {\n if (child.segmentType !== 'intercepting') continue;\n if (!child.interceptionMarker || !child.interceptedSegmentName) continue;\n\n const base = computeInterceptedBase(ownerUrlPath, child.interceptionMarker);\n const group = byBase.get(base);\n if (group) {\n group.interceptors.push(child);\n } else {\n byBase.set(base, {\n base,\n anchorIndex: base === '/' ? 0 : base.slice(1).split('/').length,\n interceptors: [child],\n });\n }\n }\n\n // Deepest anchor first: a `(.)` interception is the most local one and wins\n // over a `(...)` that reaches the same page from the root.\n return [...byBase.values()].sort((a, b) => b.anchorIndex - a.anchorIndex);\n}\n\n/**\n * Collect all interception rewrite rules from the route tree.\n *\n * Walks the tree recursively. For each intercepting segment, computes the\n * intercepted URL based on the marker and the segment's position.\n *\n * The list is returned **deepest scope first**, which is what makes\n * `findInterceptionMatch`'s first-match rule deterministic; see\n * `sortByScopeDepth`.\n */\nexport function collectInterceptionRewrites(root: SegmentNode): InterceptionRewrite[] {\n const rewrites: InterceptionRewrite[] = [];\n walkForInterceptions(root, [root], false, rewrites);\n return sortByScopeDepth(rewrites);\n}\n\n/**\n * Order the rewrites deepest scope first, so the request-time first-match rule\n * resolves two applicable rewrites by *nesting* rather than by tree-walk order.\n *\n * `findInterceptionMatch` returns the first rewrite whose pattern matches the\n * target and whose scope the source route reports. More than one can satisfy\n * both, and the case is not exotic — an owner and a directory *beneath* it may\n * each hold an interceptor for one URL:\n *\n * ```\n * app/feed/(.)photo/page.tsx scope /feed\n * app/feed/(a)/(.)photo/page.tsx scope /feed/(a)\n * ```\n *\n * A page inside `(a)` renders both owners' layouts, so the client reports both\n * tree paths and both scopes pass. Unsorted, the winner is whichever the\n * filesystem enumerated first — renaming the group flips which page a click\n * opens, the defect class TIM-1305/TIM-1308/TIM-1310 each rejected in a\n * different space. Here it is an ambiguity between *rewrites* rather than\n * between pages inside one walk, so no collision space sees it.\n *\n * **Deepest wins, and it is specified rather than rejected** because the shape\n * is one a correct app writes on purpose: a nested layout refining the\n * interception its parent declares, exactly as `groupByInterceptedAnchor`\n * already prefers the deepest *anchor* within one owner. The two rules are the\n * same preference — the more local interception is the author's more specific\n * statement — applied at the two levels the mechanism has.\n *\n * Depth is counted on the **tree path**, the identity that never collapses\n * (TIM-1279/TIM-1280); a `urlPath` prefix test would relate `(browse)` and\n * `(landing)`, which render different layouts and share no chain. Two scopes on\n * one rendered chain are strictly nested, so depth orders them totally, and two\n * scopes that are *not* on one chain — sibling groups, `feed/(a)` beside\n * `feed/(b)` — never both pass the scope check, so their relative order is not\n * observable. That is why this sorts on depth alone rather than testing\n * ancestry: it is a total order over the pairs that can compete, and it costs\n * nothing per request.\n *\n * Sorting is stable, so rewrites sharing one scope keep tree-walk order. Equal\n * scopes mean one owner, and of the three pairings that produces only one was\n * ambiguous — a children-path interceptor beside a slot one — and that is now\n * rejected at build time by `assertNoCrossModeClaims` (TIM-1319). The\n * enumeration is in design/07-routing.md §\"Selection Between Owners\" rather\n * than repeated here, so there is one place to correct if a rewrite ever grows\n * a field that makes two of them distinguishable.\n */\nfunction sortByScopeDepth(rewrites: InterceptionRewrite[]): InterceptionRewrite[] {\n return rewrites.sort(\n (a, b) => treePathDepth(b.interceptingScope) - treePathDepth(a.interceptingScope)\n );\n}\n\n/**\n * Validate that every intercepting segment sits where it can actually render.\n *\n * There are exactly two positions a renderer exists for, and they are the two\n * shapes interception has:\n *\n * 1. **Direct child of a parallel slot** — the modal overlay. The slot\n * resolver scans a slot node's *direct children* for an intercepting\n * segment (`server/slot-interception.ts`). Nothing between the slot and\n * the intercepting directory is allowed, because nothing walks it.\n * 2. **Direct child of an ordinary segment or route group** — the sibling\n * layout swap (TIM-1280). The pipeline resolves the owner by tree path and\n * builds the rendered chain through the intercepting node itself\n * (`server/children-interception.ts`).\n *\n * Everywhere else, rewrite generation still emits a rewrite and nothing\n * renders it: the rewrite matches the soft navigation, and the user is served\n * the page they were already on under a changed URL (TIM-1278). The rejected\n * positions are:\n *\n * - **Inside a slot subtree but not directly under the slot**\n * (`@modal/(inner)/(.)photo`). Neither renderer applies: the slot resolver\n * only reads direct children, and the children-path resolver builds a\n * *chain*, which cannot express a descent through a slot.\n * - **Inside another intercepting subtree** (`(.)a/(.)b`). Left unvalidated\n * `collectLeavesWithRewrites` walks the inner one as an ordinary URL\n * segment and emits a pattern containing a literal \"(.)b\".\n *\n * Two further shapes are rejected here (TIM-1288). Both are about a catch-all,\n * and both are specific to interception: elsewhere in the tree the same\n * shapes are inert or fail safely, and the general \"nothing below a catch-all\n * is reachable\" rule is tracked as TIM-1286.\n *\n * And one is specific to the children path: an owner chain that binds params\n * (see `assertOwnerChainBindsNoParams`).\n */\nexport function validateInterceptionPlacement(root: SegmentNode): void {\n walkForPlacement(root, '', {\n parentIsSlot: false,\n inSlotSubtree: false,\n inInterceptingSubtree: false,\n ancestorsBindParams: false,\n });\n}\n\n/**\n * Reject a children-path interception whose owner chain binds params.\n *\n * The rendered chain is `[…owner ancestors, interceptingNode, …descendants]`,\n * and the only pathname available to bind params from is the **target** — the\n * URL being navigated to. The intercepting subtree's own params come from it\n * by construction. The owner chain's do not, in general:\n *\n * - With a climbing marker the owner's URL segments are not in the target at\n * all. `app/[team]/(..)photo/[id]` bases at \"/\", so the target is\n * `/photo/9` — there is no team value anywhere in it, and `[team]`'s layout\n * would render with the param it declares missing.\n * - With `(.)` the base *is* the owner's `urlPath`, so the target's leading\n * parts would bind it. Supporting only that half means one marker behaves\n * differently from its siblings for reasons the author cannot see from the\n * directory names, so the rule is stated once, on the owner chain.\n *\n * A layout that silently renders without the param it declares is the failure\n * this prevents; it is rejected where it can be explained rather than left to\n * surface as `undefined` deep inside user code. Route groups and static\n * segments bind nothing and are unrestricted, which is the whole motivating\n * shape (`app/(browse)/(...)[artistSlug]/…`).\n *\n * The rule stops at a slot boundary: a slot-mode interception re-matches the\n * *source* route, which supplies the owner's params, so a dynamic owner is\n * fine there and always has been.\n */\n/**\n * Require a children-path interception's owner to have a `layout.tsx`.\n *\n * The scope is the owner's tree path, and the client reports which tree paths\n * it has mounted from `X-Timber-Segments` — which carries **one entry per\n * layout-bearing segment**, because those are the segments the client needs an\n * outlet and a cache node for. `buildSegmentInfo` skips the rest.\n *\n * For the modal shape that set is exactly the set of possible scope owners, by\n * construction: a slot is only resolved inside a rendering layout\n * (`route-element-builder.ts`), so a layoutless segment can never own one. A\n * children-path interception broke that equivalence — its owner is an ordinary\n * directory and needs no layout.\n *\n * Left unchecked the result is a swap that works exactly once: the first soft\n * navigation renders, then the client reports no such tree path, and the next\n * navigation fails the scope check and falls back to the canonical page. That\n * is fail-safe but silent, and the address-bar channel cannot recover it —\n * once the swap is on screen the address holds the *intercepted* URL, which\n * re-matches the canonical chain (the TIM-1282 problem, in the one shape the\n * rendered-scope fix does not reach).\n *\n * Requiring the layout restores the invariant the state tree is built on\n * rather than widening the wire format to carry segments that own no outlet\n * and no payload. It also costs the author nothing they were not already\n * doing: the point of this shape is to render inside the owner's own layout,\n * so an owner with no layout has no chrome to preserve.\n */\nfunction assertOwnerHasLayout(ownerPath: string, childPath: string): void {\n const owner = ownerPath || '(app root)';\n throw new Error(\n `Build error: intercepting route whose owner has no layout.\\n` +\n ` Directory: app/${childPath}\\n` +\n ` Owner: app/${owner}\\n` +\n `An intercepting directory outside a parallel slot renders inside its owner's ` +\n `layout chain, and the owner must have a layout.tsx: the client reports which ` +\n `directories it has mounted from its layout segments, and a scope it cannot ` +\n `report would let the interception fire once and then silently stop.\\n` +\n `Add app/${owner}/layout.tsx (a pass-through returning {children} is enough), or ` +\n `move the interception under a directory that has one.\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n}\n\nfunction assertOwnerChainBindsNoParams(childPath: string, marker: InterceptionMarker): void {\n throw new Error(\n `Build error: intercepting route under a dynamic segment.\\n` +\n ` Directory: app/${childPath}\\n` +\n `An intercepting directory outside a parallel slot renders inside its own ` +\n `layout chain, and the only pathname available to bind params from is the ` +\n `intercepted URL — which does not carry the values of the segments above ` +\n `\"${marker}\".\\n` +\n `Move the interception so every directory above it is a static segment or a ` +\n `route group, or place it inside a parallel slot (@modal/) to render it as an ` +\n `overlay over the source route instead.\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n}\n\n/**\n * Reject an intercepting route whose intercepted level is not well-defined.\n *\n * \"One level up\" is counted in URL segments, which works because every\n * pattern part stands for exactly one URL part — except a catch-all, which\n * stands for a number that varies per request. `anchorIndex` fixes the anchor\n * at the base's pattern length, so with an owner at `/docs/[...path]` the\n * interception resolves when the catch-all consumed exactly one segment\n * (`/docs/a/photo/5`) and is dropped when it consumed more\n * (`/docs/a/b/photo/5`), because the base no longer matches the target's\n * leading parts.\n *\n * Dropping is fail-safe — the canonical route renders, as on hard navigation\n * — so this is never a wrong page. But it is a modal that opens at one depth\n * and silently does not at another, with nothing to tell the author why, and\n * no request-time input can fix it: the only available source depth is\n * `X-Timber-URL`, which is the browser's address bar, and once a modal is\n * open that holds the *intercepted* URL rather than the source.\n *\n * So the shape is rejected where it can be explained. A marker that climbs\n * *past* the catch-all is fine — `(..)` from `/docs/[...path]` bases at\n * `/docs` — and so is `(...)`, which always bases at the root. The rule is on\n * the *computed* base, not on having a catch-all anywhere above.\n */\nfunction assertInterceptedBaseIsCountable(\n child: SegmentNode,\n ownerUrlPath: string,\n childPath: string\n): void {\n const marker = child.interceptionMarker ?? '(.)';\n const base = computeInterceptedBase(ownerUrlPath, marker);\n const baseParts = base === '/' ? [] : base.slice(1).split('/');\n const ambiguous = baseParts.find((part) => {\n const kind = classifyUrlSegment(part).kind;\n return kind === 'catch-all' || kind === 'optional-catch-all';\n });\n if (!ambiguous) return;\n\n throw new Error(\n `Build error: intercepting route below a catch-all segment.\\n` +\n ` Directory: app/${childPath}\\n` +\n ` Intercepted base: ${base}\\n` +\n `A catch-all (${ambiguous}) matches a number of URL segments that varies per ` +\n `request, so \"${marker}\" cannot identify which segment of a target pathname the ` +\n `interception starts at — the modal would open at one depth and silently not at ` +\n `another.\\n` +\n `Move the interception above the catch-all, or use a marker that climbs past it ` +\n `((..) or (...)).\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n}\n\n/**\n * Reject a catch-all inside an intercepting subtree that has directories\n * below it.\n *\n * A catch-all consumes the whole remaining path, so nothing under it is\n * reachable — `matchUrlParts` only accepts a catch-all as a terminal leaf and\n * never descends past one. Rewrite generation is not so strict: it walks the\n * subtree and emits `/feed/[...rest]/child`, and `pathnameMatchesPattern`\n * returns as soon as it reaches the catch-all without ever looking at\n * `child`. So the rewrite matches the navigation and then no renderer exists\n * — the TIM-1278 failure mode reached by a different route, with the user\n * served the page they were already on under a changed URL.\n *\n * The check covers the whole intercepting subtree, not just the intercepted\n * segment: `(.)photo/[...rest]/child` emits `/feed/photo/[...rest]/child` and\n * hijacks `/feed/photo/a/b` by exactly the same mechanism. And it does not\n * exempt a catch-all that carries its own page — that one does render, but\n * the rewrite generated for the unreachable descendant is broader than any\n * route in the tree, so the tail modal fires on navigations no directory\n * describes.\n *\n * Outside a slot the same shape merely 404s, which is why the general rule is\n * tracked separately (TIM-1286); here it is unsafe, and it only became\n * reachable when catch-all intercepted segments started matching at all\n * (TIM-1281).\n *\n * The walk descends `children` and not `slots`, mirroring\n * `collectLeavesWithRewrites` exactly: what this rejects is precisely the set\n * of paths rewrite generation emits, so the two cannot disagree about which\n * shapes exist. A slot nested inside an intercepting subtree produces no\n * rewrite and so cannot hijack anything.\n */\nfunction assertInterceptedCatchAllsAreTerminal(node: SegmentNode, nodePath: string): void {\n const { segmentType, segmentName } = effectiveUrlSegment(node);\n const isCatchAll = segmentType === 'catch-all' || segmentType === 'optional-catch-all';\n\n if (isCatchAll && node.children.length > 0) {\n throw new Error(\n `Build error: catch-all with nested segments inside an intercepting route.\\n` +\n ` Directory: app/${nodePath}\\n` +\n ` Nested: ${node.children.map((c) => c.segmentName).join(', ')}\\n` +\n `A catch-all (${segmentName}) consumes every remaining URL segment, so nothing ` +\n `below it can ever match — but the interception rewrite would still be generated ` +\n `and would hijack the navigation with no page to render.\\n` +\n `Move the nested routes above the catch-all, or intercept a named segment instead.\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n }\n\n for (const child of node.children) {\n assertInterceptedCatchAllsAreTerminal(child, `${nodePath}/${child.segmentName}`);\n }\n}\n\n/**\n * Reject a `route.ts` anywhere inside an intercepting subtree.\n *\n * An interception is a soft-navigation variant of a page — it swaps which\n * page component renders. A route handler returns a `Response` and cannot\n * be an interception target: `walkInterceptedLeaves` visits `node.page`\n * only, so no rewrite is emitted for it, but `isTerminal` in\n * `tree-match.ts` counts both `page` and `route`, so `matchUrlParts` can\n * resolve to the route handler. `slot-interception.ts` then drops the\n * match (`if (!leaf.page) continue`), silently disabling the page's\n * interception. Fail-safe (the canonical route renders) but silent.\n *\n * The walk mirrors `assertInterceptedCatchAllsAreTerminal`: it descends\n * `children` (not `slots`), matching `collectLeavesWithRewrites` exactly.\n */\nfunction assertNoRouteHandlersInSubtree(node: SegmentNode, nodePath: string): void {\n if (node.route) {\n throw new Error(\n `Build error: route handler inside an intercepting route.\\n` +\n ` File: ${node.route.filePath}\\n` +\n ` Directory: app/${nodePath}\\n` +\n `An intercepting subtree renders a page component on soft navigation — a ` +\n `route handler returns a Response and cannot be an interception target. ` +\n `The route handler would silently shadow the interception: the resolver ` +\n `would match it and then discard it because it has no page, so the modal ` +\n `would stop opening with no diagnostic.\\n` +\n `Remove the route.ts, or move it outside the intercepting subtree.\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n }\n\n for (const child of node.children) {\n assertNoRouteHandlersInSubtree(child, `${nodePath}/${child.segmentName}`);\n }\n}\n\n/**\n * Reject two pages that answer one URL among the interceptors an owner\n * resolves in a single walk.\n *\n * `validateRouteGroupCollisions` in the scanner returns early on an\n * intercepting node, so nothing else validates in here. That exemption exists\n * for a real reason — an interception's computed URL legitimately mirrors the\n * canonical route it replaces, and interceptors at *different* anchors\n * deliberately target one URL, which the resolver then orders (deepest anchor\n * first). But the exemption was broader than its reason. Two shapes slip\n * through it, and they are the same shape seen from two levels:\n *\n * feed/(.)photo/(a)/page.tsx feed/@modal/(.)[a]/page.tsx\n * feed/(.)photo/(b)/page.tsx feed/@modal/(.)[b]/page.tsx\n *\n * Both pairs answer one URL, and in both `matchUrlParts` resolves the tie by\n * whichever `children` array lists first — so the winner is whichever the\n * filesystem enumerated first, and renaming a directory flips it. That is\n * exactly what `validateRouteGroupCollisions` and\n * `validateAffixedSiblingConflicts` exist to reject for ordinary siblings.\n *\n * **The collision space is the anchor group, not the directory.** An earlier\n * revision ran once per intercepting directory, which sees the left-hand pair\n * and not the right-hand one: those leaves live in two different directories.\n * The space is `groupByInterceptedAnchor`'s output — the same grouping\n * `findInterceptingMatch` hands to one `matchUrlParts` call — so what is\n * validated together is exactly what is resolved together. Interceptors at\n * different anchors stay independent, which is the documented case, and two\n * slots on one owner are two owners and so two spaces.\n *\n * **Two checks, and the difference between them matters.** A duplicate\n * intercepted *pattern* is decided by string equality, which is exact: two\n * leaves with one pattern claim one set of URLs, whatever the precedence rules\n * are. Everything else is decided by asking the matcher — `collision-probe.ts`,\n * the same oracle the main tree and every slot use, so the spaces cannot\n * disagree about whether a pair of pages collides.\n *\n * Not covered: two affixed dynamics that tie on specificity (`(x)/a-[id]` vs\n * `(y)/[slug]-b`, ambiguous only for a part shaped `a-…-b`). No single probe\n * URL exercises both, and `validateAffixedSiblingConflicts` already rejects\n * that pair by affix length — including through groups, inside intercepting\n * subtrees, and between two interceptors sharing an anchor.\n *\n * The shape applies to both interception mechanisms — a slot interceptor and a\n * children-path one produce their leaves through the same walk, and both are\n * resolved by `findInterceptingMatch` — so this is called from the one place in\n * `walkForPlacement` that sees every owner regardless of position.\n */\nfunction assertNoInterceptedPatternCollisions(owner: SegmentNode, ownerPath: string): void {\n for (const group of groupByInterceptedAnchor(owner.urlPath, owner.children)) {\n assertAnchorGroupHasNoCollisions(group, ownerPath);\n }\n}\n\n/**\n * Reject a children-path interceptor and a slot interceptor from the same owner\n * that claim URLs in common.\n *\n * Within one mode the ambiguity is either non-existent (two children-path\n * rewrites are interchangeable, §\"Selection Between Owners\") or harmless (two\n * slots on one owner both render). Across modes one is a layout swap and the\n * other is a modal overlay, and the walk order picks the children-path shape —\n * making the modal dead, silently and deterministically (TIM-1319).\n *\n * Overlap is a **pattern intersection** test: do two intercepted URL patterns\n * share any concrete URL? This is not the same question the within-mode\n * collision oracle answers. `findOrderDependentCollision` checks whether two\n * candidates in one `matchUrlParts` walk produce different winners when\n * `children` order is reversed — that is, order-dependence. A static segment\n * always beats a dynamic one deterministically (no order-dependence), so\n * `/feed/foo` and `/feed/[id]` pass the oracle even though\n * `pathnameMatchesPattern` — the actual rewrite matcher — accepts both. The\n * pattern intersection test catches that: it walks both patterns segment by\n * segment and asks whether any value satisfies both.\n *\n * The check collects intercepted patterns from children-path interceptors and\n * from each slot's interceptors independently. Slots are checked one at a time\n * so two slot interceptors from *different* slots claiming one URL — the legal\n * \"two slots on one owner\" case — are never compared.\n */\nfunction assertNoCrossModeClaims(node: SegmentNode, ownerPath: string): void {\n // Collect children-path interceptor patterns\n const childrenLeaves: Array<{ pattern: string; filePath: string }> = [];\n for (const child of node.children) {\n if (\n child.segmentType !== 'intercepting' ||\n !child.interceptionMarker ||\n !child.interceptedSegmentName\n )\n continue;\n const urlBase = computeInterceptedUrlBase(\n node.urlPath,\n child.interceptionMarker,\n child.interceptedSegmentName\n );\n walkInterceptedLeaves(child, urlBase, [child], (page, leaf) => {\n childrenLeaves.push({ pattern: leaf.interceptedPattern, filePath: page.filePath });\n });\n }\n\n if (childrenLeaves.length === 0) return;\n\n const owner = ownerPath || '(app root)';\n\n for (const slot of Object.values(node.slots)) {\n const slotLeaves: Array<{ pattern: string; filePath: string }> = [];\n for (const child of slot.children) {\n if (\n child.segmentType !== 'intercepting' ||\n !child.interceptionMarker ||\n !child.interceptedSegmentName\n )\n continue;\n const urlBase = computeInterceptedUrlBase(\n node.urlPath,\n child.interceptionMarker,\n child.interceptedSegmentName\n );\n walkInterceptedLeaves(child, urlBase, [child], (page, leaf) => {\n slotLeaves.push({ pattern: leaf.interceptedPattern, filePath: page.filePath });\n });\n }\n\n for (const cl of childrenLeaves) {\n for (const sl of slotLeaves) {\n const overlap = interceptedPatternsOverlap(cl.pattern, sl.pattern);\n if (overlap !== null) {\n throwCrossModeClaim(owner, overlap, cl.filePath, sl.filePath);\n }\n }\n }\n }\n}\n\nfunction throwCrossModeClaim(\n owner: string,\n url: string,\n childFile: string,\n slotFile: string\n): never {\n throw new Error(\n `Build error: a children-path interception and a slot interception both ` +\n `claim the same URL.\\n` +\n ` Owner: app/${owner}\\n` +\n ` URL: ${url}\\n` +\n ` Children: ${childFile}\\n` +\n ` Slot: ${slotFile}\\n` +\n `The children-path interceptor replaces the owner's children (layout swap) ` +\n `while the slot interceptor renders inside a parallel slot (modal overlay). ` +\n `Both apply from the same scope at the same URL, and which one wins would ` +\n `depend on internal walk order rather than an explicit rule.\\n` +\n `Remove one of the two, or move them to different scopes.\\n` +\n `See design/07-routing.md §\"Selection Between Owners\".`\n );\n}\n\n/**\n * The URL-matching identity of an intercepting node, as a node.\n *\n * `matchUrlParts` reads `segmentType` directly and knows nothing about\n * interception, so a marker'd directory has to be handed to it under the\n * identity of the segment it intercepts — `(.)[id]` as `[id]`. Priority\n * ordering, affix stripping and catch-all handling then come from the shared\n * walker rather than from a second classifier here.\n *\n * Shared with `findInterceptingMatch` for the same reason the grouping is: the\n * collision space is the resolver's synthetic anchor parent, so its children\n * have to carry the identity the resolver gives them or the validator would be\n * probing a tree that does not match the one served. The caller keeps its own\n * map back to the real node — the proxy is a matching identity, not a\n * replacement (tree paths still need the directory name).\n */\nexport function interceptorProxy<TFile>(node: SegmentNode<TFile>): SegmentNode<TFile> {\n return { ...node, ...effectiveUrlSegment(node) };\n}\n\nfunction assertAnchorGroupHasNoCollisions(\n group: InterceptorAnchorGroup<RouteFile>,\n ownerPath: string\n): void {\n const { base } = group;\n const owner = ownerPath || '(app root)';\n\n // The space the resolver walks: a synthetic parent sitting at the anchor\n // base, holding every interceptor of this group under its matching identity.\n // `matchUrlParts` starts below this node, so the interceptors' own segments\n // are matched parts here — which is what lets a probe reach across two\n // directories and find the pair that competes.\n const proxies = group.interceptors.map(interceptorProxy);\n const anchorNode: SegmentNode = {\n segmentName: '',\n segmentType: 'static',\n urlPath: base,\n children: proxies,\n slots: {},\n };\n\n const patterns = new Map<string, string>();\n const probeLeaves: ProbeLeaf[] = [];\n\n for (const proxy of proxies) {\n // The proxy's name, not the directory's `interceptedSegmentName`: they are\n // the same string, and this is the one `matchUrlParts` matches on, so the\n // reported pattern and the probed URL cannot describe different segments.\n const pattern = joinInterceptedBase(base, proxy.segmentName);\n walkInterceptedLeaves(proxy, pattern, [proxy], (page, leaf) => {\n // Two leaves whose intercepted pattern is the *same string* claim exactly\n // the same set of URLs — one is ambiguous with the other, or dead. This\n // needs no probe and no reasoning about precedence: identical patterns are\n // identical URL sets by definition. It is the case route groups make easy\n // to write, `(a)/page.tsx` beside `(b)/page.tsx`, and the one the main\n // tree already rejects by comparing urlPath. Across two interceptors it\n // is `(..)photo` beside `(...)photo` under `/feed`, which both base at\n // `/` and both intercept the literal `photo`.\n const duplicate = patterns.get(leaf.interceptedPattern);\n if (duplicate) {\n throw new Error(\n `Build error: route collision between intercepting routes — two pages intercept the same URL.\\n` +\n ` Owner: app/${owner}\\n` +\n ` Intercepted base: ${base}\\n` +\n ` URL: ${leaf.interceptedPattern}\\n` +\n ` File 1: ${duplicate}\\n` +\n ` File 2: ${page.filePath}\\n` +\n `A route group is transparent for matching, so moving a page into one does not ` +\n `give it a URL of its own — these two claim the same one. Rename or move one of ` +\n `them.\\n` +\n `See design/07-routing.md §\"Collisions Among Intercepting Routes\".`\n );\n }\n patterns.set(leaf.interceptedPattern, page.filePath);\n probeLeaves.push({ filePath: page.filePath, chain: leaf.chain });\n });\n }\n\n // Differently-spelled patterns that nonetheless compete: ask the matcher.\n const collision = findOrderDependentCollision(anchorNode, probeLeaves, base);\n if (collision) {\n throw new Error(\n `Build error: route collision between intercepting routes — two pages answer the same URL.\\n` +\n ` Owner: app/${owner}\\n` +\n ` Intercepted base: ${base}\\n` +\n ` URL: ${collision.url}\\n` +\n ` File 1: ${collision.first}\\n` +\n ` File 2: ${collision.second}\\n` +\n `Both match it, so which one renders would be decided by filesystem enumeration ` +\n `order — renaming a route group can flip it. Rename or move one of them.\\n` +\n `Pages are compared by the URLs they match, not by how they are spelled: [id] and ` +\n `[slug] match the same URLs, and an optional catch-all also matches with zero ` +\n `segments.\\n` +\n `Intercepting directories based at *different* URLs may target one page — the ` +\n `resolver tries the deepest first — so this rejects only an ambiguity among the ` +\n `ones based at ${base}.\\n` +\n `See design/07-routing.md §\"Collisions Among Intercepting Routes\".`\n );\n }\n}\n\n/** What the walk knows about the position it is currently standing in. */\ninterface PlacementContext {\n /** `node` is itself a slot node, so its children are direct slot children. */\n parentIsSlot: boolean;\n /** `node` is at or below a slot node. */\n inSlotSubtree: boolean;\n /** `node` is inside an intercepting subtree. */\n inInterceptingSubtree: boolean;\n /** Some segment from the root down to `node`, inclusive, binds a param. */\n ancestorsBindParams: boolean;\n}\n\nfunction walkForPlacement(node: SegmentNode, path: string, ctx: PlacementContext): void {\n for (const child of node.children) {\n const childPath = path ? `${path}/${child.segmentName}` : child.segmentName;\n\n if (child.segmentType === 'intercepting') {\n const marker = child.interceptionMarker ?? '(.)';\n\n if (ctx.inInterceptingSubtree) {\n throw new Error(\n `Build error: intercepting route nested inside another intercepting route.\\n` +\n ` Directory: app/${childPath}\\n` +\n `An intercepting directory (${marker}) resolves against the URL its own parent ` +\n `renders at, and an intercepting subtree has no such URL — nothing walks it, so ` +\n `the rewrite would match the navigation and then have nothing to render.\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n }\n\n // Inside a slot, only a *direct* slot child is reachable: the slot\n // resolver reads `slotNode.children` and stops. The children-path\n // renderer cannot pick it up either — it builds a segment chain, which\n // has no way to express a descent through a slot.\n if (ctx.inSlotSubtree && !ctx.parentIsSlot) {\n throw new Error(\n `Build error: intercepting route inside a parallel slot but not directly under it.\\n` +\n ` Directory: app/${childPath}\\n` +\n `An intercepting directory (${marker}) inside an @slot must be a *direct* child of ` +\n `that slot, e.g. app/feed/@modal/${child.segmentName}/page.tsx. The slot resolver ` +\n `scans the slot's direct children only; elsewhere the rewrite would match the ` +\n `navigation and then have nothing to render.\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n }\n\n // Outside a slot the intercepting page replaces `children` for the\n // owner's own layout chain, so those layouts render against the\n // intercepted URL — which carries no values for params declared above\n // the interception.\n if (!ctx.parentIsSlot && ctx.ancestorsBindParams) {\n assertOwnerChainBindsNoParams(childPath, marker);\n }\n\n if (!ctx.parentIsSlot && !node.layout) {\n assertOwnerHasLayout(path, childPath);\n }\n\n // `node` is the owner — the slot (whose urlPath is the owning segment's,\n // since slots add no URL depth) or the ordinary segment holding the\n // interception. The same input rewrite generation bases from.\n assertInterceptedBaseIsCountable(child, node.urlPath, childPath);\n assertInterceptedCatchAllsAreTerminal(child, childPath);\n assertNoRouteHandlersInSubtree(child, childPath);\n }\n\n walkForPlacement(child, childPath, {\n parentIsSlot: false,\n // A slot subtree stays a slot subtree all the way down: the deeper\n // positions are exactly the ones neither renderer reaches.\n inSlotSubtree: ctx.inSlotSubtree,\n inInterceptingSubtree: ctx.inInterceptingSubtree || child.segmentType === 'intercepting',\n ancestorsBindParams: ctx.ancestorsBindParams || !!child.paramName,\n });\n }\n\n // After every child has been recursed into, so a placement error inside any\n // intercepting subtree is reported in preference to what it causes here — a\n // nested interception leaves a node `matchUrlParts` cannot match at all, and\n // the resulting nonsense below it is not what the author needs to be told\n // about. This is per *owner* rather than per intercepting directory because\n // the collision space is the anchor group; see\n // `assertNoInterceptedPatternCollisions`.\n assertNoInterceptedPatternCollisions(node, path);\n\n for (const [slotName, slotNode] of Object.entries(node.slots)) {\n walkForPlacement(slotNode, path ? `${path}/@${slotName}` : `@${slotName}`, {\n parentIsSlot: true,\n inSlotSubtree: true,\n inInterceptingSubtree: ctx.inInterceptingSubtree,\n // A slot-mode interception never binds owner params from the target —\n // the pipeline re-matches the source route, which supplies them — so the\n // rule stops at the slot boundary.\n ancestorsBindParams: false,\n });\n }\n\n // After both children and slots have been validated independently, check for\n // the cross-mode ambiguity that neither space covers: a children-path\n // interceptor and a slot interceptor from the same owner claiming one URL.\n // Within each mode the collision is either ordered (two children-path) or\n // harmless (two slots) — see §\"Selection Between Owners\" — but across modes\n // one is an overlay and the other a layout swap, and the walk order picks the\n // children-path shape silently (TIM-1319).\n assertNoCrossModeClaims(node, path);\n}\n\n/**\n * Recursively walk the segment tree to find intercepting routes.\n *\n * `ancestors` deliberately excludes slot nodes: a slot doesn't appear in a\n * matched route chain, so the last entry is always the nearest chain-visible\n * ancestor — the segment that owns the slot, and therefore the segment whose\n * presence in the source route gates the interception.\n *\n * Intercepting nodes only ever appear as a direct child of a slot or of an\n * ordinary chain segment in a valid tree; `validateInterceptionPlacement`\n * rejects every other placement at scan time, so this walk never has to\n * decide what an unrenderable intercepting node should generate.\n *\n * `parentIsSlot` is what distinguishes the two shapes, and it is the only\n * input that does: the owner, the scope and the intercepted pattern are all\n * computed identically either way.\n */\nfunction walkForInterceptions(\n node: SegmentNode,\n ancestors: SegmentNode[],\n parentIsSlot: boolean,\n rewrites: InterceptionRewrite[]\n): void {\n // Check children\n for (const child of node.children) {\n if (child.segmentType === 'intercepting' && child.interceptionMarker) {\n // Found an intercepting route — collect rewrites from its sub-tree\n collectFromInterceptingNode(child, ancestors, parentIsSlot, rewrites);\n } else {\n walkForInterceptions(child, [...ancestors, child], false, rewrites);\n }\n }\n\n // Check slots (intercepting routes are typically inside slots like @modal)\n for (const slot of Object.values(node.slots)) {\n walkForInterceptions(slot, ancestors, true, rewrites);\n }\n}\n\n/**\n * For an intercepting segment, find all leaf pages in its sub-tree and\n * generate rewrite rules for each.\n */\nfunction collectFromInterceptingNode(\n interceptingNode: SegmentNode,\n ancestors: SegmentNode[],\n parentIsSlot: boolean,\n rewrites: InterceptionRewrite[]\n): void {\n const marker = interceptingNode.interceptionMarker!;\n const segmentName = interceptingNode.interceptedSegmentName!;\n\n // Compute the intercepted URL base based on the marker\n const parentUrlPath = ancestors[ancestors.length - 1].urlPath;\n const interceptedUrlBase = computeInterceptedUrlBase(parentUrlPath, marker, segmentName);\n\n // The scope key and the re-match prefix both describe the same node — the\n // interception's owner — so they are derived here together from one\n // ancestor chain.\n const ancestorPaths = computeSegmentTreePaths(ancestors);\n const interceptingScope = ancestorPaths[ancestorPaths.length - 1];\n\n // Find all leaf pages in the intercepting sub-tree\n const shape: InterceptionShape = parentIsSlot\n ? { mode: 'slot', interceptingPrefix: parentUrlPath, interceptingScope }\n : { mode: 'children', interceptingScope };\n collectLeavesWithRewrites(interceptingNode, interceptedUrlBase, shape, rewrites);\n}\n\n/**\n * Everything about a rewrite except which URL it intercepts — one intercepting\n * directory produces one of these and one rewrite per leaf page beneath it.\n *\n * Written as a union of `Omit`s rather than `Omit` of the union: `Omit` is not\n * distributive, so the latter would silently collapse to the shapes' common\n * fields and drop `interceptingPrefix`.\n */\ntype InterceptionShape =\n | Omit<SlotInterceptionRewrite, 'interceptedPattern'>\n | Omit<ChildrenInterceptionRewrite, 'interceptedPattern'>;\n\n/**\n * Walk an intercepting sub-tree and visit every page-bearing node with the URL\n * pattern it is intercepted at, and URLs that reach it.\n *\n * Route groups are transparent, exactly as they are in the main tree — which\n * is what makes two groups under one interceptor able to claim one URL, and is\n * why the collision validator has to walk the same shape this does.\n *\n * Rewrite generation and that validator are the two callers. They share this\n * walk rather than each having their own so they cannot disagree about which\n * URLs an interceptor claims: a leaf the validator did not see is a rewrite\n * nobody checked, and a leaf only the validator sees is a build error for a\n * route that does not exist. Slots are not descended, here or in either caller\n * — a slot nested inside an intercepting subtree produces no rewrite.\n *\n * That answers the rewrite question only, and the resolution question is\n * answered elsewhere: `validateSlotsInsideInterceptingSubtree`\n * (`collision-spaces.ts`) gives each such slot its own collision space, and\n * `validateSlotPlacement` (`slot-placement.ts`) rejects the ones no renderer\n * reaches. Between them the slots this walk skips are covered; do not conclude\n * from the skip here that they are unvalidated (TIM-1308, TIM-1311).\n *\n * The walk reports the chain of URL-contributing nodes rather than building\n * probe URLs itself: choosing a probe value means running the matcher over the\n * whole subtree, which is the validator's business and not something rewrite\n * generation should be paying for or passing dummy arguments around.\n */\nfunction walkInterceptedLeaves(\n node: SegmentNode,\n interceptedUrlPath: string,\n chain: SegmentNode[],\n visit: (page: NonNullable<SegmentNode['page']>, leaf: InterceptedLeaf) => void\n): void {\n if (node.page) {\n // The page is handed over rather than the node so callers reading it do\n // not need a non-null assertion to say what this branch already proved.\n visit(node.page, { interceptedPattern: interceptedUrlPath, chain });\n }\n\n for (const child of node.children) {\n const isGroup = child.segmentType === 'group';\n walkInterceptedLeaves(\n child,\n isGroup ? interceptedUrlPath : `${interceptedUrlPath}/${child.segmentName}`,\n isGroup ? chain : [...chain, child],\n visit\n );\n }\n}\n\n/** What one page-bearing node in an intercepting subtree claims. */\ninterface InterceptedLeaf {\n /** The URL pattern the rewrite carries — the author's own spelling. */\n interceptedPattern: string;\n /**\n * The nodes from the intercepting directory down to this page that contribute\n * URL parts — route groups excluded, since they are transparent for matching.\n * Empty for the intercepting directory's own page.\n */\n chain: SegmentNode[];\n}\n\n/**\n * Recursively find leaf pages in an intercepting sub-tree and generate\n * rewrite rules for each.\n */\nfunction collectLeavesWithRewrites(\n node: SegmentNode,\n interceptedUrlPath: string,\n shape: InterceptionShape,\n rewrites: InterceptionRewrite[]\n): void {\n walkInterceptedLeaves(node, interceptedUrlPath, [], (_page, leaf) => {\n rewrites.push({ ...shape, interceptedPattern: leaf.interceptedPattern });\n });\n}\n\n/**\n * How many URL levels a marker climbs from the slot owner to reach the\n * intercepted segment, or `'root'` for a marker that climbs all the way.\n *\n * Level counting operates on URL path segments, NOT filesystem directories.\n * Route groups and parallel slots are already excluded from urlPath (they\n * don't add URL depth), so (..) correctly climbs visible segments. This\n * avoids the Vinext bug where path.dirname() on filesystem paths would\n * waste climbs on invisible route groups.\n *\n * Used only by `computeInterceptedBase` below — both rewrite generation and\n * the slot resolver go through that, so the arithmetic exists once.\n */\nfunction interceptionClimb(marker: InterceptionMarker): number | 'root' {\n switch (marker) {\n case '(.)':\n return 0;\n case '(..)':\n return 1;\n case '(..)(..)':\n return 2;\n case '(...)':\n return 'root';\n }\n}\n\n/**\n * Compute the base URL that an intercepting route intercepts, given the\n * parent's URL path and the interception marker.\n *\n * - (.) — same level: parent's URL path\n * - (..) — one level up: parent's parent URL path\n * - (...) — root level: /\n * - (..)(..) — two levels up: parent's grandparent URL path\n *\n * The result is a URL **pattern**, not a URL — `parentUrlPath` carries the\n * directory names, so a dynamic ancestor appears as `/docs/[...path]`.\n * Rewrite generation wants exactly that. The slot resolver imports this to\n * locate the intercepted segment in a concrete target pathname, and must\n * therefore account for a bracket part standing for a different number of\n * URL parts than itself (TIM-1281).\n *\n * Climbing past the root clamps to `/` rather than erroring, so generation\n * and resolution never disagree about what an over-reaching marker\n * intercepts.\n */\n/**\n * The URL pattern an intercepting directory's own page sits at — its\n * `computeInterceptedBase` joined with the segment it intercepts.\n *\n * The root case is why this is a function rather than a template literal at\n * each caller: a base of `/` must not produce `//photo`. Rewrite generation\n * and the collision validator both start their walk from this value, so the\n * two describe the same URLs by construction.\n */\nexport function computeInterceptedUrlBase(\n parentUrlPath: string,\n marker: InterceptionMarker,\n interceptedSegmentName: string\n): string {\n return joinInterceptedBase(computeInterceptedBase(parentUrlPath, marker), interceptedSegmentName);\n}\n\n/**\n * An intercepted base joined with one segment beneath it.\n *\n * The whole content of the \"function rather than a template literal\" rule\n * above: a base of `/` must not produce `//photo`. The collision validator has\n * the base in hand already (it groups by it) and joins through this rather than\n * re-deriving it from an owner path, so both callers spell the root case once.\n */\nexport function joinInterceptedBase(base: string, segmentName: string): string {\n return base === '/' ? `/${segmentName}` : `${base}/${segmentName}`;\n}\n\nexport function computeInterceptedBase(parentUrlPath: string, marker: InterceptionMarker): string {\n const climb = interceptionClimb(marker);\n if (climb === 'root') return '/';\n\n const parts = parentUrlPath.split('/').filter(Boolean);\n for (let i = 0; i < climb; i++) parts.pop();\n return parts.length === 0 ? '/' : `/${parts.join('/')}`;\n}\n","/**\n * Route collision spaces.\n *\n * A collision space is a set of routes that compete for URLs with each other,\n * and there are three: the main tree, each parallel slot, and each intercepting\n * subtree. This module owns the first two; `interception.ts` owns the third.\n * All of them decide order-dependence with the one oracle in\n * `collision-probe.ts`, so they cannot drift on what counts as two routes\n * answering a single URL.\n *\n * See design/07-routing.md §\"Route Collisions\".\n */\n\nimport type { SegmentNode } from './types.ts';\nimport { computeInterceptedUrlBase } from './interception.ts';\nimport { findOrderDependentCollision, type ProbeLeaf } from './collision-probe.ts';\n\n/**\n * Validate that no two pages/routes in one collision space answer the same URL.\n *\n * Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim\n * /login — the scanner must detect and reject this at build time.\n *\n * Each parallel slot is validated as its OWN collision space: a slot's pages\n * intentionally share URL paths with the parent tree (that's the point of\n * parallel routes), but two route groups INSIDE one slot claiming the same\n * URL — e.g. @side/(a)/x/page.tsx + @side/(b)/x/page.tsx — still collide.\n */\nexport function validateRouteGroupCollisions(root: SegmentNode): void {\n validateCollisionSpace(root, '', root.urlPath);\n}\n\n/**\n * Validate one collision space — the main tree, or one parallel slot.\n *\n * **Two checks, and the difference between them matters.** Two leaves at the\n * same `urlPath` are decided by string equality, which is exact and needs no\n * probing: `(auth)/login` and `(marketing)/login` claim one set of URLs\n * whatever the precedence rules are, and naming the shared path is the clearest\n * thing to tell the author.\n *\n * Everything else is decided by asking the matcher, through the shared oracle\n * in `collision-probe.ts` (TIM-1308). Comparing `urlPath` spellings misses\n * every pair that matches the same URLs while being *spelled* differently:\n * `x/(a)/[id]` beside `x/(b)/[slug]` are two bare dynamics in one priority\n * tier, and `x/(a)/page.tsx` beside `x/(b)/[[...rest]]` both answer `/x`\n * because an optional catch-all also matches zero parts. Both built before\n * TIM-1308 with filesystem enumeration order deciding which page renders,\n * which is precisely what this validator and `validateAffixedSiblingConflicts`\n * exist to reject.\n *\n * Segment *kinds* stay distinct without any special-casing here, because the\n * oracle inherits `matchNode`'s priority ordering: `x/(a)/[id]` beside\n * `x/(b)/[...rest]` resolves the same way in either `children` order, so it is\n * not reported. So does an affixed sibling against a bare one; the\n * equal-specificity affix tie is `validateAffixedSiblingConflicts`' job, and\n * duplicating it here would double-report.\n */\nfunction validateCollisionSpace(spaceRoot: SegmentNode, segmentPath: string, base: string): void {\n const leaves: ProbeLeaf[] = [];\n collectRoutableLeaves(spaceRoot, new Map(), segmentPath, [], leaves, base);\n\n const collision = findOrderDependentCollision(spaceRoot, leaves, base);\n if (!collision) return;\n\n throw new Error(\n `Build error: route collision — two pages/routes answer the same URL.\\n` +\n ` URL: ${collision.url}\\n` +\n ` File 1: ${collision.first}\\n` +\n ` File 2: ${collision.second}\\n` +\n `Both match it, so which one renders would be decided by filesystem enumeration ` +\n `order — renaming a route group can flip it. Rename or move one of them.\\n` +\n `Routes are compared by the URLs they match, not by how they are spelled: [id] and ` +\n `[slug] match the same URLs, and an optional catch-all also matches with zero ` +\n `segments.`\n );\n}\n\n/**\n * Walk one collision space and collect its routable leaves (page or route\n * files), throwing on an exact `urlPath` collision. Each slot recurses as its\n * OWN space — slots are parallel routes that intentionally share URL paths with\n * their parent tree, but collisions within a single slot are real errors.\n *\n * The early return at an intercepting node drops that subtree's own pages from\n * this space, but not its slots: a slot is its own collision space wherever it\n * sits, so `validateSlotsInsideInterceptingSubtree` picks those up before\n * returning.\n *\n * `chain` accumulates the URL-contributing nodes from the space root down, for\n * the probe oracle. Route groups are excluded because they are transparent for\n * matching, and the space root itself is excluded because `matchUrlParts`\n * matches parts against its *children* — the root consumes nothing.\n */\nfunction collectRoutableLeaves(\n node: SegmentNode,\n seen: Map<string, { filePath: string; segmentPath: string }>,\n segmentPath: string,\n chain: SegmentNode[],\n leaves: ProbeLeaf[],\n base: string\n): void {\n // Intercepting routes are resolved by the interception resolvers, not URL\n // matching, and their computed urlPaths are not addresses — an interception\n // mirrors the canonical route it replaces, so comparing those paths here\n // would reject every correct interception. Skipping is not \"unvalidated\":\n // `assertNoInterceptedPatternCollisions` gives each intercepting subtree its\n // own collision space, keyed by the intercepted pattern rather than by these\n // paths (TIM-1305). See design/07-routing.md §\"Collisions Inside an\n // Intercepting Subtree\" and §\"Why urlPath Stays Parent-Relative Inside an\n // Intercepting Subtree\".\n const currentPath = segmentPath\n ? `${segmentPath}/${node.segmentName}`\n : node.segmentName || '(root)';\n\n // The intercepting subtree's own pages leave this space — but its SLOTS do\n // not leave every space, so they are picked up before returning. See\n // `validateSlotsInsideInterceptingSubtree`.\n if (node.segmentType === 'intercepting') {\n // `node.urlPath` on an intercepting node is its OWNER's path — the input\n // `computeInterceptedUrlBase` wants — because an intercepting segment adds\n // no URL depth.\n validateSlotsInsideInterceptingSubtree(\n node,\n currentPath,\n interceptedBaseOf(node, node.urlPath)\n );\n return;\n }\n\n const routableFile = node.page ?? node.route;\n if (routableFile) {\n const existing = seen.get(node.urlPath);\n if (existing) {\n throw new Error(\n `Build error: route collision — multiple route groups produce a page/route at the same URL path.\\n` +\n ` URL path: ${urlFromChain(base, chain)}\\n` +\n ` File 1: ${existing.filePath} (via ${existing.segmentPath})\\n` +\n ` File 2: ${routableFile.filePath} (via ${currentPath})\\n` +\n `Each URL path must map to exactly one page or route handler. ` +\n `Rename or move one of the conflicting files.`\n );\n }\n seen.set(node.urlPath, { filePath: routableFile.filePath, segmentPath: currentPath });\n leaves.push({ filePath: routableFile.filePath, chain });\n }\n\n // Recurse into children. A route group contributes no URL part, so it is\n // absent from the chain — exactly as `collectEffectiveChildren` flattens it\n // away in the matcher.\n for (const child of node.children) {\n const isGroup = child.segmentType === 'group';\n collectRoutableLeaves(\n child,\n seen,\n currentPath,\n isGroup ? chain : [...chain, child],\n leaves,\n base\n );\n }\n\n // Recurse into slots — each slot is its own collision space, with its own\n // seen map, its own leaf set and its own probe run.\n //\n // The base is accumulated from this space's own base rather than read off\n // `slotNode.urlPath`, because this walk also runs on slot spaces nested inside\n // an intercepting subtree, where `urlPath` is parent-relative and not an\n // address. Outside one the two are identical by construction.\n for (const slotNode of Object.values(node.slots)) {\n validateCollisionSpace(slotNode, currentPath, urlFromChain(base, chain));\n }\n}\n\n/**\n * Validate every parallel slot nested anywhere inside an intercepting subtree.\n *\n * Two walks each skip these slots for their own good reason, and together they\n * left them checked by nothing: `collectRoutableLeaves` returns at the\n * intercepting node before it reaches them, and `walkInterceptedLeaves`\n * deliberately does not descend slots — correct for rewrite generation, since a\n * slot inside an intercepting subtree produces no rewrite. But \"produces no\n * rewrite\" answers the rewrite question and not the resolution one, so\n * `@side/(a)/x/page.tsx` beside `@side/(b)/x/page.tsx` under an interceptor\n * built with filesystem order picking the page — the ordinary per-slot\n * collision that fails the build anywhere else in the tree (codex, on PR #1002).\n *\n * A slot is its own collision space wherever it sits, so each one is validated\n * as one.\n *\n * **Which interception shape this still applies to.** TIM-1311 answered the\n * question this was written under — whether such a slot renders at all — and\n * the answer is per-shape:\n *\n * - **Children path** (`(browse)/(...)photo/@side`): the intercepting node\n * joins the main route chain, so `route-element-builder.ts` resolves slots on\n * it and below it like any other chain segment. They render, and this is the\n * space where the check is load-bearing.\n * - **Modal** (`feed/@modal/(.)photo/@side`): the slot sits inside another\n * slot's subtree, which no renderer descends for slots — so it never renders,\n * and `validateSlotPlacement` now rejects the placement before this runs.\n * That rule subsumes this one for the modal shape rather than conflicting\n * with it, exactly as anticipated.\n *\n * So this walk is reached only for children-path interceptors in a valid tree.\n * It is not narrowed to them: the rule it states — a slot is its own collision\n * space wherever it sits — is shape-independent, and making it conditional on a\n * property the placement validator already guarantees would just be a second\n * place for the two to disagree.\n *\n * The walk covers the whole subtree, not just the interceptor's direct slot\n * children: `(...)photo/[id]/@side` is reached by neither walk for exactly the\n * same reason as `(...)photo/@side`.\n */\nfunction validateSlotsInsideInterceptingSubtree(\n node: SegmentNode,\n segmentPath: string,\n base: string\n): void {\n for (const slotNode of Object.values(node.slots)) {\n // A slot adds no URL depth, so it answers its owner's base.\n validateCollisionSpace(slotNode, segmentPath, base);\n }\n for (const child of node.children) {\n validateSlotsInsideInterceptingSubtree(\n child,\n `${segmentPath}/${child.segmentName}`,\n childInterceptedBase(child, base)\n );\n }\n}\n\n/**\n * The URL a leaf answers, built from its space's base and its own chain.\n *\n * Read instead of `node.urlPath` because `urlPath` inside an intercepting\n * subtree is parent-relative and is not an address — the collision URL has to\n * be one the author can actually find. Outside an intercepting subtree the two\n * are identical by construction: the base is the space root's `urlPath` and the\n * chain is every URL-contributing segment below it.\n */\nfunction urlFromChain(base: string, chain: SegmentNode[]): string {\n if (chain.length === 0) return base || '/';\n const tail = chain.map((n) => n.segmentName).join('/');\n return `${base === '/' ? '' : base}/${tail}`;\n}\n\n/**\n * The intercepted URL an intercepting directory's own page sits at.\n *\n * `urlPath` inside an intercepting subtree is deliberately parent-relative and\n * is **not** an address (see design/07-routing.md §\"Why urlPath Stays\n * Parent-Relative Inside an Intercepting Subtree\"), so a collision reported\n * from one names a URL the author cannot find. The intercepted base is the real\n * address, and it is what rewrite generation and the interception collision\n * validator both already use.\n */\nfunction interceptedBaseOf(node: SegmentNode, ownerUrlPath: string): string {\n return computeInterceptedUrlBase(\n ownerUrlPath,\n node.interceptionMarker ?? '(.)',\n node.interceptedSegmentName ?? node.segmentName\n );\n}\n\n/**\n * Extend an intercepted base by one child segment.\n *\n * Route groups are transparent and contribute nothing. A nested intercepting\n * node re-bases from its own marker rather than appending — that tree is\n * rejected by `validateInterceptionPlacement`, but collision validation runs\n * first, so it still has to produce a sensible URL rather than one containing a\n * literal \"(.)b\".\n */\nfunction childInterceptedBase(child: SegmentNode, base: string): string {\n // Slots are not in `children` — `scanChildren` files them under `slots` — so\n // only groups are transparent here.\n if (child.segmentType === 'group') return base;\n if (child.segmentType === 'intercepting') return interceptedBaseOf(child, base);\n return base === '/' ? `/${child.segmentName}` : `${base}/${child.segmentName}`;\n}\n","/**\n * Parallel slot placement.\n *\n * One rule: a slot is resolved only for a segment on the **matched route\n * chain**, and a slot's own subtree is never on that chain. So a slot declared\n * anywhere inside another slot's subtree can never render, and is rejected at\n * build time rather than left as a directory the author will keep editing.\n *\n * See design/07-routing.md §\"Parallel Routes\".\n */\n\nimport type { SegmentNode } from './types.ts';\n\n/**\n * Reject every parallel slot that no renderer reaches.\n *\n * **Where slots are resolved.** Two places read `segment.slots`, and both walk\n * the main route chain: `route-element-builder.ts` (the RSC render).\n * It is not reachable from inside a slot. The slot's own\n * subtree is rendered by `slot-resolver.ts`, which walks the slot's matched\n * chain to apply that chain's layouts, error boundaries and access gates\n * (`wrapWithIntermediateSegments`, `wrapWithLayout`) and never reads `.slots`\n * on any of those nodes. So `app/feed/@modal/photo/@side/page.tsx` and\n * everything below it is dead: the file is scanned, typed, code-split and\n * shipped, and no request can render it.\n *\n * That is the TIM-1278 failure — a directory that silently never renders —\n * and the reason it is a build error rather than a documented limit is that\n * nothing at request time can report it. A slot that does not match renders\n * `default.tsx` or `null`, which is exactly what a slot the resolver never\n * looked at also produces, so the author sees the *designed* behaviour of a\n * non-matching slot and has no signal to distinguish it from a slot that\n * cannot match at all.\n *\n * **Interception is incidental.** TIM-1311 arrived as\n * `feed/@modal/(.)photo/@side`, and the interception contributes nothing: the\n * identical `feed/@modal/photo/@side` never renders either, for the same\n * reason and with no marker anywhere. Rejecting only the intercepting spelling\n * would have drawn a line the author cannot see from the directory names.\n *\n * The children-path interception shape is **not** affected and must not be:\n * its intercepting node joins the main chain (`server/children-interception.ts`\n * splices `[…owner ancestors, interceptingNode, …descendants]` into an ordinary\n * `RouteMatch`), so `route-element-builder.ts` resolves slots on it and below\n * it exactly as it does for any other chain segment. `(browse)/(...)photo/[id]/@side`\n * renders; `feed/@modal/(.)photo/@side` does not. The discriminator is the slot\n * boundary, which is what this walk tracks — not the marker.\n */\nexport function validateSlotPlacement(root: SegmentNode): void {\n walkForSlotPlacement(root, '', null);\n}\n\n/**\n * `enclosingSlotPath` is the `app/`-relative path of the nearest slot at or\n * above `node`, or null on the main chain. It is carried rather than recomputed\n * so the error can name the slot that swallows the offender — \"this is inside\n * @modal\" is the part of the diagnosis the author cannot see from the offending\n * path alone.\n */\nfunction walkForSlotPlacement(\n node: SegmentNode,\n path: string,\n enclosingSlotPath: string | null\n): void {\n for (const [slotName, slotNode] of Object.entries(node.slots)) {\n const slotPath = path ? `${path}/@${slotName}` : `@${slotName}`;\n if (enclosingSlotPath !== null) {\n throwNestedSlot(slotPath, enclosingSlotPath);\n }\n walkForSlotPlacement(slotNode, slotPath, slotPath);\n }\n\n for (const child of node.children) {\n walkForSlotPlacement(\n child,\n path ? `${path}/${child.segmentName}` : child.segmentName,\n enclosingSlotPath\n );\n }\n}\n\nfunction throwNestedSlot(slotPath: string, enclosingSlotPath: string): never {\n throw new Error(\n `Build error: parallel slot nested inside another parallel slot.\\n` +\n ` Directory: app/${slotPath}\\n` +\n ` Inside: app/${enclosingSlotPath}\\n` +\n `A parallel slot is resolved only for a segment on the matched route chain, and a ` +\n `slot's own subtree is never on that chain — the slot resolver walks the slot's ` +\n `matched chain to apply its layouts, error boundaries and access gates, and never ` +\n `looks at slots declared inside it. So app/${slotPath} and everything below it can ` +\n `never render, and no request-time signal can tell you: an unreachable slot ` +\n `produces exactly what a slot that simply did not match produces.\\n` +\n `Declare the slot on a segment of the main route chain instead, or move its pages ` +\n `into app/${enclosingSlotPath}'s own subtree.\\n` +\n `See design/07-routing.md §\"Parallel Routes\".`\n );\n}\n","import { readFileSync, statSync } from 'node:fs';\n\nconst cache = new Map<string, { mtimeMs: number; size: number; content: string }>();\n\n/**\n * Read a file's text content, returning a cached copy when the file\n * hasn't been modified since the last read (checked via mtime + size).\n *\n * Falls back to a fresh read on any stat/read error.\n */\nexport function readFileCached(filePath: string): string {\n try {\n const stat = statSync(filePath);\n const entry = cache.get(filePath);\n if (entry && entry.mtimeMs === stat.mtimeMs && entry.size === stat.size) return entry.content;\n const content = readFileSync(filePath, 'utf-8');\n cache.set(filePath, { mtimeMs: stat.mtimeMs, size: stat.size, content });\n return content;\n } catch {\n cache.delete(filePath);\n return readFileSync(filePath, 'utf-8');\n }\n}\n","/**\n * AST-based export detection for route files.\n *\n * Uses Vite's `parseAst` (backed by oxc) to precisely detect named and\n * default exports, replacing the regex-based approach that was fragile\n * with TypeScript syntax (type exports, `as` aliases, comments, etc.).\n */\n\nimport { existsSync } from 'node:fs';\nimport { parseAst } from 'vite';\nimport { readFileCached } from './file-cache.ts';\n\ninterface AstNode {\n type: string;\n exportKind?: string;\n declaration?: AstNode;\n specifiers?: Array<{\n exported?: { name: string };\n local?: { name: string };\n exportKind?: string;\n }>;\n id?: { name: string };\n declarations?: Array<{ id?: { name: string }; init?: AstNode }>;\n /** Set on re-exports: `export { X } from './y'`. Absent on local exports. */\n source?: { value?: string } | null;\n /** Set on runtime-transparent wrappers: `X as T`, `X satisfies T`, `(X)`, `X!`. */\n expression?: AstNode;\n /** Set on `Identifier` nodes. */\n name?: string;\n}\n\ninterface ProgramNode {\n body: AstNode[];\n}\n\nfunction tryParse(source: string): ProgramNode | null {\n try {\n return parseAst(source, { lang: 'tsx' }) as unknown as ProgramNode;\n } catch {\n return null;\n }\n}\n\n/**\n * Collect all named export identifiers from a parsed program.\n * Handles: `export function X`, `export const X`, `export { X }`,\n * `export { X } from '...'`, `export { Y as X }`.\n */\nfunction collectNamedExports(program: ProgramNode): Set<string> {\n const names = new Set<string>();\n for (const stmt of program.body) {\n if (stmt.type !== 'ExportNamedDeclaration') continue;\n // Skip `export type ...` declarations — they're erased at runtime\n if (stmt.exportKind === 'type') continue;\n\n if (stmt.declaration) {\n if (stmt.declaration.id?.name) {\n names.add(stmt.declaration.id.name);\n }\n if (stmt.declaration.declarations) {\n for (const decl of stmt.declaration.declarations) {\n if (decl.id?.name) names.add(decl.id.name);\n }\n }\n }\n\n if (stmt.specifiers) {\n for (const spec of stmt.specifiers) {\n // Skip `export { type X }` — per-specifier type exports\n if (spec.exportKind === 'type') continue;\n if (spec.exported?.name && spec.exported.name !== 'default') {\n names.add(spec.exported.name);\n }\n }\n }\n }\n return names;\n}\n\n/**\n * The specifier exporting a *runtime* default from this statement, if any.\n *\n * Type-only exports erase, so they are not default exports at runtime — and\n * TypeScript spells that two ways, on two different nodes: `export type { X as\n * default }` marks the **statement**, `export { type X as default }` marks the\n * **specifier**. Both have to be checked, or a type alias reads as a component:\n * a status file whose only default is a type has nothing for the error boundary\n * to receive (the renderer's `mod?.default` skips it), so treating it as one\n * would fail a build over a file that cannot break anything.\n *\n * One implementation because two callers must agree on what \"has a default\n * export\" means: `hasDefaultExport` decides whether to warn that a page has\n * none, and `fileDefaultExportIsLocalValue` decides whether to fail the build.\n */\nfunction defaultSpecifier(stmt: AstNode): NonNullable<AstNode['specifiers']>[number] | null {\n if (stmt.type !== 'ExportNamedDeclaration' || !stmt.specifiers) return null;\n if (stmt.exportKind === 'type') return null;\n for (const spec of stmt.specifiers) {\n if (spec.exportKind === 'type') continue;\n if (spec.exported?.name === 'default') return spec;\n }\n return null;\n}\n\n/**\n * Check whether a program has a default export.\n * Handles: `export default ...`, `export { X as default }`,\n * `export { default } from '...'`. Type-only defaults do not count —\n * see `defaultSpecifier`.\n */\nfunction hasDefaultExport(program: ProgramNode): boolean {\n for (const stmt of program.body) {\n if (stmt.type === 'ExportDefaultDeclaration') return true;\n if (defaultSpecifier(stmt)) return true;\n }\n return false;\n}\n\n/**\n * Check whether a program has any bare `export * from '...'` declarations.\n * Excludes `export * as X from '...'` (namespace re-exports) since those\n * create a namespace object, not individual top-level exports.\n */\nfunction hasStarExport(program: ProgramNode): boolean {\n return program.body.some(\n (stmt) =>\n stmt.type === 'ExportAllDeclaration' && !(stmt as any).exported && stmt.exportKind !== 'type'\n );\n}\n\n/**\n * Check if a file exports a specific named export.\n * Returns false if the file doesn't exist or can't be parsed.\n */\nexport function fileHasExport(filePath: string, exportName: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n return collectNamedExports(program).has(exportName);\n } catch {\n return false;\n }\n}\n\n/**\n * Check if a file has any of the given named exports.\n * Returns the set of matching export names.\n */\nexport function fileHasAnyExport(filePath: string, exportNames: readonly string[]): Set<string> {\n const matches = new Set<string>();\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return matches;\n const exports = collectNamedExports(program);\n for (const name of exportNames) {\n if (exports.has(name)) matches.add(name);\n }\n } catch {\n // Graceful fallback: return empty set\n }\n return matches;\n}\n\n/**\n * Check if a file has a default export.\n * Returns false if the file doesn't exist or can't be parsed.\n */\nexport function fileHasDefaultExport(filePath: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n return hasDefaultExport(program);\n } catch {\n return false;\n }\n}\n\n/** Node types whose value is unambiguously constructed by this module. */\nconst FUNCTION_LIKE = new Set([\n 'FunctionDeclaration',\n 'FunctionExpression',\n 'ArrowFunctionExpression',\n 'ClassDeclaration',\n 'ClassExpression',\n]);\n\n/**\n * Expression wrappers that erase at runtime and so cannot change where a value\n * was constructed: `X as T`, `X satisfies T`, `<T>X`, `X!`, `X<T>`, `(X)`.\n */\nconst TRANSPARENT_WRAPPERS = new Set([\n 'TSAsExpression',\n 'TSSatisfiesExpression',\n 'TSTypeAssertion',\n 'TSNonNullExpression',\n 'TSInstantiationExpression',\n 'ParenthesizedExpression',\n]);\n\n/**\n * Strip runtime-transparent wrappers so the node underneath can be classified.\n *\n * Without this, `(() => <h1 />) satisfies React.FC` reads as a\n * `TSSatisfiesExpression` rather than the arrow function it compiles to, and a\n * provably-local component is misread as one whose origin cannot be\n * determined — turning a build error into a warning.\n */\nfunction unwrapTransparent(node: AstNode): AstNode {\n let current = node;\n while (TRANSPARENT_WRAPPERS.has(current.type) && current.expression) {\n current = current.expression;\n }\n return current;\n}\n\n/**\n * Whether `name` is bound at module top level to a function or class written\n * in this file. An imported binding can never match: imports are\n * `ImportDeclaration` specifiers, not function/class/variable declarations,\n * and a module-level name cannot be both.\n */\nfunction bindsLocalFunction(program: ProgramNode, name: string): boolean {\n for (const stmt of program.body) {\n // `export function X() {}` binds X locally too — unwrap the export.\n const decl = stmt.type === 'ExportNamedDeclaration' ? stmt.declaration : stmt;\n if (!decl) continue;\n if (\n (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') &&\n decl.id?.name === name\n ) {\n return true;\n }\n if (decl.type === 'VariableDeclaration') {\n for (const d of decl.declarations ?? []) {\n if (d.id?.name === name && d.init && FUNCTION_LIKE.has(unwrapTransparent(d.init).type)) {\n return true;\n }\n }\n }\n }\n return false;\n}\n\n/**\n * Whether the default export is *provably* a value constructed by this module:\n * `export default function/class/() => {}`, or a name bound to a function or\n * class declared here — whether it leaves via `export default Name` or\n * `export { Name as default }`. Both spellings are the same fact about where\n * the value was built, so both must answer the same way.\n *\n * This is the sound half of \"is this a server component?\". A file with no\n * `'use client'` directive whose default export is built here is a server\n * component, full stop. Everything else — `export { default } from\n * './client-thing'`, `export default Imported`, `export default memo(X)`,\n * an unparseable file — returns false, because the exported value may have\n * been minted in a module that *does* carry the directive. Callers must not\n * infer \"server component\" from a missing directive alone.\n */\nexport function fileDefaultExportIsLocalValue(filePath: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n for (const stmt of program.body) {\n if (stmt.type === 'ExportDefaultDeclaration') {\n const decl = stmt.declaration && unwrapTransparent(stmt.declaration);\n if (!decl) return false;\n if (FUNCTION_LIKE.has(decl.type)) return true;\n if (decl.type === 'Identifier' && decl.name) {\n return bindsLocalFunction(program, decl.name);\n }\n return false;\n }\n\n // `export { Name as default }` — the same default export by another\n // spelling. `hasDefaultExport` already counts it, so an analysis that\n // skipped it here would let the exact defect this powers slip through\n // as a warning.\n const spec = defaultSpecifier(stmt);\n if (spec) {\n // `export { X as default } from './client-thing'` re-exports another\n // module's value — the directive governing it lives over there.\n if (stmt.source) return false;\n return spec.local?.name ? bindsLocalFunction(program, spec.local.name) : false;\n }\n }\n return false;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if a file has any `export * from '...'` declarations.\n * Returns false if the file doesn't exist or can't be parsed.\n */\nexport function fileHasStarExport(filePath: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n return hasStarExport(program);\n } catch {\n return false;\n }\n}\n\n/**\n * Parsed value of `export const prerender` from a route file.\n *\n * - `true` / `false` — literal boolean\n * - `{ ttl?, tags? }` — ISR options object (prerender: true is implied)\n * - `undefined` — no `export const prerender` found\n *\n * See design/45-cache-lifetimes.md §\"Route-Level Static\".\n */\nexport type PrerenderExportValue = boolean | { ttl?: number; tags?: string[] };\n\nfunction extractLiteralValue(node: AstNode): unknown {\n if (!node) return undefined;\n const n = node as any;\n if (n.type === 'Literal') return n.value;\n if (n.type === 'UnaryExpression' && n.operator === '-' && n.argument?.type === 'Literal') {\n return -(n.argument.value as number);\n }\n if (n.type === 'ArrayExpression') {\n const elements = n.elements as AstNode[] | undefined;\n if (!elements) return undefined;\n const result: unknown[] = [];\n for (const el of elements) {\n const v = extractLiteralValue(el);\n if (v === undefined) return undefined;\n result.push(v);\n }\n return result;\n }\n return undefined;\n}\n\nfunction extractObjectLiteral(node: AstNode): Record<string, unknown> | undefined {\n if ((node as any).type !== 'ObjectExpression') return undefined;\n const props = (node as any).properties as AstNode[] | undefined;\n if (!props) return undefined;\n const result: Record<string, unknown> = {};\n for (const prop of props) {\n const p = prop as any;\n if (p.type === 'SpreadElement') return undefined;\n if (p.type !== 'Property' || p.computed) return undefined;\n const key =\n p.key?.type === 'Identifier'\n ? p.key.name\n : p.key?.type === 'Literal'\n ? String(p.key.value)\n : undefined;\n if (!key) return undefined;\n const value = extractLiteralValue(p.value);\n if (value === undefined) return undefined;\n result[key] = value;\n }\n return result;\n}\n\n/**\n * Extract the value of `export const prerender` from a file.\n * Returns undefined if the file doesn't export `prerender` or can't be parsed.\n *\n * Supports:\n * export const prerender = true;\n * export const prerender = false;\n * export const prerender = { ttl: 3600, tags: ['docs'] };\n */\nexport function getPrerenderExport(filePath: string): PrerenderExportValue | undefined {\n if (!existsSync(filePath)) return undefined;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return undefined;\n\n for (const stmt of program.body) {\n if (stmt.type !== 'ExportNamedDeclaration') continue;\n if (stmt.exportKind === 'type') continue;\n if (!stmt.declaration) continue;\n const decl = stmt.declaration;\n if (decl.type !== 'VariableDeclaration') continue;\n const declarations = decl.declarations as\n | Array<{\n id?: { name?: string };\n init?: AstNode;\n }>\n | undefined;\n if (!declarations) continue;\n for (const d of declarations) {\n if (d.id?.name !== 'prerender' || !d.init) continue;\n const init = d.init as any;\n if (init.type === 'Literal' && typeof init.value === 'boolean') {\n return init.value;\n }\n if (init.type === 'ObjectExpression') {\n const obj = extractObjectLiteral(init);\n if (!obj) return undefined;\n const result: { ttl?: number; tags?: string[] } = {};\n if ('ttl' in obj && typeof obj.ttl === 'number') result.ttl = obj.ttl;\n if (\n 'tags' in obj &&\n Array.isArray(obj.tags) &&\n obj.tags.every((t: unknown) => typeof t === 'string')\n ) {\n result.tags = obj.tags as string[];\n }\n return result;\n }\n return undefined;\n }\n }\n return undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * How a file declares the per-route CSRF exemption.\n *\n * - `'absent'` — no runtime export named `csrf`.\n * - `'exempt'` — exactly `export const csrf = false` (wrappers like\n * `false as const` allowed), declared in this file.\n * - `'invalid'` — some other `csrf` export: another value, `let`, a\n * destructured binding, a specifier or named re-export (`export { csrf }`,\n * `export { x as \"csrf\" }`), a function or class. `export * from` is not\n * followed: a `csrf` it carries is neither exempt nor an error.\n *\n * The exemption is read from source at build time, never from the loaded\n * module, because the CSRF gate runs before any user code (see\n * design/08-forms-and-actions.md §\"Per-route exemption\"). Only the literal\n * form can be read that way; any other spelling is a build error rather\n * than a silent \"not exempt\", so what the developer wrote is what runs.\n */\nexport type CsrfExport = 'absent' | 'exempt' | 'invalid';\n\nexport function getCsrfExport(filePath: string): CsrfExport {\n if (!existsSync(filePath)) return 'absent';\n // Unparseable: not exempt (fail closed). The compiler reports the syntax\n // error; guessing at a half-written file would misreport it.\n const program = tryParse(readFileCached(filePath));\n if (!program) return 'absent';\n\n // Walk the statements directly rather than through `collectNamedExports`:\n // every spelling that exports a runtime `csrf` must be seen here, and\n // that helper does not see destructured declarations or string-literal\n // specifier names. What cannot be seen at all — `export * from` — is\n // not read, and is documented as such.\n let found: CsrfExport = 'absent';\n for (const stmt of program.body) {\n if (stmt.type !== 'ExportNamedDeclaration' || stmt.exportKind === 'type') continue;\n for (const spec of stmt.specifiers ?? []) {\n if (spec.exportKind !== 'type' && specifierName(spec.exported) === 'csrf') {\n return 'invalid';\n }\n }\n const decl = stmt.declaration as (AstNode & { kind?: string }) | undefined;\n if (!decl) continue;\n if (decl.id?.name === 'csrf') return 'invalid'; // function or class\n if (decl.type !== 'VariableDeclaration') continue;\n for (const d of decl.declarations ?? []) {\n const id = d.id as AstNode | undefined;\n if (id?.type === 'Identifier') {\n if (id.name !== 'csrf') continue;\n const init = d.init && (unwrapTransparent(d.init) as AstNode & { value?: unknown });\n const literalFalse = init?.type === 'Literal' && init.value === false;\n if (decl.kind !== 'const' || !literalFalse) return 'invalid';\n found = 'exempt';\n } else if (id && patternBinds(id, 'csrf')) {\n return 'invalid'; // `export const { csrf } = …`\n }\n }\n }\n return found;\n}\n\n/** An export specifier's name: `csrf` or the string literal `\"csrf\"`. */\nfunction specifierName(node: { name?: string; value?: unknown } | undefined): string | undefined {\n if (!node) return undefined;\n return typeof node.value === 'string' ? node.value : node.name;\n}\n\n/** Whether a destructuring pattern binds `name` anywhere inside it. */\nfunction patternBinds(node: unknown, name: string): boolean {\n if (!node || typeof node !== 'object') return false;\n const n = node as Record<string, unknown>;\n switch (n.type) {\n case 'Identifier':\n return n.name === name;\n case 'ObjectPattern':\n return ((n.properties as unknown[]) ?? []).some((prop) => {\n const p = prop as Record<string, unknown>;\n return patternBinds(p.type === 'RestElement' ? p.argument : p.value, name);\n });\n case 'ArrayPattern':\n return ((n.elements as unknown[]) ?? []).some((el) => patternBinds(el, name));\n case 'RestElement':\n return patternBinds(n.argument, name);\n case 'AssignmentPattern':\n return patternBinds(n.left, name);\n default:\n return false;\n }\n}\n\n/**\n * Check if a file starts with a specific directive (e.g. \"use client\").\n * Directives are string literal expression statements at the top of the file.\n */\nexport function fileHasDirective(filePath: string, directive: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n for (const stmt of program.body) {\n if (stmt.type !== 'ExpressionStatement') break;\n const expr = (stmt as any).expression;\n if (expr?.type !== 'Literal' || typeof expr.value !== 'string') break;\n if (expr.value === directive) return true;\n }\n return false;\n } catch {\n return false;\n }\n}\n","/**\n * Route discovery scanner.\n *\n * Pure function: (appDir, config) → RouteTree\n *\n * Scans the app/ directory and builds a segment tree recognizing all\n * timber.js file conventions. Does NOT handle request matching — this\n * is discovery only.\n */\n\nimport { readdirSync, statSync } from 'node:fs';\nimport { join, extname, basename } from 'node:path';\nimport type {\n RouteTree,\n SegmentNode,\n SegmentType,\n RouteFile,\n ScannerConfig,\n InterceptionMarker,\n} from './types.ts';\nimport { classifySegment, effectiveUrlSegment } from './segment-classify.ts';\nimport { validateInterceptionPlacement, computeInterceptedBase } from './interception.ts';\nimport { validateRouteGroupCollisions } from './collision-spaces.ts';\nimport { validateSlotPlacement } from './slot-placement.ts';\nimport { DEFAULT_PAGE_EXTENSIONS } from './types.ts';\nimport { classifyMetadataRoute, isDynamicMetadataExtension } from '../server/metadata-routes.ts';\nimport { swallow } from '../server/logger.ts';\nimport { getCsrfExport } from './export-detect.ts';\nimport { ENCODED_SEPARATOR_RE, NULL_BYTE_RE } from '../server/canonicalize.ts';\n\n/**\n * File convention names that use pageExtensions (can be .tsx, .ts, .jsx, .js, .mdx, etc.)\n * `error` is excluded — it only accepts script extensions (TIM-1505).\n */\nconst PAGE_EXT_CONVENTIONS = new Set(['page', 'layout', 'default', 'denied']);\n\n/**\n * File convention names that are always .ts/.tsx (never .mdx etc.)\n */\nconst FIXED_CONVENTIONS = new Set(['middleware', 'access', 'route']);\n\n/**\n * Status-code file patterns:\n * - Exact 3-digit codes: 401.tsx, 429.tsx, 503.tsx\n * - Category catch-alls: 4xx.tsx, 5xx.tsx\n */\nconst STATUS_CODE_PATTERN = /^(\\d{3}|[45]xx)$/;\n\n/**\n * Whether a basename + extension pair is a route-convention file the\n * framework itself imports: page/layout/default/denied on pageExtensions,\n * error/global-error and status-code files on script extensions only,\n * middleware/access/route/proxy on .ts/.tsx/.js/.jsx only, and\n * status-code .json files — mirroring this scanner's own recognition\n * rules per family.\n *\n * Exported for the analyze classifier so its provisional classification\n * (design/47 §2) derives from the scanner's real conventions instead of\n * a duplicate list that can drift.\n */\nexport function isRouteConventionFile(\n base: string,\n ext: string,\n pageExtensions: readonly string[] = DEFAULT_PAGE_EXTENSIONS\n): boolean {\n const isPageExt = pageExtensions.includes(ext);\n const isScriptExt = /^[jt]sx?$/.test(ext);\n if (PAGE_EXT_CONVENTIONS.has(base) && isPageExt) return true;\n if ((base === 'error' || base === 'global-error') && isScriptExt) return true;\n if ((FIXED_CONVENTIONS.has(base) || base === 'proxy') && isScriptExt) return true;\n if (STATUS_CODE_PATTERN.test(base) && (isScriptExt || ext === 'json')) return true;\n return false;\n}\n\n/**\n * Scan the app/ directory and build the route tree.\n *\n * @param appDir - Absolute path to the app/ directory\n * @param config - Scanner configuration\n * @returns The complete route tree\n */\nexport function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTree {\n const pageExtensions = config.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS;\n const extSet = new Set(pageExtensions);\n\n const tree: RouteTree = {\n root: createSegmentNode('', 'static', '/'),\n };\n\n // Check for proxy.ts at app root\n const proxyFile = findFixedFile(appDir, 'proxy');\n if (proxyFile) {\n tree.proxy = proxyFile;\n }\n\n // Check for global-error.{tsx,ts,jsx,js} at app root.\n // Tier 2 error page — renders standalone (no layouts) when no segment-level\n // error file is found. Script extensions only — MDX not supported (TIM-1505).\n // See design/10-error-handling.md §\"Tier 2\".\n const globalErrorFile = findScriptExtFile(appDir, 'global-error');\n if (globalErrorFile) {\n tree.globalError = globalErrorFile;\n }\n\n // Scan the root directory's files\n scanSegmentFiles(appDir, tree.root, extSet);\n\n // Scan children recursively\n scanChildren(appDir, tree.root, extSet);\n\n // Validate: every parallel slot sits where a renderer reaches it. First,\n // because a slot nested inside another slot never renders, and a collision\n // reported from inside one would send the author to fix an ambiguity between\n // two pages that were both already dead.\n validateSlotPlacement(tree.root);\n\n // Validate: detect route group collisions (different groups producing pages at the same URL)\n validateRouteGroupCollisions(tree.root);\n\n // Validate: detect duplicate param names in nested dynamic segments\n // e.g., /[id]/items/[id] — same param name in ancestor and descendant\n validateDuplicateParamNames(tree.root);\n\n // Validate: detect same param name used with different bracket forms\n // e.g., [id] in one route and [...id] in another\n validateUniqueBracketForms(tree.root);\n\n // Validate: detect affixed dynamic siblings with equal specificity\n // e.g., a-[id] and [slug]-b both have prefix+suffix length = 2\n validateAffixedSiblingConflicts(tree.root);\n\n // Validate: intercepting routes must be direct children of a parallel slot —\n // the slot resolver is the only thing that can render one.\n validateInterceptionPlacement(tree.root);\n\n return tree;\n}\n\n/**\n * Create an empty segment node.\n */\nfunction createSegmentNode(\n segmentName: string,\n segmentType: SegmentType,\n urlPath: string,\n paramName?: string,\n interceptionMarker?: InterceptionMarker,\n interceptedSegmentName?: string,\n paramPrefix?: string,\n paramSuffix?: string\n): SegmentNode {\n return {\n segmentName,\n segmentType,\n urlPath,\n paramName,\n paramPrefix,\n paramSuffix,\n interceptionMarker,\n interceptedSegmentName,\n children: [],\n slots: {},\n };\n}\n\n/**\n * Compute the URL path for a child segment given its parent's URL path.\n * Route groups, slots, and intercepting routes do NOT add URL depth.\n */\nfunction computeUrlPath(parentUrlPath: string, dirName: string, segmentType: SegmentType): string {\n // Groups, slots, and intercepting routes don't add to URL path\n if (segmentType === 'group' || segmentType === 'slot' || segmentType === 'intercepting') {\n return parentUrlPath;\n }\n\n const parentPath = parentUrlPath === '/' ? '' : parentUrlPath;\n return `${parentPath}/${dirName}`;\n}\n\n/**\n * Scan a directory for file conventions and populate the segment node.\n */\nfunction scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n swallow(err, `scanSegmentFiles: unreadable directory ${dirPath}`, { level: 'warn' });\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n // Skip directories — handled by scanChildren\n try {\n if (statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n const ext = extname(entry).slice(1); // remove leading dot\n const name = basename(entry, `.${ext}`);\n\n // Page-extension conventions (page, layout, default, denied)\n if (PAGE_EXT_CONVENTIONS.has(name) && extSet.has(ext)) {\n const key = name as 'page' | 'layout' | 'default' | 'denied';\n const existing = node[key];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node[key] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // error.tsx — script extensions only, not MDX/MD (TIM-1505).\n if (name === 'error' && /^[jt]sx?$/.test(ext)) {\n const existing = node.error;\n if (existing) {\n throwDuplicateConvention('error', existing.filePath, fullPath);\n }\n node.error = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Fixed conventions (middleware, access, route) — only\n // .ts/.tsx/.js/.jsx, never pageExtensions like .mdx. The regex is\n // anchored: mts/cts/mjs/cjs (and stray tail matches like `.foots`)\n // are NOT recognized. See design/07-routing.md §\"Page Extensions\".\n if (FIXED_CONVENTIONS.has(name) && /^[jt]sx?$/.test(ext)) {\n const key = name as 'middleware' | 'access' | 'route';\n const existing = node[key];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node[key] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // JSON status-code files (401.json, 4xx.json, 503.json, 5xx.json)\n // Recognized regardless of pageExtensions — .json is a data format, not a page extension.\n if (STATUS_CODE_PATTERN.test(name) && ext === 'json') {\n if (!node.jsonStatusFiles) {\n node.jsonStatusFiles = {};\n }\n node.jsonStatusFiles[name] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Status-code files (401.tsx, 4xx.tsx, 503.tsx, 5xx.tsx) — script extensions only (TIM-1505)\n if (STATUS_CODE_PATTERN.test(name) && /^[jt]sx?$/.test(ext)) {\n if (!node.statusFiles) {\n node.statusFiles = {};\n }\n const existing = node.statusFiles[name];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node.statusFiles[name] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Metadata route files (sitemap.ts, robots.ts, icon.tsx, opengraph-image.tsx, etc.)\n // Both static (.xml, .txt, .png, .ico, etc.) and dynamic (.ts, .tsx) files are recognized.\n // When both exist for the same base name, dynamic takes precedence.\n // Two files of the SAME kind (static+static or dynamic+dynamic) are a\n // build error — picking one by readdir order is platform-dependent.\n // See design/16-metadata.md §\"Metadata Routes\"\n const metaInfo = classifyMetadataRoute(entry);\n if (metaInfo) {\n if (!metaInfo.nestable && node.segmentName !== '') {\n throw new Error(\n `Build error: '${name}' is a root-only metadata convention and must be in the app root directory.\\n` +\n ` File: ${fullPath}\\n` +\n ` Move this file to the app root (not a route group or nested segment).`\n );\n }\n if (!node.metadataRoutes) {\n node.metadataRoutes = {};\n }\n const existing = node.metadataRoutes[name];\n if (existing) {\n const existingIsDynamic = isDynamicMetadataExtension(name, existing.extension);\n const newIsDynamic = isDynamicMetadataExtension(name, ext);\n if (existingIsDynamic === newIsDynamic) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n // Dynamic > static precedence: overwrite only when the new file is dynamic.\n if (newIsDynamic) {\n node.metadataRoutes[name] = { filePath: fullPath, extension: ext };\n }\n } else {\n node.metadataRoutes[name] = { filePath: fullPath, extension: ext };\n }\n }\n }\n\n // Validate: route.ts + page.* is a hard build error\n if (node.route && node.page) {\n throw new Error(\n `Build error: route.ts and page.* cannot coexist in the same segment.\\n` +\n ` route.ts: ${node.route.filePath}\\n` +\n ` page: ${node.page.filePath}\\n` +\n `A URL is either an API endpoint or a rendered page, not both.`\n );\n }\n\n // The per-route CSRF exemption is read from source here, at scan time, so\n // the manifest carries it and the request gate never loads user code to\n // decide it. Any other spelling of the export is a lint error\n // (convention-lint.ts `checkCsrfExports`).\n if (node.route && getCsrfExport(node.route.filePath) === 'exempt') {\n node.csrfExempt = true;\n }\n}\n\n/**\n * Throw a build error for two files claiming the same convention slot in\n * one segment (e.g. page.tsx + page.jsx). Which file readdir returns first\n * is platform-dependent (APFS sorts entries, ext4 does not) — silently\n * picking one would let macOS dev and Linux CI serve different\n * implementations of the same route.\n */\nfunction throwDuplicateConvention(\n convention: string,\n firstPath: string,\n secondPath: string\n): never {\n throw new Error(\n `Build error: duplicate '${convention}' files in the same route segment.\\n` +\n ` File 1: ${firstPath}\\n` +\n ` File 2: ${secondPath}\\n` +\n `Each segment may define '${convention}' at most once. Remove one of the files.`\n );\n}\n\n/**\n * Recursively scan child directories and build the segment tree.\n */\nfunction scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n swallow(err, `scanChildren: unreadable directory ${dirPath}`, { level: 'warn' });\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n try {\n if (!statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n // Reject directories with encoded path delimiters or null bytes.\n // These can cause route collisions when decoded at the URL boundary.\n // See design/13-security.md §\"Encoded separators rejected\" and §\"Null bytes rejected\".\n if (ENCODED_SEPARATOR_RE.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded path delimiter (%2F or %5C).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded separators in directory names cause route collisions when decoded. ` +\n `Rename the directory to remove the encoded delimiter.`\n );\n }\n if (NULL_BYTE_RE.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded null byte (%00).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded null bytes in directory names are not allowed. ` +\n `Rename the directory to remove the null byte encoding.`\n );\n }\n\n const {\n type,\n paramName,\n paramPrefix,\n paramSuffix,\n interceptionMarker,\n interceptedSegmentName,\n } = classifySegment(entry);\n\n // Skip private folders — underscore-prefixed dirs are excluded from routing\n if (type === 'private') continue;\n\n const urlPath = computeUrlPath(parentNode.urlPath, entry, type);\n const childNode = createSegmentNode(\n entry,\n type,\n urlPath,\n paramName,\n interceptionMarker,\n interceptedSegmentName,\n paramPrefix,\n paramSuffix\n );\n\n // Scan this segment's files\n scanSegmentFiles(fullPath, childNode, extSet);\n\n // Recurse into subdirectories\n scanChildren(fullPath, childNode, extSet);\n\n // Attach to parent: slots go into slots record, everything else is a child\n if (type === 'slot') {\n const slotName = entry.slice(1); // remove @\n parentNode.slots[slotName] = childNode;\n } else {\n parentNode.children.push(childNode);\n }\n }\n}\n\n/**\n * Validate that no route chain contains duplicate dynamic param names.\n *\n * Example violation:\n * app/[id]/items/[id]/page.tsx — 'id' appears twice in the ancestor chain.\n *\n * Route groups are transparent — params accumulate through them.\n * Slots are independent — duplicate detection does NOT cross slot boundaries.\n *\n * See design/07-routing.md §\"Duplicate Param Name Detection\"\n */\nfunction validateDuplicateParamNames(root: SegmentNode): void {\n walkForDuplicateParams(root, new Map());\n}\n\n/**\n * Recursively walk the segment tree, tracking seen param names → segment paths.\n * Throws on the first duplicate found.\n */\nfunction walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): void {\n // Read through the effective identity so an intercepting directory is seen\n // as the segment it intercepts: the scanner leaves `paramName` unset on\n // `(.)[id]`, so a validator reading the node directly is blind to a param\n // the resolver goes on to bind — `@modal/(.)[id]/[id]` would pass here and\n // then silently overwrite the outer `id` at match time, where the ordinary\n // `/[id]/[id]` is rejected. See TIM-1288.\n const { paramName } = effectiveUrlSegment(node);\n\n // If this node introduces a param name, check for duplicates\n if (paramName) {\n const existing = seen.get(paramName);\n if (existing) {\n throw new Error(\n `[timber] Duplicate param name '${paramName}' in route chain.\\n` +\n ` First defined at: ${existing}\\n` +\n ` Duplicate at: ${node.urlPath || '/'}\\n` +\n ` Rename one of the segments to avoid ambiguity.`\n );\n }\n // Add to seen for descendants (use a new Map to avoid polluting siblings)\n seen = new Map(seen);\n seen.set(paramName, node.urlPath || '/');\n }\n\n // Recurse into children (they inherit the accumulated params)\n for (const child of node.children) {\n walkForDuplicateParams(child, seen);\n }\n\n // A slot INHERITS the ancestor params rather than starting fresh: a slot\n // child may not reuse a name its owner already bound\n // (`app/gallery/[id]/@modal/[id]` is rejected). The copy is so sibling\n // slots don't see each other's names, not so the chain is reset.\n //\n // Whether that rule is right is a separate question — slot params are\n // merged over the main route's at read time, so a collision is defined\n // rather than ambiguous — but changing it would change every slot.\n // Tracked as TIM-1287; do not \"fix\" this by making the map fresh without\n // deciding that.\n for (const slotNode of Object.values(node.slots)) {\n walkForDuplicateParams(slotNode, new Map(seen));\n }\n}\n\n/**\n * Validate that each param name uses a single bracket form across the entire route tree.\n *\n * Different bracket forms produce different types:\n * [id] → string\n * [...id] → string[]\n * [[...id]] → string[] | undefined\n *\n * If the same param name appears with multiple forms, the no-argument\n * getSegmentParams() / useSegmentParams() return type becomes ambiguous.\n *\n * Same name + same form across different routes is fine and expected.\n */\nfunction validateUniqueBracketForms(root: SegmentNode): void {\n const forms = new Map<string, { segmentType: SegmentType; urlPath: string }>();\n collectBracketForms(root, forms);\n}\n\nfunction collectBracketForms(\n node: SegmentNode,\n forms: Map<string, { segmentType: SegmentType; urlPath: string }>\n): void {\n // Effective identity, so `(.)[...rest]` is compared as the catch-all it\n // intercepts rather than skipped as an 'intercepting' node (TIM-1288).\n const { paramName, segmentType } = effectiveUrlSegment(node);\n\n if (paramName && segmentType !== 'static' && segmentType !== 'group') {\n const existing = forms.get(paramName);\n if (existing && existing.segmentType !== segmentType) {\n const bracketExamples: Record<string, string> = {\n 'dynamic': `[${paramName}]`,\n 'catch-all': `[...${paramName}]`,\n 'optional-catch-all': `[[...${paramName}]]`,\n };\n throw new Error(\n `[timber] Param name '${paramName}' uses multiple bracket forms.\\n` +\n ` ${bracketExamples[existing.segmentType] ?? existing.segmentType} at: ${existing.urlPath}\\n` +\n ` ${bracketExamples[segmentType] ?? segmentType} at: ${node.urlPath || '/'}\\n` +\n ` Each param name must use a single bracket form across all routes.\\n` +\n ` Rename one of the segments to fix this (e.g., [${paramName}] → [${paramName}Id]).`\n );\n }\n if (!existing) {\n forms.set(paramName, {\n segmentType,\n urlPath: node.urlPath || '/',\n });\n }\n }\n\n for (const child of node.children) {\n collectBracketForms(child, forms);\n }\n for (const slotNode of Object.values(node.slots)) {\n collectBracketForms(slotNode, forms);\n }\n}\n\n/**\n * Validate that no sibling affixed dynamic segments have equal specificity.\n *\n * Among affixed siblings, longer combined prefix+suffix length wins. When two\n * siblings tie (same total affix length), the router would fall back to\n * filesystem scan order — which is platform-dependent. This is a build error.\n *\n * Route groups are URL-transparent, so affixed segments inside different groups\n * at the same level still conflict with each other.\n *\n * Ambiguity requires the two segments to be candidates for the *same* URL\n * part, which is narrower than \"same parent\" once interception is involved:\n *\n * - Plain children and intercepting children never compete. They are resolved\n * by different walks — `findSlotMatch` matches the slot subtree (where\n * `tree-match.ts` ignores intercepting nodes outright) and\n * `findInterceptingMatch` builds a synthetic parent holding only\n * interceptors.\n * - Two interceptors compete only at the same anchor. `(.)a-[id]` and\n * `(..)[slug]-b` in one slot are based at different URL depths, so no\n * pathname reaches both, and `anchorIndex` rejects the one whose base does\n * not match the target. Grouping them together would fail the build for a\n * pair that can never be ambiguous.\n *\n * So candidates are keyed by the position they contend for, then by affix\n * length. See design/07-routing.md §\"Priority\" and §\"Intercepting Routes\".\n */\nfunction validateAffixedSiblingConflicts(node: SegmentNode): void {\n const affixed = collectEffectiveAffixed(node);\n\n if (affixed.length >= 2) {\n const byLength = new Map<string, SegmentNode[]>();\n for (const child of affixed) {\n // `\\0` as the separator, written as the escape rather than a literal\n // byte: a raw NUL in the source makes git and grep treat this file as\n // binary. A contention key is a URL path, which cannot contain one, so\n // the two halves of the key can never run together.\n const key = `${contentionKey(node, child)}\\0${affixLengthOf(child)}`;\n const group = byLength.get(key);\n if (group) {\n group.push(child);\n } else {\n byLength.set(key, [child]);\n }\n }\n\n for (const [, group] of byLength) {\n if (group.length >= 2) {\n throw new Error(\n `Build error: ambiguous affixed dynamic siblings with equal specificity.\\n` +\n ` Parent: ${node.urlPath || '/'}\\n` +\n ` Segments: ${group.map((s) => s.segmentName).join(', ')}\\n` +\n ` Combined prefix+suffix length: ${affixLengthOf(group[0]!)}\\n` +\n `The router cannot deterministically choose between these — filesystem scan order is platform-dependent.\\n` +\n `Rename one segment to give it a longer or shorter combined affix.`\n );\n }\n }\n }\n\n for (const child of node.children) {\n validateAffixedSiblingConflicts(child);\n }\n for (const slotNode of Object.values(node.slots)) {\n validateAffixedSiblingConflicts(slotNode);\n }\n}\n\n/**\n * The URL position a child contends for, as a comparable key.\n *\n * Plain children contend for the parent's next part. An intercepting child\n * contends for a part determined by its marker's intercepted base, so two\n * markers of different depth are simply different positions — and two markers\n * that compute the *same* base do contend, which is why the key is the base\n * rather than the marker (`(..)` and `(...)` under `/feed` both base at `/`).\n *\n * `groupByInterceptedAnchor` (`routing/interception.ts`) is the canonical\n * statement of that grouping — it is what the resolver walks and what the\n * collision validator checks. This computes the same key from the same\n * `computeInterceptedBase` rather than calling it, because affix ambiguity is\n * decided per *child* and `collectEffectiveAffixed` looks through route groups,\n * so the two do not partition the same set of nodes.\n */\nfunction contentionKey(parent: SegmentNode, child: SegmentNode): string {\n if (child.segmentType !== 'intercepting') return 'child';\n return `intercept:${computeInterceptedBase(parent.urlPath, child.interceptionMarker ?? '(.)')}`;\n}\n\n/** Combined affix length of a node, read through its effective identity. */\nfunction affixLengthOf(node: SegmentNode): number {\n const { paramPrefix, paramSuffix } = effectiveUrlSegment(node);\n return (paramPrefix?.length ?? 0) + (paramSuffix?.length ?? 0);\n}\n\n/**\n * Collect affixed dynamic segments that are effective children of a node.\n * Route groups are URL-transparent, so we look through them recursively.\n *\n * Segments are read through `effectiveUrlSegment`, so an intercepting\n * directory counts as the affixed segment it intercepts: `(.)a-[id]` is\n * `a-[id]`. The proxy the slot resolver builds hands those affixes to\n * `matchUrlParts`, whose equal-length sort is not stable across equal\n * specificity — so two intercepting siblings with the same combined affix\n * length would resolve by filesystem enumeration order, which is precisely\n * what this validator exists to prevent for ordinary siblings (TIM-1288).\n */\nfunction collectEffectiveAffixed(node: SegmentNode): SegmentNode[] {\n const result: SegmentNode[] = [];\n for (const child of node.children) {\n const { segmentType, paramPrefix, paramSuffix } = effectiveUrlSegment(child);\n if (segmentType === 'dynamic' && (paramPrefix || paramSuffix)) {\n result.push(child);\n } else if (segmentType === 'group') {\n result.push(...collectEffectiveAffixed(child));\n }\n }\n return result;\n}\n\n/**\n * Find a fixed-extension file (proxy.ts) in a directory.\n */\nfunction findFixedFile(dirPath: string, name: string): RouteFile | undefined {\n for (const ext of ['ts', 'tsx']) {\n const fullPath = join(dirPath, `${name}.${ext}`);\n try {\n if (statSync(fullPath).isFile()) {\n return { filePath: fullPath, extension: ext };\n }\n } catch {\n // File doesn't exist\n }\n }\n return undefined;\n}\n\n/**\n * Find a file with script extensions (tsx, ts, jsx, js) in a directory.\n * Used for app-root conventions like global-error that only accept script\n * extensions (TIM-1505).\n */\nfunction findScriptExtFile(dirPath: string, name: string): RouteFile | undefined {\n for (const ext of ['tsx', 'ts', 'jsx', 'js']) {\n const fullPath = join(dirPath, `${name}.${ext}`);\n try {\n if (statSync(fullPath).isFile()) {\n return { filePath: fullPath, extension: ext };\n }\n } catch {\n // File doesn't exist\n }\n }\n return undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scanner-CQt12vE2.js","names":[],"sources":["../../src/routing/collision-probe.ts","../../src/routing/interception-overlap.ts","../../src/routing/interception.ts","../../src/routing/collision-spaces.ts","../../src/routing/slot-placement.ts","../../src/routing/file-cache.ts","../../src/routing/export-detect.ts","../../src/routing/scanner.ts"],"sourcesContent":["/**\n * The order-dependence oracle shared by every route collision validator.\n *\n * A \"collision space\" is a set of pages that compete for URLs with each other:\n * the main tree, each parallel slot, each intercepting subtree. Two pages in\n * one space that answer the same URL are ambiguous — `matchNode` resolves the\n * tie by whichever `children` array lists first, so which page renders is\n * decided by filesystem enumeration order and renaming a route group can flip\n * it. Every space rejects that shape, and they all reject it the same way,\n * through this module.\n *\n * **The check asks the real matcher rather than modelling it.** For each leaf\n * we build a URL it matches, then run `matchUrlParts` twice — once on the\n * space, once on a clone with every `children` array reversed — and compare the\n * winners. A URL answered by two or more candidates in the same priority tier\n * resolves to whichever comes first in `children`, so reversing the arrays\n * flips it; a URL with a single winner, or one settled by a higher tier, is\n * unaffected. Order-dependence is therefore detected exactly, with no second\n * copy of the precedence rules to drift.\n *\n * That drift is why this is written as an oracle. The interception validator's\n * first version normalized each pattern into a \"match shape\" and compared\n * shapes, which meant reimplementing `matchNode`'s tiers, its group flattening,\n * and its zero-length optional-catch-all handling. Five review rounds found\n * five divergences between the two — param spelling, a zero-length catch-all,\n * the bracket tokens colliding with legal static directory names, a page\n * settling a grouped catch-all, and a page settling a *direct* one (which the\n * model could not express at all, because `matchNode` checks every route-group\n * page before any direct optional catch-all). Each fix was locally right and\n * the next divergence surfaced anyway. The model was the defect.\n *\n * **Every claim made here is existential** — \"here is a URL these two contend\n * for\" — which is what a finite set of probes can actually establish. The\n * reverse, \"this page can never win any URL\", is universal, and a probe's\n * silence is not evidence for it: a leaf that loses every probe may still own\n * URLs no probe visited. An earlier revision reported that as an\n * unreachable-page error and produced two false build errors in one review\n * round. Probe inadequacy must only ever cost a missed collision, never a\n * rejected app — so nothing here may report a leaf as unreachable.\n *\n * See design/07-routing.md §\"Comparing Routes by the URLs They Match\".\n */\n\nimport type { SegmentNode } from './types.ts';\nimport { effectiveUrlSegment } from './segment-classify.ts';\n// The validators ask the real matcher which page answers a URL rather than\n// modelling its precedence rules a second time. `tree-match.ts` is a pure\n// walker over `SegmentNode` with no request-time state, and\n// `server/chain-url-parts.ts` already imports the other direction.\nimport { matchUrlParts } from '../server/tree-match.ts';\n\n/** One page-or-route-bearing node in a collision space. */\nexport interface ProbeLeaf {\n /** The file that serves it — what the error message names. */\n filePath: string;\n /**\n * The nodes from the space root down to this leaf that contribute URL parts\n * — route groups excluded, since they are transparent for matching. Empty\n * for the space root's own page: `matchUrlParts` matches parts against the\n * root's *children*, so the root itself consumes nothing.\n */\n chain: SegmentNode[];\n}\n\n/** Two files in one space that a single URL resolves to order-dependently. */\nexport interface ProbedCollision {\n /** The contended URL, written for a human (see `describeProbeUrl`). */\n url: string;\n /** The file that wins in filesystem order. */\n first: string;\n /** The file that wins when every `children` array is reversed. */\n second: string;\n}\n\n/**\n * Find a URL in this collision space whose winner depends on `children` order,\n * or null if every probed URL resolves the same way in both orderings.\n *\n * `spaceRoot` is the node `matchUrlParts` starts from — the app root, a slot\n * node, or an intercepting directory. `base` is the URL that root sits at, used\n * only to render the reported URL.\n */\nexport function findOrderDependentCollision(\n spaceRoot: SegmentNode,\n leaves: Iterable<ProbeLeaf>,\n base: string\n): ProbedCollision | null {\n const reversed = withReversedChildren(spaceRoot);\n const longTail = longTailLength(spaceRoot) + 1;\n // Computed once for the space, not per leaf: it bounds the second candidate\n // wave, which only runs for a leaf the first wave never reached.\n const minLength = longestStaticName(spaceRoot);\n\n // Each probe is checked as it is generated, and the first flipped winner\n // returns immediately. Accumulating every probe first and scanning afterwards\n // reports the same collision — this walks the same probes in the same order —\n // but it pays for the whole space before saying anything, and the leaves that\n // cost the most are exactly the ones a found collision makes irrelevant.\n //\n // In a space of N equivalent dynamic siblings, only the first and last can\n // win a probe (one per ordering), so every middle leaf is unreachable and\n // exhausts the entire candidate alphabet looking for a value that reaches it\n // — 300 such siblings took ~2.5s to produce a build error the very first\n // probe already proved (codex, on PR #1002). A valid space of the same size\n // scans in ~35ms, so the cost was confined to the error path, but a slow\n // error is still a bad error.\n //\n // Deduped so a space with many leaves does not re-probe one URL, and so the\n // collision reported does not depend on which leaf generated a shared probe.\n const seen = new Set<string>();\n for (const leaf of leaves) {\n for (const { parts, display } of probeUrlsForLeaf(\n spaceRoot,\n reversed,\n leaf,\n longTail,\n minLength\n )) {\n const key = parts.join('/');\n if (seen.has(key)) continue;\n seen.add(key);\n\n const first = matchedLeafFile(spaceRoot, parts);\n const second = matchedLeafFile(reversed, parts);\n if (!first || !second || first === second) continue;\n return { url: describeProbeUrl(base, display), first, second };\n }\n }\n\n return null;\n}\n\n/** Join a probe's display parts onto its space's base. */\nfunction describeProbeUrl(base: string, display: string[]): string {\n if (display.length === 0) return base || '/';\n return `${base === '/' ? '' : base}/${display.join('/')}`;\n}\n\n/**\n * The file `matchUrlParts` resolves for a URL, or null if nothing does.\n *\n * A `route.ts` counts: `isTerminal` accepts either, so a route handler is a\n * legitimate winner of the walk and an ambiguity between a page and a route\n * handler is as real as one between two pages.\n */\nfunction matchedLeafFile(node: SegmentNode, parts: string[]): string | null {\n const match = matchUrlParts(node, parts);\n if (!match) return null;\n const leaf = match.chain[match.chain.length - 1];\n return leaf.page?.filePath ?? leaf.route?.filePath ?? null;\n}\n\n/**\n * A generated URL, and how to show it to a human.\n *\n * `display` is built alongside `parts` rather than derived from it by\n * substitution: a *static* segment can legitimately be named the same as the\n * probe value, and replacing every occurrence of that string rewrote the\n * literal directory to `<any>` — reporting `/<any>/<any>` for a collision under\n * a static `atimber-probea`, which claims a URL position is dynamic when the\n * author wrote it out (codex, on PR #1002). Only the segment that generated a\n * value knows whether it stood for one.\n */\ninterface ProbeUrl {\n parts: string[];\n display: string[];\n /**\n * Whether any part of this URL stood in for a value. When nothing did, every\n * candidate generates the identical URL and the search can stop after one.\n */\n dynamic: boolean;\n}\n\n/**\n * The values a probe's dynamic parts are tried with, in order.\n *\n * A probe stands in for \"any value\" at a dynamic position, so it is only useful\n * if it reaches the leaf it was built for. A *static* sibling's literal name or\n * a more specific *affixed* sibling's pattern can claim it instead, and that\n * higher-priority segment then wins in both orderings — a genuine tie\n * underneath goes unreported, which is safe but is the whole thing these\n * validators are for.\n *\n * Which values are claimable is a question about `matchNode`'s precedence, so\n * `probeUrlsForLeaf` asks `matchUrlParts` rather than deciding it here. This\n * only has to offer enough shapes for the search to find one that works: an\n * affixed sibling constrains the head of the value, its tail, or both, so head\n * and tail are varied over an alphanumeric alphabet. The head varies fastest\n * because a prefix is the more common affix. Exhausting the list is a missed\n * collision like any other inadequate probe, so this is a search and not a\n * proof — nothing downstream may treat it as one.\n *\n * Exported so a test that has to *name* a capturing static sibling can derive\n * the name from this list rather than restate it — a fixture spelling the value\n * out by hand stops being a capture the moment the list changes, and passes.\n *\n * Three revisions of a hand-built value each dodged one more capture and were\n * each followed by another (avoid static names; avoid affix boundary\n * characters; avoid re-affixed forms of static names — then a strictly more\n * specific affixed sibling, TIM-1313). Each rule was modelling affix precedence\n * by hand, which is what the match-shape model was deleted for.\n */\nexport function* probeCandidates(minLength = 0): Generator<string> {\n const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';\n const base = 'timber-probe';\n\n function* wave(core: string): Generator<string> {\n for (const tail of alphabet) {\n for (const head of alphabet) yield `${head}${core}${tail}`;\n }\n }\n\n yield* wave(base);\n\n // A second wave, longer than every static segment name in the space.\n //\n // A static sibling claims a probe only by **string equality**, so a value\n // longer than every static name in the space cannot be claimed by one — that\n // is a fact about lengths, not a model of `matchNode`'s precedence, which is\n // what makes it safe to state as an invariant where \"pick an unclaimable\n // value\" was not. An app holding a static directory for all 1296 first-wave\n // candidates claimed every probe and hid a real collision (codex, on\n // PR #1002); it cannot do the same here, because the length is derived from\n // its own longest static and adding a longer one only pushes this further out.\n //\n // Affix capture is not addressed by length and is not addressable this way:\n // an affixed pattern matches infinitely many values, so escaping one is still\n // the head/tail search below. The first wave is byte-identical to what it was\n // before this existed, so nothing that already found a value changes.\n const padded = base.padEnd(Math.max(base.length, minLength), 'x');\n if (padded !== base) yield* wave(padded);\n}\n\n/**\n * The longest static segment name anywhere in the space.\n *\n * Read through `effectiveUrlSegment` so an intercepting directory counts as the\n * segment it intercepts, and only static kinds count: a dynamic sibling claims\n * a probe by pattern rather than by equality, which no length can escape.\n */\nfunction longestStaticName(node: SegmentNode): number {\n let longest = 0;\n for (const child of node.children) {\n const seg = effectiveUrlSegment(child);\n if (seg.segmentType === 'static') longest = Math.max(longest, seg.segmentName.length);\n longest = Math.max(longest, longestStaticName(child));\n }\n return longest;\n}\n\n/**\n * The probe URLs for one leaf, built from the first candidate value that\n * actually reaches it.\n *\n * \"Reaches it\" is `matchUrlParts` resolving one of the leaf's own URLs to the\n * leaf's file, in either ordering — either is enough, because a leaf that wins\n * only in the reversed ordering is precisely the contention being looked for.\n * A catch-all leaf may be reached by its long probe and not its short one (at\n * one part it loses to a bare dynamic sibling deterministically), so any one of\n * the generated URLs landing is enough to accept the value.\n *\n * A leaf that no candidate reaches is shadowed at its position for every value\n * tried — most often because it sits below a catch-all, which `matchNode`\n * accepts only as a terminal and never descends past. Its probes are still\n * emitted, from the first candidate: a probe that misses its own leaf is never\n * *wrong* — it is a real URL, and a winner that differs between the two\n * orderings is a real ambiguity whichever leaf it belongs to. Nothing here may\n * report the leaf as unreachable; that is the universal claim these validators\n * do not make.\n */\nfunction probeUrlsForLeaf(\n root: SegmentNode,\n reversed: SegmentNode,\n leaf: ProbeLeaf,\n longTail: number,\n minLength: number\n): ProbeUrl[] {\n let fallback: ProbeUrl[] | null = null;\n\n for (const value of probeCandidates(minLength)) {\n const urls = probeUrlsFor(leaf.chain, value, longTail);\n const reaches = urls.some(\n ({ parts }) =>\n matchedLeafFile(root, parts) === leaf.filePath ||\n matchedLeafFile(reversed, parts) === leaf.filePath\n );\n if (reaches) return urls;\n fallback ??= urls;\n\n // A leaf with no dynamic part in its chain matches exactly one URL, so the\n // candidate value never appears in it and every further candidate would\n // generate the identical probe. Searching on would cost the whole alphabet\n // per statically-shadowed leaf — and in the main tree those are ordinary\n // (anything below a catch-all) rather than the pathology they are inside an\n // intercepting subtree. Stopping is not a weaker check: the URLs are the\n // same ones, and they are still probed via `fallback`.\n if (!urls.some(({ dynamic }) => dynamic)) break;\n }\n\n // `probeCandidates` is non-empty, so this is only reached with a fallback.\n return fallback ?? [];\n}\n\n/**\n * The URL part one segment contributes to a probe.\n *\n * A dynamic segment stands for any single value, so it contributes the\n * candidate value — re-affixed, since an affixed segment only matches parts\n * that carry its prefix and suffix, so a probe without them could not reach it.\n * A catch-all stands for one or more parts, and one is enough to reach the leaf.\n *\n * The re-affixed part is the reason a value cannot be checked once for the\n * whole space: it exposes the value to every affixed sibling that *extends*\n * this segment's affixes, which differs per position. `probeUrlsForLeaf` asks\n * the matcher whether the assembled URL reaches its leaf, and moves to the next\n * candidate when it does not.\n *\n * Segments are read through `effectiveUrlSegment` so an intercepting directory\n * contributes the part the segment it intercepts would.\n */\nfunction probePartFor(node: SegmentNode, value: string): ProbePart {\n const seg = effectiveUrlSegment(node);\n switch (seg.segmentType) {\n case 'dynamic': {\n const prefix = seg.paramPrefix ?? '';\n const suffix = seg.paramSuffix ?? '';\n // The affixes are kept in the shown form too: `x-<any>` says which URLs\n // are contended for, where a bare `<any>` would claim every value at that\n // position.\n return {\n part: `${prefix}${value}${suffix}`,\n shown: `${prefix}<any>${suffix}`,\n dynamic: true,\n };\n }\n case 'catch-all':\n case 'optional-catch-all':\n return { part: value, shown: '<any>', dynamic: true };\n default:\n return { part: seg.segmentName, shown: seg.segmentName, dynamic: false };\n }\n}\n\n/** One URL part of a probe: what to send, what to show, and whether it stands for a value. */\ninterface ProbePart {\n part: string;\n shown: string;\n dynamic: boolean;\n}\n\n/**\n * The URLs that reach a leaf.\n *\n * An **optional catch-all also matches zero parts**, so it answers the URL its\n * parent sits at as well as longer ones. Both are probed: whether the shorter\n * one is actually *its* to answer is `matchUrlParts`' business, not something\n * to decide here — a page above it, or in a route group beside it, may take\n * that URL deterministically, and the probe will simply report the same winner\n * in both orderings.\n */\nfunction probeUrlsFor(chain: SegmentNode[], value: string, longTail: number): ProbeUrl[] {\n const probeParts = chain.map((node) => probePartFor(node, value));\n const url = (ps: ProbePart[]): ProbeUrl => ({\n parts: ps.map((p) => p.part),\n display: ps.map((p) => p.shown),\n dynamic: ps.some((p) => p.dynamic),\n });\n\n // The kind that decides the probe lengths is the last part-CONTRIBUTING\n // segment's, which is why it is read off the chain rather than off the leaf\n // node. An empty chain is the space root's own page: the walk matches parts\n // against that node's children, so the node's own segment consumes nothing\n // and the only URL that reaches the page is the empty one — even when the\n // root is itself a catch-all (`(.)[...rest]`).\n const leaf = chain[chain.length - 1];\n const kind = leaf && effectiveUrlSegment(leaf).segmentType;\n const isCatchAll = kind === 'catch-all' || kind === 'optional-catch-all';\n if (!isCatchAll) return [url(probeParts)];\n\n // `matchNode` accepts a catch-all only as a terminal, so a catch-all that\n // *is* the leaf owns the whole tail of the probe and it can be re-sized\n // freely. (A catch-all higher in the chain has descendants nothing can\n // reach; those leaves are the statically-shadowed case above.)\n const head = probeParts.slice(0, -1);\n const urls = [url(probeParts)];\n\n // A LONG tail as well as a short one. At one part a catch-all loses to any\n // bare dynamic sibling, deterministically — so a one-part probe reports that\n // sibling in both orderings and two catch-alls competing at every longer URL\n // go unnoticed. `longTail` is past every finite branch in the space, where\n // no fixed-length competitor can still be matching.\n if (longTail > 1) {\n // Every part is the candidate value verbatim. Decorating them to differ\n // (`${value}-0`) puts them back inside patterns like `[value]-0/[...rest]`,\n // which outranks a catch-all — the trailing counter reintroduced exactly\n // the affix capture the value is being searched for (codex, on PR #1000).\n // The parts need not differ from each other; a catch-all consumes them all.\n const tail: ProbePart[] = Array.from({ length: longTail }, () => ({\n part: value,\n shown: '<any>',\n dynamic: true,\n }));\n urls.push(url([...head, ...tail]));\n }\n\n // An optional catch-all also matches zero parts.\n if (kind === 'optional-catch-all') urls.push(url(head));\n\n return urls;\n}\n\n/**\n * One more than the longest fixed-length path in the space.\n *\n * A probe of this length cannot be claimed by any branch that consumes a fixed\n * number of URL parts, so whatever matches it is a catch-all — which is\n * exactly the competition a short probe hides.\n */\nfunction longTailLength(node: SegmentNode): number {\n let deepest = 0;\n for (const child of node.children) {\n const own = child.segmentType === 'group' ? 0 : 1;\n deepest = Math.max(deepest, own + longTailLength(child));\n }\n return deepest;\n}\n\n/**\n * The subtree with every `children` array reversed, top to bottom.\n *\n * Nodes are shallow-copied, so files and slots are shared with the original —\n * only the ordering `matchNode` reads is different. Slots are not reversed\n * because `matchNode` never walks them; each slot is validated as its own\n * collision space, with its own reversed clone.\n */\nfunction withReversedChildren(node: SegmentNode): SegmentNode {\n return { ...node, children: [...node.children].reverse().map(withReversedChildren) };\n}\n","/**\n * Pattern intersection for intercepted URL patterns.\n *\n * Checks whether two intercepted URL patterns can both match the same concrete\n * URL. Used by the cross-mode claim check (TIM-1319) to detect when a\n * children-path interceptor and a slot interceptor from the same owner overlap.\n *\n * This is not the order-dependence oracle (`findOrderDependentCollision`) the\n * within-mode collision checks use. `findInterceptionMatch` picks the mode by\n * first-match on `pathnameMatchesPattern`, which has no priority tiers, so a\n * static `/feed/foo` and a dynamic `/feed/[id]` both match `/feed/foo` even\n * though `matchUrlParts` would resolve the static deterministically. The\n * intersection test catches that: it walks both patterns segment by segment and\n * asks whether any value satisfies both.\n *\n * This is not a model of `matchNode` precedence — that decides *which*\n * candidate wins when several match one URL. This decides the prior question:\n * *do* they both match one URL at all? The two questions are independent, and\n * conflating them is what caused TIM-1308's five-round drift.\n */\n\nimport { classifyUrlSegment } from './segment-classify.ts';\n\n/**\n * Check whether two intercepted URL patterns can both match the same concrete\n * URL. Returns a display string for the overlapping URL, or null.\n *\n * Walks both patterns segment by segment:\n * - Two statics: must be identical.\n * - Static + dynamic: static must satisfy the dynamic's affix constraints.\n * - Two dynamics: affix constraints must be compatible.\n * - A catch-all at either side: compatible with everything remaining.\n *\n * If the patterns differ in length, they can only overlap if the longer one's\n * extra part is an optional catch-all (which also matches zero segments).\n */\nexport function interceptedPatternsOverlap(p1: string, p2: string): string | null {\n if (p1 === p2) return p1;\n\n const parts1 = p1 === '/' ? [] : p1.slice(1).split('/');\n const parts2 = p2 === '/' ? [] : p2.slice(1).split('/');\n const display: string[] = [];\n let i = 0;\n\n while (i < parts1.length && i < parts2.length) {\n const seg1 = classifyUrlSegment(parts1[i]);\n const seg2 = classifyUrlSegment(parts2[i]);\n\n if (\n seg1.kind === 'catch-all' ||\n seg1.kind === 'optional-catch-all' ||\n seg2.kind === 'catch-all' ||\n seg2.kind === 'optional-catch-all'\n ) {\n display.push('<any>');\n return '/' + display.join('/');\n }\n\n const part = segmentsOverlap(seg1, seg2);\n if (part === null) return null;\n display.push(part);\n i++;\n }\n\n const remaining = i < parts1.length ? parts1 : i < parts2.length ? parts2 : null;\n if (remaining) {\n const next = classifyUrlSegment(remaining[i]);\n if (next.kind !== 'optional-catch-all') return null;\n }\n\n return display.length === 0 ? '/' : '/' + display.join('/');\n}\n\n/**\n * Can some concrete value satisfy both classified URL segments?\n *\n * An affixed dynamic (`a-[id].png`) constrains both the prefix and suffix of\n * the value, so a static segment must satisfy those constraints and two\n * dynamics must have compatible affixes — prefixes where one extends the other,\n * and likewise for suffixes. Without this check, `/feed/photo.jpg` beside\n * `/feed/asset-[id].png` would be falsely rejected.\n */\nfunction segmentsOverlap(\n seg1: ReturnType<typeof classifyUrlSegment>,\n seg2: ReturnType<typeof classifyUrlSegment>\n): string | null {\n if (seg1.kind === 'static' && seg2.kind === 'static') {\n return seg1.value === seg2.value ? seg1.value : null;\n }\n\n if (seg1.kind === 'static' && seg2.kind === 'dynamic') {\n return staticMatchesAffix(seg1.value, seg2) ? seg1.value : null;\n }\n if (seg2.kind === 'static' && seg1.kind === 'dynamic') {\n return staticMatchesAffix(seg2.value, seg1) ? seg2.value : null;\n }\n\n if (seg1.kind === 'dynamic' && seg2.kind === 'dynamic') {\n return dynamicAffixesCompatible(seg1, seg2) ? '<any>' : null;\n }\n\n return '<any>';\n}\n\n/**\n * Does a static value satisfy an affixed dynamic's prefix, suffix, and\n * non-empty capture constraints?\n *\n * Mirrors the check `pathnameMatchesPattern` applies: the value must start with\n * the prefix, end with the suffix, and be strictly longer than their combined\n * length (a capture of zero characters is not a match).\n */\nfunction staticMatchesAffix(value: string, dynamic: { prefix?: string; suffix?: string }): boolean {\n const prefix = dynamic.prefix ?? '';\n const suffix = dynamic.suffix ?? '';\n if (!prefix && !suffix) return true;\n if (prefix && !value.startsWith(prefix)) return false;\n if (suffix && !value.endsWith(suffix)) return false;\n return value.length > prefix.length + suffix.length;\n}\n\n/**\n * Can some value simultaneously satisfy two affixed dynamics?\n *\n * Two prefix constraints are compatible when one extends the other (a value\n * starting with the longer one also starts with the shorter). Suffixes are\n * analogous. Without affixes on either side, every value works.\n */\nfunction dynamicAffixesCompatible(\n d1: { prefix?: string; suffix?: string },\n d2: { prefix?: string; suffix?: string }\n): boolean {\n const p1 = d1.prefix ?? '',\n s1 = d1.suffix ?? '';\n const p2 = d2.prefix ?? '',\n s2 = d2.suffix ?? '';\n\n if (p1 && p2 && !p1.startsWith(p2) && !p2.startsWith(p1)) return false;\n if (s1 && s2 && !s1.endsWith(s2) && !s2.endsWith(s1)) return false;\n return true;\n}\n","/**\n * Intercepting route utilities.\n *\n * Computes rewrite rules from the route tree that enable intercepting routes\n * to conditionally render when navigating via client-side (soft) navigation.\n *\n * The mechanism: at build time, each intercepting route directory generates a\n * conditional rewrite. On soft navigation, the client sends an `X-Timber-URL`\n * header with the current pathname. The server checks if any rewrite's source\n * (the intercepted URL) matches the target pathname AND the header matches\n * the intercepting route's parent URL. If both match, the intercepting route\n * renders instead of the normal route.\n *\n * On hard navigation (no header), no rewrite matches, and the normal route\n * renders.\n *\n * See design/07-routing.md §\"Intercepting Routes\"\n */\n\nimport type { SegmentNode, InterceptionMarker, RouteFile } from './types.ts';\nimport { computeSegmentTreePaths, treePathDepth } from './segment-keys.ts';\nimport { classifyUrlSegment, effectiveUrlSegment } from './segment-classify.ts';\n// Every collision space — the main tree, each slot, each intercepting subtree\n// — decides order-dependence with the same oracle, so the spaces cannot drift\n// on what counts as two pages answering one URL. See TIM-1308.\nimport { findOrderDependentCollision, type ProbeLeaf } from './collision-probe.ts';\nimport { interceptedPatternsOverlap } from './interception-overlap.ts';\n\n/** Fields shared by both interception shapes. */\ninterface InterceptionRewriteBase {\n /**\n * The URL pattern that this rewrite intercepts (the target of navigation).\n * E.g., \"/photo/[id]\" for a (.)photo/[id] interception.\n */\n interceptedPattern: string;\n /**\n * The tree path of the segment that OWNS the slot holding this\n * intercepting route (`computeSegmentTreePaths`, e.g. \"/feed\",\n * \"/(browse)\", \"/(browse)/feed\", or \"/\" at the app root).\n *\n * The rewrite only applies when the source route — the page the user is\n * navigating FROM — passes through that directory, because only then\n * does the slot holding the intercepting page exist in the rendered\n * tree. The server checks this by matching X-Timber-URL and testing its\n * segment chain's tree paths for this value.\n *\n * Neither of the identities already in the codebase works here:\n *\n * - A **URL prefix** cannot express it. Route groups contribute nothing\n * to `urlPath`, so `(browse)/@detail/(...)x` and a genuinely\n * root-level `@modal/(...)x` both have prefix \"/\" while meaning very\n * different things.\n * - A **state-tree key** (`computeSegmentKeys`) cannot either. It\n * qualifies a group with its ancestry but resets to `urlPath` at the\n * next URL-visible segment, so `(browse)/feed` and `(landing)/feed`\n * both key as \"/feed\" — and a navigation inside `(landing)/feed/…`\n * would reach `(browse)/feed`'s slot.\n *\n * Tree paths never reset, so two directories that render different\n * layouts never share one. \"/\" as a scope means the app root, which\n * every route passes through — preserving the intentional\n * match-from-anywhere behavior of a root-level `(...)` interception\n * (TIM-1006) without an exemption. See TIM-1279.\n */\n interceptingScope: string;\n}\n\n/**\n * A conditional rewrite rule generated from an intercepting route.\n *\n * `mode` records which of the two shapes the directory expresses, because the\n * two render through entirely different mechanisms and the fields they need\n * are not the same. It is a union rather than a flag so a reader cannot\n * re-match `interceptingPrefix` on the shape that has no such pathname.\n *\n * See design/07-routing.md §\"Intercepting Routes\".\n */\nexport type InterceptionRewrite = SlotInterceptionRewrite | ChildrenInterceptionRewrite;\n\n/**\n * A **modal overlay** interception — the intercepting directory is a direct\n * child of a parallel slot (`feed/@modal/(.)photo/[id]`).\n *\n * The server re-matches the *source* route and the slot resolver finds the\n * intercepting child inside the slot, so the intercepting page layers over\n * the page the user was already on.\n */\nexport interface SlotInterceptionRewrite extends InterceptionRewriteBase {\n mode: 'slot';\n /**\n * The pathname the server re-matches when this rewrite applies — the\n * intercepting slot owner's URL path. E.g., \"/feed\" for a (.)photo/[id]\n * inside /feed/@modal/.\n *\n * This is a build-time constant, never a client-supplied value: the\n * X-Timber-URL header selects a rewrite but never becomes the rendered\n * pathname.\n */\n interceptingPrefix: string;\n}\n\n/**\n * A **sibling layout swap** interception — the intercepting directory is a\n * direct child of an ordinary segment or route group\n * (`(browse)/(...)[artistSlug]/…`).\n *\n * There is no overlay, no slot and no `default.tsx`: the intercepting page\n * *replaces* `children` for the owner's layout chain. Nothing is re-matched\n * either. The server resolves the owner by `interceptingScope` — a tree path,\n * which identifies exactly one directory — and builds the rendered chain as\n * `[…owner ancestors, interceptingNode, …its matched descendants]`. That is an\n * ordinary segment chain, so `route-element-builder.ts` renders it with no\n * knowledge that interception happened.\n *\n * Resolving the owner from the tree rather than from a URL is what makes this\n * shape work where the modal shape cannot: `interceptingPrefix` is\n * URL-derived, and a route group contributes nothing to `urlPath`, so an owner\n * like `app/(browse)/` serializes to \"/\" and re-matching it lands in whichever\n * group owns the canonical page. A tree path never collapses (TIM-1280).\n */\nexport interface ChildrenInterceptionRewrite extends InterceptionRewriteBase {\n mode: 'children';\n}\n\n/**\n * A set of sibling interceptors that compete for one URL position.\n *\n * `base` is the intercepted base they share — the URL their own segments sit\n * directly under — and `anchorIndex` is how many URL parts that base spans,\n * which is the index in a target pathname their segments are matched at.\n * Deeper anchors are more local and are tried first.\n */\nexport interface InterceptorAnchorGroup<TFile> {\n base: string;\n anchorIndex: number;\n interceptors: SegmentNode<TFile>[];\n}\n\n/**\n * Group an owner's intercepting children by the URL position they contend for.\n *\n * This is the definition of *which interceptors compete*, and it has exactly\n * one caller shape on each side of the build: `findInterceptingMatch` resolves\n * a group with one `matchUrlParts` walk, and\n * `assertNoInterceptedPatternCollisions` validates the same group as one\n * collision space. Written twice, the two would be free to disagree about what\n * competes — and a resolver that walks a wider set than the validator checked\n * is precisely the order-dependence the validator exists to reject (TIM-1310).\n *\n * Contention is keyed by the computed intercepted **base**, not by the marker:\n * under an owner at `/feed`, `(..)` and `(...)` both base at `/` and are both\n * matched at index 0, so they are one group. Different bases are different\n * positions — a `(.)` interceptor can never claim a target that belongs to a\n * `(..)` one beside it, because the base must also match the target's leading\n * parts.\n *\n * The base is a URL *pattern*, so its length is its part count only because\n * every pattern part stands for exactly one URL part. A catch-all in the base\n * breaks that, and `assertInterceptedBaseIsCountable` rejects the shape at\n * build time.\n *\n * A node missing either marker field is skipped, which `classifySegment` never\n * produces — it sets both whenever it reports `'intercepting'`. Stating the\n * filter here rather than defaulting the marker is what makes the validator and\n * the resolver see the same set: such a node renders nothing either way, and a\n * validator more permissive than the resolver would be checking a candidate\n * that never competes while the resolver walks one nobody checked.\n *\n * `children` is read directly rather than flattened through route groups,\n * matching both resolvers: the slot resolver reads `slotNode.children`, and an\n * interceptor deeper inside a slot subtree is rejected by\n * `validateInterceptionPlacement`. On the children path an interceptor under a\n * route group is legal and the *group* is its owner, so it is grouped with that\n * group's other interceptors — which is again exactly what the resolver walks,\n * since it resolves the owner by tree path.\n */\nexport function groupByInterceptedAnchor<TFile>(\n ownerUrlPath: string,\n children: readonly SegmentNode<TFile>[]\n): InterceptorAnchorGroup<TFile>[] {\n const byBase = new Map<string, InterceptorAnchorGroup<TFile>>();\n\n for (const child of children) {\n if (child.segmentType !== 'intercepting') continue;\n if (!child.interceptionMarker || !child.interceptedSegmentName) continue;\n\n const base = computeInterceptedBase(ownerUrlPath, child.interceptionMarker);\n const group = byBase.get(base);\n if (group) {\n group.interceptors.push(child);\n } else {\n byBase.set(base, {\n base,\n anchorIndex: base === '/' ? 0 : base.slice(1).split('/').length,\n interceptors: [child],\n });\n }\n }\n\n // Deepest anchor first: a `(.)` interception is the most local one and wins\n // over a `(...)` that reaches the same page from the root.\n return [...byBase.values()].sort((a, b) => b.anchorIndex - a.anchorIndex);\n}\n\n/**\n * Collect all interception rewrite rules from the route tree.\n *\n * Walks the tree recursively. For each intercepting segment, computes the\n * intercepted URL based on the marker and the segment's position.\n *\n * The list is returned **deepest scope first**, which is what makes\n * `findInterceptionMatch`'s first-match rule deterministic; see\n * `sortByScopeDepth`.\n */\nexport function collectInterceptionRewrites(root: SegmentNode): InterceptionRewrite[] {\n const rewrites: InterceptionRewrite[] = [];\n walkForInterceptions(root, [root], false, rewrites);\n return sortByScopeDepth(rewrites);\n}\n\n/**\n * Order the rewrites deepest scope first, so the request-time first-match rule\n * resolves two applicable rewrites by *nesting* rather than by tree-walk order.\n *\n * `findInterceptionMatch` returns the first rewrite whose pattern matches the\n * target and whose scope the source route reports. More than one can satisfy\n * both, and the case is not exotic — an owner and a directory *beneath* it may\n * each hold an interceptor for one URL:\n *\n * ```\n * app/feed/(.)photo/page.tsx scope /feed\n * app/feed/(a)/(.)photo/page.tsx scope /feed/(a)\n * ```\n *\n * A page inside `(a)` renders both owners' layouts, so the client reports both\n * tree paths and both scopes pass. Unsorted, the winner is whichever the\n * filesystem enumerated first — renaming the group flips which page a click\n * opens, the defect class TIM-1305/TIM-1308/TIM-1310 each rejected in a\n * different space. Here it is an ambiguity between *rewrites* rather than\n * between pages inside one walk, so no collision space sees it.\n *\n * **Deepest wins, and it is specified rather than rejected** because the shape\n * is one a correct app writes on purpose: a nested layout refining the\n * interception its parent declares, exactly as `groupByInterceptedAnchor`\n * already prefers the deepest *anchor* within one owner. The two rules are the\n * same preference — the more local interception is the author's more specific\n * statement — applied at the two levels the mechanism has.\n *\n * Depth is counted on the **tree path**, the identity that never collapses\n * (TIM-1279/TIM-1280); a `urlPath` prefix test would relate `(browse)` and\n * `(landing)`, which render different layouts and share no chain. Two scopes on\n * one rendered chain are strictly nested, so depth orders them totally, and two\n * scopes that are *not* on one chain — sibling groups, `feed/(a)` beside\n * `feed/(b)` — never both pass the scope check, so their relative order is not\n * observable. That is why this sorts on depth alone rather than testing\n * ancestry: it is a total order over the pairs that can compete, and it costs\n * nothing per request.\n *\n * Sorting is stable, so rewrites sharing one scope keep tree-walk order. Equal\n * scopes mean one owner, and of the three pairings that produces only one was\n * ambiguous — a children-path interceptor beside a slot one — and that is now\n * rejected at build time by `assertNoCrossModeClaims` (TIM-1319). The\n * enumeration is in design/07-routing.md §\"Selection Between Owners\" rather\n * than repeated here, so there is one place to correct if a rewrite ever grows\n * a field that makes two of them distinguishable.\n */\nfunction sortByScopeDepth(rewrites: InterceptionRewrite[]): InterceptionRewrite[] {\n return rewrites.sort(\n (a, b) => treePathDepth(b.interceptingScope) - treePathDepth(a.interceptingScope)\n );\n}\n\n/**\n * Validate that every intercepting segment sits where it can actually render.\n *\n * There are exactly two positions a renderer exists for, and they are the two\n * shapes interception has:\n *\n * 1. **Direct child of a parallel slot** — the modal overlay. The slot\n * resolver scans a slot node's *direct children* for an intercepting\n * segment (`server/slot-interception.ts`). Nothing between the slot and\n * the intercepting directory is allowed, because nothing walks it.\n * 2. **Direct child of an ordinary segment or route group** — the sibling\n * layout swap (TIM-1280). The pipeline resolves the owner by tree path and\n * builds the rendered chain through the intercepting node itself\n * (`server/children-interception.ts`).\n *\n * Everywhere else, rewrite generation still emits a rewrite and nothing\n * renders it: the rewrite matches the soft navigation, and the user is served\n * the page they were already on under a changed URL (TIM-1278). The rejected\n * positions are:\n *\n * - **Inside a slot subtree but not directly under the slot**\n * (`@modal/(inner)/(.)photo`). Neither renderer applies: the slot resolver\n * only reads direct children, and the children-path resolver builds a\n * *chain*, which cannot express a descent through a slot.\n * - **Inside another intercepting subtree** (`(.)a/(.)b`). Left unvalidated\n * `collectLeavesWithRewrites` walks the inner one as an ordinary URL\n * segment and emits a pattern containing a literal \"(.)b\".\n *\n * Two further shapes are rejected here (TIM-1288). Both are about a catch-all,\n * and both are specific to interception: elsewhere in the tree the same\n * shapes are inert or fail safely, and the general \"nothing below a catch-all\n * is reachable\" rule is tracked as TIM-1286.\n *\n * And one is specific to the children path: an owner chain that binds params\n * (see `assertOwnerChainBindsNoParams`).\n */\nexport function validateInterceptionPlacement(root: SegmentNode): void {\n walkForPlacement(root, '', {\n parentIsSlot: false,\n inSlotSubtree: false,\n inInterceptingSubtree: false,\n ancestorsBindParams: false,\n });\n}\n\n/**\n * Reject a children-path interception whose owner chain binds params.\n *\n * The rendered chain is `[…owner ancestors, interceptingNode, …descendants]`,\n * and the only pathname available to bind params from is the **target** — the\n * URL being navigated to. The intercepting subtree's own params come from it\n * by construction. The owner chain's do not, in general:\n *\n * - With a climbing marker the owner's URL segments are not in the target at\n * all. `app/[team]/(..)photo/[id]` bases at \"/\", so the target is\n * `/photo/9` — there is no team value anywhere in it, and `[team]`'s layout\n * would render with the param it declares missing.\n * - With `(.)` the base *is* the owner's `urlPath`, so the target's leading\n * parts would bind it. Supporting only that half means one marker behaves\n * differently from its siblings for reasons the author cannot see from the\n * directory names, so the rule is stated once, on the owner chain.\n *\n * A layout that silently renders without the param it declares is the failure\n * this prevents; it is rejected where it can be explained rather than left to\n * surface as `undefined` deep inside user code. Route groups and static\n * segments bind nothing and are unrestricted, which is the whole motivating\n * shape (`app/(browse)/(...)[artistSlug]/…`).\n *\n * The rule stops at a slot boundary: a slot-mode interception re-matches the\n * *source* route, which supplies the owner's params, so a dynamic owner is\n * fine there and always has been.\n */\n/**\n * Require a children-path interception's owner to have a `layout.tsx`.\n *\n * The scope is the owner's tree path, and the client reports which tree paths\n * it has mounted from `X-Timber-Segments` — which carries **one entry per\n * layout-bearing segment**, because those are the segments the client needs an\n * outlet and a cache node for. `buildSegmentInfo` skips the rest.\n *\n * For the modal shape that set is exactly the set of possible scope owners, by\n * construction: a slot is only resolved inside a rendering layout\n * (`route-element-builder.ts`), so a layoutless segment can never own one. A\n * children-path interception broke that equivalence — its owner is an ordinary\n * directory and needs no layout.\n *\n * Left unchecked the result is a swap that works exactly once: the first soft\n * navigation renders, then the client reports no such tree path, and the next\n * navigation fails the scope check and falls back to the canonical page. That\n * is fail-safe but silent, and the address-bar channel cannot recover it —\n * once the swap is on screen the address holds the *intercepted* URL, which\n * re-matches the canonical chain (the TIM-1282 problem, in the one shape the\n * rendered-scope fix does not reach).\n *\n * Requiring the layout restores the invariant the state tree is built on\n * rather than widening the wire format to carry segments that own no outlet\n * and no payload. It also costs the author nothing they were not already\n * doing: the point of this shape is to render inside the owner's own layout,\n * so an owner with no layout has no chrome to preserve.\n */\nfunction assertOwnerHasLayout(ownerPath: string, childPath: string): void {\n const owner = ownerPath || '(app root)';\n throw new Error(\n `Build error: intercepting route whose owner has no layout.\\n` +\n ` Directory: app/${childPath}\\n` +\n ` Owner: app/${owner}\\n` +\n `An intercepting directory outside a parallel slot renders inside its owner's ` +\n `layout chain, and the owner must have a layout.tsx: the client reports which ` +\n `directories it has mounted from its layout segments, and a scope it cannot ` +\n `report would let the interception fire once and then silently stop.\\n` +\n `Add app/${owner}/layout.tsx (a pass-through returning {children} is enough), or ` +\n `move the interception under a directory that has one.\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n}\n\nfunction assertOwnerChainBindsNoParams(childPath: string, marker: InterceptionMarker): void {\n throw new Error(\n `Build error: intercepting route under a dynamic segment.\\n` +\n ` Directory: app/${childPath}\\n` +\n `An intercepting directory outside a parallel slot renders inside its own ` +\n `layout chain, and the only pathname available to bind params from is the ` +\n `intercepted URL — which does not carry the values of the segments above ` +\n `\"${marker}\".\\n` +\n `Move the interception so every directory above it is a static segment or a ` +\n `route group, or place it inside a parallel slot (@modal/) to render it as an ` +\n `overlay over the source route instead.\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n}\n\n/**\n * Reject an intercepting route whose intercepted level is not well-defined.\n *\n * \"One level up\" is counted in URL segments, which works because every\n * pattern part stands for exactly one URL part — except a catch-all, which\n * stands for a number that varies per request. `anchorIndex` fixes the anchor\n * at the base's pattern length, so with an owner at `/docs/[...path]` the\n * interception resolves when the catch-all consumed exactly one segment\n * (`/docs/a/photo/5`) and is dropped when it consumed more\n * (`/docs/a/b/photo/5`), because the base no longer matches the target's\n * leading parts.\n *\n * Dropping is fail-safe — the canonical route renders, as on hard navigation\n * — so this is never a wrong page. But it is a modal that opens at one depth\n * and silently does not at another, with nothing to tell the author why, and\n * no request-time input can fix it: the only available source depth is\n * `X-Timber-URL`, which is the browser's address bar, and once a modal is\n * open that holds the *intercepted* URL rather than the source.\n *\n * So the shape is rejected where it can be explained. A marker that climbs\n * *past* the catch-all is fine — `(..)` from `/docs/[...path]` bases at\n * `/docs` — and so is `(...)`, which always bases at the root. The rule is on\n * the *computed* base, not on having a catch-all anywhere above.\n */\nfunction assertInterceptedBaseIsCountable(\n child: SegmentNode,\n ownerUrlPath: string,\n childPath: string\n): void {\n const marker = child.interceptionMarker ?? '(.)';\n const base = computeInterceptedBase(ownerUrlPath, marker);\n const baseParts = base === '/' ? [] : base.slice(1).split('/');\n const ambiguous = baseParts.find((part) => {\n const kind = classifyUrlSegment(part).kind;\n return kind === 'catch-all' || kind === 'optional-catch-all';\n });\n if (!ambiguous) return;\n\n throw new Error(\n `Build error: intercepting route below a catch-all segment.\\n` +\n ` Directory: app/${childPath}\\n` +\n ` Intercepted base: ${base}\\n` +\n `A catch-all (${ambiguous}) matches a number of URL segments that varies per ` +\n `request, so \"${marker}\" cannot identify which segment of a target pathname the ` +\n `interception starts at — the modal would open at one depth and silently not at ` +\n `another.\\n` +\n `Move the interception above the catch-all, or use a marker that climbs past it ` +\n `((..) or (...)).\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n}\n\n/**\n * Reject a catch-all inside an intercepting subtree that has directories\n * below it.\n *\n * A catch-all consumes the whole remaining path, so nothing under it is\n * reachable — `matchUrlParts` only accepts a catch-all as a terminal leaf and\n * never descends past one. Rewrite generation is not so strict: it walks the\n * subtree and emits `/feed/[...rest]/child`, and `pathnameMatchesPattern`\n * returns as soon as it reaches the catch-all without ever looking at\n * `child`. So the rewrite matches the navigation and then no renderer exists\n * — the TIM-1278 failure mode reached by a different route, with the user\n * served the page they were already on under a changed URL.\n *\n * The check covers the whole intercepting subtree, not just the intercepted\n * segment: `(.)photo/[...rest]/child` emits `/feed/photo/[...rest]/child` and\n * hijacks `/feed/photo/a/b` by exactly the same mechanism. And it does not\n * exempt a catch-all that carries its own page — that one does render, but\n * the rewrite generated for the unreachable descendant is broader than any\n * route in the tree, so the tail modal fires on navigations no directory\n * describes.\n *\n * Outside a slot the same shape merely 404s, which is why the general rule is\n * tracked separately (TIM-1286); here it is unsafe, and it only became\n * reachable when catch-all intercepted segments started matching at all\n * (TIM-1281).\n *\n * The walk descends `children` and not `slots`, mirroring\n * `collectLeavesWithRewrites` exactly: what this rejects is precisely the set\n * of paths rewrite generation emits, so the two cannot disagree about which\n * shapes exist. A slot nested inside an intercepting subtree produces no\n * rewrite and so cannot hijack anything.\n */\nfunction assertInterceptedCatchAllsAreTerminal(node: SegmentNode, nodePath: string): void {\n const { segmentType, segmentName } = effectiveUrlSegment(node);\n const isCatchAll = segmentType === 'catch-all' || segmentType === 'optional-catch-all';\n\n if (isCatchAll && node.children.length > 0) {\n throw new Error(\n `Build error: catch-all with nested segments inside an intercepting route.\\n` +\n ` Directory: app/${nodePath}\\n` +\n ` Nested: ${node.children.map((c) => c.segmentName).join(', ')}\\n` +\n `A catch-all (${segmentName}) consumes every remaining URL segment, so nothing ` +\n `below it can ever match — but the interception rewrite would still be generated ` +\n `and would hijack the navigation with no page to render.\\n` +\n `Move the nested routes above the catch-all, or intercept a named segment instead.\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n }\n\n for (const child of node.children) {\n assertInterceptedCatchAllsAreTerminal(child, `${nodePath}/${child.segmentName}`);\n }\n}\n\n/**\n * Reject a `route.ts` anywhere inside an intercepting subtree.\n *\n * An interception is a soft-navigation variant of a page — it swaps which\n * page component renders. A route handler returns a `Response` and cannot\n * be an interception target: `walkInterceptedLeaves` visits `node.page`\n * only, so no rewrite is emitted for it, but `isTerminal` in\n * `tree-match.ts` counts both `page` and `route`, so `matchUrlParts` can\n * resolve to the route handler. `slot-interception.ts` then drops the\n * match (`if (!leaf.page) continue`), silently disabling the page's\n * interception. Fail-safe (the canonical route renders) but silent.\n *\n * The walk mirrors `assertInterceptedCatchAllsAreTerminal`: it descends\n * `children` (not `slots`), matching `collectLeavesWithRewrites` exactly.\n */\nfunction assertNoRouteHandlersInSubtree(node: SegmentNode, nodePath: string): void {\n if (node.route) {\n throw new Error(\n `Build error: route handler inside an intercepting route.\\n` +\n ` File: ${node.route.filePath}\\n` +\n ` Directory: app/${nodePath}\\n` +\n `An intercepting subtree renders a page component on soft navigation — a ` +\n `route handler returns a Response and cannot be an interception target. ` +\n `The route handler would silently shadow the interception: the resolver ` +\n `would match it and then discard it because it has no page, so the modal ` +\n `would stop opening with no diagnostic.\\n` +\n `Remove the route.ts, or move it outside the intercepting subtree.\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n }\n\n for (const child of node.children) {\n assertNoRouteHandlersInSubtree(child, `${nodePath}/${child.segmentName}`);\n }\n}\n\n/**\n * Reject two pages that answer one URL among the interceptors an owner\n * resolves in a single walk.\n *\n * `validateRouteGroupCollisions` in the scanner returns early on an\n * intercepting node, so nothing else validates in here. That exemption exists\n * for a real reason — an interception's computed URL legitimately mirrors the\n * canonical route it replaces, and interceptors at *different* anchors\n * deliberately target one URL, which the resolver then orders (deepest anchor\n * first). But the exemption was broader than its reason. Two shapes slip\n * through it, and they are the same shape seen from two levels:\n *\n * feed/(.)photo/(a)/page.tsx feed/@modal/(.)[a]/page.tsx\n * feed/(.)photo/(b)/page.tsx feed/@modal/(.)[b]/page.tsx\n *\n * Both pairs answer one URL, and in both `matchUrlParts` resolves the tie by\n * whichever `children` array lists first — so the winner is whichever the\n * filesystem enumerated first, and renaming a directory flips it. That is\n * exactly what `validateRouteGroupCollisions` and\n * `validateAffixedSiblingConflicts` exist to reject for ordinary siblings.\n *\n * **The collision space is the anchor group, not the directory.** An earlier\n * revision ran once per intercepting directory, which sees the left-hand pair\n * and not the right-hand one: those leaves live in two different directories.\n * The space is `groupByInterceptedAnchor`'s output — the same grouping\n * `findInterceptingMatch` hands to one `matchUrlParts` call — so what is\n * validated together is exactly what is resolved together. Interceptors at\n * different anchors stay independent, which is the documented case, and two\n * slots on one owner are two owners and so two spaces.\n *\n * **Two checks, and the difference between them matters.** A duplicate\n * intercepted *pattern* is decided by string equality, which is exact: two\n * leaves with one pattern claim one set of URLs, whatever the precedence rules\n * are. Everything else is decided by asking the matcher — `collision-probe.ts`,\n * the same oracle the main tree and every slot use, so the spaces cannot\n * disagree about whether a pair of pages collides.\n *\n * Not covered: two affixed dynamics that tie on specificity (`(x)/a-[id]` vs\n * `(y)/[slug]-b`, ambiguous only for a part shaped `a-…-b`). No single probe\n * URL exercises both, and `validateAffixedSiblingConflicts` already rejects\n * that pair by affix length — including through groups, inside intercepting\n * subtrees, and between two interceptors sharing an anchor.\n *\n * The shape applies to both interception mechanisms — a slot interceptor and a\n * children-path one produce their leaves through the same walk, and both are\n * resolved by `findInterceptingMatch` — so this is called from the one place in\n * `walkForPlacement` that sees every owner regardless of position.\n */\nfunction assertNoInterceptedPatternCollisions(owner: SegmentNode, ownerPath: string): void {\n for (const group of groupByInterceptedAnchor(owner.urlPath, owner.children)) {\n assertAnchorGroupHasNoCollisions(group, ownerPath);\n }\n}\n\n/**\n * Reject a children-path interceptor and a slot interceptor from the same owner\n * that claim URLs in common.\n *\n * Within one mode the ambiguity is either non-existent (two children-path\n * rewrites are interchangeable, §\"Selection Between Owners\") or harmless (two\n * slots on one owner both render). Across modes one is a layout swap and the\n * other is a modal overlay, and the walk order picks the children-path shape —\n * making the modal dead, silently and deterministically (TIM-1319).\n *\n * Overlap is a **pattern intersection** test: do two intercepted URL patterns\n * share any concrete URL? This is not the same question the within-mode\n * collision oracle answers. `findOrderDependentCollision` checks whether two\n * candidates in one `matchUrlParts` walk produce different winners when\n * `children` order is reversed — that is, order-dependence. A static segment\n * always beats a dynamic one deterministically (no order-dependence), so\n * `/feed/foo` and `/feed/[id]` pass the oracle even though\n * `pathnameMatchesPattern` — the actual rewrite matcher — accepts both. The\n * pattern intersection test catches that: it walks both patterns segment by\n * segment and asks whether any value satisfies both.\n *\n * The check collects intercepted patterns from children-path interceptors and\n * from each slot's interceptors independently. Slots are checked one at a time\n * so two slot interceptors from *different* slots claiming one URL — the legal\n * \"two slots on one owner\" case — are never compared.\n */\nfunction assertNoCrossModeClaims(node: SegmentNode, ownerPath: string): void {\n // Collect children-path interceptor patterns\n const childrenLeaves: Array<{ pattern: string; filePath: string }> = [];\n for (const child of node.children) {\n if (\n child.segmentType !== 'intercepting' ||\n !child.interceptionMarker ||\n !child.interceptedSegmentName\n )\n continue;\n const urlBase = computeInterceptedUrlBase(\n node.urlPath,\n child.interceptionMarker,\n child.interceptedSegmentName\n );\n walkInterceptedLeaves(child, urlBase, [child], (page, leaf) => {\n childrenLeaves.push({ pattern: leaf.interceptedPattern, filePath: page.filePath });\n });\n }\n\n if (childrenLeaves.length === 0) return;\n\n const owner = ownerPath || '(app root)';\n\n for (const slot of Object.values(node.slots)) {\n const slotLeaves: Array<{ pattern: string; filePath: string }> = [];\n for (const child of slot.children) {\n if (\n child.segmentType !== 'intercepting' ||\n !child.interceptionMarker ||\n !child.interceptedSegmentName\n )\n continue;\n const urlBase = computeInterceptedUrlBase(\n node.urlPath,\n child.interceptionMarker,\n child.interceptedSegmentName\n );\n walkInterceptedLeaves(child, urlBase, [child], (page, leaf) => {\n slotLeaves.push({ pattern: leaf.interceptedPattern, filePath: page.filePath });\n });\n }\n\n for (const cl of childrenLeaves) {\n for (const sl of slotLeaves) {\n const overlap = interceptedPatternsOverlap(cl.pattern, sl.pattern);\n if (overlap !== null) {\n throwCrossModeClaim(owner, overlap, cl.filePath, sl.filePath);\n }\n }\n }\n }\n}\n\nfunction throwCrossModeClaim(\n owner: string,\n url: string,\n childFile: string,\n slotFile: string\n): never {\n throw new Error(\n `Build error: a children-path interception and a slot interception both ` +\n `claim the same URL.\\n` +\n ` Owner: app/${owner}\\n` +\n ` URL: ${url}\\n` +\n ` Children: ${childFile}\\n` +\n ` Slot: ${slotFile}\\n` +\n `The children-path interceptor replaces the owner's children (layout swap) ` +\n `while the slot interceptor renders inside a parallel slot (modal overlay). ` +\n `Both apply from the same scope at the same URL, and which one wins would ` +\n `depend on internal walk order rather than an explicit rule.\\n` +\n `Remove one of the two, or move them to different scopes.\\n` +\n `See design/07-routing.md §\"Selection Between Owners\".`\n );\n}\n\n/**\n * The URL-matching identity of an intercepting node, as a node.\n *\n * `matchUrlParts` reads `segmentType` directly and knows nothing about\n * interception, so a marker'd directory has to be handed to it under the\n * identity of the segment it intercepts — `(.)[id]` as `[id]`. Priority\n * ordering, affix stripping and catch-all handling then come from the shared\n * walker rather than from a second classifier here.\n *\n * Shared with `findInterceptingMatch` for the same reason the grouping is: the\n * collision space is the resolver's synthetic anchor parent, so its children\n * have to carry the identity the resolver gives them or the validator would be\n * probing a tree that does not match the one served. The caller keeps its own\n * map back to the real node — the proxy is a matching identity, not a\n * replacement (tree paths still need the directory name).\n */\nexport function interceptorProxy<TFile>(node: SegmentNode<TFile>): SegmentNode<TFile> {\n return { ...node, ...effectiveUrlSegment(node) };\n}\n\nfunction assertAnchorGroupHasNoCollisions(\n group: InterceptorAnchorGroup<RouteFile>,\n ownerPath: string\n): void {\n const { base } = group;\n const owner = ownerPath || '(app root)';\n\n // The space the resolver walks: a synthetic parent sitting at the anchor\n // base, holding every interceptor of this group under its matching identity.\n // `matchUrlParts` starts below this node, so the interceptors' own segments\n // are matched parts here — which is what lets a probe reach across two\n // directories and find the pair that competes.\n const proxies = group.interceptors.map(interceptorProxy);\n const anchorNode: SegmentNode = {\n segmentName: '',\n segmentType: 'static',\n urlPath: base,\n children: proxies,\n slots: {},\n };\n\n const patterns = new Map<string, string>();\n const probeLeaves: ProbeLeaf[] = [];\n\n for (const proxy of proxies) {\n // The proxy's name, not the directory's `interceptedSegmentName`: they are\n // the same string, and this is the one `matchUrlParts` matches on, so the\n // reported pattern and the probed URL cannot describe different segments.\n const pattern = joinInterceptedBase(base, proxy.segmentName);\n walkInterceptedLeaves(proxy, pattern, [proxy], (page, leaf) => {\n // Two leaves whose intercepted pattern is the *same string* claim exactly\n // the same set of URLs — one is ambiguous with the other, or dead. This\n // needs no probe and no reasoning about precedence: identical patterns are\n // identical URL sets by definition. It is the case route groups make easy\n // to write, `(a)/page.tsx` beside `(b)/page.tsx`, and the one the main\n // tree already rejects by comparing urlPath. Across two interceptors it\n // is `(..)photo` beside `(...)photo` under `/feed`, which both base at\n // `/` and both intercept the literal `photo`.\n const duplicate = patterns.get(leaf.interceptedPattern);\n if (duplicate) {\n throw new Error(\n `Build error: route collision between intercepting routes — two pages intercept the same URL.\\n` +\n ` Owner: app/${owner}\\n` +\n ` Intercepted base: ${base}\\n` +\n ` URL: ${leaf.interceptedPattern}\\n` +\n ` File 1: ${duplicate}\\n` +\n ` File 2: ${page.filePath}\\n` +\n `A route group is transparent for matching, so moving a page into one does not ` +\n `give it a URL of its own — these two claim the same one. Rename or move one of ` +\n `them.\\n` +\n `See design/07-routing.md §\"Collisions Among Intercepting Routes\".`\n );\n }\n patterns.set(leaf.interceptedPattern, page.filePath);\n probeLeaves.push({ filePath: page.filePath, chain: leaf.chain });\n });\n }\n\n // Differently-spelled patterns that nonetheless compete: ask the matcher.\n const collision = findOrderDependentCollision(anchorNode, probeLeaves, base);\n if (collision) {\n throw new Error(\n `Build error: route collision between intercepting routes — two pages answer the same URL.\\n` +\n ` Owner: app/${owner}\\n` +\n ` Intercepted base: ${base}\\n` +\n ` URL: ${collision.url}\\n` +\n ` File 1: ${collision.first}\\n` +\n ` File 2: ${collision.second}\\n` +\n `Both match it, so which one renders would be decided by filesystem enumeration ` +\n `order — renaming a route group can flip it. Rename or move one of them.\\n` +\n `Pages are compared by the URLs they match, not by how they are spelled: [id] and ` +\n `[slug] match the same URLs, and an optional catch-all also matches with zero ` +\n `segments.\\n` +\n `Intercepting directories based at *different* URLs may target one page — the ` +\n `resolver tries the deepest first — so this rejects only an ambiguity among the ` +\n `ones based at ${base}.\\n` +\n `See design/07-routing.md §\"Collisions Among Intercepting Routes\".`\n );\n }\n}\n\n/** What the walk knows about the position it is currently standing in. */\ninterface PlacementContext {\n /** `node` is itself a slot node, so its children are direct slot children. */\n parentIsSlot: boolean;\n /** `node` is at or below a slot node. */\n inSlotSubtree: boolean;\n /** `node` is inside an intercepting subtree. */\n inInterceptingSubtree: boolean;\n /** Some segment from the root down to `node`, inclusive, binds a param. */\n ancestorsBindParams: boolean;\n}\n\nfunction walkForPlacement(node: SegmentNode, path: string, ctx: PlacementContext): void {\n for (const child of node.children) {\n const childPath = path ? `${path}/${child.segmentName}` : child.segmentName;\n\n if (child.segmentType === 'intercepting') {\n const marker = child.interceptionMarker ?? '(.)';\n\n if (ctx.inInterceptingSubtree) {\n throw new Error(\n `Build error: intercepting route nested inside another intercepting route.\\n` +\n ` Directory: app/${childPath}\\n` +\n `An intercepting directory (${marker}) resolves against the URL its own parent ` +\n `renders at, and an intercepting subtree has no such URL — nothing walks it, so ` +\n `the rewrite would match the navigation and then have nothing to render.\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n }\n\n // Inside a slot, only a *direct* slot child is reachable: the slot\n // resolver reads `slotNode.children` and stops. The children-path\n // renderer cannot pick it up either — it builds a segment chain, which\n // has no way to express a descent through a slot.\n if (ctx.inSlotSubtree && !ctx.parentIsSlot) {\n throw new Error(\n `Build error: intercepting route inside a parallel slot but not directly under it.\\n` +\n ` Directory: app/${childPath}\\n` +\n `An intercepting directory (${marker}) inside an @slot must be a *direct* child of ` +\n `that slot, e.g. app/feed/@modal/${child.segmentName}/page.tsx. The slot resolver ` +\n `scans the slot's direct children only; elsewhere the rewrite would match the ` +\n `navigation and then have nothing to render.\\n` +\n `See design/07-routing.md §\"Intercepting Routes\".`\n );\n }\n\n // Outside a slot the intercepting page replaces `children` for the\n // owner's own layout chain, so those layouts render against the\n // intercepted URL — which carries no values for params declared above\n // the interception.\n if (!ctx.parentIsSlot && ctx.ancestorsBindParams) {\n assertOwnerChainBindsNoParams(childPath, marker);\n }\n\n if (!ctx.parentIsSlot && !node.layout) {\n assertOwnerHasLayout(path, childPath);\n }\n\n // `node` is the owner — the slot (whose urlPath is the owning segment's,\n // since slots add no URL depth) or the ordinary segment holding the\n // interception. The same input rewrite generation bases from.\n assertInterceptedBaseIsCountable(child, node.urlPath, childPath);\n assertInterceptedCatchAllsAreTerminal(child, childPath);\n assertNoRouteHandlersInSubtree(child, childPath);\n }\n\n walkForPlacement(child, childPath, {\n parentIsSlot: false,\n // A slot subtree stays a slot subtree all the way down: the deeper\n // positions are exactly the ones neither renderer reaches.\n inSlotSubtree: ctx.inSlotSubtree,\n inInterceptingSubtree: ctx.inInterceptingSubtree || child.segmentType === 'intercepting',\n ancestorsBindParams: ctx.ancestorsBindParams || !!child.paramName,\n });\n }\n\n // After every child has been recursed into, so a placement error inside any\n // intercepting subtree is reported in preference to what it causes here — a\n // nested interception leaves a node `matchUrlParts` cannot match at all, and\n // the resulting nonsense below it is not what the author needs to be told\n // about. This is per *owner* rather than per intercepting directory because\n // the collision space is the anchor group; see\n // `assertNoInterceptedPatternCollisions`.\n assertNoInterceptedPatternCollisions(node, path);\n\n for (const [slotName, slotNode] of Object.entries(node.slots)) {\n walkForPlacement(slotNode, path ? `${path}/@${slotName}` : `@${slotName}`, {\n parentIsSlot: true,\n inSlotSubtree: true,\n inInterceptingSubtree: ctx.inInterceptingSubtree,\n // A slot-mode interception never binds owner params from the target —\n // the pipeline re-matches the source route, which supplies them — so the\n // rule stops at the slot boundary.\n ancestorsBindParams: false,\n });\n }\n\n // After both children and slots have been validated independently, check for\n // the cross-mode ambiguity that neither space covers: a children-path\n // interceptor and a slot interceptor from the same owner claiming one URL.\n // Within each mode the collision is either ordered (two children-path) or\n // harmless (two slots) — see §\"Selection Between Owners\" — but across modes\n // one is an overlay and the other a layout swap, and the walk order picks the\n // children-path shape silently (TIM-1319).\n assertNoCrossModeClaims(node, path);\n}\n\n/**\n * Recursively walk the segment tree to find intercepting routes.\n *\n * `ancestors` deliberately excludes slot nodes: a slot doesn't appear in a\n * matched route chain, so the last entry is always the nearest chain-visible\n * ancestor — the segment that owns the slot, and therefore the segment whose\n * presence in the source route gates the interception.\n *\n * Intercepting nodes only ever appear as a direct child of a slot or of an\n * ordinary chain segment in a valid tree; `validateInterceptionPlacement`\n * rejects every other placement at scan time, so this walk never has to\n * decide what an unrenderable intercepting node should generate.\n *\n * `parentIsSlot` is what distinguishes the two shapes, and it is the only\n * input that does: the owner, the scope and the intercepted pattern are all\n * computed identically either way.\n */\nfunction walkForInterceptions(\n node: SegmentNode,\n ancestors: SegmentNode[],\n parentIsSlot: boolean,\n rewrites: InterceptionRewrite[]\n): void {\n // Check children\n for (const child of node.children) {\n if (child.segmentType === 'intercepting' && child.interceptionMarker) {\n // Found an intercepting route — collect rewrites from its sub-tree\n collectFromInterceptingNode(child, ancestors, parentIsSlot, rewrites);\n } else {\n walkForInterceptions(child, [...ancestors, child], false, rewrites);\n }\n }\n\n // Check slots (intercepting routes are typically inside slots like @modal)\n for (const slot of Object.values(node.slots)) {\n walkForInterceptions(slot, ancestors, true, rewrites);\n }\n}\n\n/**\n * For an intercepting segment, find all leaf pages in its sub-tree and\n * generate rewrite rules for each.\n */\nfunction collectFromInterceptingNode(\n interceptingNode: SegmentNode,\n ancestors: SegmentNode[],\n parentIsSlot: boolean,\n rewrites: InterceptionRewrite[]\n): void {\n const marker = interceptingNode.interceptionMarker!;\n const segmentName = interceptingNode.interceptedSegmentName!;\n\n // Compute the intercepted URL base based on the marker\n const parentUrlPath = ancestors[ancestors.length - 1].urlPath;\n const interceptedUrlBase = computeInterceptedUrlBase(parentUrlPath, marker, segmentName);\n\n // The scope key and the re-match prefix both describe the same node — the\n // interception's owner — so they are derived here together from one\n // ancestor chain.\n const ancestorPaths = computeSegmentTreePaths(ancestors);\n const interceptingScope = ancestorPaths[ancestorPaths.length - 1];\n\n // Find all leaf pages in the intercepting sub-tree\n const shape: InterceptionShape = parentIsSlot\n ? { mode: 'slot', interceptingPrefix: parentUrlPath, interceptingScope }\n : { mode: 'children', interceptingScope };\n collectLeavesWithRewrites(interceptingNode, interceptedUrlBase, shape, rewrites);\n}\n\n/**\n * Everything about a rewrite except which URL it intercepts — one intercepting\n * directory produces one of these and one rewrite per leaf page beneath it.\n *\n * Written as a union of `Omit`s rather than `Omit` of the union: `Omit` is not\n * distributive, so the latter would silently collapse to the shapes' common\n * fields and drop `interceptingPrefix`.\n */\ntype InterceptionShape =\n | Omit<SlotInterceptionRewrite, 'interceptedPattern'>\n | Omit<ChildrenInterceptionRewrite, 'interceptedPattern'>;\n\n/**\n * Walk an intercepting sub-tree and visit every page-bearing node with the URL\n * pattern it is intercepted at, and URLs that reach it.\n *\n * Route groups are transparent, exactly as they are in the main tree — which\n * is what makes two groups under one interceptor able to claim one URL, and is\n * why the collision validator has to walk the same shape this does.\n *\n * Rewrite generation and that validator are the two callers. They share this\n * walk rather than each having their own so they cannot disagree about which\n * URLs an interceptor claims: a leaf the validator did not see is a rewrite\n * nobody checked, and a leaf only the validator sees is a build error for a\n * route that does not exist. Slots are not descended, here or in either caller\n * — a slot nested inside an intercepting subtree produces no rewrite.\n *\n * That answers the rewrite question only, and the resolution question is\n * answered elsewhere: `validateSlotsInsideInterceptingSubtree`\n * (`collision-spaces.ts`) gives each such slot its own collision space, and\n * `validateSlotPlacement` (`slot-placement.ts`) rejects the ones no renderer\n * reaches. Between them the slots this walk skips are covered; do not conclude\n * from the skip here that they are unvalidated (TIM-1308, TIM-1311).\n *\n * The walk reports the chain of URL-contributing nodes rather than building\n * probe URLs itself: choosing a probe value means running the matcher over the\n * whole subtree, which is the validator's business and not something rewrite\n * generation should be paying for or passing dummy arguments around.\n */\nfunction walkInterceptedLeaves(\n node: SegmentNode,\n interceptedUrlPath: string,\n chain: SegmentNode[],\n visit: (page: NonNullable<SegmentNode['page']>, leaf: InterceptedLeaf) => void\n): void {\n if (node.page) {\n // The page is handed over rather than the node so callers reading it do\n // not need a non-null assertion to say what this branch already proved.\n visit(node.page, { interceptedPattern: interceptedUrlPath, chain });\n }\n\n for (const child of node.children) {\n const isGroup = child.segmentType === 'group';\n walkInterceptedLeaves(\n child,\n isGroup ? interceptedUrlPath : `${interceptedUrlPath}/${child.segmentName}`,\n isGroup ? chain : [...chain, child],\n visit\n );\n }\n}\n\n/** What one page-bearing node in an intercepting subtree claims. */\ninterface InterceptedLeaf {\n /** The URL pattern the rewrite carries — the author's own spelling. */\n interceptedPattern: string;\n /**\n * The nodes from the intercepting directory down to this page that contribute\n * URL parts — route groups excluded, since they are transparent for matching.\n * Empty for the intercepting directory's own page.\n */\n chain: SegmentNode[];\n}\n\n/**\n * Recursively find leaf pages in an intercepting sub-tree and generate\n * rewrite rules for each.\n */\nfunction collectLeavesWithRewrites(\n node: SegmentNode,\n interceptedUrlPath: string,\n shape: InterceptionShape,\n rewrites: InterceptionRewrite[]\n): void {\n walkInterceptedLeaves(node, interceptedUrlPath, [], (_page, leaf) => {\n rewrites.push({ ...shape, interceptedPattern: leaf.interceptedPattern });\n });\n}\n\n/**\n * How many URL levels a marker climbs from the slot owner to reach the\n * intercepted segment, or `'root'` for a marker that climbs all the way.\n *\n * Level counting operates on URL path segments, NOT filesystem directories.\n * Route groups and parallel slots are already excluded from urlPath (they\n * don't add URL depth), so (..) correctly climbs visible segments. This\n * avoids the Vinext bug where path.dirname() on filesystem paths would\n * waste climbs on invisible route groups.\n *\n * Used only by `computeInterceptedBase` below — both rewrite generation and\n * the slot resolver go through that, so the arithmetic exists once.\n */\nfunction interceptionClimb(marker: InterceptionMarker): number | 'root' {\n switch (marker) {\n case '(.)':\n return 0;\n case '(..)':\n return 1;\n case '(..)(..)':\n return 2;\n case '(...)':\n return 'root';\n }\n}\n\n/**\n * Compute the base URL that an intercepting route intercepts, given the\n * parent's URL path and the interception marker.\n *\n * - (.) — same level: parent's URL path\n * - (..) — one level up: parent's parent URL path\n * - (...) — root level: /\n * - (..)(..) — two levels up: parent's grandparent URL path\n *\n * The result is a URL **pattern**, not a URL — `parentUrlPath` carries the\n * directory names, so a dynamic ancestor appears as `/docs/[...path]`.\n * Rewrite generation wants exactly that. The slot resolver imports this to\n * locate the intercepted segment in a concrete target pathname, and must\n * therefore account for a bracket part standing for a different number of\n * URL parts than itself (TIM-1281).\n *\n * Climbing past the root clamps to `/` rather than erroring, so generation\n * and resolution never disagree about what an over-reaching marker\n * intercepts.\n */\n/**\n * The URL pattern an intercepting directory's own page sits at — its\n * `computeInterceptedBase` joined with the segment it intercepts.\n *\n * The root case is why this is a function rather than a template literal at\n * each caller: a base of `/` must not produce `//photo`. Rewrite generation\n * and the collision validator both start their walk from this value, so the\n * two describe the same URLs by construction.\n */\nexport function computeInterceptedUrlBase(\n parentUrlPath: string,\n marker: InterceptionMarker,\n interceptedSegmentName: string\n): string {\n return joinInterceptedBase(computeInterceptedBase(parentUrlPath, marker), interceptedSegmentName);\n}\n\n/**\n * An intercepted base joined with one segment beneath it.\n *\n * The whole content of the \"function rather than a template literal\" rule\n * above: a base of `/` must not produce `//photo`. The collision validator has\n * the base in hand already (it groups by it) and joins through this rather than\n * re-deriving it from an owner path, so both callers spell the root case once.\n */\nexport function joinInterceptedBase(base: string, segmentName: string): string {\n return base === '/' ? `/${segmentName}` : `${base}/${segmentName}`;\n}\n\nexport function computeInterceptedBase(parentUrlPath: string, marker: InterceptionMarker): string {\n const climb = interceptionClimb(marker);\n if (climb === 'root') return '/';\n\n const parts = parentUrlPath.split('/').filter(Boolean);\n for (let i = 0; i < climb; i++) parts.pop();\n return parts.length === 0 ? '/' : `/${parts.join('/')}`;\n}\n","/**\n * Route collision spaces.\n *\n * A collision space is a set of routes that compete for URLs with each other,\n * and there are three: the main tree, each parallel slot, and each intercepting\n * subtree. This module owns the first two; `interception.ts` owns the third.\n * All of them decide order-dependence with the one oracle in\n * `collision-probe.ts`, so they cannot drift on what counts as two routes\n * answering a single URL.\n *\n * See design/07-routing.md §\"Route Collisions\".\n */\n\nimport type { SegmentNode } from './types.ts';\nimport { computeInterceptedUrlBase } from './interception.ts';\nimport { findOrderDependentCollision, type ProbeLeaf } from './collision-probe.ts';\n\n/**\n * Validate that no two pages/routes in one collision space answer the same URL.\n *\n * Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim\n * /login — the scanner must detect and reject this at build time.\n *\n * Each parallel slot is validated as its OWN collision space: a slot's pages\n * intentionally share URL paths with the parent tree (that's the point of\n * parallel routes), but two route groups INSIDE one slot claiming the same\n * URL — e.g. @side/(a)/x/page.tsx + @side/(b)/x/page.tsx — still collide.\n */\nexport function validateRouteGroupCollisions(root: SegmentNode): void {\n validateCollisionSpace(root, '', root.urlPath);\n}\n\n/**\n * Validate one collision space — the main tree, or one parallel slot.\n *\n * **Two checks, and the difference between them matters.** Two leaves at the\n * same `urlPath` are decided by string equality, which is exact and needs no\n * probing: `(auth)/login` and `(marketing)/login` claim one set of URLs\n * whatever the precedence rules are, and naming the shared path is the clearest\n * thing to tell the author.\n *\n * Everything else is decided by asking the matcher, through the shared oracle\n * in `collision-probe.ts` (TIM-1308). Comparing `urlPath` spellings misses\n * every pair that matches the same URLs while being *spelled* differently:\n * `x/(a)/[id]` beside `x/(b)/[slug]` are two bare dynamics in one priority\n * tier, and `x/(a)/page.tsx` beside `x/(b)/[[...rest]]` both answer `/x`\n * because an optional catch-all also matches zero parts. Both built before\n * TIM-1308 with filesystem enumeration order deciding which page renders,\n * which is precisely what this validator and `validateAffixedSiblingConflicts`\n * exist to reject.\n *\n * Segment *kinds* stay distinct without any special-casing here, because the\n * oracle inherits `matchNode`'s priority ordering: `x/(a)/[id]` beside\n * `x/(b)/[...rest]` resolves the same way in either `children` order, so it is\n * not reported. So does an affixed sibling against a bare one; the\n * equal-specificity affix tie is `validateAffixedSiblingConflicts`' job, and\n * duplicating it here would double-report.\n */\nfunction validateCollisionSpace(spaceRoot: SegmentNode, segmentPath: string, base: string): void {\n const leaves: ProbeLeaf[] = [];\n collectRoutableLeaves(spaceRoot, new Map(), segmentPath, [], leaves, base);\n\n const collision = findOrderDependentCollision(spaceRoot, leaves, base);\n if (!collision) return;\n\n throw new Error(\n `Build error: route collision — two pages/routes answer the same URL.\\n` +\n ` URL: ${collision.url}\\n` +\n ` File 1: ${collision.first}\\n` +\n ` File 2: ${collision.second}\\n` +\n `Both match it, so which one renders would be decided by filesystem enumeration ` +\n `order — renaming a route group can flip it. Rename or move one of them.\\n` +\n `Routes are compared by the URLs they match, not by how they are spelled: [id] and ` +\n `[slug] match the same URLs, and an optional catch-all also matches with zero ` +\n `segments.`\n );\n}\n\n/**\n * Walk one collision space and collect its routable leaves (page or route\n * files), throwing on an exact `urlPath` collision. Each slot recurses as its\n * OWN space — slots are parallel routes that intentionally share URL paths with\n * their parent tree, but collisions within a single slot are real errors.\n *\n * The early return at an intercepting node drops that subtree's own pages from\n * this space, but not its slots: a slot is its own collision space wherever it\n * sits, so `validateSlotsInsideInterceptingSubtree` picks those up before\n * returning.\n *\n * `chain` accumulates the URL-contributing nodes from the space root down, for\n * the probe oracle. Route groups are excluded because they are transparent for\n * matching, and the space root itself is excluded because `matchUrlParts`\n * matches parts against its *children* — the root consumes nothing.\n */\nfunction collectRoutableLeaves(\n node: SegmentNode,\n seen: Map<string, { filePath: string; segmentPath: string }>,\n segmentPath: string,\n chain: SegmentNode[],\n leaves: ProbeLeaf[],\n base: string\n): void {\n // Intercepting routes are resolved by the interception resolvers, not URL\n // matching, and their computed urlPaths are not addresses — an interception\n // mirrors the canonical route it replaces, so comparing those paths here\n // would reject every correct interception. Skipping is not \"unvalidated\":\n // `assertNoInterceptedPatternCollisions` gives each intercepting subtree its\n // own collision space, keyed by the intercepted pattern rather than by these\n // paths (TIM-1305). See design/07-routing.md §\"Collisions Inside an\n // Intercepting Subtree\" and §\"Why urlPath Stays Parent-Relative Inside an\n // Intercepting Subtree\".\n const currentPath = segmentPath\n ? `${segmentPath}/${node.segmentName}`\n : node.segmentName || '(root)';\n\n // The intercepting subtree's own pages leave this space — but its SLOTS do\n // not leave every space, so they are picked up before returning. See\n // `validateSlotsInsideInterceptingSubtree`.\n if (node.segmentType === 'intercepting') {\n // `node.urlPath` on an intercepting node is its OWNER's path — the input\n // `computeInterceptedUrlBase` wants — because an intercepting segment adds\n // no URL depth.\n validateSlotsInsideInterceptingSubtree(\n node,\n currentPath,\n interceptedBaseOf(node, node.urlPath)\n );\n return;\n }\n\n const routableFile = node.page ?? node.route;\n if (routableFile) {\n const existing = seen.get(node.urlPath);\n if (existing) {\n throw new Error(\n `Build error: route collision — multiple route groups produce a page/route at the same URL path.\\n` +\n ` URL path: ${urlFromChain(base, chain)}\\n` +\n ` File 1: ${existing.filePath} (via ${existing.segmentPath})\\n` +\n ` File 2: ${routableFile.filePath} (via ${currentPath})\\n` +\n `Each URL path must map to exactly one page or route handler. ` +\n `Rename or move one of the conflicting files.`\n );\n }\n seen.set(node.urlPath, { filePath: routableFile.filePath, segmentPath: currentPath });\n leaves.push({ filePath: routableFile.filePath, chain });\n }\n\n // Recurse into children. A route group contributes no URL part, so it is\n // absent from the chain — exactly as `collectEffectiveChildren` flattens it\n // away in the matcher.\n for (const child of node.children) {\n const isGroup = child.segmentType === 'group';\n collectRoutableLeaves(\n child,\n seen,\n currentPath,\n isGroup ? chain : [...chain, child],\n leaves,\n base\n );\n }\n\n // Recurse into slots — each slot is its own collision space, with its own\n // seen map, its own leaf set and its own probe run.\n //\n // The base is accumulated from this space's own base rather than read off\n // `slotNode.urlPath`, because this walk also runs on slot spaces nested inside\n // an intercepting subtree, where `urlPath` is parent-relative and not an\n // address. Outside one the two are identical by construction.\n for (const slotNode of Object.values(node.slots)) {\n validateCollisionSpace(slotNode, currentPath, urlFromChain(base, chain));\n }\n}\n\n/**\n * Validate every parallel slot nested anywhere inside an intercepting subtree.\n *\n * Two walks each skip these slots for their own good reason, and together they\n * left them checked by nothing: `collectRoutableLeaves` returns at the\n * intercepting node before it reaches them, and `walkInterceptedLeaves`\n * deliberately does not descend slots — correct for rewrite generation, since a\n * slot inside an intercepting subtree produces no rewrite. But \"produces no\n * rewrite\" answers the rewrite question and not the resolution one, so\n * `@side/(a)/x/page.tsx` beside `@side/(b)/x/page.tsx` under an interceptor\n * built with filesystem order picking the page — the ordinary per-slot\n * collision that fails the build anywhere else in the tree (codex, on PR #1002).\n *\n * A slot is its own collision space wherever it sits, so each one is validated\n * as one.\n *\n * **Which interception shape this still applies to.** TIM-1311 answered the\n * question this was written under — whether such a slot renders at all — and\n * the answer is per-shape:\n *\n * - **Children path** (`(browse)/(...)photo/@side`): the intercepting node\n * joins the main route chain, so `route-element-builder.ts` resolves slots on\n * it and below it like any other chain segment. They render, and this is the\n * space where the check is load-bearing.\n * - **Modal** (`feed/@modal/(.)photo/@side`): the slot sits inside another\n * slot's subtree, which no renderer descends for slots — so it never renders,\n * and `validateSlotPlacement` now rejects the placement before this runs.\n * That rule subsumes this one for the modal shape rather than conflicting\n * with it, exactly as anticipated.\n *\n * So this walk is reached only for children-path interceptors in a valid tree.\n * It is not narrowed to them: the rule it states — a slot is its own collision\n * space wherever it sits — is shape-independent, and making it conditional on a\n * property the placement validator already guarantees would just be a second\n * place for the two to disagree.\n *\n * The walk covers the whole subtree, not just the interceptor's direct slot\n * children: `(...)photo/[id]/@side` is reached by neither walk for exactly the\n * same reason as `(...)photo/@side`.\n */\nfunction validateSlotsInsideInterceptingSubtree(\n node: SegmentNode,\n segmentPath: string,\n base: string\n): void {\n for (const slotNode of Object.values(node.slots)) {\n // A slot adds no URL depth, so it answers its owner's base.\n validateCollisionSpace(slotNode, segmentPath, base);\n }\n for (const child of node.children) {\n validateSlotsInsideInterceptingSubtree(\n child,\n `${segmentPath}/${child.segmentName}`,\n childInterceptedBase(child, base)\n );\n }\n}\n\n/**\n * The URL a leaf answers, built from its space's base and its own chain.\n *\n * Read instead of `node.urlPath` because `urlPath` inside an intercepting\n * subtree is parent-relative and is not an address — the collision URL has to\n * be one the author can actually find. Outside an intercepting subtree the two\n * are identical by construction: the base is the space root's `urlPath` and the\n * chain is every URL-contributing segment below it.\n */\nfunction urlFromChain(base: string, chain: SegmentNode[]): string {\n if (chain.length === 0) return base || '/';\n const tail = chain.map((n) => n.segmentName).join('/');\n return `${base === '/' ? '' : base}/${tail}`;\n}\n\n/**\n * The intercepted URL an intercepting directory's own page sits at.\n *\n * `urlPath` inside an intercepting subtree is deliberately parent-relative and\n * is **not** an address (see design/07-routing.md §\"Why urlPath Stays\n * Parent-Relative Inside an Intercepting Subtree\"), so a collision reported\n * from one names a URL the author cannot find. The intercepted base is the real\n * address, and it is what rewrite generation and the interception collision\n * validator both already use.\n */\nfunction interceptedBaseOf(node: SegmentNode, ownerUrlPath: string): string {\n return computeInterceptedUrlBase(\n ownerUrlPath,\n node.interceptionMarker ?? '(.)',\n node.interceptedSegmentName ?? node.segmentName\n );\n}\n\n/**\n * Extend an intercepted base by one child segment.\n *\n * Route groups are transparent and contribute nothing. A nested intercepting\n * node re-bases from its own marker rather than appending — that tree is\n * rejected by `validateInterceptionPlacement`, but collision validation runs\n * first, so it still has to produce a sensible URL rather than one containing a\n * literal \"(.)b\".\n */\nfunction childInterceptedBase(child: SegmentNode, base: string): string {\n // Slots are not in `children` — `scanChildren` files them under `slots` — so\n // only groups are transparent here.\n if (child.segmentType === 'group') return base;\n if (child.segmentType === 'intercepting') return interceptedBaseOf(child, base);\n return base === '/' ? `/${child.segmentName}` : `${base}/${child.segmentName}`;\n}\n","/**\n * Parallel slot placement.\n *\n * One rule: a slot is resolved only for a segment on the **matched route\n * chain**, and a slot's own subtree is never on that chain. So a slot declared\n * anywhere inside another slot's subtree can never render, and is rejected at\n * build time rather than left as a directory the author will keep editing.\n *\n * See design/07-routing.md §\"Parallel Routes\".\n */\n\nimport type { SegmentNode } from './types.ts';\n\n/**\n * Reject every parallel slot that no renderer reaches.\n *\n * **Where slots are resolved.** Two places read `segment.slots`, and both walk\n * the main route chain: `route-element-builder.ts` (the RSC render).\n * It is not reachable from inside a slot. The slot's own\n * subtree is rendered by `slot-resolver.ts`, which walks the slot's matched\n * chain to apply that chain's layouts, error boundaries and access gates\n * (`wrapWithIntermediateSegments`, `wrapWithLayout`) and never reads `.slots`\n * on any of those nodes. So `app/feed/@modal/photo/@side/page.tsx` and\n * everything below it is dead: the file is scanned, typed, code-split and\n * shipped, and no request can render it.\n *\n * That is the TIM-1278 failure — a directory that silently never renders —\n * and the reason it is a build error rather than a documented limit is that\n * nothing at request time can report it. A slot that does not match renders\n * `default.tsx` or `null`, which is exactly what a slot the resolver never\n * looked at also produces, so the author sees the *designed* behaviour of a\n * non-matching slot and has no signal to distinguish it from a slot that\n * cannot match at all.\n *\n * **Interception is incidental.** TIM-1311 arrived as\n * `feed/@modal/(.)photo/@side`, and the interception contributes nothing: the\n * identical `feed/@modal/photo/@side` never renders either, for the same\n * reason and with no marker anywhere. Rejecting only the intercepting spelling\n * would have drawn a line the author cannot see from the directory names.\n *\n * The children-path interception shape is **not** affected and must not be:\n * its intercepting node joins the main chain (`server/children-interception.ts`\n * splices `[…owner ancestors, interceptingNode, …descendants]` into an ordinary\n * `RouteMatch`), so `route-element-builder.ts` resolves slots on it and below\n * it exactly as it does for any other chain segment. `(browse)/(...)photo/[id]/@side`\n * renders; `feed/@modal/(.)photo/@side` does not. The discriminator is the slot\n * boundary, which is what this walk tracks — not the marker.\n */\nexport function validateSlotPlacement(root: SegmentNode): void {\n walkForSlotPlacement(root, '', null);\n}\n\n/**\n * `enclosingSlotPath` is the `app/`-relative path of the nearest slot at or\n * above `node`, or null on the main chain. It is carried rather than recomputed\n * so the error can name the slot that swallows the offender — \"this is inside\n * @modal\" is the part of the diagnosis the author cannot see from the offending\n * path alone.\n */\nfunction walkForSlotPlacement(\n node: SegmentNode,\n path: string,\n enclosingSlotPath: string | null\n): void {\n for (const [slotName, slotNode] of Object.entries(node.slots)) {\n const slotPath = path ? `${path}/@${slotName}` : `@${slotName}`;\n if (enclosingSlotPath !== null) {\n throwNestedSlot(slotPath, enclosingSlotPath);\n }\n walkForSlotPlacement(slotNode, slotPath, slotPath);\n }\n\n for (const child of node.children) {\n walkForSlotPlacement(\n child,\n path ? `${path}/${child.segmentName}` : child.segmentName,\n enclosingSlotPath\n );\n }\n}\n\nfunction throwNestedSlot(slotPath: string, enclosingSlotPath: string): never {\n throw new Error(\n `Build error: parallel slot nested inside another parallel slot.\\n` +\n ` Directory: app/${slotPath}\\n` +\n ` Inside: app/${enclosingSlotPath}\\n` +\n `A parallel slot is resolved only for a segment on the matched route chain, and a ` +\n `slot's own subtree is never on that chain — the slot resolver walks the slot's ` +\n `matched chain to apply its layouts, error boundaries and access gates, and never ` +\n `looks at slots declared inside it. So app/${slotPath} and everything below it can ` +\n `never render, and no request-time signal can tell you: an unreachable slot ` +\n `produces exactly what a slot that simply did not match produces.\\n` +\n `Declare the slot on a segment of the main route chain instead, or move its pages ` +\n `into app/${enclosingSlotPath}'s own subtree.\\n` +\n `See design/07-routing.md §\"Parallel Routes\".`\n );\n}\n","import { readFileSync, statSync } from 'node:fs';\n\nconst cache = new Map<string, { mtimeMs: number; size: number; content: string }>();\n\n/**\n * Read a file's text content, returning a cached copy when the file\n * hasn't been modified since the last read (checked via mtime + size).\n *\n * Falls back to a fresh read on any stat/read error.\n */\nexport function readFileCached(filePath: string): string {\n try {\n const stat = statSync(filePath);\n const entry = cache.get(filePath);\n if (entry && entry.mtimeMs === stat.mtimeMs && entry.size === stat.size) return entry.content;\n const content = readFileSync(filePath, 'utf-8');\n cache.set(filePath, { mtimeMs: stat.mtimeMs, size: stat.size, content });\n return content;\n } catch {\n cache.delete(filePath);\n return readFileSync(filePath, 'utf-8');\n }\n}\n","/**\n * AST-based export detection for route files.\n *\n * Uses Vite's `parseAst` (backed by oxc) to precisely detect named and\n * default exports, replacing the regex-based approach that was fragile\n * with TypeScript syntax (type exports, `as` aliases, comments, etc.).\n */\n\nimport { existsSync } from 'node:fs';\nimport { parseAst } from 'vite';\nimport { readFileCached } from './file-cache.ts';\n\ninterface AstNode {\n type: string;\n exportKind?: string;\n declaration?: AstNode;\n specifiers?: Array<{\n exported?: { name: string };\n local?: { name: string };\n exportKind?: string;\n }>;\n id?: { name: string };\n declarations?: Array<{ id?: { name: string }; init?: AstNode }>;\n /** Set on re-exports: `export { X } from './y'`. Absent on local exports. */\n source?: { value?: string } | null;\n /** Set on runtime-transparent wrappers: `X as T`, `X satisfies T`, `(X)`, `X!`. */\n expression?: AstNode;\n /** Set on `Identifier` nodes. */\n name?: string;\n}\n\ninterface ProgramNode {\n body: AstNode[];\n}\n\nfunction tryParse(source: string): ProgramNode | null {\n try {\n return parseAst(source, { lang: 'tsx' }) as unknown as ProgramNode;\n } catch {\n return null;\n }\n}\n\n/**\n * Collect all named export identifiers from a parsed program.\n * Handles: `export function X`, `export const X`, `export { X }`,\n * `export { X } from '...'`, `export { Y as X }`.\n */\nfunction collectNamedExports(program: ProgramNode): Set<string> {\n const names = new Set<string>();\n for (const stmt of program.body) {\n if (stmt.type !== 'ExportNamedDeclaration') continue;\n // Skip `export type ...` declarations — they're erased at runtime\n if (stmt.exportKind === 'type') continue;\n\n if (stmt.declaration) {\n if (stmt.declaration.id?.name) {\n names.add(stmt.declaration.id.name);\n }\n if (stmt.declaration.declarations) {\n for (const decl of stmt.declaration.declarations) {\n if (decl.id?.name) names.add(decl.id.name);\n }\n }\n }\n\n if (stmt.specifiers) {\n for (const spec of stmt.specifiers) {\n // Skip `export { type X }` — per-specifier type exports\n if (spec.exportKind === 'type') continue;\n if (spec.exported?.name && spec.exported.name !== 'default') {\n names.add(spec.exported.name);\n }\n }\n }\n }\n return names;\n}\n\n/**\n * The specifier exporting a *runtime* default from this statement, if any.\n *\n * Type-only exports erase, so they are not default exports at runtime — and\n * TypeScript spells that two ways, on two different nodes: `export type { X as\n * default }` marks the **statement**, `export { type X as default }` marks the\n * **specifier**. Both have to be checked, or a type alias reads as a component:\n * a status file whose only default is a type has nothing for the error boundary\n * to receive (the renderer's `mod?.default` skips it), so treating it as one\n * would fail a build over a file that cannot break anything.\n *\n * One implementation because two callers must agree on what \"has a default\n * export\" means: `hasDefaultExport` decides whether to warn that a page has\n * none, and `fileDefaultExportIsLocalValue` decides whether to fail the build.\n */\nfunction defaultSpecifier(stmt: AstNode): NonNullable<AstNode['specifiers']>[number] | null {\n if (stmt.type !== 'ExportNamedDeclaration' || !stmt.specifiers) return null;\n if (stmt.exportKind === 'type') return null;\n for (const spec of stmt.specifiers) {\n if (spec.exportKind === 'type') continue;\n if (spec.exported?.name === 'default') return spec;\n }\n return null;\n}\n\n/**\n * Check whether a program has a default export.\n * Handles: `export default ...`, `export { X as default }`,\n * `export { default } from '...'`. Type-only defaults do not count —\n * see `defaultSpecifier`.\n */\nfunction hasDefaultExport(program: ProgramNode): boolean {\n for (const stmt of program.body) {\n if (stmt.type === 'ExportDefaultDeclaration') return true;\n if (defaultSpecifier(stmt)) return true;\n }\n return false;\n}\n\n/**\n * Check whether a program has any bare `export * from '...'` declarations.\n * Excludes `export * as X from '...'` (namespace re-exports) since those\n * create a namespace object, not individual top-level exports.\n */\nfunction hasStarExport(program: ProgramNode): boolean {\n return program.body.some(\n (stmt) =>\n stmt.type === 'ExportAllDeclaration' && !(stmt as any).exported && stmt.exportKind !== 'type'\n );\n}\n\n/**\n * Check if a file exports a specific named export.\n * Returns false if the file doesn't exist or can't be parsed.\n */\nexport function fileHasExport(filePath: string, exportName: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n return collectNamedExports(program).has(exportName);\n } catch {\n return false;\n }\n}\n\n/**\n * Check if a file has any of the given named exports.\n * Returns the set of matching export names.\n */\nexport function fileHasAnyExport(filePath: string, exportNames: readonly string[]): Set<string> {\n const matches = new Set<string>();\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return matches;\n const exports = collectNamedExports(program);\n for (const name of exportNames) {\n if (exports.has(name)) matches.add(name);\n }\n } catch {\n // Graceful fallback: return empty set\n }\n return matches;\n}\n\n/**\n * Check if a file has a default export.\n * Returns false if the file doesn't exist or can't be parsed.\n */\nexport function fileHasDefaultExport(filePath: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n return hasDefaultExport(program);\n } catch {\n return false;\n }\n}\n\n/** Node types whose value is unambiguously constructed by this module. */\nconst FUNCTION_LIKE = new Set([\n 'FunctionDeclaration',\n 'FunctionExpression',\n 'ArrowFunctionExpression',\n 'ClassDeclaration',\n 'ClassExpression',\n]);\n\n/**\n * Expression wrappers that erase at runtime and so cannot change where a value\n * was constructed: `X as T`, `X satisfies T`, `<T>X`, `X!`, `X<T>`, `(X)`.\n */\nconst TRANSPARENT_WRAPPERS = new Set([\n 'TSAsExpression',\n 'TSSatisfiesExpression',\n 'TSTypeAssertion',\n 'TSNonNullExpression',\n 'TSInstantiationExpression',\n 'ParenthesizedExpression',\n]);\n\n/**\n * Strip runtime-transparent wrappers so the node underneath can be classified.\n *\n * Without this, `(() => <h1 />) satisfies React.FC` reads as a\n * `TSSatisfiesExpression` rather than the arrow function it compiles to, and a\n * provably-local component is misread as one whose origin cannot be\n * determined — turning a build error into a warning.\n */\nfunction unwrapTransparent(node: AstNode): AstNode {\n let current = node;\n while (TRANSPARENT_WRAPPERS.has(current.type) && current.expression) {\n current = current.expression;\n }\n return current;\n}\n\n/**\n * Whether `name` is bound at module top level to a function or class written\n * in this file. An imported binding can never match: imports are\n * `ImportDeclaration` specifiers, not function/class/variable declarations,\n * and a module-level name cannot be both.\n */\nfunction bindsLocalFunction(program: ProgramNode, name: string): boolean {\n for (const stmt of program.body) {\n // `export function X() {}` binds X locally too — unwrap the export.\n const decl = stmt.type === 'ExportNamedDeclaration' ? stmt.declaration : stmt;\n if (!decl) continue;\n if (\n (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') &&\n decl.id?.name === name\n ) {\n return true;\n }\n if (decl.type === 'VariableDeclaration') {\n for (const d of decl.declarations ?? []) {\n if (d.id?.name === name && d.init && FUNCTION_LIKE.has(unwrapTransparent(d.init).type)) {\n return true;\n }\n }\n }\n }\n return false;\n}\n\n/**\n * Whether the default export is *provably* a value constructed by this module:\n * `export default function/class/() => {}`, or a name bound to a function or\n * class declared here — whether it leaves via `export default Name` or\n * `export { Name as default }`. Both spellings are the same fact about where\n * the value was built, so both must answer the same way.\n *\n * This is the sound half of \"is this a server component?\". A file with no\n * `'use client'` directive whose default export is built here is a server\n * component, full stop. Everything else — `export { default } from\n * './client-thing'`, `export default Imported`, `export default memo(X)`,\n * an unparseable file — returns false, because the exported value may have\n * been minted in a module that *does* carry the directive. Callers must not\n * infer \"server component\" from a missing directive alone.\n */\nexport function fileDefaultExportIsLocalValue(filePath: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n for (const stmt of program.body) {\n if (stmt.type === 'ExportDefaultDeclaration') {\n const decl = stmt.declaration && unwrapTransparent(stmt.declaration);\n if (!decl) return false;\n if (FUNCTION_LIKE.has(decl.type)) return true;\n if (decl.type === 'Identifier' && decl.name) {\n return bindsLocalFunction(program, decl.name);\n }\n return false;\n }\n\n // `export { Name as default }` — the same default export by another\n // spelling. `hasDefaultExport` already counts it, so an analysis that\n // skipped it here would let the exact defect this powers slip through\n // as a warning.\n const spec = defaultSpecifier(stmt);\n if (spec) {\n // `export { X as default } from './client-thing'` re-exports another\n // module's value — the directive governing it lives over there.\n if (stmt.source) return false;\n return spec.local?.name ? bindsLocalFunction(program, spec.local.name) : false;\n }\n }\n return false;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if a file has any `export * from '...'` declarations.\n * Returns false if the file doesn't exist or can't be parsed.\n */\nexport function fileHasStarExport(filePath: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n return hasStarExport(program);\n } catch {\n return false;\n }\n}\n\n/**\n * Parsed value of `export const prerender` from a route file.\n *\n * - `true` / `false` — literal boolean\n * - `{ ttl?, tags? }` — ISR options object (prerender: true is implied)\n * - `undefined` — no `export const prerender` found\n *\n * See design/45-cache-lifetimes.md §\"Route-Level Static\".\n */\nexport type PrerenderExportValue = boolean | { ttl?: number; tags?: string[] };\n\nfunction extractLiteralValue(node: AstNode): unknown {\n if (!node) return undefined;\n const n = node as any;\n if (n.type === 'Literal') return n.value;\n if (n.type === 'UnaryExpression' && n.operator === '-' && n.argument?.type === 'Literal') {\n return -(n.argument.value as number);\n }\n if (n.type === 'ArrayExpression') {\n const elements = n.elements as AstNode[] | undefined;\n if (!elements) return undefined;\n const result: unknown[] = [];\n for (const el of elements) {\n const v = extractLiteralValue(el);\n if (v === undefined) return undefined;\n result.push(v);\n }\n return result;\n }\n return undefined;\n}\n\nfunction extractObjectLiteral(node: AstNode): Record<string, unknown> | undefined {\n if ((node as any).type !== 'ObjectExpression') return undefined;\n const props = (node as any).properties as AstNode[] | undefined;\n if (!props) return undefined;\n const result: Record<string, unknown> = {};\n for (const prop of props) {\n const p = prop as any;\n if (p.type === 'SpreadElement') return undefined;\n if (p.type !== 'Property' || p.computed) return undefined;\n const key =\n p.key?.type === 'Identifier'\n ? p.key.name\n : p.key?.type === 'Literal'\n ? String(p.key.value)\n : undefined;\n if (!key) return undefined;\n const value = extractLiteralValue(p.value);\n if (value === undefined) return undefined;\n result[key] = value;\n }\n return result;\n}\n\n/**\n * Extract the value of `export const prerender` from a file.\n * Returns undefined if the file doesn't export `prerender` or can't be parsed.\n *\n * Supports:\n * export const prerender = true;\n * export const prerender = false;\n * export const prerender = { ttl: 3600, tags: ['docs'] };\n */\nexport function getPrerenderExport(filePath: string): PrerenderExportValue | undefined {\n if (!existsSync(filePath)) return undefined;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return undefined;\n\n for (const stmt of program.body) {\n if (stmt.type !== 'ExportNamedDeclaration') continue;\n if (stmt.exportKind === 'type') continue;\n if (!stmt.declaration) continue;\n const decl = stmt.declaration;\n if (decl.type !== 'VariableDeclaration') continue;\n const declarations = decl.declarations as\n | Array<{\n id?: { name?: string };\n init?: AstNode;\n }>\n | undefined;\n if (!declarations) continue;\n for (const d of declarations) {\n if (d.id?.name !== 'prerender' || !d.init) continue;\n const init = d.init as any;\n if (init.type === 'Literal' && typeof init.value === 'boolean') {\n return init.value;\n }\n if (init.type === 'ObjectExpression') {\n const obj = extractObjectLiteral(init);\n if (!obj) return undefined;\n const result: { ttl?: number; tags?: string[] } = {};\n if ('ttl' in obj && typeof obj.ttl === 'number') result.ttl = obj.ttl;\n if (\n 'tags' in obj &&\n Array.isArray(obj.tags) &&\n obj.tags.every((t: unknown) => typeof t === 'string')\n ) {\n result.tags = obj.tags as string[];\n }\n return result;\n }\n return undefined;\n }\n }\n return undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * How a file declares the per-route CSRF exemption.\n *\n * - `'absent'` — no runtime export named `csrf`.\n * - `'exempt'` — exactly `export const csrf = false` (wrappers like\n * `false as const` allowed), declared in this file.\n * - `'invalid'` — some other `csrf` export: another value, `let`, a\n * destructured binding, a specifier or named re-export (`export { csrf }`,\n * `export { x as \"csrf\" }`), a function or class. `export * from` is not\n * followed: a `csrf` it carries is neither exempt nor an error.\n *\n * The exemption is read from source at build time, never from the loaded\n * module, because the CSRF gate runs before any user code (see\n * design/08-forms-and-actions.md §\"Per-route exemption\"). Only the literal\n * form can be read that way; any other spelling is a build error rather\n * than a silent \"not exempt\", so what the developer wrote is what runs.\n */\nexport type CsrfExport = 'absent' | 'exempt' | 'invalid';\n\nexport function getCsrfExport(filePath: string): CsrfExport {\n if (!existsSync(filePath)) return 'absent';\n // Unparseable: not exempt (fail closed). The compiler reports the syntax\n // error; guessing at a half-written file would misreport it.\n const program = tryParse(readFileCached(filePath));\n if (!program) return 'absent';\n\n // Walk the statements directly rather than through `collectNamedExports`:\n // every spelling that exports a runtime `csrf` must be seen here, and\n // that helper does not see destructured declarations or string-literal\n // specifier names. What cannot be seen at all — `export * from` — is\n // not read, and is documented as such.\n let found: CsrfExport = 'absent';\n for (const stmt of program.body) {\n if (stmt.type !== 'ExportNamedDeclaration' || stmt.exportKind === 'type') continue;\n for (const spec of stmt.specifiers ?? []) {\n if (spec.exportKind !== 'type' && specifierName(spec.exported) === 'csrf') {\n return 'invalid';\n }\n }\n const decl = stmt.declaration as (AstNode & { kind?: string }) | undefined;\n if (!decl) continue;\n if (decl.id?.name === 'csrf') return 'invalid'; // function or class\n if (decl.type !== 'VariableDeclaration') continue;\n for (const d of decl.declarations ?? []) {\n const id = d.id as AstNode | undefined;\n if (id?.type === 'Identifier') {\n if (id.name !== 'csrf') continue;\n const init = d.init && (unwrapTransparent(d.init) as AstNode & { value?: unknown });\n const literalFalse = init?.type === 'Literal' && init.value === false;\n if (decl.kind !== 'const' || !literalFalse) return 'invalid';\n found = 'exempt';\n } else if (id && patternBinds(id, 'csrf')) {\n return 'invalid'; // `export const { csrf } = …`\n }\n }\n }\n return found;\n}\n\n/** An export specifier's name: `csrf` or the string literal `\"csrf\"`. */\nfunction specifierName(node: { name?: string; value?: unknown } | undefined): string | undefined {\n if (!node) return undefined;\n return typeof node.value === 'string' ? node.value : node.name;\n}\n\n/** Whether a destructuring pattern binds `name` anywhere inside it. */\nfunction patternBinds(node: unknown, name: string): boolean {\n if (!node || typeof node !== 'object') return false;\n const n = node as Record<string, unknown>;\n switch (n.type) {\n case 'Identifier':\n return n.name === name;\n case 'ObjectPattern':\n return ((n.properties as unknown[]) ?? []).some((prop) => {\n const p = prop as Record<string, unknown>;\n return patternBinds(p.type === 'RestElement' ? p.argument : p.value, name);\n });\n case 'ArrayPattern':\n return ((n.elements as unknown[]) ?? []).some((el) => patternBinds(el, name));\n case 'RestElement':\n return patternBinds(n.argument, name);\n case 'AssignmentPattern':\n return patternBinds(n.left, name);\n default:\n return false;\n }\n}\n\n/**\n * Check if a file starts with a specific directive (e.g. \"use client\").\n * Directives are string literal expression statements at the top of the file.\n */\nexport function fileHasDirective(filePath: string, directive: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n for (const stmt of program.body) {\n if (stmt.type !== 'ExpressionStatement') break;\n const expr = (stmt as any).expression;\n if (expr?.type !== 'Literal' || typeof expr.value !== 'string') break;\n if (expr.value === directive) return true;\n }\n return false;\n } catch {\n return false;\n }\n}\n","/**\n * Route discovery scanner.\n *\n * Pure function: (appDir, config) → RouteTree\n *\n * Scans the app/ directory and builds a segment tree recognizing all\n * timber.js file conventions. Does NOT handle request matching — this\n * is discovery only.\n */\n\nimport { readdirSync, statSync } from 'node:fs';\nimport { join, extname, basename } from 'node:path';\nimport type {\n RouteTree,\n SegmentNode,\n SegmentType,\n RouteFile,\n ScannerConfig,\n InterceptionMarker,\n} from './types.ts';\nimport { classifySegment, effectiveUrlSegment } from './segment-classify.ts';\nimport { validateInterceptionPlacement, computeInterceptedBase } from './interception.ts';\nimport { validateRouteGroupCollisions } from './collision-spaces.ts';\nimport { validateSlotPlacement } from './slot-placement.ts';\nimport { DEFAULT_PAGE_EXTENSIONS } from './types.ts';\nimport { classifyMetadataRoute, isDynamicMetadataExtension } from '../server/metadata-routes.ts';\nimport { swallow } from '../server/logger.ts';\nimport { getCsrfExport } from './export-detect.ts';\nimport { ENCODED_SEPARATOR_RE, NULL_BYTE_RE } from '../server/canonicalize.ts';\n\n/**\n * File convention names that use pageExtensions (can be .tsx, .ts, .jsx, .js, .mdx, etc.)\n * `error` is excluded — it only accepts script extensions (TIM-1505).\n */\nconst PAGE_EXT_CONVENTIONS = new Set(['page', 'layout', 'default', 'denied']);\n\n/**\n * File convention names that are always .ts/.tsx (never .mdx etc.)\n */\nconst FIXED_CONVENTIONS = new Set(['middleware', 'access', 'route']);\n\n/**\n * Status-code file patterns:\n * - Exact 3-digit codes: 401.tsx, 429.tsx, 503.tsx\n * - Category catch-alls: 4xx.tsx, 5xx.tsx\n */\nconst STATUS_CODE_PATTERN = /^(\\d{3}|[45]xx)$/;\n\n/**\n * Whether a basename + extension pair is a route-convention file the\n * framework itself imports: page/layout/default/denied on pageExtensions,\n * error/global-error and status-code files on script extensions only,\n * middleware/access/route/proxy on .ts/.tsx/.js/.jsx only, and\n * status-code .json files — mirroring this scanner's own recognition\n * rules per family.\n *\n * Exported for the analyze classifier so its provisional classification\n * (design/47 §2) derives from the scanner's real conventions instead of\n * a duplicate list that can drift.\n */\nexport function isRouteConventionFile(\n base: string,\n ext: string,\n pageExtensions: readonly string[] = DEFAULT_PAGE_EXTENSIONS\n): boolean {\n const isPageExt = pageExtensions.includes(ext);\n const isScriptExt = /^[jt]sx?$/.test(ext);\n if (PAGE_EXT_CONVENTIONS.has(base) && isPageExt) return true;\n if ((base === 'error' || base === 'global-error') && isScriptExt) return true;\n if ((FIXED_CONVENTIONS.has(base) || base === 'proxy') && isScriptExt) return true;\n if (STATUS_CODE_PATTERN.test(base) && (isScriptExt || ext === 'json')) return true;\n return false;\n}\n\n/**\n * Scan the app/ directory and build the route tree.\n *\n * @param appDir - Absolute path to the app/ directory\n * @param config - Scanner configuration\n * @returns The complete route tree\n */\nexport function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTree {\n const pageExtensions = config.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS;\n const extSet = new Set(pageExtensions);\n\n const tree: RouteTree = {\n root: createSegmentNode('', 'static', '/'),\n };\n\n // Check for proxy.ts at app root\n const proxyFile = findFixedFile(appDir, 'proxy');\n if (proxyFile) {\n tree.proxy = proxyFile;\n }\n\n // Check for global-error.{tsx,ts,jsx,js} at app root.\n // Tier 2 error page — renders standalone (no layouts) when no segment-level\n // error file is found. Script extensions only — MDX not supported (TIM-1505).\n // See design/10-error-handling.md §\"Tier 2\".\n const globalErrorFile = findScriptExtFile(appDir, 'global-error');\n if (globalErrorFile) {\n tree.globalError = globalErrorFile;\n }\n\n // Scan the root directory's files\n scanSegmentFiles(appDir, tree.root, extSet);\n\n // Scan children recursively\n scanChildren(appDir, tree.root, extSet);\n\n // Validate: every parallel slot sits where a renderer reaches it. First,\n // because a slot nested inside another slot never renders, and a collision\n // reported from inside one would send the author to fix an ambiguity between\n // two pages that were both already dead.\n validateSlotPlacement(tree.root);\n\n // Validate: detect route group collisions (different groups producing pages at the same URL)\n validateRouteGroupCollisions(tree.root);\n\n // Validate: detect duplicate param names in nested dynamic segments\n // e.g., /[id]/items/[id] — same param name in ancestor and descendant\n validateDuplicateParamNames(tree.root);\n\n // Validate: detect same param name used with different bracket forms\n // e.g., [id] in one route and [...id] in another\n validateUniqueBracketForms(tree.root);\n\n // Validate: detect affixed dynamic siblings with equal specificity\n // e.g., a-[id] and [slug]-b both have prefix+suffix length = 2\n validateAffixedSiblingConflicts(tree.root);\n\n // Validate: intercepting routes must be direct children of a parallel slot —\n // the slot resolver is the only thing that can render one.\n validateInterceptionPlacement(tree.root);\n\n return tree;\n}\n\n/**\n * Create an empty segment node.\n */\nfunction createSegmentNode(\n segmentName: string,\n segmentType: SegmentType,\n urlPath: string,\n paramName?: string,\n interceptionMarker?: InterceptionMarker,\n interceptedSegmentName?: string,\n paramPrefix?: string,\n paramSuffix?: string\n): SegmentNode {\n return {\n segmentName,\n segmentType,\n urlPath,\n paramName,\n paramPrefix,\n paramSuffix,\n interceptionMarker,\n interceptedSegmentName,\n children: [],\n slots: {},\n };\n}\n\n/**\n * Compute the URL path for a child segment given its parent's URL path.\n * Route groups, slots, and intercepting routes do NOT add URL depth.\n */\nfunction computeUrlPath(parentUrlPath: string, dirName: string, segmentType: SegmentType): string {\n // Groups, slots, and intercepting routes don't add to URL path\n if (segmentType === 'group' || segmentType === 'slot' || segmentType === 'intercepting') {\n return parentUrlPath;\n }\n\n const parentPath = parentUrlPath === '/' ? '' : parentUrlPath;\n return `${parentPath}/${dirName}`;\n}\n\n/**\n * Scan a directory for file conventions and populate the segment node.\n */\nfunction scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n swallow(err, `scanSegmentFiles: unreadable directory ${dirPath}`, { level: 'warn' });\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n // Skip directories — handled by scanChildren\n try {\n if (statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n const ext = extname(entry).slice(1); // remove leading dot\n const name = basename(entry, `.${ext}`);\n\n // Page-extension conventions (page, layout, default, denied)\n if (PAGE_EXT_CONVENTIONS.has(name) && extSet.has(ext)) {\n const key = name as 'page' | 'layout' | 'default' | 'denied';\n const existing = node[key];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node[key] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // error.tsx — script extensions only, not MDX/MD (TIM-1505).\n if (name === 'error' && /^[jt]sx?$/.test(ext)) {\n const existing = node.error;\n if (existing) {\n throwDuplicateConvention('error', existing.filePath, fullPath);\n }\n node.error = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Fixed conventions (middleware, access, route) — only\n // .ts/.tsx/.js/.jsx, never pageExtensions like .mdx. The regex is\n // anchored: mts/cts/mjs/cjs (and stray tail matches like `.foots`)\n // are NOT recognized. See design/07-routing.md §\"Page Extensions\".\n if (FIXED_CONVENTIONS.has(name) && /^[jt]sx?$/.test(ext)) {\n const key = name as 'middleware' | 'access' | 'route';\n const existing = node[key];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node[key] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // JSON status-code files (401.json, 4xx.json, 503.json, 5xx.json)\n // Recognized regardless of pageExtensions — .json is a data format, not a page extension.\n if (STATUS_CODE_PATTERN.test(name) && ext === 'json') {\n if (!node.jsonStatusFiles) {\n node.jsonStatusFiles = {};\n }\n node.jsonStatusFiles[name] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Status-code files (401.tsx, 4xx.tsx, 503.tsx, 5xx.tsx) — script extensions only (TIM-1505)\n if (STATUS_CODE_PATTERN.test(name) && /^[jt]sx?$/.test(ext)) {\n if (!node.statusFiles) {\n node.statusFiles = {};\n }\n const existing = node.statusFiles[name];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node.statusFiles[name] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Metadata route files (sitemap.ts, robots.ts, icon.tsx, opengraph-image.tsx, etc.)\n // Both static (.xml, .txt, .png, .ico, etc.) and dynamic (.ts, .tsx) files are recognized.\n // When both exist for the same base name, dynamic takes precedence.\n // Two files of the SAME kind (static+static or dynamic+dynamic) are a\n // build error — picking one by readdir order is platform-dependent.\n // See design/16-metadata.md §\"Metadata Routes\"\n const metaInfo = classifyMetadataRoute(entry);\n if (metaInfo) {\n if (!metaInfo.nestable && node.segmentName !== '') {\n throw new Error(\n `Build error: '${name}' is a root-only metadata convention and must be in the app root directory.\\n` +\n ` File: ${fullPath}\\n` +\n ` Move this file to the app root (not a route group or nested segment).`\n );\n }\n if (!node.metadataRoutes) {\n node.metadataRoutes = {};\n }\n const existing = node.metadataRoutes[name];\n if (existing) {\n const existingIsDynamic = isDynamicMetadataExtension(name, existing.extension);\n const newIsDynamic = isDynamicMetadataExtension(name, ext);\n if (existingIsDynamic === newIsDynamic) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n // Dynamic > static precedence: overwrite only when the new file is dynamic.\n if (newIsDynamic) {\n node.metadataRoutes[name] = { filePath: fullPath, extension: ext };\n }\n } else {\n node.metadataRoutes[name] = { filePath: fullPath, extension: ext };\n }\n }\n }\n\n // Validate: route.ts + page.* is a hard build error\n if (node.route && node.page) {\n throw new Error(\n `Build error: route.ts and page.* cannot coexist in the same segment.\\n` +\n ` route.ts: ${node.route.filePath}\\n` +\n ` page: ${node.page.filePath}\\n` +\n `A URL is either an API endpoint or a rendered page, not both.`\n );\n }\n\n // The per-route CSRF exemption is read from source here, at scan time, so\n // the manifest carries it and the request gate never loads user code to\n // decide it. Any other spelling of the export is a lint error\n // (convention-lint.ts `checkCsrfExports`).\n if (node.route && getCsrfExport(node.route.filePath) === 'exempt') {\n node.csrfExempt = true;\n }\n}\n\n/**\n * Throw a build error for two files claiming the same convention slot in\n * one segment (e.g. page.tsx + page.jsx). Which file readdir returns first\n * is platform-dependent (APFS sorts entries, ext4 does not) — silently\n * picking one would let macOS dev and Linux CI serve different\n * implementations of the same route.\n */\nfunction throwDuplicateConvention(\n convention: string,\n firstPath: string,\n secondPath: string\n): never {\n throw new Error(\n `Build error: duplicate '${convention}' files in the same route segment.\\n` +\n ` File 1: ${firstPath}\\n` +\n ` File 2: ${secondPath}\\n` +\n `Each segment may define '${convention}' at most once. Remove one of the files.`\n );\n}\n\n/**\n * Recursively scan child directories and build the segment tree.\n */\nfunction scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n swallow(err, `scanChildren: unreadable directory ${dirPath}`, { level: 'warn' });\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n try {\n if (!statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n // Reject directories with encoded path delimiters or null bytes.\n // These can cause route collisions when decoded at the URL boundary.\n // See design/13-security.md §\"Encoded separators rejected\" and §\"Null bytes rejected\".\n if (ENCODED_SEPARATOR_RE.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded path delimiter (%2F or %5C).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded separators in directory names cause route collisions when decoded. ` +\n `Rename the directory to remove the encoded delimiter.`\n );\n }\n if (NULL_BYTE_RE.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded null byte (%00).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded null bytes in directory names are not allowed. ` +\n `Rename the directory to remove the null byte encoding.`\n );\n }\n\n const {\n type,\n paramName,\n paramPrefix,\n paramSuffix,\n interceptionMarker,\n interceptedSegmentName,\n } = classifySegment(entry);\n\n // Skip private folders — underscore-prefixed dirs are excluded from routing\n if (type === 'private') continue;\n\n const urlPath = computeUrlPath(parentNode.urlPath, entry, type);\n const childNode = createSegmentNode(\n entry,\n type,\n urlPath,\n paramName,\n interceptionMarker,\n interceptedSegmentName,\n paramPrefix,\n paramSuffix\n );\n\n // Scan this segment's files\n scanSegmentFiles(fullPath, childNode, extSet);\n\n // Recurse into subdirectories\n scanChildren(fullPath, childNode, extSet);\n\n // Attach to parent: slots go into slots record, everything else is a child\n if (type === 'slot') {\n const slotName = entry.slice(1); // remove @\n parentNode.slots[slotName] = childNode;\n } else {\n parentNode.children.push(childNode);\n }\n }\n}\n\n/**\n * Validate that no route chain contains duplicate dynamic param names.\n *\n * Example violation:\n * app/[id]/items/[id]/page.tsx — 'id' appears twice in the ancestor chain.\n *\n * Route groups are transparent — params accumulate through them.\n * Slots are independent — duplicate detection does NOT cross slot boundaries.\n *\n * See design/07-routing.md §\"Duplicate Param Name Detection\"\n */\nfunction validateDuplicateParamNames(root: SegmentNode): void {\n walkForDuplicateParams(root, new Map());\n}\n\n/**\n * Recursively walk the segment tree, tracking seen param names → segment paths.\n * Throws on the first duplicate found.\n */\nfunction walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): void {\n // Read through the effective identity so an intercepting directory is seen\n // as the segment it intercepts: the scanner leaves `paramName` unset on\n // `(.)[id]`, so a validator reading the node directly is blind to a param\n // the resolver goes on to bind — `@modal/(.)[id]/[id]` would pass here and\n // then silently overwrite the outer `id` at match time, where the ordinary\n // `/[id]/[id]` is rejected. See TIM-1288.\n const { paramName } = effectiveUrlSegment(node);\n\n // If this node introduces a param name, check for duplicates\n if (paramName) {\n const existing = seen.get(paramName);\n if (existing) {\n throw new Error(\n `[timber] Duplicate param name '${paramName}' in route chain.\\n` +\n ` First defined at: ${existing}\\n` +\n ` Duplicate at: ${node.urlPath || '/'}\\n` +\n ` Rename one of the segments to avoid ambiguity.`\n );\n }\n // Add to seen for descendants (use a new Map to avoid polluting siblings)\n seen = new Map(seen);\n seen.set(paramName, node.urlPath || '/');\n }\n\n // Recurse into children (they inherit the accumulated params)\n for (const child of node.children) {\n walkForDuplicateParams(child, seen);\n }\n\n // A slot INHERITS the ancestor params rather than starting fresh: a slot\n // child may not reuse a name its owner already bound\n // (`app/gallery/[id]/@modal/[id]` is rejected). The copy is so sibling\n // slots don't see each other's names, not so the chain is reset.\n //\n // Whether that rule is right is a separate question — slot params are\n // merged over the main route's at read time, so a collision is defined\n // rather than ambiguous — but changing it would change every slot.\n // Tracked as TIM-1287; do not \"fix\" this by making the map fresh without\n // deciding that.\n for (const slotNode of Object.values(node.slots)) {\n walkForDuplicateParams(slotNode, new Map(seen));\n }\n}\n\n/**\n * Validate that each param name uses a single bracket form across the entire route tree.\n *\n * Different bracket forms produce different types:\n * [id] → string\n * [...id] → string[]\n * [[...id]] → string[] | undefined\n *\n * If the same param name appears with multiple forms, the no-argument\n * getSegmentParams() / useSegmentParams() return type becomes ambiguous.\n *\n * Same name + same form across different routes is fine and expected.\n */\nfunction validateUniqueBracketForms(root: SegmentNode): void {\n const forms = new Map<string, { segmentType: SegmentType; urlPath: string }>();\n collectBracketForms(root, forms);\n}\n\nfunction collectBracketForms(\n node: SegmentNode,\n forms: Map<string, { segmentType: SegmentType; urlPath: string }>\n): void {\n // Effective identity, so `(.)[...rest]` is compared as the catch-all it\n // intercepts rather than skipped as an 'intercepting' node (TIM-1288).\n const { paramName, segmentType } = effectiveUrlSegment(node);\n\n if (paramName && segmentType !== 'static' && segmentType !== 'group') {\n const existing = forms.get(paramName);\n if (existing && existing.segmentType !== segmentType) {\n const bracketExamples: Record<string, string> = {\n 'dynamic': `[${paramName}]`,\n 'catch-all': `[...${paramName}]`,\n 'optional-catch-all': `[[...${paramName}]]`,\n };\n throw new Error(\n `[timber] Param name '${paramName}' uses multiple bracket forms.\\n` +\n ` ${bracketExamples[existing.segmentType] ?? existing.segmentType} at: ${existing.urlPath}\\n` +\n ` ${bracketExamples[segmentType] ?? segmentType} at: ${node.urlPath || '/'}\\n` +\n ` Each param name must use a single bracket form across all routes.\\n` +\n ` Rename one of the segments to fix this (e.g., [${paramName}] → [${paramName}Id]).`\n );\n }\n if (!existing) {\n forms.set(paramName, {\n segmentType,\n urlPath: node.urlPath || '/',\n });\n }\n }\n\n for (const child of node.children) {\n collectBracketForms(child, forms);\n }\n for (const slotNode of Object.values(node.slots)) {\n collectBracketForms(slotNode, forms);\n }\n}\n\n/**\n * Validate that no sibling affixed dynamic segments have equal specificity.\n *\n * Among affixed siblings, longer combined prefix+suffix length wins. When two\n * siblings tie (same total affix length), the router would fall back to\n * filesystem scan order — which is platform-dependent. This is a build error.\n *\n * Route groups are URL-transparent, so affixed segments inside different groups\n * at the same level still conflict with each other.\n *\n * Ambiguity requires the two segments to be candidates for the *same* URL\n * part, which is narrower than \"same parent\" once interception is involved:\n *\n * - Plain children and intercepting children never compete. They are resolved\n * by different walks — `findSlotMatch` matches the slot subtree (where\n * `tree-match.ts` ignores intercepting nodes outright) and\n * `findInterceptingMatch` builds a synthetic parent holding only\n * interceptors.\n * - Two interceptors compete only at the same anchor. `(.)a-[id]` and\n * `(..)[slug]-b` in one slot are based at different URL depths, so no\n * pathname reaches both, and `anchorIndex` rejects the one whose base does\n * not match the target. Grouping them together would fail the build for a\n * pair that can never be ambiguous.\n *\n * So candidates are keyed by the position they contend for, then by affix\n * length. See design/07-routing.md §\"Priority\" and §\"Intercepting Routes\".\n */\nfunction validateAffixedSiblingConflicts(node: SegmentNode): void {\n const affixed = collectEffectiveAffixed(node);\n\n if (affixed.length >= 2) {\n const byLength = new Map<string, SegmentNode[]>();\n for (const child of affixed) {\n // `\\0` as the separator, written as the escape rather than a literal\n // byte: a raw NUL in the source makes git and grep treat this file as\n // binary. A contention key is a URL path, which cannot contain one, so\n // the two halves of the key can never run together.\n const key = `${contentionKey(node, child)}\\0${affixLengthOf(child)}`;\n const group = byLength.get(key);\n if (group) {\n group.push(child);\n } else {\n byLength.set(key, [child]);\n }\n }\n\n for (const [, group] of byLength) {\n if (group.length >= 2) {\n throw new Error(\n `Build error: ambiguous affixed dynamic siblings with equal specificity.\\n` +\n ` Parent: ${node.urlPath || '/'}\\n` +\n ` Segments: ${group.map((s) => s.segmentName).join(', ')}\\n` +\n ` Combined prefix+suffix length: ${affixLengthOf(group[0]!)}\\n` +\n `The router cannot deterministically choose between these — filesystem scan order is platform-dependent.\\n` +\n `Rename one segment to give it a longer or shorter combined affix.`\n );\n }\n }\n }\n\n for (const child of node.children) {\n validateAffixedSiblingConflicts(child);\n }\n for (const slotNode of Object.values(node.slots)) {\n validateAffixedSiblingConflicts(slotNode);\n }\n}\n\n/**\n * The URL position a child contends for, as a comparable key.\n *\n * Plain children contend for the parent's next part. An intercepting child\n * contends for a part determined by its marker's intercepted base, so two\n * markers of different depth are simply different positions — and two markers\n * that compute the *same* base do contend, which is why the key is the base\n * rather than the marker (`(..)` and `(...)` under `/feed` both base at `/`).\n *\n * `groupByInterceptedAnchor` (`routing/interception.ts`) is the canonical\n * statement of that grouping — it is what the resolver walks and what the\n * collision validator checks. This computes the same key from the same\n * `computeInterceptedBase` rather than calling it, because affix ambiguity is\n * decided per *child* and `collectEffectiveAffixed` looks through route groups,\n * so the two do not partition the same set of nodes.\n */\nfunction contentionKey(parent: SegmentNode, child: SegmentNode): string {\n if (child.segmentType !== 'intercepting') return 'child';\n return `intercept:${computeInterceptedBase(parent.urlPath, child.interceptionMarker ?? '(.)')}`;\n}\n\n/** Combined affix length of a node, read through its effective identity. */\nfunction affixLengthOf(node: SegmentNode): number {\n const { paramPrefix, paramSuffix } = effectiveUrlSegment(node);\n return (paramPrefix?.length ?? 0) + (paramSuffix?.length ?? 0);\n}\n\n/**\n * Collect affixed dynamic segments that are effective children of a node.\n * Route groups are URL-transparent, so we look through them recursively.\n *\n * Segments are read through `effectiveUrlSegment`, so an intercepting\n * directory counts as the affixed segment it intercepts: `(.)a-[id]` is\n * `a-[id]`. The proxy the slot resolver builds hands those affixes to\n * `matchUrlParts`, whose equal-length sort is not stable across equal\n * specificity — so two intercepting siblings with the same combined affix\n * length would resolve by filesystem enumeration order, which is precisely\n * what this validator exists to prevent for ordinary siblings (TIM-1288).\n */\nfunction collectEffectiveAffixed(node: SegmentNode): SegmentNode[] {\n const result: SegmentNode[] = [];\n for (const child of node.children) {\n const { segmentType, paramPrefix, paramSuffix } = effectiveUrlSegment(child);\n if (segmentType === 'dynamic' && (paramPrefix || paramSuffix)) {\n result.push(child);\n } else if (segmentType === 'group') {\n result.push(...collectEffectiveAffixed(child));\n }\n }\n return result;\n}\n\n/**\n * Find a fixed-extension file (proxy.ts) in a directory.\n */\nfunction findFixedFile(dirPath: string, name: string): RouteFile | undefined {\n for (const ext of ['ts', 'tsx']) {\n const fullPath = join(dirPath, `${name}.${ext}`);\n try {\n if (statSync(fullPath).isFile()) {\n return { filePath: fullPath, extension: ext };\n }\n } catch {\n // File doesn't exist\n }\n }\n return undefined;\n}\n\n/**\n * Find a file with script extensions (tsx, ts, jsx, js) in a directory.\n * Used for app-root conventions like global-error that only accept script\n * extensions (TIM-1505).\n */\nfunction findScriptExtFile(dirPath: string, name: string): RouteFile | undefined {\n for (const ext of ['tsx', 'ts', 'jsx', 'js']) {\n const fullPath = join(dirPath, `${name}.${ext}`);\n try {\n if (statSync(fullPath).isFile()) {\n return { filePath: fullPath, extension: ext };\n }\n } catch {\n // File doesn't exist\n }\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkFA,SAAgB,4BACd,WACA,QACA,MACwB;CACxB,MAAM,WAAW,qBAAqB,SAAS;CAC/C,MAAM,WAAW,eAAe,SAAS,IAAI;CAG7C,MAAM,YAAY,kBAAkB,SAAS;CAkB7C,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,QAAQ,QACjB,KAAK,MAAM,EAAE,OAAO,aAAa,iBAC/B,WACA,UACA,MACA,UACA,SACF,GAAG;EACD,MAAM,MAAM,MAAM,KAAK,GAAG;EAC1B,IAAI,KAAK,IAAI,GAAG,GAAG;EACnB,KAAK,IAAI,GAAG;EAEZ,MAAM,QAAQ,gBAAgB,WAAW,KAAK;EAC9C,MAAM,SAAS,gBAAgB,UAAU,KAAK;EAC9C,IAAI,CAAC,SAAS,CAAC,UAAU,UAAU,QAAQ;EAC3C,OAAO;GAAE,KAAK,iBAAiB,MAAM,OAAO;GAAG;GAAO;EAAO;CAC/D;CAGF,OAAO;AACT;;AAGA,SAAS,iBAAiB,MAAc,SAA2B;CACjE,IAAI,QAAQ,WAAW,GAAG,OAAO,QAAQ;CACzC,OAAO,GAAG,SAAS,MAAM,KAAK,KAAK,GAAG,QAAQ,KAAK,GAAG;AACxD;;;;;;;;AASA,SAAS,gBAAgB,MAAmB,OAAgC;CAC1E,MAAM,QAAQ,cAAc,MAAM,KAAK;CACvC,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM,SAAS;CAC9C,OAAO,KAAK,MAAM,YAAY,KAAK,OAAO,YAAY;AACxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,UAAiB,gBAAgB,YAAY,GAAsB;CACjE,MAAM,WAAW;CACjB,MAAM,OAAO;CAEb,UAAU,KAAK,MAAiC;EAC9C,KAAK,MAAM,QAAQ,UACjB,KAAK,MAAM,QAAQ,UAAU,MAAM,GAAG,OAAO,OAAO;CAExD;CAEA,OAAO,KAAK,IAAI;CAiBhB,MAAM,SAAS,KAAK,OAAO,KAAK,IAAI,IAAa,SAAS,GAAG,GAAG;CAChE,IAAI,WAAW,MAAM,OAAO,KAAK,MAAM;AACzC;;;;;;;;AASA,SAAS,kBAAkB,MAA2B;CACpD,IAAI,UAAU;CACd,KAAK,MAAM,SAAS,KAAK,UAAU;EACjC,MAAM,MAAM,oBAAoB,KAAK;EACrC,IAAI,IAAI,gBAAgB,UAAU,UAAU,KAAK,IAAI,SAAS,IAAI,YAAY,MAAM;EACpF,UAAU,KAAK,IAAI,SAAS,kBAAkB,KAAK,CAAC;CACtD;CACA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAS,iBACP,MACA,UACA,MACA,UACA,WACY;CACZ,IAAI,WAA8B;CAElC,KAAK,MAAM,SAAS,gBAAgB,SAAS,GAAG;EAC9C,MAAM,OAAO,aAAa,KAAK,OAAO,OAAO,QAAQ;EAMrD,IALgB,KAAK,MAClB,EAAE,YACD,gBAAgB,MAAM,KAAK,MAAM,KAAK,YACtC,gBAAgB,UAAU,KAAK,MAAM,KAAK,QAE1C,GAAS,OAAO;EACpB,aAAa;EASb,IAAI,CAAC,KAAK,MAAM,EAAE,cAAc,OAAO,GAAG;CAC5C;CAGA,OAAO,YAAY,CAAC;AACtB;;;;;;;;;;;;;;;;;;AAmBA,SAAS,aAAa,MAAmB,OAA0B;CACjE,MAAM,MAAM,oBAAoB,IAAI;CACpC,QAAQ,IAAI,aAAZ;EACE,KAAK,WAAW;GACd,MAAM,SAAS,IAAI,eAAe;GAClC,MAAM,SAAS,IAAI,eAAe;GAIlC,OAAO;IACL,MAAM,GAAG,SAAS,QAAQ;IAC1B,OAAO,GAAG,OAAO,OAAO;IACxB,SAAS;GACX;EACF;EACA,KAAK;EACL,KAAK,sBACH,OAAO;GAAE,MAAM;GAAO,OAAO;GAAS,SAAS;EAAK;EACtD,SACE,OAAO;GAAE,MAAM,IAAI;GAAa,OAAO,IAAI;GAAa,SAAS;EAAM;CAC3E;AACF;;;;;;;;;;;AAmBA,SAAS,aAAa,OAAsB,OAAe,UAA8B;CACvF,MAAM,aAAa,MAAM,KAAK,SAAS,aAAa,MAAM,KAAK,CAAC;CAChE,MAAM,OAAO,QAA+B;EAC1C,OAAO,GAAG,KAAK,MAAM,EAAE,IAAI;EAC3B,SAAS,GAAG,KAAK,MAAM,EAAE,KAAK;EAC9B,SAAS,GAAG,MAAM,MAAM,EAAE,OAAO;CACnC;CAQA,MAAM,OAAO,MAAM,MAAM,SAAS;CAClC,MAAM,OAAO,QAAQ,oBAAoB,IAAI,CAAC,CAAC;CAE/C,IAAI,EADe,SAAS,eAAe,SAAS,uBACnC,OAAO,CAAC,IAAI,UAAU,CAAC;CAMxC,MAAM,OAAO,WAAW,MAAM,GAAG,EAAE;CACnC,MAAM,OAAO,CAAC,IAAI,UAAU,CAAC;CAO7B,IAAI,WAAW,GAAG;EAMhB,MAAM,OAAoB,MAAM,KAAK,EAAE,QAAQ,SAAS,UAAU;GAChE,MAAM;GACN,OAAO;GACP,SAAS;EACX,EAAE;EACF,KAAK,KAAK,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,CAAC;CACnC;CAGA,IAAI,SAAS,sBAAsB,KAAK,KAAK,IAAI,IAAI,CAAC;CAEtD,OAAO;AACT;;;;;;;;AASA,SAAS,eAAe,MAA2B;CACjD,IAAI,UAAU;CACd,KAAK,MAAM,SAAS,KAAK,UAAU;EACjC,MAAM,MAAM,MAAM,gBAAgB,UAAU,IAAI;EAChD,UAAU,KAAK,IAAI,SAAS,MAAM,eAAe,KAAK,CAAC;CACzD;CACA,OAAO;AACT;;;;;;;;;AAUA,SAAS,qBAAqB,MAAgC;CAC5D,OAAO;EAAE,GAAG;EAAM,UAAU,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,oBAAoB;CAAE;AACrF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACjZA,SAAgB,2BAA2B,IAAY,IAA2B;CAChF,IAAI,OAAO,IAAI,OAAO;CAEtB,MAAM,SAAS,OAAO,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG;CACtD,MAAM,SAAS,OAAO,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG;CACtD,MAAM,UAAoB,CAAC;CAC3B,IAAI,IAAI;CAER,OAAO,IAAI,OAAO,UAAU,IAAI,OAAO,QAAQ;EAC7C,MAAM,OAAO,mBAAmB,OAAO,EAAE;EACzC,MAAM,OAAO,mBAAmB,OAAO,EAAE;EAEzC,IACE,KAAK,SAAS,eACd,KAAK,SAAS,wBACd,KAAK,SAAS,eACd,KAAK,SAAS,sBACd;GACA,QAAQ,KAAK,OAAO;GACpB,OAAO,MAAM,QAAQ,KAAK,GAAG;EAC/B;EAEA,MAAM,OAAO,gBAAgB,MAAM,IAAI;EACvC,IAAI,SAAS,MAAM,OAAO;EAC1B,QAAQ,KAAK,IAAI;EACjB;CACF;CAEA,MAAM,YAAY,IAAI,OAAO,SAAS,SAAS,IAAI,OAAO,SAAS,SAAS;CAC5E,IAAI,WACW;MAAA,mBAAmB,UAAU,EACtC,CAAA,CAAK,SAAS,sBAAsB,OAAO;CAAA;CAGjD,OAAO,QAAQ,WAAW,IAAI,MAAM,MAAM,QAAQ,KAAK,GAAG;AAC5D;;;;;;;;;;AAWA,SAAS,gBACP,MACA,MACe;CACf,IAAI,KAAK,SAAS,YAAY,KAAK,SAAS,UAC1C,OAAO,KAAK,UAAU,KAAK,QAAQ,KAAK,QAAQ;CAGlD,IAAI,KAAK,SAAS,YAAY,KAAK,SAAS,WAC1C,OAAO,mBAAmB,KAAK,OAAO,IAAI,IAAI,KAAK,QAAQ;CAE7D,IAAI,KAAK,SAAS,YAAY,KAAK,SAAS,WAC1C,OAAO,mBAAmB,KAAK,OAAO,IAAI,IAAI,KAAK,QAAQ;CAG7D,IAAI,KAAK,SAAS,aAAa,KAAK,SAAS,WAC3C,OAAO,yBAAyB,MAAM,IAAI,IAAI,UAAU;CAG1D,OAAO;AACT;;;;;;;;;AAUA,SAAS,mBAAmB,OAAe,SAAwD;CACjG,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,SAAS,QAAQ,UAAU;CACjC,IAAI,CAAC,UAAU,CAAC,QAAQ,OAAO;CAC/B,IAAI,UAAU,CAAC,MAAM,WAAW,MAAM,GAAG,OAAO;CAChD,IAAI,UAAU,CAAC,MAAM,SAAS,MAAM,GAAG,OAAO;CAC9C,OAAO,MAAM,SAAS,OAAO,SAAS,OAAO;AAC/C;;;;;;;;AASA,SAAS,yBACP,IACA,IACS;CACT,MAAM,KAAK,GAAG,UAAU,IACtB,KAAK,GAAG,UAAU;CACpB,MAAM,KAAK,GAAG,UAAU,IACtB,KAAK,GAAG,UAAU;CAEpB,IAAI,MAAM,MAAM,CAAC,GAAG,WAAW,EAAE,KAAK,CAAC,GAAG,WAAW,EAAE,GAAG,OAAO;CACjE,IAAI,MAAM,MAAM,CAAC,GAAG,SAAS,EAAE,KAAK,CAAC,GAAG,SAAS,EAAE,GAAG,OAAO;CAC7D,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACoCA,SAAgB,yBACd,cACA,UACiC;CACjC,MAAM,yBAAS,IAAI,IAA2C;CAE9D,KAAK,MAAM,SAAS,UAAU;EAC5B,IAAI,MAAM,gBAAgB,gBAAgB;EAC1C,IAAI,CAAC,MAAM,sBAAsB,CAAC,MAAM,wBAAwB;EAEhE,MAAM,OAAO,uBAAuB,cAAc,MAAM,kBAAkB;EAC1E,MAAM,QAAQ,OAAO,IAAI,IAAI;EAC7B,IAAI,OACF,MAAM,aAAa,KAAK,KAAK;OAE7B,OAAO,IAAI,MAAM;GACf;GACA,aAAa,SAAS,MAAM,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;GACzD,cAAc,CAAC,KAAK;EACtB,CAAC;CAEL;CAIA,OAAO,CAAC,GAAG,OAAO,OAAO,CAAC,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,cAAc,EAAE,WAAW;AAC1E;;;;;;;;;;;AAYA,SAAgB,4BAA4B,MAA0C;CACpF,MAAM,WAAkC,CAAC;CACzC,qBAAqB,MAAM,CAAC,IAAI,GAAG,OAAO,QAAQ;CAClD,OAAO,iBAAiB,QAAQ;AAClC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAS,iBAAiB,UAAwD;CAChF,OAAO,SAAS,MACb,GAAG,MAAM,cAAc,EAAE,iBAAiB,IAAI,cAAc,EAAE,iBAAiB,CAClF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,8BAA8B,MAAyB;CACrE,iBAAiB,MAAM,IAAI;EACzB,cAAc;EACd,eAAe;EACf,uBAAuB;EACvB,qBAAqB;CACvB,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,SAAS,qBAAqB,WAAmB,WAAyB;CACxE,MAAM,QAAQ,aAAa;CAC3B,MAAM,IAAI,MACR,gFACsB,UAAU,qBACV,MAAM,sTAKf,MAAM,wKAGrB;AACF;AAEA,SAAS,8BAA8B,WAAmB,QAAkC;CAC1F,MAAM,IAAI,MACR,8EACsB,UAAU,+NAI1B,OAAO,qPAKf;AACF;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAS,iCACP,OACA,cACA,WACM;CACN,MAAM,SAAS,MAAM,sBAAsB;CAC3C,MAAM,OAAO,uBAAuB,cAAc,MAAM;CAExD,MAAM,aADY,SAAS,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,EAAA,CACjC,MAAM,SAAS;EACzC,MAAM,OAAO,mBAAmB,IAAI,CAAC,CAAC;EACtC,OAAO,SAAS,eAAe,SAAS;CAC1C,CAAC;CACD,IAAI,CAAC,WAAW;CAEhB,MAAM,IAAI,MACR,gFACsB,UAAU,wBACP,KAAK,iBACZ,UAAU,kEACV,OAAO,oSAM3B;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAS,sCAAsC,MAAmB,UAAwB;CACxF,MAAM,EAAE,aAAa,gBAAgB,oBAAoB,IAAI;CAG7D,KAFmB,gBAAgB,eAAe,gBAAgB,yBAEhD,KAAK,SAAS,SAAS,GACvC,MAAM,IAAI,MACR,+FACsB,SAAS,cAChB,KAAK,SAAS,KAAK,MAAM,EAAE,WAAW,CAAC,CAAC,KAAK,IAAI,EAAE,iBAChD,YAAY,gUAKhC;CAGF,KAAK,MAAM,SAAS,KAAK,UACvB,sCAAsC,OAAO,GAAG,SAAS,GAAG,MAAM,aAAa;AAEnF;;;;;;;;;;;;;;;;AAiBA,SAAS,+BAA+B,MAAmB,UAAwB;CACjF,IAAI,KAAK,OACP,MAAM,IAAI,MACR,qEACa,KAAK,MAAM,SAAS,qBACX,SAAS,4bAQjC;CAGF,KAAK,MAAM,SAAS,KAAK,UACvB,+BAA+B,OAAO,GAAG,SAAS,GAAG,MAAM,aAAa;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,SAAS,qCAAqC,OAAoB,WAAyB;CACzF,KAAK,MAAM,SAAS,yBAAyB,MAAM,SAAS,MAAM,QAAQ,GACxE,iCAAiC,OAAO,SAAS;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAS,wBAAwB,MAAmB,WAAyB;CAE3E,MAAM,iBAA+D,CAAC;CACtE,KAAK,MAAM,SAAS,KAAK,UAAU;EACjC,IACE,MAAM,gBAAgB,kBACtB,CAAC,MAAM,sBACP,CAAC,MAAM,wBAEP;EAMF,sBAAsB,OALN,0BACd,KAAK,SACL,MAAM,oBACN,MAAM,sBAEqB,GAAS,CAAC,KAAK,IAAI,MAAM,SAAS;GAC7D,eAAe,KAAK;IAAE,SAAS,KAAK;IAAoB,UAAU,KAAK;GAAS,CAAC;EACnF,CAAC;CACH;CAEA,IAAI,eAAe,WAAW,GAAG;CAEjC,MAAM,QAAQ,aAAa;CAE3B,KAAK,MAAM,QAAQ,OAAO,OAAO,KAAK,KAAK,GAAG;EAC5C,MAAM,aAA2D,CAAC;EAClE,KAAK,MAAM,SAAS,KAAK,UAAU;GACjC,IACE,MAAM,gBAAgB,kBACtB,CAAC,MAAM,sBACP,CAAC,MAAM,wBAEP;GAMF,sBAAsB,OALN,0BACd,KAAK,SACL,MAAM,oBACN,MAAM,sBAEqB,GAAS,CAAC,KAAK,IAAI,MAAM,SAAS;IAC7D,WAAW,KAAK;KAAE,SAAS,KAAK;KAAoB,UAAU,KAAK;IAAS,CAAC;GAC/E,CAAC;EACH;EAEA,KAAK,MAAM,MAAM,gBACf,KAAK,MAAM,MAAM,YAAY;GAC3B,MAAM,UAAU,2BAA2B,GAAG,SAAS,GAAG,OAAO;GACjE,IAAI,YAAY,MACd,oBAAoB,OAAO,SAAS,GAAG,UAAU,GAAG,QAAQ;EAEhE;CAEJ;AACF;AAEA,SAAS,oBACP,OACA,KACA,WACA,UACO;CACP,MAAM,IAAI,MACR;kBAEqB,MAAM,gBACV,IAAI,gBACJ,UAAU,gBACV,SAAS,6YAO5B;AACF;;;;;;;;;;;;;;;;;AAkBA,SAAgB,iBAAwB,MAA8C;CACpF,OAAO;EAAE,GAAG;EAAM,GAAG,oBAAoB,IAAI;CAAE;AACjD;AAEA,SAAS,iCACP,OACA,WACM;CACN,MAAM,EAAE,SAAS;CACjB,MAAM,QAAQ,aAAa;CAO3B,MAAM,UAAU,MAAM,aAAa,IAAI,gBAAgB;CACvD,MAAM,aAA0B;EAC9B,aAAa;EACb,aAAa;EACb,SAAS;EACT,UAAU;EACV,OAAO,CAAC;CACV;CAEA,MAAM,2BAAW,IAAI,IAAoB;CACzC,MAAM,cAA2B,CAAC;CAElC,KAAK,MAAM,SAAS,SAKlB,sBAAsB,OADN,oBAAoB,MAAM,MAAM,WACnB,GAAS,CAAC,KAAK,IAAI,MAAM,SAAS;EAS7D,MAAM,YAAY,SAAS,IAAI,KAAK,kBAAkB;EACtD,IAAI,WACF,MAAM,IAAI,MACR,yHAC6B,MAAM,wBACV,KAAK,wBACL,KAAK,mBAAmB,wBACxB,UAAU,wBACV,KAAK,SAAS,wOAKzC;EAEF,SAAS,IAAI,KAAK,oBAAoB,KAAK,QAAQ;EACnD,YAAY,KAAK;GAAE,UAAU,KAAK;GAAU,OAAO,KAAK;EAAM,CAAC;CACjE,CAAC;CAIH,MAAM,YAAY,4BAA4B,YAAY,aAAa,IAAI;CAC3E,IAAI,WACF,MAAM,IAAI,MACR,sHAC6B,MAAM,wBACV,KAAK,wBACL,UAAU,IAAI,wBACd,UAAU,MAAM,wBAChB,UAAU,OAAO,+eAQvB,KAAK,qEAE1B;AAEJ;AAcA,SAAS,iBAAiB,MAAmB,MAAc,KAA6B;CACtF,KAAK,MAAM,SAAS,KAAK,UAAU;EACjC,MAAM,YAAY,OAAO,GAAG,KAAK,GAAG,MAAM,gBAAgB,MAAM;EAEhE,IAAI,MAAM,gBAAgB,gBAAgB;GACxC,MAAM,SAAS,MAAM,sBAAsB;GAE3C,IAAI,IAAI,uBACN,MAAM,IAAI,MACR,+FACsB,UAAU,+BACA,OAAO,mPAIzC;GAOF,IAAI,IAAI,iBAAiB,CAAC,IAAI,cAC5B,MAAM,IAAI,MACR,uGACsB,UAAU,+BACA,OAAO,gFACF,MAAM,YAAY,wMAIzD;GAOF,IAAI,CAAC,IAAI,gBAAgB,IAAI,qBAC3B,8BAA8B,WAAW,MAAM;GAGjD,IAAI,CAAC,IAAI,gBAAgB,CAAC,KAAK,QAC7B,qBAAqB,MAAM,SAAS;GAMtC,iCAAiC,OAAO,KAAK,SAAS,SAAS;GAC/D,sCAAsC,OAAO,SAAS;GACtD,+BAA+B,OAAO,SAAS;EACjD;EAEA,iBAAiB,OAAO,WAAW;GACjC,cAAc;GAGd,eAAe,IAAI;GACnB,uBAAuB,IAAI,yBAAyB,MAAM,gBAAgB;GAC1E,qBAAqB,IAAI,uBAAuB,CAAC,CAAC,MAAM;EAC1D,CAAC;CACH;CASA,qCAAqC,MAAM,IAAI;CAE/C,KAAK,MAAM,CAAC,UAAU,aAAa,OAAO,QAAQ,KAAK,KAAK,GAC1D,iBAAiB,UAAU,OAAO,GAAG,KAAK,IAAI,aAAa,IAAI,YAAY;EACzE,cAAc;EACd,eAAe;EACf,uBAAuB,IAAI;EAI3B,qBAAqB;CACvB,CAAC;CAUH,wBAAwB,MAAM,IAAI;AACpC;;;;;;;;;;;;;;;;;;AAmBA,SAAS,qBACP,MACA,WACA,cACA,UACM;CAEN,KAAK,MAAM,SAAS,KAAK,UACvB,IAAI,MAAM,gBAAgB,kBAAkB,MAAM,oBAEhD,4BAA4B,OAAO,WAAW,cAAc,QAAQ;MAEpE,qBAAqB,OAAO,CAAC,GAAG,WAAW,KAAK,GAAG,OAAO,QAAQ;CAKtE,KAAK,MAAM,QAAQ,OAAO,OAAO,KAAK,KAAK,GACzC,qBAAqB,MAAM,WAAW,MAAM,QAAQ;AAExD;;;;;AAMA,SAAS,4BACP,kBACA,WACA,cACA,UACM;CACN,MAAM,SAAS,iBAAiB;CAChC,MAAM,cAAc,iBAAiB;CAGrC,MAAM,gBAAgB,UAAU,UAAU,SAAS,EAAE,CAAC;CACtD,MAAM,qBAAqB,0BAA0B,eAAe,QAAQ,WAAW;CAKvF,MAAM,gBAAgB,wBAAwB,SAAS;CACvD,MAAM,oBAAoB,cAAc,cAAc,SAAS;CAM/D,0BAA0B,kBAAkB,oBAHX,eAC7B;EAAE,MAAM;EAAQ,oBAAoB;EAAe;CAAkB,IACrE;EAAE,MAAM;EAAY;CAAkB,GAC6B,QAAQ;AACjF;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAS,sBACP,MACA,oBACA,OACA,OACM;CACN,IAAI,KAAK,MAGP,MAAM,KAAK,MAAM;EAAE,oBAAoB;EAAoB;CAAM,CAAC;CAGpE,KAAK,MAAM,SAAS,KAAK,UAAU;EACjC,MAAM,UAAU,MAAM,gBAAgB;EACtC,sBACE,OACA,UAAU,qBAAqB,GAAG,mBAAmB,GAAG,MAAM,eAC9D,UAAU,QAAQ,CAAC,GAAG,OAAO,KAAK,GAClC,KACF;CACF;AACF;;;;;AAkBA,SAAS,0BACP,MACA,oBACA,OACA,UACM;CACN,sBAAsB,MAAM,oBAAoB,CAAC,IAAI,OAAO,SAAS;EACnE,SAAS,KAAK;GAAE,GAAG;GAAO,oBAAoB,KAAK;EAAmB,CAAC;CACzE,CAAC;AACH;;;;;;;;;;;;;;AAeA,SAAS,kBAAkB,QAA6C;CACtE,QAAQ,QAAR;EACE,KAAK,OACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,KAAK,YACH,OAAO;EACT,KAAK,SACH,OAAO;CACX;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,0BACd,eACA,QACA,wBACQ;CACR,OAAO,oBAAoB,uBAAuB,eAAe,MAAM,GAAG,sBAAsB;AAClG;;;;;;;;;AAUA,SAAgB,oBAAoB,MAAc,aAA6B;CAC7E,OAAO,SAAS,MAAM,IAAI,gBAAgB,GAAG,KAAK,GAAG;AACvD;AAEA,SAAgB,uBAAuB,eAAuB,QAAoC;CAChG,MAAM,QAAQ,kBAAkB,MAAM;CACtC,IAAI,UAAU,QAAQ,OAAO;CAE7B,MAAM,QAAQ,cAAc,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;CACrD,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,MAAM,IAAI;CAC1C,OAAO,MAAM,WAAW,IAAI,MAAM,IAAI,MAAM,KAAK,GAAG;AACtD;;;;;;;;;;;;;;ACjmCA,SAAgB,6BAA6B,MAAyB;CACpE,uBAAuB,MAAM,IAAI,KAAK,OAAO;AAC/C;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAS,uBAAuB,WAAwB,aAAqB,MAAoB;CAC/F,MAAM,SAAsB,CAAC;CAC7B,sBAAsB,2BAAW,IAAI,IAAI,GAAG,aAAa,CAAC,GAAG,QAAQ,IAAI;CAEzE,MAAM,YAAY,4BAA4B,WAAW,QAAQ,IAAI;CACrE,IAAI,CAAC,WAAW;CAEhB,MAAM,IAAI,MACR,mFACe,UAAU,IAAI,cACd,UAAU,MAAM,cAChB,UAAU,OAAO,mUAMlC;AACF;;;;;;;;;;;;;;;;;AAkBA,SAAS,sBACP,MACA,MACA,aACA,OACA,QACA,MACM;CAUN,MAAM,cAAc,cAChB,GAAG,YAAY,GAAG,KAAK,gBACvB,KAAK,eAAe;CAKxB,IAAI,KAAK,gBAAgB,gBAAgB;EAIvC,uCACE,MACA,aACA,kBAAkB,MAAM,KAAK,OAAO,CACtC;EACA;CACF;CAEA,MAAM,eAAe,KAAK,QAAQ,KAAK;CACvC,IAAI,cAAc;EAChB,MAAM,WAAW,KAAK,IAAI,KAAK,OAAO;EACtC,IAAI,UACF,MAAM,IAAI,MACR,gHACiB,aAAa,MAAM,KAAK,EAAE,gBAC1B,SAAS,SAAS,QAAQ,SAAS,YAAY,iBAC/C,aAAa,SAAS,QAAQ,YAAY,6GAG7D;EAEF,KAAK,IAAI,KAAK,SAAS;GAAE,UAAU,aAAa;GAAU,aAAa;EAAY,CAAC;EACpF,OAAO,KAAK;GAAE,UAAU,aAAa;GAAU;EAAM,CAAC;CACxD;CAKA,KAAK,MAAM,SAAS,KAAK,UAEvB,sBACE,OACA,MACA,aAJc,MAAM,gBAAgB,UAK1B,QAAQ,CAAC,GAAG,OAAO,KAAK,GAClC,QACA,IACF;CAUF,KAAK,MAAM,YAAY,OAAO,OAAO,KAAK,KAAK,GAC7C,uBAAuB,UAAU,aAAa,aAAa,MAAM,KAAK,CAAC;AAE3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAS,uCACP,MACA,aACA,MACM;CACN,KAAK,MAAM,YAAY,OAAO,OAAO,KAAK,KAAK,GAE7C,uBAAuB,UAAU,aAAa,IAAI;CAEpD,KAAK,MAAM,SAAS,KAAK,UACvB,uCACE,OACA,GAAG,YAAY,GAAG,MAAM,eACxB,qBAAqB,OAAO,IAAI,CAClC;AAEJ;;;;;;;;;;AAWA,SAAS,aAAa,MAAc,OAA8B;CAChE,IAAI,MAAM,WAAW,GAAG,OAAO,QAAQ;CACvC,MAAM,OAAO,MAAM,KAAK,MAAM,EAAE,WAAW,CAAC,CAAC,KAAK,GAAG;CACrD,OAAO,GAAG,SAAS,MAAM,KAAK,KAAK,GAAG;AACxC;;;;;;;;;;;AAYA,SAAS,kBAAkB,MAAmB,cAA8B;CAC1E,OAAO,0BACL,cACA,KAAK,sBAAsB,OAC3B,KAAK,0BAA0B,KAAK,WACtC;AACF;;;;;;;;;;AAWA,SAAS,qBAAqB,OAAoB,MAAsB;CAGtE,IAAI,MAAM,gBAAgB,SAAS,OAAO;CAC1C,IAAI,MAAM,gBAAgB,gBAAgB,OAAO,kBAAkB,OAAO,IAAI;CAC9E,OAAO,SAAS,MAAM,IAAI,MAAM,gBAAgB,GAAG,KAAK,GAAG,MAAM;AACnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACxOA,SAAgB,sBAAsB,MAAyB;CAC7D,qBAAqB,MAAM,IAAI,IAAI;AACrC;;;;;;;;AASA,SAAS,qBACP,MACA,MACA,mBACM;CACN,KAAK,MAAM,CAAC,UAAU,aAAa,OAAO,QAAQ,KAAK,KAAK,GAAG;EAC7D,MAAM,WAAW,OAAO,GAAG,KAAK,IAAI,aAAa,IAAI;EACrD,IAAI,sBAAsB,MACxB,gBAAgB,UAAU,iBAAiB;EAE7C,qBAAqB,UAAU,UAAU,QAAQ;CACnD;CAEA,KAAK,MAAM,SAAS,KAAK,UACvB,qBACE,OACA,OAAO,GAAG,KAAK,GAAG,MAAM,gBAAgB,MAAM,aAC9C,iBACF;AAEJ;AAEA,SAAS,gBAAgB,UAAkB,mBAAkC;CAC3E,MAAM,IAAI,MACR,qFACsB,SAAS,qBACT,kBAAkB,+RAIO,SAAS,sQAI1C,kBAAkB,8DAElC;AACF;;;AC9FA,IAAM,wBAAQ,IAAI,IAAgE;;;;;;;AAQlF,SAAgB,eAAe,UAA0B;CACvD,IAAI;EACF,MAAM,OAAO,SAAS,QAAQ;EAC9B,MAAM,QAAQ,MAAM,IAAI,QAAQ;EAChC,IAAI,SAAS,MAAM,YAAY,KAAK,WAAW,MAAM,SAAS,KAAK,MAAM,OAAO,MAAM;EACtF,MAAM,UAAU,aAAa,UAAU,OAAO;EAC9C,MAAM,IAAI,UAAU;GAAE,SAAS,KAAK;GAAS,MAAM,KAAK;GAAM;EAAQ,CAAC;EACvE,OAAO;CACT,QAAQ;EACN,MAAM,OAAO,QAAQ;EACrB,OAAO,aAAa,UAAU,OAAO;CACvC;AACF;;;;;;;;;;ACaA,SAAS,SAAS,QAAoC;CACpD,IAAI;EACF,OAAO,SAAS,QAAQ,EAAE,MAAM,MAAM,CAAC;CACzC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;AAOA,SAAS,oBAAoB,SAAmC;CAC9D,MAAM,wBAAQ,IAAI,IAAY;CAC9B,KAAK,MAAM,QAAQ,QAAQ,MAAM;EAC/B,IAAI,KAAK,SAAS,0BAA0B;EAE5C,IAAI,KAAK,eAAe,QAAQ;EAEhC,IAAI,KAAK,aAAa;GACpB,IAAI,KAAK,YAAY,IAAI,MACvB,MAAM,IAAI,KAAK,YAAY,GAAG,IAAI;GAEpC,IAAI,KAAK,YAAY,cACd;SAAA,MAAM,QAAQ,KAAK,YAAY,cAClC,IAAI,KAAK,IAAI,MAAM,MAAM,IAAI,KAAK,GAAG,IAAI;GAAA;EAG/C;EAEA,IAAI,KAAK,YACP,KAAK,MAAM,QAAQ,KAAK,YAAY;GAElC,IAAI,KAAK,eAAe,QAAQ;GAChC,IAAI,KAAK,UAAU,QAAQ,KAAK,SAAS,SAAS,WAChD,MAAM,IAAI,KAAK,SAAS,IAAI;EAEhC;CAEJ;CACA,OAAO;AACT;;;;;;;;;;;;;;;;AAiBA,SAAS,iBAAiB,MAAkE;CAC1F,IAAI,KAAK,SAAS,4BAA4B,CAAC,KAAK,YAAY,OAAO;CACvE,IAAI,KAAK,eAAe,QAAQ,OAAO;CACvC,KAAK,MAAM,QAAQ,KAAK,YAAY;EAClC,IAAI,KAAK,eAAe,QAAQ;EAChC,IAAI,KAAK,UAAU,SAAS,WAAW,OAAO;CAChD;CACA,OAAO;AACT;;;;;;;AAQA,SAAS,iBAAiB,SAA+B;CACvD,KAAK,MAAM,QAAQ,QAAQ,MAAM;EAC/B,IAAI,KAAK,SAAS,4BAA4B,OAAO;EACrD,IAAI,iBAAiB,IAAI,GAAG,OAAO;CACrC;CACA,OAAO;AACT;;;;;;AAOA,SAAS,cAAc,SAA+B;CACpD,OAAO,QAAQ,KAAK,MACjB,SACC,KAAK,SAAS,0BAA0B,CAAE,KAAa,YAAY,KAAK,eAAe,MAC3F;AACF;;;;;AAsBA,SAAgB,iBAAiB,UAAkB,aAA6C;CAC9F,MAAM,0BAAU,IAAI,IAAY;CAChC,IAAI;EAEF,MAAM,UAAU,SADD,eAAe,QACL,CAAM;EAC/B,IAAI,CAAC,SAAS,OAAO;EACrB,MAAM,UAAU,oBAAoB,OAAO;EAC3C,KAAK,MAAM,QAAQ,aACjB,IAAI,QAAQ,IAAI,IAAI,GAAG,QAAQ,IAAI,IAAI;CAE3C,QAAQ,CAER;CACA,OAAO;AACT;;;;;AAMA,SAAgB,qBAAqB,UAA2B;CAC9D,IAAI,CAAC,WAAW,QAAQ,GAAG,OAAO;CAClC,IAAI;EAEF,MAAM,UAAU,SADD,eAAe,QACL,CAAM;EAC/B,IAAI,CAAC,SAAS,OAAO;EACrB,OAAO,iBAAiB,OAAO;CACjC,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,IAAM,gCAAgB,IAAI,IAAI;CAC5B;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;AAMD,IAAM,uCAAuB,IAAI,IAAI;CACnC;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;AAUD,SAAS,kBAAkB,MAAwB;CACjD,IAAI,UAAU;CACd,OAAO,qBAAqB,IAAI,QAAQ,IAAI,KAAK,QAAQ,YACvD,UAAU,QAAQ;CAEpB,OAAO;AACT;;;;;;;AAQA,SAAS,mBAAmB,SAAsB,MAAuB;CACvE,KAAK,MAAM,QAAQ,QAAQ,MAAM;EAE/B,MAAM,OAAO,KAAK,SAAS,2BAA2B,KAAK,cAAc;EACzE,IAAI,CAAC,MAAM;EACX,KACG,KAAK,SAAS,yBAAyB,KAAK,SAAS,uBACtD,KAAK,IAAI,SAAS,MAElB,OAAO;EAET,IAAI,KAAK,SAAS,uBACX;QAAA,MAAM,KAAK,KAAK,gBAAgB,CAAC,GACpC,IAAI,EAAE,IAAI,SAAS,QAAQ,EAAE,QAAQ,cAAc,IAAI,kBAAkB,EAAE,IAAI,CAAC,CAAC,IAAI,GACnF,OAAO;EAAA;CAIf;CACA,OAAO;AACT;;;;;;;;;;;;;;;;AAiBA,SAAgB,8BAA8B,UAA2B;CACvE,IAAI,CAAC,WAAW,QAAQ,GAAG,OAAO;CAClC,IAAI;EAEF,MAAM,UAAU,SADD,eAAe,QACL,CAAM;EAC/B,IAAI,CAAC,SAAS,OAAO;EACrB,KAAK,MAAM,QAAQ,QAAQ,MAAM;GAC/B,IAAI,KAAK,SAAS,4BAA4B;IAC5C,MAAM,OAAO,KAAK,eAAe,kBAAkB,KAAK,WAAW;IACnE,IAAI,CAAC,MAAM,OAAO;IAClB,IAAI,cAAc,IAAI,KAAK,IAAI,GAAG,OAAO;IACzC,IAAI,KAAK,SAAS,gBAAgB,KAAK,MACrC,OAAO,mBAAmB,SAAS,KAAK,IAAI;IAE9C,OAAO;GACT;GAMA,MAAM,OAAO,iBAAiB,IAAI;GAClC,IAAI,MAAM;IAGR,IAAI,KAAK,QAAQ,OAAO;IACxB,OAAO,KAAK,OAAO,OAAO,mBAAmB,SAAS,KAAK,MAAM,IAAI,IAAI;GAC3E;EACF;EACA,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;;;;AAMA,SAAgB,kBAAkB,UAA2B;CAC3D,IAAI,CAAC,WAAW,QAAQ,GAAG,OAAO;CAClC,IAAI;EAEF,MAAM,UAAU,SADD,eAAe,QACL,CAAM;EAC/B,IAAI,CAAC,SAAS,OAAO;EACrB,OAAO,cAAc,OAAO;CAC9B,QAAQ;EACN,OAAO;CACT;AACF;AAaA,SAAS,oBAAoB,MAAwB;CACnD,IAAI,CAAC,MAAM,OAAO,KAAA;CAClB,MAAM,IAAI;CACV,IAAI,EAAE,SAAS,WAAW,OAAO,EAAE;CACnC,IAAI,EAAE,SAAS,qBAAqB,EAAE,aAAa,OAAO,EAAE,UAAU,SAAS,WAC7E,OAAO,CAAE,EAAE,SAAS;CAEtB,IAAI,EAAE,SAAS,mBAAmB;EAChC,MAAM,WAAW,EAAE;EACnB,IAAI,CAAC,UAAU,OAAO,KAAA;EACtB,MAAM,SAAoB,CAAC;EAC3B,KAAK,MAAM,MAAM,UAAU;GACzB,MAAM,IAAI,oBAAoB,EAAE;GAChC,IAAI,MAAM,KAAA,GAAW,OAAO,KAAA;GAC5B,OAAO,KAAK,CAAC;EACf;EACA,OAAO;CACT;AAEF;AAEA,SAAS,qBAAqB,MAAoD;CAChF,IAAK,KAAa,SAAS,oBAAoB,OAAO,KAAA;CACtD,MAAM,QAAS,KAAa;CAC5B,IAAI,CAAC,OAAO,OAAO,KAAA;CACnB,MAAM,SAAkC,CAAC;CACzC,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,IAAI;EACV,IAAI,EAAE,SAAS,iBAAiB,OAAO,KAAA;EACvC,IAAI,EAAE,SAAS,cAAc,EAAE,UAAU,OAAO,KAAA;EAChD,MAAM,MACJ,EAAE,KAAK,SAAS,eACZ,EAAE,IAAI,OACN,EAAE,KAAK,SAAS,YACd,OAAO,EAAE,IAAI,KAAK,IAClB,KAAA;EACR,IAAI,CAAC,KAAK,OAAO,KAAA;EACjB,MAAM,QAAQ,oBAAoB,EAAE,KAAK;EACzC,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAChC,OAAO,OAAO;CAChB;CACA,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,mBAAmB,UAAoD;CACrF,IAAI,CAAC,WAAW,QAAQ,GAAG,OAAO,KAAA;CAClC,IAAI;EAEF,MAAM,UAAU,SADD,eAAe,QACL,CAAM;EAC/B,IAAI,CAAC,SAAS,OAAO,KAAA;EAErB,KAAK,MAAM,QAAQ,QAAQ,MAAM;GAC/B,IAAI,KAAK,SAAS,0BAA0B;GAC5C,IAAI,KAAK,eAAe,QAAQ;GAChC,IAAI,CAAC,KAAK,aAAa;GACvB,MAAM,OAAO,KAAK;GAClB,IAAI,KAAK,SAAS,uBAAuB;GACzC,MAAM,eAAe,KAAK;GAM1B,IAAI,CAAC,cAAc;GACnB,KAAK,MAAM,KAAK,cAAc;IAC5B,IAAI,EAAE,IAAI,SAAS,eAAe,CAAC,EAAE,MAAM;IAC3C,MAAM,OAAO,EAAE;IACf,IAAI,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU,WACnD,OAAO,KAAK;IAEd,IAAI,KAAK,SAAS,oBAAoB;KACpC,MAAM,MAAM,qBAAqB,IAAI;KACrC,IAAI,CAAC,KAAK,OAAO,KAAA;KACjB,MAAM,SAA4C,CAAC;KACnD,IAAI,SAAS,OAAO,OAAO,IAAI,QAAQ,UAAU,OAAO,MAAM,IAAI;KAClE,IACE,UAAU,OACV,MAAM,QAAQ,IAAI,IAAI,KACtB,IAAI,KAAK,OAAO,MAAe,OAAO,MAAM,QAAQ,GAEpD,OAAO,OAAO,IAAI;KAEpB,OAAO;IACT;IACA;GACF;EACF;EACA;CACF,QAAQ;EACN;CACF;AACF;AAqBA,SAAgB,cAAc,UAA8B;CAC1D,IAAI,CAAC,WAAW,QAAQ,GAAG,OAAO;CAGlC,MAAM,UAAU,SAAS,eAAe,QAAQ,CAAC;CACjD,IAAI,CAAC,SAAS,OAAO;CAOrB,IAAI,QAAoB;CACxB,KAAK,MAAM,QAAQ,QAAQ,MAAM;EAC/B,IAAI,KAAK,SAAS,4BAA4B,KAAK,eAAe,QAAQ;EAC1E,KAAK,MAAM,QAAQ,KAAK,cAAc,CAAC,GACrC,IAAI,KAAK,eAAe,UAAU,cAAc,KAAK,QAAQ,MAAM,QACjE,OAAO;EAGX,MAAM,OAAO,KAAK;EAClB,IAAI,CAAC,MAAM;EACX,IAAI,KAAK,IAAI,SAAS,QAAQ,OAAO;EACrC,IAAI,KAAK,SAAS,uBAAuB;EACzC,KAAK,MAAM,KAAK,KAAK,gBAAgB,CAAC,GAAG;GACvC,MAAM,KAAK,EAAE;GACb,IAAI,IAAI,SAAS,cAAc;IAC7B,IAAI,GAAG,SAAS,QAAQ;IACxB,MAAM,OAAO,EAAE,QAAS,kBAAkB,EAAE,IAAI;IAChD,MAAM,eAAe,MAAM,SAAS,aAAa,KAAK,UAAU;IAChE,IAAI,KAAK,SAAS,WAAW,CAAC,cAAc,OAAO;IACnD,QAAQ;GACV,OAAO,IAAI,MAAM,aAAa,IAAI,MAAM,GACtC,OAAO;EAEX;CACF;CACA,OAAO;AACT;;AAGA,SAAS,cAAc,MAA0E;CAC/F,IAAI,CAAC,MAAM,OAAO,KAAA;CAClB,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAK;AAC5D;;AAGA,SAAS,aAAa,MAAe,MAAuB;CAC1D,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU,OAAO;CAC9C,MAAM,IAAI;CACV,QAAQ,EAAE,MAAV;EACE,KAAK,cACH,OAAO,EAAE,SAAS;EACpB,KAAK,iBACH,QAAS,EAAE,cAA4B,CAAC,EAAA,CAAG,MAAM,SAAS;GACxD,MAAM,IAAI;GACV,OAAO,aAAa,EAAE,SAAS,gBAAgB,EAAE,WAAW,EAAE,OAAO,IAAI;EAC3E,CAAC;EACH,KAAK,gBACH,QAAS,EAAE,YAA0B,CAAC,EAAA,CAAG,MAAM,OAAO,aAAa,IAAI,IAAI,CAAC;EAC9E,KAAK,eACH,OAAO,aAAa,EAAE,UAAU,IAAI;EACtC,KAAK,qBACH,OAAO,aAAa,EAAE,MAAM,IAAI;EAClC,SACE,OAAO;CACX;AACF;;;;;AAMA,SAAgB,iBAAiB,UAAkB,WAA4B;CAC7E,IAAI,CAAC,WAAW,QAAQ,GAAG,OAAO;CAClC,IAAI;EAEF,MAAM,UAAU,SADD,eAAe,QACL,CAAM;EAC/B,IAAI,CAAC,SAAS,OAAO;EACrB,KAAK,MAAM,QAAQ,QAAQ,MAAM;GAC/B,IAAI,KAAK,SAAS,uBAAuB;GACzC,MAAM,OAAQ,KAAa;GAC3B,IAAI,MAAM,SAAS,aAAa,OAAO,KAAK,UAAU,UAAU;GAChE,IAAI,KAAK,UAAU,WAAW,OAAO;EACvC;EACA,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;;;;;ACrfA,IAAM,uCAAuB,IAAI,IAAI;CAAC;CAAQ;CAAU;CAAW;AAAQ,CAAC;;;;AAK5E,IAAM,oCAAoB,IAAI,IAAI;CAAC;CAAc;CAAU;AAAO,CAAC;;;;;;AAOnE,IAAM,sBAAsB;;;;;;;;;;;;;AAc5B,SAAgB,sBACd,MACA,KACA,iBAAoC,yBAC3B;CACT,MAAM,YAAY,eAAe,SAAS,GAAG;CAC7C,MAAM,cAAc,YAAY,KAAK,GAAG;CACxC,IAAI,qBAAqB,IAAI,IAAI,KAAK,WAAW,OAAO;CACxD,KAAK,SAAS,WAAW,SAAS,mBAAmB,aAAa,OAAO;CACzE,KAAK,kBAAkB,IAAI,IAAI,KAAK,SAAS,YAAY,aAAa,OAAO;CAC7E,IAAI,oBAAoB,KAAK,IAAI,MAAM,eAAe,QAAQ,SAAS,OAAO;CAC9E,OAAO;AACT;;;;;;;;AASA,SAAgB,WAAW,QAAgB,SAAwB,CAAC,GAAc;CAChF,MAAM,iBAAiB,OAAO,kBAAkB;CAChD,MAAM,SAAS,IAAI,IAAI,cAAc;CAErC,MAAM,OAAkB,EACtB,MAAM,kBAAkB,IAAI,UAAU,GAAG,EAC3C;CAGA,MAAM,YAAY,cAAc,QAAQ,OAAO;CAC/C,IAAI,WACF,KAAK,QAAQ;CAOf,MAAM,kBAAkB,kBAAkB,QAAQ,cAAc;CAChE,IAAI,iBACF,KAAK,cAAc;CAIrB,iBAAiB,QAAQ,KAAK,MAAM,MAAM;CAG1C,aAAa,QAAQ,KAAK,MAAM,MAAM;CAMtC,sBAAsB,KAAK,IAAI;CAG/B,6BAA6B,KAAK,IAAI;CAItC,4BAA4B,KAAK,IAAI;CAIrC,2BAA2B,KAAK,IAAI;CAIpC,gCAAgC,KAAK,IAAI;CAIzC,8BAA8B,KAAK,IAAI;CAEvC,OAAO;AACT;;;;AAKA,SAAS,kBACP,aACA,aACA,SACA,WACA,oBACA,wBACA,aACA,aACa;CACb,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,UAAU,CAAC;EACX,OAAO,CAAC;CACV;AACF;;;;;AAMA,SAAS,eAAe,eAAuB,SAAiB,aAAkC;CAEhG,IAAI,gBAAgB,WAAW,gBAAgB,UAAU,gBAAgB,gBACvE,OAAO;CAIT,OAAO,GADY,kBAAkB,MAAM,KAAK,cAC3B,GAAG;AAC1B;;;;AAKA,SAAS,iBAAiB,SAAiB,MAAmB,QAA2B;CACvF,IAAI;CACJ,IAAI;EACF,UAAU,YAAY,OAAO;CAC/B,SAAS,KAAK;EACZ,QAAQ,KAAK,0CAA0C,WAAW,EAAE,OAAO,OAAO,CAAC;EACnF;CACF;CAEA,KAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,KAAK,SAAS,KAAK;EAGpC,IAAI;GACF,IAAI,SAAS,QAAQ,CAAC,CAAC,YAAY,GAAG;EACxC,QAAQ;GACN;EACF;EAEA,MAAM,MAAM,QAAQ,KAAK,CAAC,CAAC,MAAM,CAAC;EAClC,MAAM,OAAO,SAAS,OAAO,IAAI,KAAK;EAGtC,IAAI,qBAAqB,IAAI,IAAI,KAAK,OAAO,IAAI,GAAG,GAAG;GACrD,MAAM,MAAM;GACZ,MAAM,WAAW,KAAK;GACtB,IAAI,UACF,yBAAyB,MAAM,SAAS,UAAU,QAAQ;GAE5D,KAAK,OAAO;IAAE,UAAU;IAAU,WAAW;GAAI;GACjD;EACF;EAGA,IAAI,SAAS,WAAW,YAAY,KAAK,GAAG,GAAG;GAC7C,MAAM,WAAW,KAAK;GACtB,IAAI,UACF,yBAAyB,SAAS,SAAS,UAAU,QAAQ;GAE/D,KAAK,QAAQ;IAAE,UAAU;IAAU,WAAW;GAAI;GAClD;EACF;EAMA,IAAI,kBAAkB,IAAI,IAAI,KAAK,YAAY,KAAK,GAAG,GAAG;GACxD,MAAM,MAAM;GACZ,MAAM,WAAW,KAAK;GACtB,IAAI,UACF,yBAAyB,MAAM,SAAS,UAAU,QAAQ;GAE5D,KAAK,OAAO;IAAE,UAAU;IAAU,WAAW;GAAI;GACjD;EACF;EAIA,IAAI,oBAAoB,KAAK,IAAI,KAAK,QAAQ,QAAQ;GACpD,IAAI,CAAC,KAAK,iBACR,KAAK,kBAAkB,CAAC;GAE1B,KAAK,gBAAgB,QAAQ;IAAE,UAAU;IAAU,WAAW;GAAI;GAClE;EACF;EAGA,IAAI,oBAAoB,KAAK,IAAI,KAAK,YAAY,KAAK,GAAG,GAAG;GAC3D,IAAI,CAAC,KAAK,aACR,KAAK,cAAc,CAAC;GAEtB,MAAM,WAAW,KAAK,YAAY;GAClC,IAAI,UACF,yBAAyB,MAAM,SAAS,UAAU,QAAQ;GAE5D,KAAK,YAAY,QAAQ;IAAE,UAAU;IAAU,WAAW;GAAI;GAC9D;EACF;EAQA,MAAM,WAAW,sBAAsB,KAAK;EAC5C,IAAI,UAAU;GACZ,IAAI,CAAC,SAAS,YAAY,KAAK,gBAAgB,IAC7C,MAAM,IAAI,MACR,iBAAiB,KAAK,uFACT,SAAS,0EAExB;GAEF,IAAI,CAAC,KAAK,gBACR,KAAK,iBAAiB,CAAC;GAEzB,MAAM,WAAW,KAAK,eAAe;GACrC,IAAI,UAAU;IACZ,MAAM,oBAAoB,2BAA2B,MAAM,SAAS,SAAS;IAC7E,MAAM,eAAe,2BAA2B,MAAM,GAAG;IACzD,IAAI,sBAAsB,cACxB,yBAAyB,MAAM,SAAS,UAAU,QAAQ;IAG5D,IAAI,cACF,KAAK,eAAe,QAAQ;KAAE,UAAU;KAAU,WAAW;IAAI;GAErE,OACE,KAAK,eAAe,QAAQ;IAAE,UAAU;IAAU,WAAW;GAAI;EAErE;CACF;CAGA,IAAI,KAAK,SAAS,KAAK,MACrB,MAAM,IAAI,MACR,qFACiB,KAAK,MAAM,SAAS,gBACpB,KAAK,KAAK,SAAS,gEAEtC;CAOF,IAAI,KAAK,SAAS,cAAc,KAAK,MAAM,QAAQ,MAAM,UACvD,KAAK,aAAa;AAEtB;;;;;;;;AASA,SAAS,yBACP,YACA,WACA,YACO;CACP,MAAM,IAAI,MACR,2BAA2B,WAAW,gDACvB,UAAU,cACV,WAAW,6BACI,WAAW,yCAC3C;AACF;;;;AAKA,SAAS,aAAa,SAAiB,YAAyB,QAA2B;CACzF,IAAI;CACJ,IAAI;EACF,UAAU,YAAY,OAAO;CAC/B,SAAS,KAAK;EACZ,QAAQ,KAAK,sCAAsC,WAAW,EAAE,OAAO,OAAO,CAAC;EAC/E;CACF;CAEA,KAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,KAAK,SAAS,KAAK;EAEpC,IAAI;GACF,IAAI,CAAC,SAAS,QAAQ,CAAC,CAAC,YAAY,GAAG;EACzC,QAAQ;GACN;EACF;EAKA,IAAI,qBAAqB,KAAK,KAAK,GACjC,MAAM,IAAI,MACR,8FACkB,SAAS,mIAG7B;EAEF,IAAI,aAAa,KAAK,KAAK,GACzB,MAAM,IAAI,MACR,kFACkB,SAAS,gHAG7B;EAGF,MAAM,EACJ,MACA,WACA,aACA,aACA,oBACA,2BACE,gBAAgB,KAAK;EAGzB,IAAI,SAAS,WAAW;EAGxB,MAAM,YAAY,kBAChB,OACA,MAHc,eAAe,WAAW,SAAS,OAAO,IAIxD,GACA,WACA,oBACA,wBACA,aACA,WACF;EAGA,iBAAiB,UAAU,WAAW,MAAM;EAG5C,aAAa,UAAU,WAAW,MAAM;EAGxC,IAAI,SAAS,QAAQ;GACnB,MAAM,WAAW,MAAM,MAAM,CAAC;GAC9B,WAAW,MAAM,YAAY;EAC/B,OACE,WAAW,SAAS,KAAK,SAAS;CAEtC;AACF;;;;;;;;;;;;AAaA,SAAS,4BAA4B,MAAyB;CAC5D,uBAAuB,sBAAM,IAAI,IAAI,CAAC;AACxC;;;;;AAMA,SAAS,uBAAuB,MAAmB,MAAiC;CAOlF,MAAM,EAAE,cAAc,oBAAoB,IAAI;CAG9C,IAAI,WAAW;EACb,MAAM,WAAW,KAAK,IAAI,SAAS;EACnC,IAAI,UACF,MAAM,IAAI,MACR,kCAAkC,UAAU,yCACnB,SAAS,oBACb,KAAK,WAAW,IAAI,mDAE3C;EAGF,OAAO,IAAI,IAAI,IAAI;EACnB,KAAK,IAAI,WAAW,KAAK,WAAW,GAAG;CACzC;CAGA,KAAK,MAAM,SAAS,KAAK,UACvB,uBAAuB,OAAO,IAAI;CAapC,KAAK,MAAM,YAAY,OAAO,OAAO,KAAK,KAAK,GAC7C,uBAAuB,UAAU,IAAI,IAAI,IAAI,CAAC;AAElD;;;;;;;;;;;;;;AAeA,SAAS,2BAA2B,MAAyB;CAE3D,oBAAoB,sBAAM,IADR,IACQ,CAAK;AACjC;AAEA,SAAS,oBACP,MACA,OACM;CAGN,MAAM,EAAE,WAAW,gBAAgB,oBAAoB,IAAI;CAE3D,IAAI,aAAa,gBAAgB,YAAY,gBAAgB,SAAS;EACpE,MAAM,WAAW,MAAM,IAAI,SAAS;EACpC,IAAI,YAAY,SAAS,gBAAgB,aAAa;GACpD,MAAM,kBAA0C;IAC9C,WAAW,IAAI,UAAU;IACzB,aAAa,OAAO,UAAU;IAC9B,sBAAsB,QAAQ,UAAU;GAC1C;GACA,MAAM,IAAI,MACR,wBAAwB,UAAU,oCAC3B,gBAAgB,SAAS,gBAAgB,SAAS,YAAY,OAAO,SAAS,QAAQ,MACtF,gBAAgB,gBAAgB,YAAY,OAAO,KAAK,WAAW,IAAI,0HAExB,UAAU,OAAO,UAAU,MACnF;EACF;EACA,IAAI,CAAC,UACH,MAAM,IAAI,WAAW;GACnB;GACA,SAAS,KAAK,WAAW;EAC3B,CAAC;CAEL;CAEA,KAAK,MAAM,SAAS,KAAK,UACvB,oBAAoB,OAAO,KAAK;CAElC,KAAK,MAAM,YAAY,OAAO,OAAO,KAAK,KAAK,GAC7C,oBAAoB,UAAU,KAAK;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAS,gCAAgC,MAAyB;CAChE,MAAM,UAAU,wBAAwB,IAAI;CAE5C,IAAI,QAAQ,UAAU,GAAG;EACvB,MAAM,2BAAW,IAAI,IAA2B;EAChD,KAAK,MAAM,SAAS,SAAS;GAK3B,MAAM,MAAM,GAAG,cAAc,MAAM,KAAK,EAAE,IAAI,cAAc,KAAK;GACjE,MAAM,QAAQ,SAAS,IAAI,GAAG;GAC9B,IAAI,OACF,MAAM,KAAK,KAAK;QAEhB,SAAS,IAAI,KAAK,CAAC,KAAK,CAAC;EAE7B;EAEA,KAAK,MAAM,GAAG,UAAU,UACtB,IAAI,MAAM,UAAU,GAClB,MAAM,IAAI,MACR,sFACe,KAAK,WAAW,IAAI,gBAClB,MAAM,KAAK,MAAM,EAAE,WAAW,CAAC,CAAC,KAAK,IAAI,EAAE,qCACtB,cAAc,MAAM,EAAG,EAAE,6KAGjE;CAGN;CAEA,KAAK,MAAM,SAAS,KAAK,UACvB,gCAAgC,KAAK;CAEvC,KAAK,MAAM,YAAY,OAAO,OAAO,KAAK,KAAK,GAC7C,gCAAgC,QAAQ;AAE5C;;;;;;;;;;;;;;;;;AAkBA,SAAS,cAAc,QAAqB,OAA4B;CACtE,IAAI,MAAM,gBAAgB,gBAAgB,OAAO;CACjD,OAAO,aAAa,uBAAuB,OAAO,SAAS,MAAM,sBAAsB,KAAK;AAC9F;;AAGA,SAAS,cAAc,MAA2B;CAChD,MAAM,EAAE,aAAa,gBAAgB,oBAAoB,IAAI;CAC7D,QAAQ,aAAa,UAAU,MAAM,aAAa,UAAU;AAC9D;;;;;;;;;;;;;AAcA,SAAS,wBAAwB,MAAkC;CACjE,MAAM,SAAwB,CAAC;CAC/B,KAAK,MAAM,SAAS,KAAK,UAAU;EACjC,MAAM,EAAE,aAAa,aAAa,gBAAgB,oBAAoB,KAAK;EAC3E,IAAI,gBAAgB,cAAc,eAAe,cAC/C,OAAO,KAAK,KAAK;OACZ,IAAI,gBAAgB,SACzB,OAAO,KAAK,GAAG,wBAAwB,KAAK,CAAC;CAEjD;CACA,OAAO;AACT;;;;AAKA,SAAS,cAAc,SAAiB,MAAqC;CAC3E,KAAK,MAAM,OAAO,CAAC,MAAM,KAAK,GAAG;EAC/B,MAAM,WAAW,KAAK,SAAS,GAAG,KAAK,GAAG,KAAK;EAC/C,IAAI;GACF,IAAI,SAAS,QAAQ,CAAC,CAAC,OAAO,GAC5B,OAAO;IAAE,UAAU;IAAU,WAAW;GAAI;EAEhD,QAAQ,CAER;CACF;AAEF;;;;;;AAOA,SAAS,kBAAkB,SAAiB,MAAqC;CAC/E,KAAK,MAAM,OAAO;EAAC;EAAO;EAAM;EAAO;CAAI,GAAG;EAC5C,MAAM,WAAW,KAAK,SAAS,GAAG,KAAK,GAAG,KAAK;EAC/C,IAAI;GACF,IAAI,SAAS,QAAQ,CAAC,CAAC,OAAO,GAC5B,OAAO;IAAE,UAAU;IAAU,WAAW;GAAI;EAEhD,QAAQ,CAER;CACF;AAEF"}