@node-boost/node-boost 0.1.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 (64) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +196 -0
  4. package/dist/cli.js +3000 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/index.d.ts +380 -0
  7. package/dist/index.js +2708 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +82 -0
  10. package/resources/react/architectures/component-composition/guideline.md +42 -0
  11. package/resources/react/architectures/component-composition/skill/SKILL.md +19 -0
  12. package/resources/react/architectures/custom-hooks/guideline.md +38 -0
  13. package/resources/react/architectures/custom-hooks/skill/SKILL.md +22 -0
  14. package/resources/react/architectures/data-access-layer/guideline.md +43 -0
  15. package/resources/react/architectures/data-access-layer/skill/SKILL.md +26 -0
  16. package/resources/react/architectures/error-loading-boundaries/guideline.md +36 -0
  17. package/resources/react/architectures/error-loading-boundaries/skill/SKILL.md +18 -0
  18. package/resources/react/architectures/feature-modules/guideline.md +34 -0
  19. package/resources/react/architectures/feature-modules/skill/SKILL.md +28 -0
  20. package/resources/react/architectures/feature-modules/variants/forbid.md +36 -0
  21. package/resources/react/architectures/feature-modules/variants/public-api.md +37 -0
  22. package/resources/react/architectures/modern-typescript/guideline.md +47 -0
  23. package/resources/react/architectures/modern-typescript/skill/SKILL.md +23 -0
  24. package/resources/react/architectures/secure-by-default/guideline.md +38 -0
  25. package/resources/react/architectures/secure-by-default/skill/SKILL.md +22 -0
  26. package/resources/react/architectures/server-first-components/guideline.md +34 -0
  27. package/resources/react/architectures/server-first-components/skill/SKILL.md +23 -0
  28. package/resources/react/architectures/state-management/guideline.md +34 -0
  29. package/resources/react/architectures/state-management/skill/SKILL.md +21 -0
  30. package/resources/react/architectures/styling-tailwind/guideline.md +33 -0
  31. package/resources/react/architectures/styling-tailwind/skill/SKILL.md +18 -0
  32. package/resources/react/architectures/testing-strategy/guideline.md +33 -0
  33. package/resources/react/architectures/testing-strategy/skill/SKILL.md +19 -0
  34. package/resources/react/architectures/typed-contracts/guideline.md +31 -0
  35. package/resources/react/architectures/typed-contracts/skill/SKILL.md +23 -0
  36. package/resources/react/architectures/ui-states/guideline.md +45 -0
  37. package/resources/react/architectures/ui-states/skill/SKILL.md +19 -0
  38. package/resources/react/guidelines/core.md +15 -0
  39. package/resources/react/guidelines/next/15.md +9 -0
  40. package/resources/react/guidelines/next/16.md +18 -0
  41. package/resources/react/guidelines/next/core.md +8 -0
  42. package/resources/react/guidelines/react/18.md +8 -0
  43. package/resources/react/guidelines/react/19.md +10 -0
  44. package/resources/react/guidelines/react/core.md +9 -0
  45. package/resources/react/guidelines/react-query/core.md +8 -0
  46. package/resources/react/guidelines/react-router/7.md +9 -0
  47. package/resources/react/guidelines/react-router/core.md +7 -0
  48. package/resources/react/guidelines/tailwindcss/3.md +8 -0
  49. package/resources/react/guidelines/tailwindcss/4.md +9 -0
  50. package/resources/react/guidelines/tailwindcss/core.md +8 -0
  51. package/resources/react/guidelines/testing/playwright.md +8 -0
  52. package/resources/react/guidelines/testing/vitest.md +8 -0
  53. package/resources/react/guidelines/typescript/core.md +7 -0
  54. package/resources/react/guidelines/vite/core.md +8 -0
  55. package/resources/react/guidelines/zod/3.md +8 -0
  56. package/resources/react/guidelines/zod/4.md +10 -0
  57. package/resources/react/guidelines/zod/core.md +7 -0
  58. package/resources/react/guidelines/zustand/core.md +8 -0
  59. package/resources/react/skills/next-development/SKILL.md +19 -0
  60. package/resources/react/skills/react-development/SKILL.md +18 -0
  61. package/resources/react/skills/spa-routing/SKILL.md +18 -0
  62. package/resources/react/skills/tailwindcss-development/SKILL.md +17 -0
  63. package/resources/react/skills/testing-frontend/SKILL.md +18 -0
  64. package/schema.json +216 -0
@@ -0,0 +1,38 @@
1
+ # Custom Hooks
2
+
3
+ Extract stateful logic into `useX` hooks so components stay declarative — thin controllers over a view.
4
+
5
+ ## When to extract
6
+
7
+ - The logic is used in two or more places, OR
8
+ - the component is unreadable (JSX buried under state/effects), OR
9
+ - the logic deserves its own test.
10
+
11
+ When none apply, don't: a hook wrapping a single `useState` is ceremony.
12
+
13
+ <code-snippet name="Thin component, logic in a hook" lang="tsx">
14
+ function InvoiceList() {
15
+ const { invoices, filter, setFilter, selected, toggleSelection } = useInvoiceSelection()
16
+ return <Table rows={invoices} filter={filter} onFilter={setFilter} onToggle={toggleSelection} />
17
+ }
18
+ </code-snippet>
19
+
20
+ ## Conventions
21
+
22
+ - A hook belongs to its feature (`features/<x>/hooks/`); generic ones go to `src/hooks/`.
23
+ - Return an object of named fields when returning more than two values — not a tuple.
24
+ - `useX` only if it calls other hooks. Stateless logic is a plain function in `lib/` — naming it `useX` is a lie.
25
+
26
+ ## React Compiler era (1.0 is stable)
27
+
28
+ Do **not** wrap everything in `useCallback`/`useMemo` "for performance" — the compiler memoizes automatically. Reach for them only when you need a referential guarantee (e.g. a value in a `useEffect` dependency array). Ritual memoization is legacy noise; do not generate it.
29
+
30
+ ## Effects are a last resort
31
+
32
+ Before writing `useEffect`, check (per react.dev "You Might Not Need an Effect"):
33
+
34
+ - Derived state? Compute it during render.
35
+ - Reaction to a user event? Handle it in the event handler.
36
+ - Data fetching? That belongs to the data layer / query hooks, not a hand-rolled effect.
37
+
38
+ Legitimate effects synchronize with external systems (subscriptions, DOM APIs, analytics) — little else.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: custom-hooks
3
+ description: Extract component logic into custom hooks with clean APIs, avoid ritual memoization and unnecessary effects.
4
+ ---
5
+
6
+ # Custom Hooks
7
+
8
+ ## When to use this skill
9
+
10
+ Use when a component's logic outgrows its JSX, when duplicating stateful logic, or when reviewing `useEffect`/`useCallback` usage.
11
+
12
+ ## Procedure
13
+
14
+ 1. Identify the cohesive slice of state + handlers; move it to `features/<x>/hooks/use-<name>.ts`.
15
+ 2. Return an object of named fields; keep the component consuming it declarative.
16
+ 3. Compose from existing hooks (query hooks, other custom hooks) instead of duplicating.
17
+ 4. Audit every `useEffect` you are about to write: derived state → compute in render; event reaction → handler; fetching → data layer. Keep effects only for external-system sync.
18
+ 5. Skip `useCallback`/`useMemo` unless a referential guarantee is required — React Compiler handles memoization.
19
+
20
+ ## Testing
21
+
22
+ Complex hook logic gets a `renderHook` test; simple glue is covered by the component's tests.
@@ -0,0 +1,43 @@
1
+ # Data Access Layer
2
+
3
+ Components never call `fetch` directly. All data access lives in a dedicated layer: server functions in `api/` directories and client-side query hooks.
4
+
5
+ ## Server side (Next)
6
+
7
+ <code-snippet name="Data layer function" lang="ts">
8
+ // features/invoices/api/get-invoices.ts
9
+ import "server-only"
10
+
11
+ export async function getInvoices(filter: InvoiceFilter) {
12
+ "use cache"
13
+ const res = await apiClient.get("/invoices", { query: filter })
14
+ return invoiceListSchema.parse(res) // validate at the boundary (typed-contracts)
15
+ }
16
+ </code-snippet>
17
+
18
+ Server Components call `getInvoices()`, not `fetch`. Auth headers, error handling, retries and cache tags live in one place. Mutations go through Server Actions (`"use server"`) that validate input and call the data layer.
19
+
20
+ ## Client side
21
+
22
+ Components consume query hooks, never raw fetch:
23
+
24
+ <code-snippet name="Query hook" lang="ts">
25
+ // features/invoices/hooks/use-invoices.ts
26
+ export function useInvoices(filter: InvoiceFilter) {
27
+ return useQuery({ queryKey: ["invoices", filter], queryFn: () => invoicesApi.list(filter) })
28
+ }
29
+ </code-snippet>
30
+
31
+ Generated API clients (orval, openapi-typescript) **are** the data layer — use them instead of hand-writing one.
32
+
33
+ ## Rules
34
+
35
+ - `NB-ARCH-005` (error): `fetch`/axios/ky in a client component outside the data layer and outside query hooks.
36
+ - `NB-ARCH-006` (warn): raw `fetch` in a Server Component outside the data layer. Fetching directly in RSC is the framework idiom, but a layer keeps caching, headers and validation consistent — prefer it.
37
+ - The layer's location is configurable via `audit.ruleOptions` (`dataLayerGlobs`, default `**/api/**`, `**/server/**`, `lib/api/**`, `route.ts`).
38
+
39
+ ## Anti-patterns
40
+
41
+ - `useEffect` + `useState` + `fetch` in a component — that is a hand-rolled, worse react-query.
42
+ - Copy-pasting auth headers per call site.
43
+ - Mutations bypassing Server Actions / the API client, losing validation and cache invalidation.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: data-access-layer
3
+ description: Add or modify data fetching and mutations through the data layer — server api/ functions, query hooks, and Server Actions — instead of inline fetch.
4
+ ---
5
+
6
+ # Data Access Layer
7
+
8
+ ## When to use this skill
9
+
10
+ Use whenever code needs to read or write remote data, or when fixing `NB-ARCH-005`/`NB-ARCH-006` findings.
11
+
12
+ ## Reading data
13
+
14
+ 1. Server (Next): add a function in the feature's `api/` directory: `import "server-only"`, call the shared client, validate the response with the schema, add `"use cache"` when cacheable. Call it from the Server Component.
15
+ 2. Client: add/extend a query hook (`useQuery`) in the feature's `hooks/`, delegating to the API client. Components call the hook.
16
+ 3. Project has a generated client (orval/openapi-typescript)? Use it — do not hand-write parallel fetchers.
17
+
18
+ ## Writing data
19
+
20
+ 1. Next: Server Action (`"use server"`) that validates input (zod) and calls the data layer; invalidate affected cache tags/queries.
21
+ 2. SPA: `useMutation` delegating to the API client, with `invalidateQueries` on success.
22
+
23
+ ## Fixing findings
24
+
25
+ - `NB-ARCH-005`: move the call into a query hook or `api/` function; the component receives data via props/hook.
26
+ - `NB-ARCH-006`: wrap the raw RSC fetch in a data-layer function (adds validation + caching in one place).
@@ -0,0 +1,36 @@
1
+ # Error and Loading Boundaries
2
+
3
+ Every App Router segment that fetches data needs designed boundaries: `loading.tsx` streams an instant skeleton, `error.tsx` scopes failures to the segment.
4
+
5
+ <code-snippet name="Segment with boundaries" lang="text">
6
+ app/
7
+ ├── error.tsx # root boundary — the global handler
8
+ └── invoices/
9
+ ├── page.tsx # async, awaits data
10
+ ├── loading.tsx # streamed IMMEDIATELY while page awaits
11
+ └── error.tsx # failure renders here, rest of the app lives
12
+ </code-snippet>
13
+
14
+ ## Rules
15
+
16
+ - `NB-ARCH-010` (warn): a segment whose `page.tsx` fetches data with no `loading.*` **and** no `error.*` anywhere up its branch. A root-level `error.tsx` satisfies the whole tree — this is not a mandate for twenty files.
17
+ - `error.tsx` **must be a Client Component**; it receives `error` and `reset()` for retry. Add `global-error.tsx` for failures in the root layout, `not-found.tsx` for 404 semantics.
18
+ - `loading.tsx` is an automatic Suspense boundary around the page. For slow islands inside a fast page, wrap the island alone:
19
+
20
+ <code-snippet name="Granular Suspense" lang="tsx">
21
+ <Suspense fallback={<ChartSkeleton />}>
22
+ <RevenueChart /> {/* slow await only here; rest streams instantly */}
23
+ </Suspense>
24
+ </code-snippet>
25
+
26
+ ## Why this is structural in Next 16
27
+
28
+ Partial Prerendering splits the page into a static shell plus dynamic holes — and the holes are defined by Suspense boundaries. No boundaries, no PPR.
29
+
30
+ ## Judgment calls
31
+
32
+ - Skeletons mirror the shape of the coming content (anti-layout-shift); a page rendered fully from cache may not need one.
33
+ - Skeleton fatigue is real: one skeleton per meaningful region, not five spinners racing.
34
+ - Boundary reset drops the subtree's state — fine, but know it.
35
+
36
+ Design of the fallback *content* (empty/error UX) lives in the ui-states guideline.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: error-loading-boundaries
3
+ description: Add loading.tsx / error.tsx / Suspense boundaries to App Router segments that fetch data.
4
+ ---
5
+
6
+ # Error and Loading Boundaries
7
+
8
+ ## When to use this skill
9
+
10
+ Use when creating a data-fetching page/segment in the App Router, or fixing `NB-ARCH-010`.
11
+
12
+ ## Procedure
13
+
14
+ 1. New segment fetching data → add `loading.tsx` (skeleton mirroring the content shape) and decide the `error.tsx` level: segment-local when the segment can fail independently, otherwise rely on a parent boundary.
15
+ 2. `error.tsx`: mark `"use client"`, render a short explanation + retry button calling `reset()`; report the error to monitoring if the project has it.
16
+ 3. Slow widget inside a fast page → wrap only that widget in `<Suspense fallback={<Skeleton />}>` instead of blocking the page.
17
+ 4. 404 semantics → `not-found.tsx` + `notFound()` from the data layer.
18
+ 5. Do not add skeletons to fully cached/static pages — verify the page is actually dynamic first.
@@ -0,0 +1,34 @@
1
+ # Feature Modules
2
+
3
+ Organize code by feature, not by file type. Each feature is a self-contained module with a public API.
4
+
5
+ ## Structure
6
+
7
+ <code-snippet name="Feature-first layout" lang="text">
8
+ src/
9
+ ├── features/
10
+ │ └── invoices/
11
+ │ ├── api/ # data access for this feature (see data-access-layer)
12
+ │ ├── components/
13
+ │ ├── hooks/
14
+ │ ├── types.ts
15
+ │ └── index.ts # PUBLIC API — the only entry point for other code
16
+ ├── components/ui/ # shared, domain-agnostic UI kit
17
+ ├── lib/ # shared utilities, api client
18
+ └── app/ # routes — compose features, contain no business logic
19
+ </code-snippet>
20
+
21
+ ## Rules
22
+
23
+ - Dependency direction is one-way: `shared → features → app`. A feature must never import from `app/**` (rule `NB-ARCH-002`, error). Routes compose features; features do not know about routing entry points.
24
+ - Keep `index.ts` small: export only what other features/routes genuinely need. Everything not exported is private.
25
+ - When two features need the same code, move it down to `lib/` or `components/ui/` — do not import it sideways.
26
+ - Do not introduce `features/` prematurely. A handful of screens does not need it; adopt this structure when a second feature starts bleeding into the first.
27
+
28
+ ## Anti-patterns
29
+
30
+ - Type-first folders at scale (`components/` with 200 files from every domain).
31
+ - Barrel files re-exporting everything — export the public surface only; large barrels hurt tree-shaking and build times.
32
+ - "Shared" modules importing from features (upward dependency).
33
+
34
+ For editor-time feedback on these boundaries, `eslint-plugin-boundaries` is a good optional companion; node-boost enforces the same rules in `audit`/`guard`.
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: feature-modules
3
+ description: Create and evolve feature modules with enforced boundaries — where new code goes, how features talk to each other, and when code moves to shared.
4
+ ---
5
+
6
+ # Feature Modules
7
+
8
+ ## When to use this skill
9
+
10
+ Use when adding a new feature, deciding where a new file belongs, or resolving a cross-feature import finding (`NB-ARCH-001`/`NB-ARCH-002`).
11
+
12
+ ## Creating a feature
13
+
14
+ 1. Create `src/features/<name>/` with only the folders you need (`api/`, `components/`, `hooks/`, `types.ts`).
15
+ 2. Add `index.ts` exporting the minimal public surface.
16
+ 3. Wire it into a route in `app/` — routes compose features, never the other way around.
17
+
18
+ ## Deciding where code goes
19
+
20
+ - Used by one feature → inside that feature.
21
+ - Used by two or more features → `lib/` (logic) or `components/ui/` (domain-agnostic UI).
22
+ - Needs another feature's UI + data on one screen → compose in the route, pass props.
23
+
24
+ ## Fixing boundary violations
25
+
26
+ - `NB-ARCH-001`: import via the feature's `index.ts` (public-api variant) or restructure — move shared code down, or compose at the route (forbid variant).
27
+ - `NB-ARCH-002`: the feature imports route-level code; invert it — the route should pass data/callbacks into the feature.
28
+ - A justified exception needs `// nb-disable NB-ARCH-001 -- <reason>` with a real reason.
@@ -0,0 +1,36 @@
1
+ # Feature Modules (forbid boundary)
2
+
3
+ Organize code by feature. In this strict variant features are fully isolated: **no imports between features at all**. Shared code moves down to `lib/`/`components/ui/`; composite screens are assembled at the route level.
4
+
5
+ ## Structure
6
+
7
+ <code-snippet name="Feature-first layout" lang="text">
8
+ src/
9
+ ├── features/
10
+ │ └── invoices/
11
+ │ ├── api/
12
+ │ ├── components/
13
+ │ ├── hooks/
14
+ │ ├── types.ts
15
+ │ └── index.ts # public API consumed ONLY by app/ routes
16
+ ├── components/ui/ # shared, domain-agnostic UI kit
17
+ ├── lib/ # shared utilities, api client
18
+ └── app/ # routes — the ONLY place features are combined
19
+ </code-snippet>
20
+
21
+ ## Boundary rules (enforced by node-boost audit)
22
+
23
+ - `NB-ARCH-001` (error): **any** import from one feature into another is forbidden — including via `index.ts`.
24
+ - `NB-ARCH-002` (error): a feature must never import from `app/**`. Dependency direction is `shared → features → app`.
25
+
26
+ ## How to work within the rules
27
+
28
+ - Need feature B's component inside feature A's screen? Compose both in the route (`app/`), passing data via props.
29
+ - Need feature B's types or helpers? Move them to `lib/` — if two features need it, it is shared by definition.
30
+ - The payoff: zero dependency cycles, features are deletable and extractable wholesale, and every feature can be understood in isolation.
31
+
32
+ ## Anti-patterns
33
+
34
+ - Smuggling cross-feature access through `lib/` wrappers that just re-export a feature's internals.
35
+ - A giant "shared" dumping ground — `lib/` is for genuinely generic code, not a backdoor.
36
+ - Deep relative paths that dodge aliases — the boundary applies regardless of import style.
@@ -0,0 +1,37 @@
1
+ # Feature Modules (public-api boundary)
2
+
3
+ Organize code by feature. Each feature is a module with a public API in `index.ts`; cross-feature imports are allowed **only through that public API**.
4
+
5
+ ## Structure
6
+
7
+ <code-snippet name="Feature-first layout" lang="text">
8
+ src/
9
+ ├── features/
10
+ │ └── invoices/
11
+ │ ├── api/ # data access for this feature
12
+ │ ├── components/
13
+ │ ├── hooks/
14
+ │ ├── types.ts
15
+ │ └── index.ts # PUBLIC API — the only cross-feature entry point
16
+ ├── components/ui/ # shared, domain-agnostic UI kit
17
+ ├── lib/ # shared utilities, api client
18
+ └── app/ # routes — compose features, no business logic
19
+ </code-snippet>
20
+
21
+ ## Boundary rules (enforced by node-boost audit)
22
+
23
+ - `NB-ARCH-001` (error): importing another feature's internals is forbidden. `import { CartSummary } from "@/features/cart"` is fine; `import { store } from "@/features/cart/internal/store"` is not.
24
+ - `NB-ARCH-002` (error): a feature must never import from `app/**`. Dependency direction is `shared → features → app`.
25
+
26
+ ## Conventions
27
+
28
+ - Keep `index.ts` deliberately small — every export is a contract. If a feature's public API grows past ~10 exports, it is probably two features.
29
+ - Shared code goes down to `lib/` or `components/ui/`, never sideways between features.
30
+ - Watch for cycles: if feature A imports B and B imports A (even via `index.ts`), extract the shared piece to `lib/` or compose both at the route level.
31
+ - Do not introduce `features/` prematurely — adopt it when a second feature starts bleeding into the first.
32
+
33
+ ## Anti-patterns
34
+
35
+ - Re-exporting everything from `index.ts` "just in case" — large barrels hurt tree-shaking and make everything public.
36
+ - "Shared" modules importing from features (upward dependency).
37
+ - Deep relative paths (`../../cart/...`) that dodge the alias — the boundary applies regardless of import style.
@@ -0,0 +1,47 @@
1
+ # Modern TypeScript
2
+
3
+ The language contract for this codebase: strict compiler, no `any`, closed sets as unions, IDs as branded types.
4
+
5
+ ## Compiler
6
+
7
+ `strict: true` is non-negotiable (`NB-ARCH-013`, warn). New projects also enable `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes`.
8
+
9
+ ## No `any` — narrow `unknown`
10
+
11
+ `any` switches the type checker off at every use site (`NB-ARCH-014`, warn). For data of unknown shape use `unknown` and narrow with guards or parse with zod (typed-contracts). Casting external data with `as` is the same lie with better manners.
12
+
13
+ ## Closed sets: discriminated unions + exhaustiveness
14
+
15
+ <code-snippet name="Union with exhaustive switch" lang="ts">
16
+ type PaymentState =
17
+ | { kind: "pending" }
18
+ | { kind: "paid"; paidAt: Date }
19
+ | { kind: "failed"; reason: string }
20
+
21
+ function label(state: PaymentState): string {
22
+ switch (state.kind) {
23
+ case "pending": return "Pending"
24
+ case "paid": return `Paid ${state.paidAt.toLocaleDateString()}`
25
+ case "failed": return state.reason
26
+ default: { const _exhaustive: never = state; throw new Error("unreachable") }
27
+ }
28
+ }
29
+ // Adding { kind: "refunded" } breaks the build HERE — every switch is found for you.
30
+ </code-snippet>
31
+
32
+ Do not use TS `enum` — runtime quirks, poor tree-shaking. Closed value sets are const objects: `const Role = { admin: "admin", user: "user" } as const; type Role = keyof typeof Role`.
33
+
34
+ ## Branded types for identifiers
35
+
36
+ <code-snippet name="IDs that cannot be swapped" lang="ts">
37
+ type UserId = string & { readonly __brand: "UserId" }
38
+ type InvoiceId = string & { readonly __brand: "InvoiceId" }
39
+ // payInvoice(userId, invoiceId) with swapped args → compile error, not a prod bug
40
+ </code-snippet>
41
+
42
+ Zero runtime cost — compile-time value objects. Use for IDs and easily-confused scalars (amounts, emails).
43
+
44
+ ## Also
45
+
46
+ - `satisfies` to type-check without widening: `const config = {...} satisfies Config`.
47
+ - Prefer `readonly` arrays/fields on data that must not mutate.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: modern-typescript
3
+ description: Apply strict TypeScript idioms — unknown over any, discriminated unions with exhaustive switches, branded IDs, const objects over enums.
4
+ ---
5
+
6
+ # Modern TypeScript
7
+
8
+ ## When to use this skill
9
+
10
+ Use when modeling domain types, handling unknown-shaped data, or fixing `NB-ARCH-013`/`NB-ARCH-014`.
11
+
12
+ ## Procedure
13
+
14
+ 1. State that varies by kind → discriminated union with a `kind` field; every `switch` gets a `never` exhaustiveness check in `default`.
15
+ 2. Closed value set → const object + `keyof typeof`, never `enum`.
16
+ 3. New identifier type → brand it (`string & { readonly __brand: "XId" }`); constructors live next to the schema (`invoiceIdSchema.parse(raw) as InvoiceId` at the boundary only).
17
+ 4. Unknown-shaped data → `unknown`, then a type guard or zod parse. Never `any`, never bare `as`.
18
+ 5. Config-like literals → `satisfies` instead of a type annotation.
19
+
20
+ ## Fixing findings
21
+
22
+ - `NB-ARCH-013`: enable `strict: true` in tsconfig; fix fallout incrementally (start with `strictNullChecks` errors).
23
+ - `NB-ARCH-014`: replace `any` with `unknown` + narrowing, a precise type, or a generic; in tests it is tolerated (rule skips test files).
@@ -0,0 +1,38 @@
1
+ # Secure by Default
2
+
3
+ JSX escapes everything automatically. The vulnerabilities live in the escape hatches — close them by default.
4
+
5
+ ## XSS: one door, keep it locked
6
+
7
+ <code-snippet name="Sanitize or don't render" lang="tsx">
8
+ // NB-ARCH-011 (error): raw HTML from any dynamic source
9
+ <div dangerouslySetInnerHTML={{ __html: comment.body }} />
10
+
11
+ // The only acceptable form — sanitize first:
12
+ <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(comment.body) }} />
13
+ </code-snippet>
14
+
15
+ Prefer rendering data as JSX; reach for raw HTML only for trusted rich text (CMS/editor output), always sanitized (DOMPurify client-side, isomorphic-dompurify/sanitize-html on the server — or sanitize at write time). Trusted static literals may use `// nb-disable NB-ARCH-011 -- <reason>`.
16
+
17
+ ## Public env prefixes are publication
18
+
19
+ Everything named `NEXT_PUBLIC_*` / `VITE_*` ships inside the JS bundle, readable in DevTools by anyone. `NB-ARCH-012` (warn) flags secret-sounding names (`SECRET`, `TOKEN`, `PRIVATE`, `PASSWORD`, `*_KEY`) behind a public prefix. A key that grants write/admin access must never be public; server-side secrets stay unprefixed.
20
+
21
+ ## Server Actions are public endpoints
22
+
23
+ Every `"use server"` function compiles to an HTTP endpoint anyone can call. Treat each like a controller route:
24
+
25
+ 1. **Authenticate** the caller,
26
+ 2. **authorize** the operation,
27
+ 3. **validate** the input (zod — typed-contracts),
28
+ 4. return only what the caller may see.
29
+
30
+ Never assume "only my form calls this".
31
+
32
+ ## Keep server code on the server
33
+
34
+ Add `import "server-only"` to data-layer modules that touch secrets — importing them from a Client Component then fails the build instead of leaking at runtime. React taint APIs are an extra experimental layer, not a substitute.
35
+
36
+ ## Baseline
37
+
38
+ Set a Content-Security-Policy (via `next.config`/middleware or hosting), keep dependencies patched, and never log tokens or PII to the browser console.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: secure-by-default
3
+ description: Handle untrusted content, env variables, Server Actions and server-only code safely in React/Next apps.
4
+ ---
5
+
6
+ # Secure by Default
7
+
8
+ ## When to use this skill
9
+
10
+ Use when rendering user/CMS-provided content, adding env variables, writing Server Actions, or fixing `NB-ARCH-011`/`NB-ARCH-012`.
11
+
12
+ ## Procedure
13
+
14
+ 1. Rich text/HTML to render → sanitize (`DOMPurify.sanitize`) before `dangerouslySetInnerHTML`; if the content can be plain data, render it as JSX instead.
15
+ 2. New env variable → secret? no public prefix, access via `env.ts` server-side only. Client-needed? confirm it is harmless when public, then prefix (`NEXT_PUBLIC_`/`VITE_`).
16
+ 3. New Server Action → first lines: auth check, then `schema.parse(input)`; return minimal data; add cache invalidation after writes.
17
+ 4. Data-layer module touching secrets → `import "server-only"` at the top.
18
+
19
+ ## Fixing findings
20
+
21
+ - `NB-ARCH-011`: wrap the value in a sanitizer call, or replace raw HTML with JSX rendering.
22
+ - `NB-ARCH-012`: remove the public prefix and move usage server-side; if genuinely public, rename so it does not masquerade as a secret and suppress with a reason.
@@ -0,0 +1,34 @@
1
+ # Server-First Components
2
+
3
+ In the App Router, components are Server Components by default. Keep them that way: fetch data on the server, ship less JavaScript, and push `"use client"` down to interactive leaves.
4
+
5
+ ## Rules
6
+
7
+ - Never put `"use client"` in `page.tsx` or `layout.tsx` (`NB-ARCH-003`, error). Route entries stay server-side; extract the interactive part into a leaf component instead.
8
+ - Do not add `"use client"` to files with no hooks, event handlers, or browser APIs (`NB-ARCH-004`, warn) — the directive is not a talisman, it makes the whole subtree client-side.
9
+ - A genuinely full-client page (canvas playground, embedded editor shell) is a documented exception: `// nb-disable NB-ARCH-003 -- <reason>`.
10
+
11
+ <code-snippet name="Interactive leaf, server entry" lang="tsx">
12
+ // app/invoices/page.tsx — Server Component: async, fetches data
13
+ export default async function InvoicesPage() {
14
+ const invoices = await getInvoices()
15
+ return <InvoiceTable invoices={invoices} toolbar={<InvoiceFilter />} />
16
+ }
17
+
18
+ // features/invoices/components/InvoiceFilter.tsx — client leaf
19
+ "use client"
20
+ export function InvoiceFilter() {
21
+ const [query, setQuery] = useState("")
22
+ return <input value={query} onChange={(e) => setQuery(e.target.value)} />
23
+ }
24
+ </code-snippet>
25
+
26
+ ## Mental model
27
+
28
+ `"use client"` marks a boundary, not a component: everything imported below it becomes client code. Server Components can render Client Components and pass serializable props (no functions, no class instances) — compose interactivity in, don't lift the whole page out.
29
+
30
+ ## Next 16 specifics
31
+
32
+ - Nothing is cached by default; caching is explicit via `"use cache"` on components/functions — which only works if the tree stays server-first.
33
+ - Partial Prerendering splits pages into a static shell plus dynamic holes in `<Suspense>`. A client-rooted page opts out of all of it.
34
+ - Do not generate legacy idioms: `getServerSideProps`/`getStaticProps` belong to the Pages Router, not here.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: server-first-components
3
+ description: Decide where the "use client" boundary goes in Next.js App Router code and keep pages/layouts server-side.
4
+ ---
5
+
6
+ # Server-First Components
7
+
8
+ ## When to use this skill
9
+
10
+ Use when creating pages/layouts, when tempted to add `"use client"`, or when fixing `NB-ARCH-003`/`NB-ARCH-004` findings.
11
+
12
+ ## Procedure
13
+
14
+ 1. Start every component as a Server Component (no directive). Fetch data with `await` in the component body via the data layer.
15
+ 2. Add `"use client"` only when the file actually uses state/effects, event handlers, or browser APIs.
16
+ 3. If a page needs interactivity, extract the interactive part into a leaf component in the feature and render it from the server page — never mark the page itself.
17
+ 4. Pass only serializable props across the boundary. Need a callback? Move the state down into the client leaf, or use a Server Action.
18
+ 5. For caching, add `"use cache"` in the data layer function, not in the component; wrap truly dynamic islands in `<Suspense>`.
19
+
20
+ ## Fixing findings
21
+
22
+ - `NB-ARCH-003`: move interactivity to a leaf; keep the entry async and server-side.
23
+ - `NB-ARCH-004`: delete the unnecessary directive — the file has nothing client-only in it.
@@ -0,0 +1,34 @@
1
+ # State Management
2
+
3
+ Classify state before placing it. Each kind has exactly one correct home; most bugs come from putting server data in a client store.
4
+
5
+ | Kind | Example | Home |
6
+ |---|---|---|
7
+ | Server state | invoices, users | react-query cache (or RSC props in Next) |
8
+ | Local UI state | open modal, input value | `useState` in the component |
9
+ | Shared client state | theme, sidebar, multi-step draft | Context (rare updates) / store like zustand (frequent) |
10
+ | URL state | filters, pagination, active tab | `searchParams` |
11
+
12
+ ## The one big rule: server state is a cache, not state
13
+
14
+ <code-snippet name="Anti-pattern: copying cache into a store" lang="ts">
15
+ // WRONG — two sources of truth, manual sync, staleness bugs (NB-ARCH-009, warn)
16
+ const invoices = await api.getInvoices()
17
+ useInvoiceStore.setState({ invoices })
18
+ </code-snippet>
19
+
20
+ react-query already is the store for server data: keys, staleness, refetch, `invalidateQueries` after mutations. A client store holds only what the backend does not know (preferences, drafts, selections).
21
+
22
+ ## Escalation ladder
23
+
24
+ `useState` → URL → Context → store. Escalate only when the level below fails. URL state is underused: filters and pagination in `searchParams` give deep links, back button and refresh survival for free.
25
+
26
+ ## Next specifics
27
+
28
+ Server Components remove the need for much client state — data arriving as props needs no client cache. Keep react-query for interactive islands, not for whole server-rendered pages.
29
+
30
+ ## Anti-patterns
31
+
32
+ - A global store as junk drawer (2016-Redux style).
33
+ - Deriving state with `useEffect` + `setState` — compute it during render instead.
34
+ - Duplicating URL state in a store and syncing by hand.
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: state-management
3
+ description: Choose the right home for a piece of state (react-query, useState, URL, Context, store) and keep server data out of client stores.
4
+ ---
5
+
6
+ # State Management
7
+
8
+ ## When to use this skill
9
+
10
+ Use when introducing any new state, choosing between store/context/URL, or fixing `NB-ARCH-009`.
11
+
12
+ ## Procedure
13
+
14
+ 1. Classify: does the backend own this data? → react-query (or RSC props). Is it one component's concern? → `useState`. Should a link/refresh preserve it? → URL `searchParams`. Only genuinely shared, client-owned state goes to Context/store.
15
+ 2. Mutating server data: `useMutation`/Server Action + `invalidateQueries` — never write the response into a store.
16
+ 3. Before adding a store: check the ladder (`useState` → URL → Context → store) and justify each escalation.
17
+ 4. Deriving from existing state? Compute in render (or `useMemo` if expensive) — not `useEffect` + `setState`.
18
+
19
+ ## Fixing findings
20
+
21
+ - `NB-ARCH-009`: delete the store write; read the data via the query hook where it is needed. If the store held a derived selection, keep only the selection (ids), not the server objects.
@@ -0,0 +1,33 @@
1
+ # Styling with Tailwind
2
+
3
+ Utility-first with discipline: tokens from the theme, variants through cva, reuse through components.
4
+
5
+ ## Check the Tailwind major first
6
+
7
+ Tailwind 4 configures in CSS — there is **no `tailwind.config.js`**:
8
+
9
+ <code-snippet name="Tailwind 4 CSS-first config" lang="css">
10
+ @import "tailwindcss";
11
+ @theme {
12
+ --color-brand: oklch(0.62 0.19 260);
13
+ --spacing-gutter: 1.5rem;
14
+ }
15
+ </code-snippet>
16
+
17
+ Generating a v3-style JS config in a v4 project is a common error — follow the versioned tailwindcss guideline for this project.
18
+
19
+ ## Rules
20
+
21
+ - **Tokens over arbitrary values**: colors/spacing come from `@theme`; `w-[347px]` needs a reason, or the codebase grows fifty shades of gray.
22
+ - **Variants via cva + tailwind-merge**, not string concatenation:
23
+
24
+ <code-snippet name="Variants with cva" lang="ts">
25
+ const button = cva("rounded px-4 py-2 font-medium", {
26
+ variants: { intent: { primary: "bg-brand text-white", danger: "bg-red-600 text-white" } },
27
+ defaultVariants: { intent: "primary" },
28
+ })
29
+ </code-snippet>
30
+
31
+ - **Never build class names dynamically**: `text-${color}-500` does not exist in the build — the compiler scans sources statically; a class not written literally is a class not generated. Map to full literal classes instead.
32
+ - **Repeating a class string? Extract a component**, not an `@apply` class — the unit of reuse is the React component. Keep `@apply` to a minimum.
33
+ - Conditional classes through a `cn()` helper (clsx + tailwind-merge); class order handled by prettier-plugin-tailwindcss (or Biome's sorting when the project uses Biome).
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: styling-tailwind
3
+ description: Style components with Tailwind using theme tokens, cva variants and component extraction — matching the project's Tailwind major version.
4
+ ---
5
+
6
+ # Styling with Tailwind
7
+
8
+ ## When to use this skill
9
+
10
+ Use when styling components, adding visual variants, or touching Tailwind configuration.
11
+
12
+ ## Procedure
13
+
14
+ 1. Check the project's Tailwind major (see application_info / package.json). v4: config lives in CSS (`@theme`); never create `tailwind.config.js`. v3: JS config applies.
15
+ 2. New color/spacing → add a token to the theme; avoid arbitrary values unless one-off and justified.
16
+ 3. Component with visual variants → define with cva, merge overrides with tailwind-merge, expose a `variant` prop.
17
+ 4. Conditional classes → `cn(base, cond && "...")`; never template-interpolate class fragments.
18
+ 5. Same class string appearing twice → extract a component in `components/ui/`.