paramour 0.5.1 → 0.7.0

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.
package/dist/path.js CHANGED
@@ -1,14 +1,15 @@
1
- import { describeType, ParamourError, ParamsDecodeError, ParseError, SerializeError, } from "./errors.js";
1
+ import { codecShapeLabel } from "./describe.js";
2
+ import { describeType, ParamourError, ParamsDecodeError, ParseError, parseIssueReason, SerializeError, } from "./errors.js";
2
3
  import { encodeComponent, readInputValue, serializeValue } from "./search.js";
3
- // Anchored per the wire-format spec's regex ethos; name charset excludes
4
- // brackets so nesting can't smuggle through. Match order mirrors the type
5
- // grammar: `[[...` before `[...` before `[`.
4
+ // Anchored, like the wire-value grammars; the name charset excludes brackets
5
+ // so nesting can't smuggle through. Match order mirrors the type grammar:
6
+ // `[[...` before `[...` before `[`.
6
7
  const OPTIONAL_CATCHALL_TOKEN = /^\[\[\.\.\.([^\][]+)\]\]$/;
7
8
  const CATCHALL_TOKEN = /^\[\.\.\.([^\][]+)\]$/;
8
9
  const SINGLE_TOKEN = /^\[([^\][]+)\]$/;
9
10
  const GROUP_SEGMENT = /^\(.*\)$/;
10
11
  /**
11
- * Builds the path portion of an href (RL5): `/` plus the encoded segments
12
+ * Builds the path portion of an href: `/` plus the encoded segments
12
13
  * joined with `/`. R2's element joining falls out of the same join as
13
14
  * everything else; a fully-elided path (an optional catch-all at the root)
14
15
  * yields "/".
@@ -17,7 +18,7 @@ export function buildPath(route, params) {
17
18
  return `/${encodeParams(route, params).join("/")}`;
18
19
  }
19
20
  /**
20
- * Decodes a params source against a route's codecs (RL7), the sync twin of
21
+ * Decodes a params source against a route's codecs, the sync twin of
21
22
  * `route.parseParams` — mirrors decodeSearch: per-key {@link Issue}
22
23
  * aggregation into {@link ParamsDecodeError}. Shape validation is strict:
23
24
  * `[id]` given an array, a catch-all given a string, a missing required key,
@@ -46,6 +47,12 @@ export function decodeParams(route, source, options) {
46
47
  if (segment.kind === "static")
47
48
  continue;
48
49
  const codec = requireCodec(config, segment.name, route.path);
50
+ // One "expected" label per param, whatever the failure mode: a
51
+ // catch-all's codec describes ONE element, but the param's shape is the
52
+ // array, so every catch-all issue — missing, wrong shape, or a single
53
+ // failing element — cites the repeated form (integer[]), matching
54
+ // decodeSearch's array-element issues.
55
+ const expected = codecShapeLabel(codec, segment.kind !== "single");
49
56
  // Own properties only: unknown keys are never read, and inherited
50
57
  // Object.prototype members must not count as present values.
51
58
  const value = Object.hasOwn(source, segment.name)
@@ -54,16 +61,20 @@ export function decodeParams(route, source, options) {
54
61
  if (segment.kind === "single") {
55
62
  if (value === undefined) {
56
63
  issues.push({
64
+ expected,
57
65
  key: segment.name,
58
66
  message: "required route param is missing",
67
+ reason: "missing",
59
68
  });
60
69
  }
61
70
  else if (typeof value !== "string") {
62
- // RL7: shape mismatches are recorded issues, never ParseErrors —
71
+ // Shape mismatches are recorded issues, never ParseErrors —
63
72
  // .catch() cannot recover them.
64
73
  issues.push({
74
+ expected,
65
75
  key: segment.name,
66
76
  message: `expected a single segment value, got ${Array.isArray(value) ? "an array" : typeof value}`,
77
+ reason: "shape",
67
78
  });
68
79
  }
69
80
  else {
@@ -82,7 +93,18 @@ export function decodeParams(route, source, options) {
82
93
  entries.push([segment.name, codec["~catchValue"]()]);
83
94
  }
84
95
  else if (error instanceof ParseError) {
85
- issues.push({ key: segment.name, message: error.message });
96
+ issues.push({
97
+ expected,
98
+ key: segment.name,
99
+ message: error.message,
100
+ // "parse" vs "validate" comes from the ParseError's own
101
+ // selfDescribing flag — structural, never message sniffing.
102
+ reason: parseIssueReason(error),
103
+ // Issue.wire is the codec-grammar-layer value — the DECODED
104
+ // segment, not the raw URL text — matching decodeSearch,
105
+ // whose sources arrive platform-decoded.
106
+ wire: decoded,
107
+ });
86
108
  }
87
109
  else {
88
110
  throw error;
@@ -98,25 +120,33 @@ export function decodeParams(route, source, options) {
98
120
  }
99
121
  else {
100
122
  issues.push({
123
+ expected,
101
124
  key: segment.name,
102
125
  message: "required route param is missing",
126
+ reason: "missing",
103
127
  });
104
128
  }
105
129
  continue;
106
130
  }
107
131
  if (!Array.isArray(value)) {
108
132
  issues.push({
133
+ expected,
109
134
  key: segment.name,
110
135
  message: `expected catch-all values (an array), got ${typeof value}`,
136
+ reason: "shape",
111
137
  });
112
138
  continue;
113
139
  }
114
140
  if (segment.kind === "catchall" && value.length === 0) {
115
- // RL7: no URL produces a present-but-empty required catch-all; only
116
- // hand-built props can — mirrors R3's encode-side stance.
141
+ // No URL produces a present-but-empty required catch-all; only
142
+ // hand-built props can — mirrors R3's encode-side stance. Reason
143
+ // "missing", not "shape": the key exists but its VALUES are missing,
144
+ // so pointing at the expected form is what helps.
117
145
  issues.push({
146
+ expected,
118
147
  key: segment.name,
119
148
  message: "required catch-all received no segment values",
149
+ reason: "missing",
120
150
  });
121
151
  continue;
122
152
  }
@@ -129,8 +159,10 @@ export function decodeParams(route, source, options) {
129
159
  for (const [index, element] of elements.entries()) {
130
160
  if (typeof element !== "string") {
131
161
  issues.push({
162
+ expected,
132
163
  key: segment.name,
133
164
  message: `element ${String(index)}: expected a string, got ${typeof element}`,
165
+ reason: "shape",
134
166
  });
135
167
  failed = true;
136
168
  continue;
@@ -143,7 +175,7 @@ export function decodeParams(route, source, options) {
143
175
  parsed.push(codec["~parseElement"](decoded));
144
176
  }
145
177
  catch (error) {
146
- // Element-wise recovery (RL7, forced by D6): the codec describes ONE
178
+ // Element-wise recovery (forced by D6): the codec describes ONE
147
179
  // element, so a .catch() fallback is element-typed — each failing
148
180
  // element recovers independently ("1","x","3" → 1, fallback, 3).
149
181
  if (error instanceof ParseError && codec["~catchValue"] !== undefined) {
@@ -151,8 +183,13 @@ export function decodeParams(route, source, options) {
151
183
  }
152
184
  else if (error instanceof ParseError) {
153
185
  issues.push({
186
+ expected,
154
187
  key: segment.name,
155
188
  message: `element ${String(index)}: ${error.message}`,
189
+ // Classified from the flag, as in the single-param branch.
190
+ reason: parseIssueReason(error),
191
+ // Grammar-layer (decoded) value, as in the single-param branch.
192
+ wire: decoded,
156
193
  });
157
194
  failed = true;
158
195
  }
@@ -165,17 +202,17 @@ export function decodeParams(route, source, options) {
165
202
  entries.push([segment.name, parsed]);
166
203
  }
167
204
  if (issues.length > 0) {
168
- throw new ParamsDecodeError(issues);
205
+ throw new ParamsDecodeError(issues, route.path);
169
206
  }
170
207
  return Object.fromEntries(entries);
171
208
  }
172
209
  /**
173
210
  * Encodes a params input into ordered, already-percent-encoded URL segment
174
- * strings (RL5) — ONE entry per emitted URL segment: a static segment is
175
- * emitted verbatim, a single param contributes one entry (R1), a catch-all
176
- * one per element (R2), an elided optional catch-all none (R3). Codec
177
- * serialize errors and schema-refinement failures (N9) propagate unchanged,
178
- * already branded at their own chokepoints.
211
+ * strings — ONE entry per emitted URL segment: a static segment is emitted
212
+ * verbatim, a single param contributes one entry (R1), a catch-all one per
213
+ * element (R2), an elided optional catch-all none (R3). Codec serialize
214
+ * errors and schema-refinement failures propagate unchanged, already branded
215
+ * at their own chokepoints.
179
216
  */
180
217
  export function encodeParams(route, params) {
181
218
  // The TS contract forbids non-object inputs, but plain-JS callers reach
@@ -189,8 +226,8 @@ export function encodeParams(route, params) {
189
226
  const segments = [];
190
227
  for (const segment of routeSegments(route)) {
191
228
  if (segment.kind === "static") {
192
- // RL2/RL5: the literal is URL-shaped and emitted as-is — static
193
- // segments are never re-encoded.
229
+ // The path literal is URL-shaped and emitted as-is — static segments
230
+ // are never re-encoded.
194
231
  segments.push(segment.raw);
195
232
  continue;
196
233
  }
@@ -215,17 +252,17 @@ export function encodeParams(route, params) {
215
252
  /**
216
253
  * Encodes a params input into the per-param wire-value record the static
217
254
  * generation surfaces expect: App Router `generateStaticParams` entries and
218
- * Pages Router `getStaticPaths` `{ params }` objects (PR10's static story).
219
- * Same codec serialization and R1–R4 validation as {@link encodeParams},
220
- * with two deliberate differences: static segments are skipped (Next wants
221
- * only the dynamic params, keyed by name), and values are NOT percent-encoded
222
- * — Next percent-encodes static-params values itself when it materializes
223
- * the concrete URLs, so pre-encoding here would double-encode (the
224
- * encode-side mirror of R5's decode asymmetry). That also means the S7
225
- * lone-surrogate brand stays an encodeParams concern: strings are handed to
226
- * Next verbatim. An elided optional catch-all (R3) OMITS its key — an absent
227
- * key is the base-path variant on both routers (Pages also accepts
228
- * undefined/[]; omission is the one spelling valid on both).
255
+ * Pages Router `getStaticPaths` `{ params }` objects. Same codec
256
+ * serialization and R1–R4 validation as {@link encodeParams}, with two
257
+ * deliberate differences: static segments are skipped (Next wants only the
258
+ * dynamic params, keyed by name), and values are NOT percent-encoded — Next
259
+ * percent-encodes static-params values itself when it materializes the
260
+ * concrete URLs, so pre-encoding here would double-encode (the encode-side
261
+ * mirror of R5's decode asymmetry). That also means the S7 lone-surrogate
262
+ * brand stays an encodeParams concern: strings are handed to Next verbatim.
263
+ * An elided optional catch-all (R3) OMITS its key — an absent key is the
264
+ * base-path variant on both routers (Pages also accepts undefined/[];
265
+ * omission is the one spelling valid on both).
229
266
  */
230
267
  export function encodeStaticParams(route, params) {
231
268
  // The TS contract forbids non-object inputs, but plain-JS callers reach
@@ -255,12 +292,12 @@ export function encodeStaticParams(route, params) {
255
292
  }
256
293
  /**
257
294
  * Tokenizes a path literal into segments, throwing ParamourError on every
258
- * RL1 rejection. Shared by the route constructors (define-time validation)
259
- * and the R-rule runtime here, so encode/decode never re-derive segment
260
- * kinds.
295
+ * rejected literal. Shared by the route constructors (define-time
296
+ * validation) and the R-rule runtime here, so encode/decode never re-derive
297
+ * segment kinds.
261
298
  */
262
299
  export function tokenizePath(path) {
263
- // RL1: either would corrupt href's fixed path–query–fragment assembly (RL4).
300
+ // Either would corrupt href's fixed path–query–fragment assembly.
264
301
  if (path.includes("?")) {
265
302
  throw new ParamourError(`route path must not contain "?": "${path}" (declare search params in the search config)`);
266
303
  }
@@ -281,8 +318,8 @@ export function tokenizePath(path) {
281
318
  if (raw === "") {
282
319
  throw new ParamourError(`route path contains an empty segment: "${path}"`);
283
320
  }
284
- // RL2: path literals are URL-shaped, so group/slot spellings are
285
- // filesystem paths by definition — the most likely migration mistake.
321
+ // Path literals are URL-shaped, so group/slot spellings are filesystem
322
+ // paths by definition — the most likely migration mistake.
286
323
  if (GROUP_SEGMENT.test(raw)) {
287
324
  throw new ParamourError(`route paths are URL-shaped: "${raw}" in "${path}" is a route-group folder name; use the URL path without it`);
288
325
  }
@@ -291,7 +328,7 @@ export function tokenizePath(path) {
291
328
  }
292
329
  const segment = tokenizeSegment(raw, path);
293
330
  if (segment.kind !== "static") {
294
- // RL1: not expressible as a compile error — the mapped type silently
331
+ // Not expressible as a compile error — the mapped type silently
295
332
  // collapses duplicate keys.
296
333
  if (seen.has(segment.name)) {
297
334
  throw new ParamourError(`route path declares param "${segment.name}" more than once: "${path}"`);
@@ -301,7 +338,7 @@ export function tokenizePath(path) {
301
338
  segments.push(segment);
302
339
  }
303
340
  segments.forEach((segment, index) => {
304
- // RL1: Next itself requires catch-alls to be final.
341
+ // Next itself requires catch-alls to be final.
305
342
  if ((segment.kind === "catchall" || segment.kind === "optional-catchall") &&
306
343
  index < segments.length - 1) {
307
344
  throw new ParamourError(`catch-all segment "${segment.raw}" must be the final segment: "${path}"`);
@@ -385,9 +422,8 @@ function serializeDynamicSegment(codec, segment, value) {
385
422
  // R2: each element is serialized independently; on the path surface an
386
423
  // element containing "/" becomes %2F and round-trips as a single element —
387
424
  // core's decodeParams restores it via percentDecodeSegment (R5), since Next
388
- // hands the encoded value straight back on the params surface (wire-spec
389
- // open item 1). The static surface passes the array whole, so "/" needs no
390
- // escaping there.
425
+ // hands the encoded value straight back on the params surface. The static
426
+ // surface passes the array whole, so "/" needs no escaping there.
391
427
  return {
392
428
  kind: "many",
393
429
  values: value.map((element) => serializeSegmentValue(codec, segment.name, element)),
@@ -425,7 +461,7 @@ function tokenizeSegment(raw, path) {
425
461
  if (!raw.startsWith("[...") && single?.[1] !== undefined) {
426
462
  return { kind: "single", name: single[1], raw };
427
463
  }
428
- // RL1: the type layer lets these fall through as static text (RL3), and
464
+ // The type layer lets these fall through as static text, and
429
465
  // pre-generation there is no registry to catch them; href would otherwise
430
466
  // emit the token verbatim.
431
467
  if (raw.includes("[") || raw.includes("]")) {
package/dist/route.d.ts CHANGED
@@ -3,23 +3,23 @@ import { type RouteDecodeError } from "./errors.js";
3
3
  import { type ParamsSource, type PathSegment } from "./path.js";
4
4
  import { type SearchOutputOf, type SearchSlot } from "./search.js";
5
5
  /**
6
- * `any` is deliberate (RL4, same variance gotcha as AnyCodec): codec configs
6
+ * `any` is deliberate (same variance gotcha as AnyCodec): codec configs
7
7
  * reach contravariant positions through the parse methods and `HrefArgs`;
8
8
  * the `unknown` form would reject every concrete route.
9
9
  */
10
10
  export type AnyAppRoute = AppRoute<string, any, any>;
11
- /** Pages twin of {@link AnyAppRoute} (PR3). */
11
+ /** Pages twin of {@link AnyAppRoute}. */
12
12
  export type AnyPagesRoute = PagesRoute<string, any, any>;
13
13
  /**
14
- * Router-agnostic (PR3): matches both brands. This is the bound for
14
+ * Router-agnostic: matches both brands. This is the bound for
15
15
  * everything that only needs the data core — `href()`, the standalone
16
16
  * decoders, `InferRouteParams` — none of which differ by router.
17
17
  */
18
18
  export type AnyRoute = Route<string, any, any>;
19
19
  /**
20
- * An App Router route (PR3/PR7): the async props-based parse surface —
21
- * three surfaces × throwing/safe (RL1/RL6). Props may be promised (Next
22
- * 15/16) and are awaited before any decode runs.
20
+ * An App Router route: the async props-based parse surface — three surfaces
21
+ * × throwing/safe. Props may be promised (Next 15/16) and are awaited before
22
+ * any decode runs.
23
23
  */
24
24
  export interface AppRoute<Path extends string, PC extends ParamsConfig<Path>, SC extends SearchSlot> extends Route<Path, PC, SC, "app"> {
25
25
  /**
@@ -31,9 +31,9 @@ export interface AppRoute<Path extends string, PC extends ParamsConfig<Path>, SC
31
31
  params: ParamsOutput<Path, PC>;
32
32
  search: SearchOutputOf<SC>;
33
33
  }>;
34
- /** Bare params object (RL6) — layout props are structurally assignable. */
34
+ /** Bare params object — layout props are structurally assignable. */
35
35
  parseParams(props: ParamsPropsInput): Promise<ParamsOutput<Path, PC>>;
36
- /** Bare search object (RL6) — the search half alone. */
36
+ /** Bare search object — the search half alone. */
37
37
  parseSearch(props: SearchPropsInput): Promise<SearchOutputOf<SC>>;
38
38
  safeParse(props: RoutePropsInput): Promise<SafeResult<{
39
39
  params: ParamsOutput<Path, PC>;
@@ -42,18 +42,18 @@ export interface AppRoute<Path extends string, PC extends ParamsConfig<Path>, SC
42
42
  safeParseParams(props: ParamsPropsInput): Promise<SafeResult<ParamsOutput<Path, PC>>>;
43
43
  safeParseSearch(props: SearchPropsInput): Promise<SafeResult<SearchOutputOf<SC>>>;
44
44
  }
45
- /** Names of `[...name]` catch-all segments in the path literal (RL3). */
45
+ /** Names of `[...name]` catch-all segments in the path literal. */
46
46
  export type CatchAllNames<Path extends string> = Segments<Path> extends infer S extends string ? S extends `[...${infer Name}]` ? NonEmptyName<Name> : never : never;
47
47
  /**
48
- * Exact-key enforcement (RL1): every excess key's value type becomes `never`,
48
+ * Exact-key enforcement: every excess key's value type becomes `never`,
49
49
  * so a misspelled param fails to compile on its own property line while `PC`
50
50
  * itself stays the naked inference site for `const` codec-literal retention.
51
51
  */
52
52
  export type ConformParams<Path extends string, PC> = PC & Record<Exclude<keyof PC, PathParamNames<Path>>, never>;
53
- /** Decoded params object type for a route (RL3); see {@link ParamsOutput}. */
53
+ /** Decoded params object type for a route; see {@link ParamsOutput}. */
54
54
  export type InferRouteParams<R extends AnyRoute> = ParamsOutput<R["path"], R["~params"]>;
55
55
  /**
56
- * Accepts promised props and plain objects alike (RL6). This width lives on
56
+ * Accepts promised props and plain objects alike. This width lives on
57
57
  * the parse INPUT surface ({@link RoutePropsInput} and friends), not on the
58
58
  * annotation types: every supported Next (peer `>=15`) delivers page props
59
59
  * as promises, and Next 15.5's generated `.next/types` page check requires
@@ -63,32 +63,32 @@ export type InferRouteParams<R extends AnyRoute> = ParamsOutput<R["path"], R["~p
63
63
  */
64
64
  export type MaybePromise<T> = Promise<T> | T;
65
65
  /**
66
- * RL3: an empty name (`[]`, `[...]`, `[[...]]`) is not a token — it falls
67
- * through as static text; the runtime malformed-bracket check is the backstop.
66
+ * An empty name (`[]`, `[...]`, `[[...]]`) is not a token — it falls through
67
+ * as static text; the runtime malformed-bracket check is the backstop.
68
68
  */
69
69
  export type NonEmptyName<Name extends string> = Name extends "" ? never : Name;
70
70
  /**
71
- * Names of `[[...name]]` optional catch-all segments (RL3). The `infer S`
71
+ * Names of `[[...name]]` optional catch-all segments. The `infer S`
72
72
  * indirection is load-bearing: conditionals distribute only over naked type
73
73
  * parameters, and `Segments<Path>` is an alias application, not a parameter.
74
74
  */
75
75
  export type OptionalCatchAllNames<Path extends string> = Segments<Path> extends infer S extends string ? S extends `[[...${infer Name}]]` ? NonEmptyName<Name> : never : never;
76
76
  /**
77
- * Structural context contract for the pages parse surface (PR10): the shape
77
+ * Structural context contract for the pages parse surface: the shape
78
78
  * `getServerSideProps` and `getInitialProps` contexts share, with no
79
79
  * `next/*` import (the ParamsProps/SearchProps precedent). `query` is
80
80
  * REQUIRED: `GetStaticPropsContext` has no query string, so it fails to
81
81
  * compose here by design — typed search at build time would be a lie; the
82
82
  * static story is core's `decodeParams`/`safeDecodeParams`. Both
83
83
  * assignability claims are pinned per supported Next major in
84
- * `examples/next-compat/src/contexts.ts` (PR13).
84
+ * `examples/next-compat/src/contexts.ts`.
85
85
  */
86
86
  export interface PagesContext {
87
87
  readonly params?: ParamsSource | undefined;
88
88
  readonly query: ParamsSource;
89
89
  }
90
90
  /**
91
- * A Pages Router route (PR3/PR10): the sync context-based parse surface.
91
+ * A Pages Router route: the sync context-based parse surface.
92
92
  * `getServerSideProps` / `getInitialProps` hand params and query
93
93
  * synchronously and pre-merged, so there is no promised-props machinery
94
94
  * here — the context split (params authoritative, query minus path-param
@@ -101,25 +101,25 @@ export interface PagesRoute<Path extends string, PC extends ParamsConfig<Path>,
101
101
  * present; when absent (`getInitialProps` — `NextPageContext` has no
102
102
  * `params` even on dynamic routes) they are extracted from `query` by
103
103
  * segment name, which is sound because Next's own merge gives route
104
- * params precedence in `query` (PR10).
104
+ * params precedence in `query`.
105
105
  */
106
106
  parseContext(context: PagesContext): {
107
107
  params: ParamsOutput<Path, PC>;
108
108
  search: SearchOutputOf<SC>;
109
109
  };
110
- /** {@link parseContext} in the safe shape — `safely`'s taxonomy (PR12). */
110
+ /** {@link parseContext} in the safe shape — `safely`'s taxonomy. */
111
111
  safeParseContext(context: PagesContext): SafeResult<{
112
112
  params: ParamsOutput<Path, PC>;
113
113
  search: SearchOutputOf<SC>;
114
114
  }>;
115
115
  }
116
116
  /**
117
- * Augmented by codegen with per-router path unions (RL8/PR9):
117
+ * Augmented by codegen with per-router path unions:
118
118
  * `{ appRoutes: "/a" | …; pagesRoutes: "/x" | … }`. Each member is
119
- * independently ABSENT when its scan is empty (TR3's absent-not-`never`
120
- * rule), preserving per-router world-A/B independence. The generated
121
- * artifact is a pure `.d.ts` module augmentation — no runtime import, so
122
- * tree-shaking is untouched (spike-01 lock-ins #3/#4).
119
+ * independently ABSENT when its scan is empty — absent, never `never` —
120
+ * preserving per-router world-A/B independence. The generated artifact is a
121
+ * pure `.d.ts` module augmentation — no runtime import, so tree-shaking is
122
+ * untouched.
123
123
  */
124
124
  export interface ParamourRegister {
125
125
  }
@@ -127,15 +127,15 @@ export interface ParamourRegister {
127
127
  export type ParamOutput<PC, K extends PropertyKey> = K extends keyof PC ? PC[K] extends AnyCodec ? OutputOf<PC[K]> : never : never;
128
128
  /**
129
129
  * Params schema shape for a path: one codec per dynamic segment name. The
130
- * codec describes ONE segment element (design-02 D5/D6) — arrays come from
131
- * the segment kind, and presence modifiers are compile errors (`ParamCodec`).
132
- * RL9 assigned this to path.ts; it stays here instead so the whole path
133
- * grammar lives in one module — path.ts consumes it via type-only imports,
134
- * keeping runtime imports one-directional (route.ts → path.ts).
130
+ * codec describes ONE segment element (D5/D6) — arrays come from the segment
131
+ * kind, and presence modifiers are compile errors (`ParamCodec`). It lives
132
+ * here rather than in path.ts so the whole path grammar sits in one module —
133
+ * path.ts consumes it via type-only imports, keeping runtime imports
134
+ * one-directional (route.ts → path.ts).
135
135
  */
136
136
  export type ParamsConfig<Path extends string> = Readonly<Record<PathParamNames<Path>, ParamCodec>>;
137
137
  /**
138
- * Parse-output shape (RL3): `[id]` → `Out`, `[...slug]` → `Out[]`,
138
+ * Parse-output shape: `[id]` → `Out`, `[...slug]` → `Out[]`,
139
139
  * `[[...slug]]` → `Out[]` — every key REQUIRED on the output side; an absent
140
140
  * optional catch-all normalizes to `[]` at decode time (D6), so no `?:`
141
141
  * split exists here (that split is the href-input side's concern). Keyed by
@@ -146,7 +146,7 @@ export type ParamsOutput<Path extends string, PC> = {
146
146
  [K in PathParamNames<Path>]: K extends CatchAllNames<Path> | OptionalCatchAllNames<Path> ? ParamOutput<PC, K>[] : ParamOutput<PC, K>;
147
147
  };
148
148
  /**
149
- * Structural props contract for the params half (RL6): layout props are
149
+ * Structural props contract for the params half: layout props are
150
150
  * assignable, and a missing member decodes like an empty source
151
151
  * (required-missing issues, never a crash). Deliberately NOT Next's
152
152
  * generated `PageProps` global — core stays framework-agnostic, and that
@@ -156,52 +156,52 @@ export interface ParamsProps {
156
156
  readonly params?: Promise<ParamsSource>;
157
157
  }
158
158
  /**
159
- * What `parseParams` ACCEPTS (RL6): {@link ParamsProps} plus plain sync
159
+ * What `parseParams` ACCEPTS: {@link ParamsProps} plus plain sync
160
160
  * objects — see {@link MaybePromise} for why the annotation type is
161
161
  * promise-only while the parse input stays wide.
162
162
  */
163
163
  export interface ParamsPropsInput {
164
164
  readonly params?: MaybePromise<ParamsSource>;
165
165
  }
166
- /** Every dynamic segment name in the path literal (RL3). */
166
+ /** Every dynamic segment name in the path literal. */
167
167
  export type PathParamNames<Path extends string> = CatchAllNames<Path> | OptionalCatchAllNames<Path> | SingleParamNames<Path>;
168
168
  /**
169
169
  * Pre-generation: ParamourRegister has no `appRoutes` member, so this
170
170
  * resolves to `string` and any path literal is accepted (unverified).
171
171
  * Post-generation it resolves to the union of filesystem-verified app-router
172
- * paths (RL8, spike-01). Per-router on purpose (PR9): an empty app scan
173
- * keeps THIS fallback while `pagesRoutes` narrows, and vice versa.
172
+ * paths. Per-router on purpose: an empty app scan keeps THIS fallback while
173
+ * `pagesRoutes` narrows, and vice versa.
174
174
  */
175
175
  export type RegisteredAppRoutePaths = ParamourRegister extends {
176
176
  appRoutes: infer R extends string;
177
177
  } ? R : string;
178
- /** Pages twin of {@link RegisteredAppRoutePaths} (PR9). */
178
+ /** Pages twin of {@link RegisteredAppRoutePaths}. */
179
179
  export type RegisteredPagesRoutePaths = ParamourRegister extends {
180
180
  pagesRoutes: infer R extends string;
181
181
  } ? R : string;
182
182
  /**
183
- * Static-only subset of {@link RegisteredAppRoutePaths} (SH2): derived by
183
+ * Static-only subset of {@link RegisteredAppRoutePaths}: derived by
184
184
  * syntactic filter, not emitted — dynamic-ness is a property of the path
185
185
  * literal, so the registry format doesn't change. Same world-A `string`
186
186
  * fallback as the full union (the filter passes `string` through).
187
187
  */
188
188
  export type RegisteredStaticAppRoutePaths = StaticPathsOf<RegisteredAppRoutePaths>;
189
- /** Pages twin of {@link RegisteredStaticAppRoutePaths} (SH2). */
189
+ /** Pages twin of {@link RegisteredStaticAppRoutePaths}. */
190
190
  export type RegisteredStaticPagesRoutePaths = StaticPathsOf<RegisteredPagesRoutePaths>;
191
191
  /**
192
192
  * Every registered STATIC path across both routers — the string form of
193
- * href's path argument (SH1/SH2). Deliberately NOT the union of the two
194
- * per-router types (SH3): each falls back to `string` when its registry
195
- * member is absent, and in a single-router project the absent side's
196
- * `string` would swallow the union and erase verification for the router
197
- * that HAS routes. The permissive fallback applies only when NEITHER member
198
- * is present (world A / TR3's empty merge).
193
+ * href's path argument. Deliberately NOT the union of the two per-router
194
+ * types: each falls back to `string` when its registry member is absent, and
195
+ * in a single-router project the absent side's `string` would swallow the
196
+ * union and erase verification for the router that HAS routes. The
197
+ * permissive fallback applies only when NEITHER member is present (world A,
198
+ * where codegen has merged nothing into the registry).
199
199
  */
200
200
  export type RegisteredStaticRoutePaths = [PresentRegisteredPaths] extends [
201
201
  never
202
202
  ] ? string : StaticPathsOf<PresentRegisteredPaths>;
203
203
  /**
204
- * The router-agnostic core of a defined route (PR3): path, configs, and the
204
+ * The router-agnostic core of a defined route: path, configs, and the
205
205
  * define-time token cache. The parse surface is router-specific and lives on
206
206
  * {@link AppRoute} / {@link PagesRoute} — gating it via the interface split
207
207
  * makes the wrong surface ABSENT, not just ill-typed. `~`-prefixed members
@@ -211,7 +211,7 @@ export type RegisteredStaticRoutePaths = [PresentRegisteredPaths] extends [
211
211
  export interface Route<Path extends string, PC extends ParamsConfig<Path>, SC extends SearchSlot, R extends RouterKind = RouterKind> {
212
212
  readonly path: Path;
213
213
  readonly "~params": PC;
214
- /** The router brand (PR3) — type-state, same discipline as Codec's P/C/A. */
214
+ /** The router brand — type-state, same discipline as Codec's P/C/A. */
215
215
  readonly "~router": R;
216
216
  readonly "~search": SC;
217
217
  /**
@@ -222,10 +222,10 @@ export interface Route<Path extends string, PC extends ParamsConfig<Path>, SC ex
222
222
  readonly "~segments": readonly PathSegment[];
223
223
  }
224
224
  /**
225
- * Conditional on the path shape (RL1, spike-01 lock-in #2): dynamic paths
226
- * REQUIRE `params` with exactly the extracted segment names; static paths
227
- * REJECT it (`?: never` — may be absent, may never be present, which under
228
- * exactOptionalPropertyTypes holds even for non-fresh objects).
225
+ * Conditional on the path shape: dynamic paths REQUIRE `params` with exactly
226
+ * the extracted segment names; static paths REJECT it (`?: never` — may be
227
+ * absent, may never be present, which under exactOptionalPropertyTypes holds
228
+ * even for non-fresh objects).
229
229
  */
230
230
  export type RouteConfig<Path extends string, PC extends ParamsConfig<Path>, SC extends SearchSlot> = [PathParamNames<Path>] extends [never] ? {
231
231
  readonly params?: never;
@@ -235,7 +235,7 @@ export type RouteConfig<Path extends string, PC extends ParamsConfig<Path>, SC e
235
235
  readonly search?: SC;
236
236
  };
237
237
  /**
238
- * Full page-props contract (RL6): the type a page annotates its props with.
238
+ * Full page-props contract: the type a page annotates its props with.
239
239
  * Next's `PageProps` is structurally assignable, and both members are
240
240
  * promise-only so the annotation survives Next 15.5's generated page check
241
241
  * (see {@link MaybePromise}). Deliberately NOT Next's generated `PageProps`
@@ -243,16 +243,16 @@ export type RouteConfig<Path extends string, PC extends ParamsConfig<Path>, SC e
243
243
  */
244
244
  export interface RouteProps extends ParamsProps, SearchProps {
245
245
  }
246
- /** What `parse`/`safeParse` ACCEPT (RL6): {@link RouteProps} plus sync props. */
246
+ /** What `parse`/`safeParse` ACCEPT: {@link RouteProps} plus sync props. */
247
247
  export interface RoutePropsInput extends ParamsPropsInput, SearchPropsInput {
248
248
  }
249
- /** Which router a route belongs to (PR3) — the value of the `~router` brand. */
249
+ /** Which router a route belongs to — the value of the `~router` brand. */
250
250
  export type RouterKind = "app" | "pages";
251
251
  /**
252
- * Status-discriminated result shape (RL6, design-06 PR12 — unified with the
253
- * pages hooks' `RouterResult`, which extends this union by one `pending`
254
- * member): `if (result.status === "error")` narrows both arms, and both
255
- * routers' results destructure identically.
252
+ * Status-discriminated result shape, unified with the pages hooks'
253
+ * `RouterResult` (which extends this union by one `pending` member):
254
+ * `if (result.status === "error")` narrows both arms, and both routers'
255
+ * results destructure identically.
256
256
  */
257
257
  export type SafeResult<T> = {
258
258
  data: T;
@@ -262,7 +262,7 @@ export type SafeResult<T> = {
262
262
  status: "error";
263
263
  };
264
264
  /**
265
- * Structural props contract for the search half (RL6). The wire record
265
+ * Structural props contract for the search half. The wire record
266
266
  * shape is the same as the params side's, hence the shared source type.
267
267
  */
268
268
  export interface SearchProps {
@@ -274,19 +274,19 @@ export interface SearchPropsInput {
274
274
  }
275
275
  /**
276
276
  * Distributes a path literal into the union of its `/`-separated segment
277
- * literals. Malformed bracket tokens fall through as static text — no
278
- * type-level path linting (RL3); tokenizePath is the runtime backstop.
277
+ * literals. Malformed bracket tokens fall through as static text — there is
278
+ * no type-level path linting; tokenizePath is the runtime backstop.
279
279
  */
280
280
  export type Segments<S extends string> = S extends `${infer Head}/${infer Rest}` ? Segments<Head> | Segments<Rest> : S;
281
281
  /**
282
- * Names of single `[name]` segments (RL3). Conditional order is load-bearing
282
+ * Names of single `[name]` segments. Conditional order is load-bearing
283
283
  * and mirrors tokenizePath: both catch-all forms must be excluded first or
284
284
  * `[...slug]` would extract as a single param named `"...slug"`.
285
285
  */
286
286
  export type SingleParamNames<Path extends string> = Segments<Path> extends infer S extends string ? S extends `[[...${string}]]` ? never : S extends `[...${string}]` ? never : S extends `[${infer Name}]` ? NonEmptyName<Name> : never : never;
287
287
  /**
288
288
  * Union of the registry members that are actually PRESENT — `never` when
289
- * neither router has generated routes. The input to SH3's combined-union
289
+ * neither router has generated routes. The input to the combined-union
290
290
  * fallback rule; see {@link RegisteredStaticRoutePaths}.
291
291
  */
292
292
  type PresentRegisteredPaths = (ParamourRegister extends {
@@ -295,28 +295,28 @@ type PresentRegisteredPaths = (ParamourRegister extends {
295
295
  pagesRoutes: infer P extends string;
296
296
  } ? P : never);
297
297
  /**
298
- * Filters a path union to its static members (SH2): any `[` marks a dynamic
298
+ * Filters a path union to its static members: any `[` marks a dynamic
299
299
  * segment. `string` passes through (it doesn't extend the bracket template),
300
300
  * which is exactly what keeps the world-A fallback intact. Note reachability
301
- * ≠ staticness (SH7): `/docs/[[...slug]]` serves `/docs`, but it carries a
302
- * codec and decode expectations, so it is excluded here.
301
+ * ≠ staticness: `/docs/[[...slug]]` serves `/docs`, but it carries a codec
302
+ * and decode expectations, so it is excluded here.
303
303
  */
304
304
  type StaticPathsOf<P extends string> = P extends `${string}[${string}` ? never : P;
305
305
  /**
306
- * Defines an App Router route: the URL-shaped path literal (RL2) plus its
307
- * param/search codec configs. Validates the literal eagerly (RL1 —
308
- * fail-fast at config definition time, same stance as eager `.default()`
309
- * serialization). The router is a *declaration*, not an inference (PR7):
306
+ * Defines an App Router route: the URL-shaped path literal plus its
307
+ * param/search codec configs. Validates the literal eagerly — fail-fast at
308
+ * config definition time, the same stance as eager `.default()`
309
+ * serialization. The router is a *declaration*, not an inference:
310
310
  * pre-codegen the registry cannot distinguish routers, so an inferred brand
311
311
  * would silently degrade in world A — the split constructor is what keeps
312
312
  * the brand intact there.
313
313
  */
314
314
  export declare function defineAppRoute<Path extends RegisteredAppRoutePaths & string, const PC extends ParamsConfig<Path> = ParamsConfig<Path>, const SC extends SearchSlot = Record<never, never>>(path: Path, config: RouteConfig<Path, PC, SC>): AppRoute<Path, PC, SC>;
315
315
  /**
316
- * Defines a Pages Router route (PR7 — neither router is the default; see
316
+ * Defines a Pages Router route — neither router is the default; see
317
317
  * {@link defineAppRoute} for why the constructor is split rather than
318
- * inferred). Same eager literal validation (RL1); the parse surface is the
319
- * sync context pair (PR10).
318
+ * inferred. Same eager literal validation; the parse surface is the sync
319
+ * context pair.
320
320
  */
321
321
  export declare function definePagesRoute<Path extends RegisteredPagesRoutePaths & string, const PC extends ParamsConfig<Path> = ParamsConfig<Path>, const SC extends Readonly<Partial<Record<PathParamNames<Path>, never>>> & SearchSlot = Record<never, never>>(path: Path, config: RouteConfig<Path, PC, SC>): PagesRoute<Path, PC, SC>;
322
322
  export {};