@orkestrel/router 0.0.12 → 0.0.13

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.
@@ -3,7 +3,7 @@ import { EmitterHooks } from '@orkestrel/emitter';
3
3
  import { EmitterInterface } from '@orkestrel/emitter';
4
4
 
5
5
  /**
6
- * The native-override seam — a predicate deciding whether an entry's `meta`
6
+ * Represents the native-override seam — a predicate deciding whether an entry's `meta`
7
7
  * ANSWERS a given `match` call, beyond path matching.
8
8
  *
9
9
  * @typeParam Meta - The entry payload the predicate reads
@@ -14,12 +14,12 @@ import { EmitterInterface } from '@orkestrel/emitter';
14
14
  * === requestMethod`), the `Navigator` omits the predicate entirely (every
15
15
  * path match always answers). Passed per-call to {@link RouterInterface.match};
16
16
  * when omitted, every entry whose path matches is eligible. Total — it never
17
- * throws (a consumer keeps it pure, per AGENTS §14 guard totality).
17
+ * throws, so a consumer keeps it pure.
18
18
  */
19
19
  export declare type AnswerHandler<Meta> = (meta: Meta) => boolean;
20
20
 
21
21
  /**
22
- * Canonicalize a route path for REGISTRY IDENTITY — strip a single trailing
22
+ * Canonicalizes a route path for REGISTRY IDENTITY — strips a single trailing
23
23
  * slash, except the root `/` (and the empty pattern). The trailing-slash fold
24
24
  * {@link compilePath} normalizes a pattern through, so identity agrees with the
25
25
  * matcher.
@@ -45,10 +45,10 @@ export declare type AnswerHandler<Meta> = (meta: Meta) => boolean;
45
45
  export declare function canonicalizePath(path: string): string;
46
46
 
47
47
  /**
48
- * Classify one path segment into its specificity TIER — the SAME syntax
48
+ * Classifies one path segment into its specificity TIER — the SAME syntax
49
49
  * {@link compilePath} rewrites: a syntactically valid `:name` head is a PARAM
50
50
  * segment, a final `*name` is a WILDCARD segment, everything else (including a
51
- * literal segment that merely CONTAINS a `:` mid-string, e.g. `a:b`) is a
51
+ * literal segment that merely CONTAINS a `:` mid-string, for example `a:b`) is a
52
52
  * LITERAL segment.
53
53
  *
54
54
  * @remarks
@@ -56,11 +56,12 @@ export declare function canonicalizePath(path: string): string;
56
56
  * segment `includes(':')` as a param, so a literal segment like `a:b` was
57
57
  * mis-tiered even though {@link compilePath} compiles it literally. Sharing one
58
58
  * segment parser between compilation and classification keeps the two in
59
- * agreement (§4 fixes). Pure and total.
59
+ * agreement. Pure and total.
60
60
  *
61
61
  * @param segment - One `/`-split path segment
62
- * @param isFinal - Whether `segment` is the last segment of its path (only the
63
- * final segment may be classified as a wildcard)
62
+ * @param isFinal - If `true`, `segment` is the path's last segment and may
63
+ * classify as a wildcard; if `false`, a wildcard-shaped segment classifies as
64
+ * a literal
64
65
  * @returns The segment's specificity tier — {@link import('./constants.js').TIER_LITERAL},
65
66
  * {@link import('./constants.js').TIER_PARAM}, or
66
67
  * {@link import('./constants.js').TIER_WILDCARD}
@@ -76,7 +77,7 @@ export declare function canonicalizePath(path: string): string;
76
77
  export declare function classifySegment(segment: string, isFinal: boolean): number;
77
78
 
78
79
  /**
79
- * Compare two route paths by SPECIFICITY — the comparator that picks the
80
+ * Compares two route paths by SPECIFICITY — the comparator that picks the
80
81
  * most-specific matching route (literal-over-param-over-wildcard,
81
82
  * registration-order-independent).
82
83
  *
@@ -104,12 +105,12 @@ export declare function classifySegment(segment: string, isFinal: boolean): numb
104
105
  export declare function compareSpecificity(a: string, b: string): number;
105
106
 
106
107
  /**
107
- * A compiled route path — the anchored regex plus its ordered param names.
108
+ * Represents a compiled route path — the anchored regex plus its ordered param names.
108
109
  *
109
110
  * @remarks
110
- * The once-per-path compile output of `compilePath` (U1 `helpers.ts`):
111
+ * The once-per-path compile output of `compilePath` (the path compiler in `helpers.ts`):
111
112
  * `regex` is anchored (`^…$`) and matches the WHOLE pathname (with an
112
- * optional trailing slash, §4), and `params` lists each captured segment's
113
+ * optional trailing slash), and `params` lists each captured segment's
113
114
  * name (`:name` or the final `*name`) in order, so a `regex.exec` result's
114
115
  * capture groups line up with `params` positionally (the walk `matchPath`
115
116
  * performs). Plain data — no behavior.
@@ -120,7 +121,7 @@ export declare interface CompiledPath {
120
121
  }
121
122
 
122
123
  /**
123
- * Compile a route path pattern into an anchored regex and its ordered param
124
+ * Compiles a route path pattern into an anchored regex and its ordered param
124
125
  * names.
125
126
  *
126
127
  * @remarks
@@ -128,7 +129,7 @@ export declare interface CompiledPath {
128
129
  * `([^/]+)` capture group; the FINAL segment may instead be `*name`, which
129
130
  * becomes a `(.+)` capture spanning the REST of the path including slashes — a
130
131
  * wildcard segment anywhere but last is a registration-time programmer error
131
- * and throws `TypeError` (§14 construction/registration boundary). Every regex
132
+ * and throws a `ContractError` at the construction/registration boundary. Every regex
132
133
  * metacharacter in a literal segment is escaped first ({@link escapeRegExp}),
133
134
  * so a path like `/files/:name.json` matches the `.` literally apart from the
134
135
  * param. The regex is anchored (`^…$`), so it matches the whole pathname, not
@@ -146,942 +147,975 @@ export declare interface CompiledPath {
146
147
  * regex flag, so `/Users` matches `/users`. The pattern's own casing is never
147
148
  * altered — only the matching behavior.
148
149
  *
149
- * @param path - The route path pattern (e.g. `/users/:id`, `/files/*rest`)
150
- * @param sensitive - Case-sensitive matching (default `true`)
150
+ * @param path - The route path pattern (for example `/users/:id`, `/files/*rest`)
151
+ * @param sensitive - If `true`, matching is case-sensitive; if `false`, case is
152
+ * folded during matching. Default: `true`
151
153
  * @returns The {@link CompiledPath} — its `regex` + ordered `params`
152
- * @throws {TypeError} When a `*name` wildcard segment is not the FINAL segment
153
- *
154
- * @example
155
- * ```ts
156
- * const { regex, params } = compilePath('/users/:id/posts/:slug')
157
- * params // ['id', 'slug']
158
- * regex.exec('/users/7/posts/hello') // ['', '7', 'hello']
159
- * regex.test('/users/7/posts/hello/') // true the trailing slash is optional
160
- *
161
- * compilePath('/files/*rest').regex.test('/files/a/b.png') // true
162
- * compilePath('/Users', false).regex.test('/users') // true — case-insensitive
163
- * ```
164
- */
165
- export declare function compilePath(path: string, sensitive?: boolean): CompiledPath;
154
+ * @throws {@link import('@orkestrel/contract').ContractError} Thrown when a
155
+ * `*name` wildcard segment is not the FINAL segment
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * const { regex, params } = compilePath('/users/:id/posts/:slug')
160
+ * params // ['id', 'slug']
161
+ * regex.exec('/users/7/posts/hello') // ['…', '7', 'hello']
162
+ * regex.test('/users/7/posts/hello/') // true — the trailing slash is optional
163
+ *
164
+ * compilePath('/files/*rest').regex.test('/files/a/b.png') // true
165
+ * compilePath('/Users', false).regex.test('/users') // true — case-insensitive
166
+ * ```
167
+ */
168
+ export declare function compilePath(path: string, sensitive?: boolean): CompiledPath;
166
169
 
167
- /**
168
- * Compute the registry key for a method-dimensioned dispatcher route.
169
- *
170
- * @remarks
171
- * Combines the route record's HTTP method with the outer entry's canonical
172
- * path, so registrations that differ only by one trailing slash replace each
173
- * other while registrations for different methods remain distinct.
174
- *
175
- * @param entry - The dispatcher route entry to identify
176
- * @returns The canonical `METHOD /path` registry key
177
- *
178
- * @example
179
- * ```ts
180
- * computeDispatchKey({ path: '/health/', meta: { method: 'GET' } }) // 'GET /health'
181
- * ```
182
- */
183
- export declare function computeDispatchKey(entry: RouteEntry<{
184
- readonly method: Method;
185
- }>): string;
170
+ /**
171
+ * Computes the registry key for a method-dimensioned dispatcher route.
172
+ *
173
+ * @remarks
174
+ * Combines the route record's HTTP method with the outer entry's canonical
175
+ * path, so registrations that differ only by one trailing slash replace each
176
+ * other while registrations for different methods remain distinct.
177
+ *
178
+ * @param entry - The dispatcher route entry to identify
179
+ * @returns The canonical `METHOD /path` registry key
180
+ *
181
+ * @example
182
+ * ```ts
183
+ * computeDispatchKey({ path: '/health/', meta: { method: 'GET' } }) // 'GET /health'
184
+ * ```
185
+ */
186
+ export declare function computeDispatchKey(entry: RouteEntry<{
187
+ readonly method: Method;
188
+ }>): string;
186
189
 
187
- /**
188
- * Compute a route path's SPECIFICITY VECTOR — the per-segment type ranking
189
- * that breaks a tie when several registered routes match the same concrete
190
- * pathname.
191
- *
192
- * @remarks
193
- * Splits the CANONICALIZED path into segments (on `/`) and maps each to its
194
- * specificity tier via {@link classifySegment} — the same segment parser
195
- * {@link compilePath} uses, so a literal segment that merely contains a `:`
196
- * (e.g. `a:b`) is correctly tiered as literal rather than param (the old
197
- * engine's bug, fixed here). The standard route-precedence rule compares two
198
- * matching routes' vectors LEFT-TO-RIGHT: at the first index where the tiers
199
- * differ, the HIGHER tier (a literal over a param over a wildcard) is MORE
200
- * SPECIFIC and wins — so `/users/me` (`[2, 2]`) beats `/users/:id` (`[2, 1]`)
201
- * beats `/users/*rest` (`[2, 0]`) regardless of registration order. Two routes
202
- * that match the SAME concrete pathname necessarily have the same segment
203
- * count in the common case; {@link compareSpecificity} handles the general
204
- * case for totality.
205
- *
206
- * @param path - The route path pattern (e.g. `/users/:id`)
207
- * @returns The per-segment specificity tiers, in order
208
- *
209
- * @example
210
- * ```ts
211
- * computeSpecificity('/users/me') // [2, 2]
212
- * computeSpecificity('/users/:id') // [2, 1]
213
- * computeSpecificity('/files/*rest') // [2, 0]
214
- * computeSpecificity('/a:b') // [2] — literal, not param — the classification fix
215
- * ```
216
- */
217
- export declare function computeSpecificity(path: string): readonly number[];
190
+ /**
191
+ * Computes a route path's SPECIFICITY VECTOR — the per-segment type ranking
192
+ * that breaks a tie when several registered routes match the same concrete
193
+ * pathname.
194
+ *
195
+ * @remarks
196
+ * Splits the CANONICALIZED path into segments (on `/`) and maps each to its
197
+ * specificity tier through {@link classifySegment} — the same segment parser
198
+ * {@link compilePath} uses, so a literal segment that merely contains a `:`
199
+ * (for example `a:b`) is correctly tiered as literal rather than param (the old
200
+ * engine's bug, fixed here). The standard route-precedence rule compares two
201
+ * matching routes' vectors LEFT-TO-RIGHT: at the first index where the tiers
202
+ * differ, the HIGHER tier (a literal over a param over a wildcard) is MORE
203
+ * SPECIFIC and wins — so `/users/me` (`[2, 2]`) beats `/users/:id` (`[2, 1]`)
204
+ * beats `/users/*rest` (`[2, 0]`) regardless of registration order. Two routes
205
+ * that match the SAME concrete pathname necessarily have the same segment
206
+ * count in the common case; {@link compareSpecificity} handles the general
207
+ * case for totality.
208
+ *
209
+ * @param path - The route path pattern (for example `/users/:id`)
210
+ * @returns The per-segment specificity tiers, in order
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * computeSpecificity('/users/me') // [2, 2]
215
+ * computeSpecificity('/users/:id') // [2, 1]
216
+ * computeSpecificity('/files/*rest') // [2, 0]
217
+ * computeSpecificity('/a:b') // [2] — literal, not param — the classification fix
218
+ * ```
219
+ */
220
+ export declare function computeSpecificity(path: string): readonly number[];
218
221
 
219
- /**
220
- * Create a {@link DispatcherInterface} — the fetch-standard, method-
221
- * dimensioned dispatch entity over one internal `Router<RouteRecord<TState>>`.
222
- *
223
- * @remarks
224
- * Prefer this over `new Dispatcher(...)` at call sites that only need the
225
- * interface.
226
- *
227
- * @typeParam TState - The consumer's opaque per-request state type (default
228
- * `undefined` for stateless use)
229
- * @param options - Optional initial `routes`, the `sensitive` case toggle,
230
- * the `unmatched`/`unmethoded` default-responder overrides, and the AGENTS
231
- * §13 emitter `on`/`error` wiring
232
- * @returns A {@link DispatcherInterface}
233
- *
234
- * @example
235
- * ```ts
236
- * import { createDispatcher } from '@src/core'
237
- *
238
- * const dispatcher = createDispatcher<{ readonly userId: string }>({
239
- * routes: [
240
- * { method: 'GET', path: '/health', handler: () => new Response('ok') },
241
- * ],
242
- * })
243
- * const response = await dispatcher.handle(new Request('http://x/health'), { userId: 'me' })
244
- * ```
245
- */
246
- export declare function createDispatcher<TState = undefined>(options?: DispatcherOptions<TState>): DispatcherInterface<TState>;
222
+ /**
223
+ * Creates a {@link DispatcherInterface} — the fetch-standard, method-
224
+ * dimensioned dispatch entity over one internal `Router<RouteRecord<TState>>`.
225
+ *
226
+ * @remarks
227
+ * Prefer this over `new Dispatcher(...)` at call sites that only need the
228
+ * interface.
229
+ *
230
+ * @typeParam TState - The consumer's opaque per-request state type (default
231
+ * `undefined` for stateless use)
232
+ * @param options - Optional initial `routes`, the `sensitive` case toggle,
233
+ * the `unmatched`/`unmethoded` default-responder overrides, and the
234
+ * Emitter pattern's `on`/`error` wiring
235
+ * @returns A {@link DispatcherInterface}
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * import { createDispatcher } from '@src/core'
240
+ *
241
+ * const dispatcher = createDispatcher<{ readonly userId: string }>({
242
+ * routes: [
243
+ * { method: 'GET', path: '/health', handler: () => new Response('ok') },
244
+ * ],
245
+ * })
246
+ * const response = await dispatcher.handle(new Request('http://x/health'), { userId: 'me' })
247
+ * ```
248
+ */
249
+ export declare function createDispatcher<TState = undefined>(options?: DispatcherOptions<TState>): DispatcherInterface<TState>;
247
250
 
248
- /**
249
- * Create a {@link RouterInterface} — the pure path-matching + registry engine
250
- * shared by the browser `Navigator` and the core `Dispatcher`.
251
- *
252
- * @remarks
253
- * Prefer this over `new Router(...)` at call sites that only need the
254
- * interface; an entity that OWNS a `Router` internally (like `Dispatcher`)
255
- * still constructs `new Router(...)` directly.
256
- *
257
- * @typeParam Meta - The opaque payload each entry carries and a match returns
258
- * @param options - Optional initial `entries`, the `sensitive` case toggle
259
- * (default `true`), and a `key` dedup identity function
260
- * @returns A {@link RouterInterface}
261
- *
262
- * @example
263
- * ```ts
264
- * import { createRouter } from '@src/core'
265
- *
266
- * const router = createRouter<{ readonly page: string }>()
267
- * router.add({ path: '/users/:id', meta: { page: 'profile' } })
268
- * router.match('/users/7') // { path: '/users/:id', params: { id: '7' }, meta: { page: 'profile' } }
269
- * ```
270
- */
271
- export declare function createRouter<Meta>(options?: RouterOptions<Meta>): RouterInterface<Meta>;
251
+ /**
252
+ * Creates a {@link RouterInterface} — the pure path-matching + registry engine
253
+ * shared by the browser `Navigator` and the core `Dispatcher`.
254
+ *
255
+ * @remarks
256
+ * Prefer this over `new Router(...)` at call sites that only need the
257
+ * interface; an entity that OWNS a `Router` internally (like `Dispatcher`)
258
+ * still constructs `new Router(...)` directly.
259
+ *
260
+ * @typeParam Meta - The opaque payload each entry carries and a match returns
261
+ * @param options - Optional initial `entries`, the `sensitive` case toggle
262
+ * (default `true`), and a `key` dedup identity function
263
+ * @returns A {@link RouterInterface}
264
+ *
265
+ * @example
266
+ * ```ts
267
+ * import { createRouter } from '@src/core'
268
+ *
269
+ * const router = createRouter<{ readonly page: string }>()
270
+ * router.add({ path: '/users/:id', meta: { page: 'profile' } })
271
+ * router.match('/users/7') // { path: '/users/:id', params: { id: '7' }, meta: { page: 'profile' } }
272
+ * ```
273
+ */
274
+ export declare function createRouter<Meta>(options?: RouterOptions<Meta>): RouterInterface<Meta>;
272
275
 
273
- /**
274
- * URL-decode one captured param value, tolerating a malformed percent-escape —
275
- * the decode {@link matchPath} applies to each captured group.
276
- *
277
- * @remarks
278
- * A bad `%` sequence is not a reason to reject an otherwise-matching route, so
279
- * a `decodeURIComponent` that would throw falls back to the raw value
280
- * (mirroring the cookie / token boundary readers, AGENTS §14). Total — never
281
- * throws.
282
- *
283
- * @param value - The raw captured param value
284
- * @returns The URL-decoded value, or the raw value when decoding would throw
285
- *
286
- * @example
287
- * ```ts
288
- * decodeParam('a%2Fb') // 'a/b'
289
- * decodeParam('100%25') // '100%'
290
- * decodeParam('%') // '%' — malformed escape stays literal
291
- * ```
292
- */
293
- export declare function decodeParam(value: string): string;
276
+ /**
277
+ * URL-decodes one captured param value, tolerating a malformed percent-escape —
278
+ * the decode {@link matchPath} applies to each captured group.
279
+ *
280
+ * @remarks
281
+ * A bad `%` sequence is not a reason to reject an otherwise-matching route, so
282
+ * a `decodeURIComponent` that would throw falls back to the raw value
283
+ * (mirroring the cookie / token boundary readers). Total — never
284
+ * throws.
285
+ *
286
+ * @param value - The raw captured param value
287
+ * @returns The URL-decoded value, or the raw value when decoding would throw
288
+ *
289
+ * @example
290
+ * ```ts
291
+ * decodeParam('a%2Fb') // 'a/b'
292
+ * decodeParam('100%25') // '100%'
293
+ * decodeParam('%') // '%' — malformed escape stays literal
294
+ * ```
295
+ */
296
+ export declare function decodeParam(value: string): string;
294
297
 
295
- /**
296
- * The fetch-standard, method-dimensioned dispatch entity layers HTTP method
297
- * dispatch and web-standard `Request`/`Response` handling over one internal
298
- * `Router<RouteRecord<TState>>`. The core machine the eventual server face
299
- * (§7) and any fetch-native runtime consumes directly.
300
- *
301
- * @typeParam TState - The consumer's opaque per-request state type
302
- *
303
- * @remarks
304
- * - **Dedup by `method + canonicalizePath`.** The underlying `Router` is
305
- * constructed with a `key` function so registering the same method+path
306
- * twice REPLACES the prior route in place (§5.1).
307
- * - **Registration boundary guard (§14).** `add` validates each input's
308
- * `handler` (`isFunction`) and `method` (must be in {@link METHODS})
309
- * throws `TypeError` on a malformed registration; path validation is
310
- * delegated to the underlying `Router`'s own guard. `match`/`handle` stay
311
- * guard-free.
312
- * - **Auto-`HEAD` / auto-`OPTIONS`.** A `HEAD` request with no registered
313
- * `HEAD` route runs the matching `GET` handler and strips the response
314
- * body; an `OPTIONS` request with no registered `OPTIONS` route answers
315
- * `204` with a derived `Allow` header.
316
- * - **Handler throws propagate.** `handle` never invents an error boundary —
317
- * a handler throw reaches the caller uncaught (§5.1).
318
- * - **Emitter (§13).** Owns a `#emitter` for {@link DispatcherEventMap};
319
- * `match`/`miss` fire AFTER resolution, before the handler/responder runs.
320
- *
321
- * @example
322
- * ```ts
323
- * const dispatcher = new Dispatcher<{ readonly userId: string }>()
324
- * dispatcher.add({
325
- * method: 'GET',
326
- * path: '/users/:id',
327
- * handler: (request, context) => Response.json({ id: context.params.id }),
328
- * })
329
- * const response = await dispatcher.handle(new Request('http://x/users/7'), { userId: 'me' })
330
- * ```
331
- */
332
- export declare class Dispatcher<TState = undefined> implements DispatcherInterface<TState> {
333
- #private;
334
- readonly router: RouterInterface<RouteRecord<TState>>;
335
- constructor(options?: DispatcherOptions<TState>);
336
- get emitter(): EmitterInterface<DispatcherEventMap>;
337
- add<Path extends string>(input: RouteInput<Path, TState>): void;
338
- add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
339
- group(prefix: string): DispatchGroupInterface<TState>;
340
- match(method: Method, pathname: string): DispatchResult<TState>;
341
- handle(request: Request, state: TState): Promise<Response>;
342
- destroy(): void;
343
- }
298
+ /**
299
+ * Provides an identity pass-through for a {@link RouteInput} that pins its `Path` generic
300
+ * to the LITERAL registration-site string, so `context.params` types
301
+ * correctly through {@link PathParams} without an explicit type argument.
302
+ *
303
+ * @remarks
304
+ * A bare object literal handed straight to {@link import('./types.js').DispatcherInterface}'s
305
+ * `add` already infers `Path` as a literal at that call site — but the moment
306
+ * the object is built through an intermediate binding (a local `const route =
307
+ * { method, path, handler }`) TypeScript widens `path` to `string` unless the
308
+ * binding's own type is pinned. Wrapping the literal in `defineRoute(...)`
309
+ * supplies that pin: its `const Path extends string` type parameter infers the
310
+ * NARROW literal from the call, and the function returns its input completely
311
+ * unchanged (same reference, no cloning, no validation) this is a
312
+ * compile-time typing aid only, not a construction step (contrast
313
+ * {@link import('./factories.js')} `create*` entity factories). A
314
+ * heterogeneous `RouteInput[]` built from several `defineRoute(...)` calls
315
+ * still widens each element's `Path` to `string` after collection into one array —
316
+ * the realistic ceiling this helper raises is PER-CALL typing at the
317
+ * registration site, not a stored, still-literal-typed record.
318
+ *
319
+ * @typeParam Path - The route path pattern literal (drives `context.params`
320
+ * through {@link PathParams})
321
+ * @typeParam TState - The consumer's opaque per-request state type
322
+ * @param input - The {@link RouteInput} to pass through unchanged
323
+ * @returns `input`, unchanged (same reference)
324
+ *
325
+ * @example
326
+ * ```ts
327
+ * const input = defineRoute({
328
+ * method: 'GET',
329
+ * path: '/users/:id',
330
+ * handler: (_request, context) => new Response(context.params.id), // typed string
331
+ * })
332
+ * dispatcher.add(input)
333
+ * ```
334
+ */
335
+ export declare function defineRoute<const Path extends string, TState = undefined>(input: RouteInput<Path, TState>): RouteInput<Path, TState>;
344
336
 
345
- /**
346
- * The `Dispatcher`'s event map (AGENTS §13)the two dispatch-outcome
347
- * signals a consumer can observe alongside the return value of `handle`.
348
- *
349
- * @remarks
350
- * - `match` — emitted on every dispatch that resolves to a handler
351
- * (including the auto-`HEAD`/auto-`OPTIONS` derived cases): the request
352
- * `method` and the winning `pattern`.
353
- * - `miss` — emitted on every non-matching dispatch: the RAW request
354
- * `method` (a plain `string`, not narrowed to {@link Method} — an unknown
355
- * verb like `PURGE` is observable here exactly as sent, never coerced),
356
- * the raw `pathname`, and which tier missed (`'unmatched'` — nothing
357
- * matched the path at all; `'unmethoded'` the path matched but not the
358
- * method, including an unknown verb against a path with other registered
359
- * methods).
360
- */
361
- export declare type DispatcherEventMap = {
362
- readonly match: readonly [method: Method, pattern: string];
363
- readonly miss: readonly [method: string, pathname: string, reason: 'unmatched' | 'unmethoded'];
364
- };
337
+ /**
338
+ * Represents the fetch-standard, method-dimensioned dispatch entitylayers HTTP method
339
+ * dispatch and web-standard `Request`/`Response` handling over one internal
340
+ * `Router<RouteRecord<TState>>`. The core machine the eventual server face
341
+ * and any fetch-native runtime consumes directly.
342
+ *
343
+ * @typeParam TState - The consumer's opaque per-request state type
344
+ *
345
+ * @remarks
346
+ * - **Dedup by `method + canonicalizePath`.** The underlying `Router` is
347
+ * constructed with a `key` function so registering the same method+path
348
+ * twice REPLACES the prior route in place.
349
+ * - **Registration boundary guard.** `add` validates each input's
350
+ * `handler` (`isFunction`) and `method` (must be in {@link METHODS})
351
+ * throws a `ContractError` on a malformed registration; path validation is
352
+ * delegated to the underlying `Router`'s own guard. `match`/`handle` stay
353
+ * guard-free.
354
+ * - **Auto-`HEAD` / auto-`OPTIONS`.** A `HEAD` request with no registered
355
+ * `HEAD` route runs the matching `GET` handler and strips the response
356
+ * body; an `OPTIONS` request with no registered `OPTIONS` route answers
357
+ * `204` with a derived `Allow` header.
358
+ * - **Handler throws propagate.** `handle` never invents an error boundary —
359
+ * a handler throw reaches the caller uncaught.
360
+ * - **Emitter.** Owns a `#emitter` for {@link DispatcherEventMap};
361
+ * `match`/`miss` fire AFTER resolution, before the handler/responder runs.
362
+ *
363
+ * @example
364
+ * ```ts
365
+ * const dispatcher = new Dispatcher<{ readonly userId: string }>()
366
+ * dispatcher.add({
367
+ * method: 'GET',
368
+ * path: '/users/:id',
369
+ * handler: (request, context) => Response.json({ id: context.params.id }),
370
+ * })
371
+ * const response = await dispatcher.handle(new Request('http://x/users/7'), { userId: 'me' })
372
+ * ```
373
+ */
374
+ export declare class Dispatcher<TState = undefined> implements DispatcherInterface<TState> {
375
+ #private;
376
+ constructor(options?: DispatcherOptions<TState>);
377
+ get router(): RouterInterface<RouteRecord<TState>>;
378
+ get emitter(): EmitterInterface<DispatcherEventMap>;
379
+ add<Path extends string>(input: RouteInput<Path, TState>): void;
380
+ add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
381
+ group(prefix: string): DispatchGroupInterface<TState>;
382
+ match(method: Method, pathname: string): DispatchResult<TState>;
383
+ handle(request: Request, state: TState): Promise<Response>;
384
+ destroy(): void;
385
+ }
365
386
 
366
- /**
367
- * The fetch-standard, method-dimensioned dispatch entity contract (the §4.5
368
- * behavioral-interface role for the one-class-per-file `Dispatcher`). Layers
369
- * HTTP method dispatch and web-standard `Request`/`Response` handling over a
370
- * single internal `Router<RouteRecord<TState>>`.
371
- *
372
- * @typeParam TState - The consumer's opaque per-request state type, threaded
373
- * into every {@link RouteContext} (default `undefined` for stateless use)
374
- *
375
- * @remarks
376
- * - `router` the underlying registry, exposed READONLY for introspection
377
- * (the same object `add`/`group`/`match` operate on).
378
- * - `emitter` the AGENTS §13 observable surface for {@link DispatcherEventMap}.
379
- * - `add(input)` / `add(inputs)` — register ONE / MANY {@link RouteInput}s
380
- * (§9.2 batch); throws `TypeError` on a malformed registration (a
381
- * non-`/`-prefixed path, a non-function handler, or a method outside
382
- * {@link import('./constants.js').METHODS}) — the construction/registration
383
- * boundary guard (§14); `match`/`handle` hot paths carry zero guards.
384
- * - `group(prefix)` a {@link DispatchGroupInterface} scoped under `prefix`.
385
- * - `match(method, pathname)` the raw {@link DispatchResult} for a method +
386
- * pathname pair, with no `Request`/`Response` involvement — the pure
387
- * decision `handle` builds its response from.
388
- * - `handle(request, state)` — the full dispatch: parses `request.url`,
389
- * calls `match`, and either invokes the winning handler (auto-stripping
390
- * the body for a derived `HEAD`, auto-answering a derived `OPTIONS` with
391
- * the `Allow` set), or invokes the `unmatched`/`unmethoded` responder.
392
- * Emits `match`/`miss` accordingly. A handler throw propagates uncaught.
393
- * - `destroy()` — tears down the `#emitter` (AGENTS §13); the underlying
394
- * router is left registered (not cleared) so introspection remains valid
395
- * after destroy.
396
- */
397
- export declare interface DispatcherInterface<TState = undefined> {
398
- readonly router: RouterInterface<RouteRecord<TState>>;
399
- readonly emitter: EmitterInterface<DispatcherEventMap>;
400
- add<Path extends string>(input: RouteInput<Path, TState>): void;
401
- add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
402
- group(prefix: string): DispatchGroupInterface<TState>;
403
- match(method: Method, pathname: string): DispatchResult<TState>;
404
- handle(request: Request, state: TState): Promise<Response>;
405
- destroy(): void;
406
- }
387
+ /**
388
+ * Represents the `Dispatcher`'s event map the dispatch-outcome
389
+ * signals a consumer can observe alongside the return value of `handle`.
390
+ *
391
+ * @remarks
392
+ * - `match` — emitted on every dispatch the dispatcher answers, the derived
393
+ * `HEAD` and `OPTIONS` cases included: the request `method` and the winning
394
+ * `pattern` (for a derived `OPTIONS` answer, the most-specific pattern the
395
+ * pathname resolved to).
396
+ * - `miss` — emitted on every non-matching dispatch: the RAW request
397
+ * `method` (a plain `string`, not narrowed to {@link Method} an unknown
398
+ * verb like `PURGE` is observable here exactly as sent, never coerced),
399
+ * the raw `pathname`, and which tier missed (`'unmatched'` nothing
400
+ * matched the path at all; `'unmethoded'` — the path matched but not the
401
+ * method, including an unknown verb against a path with other registered
402
+ * methods).
403
+ */
404
+ export declare type DispatcherEventMap = {
405
+ readonly match: readonly [method: Method, pattern: string];
406
+ readonly miss: readonly [method: string, pathname: string, status: 'unmatched' | 'unmethoded'];
407
+ };
407
408
 
408
- /**
409
- * Options for `createDispatcher` initial routes, case sensitivity, the two
410
- * default-responder overrides, and the AGENTS §13 emitter wiring.
411
- *
412
- * @typeParam TState - The consumer's opaque per-request state type
413
- *
414
- * @remarks
415
- * - `routes` the initial route inputs to register, equivalent to a bare
416
- * `createDispatcher()` followed by `add(routes)`. Omitted ⇒ no routes.
417
- * - `sensitive` — forwarded to the underlying `Router` (default `true`, §4).
418
- * - `unmatched` — the responder invoked when nothing matches the pathname at
419
- * all (default: a `404` `Response`).
420
- * - `unmethoded` — the responder invoked when the pathname matches but not
421
- * the method, given the derived `Allow` set (default: a `405` `Response`
422
- * with an `Allow` header).
423
- * - `on` initial `DispatcherEventMap` listeners (AGENTS §8/§13).
424
- * - `error` — the emitter's own listener-error handler (AGENTS §13),
425
- * forwarded alongside `on`.
426
- */
427
- export declare interface DispatcherOptions<TState> {
428
- readonly routes?: ReadonlyArray<RouteInput<string, TState>>;
429
- readonly sensitive?: boolean;
430
- readonly unmatched?: (request: Request) => Response | Promise<Response>;
431
- readonly unmethoded?: (request: Request, allow: readonly Method[]) => Response | Promise<Response>;
432
- readonly on?: EmitterHooks<DispatcherEventMap>;
433
- readonly error?: EmitterErrorHandler;
434
- }
409
+ /**
410
+ * Represents the fetch-standard, method-dimensioned dispatch entity contract (the
411
+ * behavioral-interface role for the one-class-per-file `Dispatcher`). Layers
412
+ * HTTP method dispatch and web-standard `Request`/`Response` handling over a
413
+ * single internal `Router<RouteRecord<TState>>`.
414
+ *
415
+ * @typeParam TState - The consumer's opaque per-request state type, threaded
416
+ * into every {@link RouteContext} (default `undefined` for stateless use)
417
+ *
418
+ * @remarks
419
+ * - `router` — the underlying registry, exposed READONLY for introspection
420
+ * (the same object `add`/`group`/`match` operate on).
421
+ * - `emitter` — the observable surface for {@link DispatcherEventMap}.
422
+ * - `add(input)` / `add(inputs)` register ONE / MANY {@link RouteInput}s
423
+ * (batch registration); throws a `ContractError` on a malformed registration (a
424
+ * non-`/`-prefixed path, a non-function handler, or a method outside
425
+ * {@link import('./constants.js').METHODS}) — the construction/registration
426
+ * boundary guard; `match`/`handle` hot paths carry zero guards.
427
+ * - `group(prefix)` — a {@link DispatchGroupInterface} scoped under `prefix`.
428
+ * - `match(method, pathname)` — the raw {@link DispatchResult} for a method +
429
+ * pathname pair, with no `Request`/`Response` involvement — the pure
430
+ * decision `handle` builds its response from.
431
+ * - `handle(request, state)` the full dispatch: parses `request.url`,
432
+ * calls `match`, and either invokes the winning handler (auto-stripping
433
+ * the body for a derived `HEAD`, auto-answering a derived `OPTIONS` with
434
+ * the `Allow` set), or invokes the `unmatched`/`unmethoded` responder.
435
+ * Emits `match`/`miss` accordingly. A handler throw propagates uncaught.
436
+ * - `destroy()` — tears down the `#emitter`; the underlying
437
+ * router is left registered (not cleared) so introspection remains valid
438
+ * after destroy.
439
+ */
440
+ export declare interface DispatcherInterface<TState = undefined> {
441
+ readonly router: RouterInterface<RouteRecord<TState>>;
442
+ readonly emitter: EmitterInterface<DispatcherEventMap>;
443
+ add<Path extends string>(input: RouteInput<Path, TState>): void;
444
+ add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
445
+ group(prefix: string): DispatchGroupInterface<TState>;
446
+ match(method: Method, pathname: string): DispatchResult<TState>;
447
+ handle(request: Request, state: TState): Promise<Response>;
448
+ destroy(): void;
449
+ }
435
450
 
436
- /**
437
- * A prefix-scoped registration handle over a
438
- * {@link import('./Dispatcher.js').Dispatcher} the method-dimensioned
439
- * counterpart of `Group` (`Group.ts`).
440
- *
441
- * @typeParam TState - The consumer's opaque per-request state type, matching
442
- * the owning dispatcher
443
- *
444
- * @remarks
445
- * Every `add` composes `input.path` via {@link joinPaths} against
446
- * `this.prefix` and forwards to the OWNING dispatcher's `add` (its own §14
447
- * boundary guard still applies). Pure string composition (§4.2.2) — no
448
- * independent state or storage.
449
- *
450
- * @example
451
- * ```ts
452
- * import { Dispatcher } from '@src/core'
453
- *
454
- * const dispatcher = new Dispatcher()
455
- * const api = dispatcher.group('/api')
456
- * api.add({ method: 'GET', path: '/users', handler: () => new Response('ok') })
457
- * ```
458
- */
459
- export declare class DispatchGroup<TState> implements DispatchGroupInterface<TState> {
460
- #private;
461
- readonly prefix: string;
462
- constructor(parent: DispatcherInterface<TState>, prefix: string);
463
- add<Path extends string>(input: RouteInput<Path, TState>): void;
464
- add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
465
- group(prefix: string): DispatchGroupInterface<TState>;
466
- }
451
+ /**
452
+ * Represents the options for `createDispatcher` — initial routes, case sensitivity, the
453
+ * default-responder overrides, and the Emitter pattern's wiring.
454
+ *
455
+ * @typeParam TState - The consumer's opaque per-request state type
456
+ *
457
+ * @remarks
458
+ * - `routes` — the initial route inputs to register, equivalent to a bare
459
+ * `createDispatcher()` followed by `add(routes)`. Omitted ⇒ no routes.
460
+ * - `sensitive` forwarded to the underlying `Router` (default `true`).
461
+ * - `unmatched` the responder invoked when nothing matches the pathname at
462
+ * all (default: a `404` `Response`).
463
+ * - `unmethoded` the responder invoked when the pathname matches but not
464
+ * the method, given the derived `Allow` set (default: a `405` `Response`
465
+ * with an `Allow` header).
466
+ * - `on` — initial `DispatcherEventMap` listeners (the Emitter pattern's
467
+ * similar-surface pin).
468
+ * - `error` — the emitter's own listener-error handler, forwarded alongside
469
+ * `on`.
470
+ */
471
+ export declare interface DispatcherOptions<TState> {
472
+ readonly routes?: ReadonlyArray<RouteInput<string, TState>>;
473
+ readonly sensitive?: boolean;
474
+ readonly unmatched?: (request: Request) => Response | Promise<Response>;
475
+ readonly unmethoded?: (request: Request, allow: readonly Method[]) => Response | Promise<Response>;
476
+ readonly on?: EmitterHooks<DispatcherEventMap>;
477
+ readonly error?: EmitterErrorHandler;
478
+ }
467
479
 
468
- /**
469
- * A prefix-scoped registration handle over a {@link DispatcherInterface} —
470
- * the method-dimensioned counterpart of {@link GroupInterface}.
471
- *
472
- * @typeParam TState - The consumer's opaque per-request state type, matching
473
- * the owning dispatcher
474
- *
475
- * @remarks
476
- * - `prefix` — the path prefix this group prepends to every route it
477
- * registers (and to every nested group's own prefix).
478
- * - `add(input)` / `add(inputs)` register ONE / MANY {@link RouteInput}s on
479
- * the OWNING dispatcher, each input's `path` composed as
480
- * `prefix + input.path` (§9.2 batch, mirroring
481
- * {@link DispatcherInterface.add}).
482
- * - `group(prefix)` — a nested group whose prefix is `this.prefix + prefix`.
483
- */
484
- export declare interface DispatchGroupInterface<TState> {
485
- readonly prefix: string;
486
- add<Path extends string>(input: RouteInput<Path, TState>): void;
487
- add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
488
- group(prefix: string): DispatchGroupInterface<TState>;
489
- }
480
+ /**
481
+ * Represents a prefix-scoped registration handle over a
482
+ * {@link import('./Dispatcher.js').Dispatcher} — the method-dimensioned
483
+ * counterpart of `Group` (`Group.ts`).
484
+ *
485
+ * @typeParam TState - The consumer's opaque per-request state type, matching
486
+ * the owning dispatcher
487
+ *
488
+ * @remarks
489
+ * Every `add` composes `input.path` through {@link joinPaths} against
490
+ * `this.prefix` and forwards to the OWNING dispatcher's `add` (its own
491
+ * registration boundary guard still applies). Pure string composition — no
492
+ * independent state or storage.
493
+ *
494
+ * @example
495
+ * ```ts
496
+ * import { Dispatcher } from '@src/core'
497
+ *
498
+ * const dispatcher = new Dispatcher()
499
+ * const api = dispatcher.group('/api')
500
+ * api.add({ method: 'GET', path: '/users', handler: () => new Response('ok') })
501
+ * ```
502
+ */
503
+ export declare class DispatchGroup<TState> implements DispatchGroupInterface<TState> {
504
+ #private;
505
+ readonly prefix: string;
506
+ constructor(parent: DispatcherInterface<TState>, prefix: string);
507
+ add<Path extends string>(input: RouteInput<Path, TState>): void;
508
+ add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
509
+ group(prefix: string): DispatchGroupInterface<TState>;
510
+ }
490
511
 
491
- /**
492
- * The outcome of {@link DispatcherInterface.match} — a discriminated union
493
- * over the three dispatch tiers: a full hit, a path-matches-but-method-
494
- * doesn't (405 territory), or nothing matched at all (404 territory).
495
- *
496
- * @typeParam TState - The consumer's opaque per-request state type
497
- *
498
- * @remarks
499
- * - `'matched'` — carries the winning {@link RouterMatch} (its `meta` is a
500
- * {@link RouteRecord}).
501
- * - `'unmethoded'` the pathname matched at least one entry but none for
502
- * the requested method; `allow` is the derived `Allow` method set (from
503
- * `router.entries(pathname)`).
504
- * - `'unmatched'` — no registered pattern matches the pathname at all.
505
- */
506
- export declare type DispatchResult<TState> = {
507
- readonly status: 'matched';
508
- readonly match: RouterMatch<RouteRecord<TState>>;
509
- } | {
510
- readonly status: 'unmethoded';
511
- readonly allow: readonly Method[];
512
- } | {
513
- readonly status: 'unmatched';
514
- };
512
+ /**
513
+ * Represents a prefix-scoped registration handle over a {@link DispatcherInterface} —
514
+ * the method-dimensioned counterpart of {@link GroupInterface}.
515
+ *
516
+ * @typeParam TState - The consumer's opaque per-request state type, matching
517
+ * the owning dispatcher
518
+ *
519
+ * @remarks
520
+ * - `prefix` — the path prefix this group prepends to every route it
521
+ * registers (and to every nested group's own prefix).
522
+ * - `add(input)` / `add(inputs)` register ONE / MANY {@link RouteInput}s on
523
+ * the OWNING dispatcher, each input's `path` composed as
524
+ * `prefix + input.path` (batch registration, mirroring
525
+ * {@link DispatcherInterface.add}).
526
+ * - `group(prefix)` — a nested group whose prefix is `this.prefix + prefix`.
527
+ */
528
+ export declare interface DispatchGroupInterface<TState> {
529
+ readonly prefix: string;
530
+ add<Path extends string>(input: RouteInput<Path, TState>): void;
531
+ add(inputs: ReadonlyArray<RouteInput<string, TState>>): void;
532
+ group(prefix: string): DispatchGroupInterface<TState>;
533
+ }
515
534
 
516
- /**
517
- * Escape every regex metacharacter in a literal string so it can be embedded
518
- * inside a larger `RegExp` source without being interpreted as syntax.
519
- *
520
- * @remarks
521
- * {@link compilePath} escapes the literal segments of a route pattern with this
522
- * before splicing in `:name` / `*name` capture groups, so a path like
523
- * `/files/:name.json` matches the `.` literally rather than as "any character".
524
- * Pure and total never throws.
525
- *
526
- * @param value - The literal string to escape
527
- * @returns `value` with every regex metacharacter backslash-escaped
528
- *
529
- * @example
530
- * ```ts
531
- * escapeRegExp('a.b+c') // 'a\\.b\\+c'
532
- * new RegExp(`^${escapeRegExp('a.b')}$`).test('a.b') // true
533
- * new RegExp(`^${escapeRegExp('a.b')}$`).test('axb') // false
534
- * ```
535
- */
536
- export declare function escapeRegExp(value: string): string;
535
+ /**
536
+ * Represents the outcome of {@link DispatcherInterface.match} a discriminated union
537
+ * over the dispatch tiers: a full hit, a path-matches-but-method-
538
+ * doesn't (405 territory), or nothing matched at all (404 territory).
539
+ *
540
+ * @typeParam TState - The consumer's opaque per-request state type
541
+ *
542
+ * @remarks
543
+ * - `'matched'`carries the winning {@link RouterMatch} (its `meta` is a
544
+ * {@link RouteRecord}).
545
+ * - `'unmethoded'` the pathname matched at least one entry but none for
546
+ * the requested method; `allow` is the derived `Allow` method set (from
547
+ * `router.entries(pathname)`).
548
+ * - `'unmatched'` — no registered pattern matches the pathname at all.
549
+ */
550
+ export declare type DispatchResult<TState> = {
551
+ readonly status: 'matched';
552
+ readonly match: RouterMatch<RouteRecord<TState>>;
553
+ } | {
554
+ readonly status: 'unmethoded';
555
+ readonly allow: readonly Method[];
556
+ } | {
557
+ readonly status: 'unmatched';
558
+ };
537
559
 
538
- /**
539
- * A prefix-scoped registration handle over a {@link import('./Router.js').Router}
540
- * pure string composition (AGENTS §4.2.2), no independent state or storage.
541
- *
542
- * @typeParam Meta - The entry payload type, matching the owning router
543
- *
544
- * @remarks
545
- * Every `add` composes `entry.path` as `joinPaths(prefix, entry.path)` and
546
- * forwards to the OWNING router, so grouped routes land in the SAME registry.
547
- * `group(prefix)` nests, composing prefixes via {@link joinPaths}.
548
- *
549
- * @example
550
- * ```ts
551
- * import { Router } from '@src/core'
552
- *
553
- * const router = new Router<{ readonly page: string }>()
554
- * const api = router.group('/api')
555
- * api.add({ path: '/users', meta: { page: 'list' } })
556
- * router.match('/api/users')?.path // '/api/users'
557
- * ```
558
- */
559
- export declare class Group<Meta> implements GroupInterface<Meta> {
560
- #private;
561
- readonly prefix: string;
562
- constructor(parent: RouterInterface<Meta>, prefix: string);
563
- add(entry: RouteEntry<Meta>): void;
564
- add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
565
- group(prefix: string): GroupInterface<Meta>;
566
- }
560
+ /**
561
+ * Escapes every regex metacharacter in a literal string so it can be embedded
562
+ * inside a larger `RegExp` source without being interpreted as syntax.
563
+ *
564
+ * @remarks
565
+ * {@link compilePath} escapes the literal segments of a route pattern with this
566
+ * before splicing in `:name` / `*name` capture groups, so a path like
567
+ * `/files/:name.json` matches the `.` literally rather than as "any character".
568
+ * Pure and total never throws.
569
+ *
570
+ * @param value - The literal string to escape
571
+ * @returns `value` with every regex metacharacter backslash-escaped
572
+ *
573
+ * @example
574
+ * ```ts
575
+ * escapeRegExp('a.b+c') // 'a\\.b\\+c'
576
+ * new RegExp(`^${escapeRegExp('a.b')}$`).test('a.b') // true
577
+ * new RegExp(`^${escapeRegExp('a.b')}$`).test('axb') // false
578
+ * ```
579
+ */
580
+ export declare function escapeRegExp(value: string): string;
567
581
 
568
- /**
569
- * A prefix-scoped registration handle over a {@link RouterInterface} — pure
570
- * string composition (§4.2.2), no independent state or storage.
571
- *
572
- * @typeParam Meta - The entry payload type, matching the owning router
573
- *
574
- * @remarks
575
- * - `prefix` the path prefix this group prepends to every entry it
576
- * registers (and to every nested group's own prefix).
577
- * - `add(entry)` / `add(entries)` register ONE / MANY entries on the
578
- * OWNING router, each entry's `path` composed as `prefix + entry.path`
579
- * (§9.2 batch, mirroring {@link RouterInterface.add}).
580
- * - `group(prefix)` — a nested group whose prefix is `this.prefix + prefix`;
581
- * nesting composes prefixes left to right with no depth limit.
582
- */
583
- export declare interface GroupInterface<Meta> {
584
- readonly prefix: string;
585
- add(entry: RouteEntry<Meta>): void;
586
- add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
587
- group(prefix: string): GroupInterface<Meta>;
588
- }
582
+ /**
583
+ * Represents a prefix-scoped registration handle over a {@link import('./Router.js').Router} —
584
+ * pure string composition, no independent state or storage.
585
+ *
586
+ * @typeParam Meta - The entry payload type, matching the owning router
587
+ *
588
+ * @remarks
589
+ * Every `add` composes `entry.path` as `joinPaths(prefix, entry.path)` and
590
+ * forwards to the OWNING router, so grouped routes land in the SAME registry.
591
+ * `group(prefix)` nests, composing prefixes through {@link joinPaths}.
592
+ *
593
+ * @example
594
+ * ```ts
595
+ * import { Router } from '@src/core'
596
+ *
597
+ * const router = new Router<{ readonly page: string }>()
598
+ * const api = router.group('/api')
599
+ * api.add({ path: '/users', meta: { page: 'list' } })
600
+ * router.match('/api/users')?.path // '/api/users'
601
+ * ```
602
+ */
603
+ export declare class Group<Meta> implements GroupInterface<Meta> {
604
+ #private;
605
+ readonly prefix: string;
606
+ constructor(parent: RouterInterface<Meta>, prefix: string);
607
+ add(entry: RouteEntry<Meta>): void;
608
+ add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
609
+ group(prefix: string): GroupInterface<Meta>;
610
+ }
589
611
 
590
- /**
591
- * The identifier CONTINUATION characters after the firstmirrors the
592
- * runtime classifier's `[A-Za-z0-9_]*` tail class.
593
- */
594
- export declare type IdentifierChar = IdentifierStartChar | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
612
+ /**
613
+ * Represents a prefix-scoped registration handle over a {@link RouterInterface} pure
614
+ * string composition, no independent state or storage.
615
+ *
616
+ * @typeParam Meta - The entry payload type, matching the owning router
617
+ *
618
+ * @remarks
619
+ * - `prefix` — the path prefix this group prepends to every entry it
620
+ * registers (and to every nested group's own prefix).
621
+ * - `add(entry)` / `add(entries)` — register ONE / MANY entries on the
622
+ * OWNING router, each entry's `path` composed as `prefix + entry.path`
623
+ * (batch registration, mirroring {@link RouterInterface.add}).
624
+ * - `group(prefix)` — a nested group whose prefix is `this.prefix + prefix`;
625
+ * nesting composes prefixes left to right with no depth limit.
626
+ */
627
+ export declare interface GroupInterface<Meta> {
628
+ readonly prefix: string;
629
+ add(entry: RouteEntry<Meta>): void;
630
+ add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
631
+ group(prefix: string): GroupInterface<Meta>;
632
+ }
595
633
 
596
- export declare type IdentifierHead<S extends string> = S extends `${infer Head}${infer Tail}` ? Head extends IdentifierStartChar ? TakeIdentifierTail<Tail, Head> : '' : '';
634
+ /**
635
+ * Names the identifier CONTINUATION characters after the first — mirrors the
636
+ * runtime classifier's `[A-Za-z0-9_]*` tail class.
637
+ */
638
+ export declare type IdentifierChar = IdentifierStartChar | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
597
639
 
598
- /**
599
- * The identifier START characters an identifier-grammar param name may
600
- * begin with — mirrors the runtime classifier's `[A-Za-z_]` head class
601
- * (`classifySegment` / `compilePath`, `helpers.ts`).
602
- */
603
- export declare type IdentifierStartChar = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | '_';
640
+ export declare type IdentifierHead<S extends string> = S extends `${infer Head}${infer Tail}` ? Head extends IdentifierStartChar ? TakeIdentifierTail<Tail, Head> : '' : '';
604
641
 
605
- /**
606
- * Join a group prefix and a route path into one `/`-prefixed path, normalizing
607
- * duplicate or missing joining slashes.
608
- *
609
- * @remarks
610
- * {@link import('./types.js').GroupInterface} / {@link import('./types.js').DispatchGroupInterface}
611
- * compose a prefix with each registered entry's path this way — pure string
612
- * composition (§4.2.2), no independent state. Both a duplicated slash
613
- * (`'/api/'` + `'/users'`) and a missing one (`'/api'` + `'users'`) normalize
614
- * to a single joining slash. An empty `prefix` returns `path` unchanged (after
615
- * ensuring a leading slash); an empty `path` returns `prefix` unchanged.
616
- * Pure and total.
617
- *
618
- * @param prefix - The group prefix (e.g. `/api`)
619
- * @param path - The route path being joined under the prefix (e.g. `/users`)
620
- * @returns The joined `/`-prefixed path
621
- *
622
- * @example
623
- * ```ts
624
- * joinPaths('/api', '/users') // '/api/users'
625
- * joinPaths('/api/', '/users') // '/api/users'
626
- * joinPaths('/api', 'users') // '/api/users'
627
- * joinPaths('', '/users') // '/users'
628
- * joinPaths('/api', '') // '/api'
629
- * ```
630
- */
631
- export declare function joinPaths(prefix: string, path: string): string;
642
+ /**
643
+ * Names the identifier START characters an identifier-grammar param name may
644
+ * begin with mirrors the runtime classifier's `[A-Za-z_]` head class
645
+ * (`classifySegment` / `compilePath`, `helpers.ts`).
646
+ */
647
+ export declare type IdentifierStartChar = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | '_';
632
648
 
633
- /**
634
- * Extract the URL-decoded params a compiled path captures from a concrete
635
- * pathname, or `undefined` when the pathname does not match.
636
- *
637
- * @remarks
638
- * Runs the {@link CompiledPath} `regex` against `pathname` (a single `exec`); a
639
- * miss returns `undefined`. On a hit it walks `params` POSITIONALLYthe
640
- * `n`-th param name pairs with the `n`-th capture group — and URL-decodes each
641
- * value with {@link decodeParam}. Returns a frozen `name value` record (empty
642
- * for a parameterless path). Total never throws.
643
- *
644
- * @param compiled - The {@link CompiledPath} from {@link compilePath}
645
- * @param pathname - The concrete request pathname to match (e.g. `/users/7`)
646
- * @returns The decoded params on a hit, or `undefined` on a miss
647
- *
648
- * @example
649
- * ```ts
650
- * const compiled = compilePath('/users/:id')
651
- * matchPath(compiled, '/users/7') // { id: '7' }
652
- * matchPath(compiled, '/users/a%2Fb') // { id: 'a/b' } — decoded
653
- * matchPath(compiled, '/posts/7') // undefined
654
- * ```
655
- */
656
- export declare function matchPath(compiled: CompiledPath, pathname: string): Readonly<Record<string, string>> | undefined;
649
+ /**
650
+ * Joins a group prefix and a route path into one `/`-prefixed path, normalizing
651
+ * duplicate or missing joining slashes.
652
+ *
653
+ * @remarks
654
+ * {@link import('./types.js').GroupInterface} / {@link import('./types.js').DispatchGroupInterface}
655
+ * compose a prefix with each registered entry's path this waypure string
656
+ * composition, no independent state. Both a duplicated slash
657
+ * (`'/api/'` + `'/users'`) and a missing one (`'/api'` + `'users'`) normalize
658
+ * to a single joining slash. An empty `prefix` returns `path` unchanged (after
659
+ * ensuring a leading slash); an empty `path` returns `prefix` unchanged.
660
+ * Pure and total.
661
+ *
662
+ * @param prefix - The group prefix (for example `/api`)
663
+ * @param path - The route path being joined under the prefix (for example `/users`)
664
+ * @returns The joined `/`-prefixed path
665
+ *
666
+ * @example
667
+ * ```ts
668
+ * joinPaths('/api', '/users') // '/api/users'
669
+ * joinPaths('/api/', '/users') // '/api/users'
670
+ * joinPaths('/api', 'users') // '/api/users'
671
+ * joinPaths('', '/users') // '/users'
672
+ * joinPaths('/api', '') // '/api'
673
+ * ```
674
+ */
675
+ export declare function joinPaths(prefix: string, path: string): string;
657
676
 
658
- /**
659
- * The seven HTTP methods a {@link DispatcherInterface} dimensions dispatch
660
- * over the value-level counterpart is {@link import('./constants.js').METHODS}.
661
- *
662
- * @remarks
663
- * `HEAD` is a valid explicit registration even though a `GET` route already
664
- * auto-answers `HEAD` (§5.1 dispatch semantics) an explicit `HEAD` handler
665
- * always takes precedence over the derived one.
666
- */
667
- export declare type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
677
+ /**
678
+ * Extracts the URL-decoded params a compiled path captures from a concrete
679
+ * pathname, or `undefined` when the pathname does not match.
680
+ *
681
+ * @remarks
682
+ * Runs the {@link CompiledPath} `regex` against `pathname` (a single `exec`); a
683
+ * miss returns `undefined`. On a hit it walks `params` POSITIONALLY — the
684
+ * `n`-th param name pairs with the `n`-th capture group — and URL-decodes each
685
+ * value with {@link decodeParam}. Returns a frozen `name → value` record (empty
686
+ * for a parameterless path). Total never throws.
687
+ *
688
+ * @param compiled - The {@link CompiledPath} from {@link compilePath}
689
+ * @param pathname - The concrete request pathname to match (for example `/users/7`)
690
+ * @returns The decoded params on a hit, or `undefined` on a miss
691
+ *
692
+ * @example
693
+ * ```ts
694
+ * const compiled = compilePath('/users/:id')
695
+ * matchPath(compiled, '/users/7') // { id: '7' }
696
+ * matchPath(compiled, '/users/a%2Fb') // { id: 'a/b' } — decoded
697
+ * matchPath(compiled, '/posts/7') // undefined
698
+ * ```
699
+ */
700
+ export declare function matchPath(compiled: CompiledPath, pathname: string): Readonly<Record<string, string>> | undefined;
668
701
 
669
- /**
670
- * The complete set of HTTP methods a {@link import('./types.js').Dispatcher}
671
- * registers routes under backs the registration guard (`add` rejects any
672
- * `method` outside this set) and the auto-`OPTIONS` `Allow` derivation.
673
- *
674
- * @remarks
675
- * A `ReadonlySet` of the seven {@link import('./types.js').Method} literals:
676
- * `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. `HEAD` is
677
- * included even though it is never required at registration (a `GET` route
678
- * auto-answers `HEAD`) — it is still a valid method to register explicitly.
679
- *
680
- * @example
681
- * ```ts
682
- * METHODS.has('GET') // true
683
- * METHODS.has('TRACE') // false
684
- * ```
685
- */
686
- export declare const METHODS: ReadonlySet<string>;
702
+ /**
703
+ * Names the HTTP methods a {@link DispatcherInterface} dimensions dispatch
704
+ * overderived from {@link import('./constants.js').METHOD_LIST}, whose
705
+ * membership counterpart is {@link import('./constants.js').METHODS}.
706
+ *
707
+ * @remarks
708
+ * Resolves to `'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' |
709
+ * 'OPTIONS'`. Deriving the union from the constant keeps one home for the
710
+ * method set: a verb added there widens this type, the membership set, and the
711
+ * `parseMethod` narrowing together. `HEAD` is a valid explicit registration
712
+ * even though a `GET` route already auto-answers `HEAD` — an explicit
713
+ * `HEAD` handler always takes precedence over the derived one.
714
+ */
715
+ export declare type Method = (typeof METHOD_LIST)[number];
687
716
 
688
- /**
689
- * Narrow a raw `request.method` string into a typed {@link Method} — total,
690
- * never throws.
691
- *
692
- * @remarks
693
- * Guarded via {@link import('./constants.js').METHODS} (the seven registrable
694
- * HTTP methods); any other value (an unknown verb, non-uppercase casing)
695
- * resolves to `undefined` rather than throwing (§14 guard totality). Pure
696
- * leaf shared by the `Dispatcher`'s `handle` (§5.1 unknown-verb honesty) and
697
- * anywhere else a raw method string needs narrowing.
698
- *
699
- * @param value - The raw `request.method` string to narrow
700
- * @returns The matching {@link Method}, or `undefined` when `value` is not one
701
- * of the seven registrable methods
702
- *
703
- * @example
704
- * ```ts
705
- * parseMethod('GET') // 'GET'
706
- * parseMethod('PURGE') // undefined
707
- * parseMethod('get') // undefined case-sensitive
708
- * ```
709
- */
710
- export declare function parseMethod(value: string): Method | undefined;
717
+ /**
718
+ * Lists the HTTP methods a {@link import('./types.js').DispatcherInterface}
719
+ * registers routes under, in canonical order — the single source the
720
+ * {@link import('./types.js').Method} type, {@link METHODS}, and
721
+ * `parseMethod` are all derived from.
722
+ *
723
+ * @remarks
724
+ * A frozen tuple of the verbs: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`,
725
+ * `HEAD`, `OPTIONS`. Adding a verb here widens the `Method` type, the
726
+ * {@link METHODS} membership set, and the `parseMethod` narrowing together, so
727
+ * the method set cannot drift between them. Prefer {@link METHODS} for a
728
+ * membership test; use this tuple where order or literal typing matters.
729
+ *
730
+ * @example
731
+ * ```ts
732
+ * METHOD_LIST[0] // 'GET'
733
+ * METHOD_LIST.includes('GET') // true
734
+ * ```
735
+ */
736
+ export declare const METHOD_LIST: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
711
737
 
712
- /**
713
- * Extracts `{ name: string }` param records from a path pattern at the type
714
- * level the typed half of the path grammar (§4).
715
- *
716
- * @typeParam Path - A route path pattern literal (`/users/:id/posts/:slug`,
717
- * `/files/*rest`, or a parameterless literal path)
718
- *
719
- * @remarks
720
- * A `:name` segment contributes `{ name: string }`; a trailing `*name`
721
- * wildcard (the grammar's only allowed wildcard position) contributes
722
- * `{ name: string }` capturing the rest of the path; a parameterless pattern
723
- * resolves to an empty record. Built over {@link PathParamsRaw} and flattened
724
- * through an identity-mapped type so editor hovers show the resolved shape
725
- * (`{ id: string; slug: string }`) rather than an unresolved intersection.
726
- *
727
- * @example
728
- * ```ts
729
- * type A = PathParams<'/users/:id/posts/:slug'> // { readonly id: string; readonly slug: string }
730
- * type B = PathParams<'/files/*rest'> // { readonly rest: string }
731
- * type C = PathParams<'/health'> // Record<string, never>
732
- * ```
733
- */
734
- export declare type PathParams<Path extends string> = {
735
- readonly [K in keyof PathParamsRaw<Path>]: PathParamsRaw<Path>[K];
736
- };
738
+ /**
739
+ * Holds the complete set of HTTP methods a
740
+ * {@link import('./types.js').DispatcherInterface} registers routes under
741
+ * backs the registration guard (`add` rejects any `method` outside this set)
742
+ * and the auto-`OPTIONS` `Allow` derivation.
743
+ *
744
+ * @remarks
745
+ * A `ReadonlySet` built from {@link METHOD_LIST}, so it carries exactly the
746
+ * {@link import('./types.js').Method} literals: `GET`, `POST`, `PUT`,
747
+ * `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. `HEAD` is included even though it is
748
+ * never required at registration (a `GET` route auto-answers `HEAD`) it is
749
+ * still a valid method to register explicitly. The element type stays `string`
750
+ * so a raw, unnarrowed `request.method` can be tested directly.
751
+ *
752
+ * @example
753
+ * ```ts
754
+ * METHODS.has('GET') // true
755
+ * METHODS.has('TRACE') // false
756
+ * ```
757
+ */
758
+ export declare const METHODS: ReadonlySet<string>;
737
759
 
738
- /**
739
- * Recursive, unflattened param extraction for {@link PathParams} — walks a
740
- * path pattern segment by segment (split on `/`), extracting each segment's
741
- * {@link SegmentParam} contribution and intersecting the rest.
742
- *
743
- * @typeParam Path - The path pattern literal being decomposed
744
- *
745
- * @remarks
746
- * Splits `Path` at its first `/` into `Segment` + `Rest`, intersects
747
- * `SegmentParam<Segment>` with the recursive walk of `Rest`, and once no
748
- * further `/` remains resolves the final segment's own `SegmentParam`
749
- * directly. `SegmentParam` mirrors the runtime `classifySegment` grammar
750
- * exactly: a `:name` HEAD (identifier-char run, stopping at the first
751
- * non-identifier char) captures a param; a segment whose `:` is not at the
752
- * segment START (`a:b`) contributes nothing (the classification fix, §4); a
753
- * final `*name` wildcard captures the rest-of-path param the same way. A
754
- * fully parameterless `Path` resolves to `unknown` here (the intersection
755
- * identity every non-capturing segment contributes) — {@link PathParams}
756
- * flattens that down to a clean empty record. This type is exported so
757
- * {@link PathParams} (which flattens it into a clean mapped type for IDE
758
- * hovers) has a documented, testable recursive step; consumers reach for the
759
- * flattened {@link PathParams} form, never this raw recursion.
760
- */
761
- export declare type PathParamsRaw<Path extends string> = string extends Path ? Readonly<Record<string, string>> : Path extends `${infer Segment}/${infer Rest}` ? SegmentParam<Segment> & PathParamsRaw<Rest> : SegmentParam<Path>;
760
+ /**
761
+ * Narrows a raw `request.method` string into a typed {@link Method} — total,
762
+ * never throws.
763
+ *
764
+ * @remarks
765
+ * Consults {@link import('./constants.js').METHOD_LIST} (the one home for the
766
+ * registrable HTTP methods), so a verb added there narrows here without
767
+ * a second list to update; any other value (an unknown verb, non-uppercase
768
+ * casing) resolves to `undefined` rather than throwing (total guard behavior).
769
+ * Pure leaf shared by the `Dispatcher`'s `handle` (honest about an unknown verb)
770
+ * and anywhere else a raw method string needs narrowing.
771
+ *
772
+ * @param value - The raw `request.method` string to narrow
773
+ * @returns The matching {@link Method}, or `undefined` when `value` is not a
774
+ * registrable method
775
+ *
776
+ * @example
777
+ * ```ts
778
+ * parseMethod('GET') // 'GET'
779
+ * parseMethod('PURGE') // undefined
780
+ * parseMethod('get') // undefined case-sensitive
781
+ * ```
782
+ */
783
+ export declare function parseMethod(value: string): Method | undefined;
762
784
 
763
- /**
764
- * Identity pass-through for a {@link RouteInput} that pins its `Path` generic
765
- * to the LITERAL registration-site string, so `context.params` types
766
- * correctly through {@link PathParams} without an explicit type argument.
767
- *
768
- * @remarks
769
- * A bare object literal handed straight to {@link import('./types.js').DispatcherInterface}'s
770
- * `add` already infers `Path` as a literal at that call site — but the moment
771
- * the object is built through an intermediate binding (a local `const route =
772
- * { method, path, handler }`) TypeScript widens `path` to `string` unless the
773
- * binding's own type is pinned. Wrapping the literal in `route(...)` supplies
774
- * that pin: its `const Path extends string` type parameter infers the NARROW
775
- * literal from the call, and the function returns its input completely
776
- * unchanged (same reference, no cloning, no validation) this is a
777
- * compile-time typing aid only, not a construction step (contrast
778
- * {@link import('./factories.js')} `create*` entity factories). A
779
- * heterogeneous `RouteInput[]` built from several `route(...)` calls still
780
- * widens each element's `Path` to `string` once collected into one array
781
- * (§14) the realistic ceiling this helper raises is PER-CALL typing at the
782
- * registration site, not a stored, still-literal-typed record.
783
- *
784
- * @typeParam Path - The route path pattern literal (drives `context.params`
785
- * via {@link PathParams})
786
- * @typeParam TState - The consumer's opaque per-request state type
787
- * @param input - The {@link RouteInput} to pass through unchanged
788
- * @returns `input`, unchanged (same reference)
789
- *
790
- * @example
791
- * ```ts
792
- * const input = route({
793
- * method: 'GET',
794
- * path: '/users/:id',
795
- * handler: (_request, context) => new Response(context.params.id), // typed string
796
- * })
797
- * dispatcher.add(input)
798
- * ```
799
- */
800
- export declare function route<const Path extends string, TState = undefined>(input: RouteInput<Path, TState>): RouteInput<Path, TState>;
785
+ /**
786
+ * Extracts `{ name: string }` param records from a path pattern at the type
787
+ * level the typed half of the path grammar.
788
+ *
789
+ * @typeParam Path - A route path pattern literal (`/users/:id/posts/:slug`,
790
+ * `/files/*rest`, or a parameterless literal path)
791
+ *
792
+ * @remarks
793
+ * A `:name` segment contributes `{ name: string }`; a trailing `*name`
794
+ * wildcard (the grammar's only allowed wildcard position) contributes
795
+ * `{ name: string }` capturing the rest of the path; a parameterless pattern
796
+ * resolves to an empty record. Built over {@link PathParamsRaw} and flattened
797
+ * through an identity-mapped type so editor hovers show the resolved shape
798
+ * (`{ id: string; slug: string }`) rather than an unresolved intersection.
799
+ *
800
+ * @example
801
+ * ```ts
802
+ * type A = PathParams<'/users/:id/posts/:slug'> // { readonly id: string; readonly slug: string }
803
+ * type B = PathParams<'/files/*rest'> // { readonly rest: string }
804
+ * type C = PathParams<'/health'> // Record<string, never>
805
+ * ```
806
+ */
807
+ export declare type PathParams<Path extends string> = {
808
+ readonly [K in keyof PathParamsRaw<Path>]: PathParamsRaw<Path>[K];
809
+ };
801
810
 
802
- /**
803
- * The ambient context a {@link RouteHandler} receives alongside the raw
804
- * `Request` decoded params, the winning pattern, the parsed URL, and the
805
- * consumer's opaque per-request state.
806
- *
807
- * @typeParam Path - The route path pattern the handler was registered under
808
- * (drives the typed shape of `params` via {@link PathParams})
809
- * @typeParam TState - The consumer's opaque per-request state type
810
- *
811
- * @remarks
812
- * - `params` — the decoded param record, typed from `Path` via
813
- * {@link PathParams} (empty for a parameterless path).
814
- * - `pattern` the winning REGISTERED pattern (matches
815
- * {@link RouterMatch.path}), useful for logging/metrics.
816
- * - `url` the request URL, already parsed once by the dispatcher.
817
- * - `state` the consumer's per-request payload (logger, session, DI bag),
818
- * threaded opaquely through `handle()` the router never inspects it.
819
- */
820
- export declare interface RouteContext<Path extends string = string, TState = undefined> {
821
- readonly params: PathParams<Path>;
822
- readonly pattern: string;
823
- readonly url: URL;
824
- readonly state: TState;
825
- }
811
+ /**
812
+ * Performs recursive, unflattened param extraction for {@link PathParams} walks a
813
+ * path pattern segment by segment (split on `/`), extracting each segment's
814
+ * {@link SegmentParam} contribution and intersecting the rest.
815
+ *
816
+ * @typeParam Path - The path pattern literal being decomposed
817
+ *
818
+ * @remarks
819
+ * Splits `Path` at its first `/` into `Segment` + `Rest`, intersects
820
+ * `SegmentParam<Segment>` with the recursive walk of `Rest`, and — after no
821
+ * further `/` remains resolves the final segment's own `SegmentParam`
822
+ * directly. `SegmentParam` mirrors the runtime `classifySegment` grammar
823
+ * exactly: a `:name` HEAD (identifier-char run, stopping at the first
824
+ * non-identifier char) captures a param; a segment whose `:` is not at the
825
+ * segment START (`a:b`) contributes nothing (the classification fix); a
826
+ * final `*name` wildcard captures the rest-of-path param the same way. A
827
+ * fully parameterless `Path` resolves to `unknown` here (the intersection
828
+ * identity every non-capturing segment contributes) — {@link PathParams}
829
+ * flattens that down to a clean empty record. This type is exported so
830
+ * {@link PathParams} (which flattens it into a clean mapped type for IDE
831
+ * hovers) has a documented, testable recursive step; consumers reach for the
832
+ * flattened {@link PathParams} form, never this raw recursion.
833
+ */
834
+ export declare type PathParamsRaw<Path extends string> = string extends Path ? Readonly<Record<string, string>> : Path extends `${infer Segment}/${infer Rest}` ? SegmentParam<Segment> & PathParamsRaw<Rest> : SegmentParam<Path>;
826
835
 
827
- /**
828
- * One registered route in a {@link RouterInterface} the `path` pattern plus
829
- * the opaque `meta` payload to return on a match, with an optional `name`.
830
- *
831
- * @typeParam Meta - The payload to carry on a match (opaque to the engine —
832
- * a route handler + method on the `Dispatcher`, a component/loader
833
- * reference on the `Navigator`)
834
- *
835
- * @remarks
836
- * - `path` — the `/`-prefixed route path pattern (`/users/:id`); compiled
837
- * once at registration.
838
- * - `meta` the opaque payload returned (as {@link RouterMatch.meta}) when
839
- * this entry is the most-specific match. The engine never inspects it
840
- * the consumer's {@link AnswerHandler} predicate (the override seam)
841
- * decides eligibility from it.
842
- * - `name` — an optional route identifier, carried through onto a match for
843
- * consumers that build named links or debug output.
844
- */
845
- export declare interface RouteEntry<Meta> {
846
- readonly path: string;
847
- readonly meta: Meta;
848
- readonly name?: string;
849
- }
836
+ /**
837
+ * Represents the ambient context a {@link RouteHandler} receives alongside the raw
838
+ * `Request` decoded params, the winning pattern, the parsed URL, and the
839
+ * consumer's opaque per-request state.
840
+ *
841
+ * @typeParam Path - The route path pattern the handler was registered under
842
+ * (drives the typed shape of `params` through {@link PathParams})
843
+ * @typeParam TState - The consumer's opaque per-request state type
844
+ *
845
+ * @remarks
846
+ * - `params` — the decoded param record, typed from `Path` through
847
+ * {@link PathParams} (empty for a parameterless path).
848
+ * - `pattern` the winning REGISTERED pattern (matches
849
+ * {@link RouterMatch.path}), useful for logging/metrics.
850
+ * - `url` — the request URL, already parsed once by the dispatcher.
851
+ * - `state` — the consumer's per-request payload (logger, session, DI bag),
852
+ * threaded opaquely through `handle()` the router never inspects it.
853
+ */
854
+ export declare interface RouteContext<Path extends string = string, TState = undefined> {
855
+ readonly params: PathParams<Path>;
856
+ readonly pattern: string;
857
+ readonly url: URL;
858
+ readonly state: TState;
859
+ }
850
860
 
851
- /**
852
- * A route handler receives the raw fetch `Request` plus its typed
853
- * {@link RouteContext} and returns (or resolves) a fetch `Response`.
854
- *
855
- * @typeParam Path - The route path pattern the handler is registered under
856
- * @typeParam TState - The consumer's opaque per-request state type
857
- *
858
- * @remarks
859
- * A handler throw propagates to the caller of `dispatcher.handle` — the
860
- * dispatcher never invents an error boundary; mapping throws to responses is
861
- * the consuming server's policy (§5.1).
862
- */
863
- export declare type RouteHandler<Path extends string = string, TState = undefined> = (request: Request, context: RouteContext<Path, TState>) => Response | Promise<Response>;
861
+ /**
862
+ * Represents one registered route in a {@link RouterInterface} the `path` pattern plus
863
+ * the opaque `meta` payload to return on a match, with an optional `name`.
864
+ *
865
+ * @typeParam Meta - The payload to carry on a match (opaque to the engine —
866
+ * a route handler + method on the `Dispatcher`, a component/loader
867
+ * reference on the `Navigator`)
868
+ *
869
+ * @remarks
870
+ * - `path` the `/`-prefixed route path pattern (`/users/:id`); compiled
871
+ * once at registration.
872
+ * - `meta` — the opaque payload returned (as {@link RouterMatch.meta}) when
873
+ * this entry is the most-specific match. The engine never inspects it
874
+ * the consumer's {@link AnswerHandler} predicate (the override seam)
875
+ * decides eligibility from it.
876
+ * - `name` — an optional route identifier, carried through onto a match for
877
+ * consumers that build named links or debug output.
878
+ */
879
+ export declare interface RouteEntry<Meta> {
880
+ readonly path: string;
881
+ readonly meta: Meta;
882
+ readonly name?: string;
883
+ }
864
884
 
865
- /**
866
- * One route registration input for {@link DispatcherInterface.add} the
867
- * method-dimensioned counterpart of {@link RouteEntry}.
868
- *
869
- * @typeParam Path - The route path pattern literal (drives the typed
870
- * `handler`'s `context.params` via {@link PathParams})
871
- * @typeParam TState - The consumer's opaque per-request state type
872
- *
873
- * @remarks
874
- * - `method` the HTTP method this route answers.
875
- * - `path` — the `/`-prefixed route path pattern.
876
- * - `handler` — the {@link RouteHandler} invoked on a match.
877
- * - `name` an optional route identifier, carried through onto a
878
- * {@link RouterMatch}.
879
- */
880
- export declare interface RouteInput<Path extends string = string, TState = undefined> {
881
- readonly method: Method;
882
- readonly path: Path;
883
- readonly handler: RouteHandler<Path, TState>;
884
- readonly name?: string;
885
- }
885
+ /**
886
+ * Receives the raw fetch `Request` plus its typed {@link RouteContext} and returns (or resolves)
887
+ * a fetch `Response`.
888
+ *
889
+ * @typeParam Path - The route path pattern the handler is registered under
890
+ * @typeParam TState - The consumer's opaque per-request state type
891
+ *
892
+ * @remarks
893
+ * A handler throw propagates to the caller of `dispatcher.handle` — the
894
+ * dispatcher never invents an error boundary; mapping throws to responses is
895
+ * the consuming server's policy.
896
+ */
897
+ export declare type RouteHandler<Path extends string = string, TState = undefined> = (request: Request, context: RouteContext<Path, TState>) => Response | Promise<Response>;
886
898
 
887
- /**
888
- * The path-matching + registry engine registers `{ path, meta, name? }`
889
- * entries (compiling each path once) and resolves a concrete pathname to the
890
- * MOST SPECIFIC matching entry. The shared machine both the `Navigator`
891
- * (browser) and the `Dispatcher` (core, method-dimensioned) compose.
892
- *
893
- * @typeParam Meta - The opaque payload each entry carries and a match returns
894
- *
895
- * @remarks
896
- * - **Registration boundary guard (§14).** `add` validates each entry's
897
- * `path` `isString` plus a leading `/` and throws `TypeError` on a
898
- * malformed registration; `match` stays guard-free (the hot path).
899
- * - **Compile-once.** Each path is compiled exactly once at registration into
900
- * a parallel `#compiled` array, so `match` runs only a cached `exec` per
901
- * candidate.
902
- * - **Dedup via `key`.** When `options.key` is set, an entry whose computed
903
- * key already exists REPLACES the prior one IN PLACE (both the `#entries`
904
- * and `#compiled` arrays, at the existing index) — last write wins, no
905
- * engine rebuild. Omitted ⇒ every entry is kept, even duplicate paths.
906
- * - **Groups.** `group(prefix)` returns a {@link GroupInterface} that composes
907
- * `prefix` onto every entry it registers, nesting via {@link joinPaths}.
908
- *
909
- * @example
910
- * ```ts
911
- * const router = new Router<{ readonly page: string }>()
912
- * router.add({ path: '/users/:id', meta: { page: 'profile' } })
913
- * router.match('/users/7') // { path: '/users/:id', params: { id: '7' }, meta: { page: 'profile' } }
914
- * ```
915
- */
916
- export declare class Router<Meta> implements RouterInterface<Meta> {
917
- #private;
918
- constructor(options?: RouterOptions<Meta>);
919
- get count(): number;
920
- add(entry: RouteEntry<Meta>): void;
921
- add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
922
- match(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined;
923
- entries(): ReadonlyArray<RouteEntry<Meta>>;
924
- entries(pathname: string): ReadonlyArray<RouteEntry<Meta>>;
925
- group(prefix: string): GroupInterface<Meta>;
926
- clear(): void;
927
- }
899
+ /**
900
+ * Represents one route registration input for {@link DispatcherInterface.add} the
901
+ * method-dimensioned counterpart of {@link RouteEntry}.
902
+ *
903
+ * @typeParam Path - The route path pattern literal (drives the typed
904
+ * `handler`'s `context.params` through {@link PathParams})
905
+ * @typeParam TState - The consumer's opaque per-request state type
906
+ *
907
+ * @remarks
908
+ * - `method` the HTTP method this route answers.
909
+ * - `path` — the `/`-prefixed route path pattern.
910
+ * - `handler` the {@link RouteHandler} invoked on a match.
911
+ * - `name` an optional route identifier, carried through onto a
912
+ * {@link RouterMatch}.
913
+ */
914
+ export declare interface RouteInput<Path extends string = string, TState = undefined> {
915
+ readonly method: Method;
916
+ readonly path: Path;
917
+ readonly handler: RouteHandler<Path, TState>;
918
+ readonly name?: string;
919
+ }
928
920
 
929
- /**
930
- * The `meta` payload a {@link DispatcherInterface} stores in its underlying
931
- * `Router` what {@link RouterInterface.match} returns as
932
- * {@link RouterMatch.meta} on a dispatch hit.
933
- *
934
- * @typeParam TState - The consumer's opaque per-request state type
935
- *
936
- * @remarks
937
- * The handler is typed over `string` (not the original literal `Path`) at
938
- * storage — path-specific param typing is recovered at the call site through
939
- * {@link RouteInput}'s generic `Path`, not preserved in the stored record.
940
- */
941
- export declare interface RouteRecord<TState> {
942
- readonly method: Method;
943
- readonly handler: RouteHandler<string, TState>;
944
- readonly name?: string;
945
- }
921
+ /**
922
+ * Represents the path-matching + registry engine — registers `{ path, meta, name? }`
923
+ * entries (compiling each path once) and resolves a concrete pathname to the
924
+ * MOST SPECIFIC matching entry. The shared machine both the `Navigator`
925
+ * (browser) and the `Dispatcher` (core, method-dimensioned) compose.
926
+ *
927
+ * @typeParam Meta - The opaque payload each entry carries and a match returns
928
+ *
929
+ * @remarks
930
+ * - **Registration boundary guard.** `add` validates each entry's
931
+ * `path` `isString` plus a leading `/` and throws a `ContractError` on a
932
+ * malformed registration; `match` stays guard-free (the hot path).
933
+ * - **Compile-once.** Each path is compiled exactly once at registration into
934
+ * a parallel `#compiled` array, so `match` runs only a cached `exec` per
935
+ * candidate.
936
+ * - **Dedup through `key`.** When `options.key` is set, an entry whose computed
937
+ * key already exists REPLACES the prior one IN PLACE (both the `#entries`
938
+ * and `#compiled` arrays, at the existing index) — last write wins, no
939
+ * engine rebuild. Omitted ⇒ every entry is kept, even duplicate paths.
940
+ * - **Groups.** `group(prefix)` returns a {@link GroupInterface} that composes
941
+ * `prefix` onto every entry it registers, nesting through {@link joinPaths}.
942
+ *
943
+ * @example
944
+ * ```ts
945
+ * const router = new Router<{ readonly page: string }>()
946
+ * router.add({ path: '/users/:id', meta: { page: 'profile' } })
947
+ * router.match('/users/7') // { path: '/users/:id', params: { id: '7' }, meta: { page: 'profile' } }
948
+ * ```
949
+ */
950
+ export declare class Router<Meta> implements RouterInterface<Meta> {
951
+ #private;
952
+ constructor(options?: RouterOptions<Meta>);
953
+ get count(): number;
954
+ add(entry: RouteEntry<Meta>): void;
955
+ add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
956
+ match(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined;
957
+ entries(): ReadonlyArray<RouteEntry<Meta>>;
958
+ entries(pathname: string): ReadonlyArray<RouteEntry<Meta>>;
959
+ group(prefix: string): GroupInterface<Meta>;
960
+ clear(): void;
961
+ }
946
962
 
947
- /**
948
- * The path-matching + registry engine contract (the §4.5 behavioral-interface
949
- * role for the one-class-per-file `Router`). Registers `{ path, meta, name? }`
950
- * entries (compiling each path once) and resolves a concrete pathname to the
951
- * MOST SPECIFIC matching entry — a literal segment beats a param beats a
952
- * wildcard at the earliest differing segment, registration-order-independent
953
- * (§4). The shared engine both the `Navigator` (browser) and the `Dispatcher`
954
- * (core, method-dimensioned) compose.
955
- *
956
- * @typeParam Meta - The opaque payload each entry carries and a match returns
957
- *
958
- * @remarks
959
- * - `count` the number of registered entries.
960
- * - `add(entry)` / `add(entries)` — register ONE / MANY entries (§9.2 batch);
961
- * each path is compiled once here. When constructed with a `key` option,
962
- * an entry whose key already exists replaces the prior one in place;
963
- * otherwise every entry is kept, even duplicate paths.
964
- * - `match(pathname, answers?)` — the MOST-SPECIFIC matching entry as a
965
- * {@link RouterMatch} (its winning `path`, decoded `params`, `meta`, and
966
- * `name`), or `undefined`. The optional {@link AnswerHandler} predicate
967
- * filters candidates by `meta` first; omitted ⇒ every path match is
968
- * eligible.
969
- * - `entries()` — ALL registered entries in registration order.
970
- * - `entries(pathname)` — only entries whose path matches `pathname` (the
971
- * §9 plural accessor's filtered form; backs a consumer's allow/405 set).
972
- * - `group(prefix)` — a {@link GroupInterface} scoped under `prefix`; entries
973
- * added through the group are registered on this same router with `prefix`
974
- * prepended to each path.
975
- * - `clear()` — drop every entry (§10), leaving the router reusable.
976
- */
977
- export declare interface RouterInterface<Meta> {
978
- readonly count: number;
979
- add(entry: RouteEntry<Meta>): void;
980
- add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
981
- match(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined;
982
- entries(): ReadonlyArray<RouteEntry<Meta>>;
983
- entries(pathname: string): ReadonlyArray<RouteEntry<Meta>>;
984
- group(prefix: string): GroupInterface<Meta>;
985
- clear(): void;
986
- }
963
+ /**
964
+ * Represents the `meta` payload a {@link DispatcherInterface} stores in its underlying
965
+ * `Router` what {@link RouterInterface.match} returns as
966
+ * {@link RouterMatch.meta} on a dispatch hit.
967
+ *
968
+ * @typeParam TState - The consumer's opaque per-request state type
969
+ *
970
+ * @remarks
971
+ * The handler is typed over `string` (not the original literal `Path`) at
972
+ * storage path-specific param typing is recovered at the call site through
973
+ * {@link RouteInput}'s generic `Path`, not preserved in the stored record.
974
+ */
975
+ export declare interface RouteRecord<TState> {
976
+ readonly method: Method;
977
+ readonly handler: RouteHandler<string, TState>;
978
+ readonly name?: string;
979
+ }
987
980
 
988
- /**
989
- * One matched route the winning entry's PATTERN, decoded params, `meta`
990
- * payload, and optional `name`.
991
- *
992
- * @typeParam Meta - The payload the winning entry carries
993
- *
994
- * @remarks
995
- * What {@link RouterInterface.match} returns on a hit (`undefined` on a
996
- * miss). `path` is the winning entry's REGISTERED PATTERN (not the concrete
997
- * pathname that was matched) useful for consumers that need to know which
998
- * route fired. `params` is a frozen `name → value` record (empty for a
999
- * parameterless path), each value URL-decoded with a malformed `%` escape
1000
- * tolerated as a literal (§4, never throws). Plain data — no behavior.
1001
- */
1002
- export declare interface RouterMatch<Meta> {
1003
- readonly path: string;
1004
- readonly params: Readonly<Record<string, string>>;
1005
- readonly meta: Meta;
1006
- readonly name?: string;
1007
- }
981
+ /**
982
+ * Represents the path-matching + registry engine contract (the behavioral-interface
983
+ * role for the one-class-per-file `Router`). Registers `{ path, meta, name? }`
984
+ * entries (compiling each path once) and resolves a concrete pathname to the
985
+ * MOST SPECIFIC matching entry a literal segment beats a param beats a
986
+ * wildcard at the earliest differing segment, registration-order-independent.
987
+ * The shared engine both the `Navigator` (browser) and the `Dispatcher`
988
+ * (core, method-dimensioned) compose.
989
+ *
990
+ * @typeParam Meta - The opaque payload each entry carries and a match returns
991
+ *
992
+ * @remarks
993
+ * - `count` the number of registered entries.
994
+ * - `add(entry)` / `add(entries)` — register ONE / MANY entries (batch registration);
995
+ * each path is compiled once here. When constructed with a `key` option,
996
+ * an entry whose key already exists replaces the prior one in place;
997
+ * otherwise every entry is kept, even duplicate paths.
998
+ * - `match(pathname, answers?)` — the MOST-SPECIFIC matching entry as a
999
+ * {@link RouterMatch} (its winning `path`, decoded `params`, `meta`, and
1000
+ * `name`), or `undefined`. The optional {@link AnswerHandler} predicate
1001
+ * filters candidates by `meta` first; omitted ⇒ every path match is
1002
+ * eligible.
1003
+ * - `entries()` — ALL registered entries in registration order.
1004
+ * - `entries(pathname)` — only entries whose path matches `pathname` (the
1005
+ * plural accessor's filtered form; backs a consumer's allow/405 set).
1006
+ * - `group(prefix)` — a {@link GroupInterface} scoped under `prefix`; entries
1007
+ * added through the group are registered on this same router with `prefix`
1008
+ * prepended to each path.
1009
+ * - `clear()` — drop every entry, leaving the router reusable.
1010
+ */
1011
+ export declare interface RouterInterface<Meta> {
1012
+ readonly count: number;
1013
+ add(entry: RouteEntry<Meta>): void;
1014
+ add(entries: ReadonlyArray<RouteEntry<Meta>>): void;
1015
+ match(pathname: string, answers?: AnswerHandler<Meta>): RouterMatch<Meta> | undefined;
1016
+ entries(): ReadonlyArray<RouteEntry<Meta>>;
1017
+ entries(pathname: string): ReadonlyArray<RouteEntry<Meta>>;
1018
+ group(prefix: string): GroupInterface<Meta>;
1019
+ clear(): void;
1020
+ }
1008
1021
 
1009
- /**
1010
- * Options for `createRouter`an optional initial entry set, the case-
1011
- * sensitivity toggle, and the dedup identity function.
1012
- *
1013
- * @typeParam Meta - The entry payload type
1014
- *
1015
- * @remarks
1016
- * - `entries` the initial `{ path, meta, name? }` entries to register (each
1017
- * path compiled once), equivalent to a bare `createRouter()` followed by
1018
- * `add(entries)`. Omitted an empty router.
1019
- * - `sensitive` case-sensitive path matching (default `true`, §4). Set
1020
- * `false` to fold case during matching (`/Users` matches `/users`);
1021
- * registered patterns are never case-folded in storage, only in matching.
1022
- * - `key` — an optional dedup identity function computed per entry
1023
- * (`RouteEntry<Meta> → string`). When provided, registering an entry whose
1024
- * key already exists REPLACES the prior entry in place (last write wins,
1025
- * no engine rebuild) instead of adding a second candidate. Omitted ⇒ every
1026
- * registered entry is kept, even duplicate paths.
1027
- */
1028
- export declare interface RouterOptions<Meta> {
1029
- readonly entries?: ReadonlyArray<RouteEntry<Meta>>;
1030
- readonly sensitive?: boolean;
1031
- readonly key?: (entry: RouteEntry<Meta>) => string;
1032
- }
1022
+ /**
1023
+ * Represents one matched route the winning entry's PATTERN, decoded params, `meta`
1024
+ * payload, and optional `name`.
1025
+ *
1026
+ * @typeParam Meta - The payload the winning entry carries
1027
+ *
1028
+ * @remarks
1029
+ * What {@link RouterInterface.match} returns on a hit (`undefined` on a
1030
+ * miss). `path` is the winning entry's REGISTERED PATTERN (not the concrete
1031
+ * pathname that was matched) useful for consumers that need to know which
1032
+ * route fired. `params` is a frozen `name value` record (empty for a
1033
+ * parameterless path), each value URL-decoded with a malformed `%` escape
1034
+ * tolerated as a literal, never throws. Plain data no behavior.
1035
+ */
1036
+ export declare interface RouterMatch<Meta> {
1037
+ readonly path: string;
1038
+ readonly params: Readonly<Record<string, string>>;
1039
+ readonly meta: Meta;
1040
+ readonly name?: string;
1041
+ }
1033
1042
 
1034
- export declare type SegmentParam<Segment extends string> = Segment extends `:${infer Rest}` ? IdentifierHead<Rest> extends infer Name extends string ? Name extends '' ? unknown : {
1035
- readonly [K in Name]: string;
1036
- } : unknown : Segment extends `*${infer Rest}` ? IdentifierHead<Rest> extends infer Name extends string ? Name extends '' ? unknown : {
1037
- readonly [K in Name]: string;
1038
- } : unknown : unknown;
1043
+ /**
1044
+ * Represents the options for `createRouter` — an optional initial entry set, the case-
1045
+ * sensitivity toggle, and the dedup identity function.
1046
+ *
1047
+ * @typeParam Meta - The entry payload type
1048
+ *
1049
+ * @remarks
1050
+ * - `entries` — the initial `{ path, meta, name? }` entries to register (each
1051
+ * path compiled once), equivalent to a bare `createRouter()` followed by
1052
+ * `add(entries)`. Omitted ⇒ an empty router.
1053
+ * - `sensitive` — case-sensitive path matching (default `true`). Set
1054
+ * `false` to fold case during matching (`/Users` matches `/users`);
1055
+ * registered patterns are never case-folded in storage, only in matching.
1056
+ * - `key` — an optional dedup identity function computed per entry
1057
+ * (`RouteEntry<Meta> → string`). When provided, registering an entry whose
1058
+ * key already exists REPLACES the prior entry in place (last write wins,
1059
+ * no engine rebuild) instead of adding a second candidate. Omitted ⇒ every
1060
+ * registered entry is kept, even duplicate paths.
1061
+ */
1062
+ export declare interface RouterOptions<Meta> {
1063
+ readonly entries?: ReadonlyArray<RouteEntry<Meta>>;
1064
+ readonly sensitive?: boolean;
1065
+ readonly key?: (entry: RouteEntry<Meta>) => string;
1066
+ }
1039
1067
 
1040
- export declare type TakeIdentifierTail<S extends string, Acc extends string> = S extends `${infer Head}${infer Tail}` ? Head extends IdentifierChar ? TakeIdentifierTail<Tail, `${Acc}${Head}`> : Acc : Acc;
1068
+ export declare type SegmentParam<Segment extends string> = Segment extends `:${infer Rest}` ? IdentifierHead<Rest> extends infer Name extends string ? Name extends '' ? unknown : {
1069
+ readonly [K in Name]: string;
1070
+ } : unknown : Segment extends `*${infer Rest}` ? IdentifierHead<Rest> extends infer Name extends string ? Name extends '' ? unknown : {
1071
+ readonly [K in Name]: string;
1072
+ } : unknown : unknown;
1041
1073
 
1042
- /**
1043
- * Specificity tier for a **literal** path segment (`/users`) — the highest
1044
- * tier, always outranking a param or wildcard segment at the same position.
1045
- *
1046
- * @remarks
1047
- * Consumed by `computeSpecificity` (U1 `helpers.ts`) when ranking candidate
1048
- * matches left-to-right at the earliest differing segment (§4 precedence).
1049
- *
1050
- * @example
1051
- * ```ts
1052
- * TIER_LITERAL > TIER_PARAM // true
1053
- * ```
1054
- */
1055
- export declare const TIER_LITERAL = 2;
1074
+ export declare type TakeIdentifierTail<S extends string, Acc extends string> = S extends `${infer Head}${infer Tail}` ? Head extends IdentifierChar ? TakeIdentifierTail<Tail, `${Acc}${Head}`> : Acc : Acc;
1056
1075
 
1057
- /**
1058
- * Specificity tier for a **param** path segment (`:name`) — ranks below a
1059
- * literal segment and above a wildcard segment at the same position.
1060
- *
1061
- * @remarks
1062
- * Consumed by `computeSpecificity` (U1 `helpers.ts`) alongside {@link TIER_LITERAL}
1063
- * and {@link TIER_WILDCARD}.
1064
- *
1065
- * @example
1066
- * ```ts
1067
- * TIER_PARAM > TIER_WILDCARD // true
1068
- * ```
1069
- */
1070
- export declare const TIER_PARAM = 1;
1076
+ /**
1077
+ * Names the specificity tier for a **literal** path segment (`/users`) — the highest
1078
+ * tier, always outranking a param or wildcard segment at the same position.
1079
+ *
1080
+ * @remarks
1081
+ * Consumed by `computeSpecificity` (the path compiler in `helpers.ts`) when ranking candidate
1082
+ * matches left-to-right at the earliest differing segment.
1083
+ *
1084
+ * @example
1085
+ * ```ts
1086
+ * TIER_LITERAL > TIER_PARAM // true
1087
+ * ```
1088
+ */
1089
+ export declare const TIER_LITERAL = 2;
1071
1090
 
1072
- /**
1073
- * Specificity tier for a **wildcard** path segment (`*name`) — the lowest
1074
- * tier; a wildcard only ever wins against another wildcard shape (an
1075
- * equal-specificity tie resolved by registration order).
1076
- *
1077
- * @remarks
1078
- * Consumed by `computeSpecificity` (U1 `helpers.ts`).
1079
- *
1080
- * @example
1081
- * ```ts
1082
- * TIER_WILDCARD // 0
1083
- * ```
1084
- */
1085
- export declare const TIER_WILDCARD = 0;
1091
+ /**
1092
+ * Names the specificity tier for a **param** path segment (`:name`) — ranks below a
1093
+ * literal segment and above a wildcard segment at the same position.
1094
+ *
1095
+ * @remarks
1096
+ * Consumed by `computeSpecificity` (the path compiler in `helpers.ts`) alongside {@link TIER_LITERAL}
1097
+ * and {@link TIER_WILDCARD}.
1098
+ *
1099
+ * @example
1100
+ * ```ts
1101
+ * TIER_PARAM > TIER_WILDCARD // true
1102
+ * ```
1103
+ */
1104
+ export declare const TIER_PARAM = 1;
1105
+
1106
+ /**
1107
+ * Names the specificity tier for a **wildcard** path segment (`*name`) — the lowest
1108
+ * tier; a wildcard only ever wins against another wildcard shape (an
1109
+ * equal-specificity tie resolved by registration order).
1110
+ *
1111
+ * @remarks
1112
+ * Consumed by `computeSpecificity` (the path compiler in `helpers.ts`).
1113
+ *
1114
+ * @example
1115
+ * ```ts
1116
+ * TIER_WILDCARD // 0
1117
+ * ```
1118
+ */
1119
+ export declare const TIER_WILDCARD = 0;
1086
1120
 
1087
- export { }
1121
+ export { }