@paramour-js/next 0.4.0 → 0.5.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.
Files changed (74) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.ts +48 -49
  3. package/dist/app.js +11 -12
  4. package/dist/cli-args.d.ts +4 -4
  5. package/dist/cli-args.js +4 -4
  6. package/dist/cli-inputs.d.ts +6 -7
  7. package/dist/cli-inputs.js +6 -7
  8. package/dist/cli.js +1 -1
  9. package/dist/collisions.d.ts +8 -8
  10. package/dist/collisions.js +9 -9
  11. package/dist/commands/doctor.d.ts +12 -0
  12. package/dist/commands/doctor.js +12 -7
  13. package/dist/commands/generate.d.ts +55 -7
  14. package/dist/commands/generate.js +29 -22
  15. package/dist/commands/init.d.ts +40 -0
  16. package/dist/commands/init.js +111 -19
  17. package/dist/commands/list.d.ts +21 -0
  18. package/dist/commands/list.js +12 -7
  19. package/dist/commands/skills.d.ts +45 -0
  20. package/dist/commands/skills.js +222 -0
  21. package/dist/config.d.ts +17 -10
  22. package/dist/config.js +17 -4
  23. package/dist/devtools-seam.d.ts +19 -19
  24. package/dist/devtools-seam.js +3 -3
  25. package/dist/doctor/checks.js +7 -3
  26. package/dist/emit.d.ts +12 -12
  27. package/dist/emit.js +13 -13
  28. package/dist/generate.d.ts +14 -14
  29. package/dist/generate.js +11 -11
  30. package/dist/init/agents-md.d.ts +32 -0
  31. package/dist/init/agents-md.js +78 -0
  32. package/dist/init/scaffold.js +54 -0
  33. package/dist/list/discover-route-defs.d.ts +4 -4
  34. package/dist/list/discover-route-defs.js +4 -4
  35. package/dist/lock.d.ts +8 -9
  36. package/dist/lock.js +11 -12
  37. package/dist/navigation-adapter.d.ts +17 -18
  38. package/dist/navigation-adapter.js +4 -4
  39. package/dist/observe.d.ts +14 -14
  40. package/dist/observe.js +4 -4
  41. package/dist/pages.d.ts +30 -31
  42. package/dist/pages.js +10 -10
  43. package/dist/run-cli.d.ts +3 -1
  44. package/dist/run-cli.js +7 -3
  45. package/dist/scan-app.d.ts +10 -10
  46. package/dist/scan-app.js +30 -30
  47. package/dist/scan-pages.d.ts +6 -6
  48. package/dist/scan-pages.js +28 -26
  49. package/dist/scan.d.ts +13 -10
  50. package/dist/scan.js +7 -7
  51. package/dist/select.d.ts +40 -40
  52. package/dist/select.js +30 -30
  53. package/dist/skills/doctor.d.ts +11 -0
  54. package/dist/skills/doctor.js +71 -0
  55. package/dist/skills/manifest.d.ts +41 -0
  56. package/dist/skills/manifest.js +96 -0
  57. package/dist/skills/packaged.d.ts +21 -0
  58. package/dist/skills/packaged.js +32 -0
  59. package/dist/skills/sync.d.ts +84 -0
  60. package/dist/skills/sync.js +136 -0
  61. package/dist/skills/targets.d.ts +29 -0
  62. package/dist/skills/targets.js +73 -0
  63. package/dist/testing.d.ts +28 -32
  64. package/dist/testing.js +15 -15
  65. package/dist/watch.d.ts +12 -12
  66. package/dist/watch.js +13 -13
  67. package/dist/with-typed-routes.d.ts +11 -10
  68. package/dist/with-typed-routes.js +42 -39
  69. package/package.json +4 -3
  70. package/skills/paramour/SKILL.md +43 -0
  71. package/skills/paramour/references/authoring.md +113 -0
  72. package/skills/paramour/references/migration.md +141 -0
  73. package/skills/paramour/references/reference.md +96 -0
  74. package/skills/paramour/references/setup.md +139 -0
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The Next.js integration for [paramour](https://paramour.dev):
4
4
  `withTypedRoutes` (build-time registry generation and drift checking),
5
5
  typed client hooks for both routers, and the `paramour` CLI
6
- (`generate` / `check` / `init` / `list` / `doctor`).
6
+ (`generate` / `check` / `init` / `list` / `doctor` / `skills`).
7
7
 
8
8
  ```sh
9
9
  pnpm add paramour @paramour-js/next
package/dist/app.d.ts CHANGED
@@ -2,67 +2,66 @@ import { type AnyAppRoute, type InferRouteParams, type SafeResult, type SearchOu
2
2
  import { type SelectOptions } from "./select.js";
3
3
  export type { SelectOptions } from "./select.js";
4
4
  /**
5
- * Client hooks (DESIGN §9, design-07). Each layers over Next's
6
- * `useSearchParams()` / `useParams()` — App-Router params are synchronous on
7
- * the client, so there is no loading state, no `useEffect`/`useState`, and
8
- * the result is SSR-consistent. Two layers per hook (design-07):
5
+ * Client hooks. Each layers over Next's `useSearchParams()` / `useParams()`
6
+ * — App-Router params are synchronous on the client, so there is no loading
7
+ * state, no `useEffect`/`useState`, and the result is SSR-consistent. Two
8
+ * layers per hook:
9
9
  *
10
- * - Raw-slice stabilization (SEL4): the decode is keyed on the DECLARED
11
- * slice of the raw source, not on Next's object reference — a URL change
12
- * that only touches keys the route doesn't own (`?utm_source=` churn)
13
- * returns the previous result by identity, without re-decoding. Next still
14
- * re-renders every subscriber on any URL change (it owns the subscription
15
- * — SEL7: selectors stabilize slices, they cannot skip renders); this
16
- * layer makes that render cheap and downstream-invisible.
17
- * - Selection (SEL1–SEL3): every hook takes an optional `{ select }` that
18
- * projects the decoded value, with result-equality checking (`Object.is`,
10
+ * - Raw-slice stabilization: the decode is keyed on the DECLARED slice of
11
+ * the raw source, not on Next's object reference — a URL change that only
12
+ * touches keys the route doesn't own (`?utm_source=` churn) returns the
13
+ * previous result by identity, without re-decoding. Next still re-renders
14
+ * every subscriber on any URL change (it owns the subscription; selectors
15
+ * stabilize slices, they cannot skip renders); this layer makes that
16
+ * render cheap and downstream-invisible.
17
+ * - Selection: every hook takes an optional `{ select }` that projects the
18
+ * decoded value, with result-equality checking (`Object.is`,
19
19
  * `equality: "shallow"` opt-in) so an unchanged selection keeps its
20
20
  * previous reference when OTHER params change.
21
21
  *
22
- * Both layers are render-phase ref caches (SEL8) — the one sanctioned
23
- * departure from the pure-`useMemo` discipline these hooks previously held.
22
+ * Both layers are render-phase ref caches — the one sanctioned departure
23
+ * from the pure-`useMemo` discipline these hooks previously held.
24
24
  *
25
25
  * Two surfaces per half, mirroring core's server `parse` vs `safeParse`:
26
26
  * - `useSearch` / `useRouteParams` return the `SafeResult` union
27
- * (discriminated on `status`, PR12) — a user editing the URL never crashes
28
- * the component. The selector runs on the success arm only (SEL2).
27
+ * (discriminated on `status`) — a user editing the URL never crashes the
28
+ * component. The selector runs on the success arm only.
29
29
  * - `useSearchOrThrow` / `useRouteParamsOrThrow` throw the decode error in
30
30
  * render, to the nearest client error boundary.
31
31
  *
32
32
  * Both read the route's blessed-internal `~search` / `~params` via the core
33
- * decoders (design-03 RL6 — `@paramour/next` is a sanctioned consumer).
33
+ * decoders — `@paramour/next` is a sanctioned consumer of those internals.
34
34
  *
35
- * Every hook is gated to `AnyAppRoute` (design-06 PR3): a pages-branded route
36
- * at one of these call sites is a compile error, not a runtime surprise —
37
- * these hooks read Next's App-Router navigation hooks, whose pages twin has
38
- * different state cardinality (`@paramour-js/next/pages`).
35
+ * Every hook is gated to `AnyAppRoute`: a pages-branded route at one of
36
+ * these call sites is a compile error, not a runtime surprise — these hooks
37
+ * read Next's App-Router navigation hooks, whose pages twin has different
38
+ * state cardinality (`@paramour-js/next/pages`).
39
39
  *
40
- * Devtools instrumentation (design-12): each hook reports through the shared
41
- * emitter in observe.ts — `observe` from inside the `useStableResult`
42
- * compute callback, which runs exactly on a `(route, fingerprint)` cache
43
- * miss, so the SEL4 fingerprint layer IS the decode-change dedup (DT4;
44
- * StrictMode's dev double render reuses the ref cache and cannot
45
- * double-emit), and `refresh` after the stable result returns, re-emitting
46
- * the CACHED result when the pathname moved under an unchanged decode so
47
- * the seam's `navigate`/`pathname` never go stale (DT8). Observations carry
48
- * the full pre-`select` result (DT12), and the `OrThrow` hooks report the
49
- * error observation BEFORE rethrowing — only render-phase can, since an
50
- * effect never runs for a throwing render. Every emit sits behind
51
- * `process.env.NODE_ENV !== "production"`, which bundlers constant-fold and
52
- * erase along with the seam module (DT6); the spec each hook hands the
53
- * emitter is built behind the same literal guard, so prod allocates
54
- * nothing. `useRouter` and `usePathname` are called unconditionally in
55
- * every hook (rules of hooks — a build-constant-guarded call would make
56
- * hook order differ between dev and prod bundles); their cost is a
57
- * referentially-stable context read each, and only the dev-only spec
58
- * captures them. The `navigate` capability receives the panel's SEARCH
40
+ * Devtools instrumentation: each hook reports through the shared emitter in
41
+ * observe.ts — `observe` from inside the `useStableResult` compute callback,
42
+ * which runs exactly on a `(route, fingerprint)` cache miss, so the
43
+ * fingerprint layer IS the decode-change dedup (StrictMode's dev double
44
+ * render reuses the ref cache and cannot double-emit), and `refresh` after
45
+ * the stable result returns, re-emitting the CACHED result when the pathname
46
+ * moved under an unchanged decode so the seam's `navigate`/`pathname` never
47
+ * go stale. Observations carry the full pre-`select` result, and the
48
+ * `OrThrow` hooks report the error observation BEFORE rethrowing — only
49
+ * render-phase can, since an effect never runs for a throwing render. Every
50
+ * emit sits behind `process.env.NODE_ENV !== "production"`, which bundlers
51
+ * constant-fold and erase along with the seam module; the spec each hook
52
+ * hands the emitter is built behind the same literal guard, so prod
53
+ * allocates nothing. `useRouter` and `usePathname` are called
54
+ * unconditionally in every hook (rules of hooks — a build-constant-guarded
55
+ * call would make hook order differ between dev and prod bundles); their
56
+ * cost is a referentially-stable context read each, and only the dev-only
57
+ * spec captures them. The `navigate` capability receives the panel's SEARCH
59
58
  * STRING only and resolves it against `usePathname()` — basePath-/locale-
60
- * relative, exactly what `router.replace` expects back (DT8; the live hash
61
- * is preserved at call time).
59
+ * relative, exactly what `router.replace` expects back (the live hash is
60
+ * preserved at call time).
62
61
  */
63
62
  /**
64
- * Decoded route params as a `SafeResult` (discriminated on `status`, PR12),
65
- * optionally projected through `options.select` (design-07 SEL1/SEL2).
63
+ * Decoded route params as a `SafeResult` (discriminated on `status`),
64
+ * optionally projected through `options.select`.
66
65
  *
67
66
  * `useParams()` returns `null` outside an App-Router tree — including the
68
67
  * initial render of every pages-router page in a hybrid app — so a `null`
@@ -76,7 +75,7 @@ export declare function useRouteParams<R extends AnyAppRoute, U>(route: R, optio
76
75
  /**
77
76
  * Decoded route params, or a thrown {@link ParamsDecodeError} (→ nearest
78
77
  * client error boundary) on a malformed URL. Optionally projected through
79
- * `options.select` (design-07 SEL1/SEL2).
78
+ * `options.select`.
80
79
  *
81
80
  * A `null` `useParams()` (outside an App-Router tree, e.g. a hybrid app's
82
81
  * pages-router initial render) degrades to `{}` so required params throw the
@@ -85,15 +84,15 @@ export declare function useRouteParams<R extends AnyAppRoute, U>(route: R, optio
85
84
  export declare function useRouteParamsOrThrow<R extends AnyAppRoute>(route: R): InferRouteParams<R>;
86
85
  export declare function useRouteParamsOrThrow<R extends AnyAppRoute, U>(route: R, options: SelectOptions<InferRouteParams<R>, U>): U;
87
86
  /**
88
- * Decoded search params as a `SafeResult` (discriminated on `status`, PR12),
89
- * optionally projected through `options.select` (design-07 SEL1/SEL2).
87
+ * Decoded search params as a `SafeResult` (discriminated on `status`),
88
+ * optionally projected through `options.select`.
90
89
  */
91
90
  export declare function useSearch<R extends AnyAppRoute>(route: R): SafeResult<SearchOutputOf<R["~search"]>>;
92
91
  export declare function useSearch<R extends AnyAppRoute, U>(route: R, options: SelectOptions<SearchOutputOf<R["~search"]>, U>): SafeResult<U>;
93
92
  /**
94
93
  * Decoded search params, or a thrown {@link SearchDecodeError} (→ nearest
95
94
  * client error boundary) on a malformed URL. Optionally projected through
96
- * `options.select` (design-07 SEL1/SEL2).
95
+ * `options.select`.
97
96
  */
98
97
  export declare function useSearchOrThrow<R extends AnyAppRoute>(route: R): SearchOutputOf<R["~search"]>;
99
98
  export declare function useSearchOrThrow<R extends AnyAppRoute, U>(route: R, options: SelectOptions<SearchOutputOf<R["~search"]>, U>): U;
package/dist/app.js CHANGED
@@ -54,7 +54,7 @@ export function useRouteParamsOrThrow(route, options) {
54
54
  };
55
55
  const value = useStableResult(route, paramsFingerprint(route, params), () => {
56
56
  // The duplicated decode call across the prod/dev branches is the price
57
- // of literal-zero prod cost — the bundler keeps exactly one branch (DT6).
57
+ // of literal-zero prod cost — the bundler keeps exactly one branch.
58
58
  if (process.env.NODE_ENV === "production") {
59
59
  return decodeParams(route, params);
60
60
  }
@@ -66,7 +66,7 @@ export function useRouteParamsOrThrow(route, options) {
66
66
  return data;
67
67
  }
68
68
  catch (error) {
69
- // Report BEFORE the throw reaches the error boundary (DT4). Only the
69
+ // Report BEFORE the throw reaches the error boundary. Only the
70
70
  // decode-error class is observed — foreign errors are not URL facts
71
71
  // the panel explains, matching safeDecode*'s taxonomy.
72
72
  if (error instanceof ParamsDecodeError && spec !== undefined) {
@@ -127,7 +127,7 @@ export function useSearchOrThrow(route, options) {
127
127
  wire: () => searchWireSnapshot(searchParams),
128
128
  };
129
129
  const value = useStableResult(route, searchParamsFingerprint(route, searchParams),
130
- // decodeSearch is keyed on SearchOutputOf (design-04 SS6) — the correct
130
+ // decodeSearch is keyed on SearchOutputOf (SS6) — the correct
131
131
  // public type — but AnyAppRoute erases its SC to `any`, so for a still-
132
132
  // generic R the call's SearchOutputOf<R["~search"]> reduces to `unknown`
133
133
  // on the value side while staying deferred on the annotation side. The
@@ -135,22 +135,21 @@ export function useSearchOrThrow(route, options) {
135
135
  // rawSearch route now infers its schema output here, not a garbage
136
136
  // {~kind, ~schema} shape. The cast appears in both branches below — the
137
137
  // prod/dev split (and its duplicated decode call) is the price of
138
- // literal-zero prod cost; the bundler keeps exactly one branch (DT6).
138
+ // literal-zero prod cost; the bundler keeps exactly one branch.
139
139
  () => {
140
140
  if (process.env.NODE_ENV === "production") {
141
- return decodeSearch(route["~search"], searchParams);
141
+ return decodeSearch(route["~search"], searchParams, route.path);
142
142
  }
143
143
  try {
144
- const data = decodeSearch(route["~search"], searchParams);
144
+ const data = decodeSearch(route["~search"], searchParams, route.path);
145
145
  if (spec !== undefined) {
146
146
  emitter.observe(spec, { data, status: "success" });
147
147
  }
148
148
  return data;
149
149
  }
150
150
  catch (error) {
151
- // Report BEFORE the throw reaches the error boundary (DT4); only
152
- // the decode-error class is observed, matching safeDecode*'s
153
- // taxonomy.
151
+ // Report BEFORE the throw reaches the error boundary; only the
152
+ // decode-error class is observed, matching safeDecode*'s taxonomy.
154
153
  if (error instanceof SearchDecodeError && spec !== undefined) {
155
154
  emitter.observe(spec, { error, status: "error" });
156
155
  }
@@ -163,8 +162,8 @@ export function useSearchOrThrow(route, options) {
163
162
  return useSelectedValue(value, options);
164
163
  }
165
164
  /**
166
- * Real-Next fallback for the adapter seam (design-16 TA3): the /testing
167
- * provider overrides these reads through {@link AppNavigationContext}; with
165
+ * Real-Next fallback for the adapter seam: the /testing provider overrides
166
+ * these reads through {@link AppNavigationContext}; with
168
167
  * no provider mounted the context's `null` default resolves here, so
169
168
  * production behavior (and this module's `next/navigation`-only bundle
170
169
  * graph, per dist.test.ts) is unchanged.
@@ -179,7 +178,7 @@ const realAppAdapter = {
179
178
  * Every hook resolves the adapter ONCE at its top and calls the adapter's
180
179
  * reads unconditionally, exactly where the direct Next calls previously sat
181
180
  * — hook call order is identical across renders and across provider
182
- * presence (TA4).
181
+ * presence.
183
182
  */
184
183
  function useAppNavigation() {
185
184
  return useContext(AppNavigationContext) ?? realAppAdapter;
@@ -15,10 +15,10 @@ type ParsedValues<T extends ParseArgsOptionsConfig> = ReturnType<typeof parseArg
15
15
  options: T;
16
16
  }>>["values"];
17
17
  /**
18
- * The shared command prologue (TR7): parse flags, print usage on a parse
19
- * error (exit 2) or `--help` (exit 0), and reject positionals — no command
20
- * takes one. Callers branch on `"exit" in result`; anything past the
21
- * prologue (mode merging, flag exclusivity) stays per-command.
18
+ * The shared command prologue: parse flags, print usage on a parse error
19
+ * (exit 2) or `--help` (exit 0), and reject positionals — no command takes
20
+ * one. Callers branch on `"exit" in result`; anything past the prologue
21
+ * (mode merging, flag exclusivity) stays per-command.
22
22
  */
23
23
  export declare function parseCommandFlags<const T extends HelpOption & ParseArgsOptionsConfig>(argv: readonly string[], options: T, usage: string, { stderr, stdout }: ResolvedIo): {
24
24
  exit: 0 | 2;
package/dist/cli-args.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { message } from "./cli-io.js";
3
3
  /**
4
- * The shared command prologue (TR7): parse flags, print usage on a parse
5
- * error (exit 2) or `--help` (exit 0), and reject positionals — no command
6
- * takes one. Callers branch on `"exit" in result`; anything past the
7
- * prologue (mode merging, flag exclusivity) stays per-command.
4
+ * The shared command prologue: parse flags, print usage on a parse error
5
+ * (exit 2) or `--help` (exit 0), and reject positionals — no command takes
6
+ * one. Callers branch on `"exit" in result`; anything past the prologue
7
+ * (mode merging, flag exclusivity) stays per-command.
8
8
  */
9
9
  export function parseCommandFlags(argv, options, usage, { stderr, stdout }) {
10
10
  let parsed;
@@ -18,13 +18,12 @@ export interface InputFlags {
18
18
  export declare class NoRouteDirsError extends Error {
19
19
  }
20
20
  /**
21
- * Precedence lives in exactly this function (TR7 / §7.2): flags → config
22
- * file → joint discovery (PR8). Paths resolve against the project root
23
- * (= cwd, where `next` itself would run). Discovery only runs for dirs not
24
- * explicitly given — passing both bypasses it (and its populated-ignored-dir
25
- * config error) entirely, which is the documented escape hatch. Only when
26
- * NEITHER dir exists is that an error (PR8): app-only and pages-only
27
- * projects are both fine.
21
+ * Precedence lives in exactly this function: flags → config file → joint
22
+ * discovery. Paths resolve against the project root (= cwd, where `next`
23
+ * itself would run). Discovery only runs for dirs not explicitly given —
24
+ * passing both bypasses it (and its populated-ignored-dir config error)
25
+ * entirely, which is the documented escape hatch. Only when NEITHER dir
26
+ * exists is that an error: app-only and pages-only projects are both fine.
28
27
  *
29
28
  * Commands that already loaded the config file (for fields beyond these,
30
29
  * e.g. `list`'s routeFiles) pass it as `preloaded` so jiti runs once.
@@ -12,13 +12,12 @@ import { resolveRouteDirs } from "./scan.js";
12
12
  export class NoRouteDirsError extends Error {
13
13
  }
14
14
  /**
15
- * Precedence lives in exactly this function (TR7 / §7.2): flags → config
16
- * file → joint discovery (PR8). Paths resolve against the project root
17
- * (= cwd, where `next` itself would run). Discovery only runs for dirs not
18
- * explicitly given — passing both bypasses it (and its populated-ignored-dir
19
- * config error) entirely, which is the documented escape hatch. Only when
20
- * NEITHER dir exists is that an error (PR8): app-only and pages-only
21
- * projects are both fine.
15
+ * Precedence lives in exactly this function: flags → config file → joint
16
+ * discovery. Paths resolve against the project root (= cwd, where `next`
17
+ * itself would run). Discovery only runs for dirs not explicitly given —
18
+ * passing both bypasses it (and its populated-ignored-dir config error)
19
+ * entirely, which is the documented escape hatch. Only when NEITHER dir
20
+ * exists is that an error: app-only and pages-only projects are both fine.
22
21
  *
23
22
  * Commands that already loaded the config file (for fields beyond these,
24
23
  * e.g. `list`'s routeFiles) pass it as `preloaded` so jiti runs once.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { runCli } from "./run-cli.js";
3
- // The bin entry (TR7): all logic lives in run-cli.ts so tests never execute
3
+ // The bin entry: all logic lives in run-cli.ts so tests never execute
4
4
  // this statement. exitCode, not exit() — pending stdio writes must flush.
5
5
  process.exitCode = await runCli(process.argv.slice(2));
@@ -1,21 +1,21 @@
1
- /** A scanned route path labeled with the router that produced it (PR9). */
1
+ /** A scanned route path labeled with the router that produced it. */
2
2
  export interface ScannedRoute {
3
3
  path: string;
4
4
  router: "app" | "pages";
5
5
  }
6
6
  /**
7
- * Route-collision failure mode (PR9): states Next itself refuses to build
8
- * have no valid artifact, so the scanners throw instead of emitting one.
9
- * Composition points map this error to their ruled exits — CLI exit 2,
10
- * `withTypedRoutes` throw during config evaluation, and a non-fatal loud
11
- * log under watch (the TR5 exception: a collision mid-`--watch` is usually
12
- * a file mid-move, so the last good artifact stays on disk).
7
+ * Route-collision failure mode: states Next itself refuses to build have no
8
+ * valid artifact, so the scanners throw instead of emitting one. Composition
9
+ * points map this error to their own exits — CLI exit 2, `withTypedRoutes`
10
+ * throw during config evaluation, and a non-fatal loud log under watch (a
11
+ * collision mid-`--watch` is usually a file mid-move, so the last good
12
+ * artifact stays on disk).
13
13
  */
14
14
  export declare class RouteCollisionError extends Error {
15
15
  name: string;
16
16
  }
17
17
  /**
18
- * PR9's structural collisions — same detection pass, non-equal strings. Two
18
+ * Structural collisions — same detection pass, non-equal strings. Two
19
19
  * states Next also refuses to build that plain string equality misses:
20
20
  *
21
21
  * - **Different slug names at one level**: `/x/[id]` + `/x/[slug]` — Next:
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Route-collision failure mode (PR9): states Next itself refuses to build
3
- * have no valid artifact, so the scanners throw instead of emitting one.
4
- * Composition points map this error to their ruled exits — CLI exit 2,
5
- * `withTypedRoutes` throw during config evaluation, and a non-fatal loud
6
- * log under watch (the TR5 exception: a collision mid-`--watch` is usually
7
- * a file mid-move, so the last good artifact stays on disk).
2
+ * Route-collision failure mode: states Next itself refuses to build have no
3
+ * valid artifact, so the scanners throw instead of emitting one. Composition
4
+ * points map this error to their own exits — CLI exit 2, `withTypedRoutes`
5
+ * throw during config evaluation, and a non-fatal loud log under watch (a
6
+ * collision mid-`--watch` is usually a file mid-move, so the last good
7
+ * artifact stays on disk).
8
8
  */
9
9
  export class RouteCollisionError extends Error {
10
10
  name = "RouteCollisionError";
@@ -22,7 +22,7 @@ const OPTIONAL_CATCH_ALL = /^\[\[\.\.\..+\]\]$/;
22
22
  */
23
23
  const DYNAMIC_SEGMENT = /^(?:\[\[\.\.\.(?<optional>.+)\]\]|\[\.\.\.(?<catchAll>.+)\]|\[(?<plain>[^[\]]+)\])$/;
24
24
  /**
25
- * PR9's structural collisions — same detection pass, non-equal strings. Two
25
+ * Structural collisions — same detection pass, non-equal strings. Two
26
26
  * states Next also refuses to build that plain string equality misses:
27
27
  *
28
28
  * - **Different slug names at one level**: `/x/[id]` + `/x/[slug]` — Next:
@@ -56,7 +56,7 @@ export function assertNoStructuralCollisions(routes) {
56
56
  const key = `${segments.slice(0, index).join("/")}#${String(index)}#${kind}`;
57
57
  const existing = dynamicAt.get(key);
58
58
  if (existing !== undefined && existing.segment !== segment) {
59
- throw new RouteCollisionError(`route collision: "${existing.path}" (${existing.router}) and "${route.path}" (${route.router}) declare conflicting dynamic segments (${existing.segment} vs ${segment}) at the same position — Next refuses different slug names for the same dynamic path (PR9)`);
59
+ throw new RouteCollisionError(`route collision: "${existing.path}" (${existing.router}) and "${route.path}" (${route.router}) declare conflicting dynamic segments (${existing.segment} vs ${segment}) at the same position — Next refuses different slug names for the same dynamic path`);
60
60
  }
61
61
  if (existing === undefined) {
62
62
  dynamicAt.set(key, { ...route, segment });
@@ -67,7 +67,7 @@ export function assertNoStructuralCollisions(routes) {
67
67
  const base = segments.length === 1 ? "/" : `/${segments.slice(0, -1).join("/")}`;
68
68
  const baseRoute = byPath.get(base);
69
69
  if (baseRoute !== undefined) {
70
- throw new RouteCollisionError(`route collision: "${route.path}" (${route.router}) also matches "${base}" (${baseRoute.router}) — an optional catch-all has the same specificity as its base path, which Next refuses to build (PR9)`);
70
+ throw new RouteCollisionError(`route collision: "${route.path}" (${route.router}) also matches "${base}" (${baseRoute.router}) — an optional catch-all has the same specificity as its base path, which Next refuses to build`);
71
71
  }
72
72
  }
73
73
  }
@@ -1,4 +1,16 @@
1
1
  import { type CliIo } from "../cli-io.js";
2
+ /** @internal `paramour doctor` flags — skill-drift test's source of truth. */
3
+ export declare const DOCTOR_OPTIONS: {
4
+ readonly help: {
5
+ readonly default: false;
6
+ readonly short: "h";
7
+ readonly type: "boolean";
8
+ };
9
+ readonly json: {
10
+ readonly default: false;
11
+ readonly type: "boolean";
12
+ };
13
+ };
2
14
  /**
3
15
  * @internal `paramour doctor` — a verification, so its exit codes follow
4
16
  * `check`'s class: 0 pass/warn, 1 any fail, 2 only when doctor itself
@@ -6,9 +6,9 @@ const USAGE = [
6
6
  "Usage: paramour doctor [options]",
7
7
  "",
8
8
  "Diagnose the project's paramour setup: config validity, artifact",
9
- "freshness, next.config wrapping, version alignment, tsconfig coverage,",
10
- "and route-definition discovery (which evaluates matched modules, like",
11
- "`paramour list`).",
9
+ "freshness, next.config wrapping, version alignment, installed agent",
10
+ "skills, tsconfig coverage, and route-definition discovery (which",
11
+ "evaluates matched modules, like `paramour list`).",
12
12
  "",
13
13
  "Exit codes: 0 all checks pass (warnings allowed), 1 any check fails.",
14
14
  "",
@@ -16,6 +16,11 @@ const USAGE = [
16
16
  " --help, -h show this help",
17
17
  " --json machine-readable output",
18
18
  ].join("\n");
19
+ /** @internal `paramour doctor` flags — skill-drift test's source of truth. */
20
+ export const DOCTOR_OPTIONS = {
21
+ help: { default: false, short: "h", type: "boolean" },
22
+ json: { default: false, type: "boolean" },
23
+ };
19
24
  /**
20
25
  * @internal `paramour doctor` — a verification, so its exit codes follow
21
26
  * `check`'s class: 0 pass/warn, 1 any fail, 2 only when doctor itself
@@ -23,10 +28,10 @@ const USAGE = [
23
28
  */
24
29
  export async function runDoctor(argv, io) {
25
30
  const { stderr, stdout } = resolveIo(io);
26
- const parsed = parseCommandFlags(argv, {
27
- help: { default: false, short: "h", type: "boolean" },
28
- json: { default: false, type: "boolean" },
29
- }, USAGE, { stderr, stdout });
31
+ const parsed = parseCommandFlags(argv, DOCTOR_OPTIONS, USAGE, {
32
+ stderr,
33
+ stdout,
34
+ });
30
35
  if ("exit" in parsed)
31
36
  return parsed.exit;
32
37
  const flags = parsed.values;
@@ -1,11 +1,59 @@
1
1
  import { type CliIo } from "../cli-io.js";
2
+ /** @internal `paramour check` flags — skill-drift test's source of truth. */
3
+ export declare const CHECK_OPTIONS: {
4
+ readonly "app-dir": {
5
+ readonly type: "string";
6
+ };
7
+ readonly help: {
8
+ readonly default: false;
9
+ readonly short: "h";
10
+ readonly type: "boolean";
11
+ };
12
+ readonly "out-file": {
13
+ readonly type: "string";
14
+ };
15
+ readonly "page-extensions": {
16
+ readonly type: "string";
17
+ };
18
+ readonly "pages-dir": {
19
+ readonly type: "string";
20
+ };
21
+ };
22
+ /** @internal `paramour generate` flags — skill-drift test's source of truth. */
23
+ export declare const GENERATE_OPTIONS: {
24
+ readonly check: {
25
+ readonly default: false;
26
+ readonly type: "boolean";
27
+ };
28
+ readonly watch: {
29
+ readonly default: false;
30
+ readonly type: "boolean";
31
+ };
32
+ readonly "app-dir": {
33
+ readonly type: "string";
34
+ };
35
+ readonly help: {
36
+ readonly default: false;
37
+ readonly short: "h";
38
+ readonly type: "boolean";
39
+ };
40
+ readonly "out-file": {
41
+ readonly type: "string";
42
+ };
43
+ readonly "page-extensions": {
44
+ readonly type: "string";
45
+ };
46
+ readonly "pages-dir": {
47
+ readonly type: "string";
48
+ };
49
+ };
2
50
  /**
3
- * @internal `paramour generate` and its `check` alias (TR7), in-process
4
- * testable: returns the exit code instead of exiting. Codes are grep-style
5
- * so CI can tell drift from breakage: 0 success, 1 check-drift ONLY, 2
6
- * usage/config/operational errors — route collisions included (PR9: Next
7
- * itself fails that build, so there is no artifact to emit). Unlike the
8
- * wrapper's never-load-bearing stance (§7.3), the CLI fails loudly —
9
- * running it is explicit user intent.
51
+ * @internal `paramour generate` and its `check` alias, in-process testable:
52
+ * returns the exit code instead of exiting. Codes are grep-style so CI can
53
+ * tell drift from breakage: 0 success, 1 check-drift ONLY, 2
54
+ * usage/config/operational errors — route collisions included (Next itself
55
+ * fails that build, so there is no artifact to emit). Unlike the wrapper,
56
+ * which is never load-bearing, the CLI fails loudly — running it is explicit
57
+ * user intent.
10
58
  */
11
59
  export declare function runGenerate(argv: readonly string[], io: CliIo, mode: "check" | "generate"): Promise<number>;
@@ -12,6 +12,14 @@ const SHARED_OPTIONS = {
12
12
  "page-extensions": { type: "string" },
13
13
  "pages-dir": { type: "string" },
14
14
  };
15
+ /** @internal `paramour check` flags — skill-drift test's source of truth. */
16
+ export const CHECK_OPTIONS = SHARED_OPTIONS;
17
+ /** @internal `paramour generate` flags — skill-drift test's source of truth. */
18
+ export const GENERATE_OPTIONS = {
19
+ ...SHARED_OPTIONS,
20
+ check: { default: false, type: "boolean" },
21
+ watch: { default: false, type: "boolean" },
22
+ };
15
23
  const SHARED_OPTION_LINES = [
16
24
  " --app-dir <dir> app directory (default: discovered app/ or src/app/)",
17
25
  " --help, -h show this help",
@@ -38,24 +46,23 @@ const CHECK_USAGE = [
38
46
  ...SHARED_OPTION_LINES,
39
47
  ].join("\n");
40
48
  /**
41
- * @internal `paramour generate` and its `check` alias (TR7), in-process
42
- * testable: returns the exit code instead of exiting. Codes are grep-style
43
- * so CI can tell drift from breakage: 0 success, 1 check-drift ONLY, 2
44
- * usage/config/operational errors — route collisions included (PR9: Next
45
- * itself fails that build, so there is no artifact to emit). Unlike the
46
- * wrapper's never-load-bearing stance (§7.3), the CLI fails loudly —
47
- * running it is explicit user intent.
49
+ * @internal `paramour generate` and its `check` alias, in-process testable:
50
+ * returns the exit code instead of exiting. Codes are grep-style so CI can
51
+ * tell drift from breakage: 0 success, 1 check-drift ONLY, 2
52
+ * usage/config/operational errors — route collisions included (Next itself
53
+ * fails that build, so there is no artifact to emit). Unlike the wrapper,
54
+ * which is never load-bearing, the CLI fails loudly — running it is explicit
55
+ * user intent.
48
56
  */
49
57
  export async function runGenerate(argv, io, mode) {
50
58
  const { stderr, stdout } = resolveIo(io);
51
59
  const usage = mode === "check" ? CHECK_USAGE : GENERATE_USAGE;
52
60
  let flags;
53
61
  if (mode === "generate") {
54
- const parsed = parseCommandFlags(argv, {
55
- ...SHARED_OPTIONS,
56
- check: { default: false, type: "boolean" },
57
- watch: { default: false, type: "boolean" },
58
- }, usage, { stderr, stdout });
62
+ const parsed = parseCommandFlags(argv, GENERATE_OPTIONS, usage, {
63
+ stderr,
64
+ stdout,
65
+ });
59
66
  if ("exit" in parsed)
60
67
  return parsed.exit;
61
68
  flags = parsed.values;
@@ -63,7 +70,7 @@ export async function runGenerate(argv, io, mode) {
63
70
  else {
64
71
  // `check` omits --check/--watch entirely: `paramour check --watch`
65
72
  // fails as an unknown option, which is the right message for it.
66
- const parsed = parseCommandFlags(argv, SHARED_OPTIONS, usage, {
73
+ const parsed = parseCommandFlags(argv, CHECK_OPTIONS, usage, {
67
74
  stderr,
68
75
  stdout,
69
76
  });
@@ -105,7 +112,7 @@ function describeRoutes(result) {
105
112
  ];
106
113
  return parts.length === 0 ? "0 routes" : parts.join(", ");
107
114
  }
108
- /** `--check` (TR7): exit 1 on any drift, including a missing artifact. */
115
+ /** `--check`: exit 1 on any drift, including a missing artifact. */
109
116
  function runCheck(inputs, stdout, stderr) {
110
117
  let result;
111
118
  try {
@@ -132,7 +139,7 @@ function runCheck(inputs, stdout, stderr) {
132
139
  stderr("Run `paramour generate` and commit the result.");
133
140
  return 1;
134
141
  }
135
- /** One-shot `paramour generate` (TR7). */
142
+ /** One-shot `paramour generate`. */
136
143
  function runOnce(inputs, stdout, stderr) {
137
144
  let result;
138
145
  try {
@@ -149,8 +156,8 @@ function runOnce(inputs, stdout, stderr) {
149
156
  return 0;
150
157
  }
151
158
  /**
152
- * `--watch` (TR7): TR5 watcher behind the TR6 lock, over both route dirs
153
- * (PR8). A declined lock exits 0 — another live watcher (usually `next dev`)
159
+ * `--watch`: the watcher behind the cross-process lock, over both route
160
+ * dirs. A declined lock exits 0 — another live watcher (usually `next dev`)
154
161
  * is the designed dedupe case, and the initial generation already ran.
155
162
  * Without an abort signal the returned promise stays pending; the process
156
163
  * lives via the FSWatcher refs and dies with the standard signal exits
@@ -172,7 +179,7 @@ function runWatch(inputs, projectRoot, io, stdout, stderr) {
172
179
  }
173
180
  catch (error) {
174
181
  // A corrupt lock location (e.g. a directory at the pidfile path) is an
175
- // operational error, not a crash: exit 2 like every other one (TR7).
182
+ // operational error, not a crash: exit 2 like every other one.
176
183
  stderr(`paramour: ${message(error)}`);
177
184
  return 2;
178
185
  }
@@ -196,13 +203,13 @@ function runWatch(inputs, projectRoot, io, stdout, stderr) {
196
203
  }
197
204
  catch (error) {
198
205
  if (error instanceof RouteCollisionError) {
199
- // PR9's watch exception: a collision mid-watch is usually a file
200
- // mid-move — log loudly every time, keep the last good artifact
201
- // on disk, keep running (TR5).
206
+ // The collision watch exception: a collision mid-watch is usually
207
+ // a file mid-move — log loudly every time, keep the last good
208
+ // artifact on disk, keep running.
202
209
  stderr(`paramour: ${message(error)}; keeping the last good artifact and watching for the fix`);
203
210
  return;
204
211
  }
205
- throw error; // routed to onError by the watcher (TR5 non-fatal)
212
+ throw error; // routed to onError by the watcher — non-fatal
206
213
  }
207
214
  },
208
215
  });