@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
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Walk a pages dir and return the sorted union of URL-shaped route paths —
3
- * exactly the strings `definePagesRoute` accepts (PR4). A route is any file
4
- * whose extension is in `pageExtensions`, mapped by its path relative to the
5
- * dir; `index.<ext>` maps to its directory. Two files resolving to one URL
6
- * path — folder/file spelling (`blog.tsx` + `blog/index.tsx`) or extension
7
- * twins (`about.tsx` + `about.jsx`) — throw a {@link RouteCollisionError},
8
- * never dedupe: both are Next's own build errors (PR9).
3
+ * exactly the strings `definePagesRoute` accepts. A route is any file whose
4
+ * extension is in `pageExtensions`, mapped by its path relative to the dir;
5
+ * `index.<ext>` maps to its directory. Two files resolving to one URL path —
6
+ * folder/file spelling (`blog.tsx` + `blog/index.tsx`) or extension twins
7
+ * (`about.tsx` + `about.jsx`) — throw a {@link RouteCollisionError}, never
8
+ * dedupe: both are Next's own build errors.
9
9
  */
10
10
  export declare function scanPagesRoutes(pagesDir: string, pageExtensions?: readonly string[]): string[];
@@ -3,18 +3,18 @@ import { join } from "node:path";
3
3
  import { assertNoStructuralCollisions, RouteCollisionError, } from "./collisions.js";
4
4
  import { DEFAULT_PAGE_EXTENSIONS, resolvesToFile } from "./scan-app.js";
5
5
  /**
6
- * Pages Router scanner (PR4). Deliberately a separate walker from
7
- * `scan-app.ts` — the rule sets barely overlap (routes live on FILES here,
8
- * and none of TR2's skip rules apply), and a shared walker would have to
9
- * take its rule set as a parameter to be worth having (PR8).
6
+ * Pages Router scanner. Deliberately a separate walker from `scan-app.ts` —
7
+ * the rule sets barely overlap (routes live on FILES here, and none of the
8
+ * app scanner's skip rules apply), and a shared walker would have to take
9
+ * its rule set as a parameter to be worth having.
10
10
  */
11
11
  /**
12
- * Names special to Next at the TOP level of the pages dir only (PR4):
12
+ * Names special to Next at the TOP level of the pages dir only:
13
13
  * `_app`/`_document`/`_error` are framework files, `404`/`500` are error
14
14
  * pages, not navigation targets — `href("/404")` should not type-check.
15
15
  * Nested twins (`pages/blog/404.tsx`) are ordinary pages and route.
16
- * Every other `_`-prefixed file routes too (spike 1: co-location under
17
- * `pages/` was requested, vercel/next.js#8454, and never implemented).
16
+ * Every other `_`-prefixed file routes too — co-location under `pages/` was
17
+ * requested (vercel/next.js#8454) and never implemented.
18
18
  */
19
19
  const TOP_LEVEL_EXCLUDED = new Set([
20
20
  "404",
@@ -25,22 +25,22 @@ const TOP_LEVEL_EXCLUDED = new Set([
25
25
  ]);
26
26
  /**
27
27
  * Walk a pages dir and return the sorted union of URL-shaped route paths —
28
- * exactly the strings `definePagesRoute` accepts (PR4). A route is any file
29
- * whose extension is in `pageExtensions`, mapped by its path relative to the
30
- * dir; `index.<ext>` maps to its directory. Two files resolving to one URL
31
- * path — folder/file spelling (`blog.tsx` + `blog/index.tsx`) or extension
32
- * twins (`about.tsx` + `about.jsx`) — throw a {@link RouteCollisionError},
33
- * never dedupe: both are Next's own build errors (PR9).
28
+ * exactly the strings `definePagesRoute` accepts. A route is any file whose
29
+ * extension is in `pageExtensions`, mapped by its path relative to the dir;
30
+ * `index.<ext>` maps to its directory. Two files resolving to one URL path —
31
+ * folder/file spelling (`blog.tsx` + `blog/index.tsx`) or extension twins
32
+ * (`about.tsx` + `about.jsx`) — throw a {@link RouteCollisionError}, never
33
+ * dedupe: both are Next's own build errors.
34
34
  */
35
35
  export function scanPagesRoutes(pagesDir, pageExtensions = DEFAULT_PAGE_EXTENSIONS) {
36
36
  // Path → the fs path (relative to pagesDir) that produced it, so a
37
37
  // collision can name both files.
38
38
  const out = new Map();
39
39
  walk(pagesDir, [], pageExtensions, out, true);
40
- // Code-unit sort, never localeCompare — locale independence feeds TR3's
40
+ // Code-unit sort, never localeCompare — locale independence feeds the
41
41
  // byte-identical-on-every-OS guarantee.
42
42
  const paths = [...out.keys()].sort();
43
- // PR9 structural collisions (different slug names, optional-catch-all
43
+ // Structural collisions (different slug names, optional-catch-all
44
44
  // specificity) — non-equal strings the Map above cannot catch.
45
45
  assertNoStructuralCollisions(paths.map((path) => ({ path, router: "pages" })));
46
46
  return paths;
@@ -62,10 +62,11 @@ function walk(dir, urlSegments, pageExtensions, out, isTopLevel) {
62
62
  const name = entry.name;
63
63
  // A real file, or a symlink whose target is a file: Next resolves and
64
64
  // serves symlinked page files, so a file symlink routes exactly like a
65
- // real file (Bug 4, TR2 shared posture). Directory symlinks fall through
66
- // to the directory guard below and stay not-followed.
65
+ // real file (Bug 4, the same posture the app scanner takes). Directory
66
+ // symlinks fall through to the directory guard below and stay
67
+ // not-followed.
67
68
  if (resolvesToFile(entry, dir)) {
68
- // Declaration files match `.ts` but are never pages (PR11 §1).
69
+ // Declaration files match `.ts` but are never pages.
69
70
  if (name.endsWith(".d.ts"))
70
71
  continue;
71
72
  const ext = matchExtension(name, pageExtensions);
@@ -75,26 +76,27 @@ function walk(dir, urlSegments, pageExtensions, out, isTopLevel) {
75
76
  if (isTopLevel && TOP_LEVEL_EXCLUDED.has(base))
76
77
  continue;
77
78
  // `index.<ext>` maps to its directory; everything else — dynamic
78
- // segments included — is a path segment of its own (PR4).
79
+ // segments included — is a path segment of its own.
79
80
  const segments = base === "index" ? urlSegments : [...urlSegments, base];
80
81
  const path = segments.length === 0 ? "/" : `/${segments.join("/")}`;
81
82
  const file = [...urlSegments, name].join("/");
82
83
  const existing = out.get(path);
83
84
  if (existing !== undefined) {
84
- throw new RouteCollisionError(`pages route collision at "${path}": ${existing} and ${file} resolve to the same path (PR9)`);
85
+ throw new RouteCollisionError(`pages route collision at "${path}": ${existing} and ${file} resolve to the same path`);
85
86
  }
86
87
  out.set(path, file);
87
88
  continue;
88
89
  }
89
- // Symlinked directories are deliberately not followed (TR2 v1 stance,
90
- // shared posture): `resolvesToFile` returned false and `isDirectory()` is
91
- // false for the link Dirent, so the subtree is skipped here.
90
+ // Symlinked directories are deliberately not followed (the v1 stance,
91
+ // shared with the app scanner): `resolvesToFile` returned false and
92
+ // `isDirectory()` is false for the link Dirent, so the subtree is
93
+ // skipped here.
92
94
  if (!entry.isDirectory())
93
95
  continue;
94
96
  // `pages/api/**` is excluded — top level only, so `pages/foo/api/bar.tsx`
95
- // routes (API-route typing is deferred to v1.x, PR4/§14). NO app-style
96
- // skip rules beyond this: `(group)`, `@slot`, `(.)x`, and `_`-prefixed
97
- // dirs are ordinary literal segments in the Pages Router (PR4).
97
+ // routes (API-route typing is deferred to v1.x). NO app-style skip rules
98
+ // beyond this: `(group)`, `@slot`, `(.)x`, and `_`-prefixed dirs are
99
+ // ordinary literal segments in the Pages Router.
98
100
  if (isTopLevel && name === "api")
99
101
  continue;
100
102
  walk(join(dir, name), [...urlSegments, name], pageExtensions, out, false);
package/dist/scan.d.ts CHANGED
@@ -1,20 +1,23 @@
1
1
  /**
2
- * The thin orchestrator over the two scanners (PR8): joint directory
3
- * discovery, delegation, and the cross-router collision checks (PR9).
2
+ * The thin orchestrator over the two scanners: joint directory discovery,
3
+ * delegation, and the cross-router collision checks.
4
+ */
5
+ /**
6
+ * The two route dirs of a project; either may be absent — hybrid app/pages
7
+ * projects are supported, as are app-only and pages-only ones.
4
8
  */
5
- /** The two route dirs of a project; either may be absent (PR1 hybrid). */
6
9
  export interface RouteDirs {
7
10
  appDir?: string | undefined;
8
11
  pagesDir?: string | undefined;
9
12
  }
10
- /** Result of {@link scanRoutes} — the input shape of the PR9 artifact. */
13
+ /** Result of {@link scanRoutes} — the input shape of the artifact. */
11
14
  export interface ScanRoutesResult {
12
15
  appRoutes: string[];
13
16
  pagesRoutes: string[];
14
17
  }
15
18
  /**
16
- * Joint route-dir discovery (spike-2 ruling). Next's documented rule is one
17
- * decision, not two probes: `src/app` AND `src/pages` are both ignored
19
+ * Joint route-dir discovery. Next's documented rule is one decision, not
20
+ * two probes: `src/app` AND `src/pages` are both ignored
18
21
  * whenever `app/` OR `pages/` exists at the project root. An ignored src dir
19
22
  * that contains page files is a hard config error — Next silently serves
20
23
  * none of those pages (and has shipped bugs in the mixed case,
@@ -22,10 +25,10 @@ export interface ScanRoutesResult {
22
25
  */
23
26
  export declare function resolveRouteDirs(projectRoot: string, pageExtensions?: readonly string[]): RouteDirs;
24
27
  /**
25
- * Scan whichever route dirs exist and return both route unions (PR1). After
26
- * each scanner's own intra-router checks, two cross-router passes run (PR9):
27
- * a path in BOTH unions is Next's "Conflicting app and page file" build
28
- * error, and the structural pass re-runs over the merged, labeled union so
28
+ * Scan whichever route dirs exist and return both route unions. After each
29
+ * scanner's own intra-router checks, two cross-router passes run: a path in
30
+ * BOTH unions is Next's "Conflicting app and page file" build error, and the
31
+ * structural pass re-runs over the merged, labeled union so
29
32
  * shared-prefix slug conflicts and cross-router optional-catch-all
30
33
  * specificity are caught too.
31
34
  */
package/dist/scan.js CHANGED
@@ -4,8 +4,8 @@ import { assertNoStructuralCollisions, RouteCollisionError, } from "./collisions
4
4
  import { DEFAULT_PAGE_EXTENSIONS, scanAppRoutes } from "./scan-app.js";
5
5
  import { scanPagesRoutes } from "./scan-pages.js";
6
6
  /**
7
- * Joint route-dir discovery (spike-2 ruling). Next's documented rule is one
8
- * decision, not two probes: `src/app` AND `src/pages` are both ignored
7
+ * Joint route-dir discovery. Next's documented rule is one decision, not
8
+ * two probes: `src/app` AND `src/pages` are both ignored
9
9
  * whenever `app/` OR `pages/` exists at the project root. An ignored src dir
10
10
  * that contains page files is a hard config error — Next silently serves
11
11
  * none of those pages (and has shipped bugs in the mixed case,
@@ -47,10 +47,10 @@ export function resolveRouteDirs(projectRoot, pageExtensions = DEFAULT_PAGE_EXTE
47
47
  return { appDir: rootApp, pagesDir: rootPages };
48
48
  }
49
49
  /**
50
- * Scan whichever route dirs exist and return both route unions (PR1). After
51
- * each scanner's own intra-router checks, two cross-router passes run (PR9):
52
- * a path in BOTH unions is Next's "Conflicting app and page file" build
53
- * error, and the structural pass re-runs over the merged, labeled union so
50
+ * Scan whichever route dirs exist and return both route unions. After each
51
+ * scanner's own intra-router checks, two cross-router passes run: a path in
52
+ * BOTH unions is Next's "Conflicting app and page file" build error, and the
53
+ * structural pass re-runs over the merged, labeled union so
54
54
  * shared-prefix slug conflicts and cross-router optional-catch-all
55
55
  * specificity are caught too.
56
56
  */
@@ -62,7 +62,7 @@ export function scanRoutes(dirs, pageExtensions = DEFAULT_PAGE_EXTENSIONS) {
62
62
  const appSet = new Set(appRoutes);
63
63
  const shared = pagesRoutes.filter((path) => appSet.has(path));
64
64
  if (shared.length > 0) {
65
- throw new RouteCollisionError(`route collision between app/ and pages/: ${shared.map((path) => `"${path}"`).join(", ")} — Next fails the build on conflicting app and page files (PR9)`);
65
+ throw new RouteCollisionError(`route collision between app/ and pages/: ${shared.map((path) => `"${path}"`).join(", ")} — Next fails the build on conflicting app and page files`);
66
66
  }
67
67
  assertNoStructuralCollisions([
68
68
  ...appRoutes.map((path) => ({ path, router: "app" })),
package/dist/select.d.ts CHANGED
@@ -1,63 +1,63 @@
1
1
  import type { AnyRoute, ParamsSource, SafeResult } from "paramour";
2
2
  /**
3
- * Shared internals of the read hooks' selector surface (design-07): the
4
- * raw-slice stabilization layer (SEL4) and the selector layer (SEL2/SEL3).
5
- * Deliberately NO `"use client"` directive: app.ts (which carries one) and
6
- * pages.ts (which must not carry one, design-06 PR2) both import from here,
7
- * and the directive belongs on the entry modules, not a shared leaf.
3
+ * Shared internals of the read hooks' selector surface: the raw-slice
4
+ * stabilization layer and the selector layer. Deliberately NO `"use client"`
5
+ * directive: app.ts (which carries one) and pages.ts (which must not carry
6
+ * one) both import from here, and the directive belongs on the entry
7
+ * modules, not a shared leaf.
8
8
  *
9
- * Both layers are `useRef` caches mutated during render (SEL8) — the
10
- * Redux/TanStack selector pattern, and the one sanctioned departure from the
11
- * hooks' pure-`useMemo` discipline: result equality needs memory across
12
- * renders, which no pure memo can provide. Every cache is cleared BEFORE a
13
- * compute that can throw, so a throwing decode or selector never strands a
14
- * stale entry.
9
+ * Both layers are `useRef` caches mutated during render — the Redux/TanStack
10
+ * selector pattern, and the one sanctioned departure from the hooks'
11
+ * pure-`useMemo` discipline: result equality needs memory across renders,
12
+ * which no pure memo can provide. Every cache is cleared BEFORE a compute
13
+ * that can throw, so a throwing decode or selector never strands a stale
14
+ * entry.
15
15
  */
16
16
  /**
17
- * Options bag accepted by every read hook (design-07 SEL1).
17
+ * Options bag accepted by every read hook.
18
18
  */
19
19
  export interface SelectOptions<T, U> {
20
20
  /**
21
- * Result-equality mode for the selected value (SEL3): `Object.is` by
22
- * default — free and correct for primitive selections — with one-level
23
- * `"shallow"` as the opt-in for tuple/object selections.
21
+ * Result-equality mode for the selected value: `Object.is` by default —
22
+ * free and correct for primitive selections — with one-level `"shallow"`
23
+ * as the opt-in for tuple/object selections.
24
24
  */
25
25
  readonly equality?: "shallow";
26
26
  /**
27
- * Pure projection of the decoded value; runs only on the success arm
28
- * (SEL2). Identity is never compared, so inline arrows are fine — and when
29
- * the underlying result is reference-stable the selector is NOT re-run
30
- * (SEL6), so it must not read changing outside state. A throw propagates to
31
- * the nearest error boundary (SEL5): a selector bug is a code bug, never
32
- * the `SafeResult` error arm, which is reserved for URL data problems.
27
+ * Pure projection of the decoded value; runs only on the success arm.
28
+ * Identity is never compared, so inline arrows are fine — and when the
29
+ * underlying result is reference-stable the selector is NOT re-run, so it
30
+ * must not read changing outside state. A throw propagates to the nearest
31
+ * error boundary: a selector bug is a code bug, never the `SafeResult`
32
+ * error arm, which is reserved for URL data problems.
33
33
  */
34
34
  readonly select: (value: T) => U;
35
35
  }
36
36
  /**
37
- * Fingerprint of the pages hooks' pre-`isReady` state (design-06 PR5). Every
38
- * real fingerprint is a `JSON.stringify`'d array (starts with `[`), so this
39
- * can never collide with one.
37
+ * Fingerprint of the pages hooks' pre-`isReady` state. Every real
38
+ * fingerprint is a `JSON.stringify`'d array (starts with `[`), so this can
39
+ * never collide with one.
40
40
  */
41
41
  export declare const PENDING_FINGERPRINT = "pending";
42
42
  /**
43
- * Raw slice of a params source (SEL4): the route's dynamic segment names'
44
- * raw values, from the define-time `~segments` token cache. Unknown keys —
43
+ * Raw slice of a params source: the route's dynamic segment names' raw
44
+ * values, from the define-time `~segments` token cache. Unknown keys —
45
45
  * e.g. a parallel route's params in the same `useParams()` bag — never bust
46
46
  * the fingerprint, because the decode never reads them.
47
47
  */
48
48
  export declare function paramsFingerprint(route: AnyRoute, source: ParamsSource): string;
49
49
  /**
50
- * Raw slice of a pages `router.query` bag for the search half (SEL4). A
51
- * codec-map route reads exactly its declared keys (query junk and the
52
- * route's own path params are invisible to the decode, PR9 disjointness); a
50
+ * Raw slice of a pages `router.query` bag for the search half. A codec-map
51
+ * route reads exactly its declared keys (query junk and the route's own path
52
+ * params are invisible to the decode — the two namespaces are disjoint); a
53
53
  * `rawSearch` route has no enumerable declared-key set — the schema sees
54
- * every key except the route's path params (design-06 PR5 subtraction), so
55
- * exactly that slice is fingerprinted, in sorted-key order for record-order
56
- * independence.
54
+ * every key except the route's path params (by subtracting them from the
55
+ * bag), so exactly that slice is fingerprinted, in sorted-key order for
56
+ * record-order independence.
57
57
  */
58
58
  export declare function queryFingerprint(route: AnyRoute, query: ParamsSource): string;
59
59
  /**
60
- * Raw slice of an app `useSearchParams()` source (SEL4): the declared keys'
60
+ * Raw slice of an app `useSearchParams()` source: the declared keys'
61
61
  * `[key, value]` pairs in wire order — order is load-bearing for repeated
62
62
  * keys (array codecs decode in wire order, P5/S5), and iterating the live
63
63
  * pairs preserves the relative order of declared entries while `?utm_*`
@@ -66,8 +66,8 @@ export declare function queryFingerprint(route: AnyRoute, query: ParamsSource):
66
66
  */
67
67
  export declare function searchParamsFingerprint(route: AnyRoute, source: URLSearchParams): string;
68
68
  /**
69
- * The selector layer for the safe hooks (SEL2): projects the success arm and
70
- * reference-stabilizes the projected WRAPPER by result equality (SEL3) — a
69
+ * The selector layer for the safe hooks: projects the success arm and
70
+ * reference-stabilizes the projected WRAPPER by result equality — a
71
71
  * stable `data` inside a fresh wrapper would still churn every consumer.
72
72
  * Error and pending arms pass through untouched; they are already
73
73
  * reference-stabilized by {@link useStableResult}'s raw-slice layer.
@@ -79,16 +79,16 @@ export declare function useSelectedResult<T, U>(result: SafeResult<T> | {
79
79
  status: "pending";
80
80
  };
81
81
  /**
82
- * {@link useSelectedResult}'s twin for the `*OrThrow` hooks (SEL2): same
83
- * layering, no wrapper — the hook's return IS the (selected) value.
82
+ * {@link useSelectedResult}'s twin for the `*OrThrow` hooks: same layering,
83
+ * no wrapper — the hook's return IS the (selected) value.
84
84
  */
85
85
  export declare function useSelectedValue<T, U>(value: T, options: SelectOptions<T, U> | undefined): T | U;
86
86
  /**
87
- * The raw-slice stabilization layer (SEL4): while `route` and `fingerprint`
88
- * are unchanged from the previous render, the previous result — success OR
87
+ * The raw-slice stabilization layer: while `route` and `fingerprint` are
88
+ * unchanged from the previous render, the previous result — success OR
89
89
  * error arm — is returned without recomputing, so a fresh `useSearchParams()`
90
90
  * / `query` object whose DECLARED slice is unchanged (`?utm_source=` churn)
91
91
  * costs neither a decode nor anyone's referential equality. This replaces
92
- * the pre-design-07 "memo keyed on Next's object reference" behavior.
92
+ * the earlier "memo keyed on Next's object reference" behavior.
93
93
  */
94
94
  export declare function useStableResult<T>(route: AnyRoute, fingerprint: string, compute: () => T): T;
package/dist/select.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { useRef } from "react";
2
2
  /**
3
- * Fingerprint of the pages hooks' pre-`isReady` state (design-06 PR5). Every
4
- * real fingerprint is a `JSON.stringify`'d array (starts with `[`), so this
5
- * can never collide with one.
3
+ * Fingerprint of the pages hooks' pre-`isReady` state. Every real
4
+ * fingerprint is a `JSON.stringify`'d array (starts with `[`), so this can
5
+ * never collide with one.
6
6
  */
7
7
  export const PENDING_FINGERPRINT = "pending";
8
8
  /**
9
- * Raw slice of a params source (SEL4): the route's dynamic segment names'
10
- * raw values, from the define-time `~segments` token cache. Unknown keys —
9
+ * Raw slice of a params source: the route's dynamic segment names' raw
10
+ * values, from the define-time `~segments` token cache. Unknown keys —
11
11
  * e.g. a parallel route's params in the same `useParams()` bag — never bust
12
12
  * the fingerprint, because the decode never reads them.
13
13
  */
@@ -15,13 +15,13 @@ export function paramsFingerprint(route, source) {
15
15
  return recordFingerprint(dynamicSegmentNames(route), source);
16
16
  }
17
17
  /**
18
- * Raw slice of a pages `router.query` bag for the search half (SEL4). A
19
- * codec-map route reads exactly its declared keys (query junk and the
20
- * route's own path params are invisible to the decode, PR9 disjointness); a
18
+ * Raw slice of a pages `router.query` bag for the search half. A codec-map
19
+ * route reads exactly its declared keys (query junk and the route's own path
20
+ * params are invisible to the decode — the two namespaces are disjoint); a
21
21
  * `rawSearch` route has no enumerable declared-key set — the schema sees
22
- * every key except the route's path params (design-06 PR5 subtraction), so
23
- * exactly that slice is fingerprinted, in sorted-key order for record-order
24
- * independence.
22
+ * every key except the route's path params (by subtracting them from the
23
+ * bag), so exactly that slice is fingerprinted, in sorted-key order for
24
+ * record-order independence.
25
25
  */
26
26
  export function queryFingerprint(route, query) {
27
27
  const declared = declaredSearchKeys(route);
@@ -34,7 +34,7 @@ export function queryFingerprint(route, query) {
34
34
  return recordFingerprint(keys, query);
35
35
  }
36
36
  /**
37
- * Raw slice of an app `useSearchParams()` source (SEL4): the declared keys'
37
+ * Raw slice of an app `useSearchParams()` source: the declared keys'
38
38
  * `[key, value]` pairs in wire order — order is load-bearing for repeated
39
39
  * keys (array codecs decode in wire order, P5/S5), and iterating the live
40
40
  * pairs preserves the relative order of declared entries while `?utm_*`
@@ -62,15 +62,15 @@ export function useSelectedResult(result, options) {
62
62
  return result;
63
63
  const previous = cache.current;
64
64
  if (previous !== null && Object.is(previous.input, result.data)) {
65
- // Reference-stable input ⇒ equal output by selector purity (SEL6); the
66
- // selector is deliberately not re-run.
65
+ // Reference-stable input ⇒ equal output by selector purity; the selector
66
+ // is deliberately not re-run.
67
67
  return previous.wrapped;
68
68
  }
69
- const selected = options.select(result.data); // a throw propagates (SEL5)
69
+ const selected = options.select(result.data); // a throw propagates
70
70
  if (previous !== null &&
71
71
  selectedEquals(options.equality, previous.wrapped.data, selected)) {
72
- // Same selection out of a new decode: keep the previous wrapper (SEL2)
73
- // and re-key the cache so the next render takes the reference fast path.
72
+ // Same selection out of a new decode: keep the previous wrapper and
73
+ // re-key the cache so the next render takes the reference fast path.
74
74
  previous.input = result.data;
75
75
  return previous.wrapped;
76
76
  }
@@ -82,8 +82,8 @@ export function useSelectedResult(result, options) {
82
82
  return wrapped;
83
83
  }
84
84
  /**
85
- * {@link useSelectedResult}'s twin for the `*OrThrow` hooks (SEL2): same
86
- * layering, no wrapper — the hook's return IS the (selected) value.
85
+ * {@link useSelectedResult}'s twin for the `*OrThrow` hooks: same layering,
86
+ * no wrapper — the hook's return IS the (selected) value.
87
87
  */
88
88
  export function useSelectedValue(value, options) {
89
89
  const cache = useRef(null);
@@ -91,9 +91,9 @@ export function useSelectedValue(value, options) {
91
91
  return value;
92
92
  const previous = cache.current;
93
93
  if (previous !== null && Object.is(previous.input, value)) {
94
- return previous.selected; // SEL6: selector purity, not re-run
94
+ return previous.selected; // selector purity: not re-run
95
95
  }
96
- const selected = options.select(value); // a throw propagates (SEL5)
96
+ const selected = options.select(value); // a throw propagates
97
97
  if (previous !== null &&
98
98
  selectedEquals(options.equality, previous.selected, selected)) {
99
99
  previous.input = value;
@@ -103,12 +103,12 @@ export function useSelectedValue(value, options) {
103
103
  return selected;
104
104
  }
105
105
  /**
106
- * The raw-slice stabilization layer (SEL4): while `route` and `fingerprint`
107
- * are unchanged from the previous render, the previous result — success OR
106
+ * The raw-slice stabilization layer: while `route` and `fingerprint` are
107
+ * unchanged from the previous render, the previous result — success OR
108
108
  * error arm — is returned without recomputing, so a fresh `useSearchParams()`
109
109
  * / `query` object whose DECLARED slice is unchanged (`?utm_source=` churn)
110
110
  * costs neither a decode nor anyone's referential equality. This replaces
111
- * the pre-design-07 "memo keyed on Next's object reference" behavior.
111
+ * the earlier "memo keyed on Next's object reference" behavior.
112
112
  */
113
113
  export function useStableResult(route, fingerprint, compute) {
114
114
  const cache = useRef(null);
@@ -120,7 +120,7 @@ export function useStableResult(route, fingerprint, compute) {
120
120
  throw cached.outcome.thrown;
121
121
  return cached.outcome.value;
122
122
  }
123
- // Cleared BEFORE computing (SEL8): no half-computed state may survive a
123
+ // Cleared BEFORE computing: no half-computed state may survive a
124
124
  // throw, and a NEW fingerprint always recomputes — an error boundary
125
125
  // reset after the URL is fixed can never be served a stale entry.
126
126
  cache.current = null;
@@ -152,7 +152,7 @@ export function useStableResult(route, fingerprint, compute) {
152
152
  * Declared search keys of a route's `~search` slot, or `null` for a
153
153
  * `rawSearch` route (whose schema owns every key, so no declared subset
154
154
  * exists). The `~kind` marker is unambiguous against a codec map, which
155
- * never carries a top-level `~`-prefixed key (design-04 SS2).
155
+ * never carries a top-level `~`-prefixed key (SS2).
156
156
  */
157
157
  function declaredSearchKeys(route) {
158
158
  const config = route["~search"];
@@ -185,16 +185,16 @@ function recordFingerprint(keys, source) {
185
185
  Object.hasOwn(source, key) ? (source[key] ?? null) : null,
186
186
  ]));
187
187
  }
188
- /** SEL3: `Object.is`, widened one level by the `"shallow"` opt-in. */
188
+ /** Result equality: `Object.is`, widened one level by `"shallow"`. */
189
189
  function selectedEquals(equality, a, b) {
190
190
  if (Object.is(a, b))
191
191
  return true;
192
192
  return equality === "shallow" && shallowEqual(a, b);
193
193
  }
194
194
  /**
195
- * One-level equality for the `"shallow"` opt-in (SEL3): arrays element-wise,
196
- * plain objects by own enumerable keys — `Object.is` at each leaf, nothing
197
- * recursive (deep comparison in a render path is a non-goal, design-07).
195
+ * One-level equality for the `"shallow"` opt-in: arrays element-wise, plain
196
+ * objects by own enumerable keys — `Object.is` at each leaf, nothing
197
+ * recursive (deep comparison in a render path is a non-goal).
198
198
  */
199
199
  function shallowEqual(a, b) {
200
200
  if (typeof a !== "object" ||
@@ -0,0 +1,11 @@
1
+ import type { DoctorCheck } from "../doctor/checks.js";
2
+ /**
3
+ * Doctor's skills battery. Intent comes from manifest presence — doctor is
4
+ * passive and must not nag a project that never opted into skills — unlike
5
+ * `skills --check`, whose intent is the detection result (it is added to CI
6
+ * deliberately). Findings are warn-level, never fail: stale agent guidance
7
+ * degrades agent output but breaks nothing at build or runtime, and a fail
8
+ * here would flip doctor's exit to 1 in every consumer the day after every
9
+ * paramour release. The CI-fatal surface is `paramour skills --check`.
10
+ */
11
+ export declare function skillsDoctorChecks(projectRoot: string): DoctorCheck[];
@@ -0,0 +1,71 @@
1
+ import { message } from "../cli-io.js";
2
+ import { loadPackagedSkill } from "./packaged.js";
3
+ import { auditTarget, FILE_STATUS_DETAIL, isOutdated } from "./sync.js";
4
+ import { detectTargets } from "./targets.js";
5
+ /**
6
+ * Doctor's skills battery. Intent comes from manifest presence — doctor is
7
+ * passive and must not nag a project that never opted into skills — unlike
8
+ * `skills --check`, whose intent is the detection result (it is added to CI
9
+ * deliberately). Findings are warn-level, never fail: stale agent guidance
10
+ * degrades agent output but breaks nothing at build or runtime, and a fail
11
+ * here would flip doctor's exit to 1 in every consumer the day after every
12
+ * paramour release. The CI-fatal surface is `paramour skills --check`.
13
+ */
14
+ export function skillsDoctorChecks(projectRoot) {
15
+ try {
16
+ const detected = detectTargets(projectRoot);
17
+ if (detected.length === 0) {
18
+ return [
19
+ {
20
+ label: "skills: no agent tooling detected — skipped",
21
+ status: "pass",
22
+ },
23
+ ];
24
+ }
25
+ const packaged = loadPackagedSkill();
26
+ const audits = detected
27
+ .map((target) => auditTarget(target, packaged))
28
+ .filter((audit) => audit.manifest !== undefined);
29
+ if (audits.length === 0) {
30
+ // Pass with an advisory label (check 1's "defaults in effect"
31
+ // precedent): declining skills is a legitimate steady state.
32
+ return [
33
+ {
34
+ label: `skills: not installed (\`paramour skills\` installs agent skills for ${detected
35
+ .map((target) => `${target.rel.split("/")[0] ?? ""}/`)
36
+ .join(", ")})`,
37
+ status: "pass",
38
+ },
39
+ ];
40
+ }
41
+ return audits.map((audit) => {
42
+ const detail = audit.files
43
+ .map((file) => {
44
+ const note = FILE_STATUS_DETAIL[file.status];
45
+ return note === undefined ? undefined : `${file.relPath}: ${note}`;
46
+ })
47
+ .filter((line) => line !== undefined);
48
+ if (detail.length === 0) {
49
+ return {
50
+ label: `skills: ${audit.target.rel} is up to date`,
51
+ status: "pass",
52
+ };
53
+ }
54
+ const outdated = audit.files.some((file) => isOutdated(file.status));
55
+ return {
56
+ detail,
57
+ label: `skills: ${audit.target.rel} ${outdated ? "is stale" : "has local edits"}`,
58
+ status: "warn",
59
+ };
60
+ });
61
+ }
62
+ catch (error) {
63
+ return [
64
+ {
65
+ detail: [message(error)],
66
+ label: "skills: could not audit installed skills",
67
+ status: "warn",
68
+ },
69
+ ];
70
+ }
71
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The sidecar stamp `paramour skills` writes next to every installed copy.
3
+ * It lives inside the installed skill directory (not at the project root) so
4
+ * deleting a tool directory is a clean uninstall — no orphaned record keeps
5
+ * reporting the skill as missing after a deliberate removal. Skill loaders
6
+ * ignore dotfiles under progressive disclosure, so the installed skill files
7
+ * themselves stay byte-identical to the published ones.
8
+ */
9
+ export interface SkillManifest {
10
+ /** POSIX-relative path → `sha256:<hex>` of the content last synced. */
11
+ files: Record<string, string>;
12
+ skill: "paramour";
13
+ /**
14
+ * The `@paramour-js/next` version that last wrote this manifest.
15
+ * Informational only: staleness truth is always hash comparison, so a
16
+ * version bump without content changes never reads as stale.
17
+ */
18
+ version: string;
19
+ }
20
+ export declare const MANIFEST_FILENAME = ".paramour-skills.json";
21
+ /**
22
+ * CRLF-normalized sha256. Normalizing before hashing makes every status
23
+ * computation immune to a consumer repo's git `autocrlf` rewriting the
24
+ * installed markdown on checkout — a line-ending flip is not a content edit.
25
+ */
26
+ export declare function hashContent(text: string): string;
27
+ /**
28
+ * Read a target's manifest; `undefined` when absent or malformed. A corrupt
29
+ * manifest degrades to "no provenance": byte-identical files still audit as
30
+ * fresh and anything else audits as locally modified, which the installer
31
+ * refuses to overwrite without `--force` — the safe direction. A manifest
32
+ * containing any file key that could escape the skill directory is treated
33
+ * as corrupt the same way, since those keys become deletion paths in sync.
34
+ */
35
+ export declare function readSkillManifest(skillDir: string): SkillManifest | undefined;
36
+ /**
37
+ * Deterministic serialization: sorted file keys, two-space indent, LF,
38
+ * trailing newline, no timestamps — same doctrine as the generated artifact,
39
+ * so a re-run that changes nothing writes nothing.
40
+ */
41
+ export declare function renderManifest(manifest: SkillManifest): string;