{
  "about": "An entry of UpFork's ledger. Check it by hand as https://upfork.dev/docs/ledger says, or at https://upfork.dev/verify/E-596",
  "entry": {
    "seq": 596,
    "kind": "article-version",
    "record": "{\"ai\":{\"apps\":[\"Claude\"],\"models\":[\"Claude Opus 5.5\"]},\"article\":\"setting-up-and-shipping-a-next-js-16-template-17-traps-with-shadcn-ui-eslint\",\"author\":{\"account\":\"a_9d470b566acab5f65bdc\"},\"body\":\"## What it is\\n\\nMeridian is an enterprise finance dashboard template for Next.js and shadcn/ui: 22 screens in light and dark, every figure computed from one seeded book ([meridian.upfork.dev](https://meridian.upfork.dev)). It was scaffolded on 28 October 2026 with the latest stable version of every package and shipped on 30 October as a release ZIP made with `git archive`. Seventeen things on that road didn't behave as expected: version ranges that no longer fit, CLIs that answer prompts for you, a Next.js that writes into your project, and a release ZIP that once came out empty and once carried CRLFs git wouldn't convert.\\n\\nWhat you'll learn here: how to set up Next.js 16.4 with TypeScript, ESLint 10, Tailwind 4, pnpm 12 and shadcn/ui 4.23 from a script, and how to make and check a release ZIP from a monorepo, each with the exact error and a fix you can reuse. How the dashboard itself works is in the [main article](https://upfork.dev/articles/how-to-build-a-finance-dashboard-where-every-number-agrees-one-seeded-book-l).\\n\\nWhat Meridian ended up on:\\n\\n| Package | Version |\\n| --- | --- |\\n| next / react | 16.4.3 / 19.3.1 |\\n| typescript | ~6.0.3 |\\n| tailwindcss | 4.3.3 |\\n| shadcn (CLI) / radix-ui | 4.23.0, style new-york / 1.8.1 |\\n| eslint / eslint-config-next | 10.13.0 / 16.4.3 |\\n| eslint-plugin-tailwindcss | 4.4.0 |\\n| cn | 0.5.0 |\\n| pnpm / Node | 12.8.1 / 22.23.1 |\\n\\n## TypeScript, ESLint and pnpm\\n\\nThree of the newest releases didn't fit together, and two tools changed their defaults.\\n\\n:::lesson{title=\\\"typescript@latest is 7.0 (the native port), but typescript-eslint 8.72 accepts only TypeScript below 6.1\\\" kind=\\\"newer\\\" by=\\\"ai\\\" packages=\\\"typescript@6.0.3, typescript-eslint@8.72.1, eslint-config-next@16.4.3\\\"}\\n**Rule:** If `pnpm view typescript version` says 7.x and the project lints with typescript-eslint 8 (eslint-config-next), install typescript ~6.0 instead: typescript-eslint peers <6.1.0.\\n**Snippet:**\\n```json\\n// package.json, devDependencies\\n- \\\"typescript\\\": \\\"^7.0.2\\\"\\n+ \\\"typescript\\\": \\\"~6.0.3\\\"\\n```\\n**Check:** `pnpm typecheck` (tsc --noEmit) exit 0; `pnpm lint` exit 0; `pnpm build` runs \\\"Running TypeScript … Finished TypeScript in 9.4s\\\"; `pnpm peers check` → \\\"No peer dependency issues found\\\".\\n**Applies to:** typescript@>=7 <8, typescript-eslint@>=8.72 <9, eslint-config-next@>=16.4 <17\\n**I assumed:** \\\"latest stable\\\" for TypeScript means the `latest` dist-tag, as for every other package in the brief.\\n**What happened:** `pnpm view typescript version` → 7.0.2 (published 2026-07-08). `pnpm view typescript-eslint peerDependencies` → `\\\"typescript\\\": \\\">=4.8.4 <6.1.0\\\"`, and eslint-config-next 16.4.3 depends on `typescript-eslint ^8.56.0`.\\n**Why:** TypeScript 7 is the Go rewrite; typescript-eslint drives the type checker through the JavaScript compiler API of the 5.x/6.x line.\\n**The fix:** `\\\"typescript\\\": \\\"~6.0.3\\\"` (the newest 6.0 release, 2026-04-16) in devDependencies.\\n**Evidence:** install summary `+ typescript 6.0.3`; peer range quoted above from the registry on 2026-10-28.\\n:::\\n\\n:::lesson{title=\\\"ESLint 10 runs every eslint-config-next 16.4 rule, though three bundled plugins' peer ranges stop at ESLint 9\\\" kind=\\\"not-in-docs\\\" by=\\\"ai\\\" packages=\\\"eslint@10.13.0, eslint-config-next@16.4.3, eslint-plugin-react@7.37.5, eslint-plugin-import@2.32.0, eslint-plugin-jsx-a11y@6.10.2, eslint-plugin-react-hooks@7.1.1, eslint-plugin-tailwindcss@4.4.0\\\"}\\n**Rule:** If pnpm warns \\\"deprecated eslint@9\\\" and eslint-config-next's plugins peer-require ESLint ≤9, use ESLint 10 and add peerDependencyRules.allowedVersions for those plugins: their rules work.\\n**Snippet:**\\n```yaml\\n# pnpm-workspace.yaml\\npeerDependencyRules:\\n  allowedVersions:\\n    eslint-plugin-import>eslint: \\\"10\\\"\\n    eslint-plugin-jsx-a11y>eslint: \\\"10\\\"\\n    eslint-plugin-react>eslint: \\\"10\\\"\\n```\\n**Check:** `pnpm peers check` → `No peer dependency issues found`; `pnpm lint` exit 0 on the project; the probe file (deleted afterwards) reported the 10 problems listed below.\\n**Applies to:** eslint@>=10 <11, eslint-config-next@>=16.4 <17\\n**I assumed:** eslint-config-next needs ESLint 9, because eslint-plugin-react (`^9.7`), eslint-plugin-import (`^9`) and eslint-plugin-jsx-a11y (`^9`) say so in their peer ranges.\\n**What happened:** installing eslint@9.39.5 printed `[WARN] deprecated eslint@9.39.5. 10.13.0 is not deprecated, outside the range you declared.` After `pnpm add -D eslint@^10.13.0`: `[WARN] Issues with peer dependencies found` (unmet peer eslint for those three), but `eslint` ran, and a probe file tripped react-hooks/rules-of-hooks, react/jsx-key, react/display-name, react/no-unescaped-entities, jsx-a11y/alt-text, import/no-anonymous-default-export, @next/next/no-img-element, tailwindcss/classnames-order and no-console — `✖ 10 problems (5 errors, 5 warnings)`.\\n**Why:** the plugins' rules don't use the context methods ESLint 10 removed; only their declared peer ranges lag.\\n**The fix:** `\\\"eslint\\\": \\\"^10.13.0\\\"` and, in pnpm-workspace.yaml, `peerDependencyRules.allowedVersions` for `eslint-plugin-import>eslint`, `eslint-plugin-jsx-a11y>eslint`, `eslint-plugin-react>eslint` = \\\"10\\\".\\n**Evidence:** install and lint output, 2026-10-28.\\n:::\\n\\n:::lesson{title=\\\"eslint-plugin-tailwindcss 4 has no configs['flat/recommended']: ESLint crashes reading 'basePath'\\\" kind=\\\"newer\\\" by=\\\"ai\\\" packages=\\\"eslint-plugin-tailwindcss@4.4.0, eslint@10.13.0, tailwindcss@4.3.3\\\"}\\n**Rule:** If ESLint crashes \\\"Cannot use 'in' operator to search for 'basePath' in undefined\\\" in defineConfig, an extends entry is undefined: eslint-plugin-tailwindcss 4 exports only configs.recommended.\\n**Snippet:**\\n```js\\n// eslint.config.mjs\\n{\\n  files: [\\\"src/**/*.{ts,tsx}\\\"],\\n  ignores: [\\\"src/components/ui/**\\\"],\\n- extends: [tailwind.configs[\\\"flat/recommended\\\"]],\\n+ extends: [tailwind.configs.recommended],\\n  settings: { tailwindcss: { cssConfigPath: \\\"./src/app/globals.css\\\" } },\\n}\\n```\\n**Check:** `pnpm lint` then ran and reported `No need for arbitrary 'max-w-[1440px]', use 'max-w-360' instead` (it reads the v4 theme), 0 problems after the fix.\\n**Applies to:** eslint-plugin-tailwindcss@>=4 <5\\n**I assumed:** the flat config is `configs[\\\"flat/recommended\\\"]`, as in eslint-plugin-tailwindcss 3.x.\\n**What happened:** `pnpm lint` → `TypeError: Cannot use 'in' operator to search for 'basePath' in undefined at processExtends (@eslint/config-helpers/dist/cjs/index.cjs:457:18)`. `Object.keys(plugin.configs)` → `['recommended']`; that object is already a flat config. The README's own example hedges with `configs[\\\"flat/recommended\\\"] || configs.recommended`.\\n**Why:** 4.x (the Tailwind v4 rewrite) dropped the legacy eslintrc presets, so the flat preset took the plain name.\\n**The fix:** `extends: [tailwind.configs.recommended]` plus the mandatory `settings.tailwindcss.cssConfigPath: \\\"./src/app/globals.css\\\"`.\\n**Evidence:** stack trace above; lint output after the change (checked on ESLint 9.39.5, and again on 10.13.0).\\n:::\\n\\n:::lesson{title=\\\"pnpm 12 no longer refuses a too-new release under a caret floor: it adds the exclusion to pnpm-workspace.yaml itself\\\" kind=\\\"newer\\\" by=\\\"ai\\\" packages=\\\"pnpm@12.8.1, @hookform/resolvers@6.0.2, lucide-react@1.63.0, motion@14.7.1\\\"}\\n**Rule:** If pnpm-workspace.yaml gains minimumReleaseAgeExclude entries after `pnpm install`, pnpm 12 wrote them for versions under a day old; keep them, or set minimumReleaseAgeStrict to be asked first.\\n**Snippet:**\\n```yaml\\n# pnpm-workspace.yaml, after `pnpm install` (written by pnpm)\\nminimumReleaseAgeExclude:\\n  - '@hookform/resolvers@6.0.2'\\n  - framer-motion@14.7.1\\n  - lucide-react@1.63.0\\n  - motion-dom@14.7.1\\n  - motion@14.7.1\\n```\\n**Check:** `pnpm install` → `Done in 17m 34.5s using pnpm v12.8.1`, exit 0, `+ @hookform/resolvers 6.0.2`, `+ lucide-react 1.63.0`, `+ motion 14.7.1`; pnpm-workspace.yaml lists the 5 entries.\\n**Applies to:** other: pnpm@>=12 <13 with its default minimumReleaseAge\\n**I assumed:** from UpFork lesson L-ZWCJ, pnpm refuses a release under a day old, and you pin the version before it or add an exclusion by hand.\\n**What happened:** the install succeeded and printed `Added 5 entries to minimumReleaseAgeExclude in pnpm-workspace.yaml (set minimumReleaseAgeStrict to true to gate these updates with a prompt):` — @hookform/resolvers@6.0.2, framer-motion@14.7.1, lucide-react@1.63.0, motion-dom@14.7.1, motion@14.7.1 — and rewrote the file.\\n**Why:** likely because each caret range's floor was the fresh version itself (`^6.0.2` published 2026-10-28T07:01Z, `^1.63.0` 2026-10-28T06:10Z, `^14.7.1` 2026-10-27T11:42Z), so no older version could satisfy it; with minimumReleaseAgeStrict off, pnpm 12 lets them through and records the exception. This extends L-ZWCJ, where pnpm refused a Next.js release under a day old: when the range's floor is the new release itself, pnpm 12.8 doesn't refuse it, it adds the exclusion and rewrites pnpm-workspace.yaml.\\n**The fix:** kept the entries (the brief asks for the latest stable versions). They can be deleted once those versions are older than the release-age window.\\n**Evidence:** install log, 2026-10-28; publish times from `pnpm view <pkg> time --json`. Two days later the five entries were removed and `pnpm install --frozen-lockfile` passed (\\\"Lockfile passes supply-chain policies (782 entries …)\\\"), leaving the file as it was.\\n:::\\n\\n:::lesson{title=\\\"`next dev` 16.4 writes an AGENTS.md into the project root on start; a template folder picks it up untracked\\\" kind=\\\"newer\\\" by=\\\"ai\\\" packages=\\\"next@16.4.3\\\"}\\n**Rule:** If `git status` shows an untracked AGENTS.md after `next dev` (log: \\\"Generated AGENTS.md for AI agents\\\"), set `agentRules: false` in next.config and delete the file, unless you want it shipped.\\n**Snippet:**\\n```ts\\n// next.config.ts\\nconst nextConfig: NextConfig = {\\n  devIndicators: false,\\n+ // `next dev` 16.4 otherwise writes an AGENTS.md into the project root on every start\\n+ agentRules: false,\\n}\\n```\\n**Check:** `pnpm dev` with the setting: the log has no \\\"Generated AGENTS.md\\\" line, `curl /transactions` → 200, `ls AGENTS.md` → \\\"No such file or directory\\\"; `pnpm typecheck` exit 0 (the key is typed); `git status --short meridian` shows no AGENTS.md.\\n**Applies to:** next@>=16.4 <17\\n**I assumed:** `next dev` only writes inside `.next/`; the project folder stays as committed.\\n**What happened:** the dev log printed \\\"✓ Generated AGENTS.md for AI agents. Set `agentRules: false` in next.config to disable.\\\" and `git status --short meridian` showed `?? meridian/AGENTS.md` (679 bytes, a \\\"This is NOT the Next.js you know\\\" block between `<!-- BEGIN:nextjs-agent-rules -->` markers). The block says `next dev` re-adds it if it is removed from a diff.\\n**Why:** Next.js 16.4 generates agent rules (node_modules/next/dist/server/lib/generate-agent-files.js) on dev start; the option is `agentRules?: boolean` in NextConfig (config-schema.js accepts `agentRules: z.boolean().optional()`).\\n**The fix:** `agentRules: false` in next.config.ts (with a comment), the generated file deleted.\\n**Evidence:** dev log line above; config-shared.d.ts:1753 `agentRules?: boolean;`; `git status` before/after.\\n:::\\n\\n## shadcn/ui 4.23, run from a script\\n\\nshadcn's CLI 4.23 sets up projects from presets, so Meridian's `components.json` (style new-york, Radix, neutral) was written by hand, and every component was added with `pnpm dlx shadcn@4.23.0 add <names> --yes`. Primitives edited for Meridian carry a comment starting `Meridian:`, so a buyer re-adding one with `--overwrite` can see what they would undo. Chart, form, checkbox, select, table and five more were written from the registry's JSON instead of the CLI, because `chart.json` pins recharts@3.8.0 and the project has 3.10.1.\\n\\n:::lesson{title=\\\"`shadcn init` 4.23 asks for a preset (Nova, Vega, …) and never offers the new-york style\\\" kind=\\\"newer\\\" by=\\\"ai\\\" packages=\\\"shadcn@4.23.0, next@16.4.3, tailwindcss@4.3.3\\\"}\\n**Rule:** If `shadcn init` asks \\\"Which preset would you like to use?\\\" but you need style new-york, write components.json with \\\"style\\\": \\\"new-york\\\" yourself and run `shadcn add index --yes --overwrite`.\\n**Snippet:**\\n```json\\n{\\n  \\\"$schema\\\": \\\"https://ui.shadcn.com/schema.json\\\",\\n  \\\"style\\\": \\\"new-york\\\",\\n  \\\"rsc\\\": true,\\n  \\\"tsx\\\": true,\\n  \\\"tailwind\\\": { \\\"config\\\": \\\"\\\", \\\"css\\\": \\\"src/app/globals.css\\\", \\\"baseColor\\\": \\\"neutral\\\", \\\"cssVariables\\\": true, \\\"prefix\\\": \\\"\\\" },\\n  \\\"iconLibrary\\\": \\\"lucide\\\",\\n  \\\"aliases\\\": { \\\"components\\\": \\\"@/components\\\", \\\"utils\\\": \\\"@/lib/utils\\\", \\\"ui\\\": \\\"@/components/ui\\\", \\\"lib\\\": \\\"@/lib\\\", \\\"hooks\\\": \\\"@/hooks\\\" }\\n}\\n```\\n```bash\\npnpm dlx shadcn@4.23.0 add index --yes --overwrite\\n```\\n**Check:** `pnpm dlx shadcn@4.23.0 add button` then creates src/components/ui/button.tsx from new-york-v4; `pnpm build` → \\\"✓ Compiled successfully\\\"; the page renders with the neutral tokens.\\n**Applies to:** shadcn@>=4.23 <5\\n**I assumed:** init prompts for a style (New York) and a base color, as in shadcn 2.x/3.x, and `-b radix` picks Radix primitives.\\n**What happened:** `pnpm dlx shadcn@4.23.0 init -b radix --no-monorepo` stopped at `Which preset would you like to use?` — Nova (Lucide / Geist), Vega, Maia, Lyra, Mira, Luma, Sera, Rhea, Custom. Each preset fixes style, font and icon library (Vega = \\\"Lucide / Inter\\\", style \\\"vega\\\"). With a components.json already present, init only asks `A components.json file already exists. Would you like to overwrite it?`.\\n**Why:** init in 4.x builds a project from a preset; the new-york style still lives in the registry (`styles/new-york-v4`) and is the CLI's fallback style for `add`.\\n**The fix:** hand-wrote components.json (style new-york, rsc, tsx, baseColor neutral, cssVariables true, iconLibrary lucide, the usual aliases), then `pnpm dlx shadcn@4.23.0 add index --yes --overwrite`. It installed the style's dependencies (class-variance-authority, cn, lucide-react, radix-ui, tw-animate-css, shadcn) and wrote globals.css: `@import \\\"tw-animate-css\\\"`, `@import \\\"shadcn/tailwind.css\\\"`, `@custom-variant dark`, `@theme inline`, the neutral `:root`/`.dark` variables and the base layer.\\n**Evidence:** CLI prompt text above; globals.css grew from 1 line to 126.\\n:::\\n\\n:::lesson{title=\\\"shadcn's registry components now import `cn` from the npm package “cn”, not from @/lib/utils\\\" kind=\\\"newer\\\" by=\\\"ai\\\" packages=\\\"shadcn@4.23.0, cn@0.5.0\\\"}\\n**Rule:** If a freshly added shadcn component imports { cn } from \\\"cn\\\", that is the registry now: make lib/utils.ts `export { cn } from \\\"cn\\\"` so your code and shadcn's use one function.\\n**Snippet:**\\n```ts\\n// src/lib/utils.ts\\n- import { clsx, type ClassValue } from \\\"clsx\\\"\\n- import { twMerge } from \\\"tailwind-merge\\\"\\n- export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }\\n+ export { cn } from \\\"cn\\\"\\n```\\n**Check:** `head -4 src/components/ui/button.tsx` shows `import { cn } from \\\"cn\\\"`; `pnpm typecheck`, `pnpm lint`, `pnpm build` exit 0.\\n**Applies to:** shadcn@>=4.22 <5, cn@>=0.5 <1\\n**I assumed:** shadcn components import `cn` from \\\"@/lib/utils\\\", a local clsx + tailwind-merge helper the CLI writes once.\\n**What happened:** `shadcn add button` added `\\\"cn\\\": \\\"^0.5.0\\\"` to dependencies and wrote `import { cn } from \\\"cn\\\"` — also after I had created src/lib/utils.ts with the clsx/twMerge helper. The registry item https://ui.shadcn.com/r/styles/new-york-v4/button.json lists `dependencies: [\\\"cn\\\", \\\"radix-ui\\\"]`, and utils.json's file is `export { cn } from \\\"cn\\\"`.\\n**Why:** cn (repo shadcn-ui/cn) describes itself as \\\"Fast, small, compiled class-name merging for Tailwind CSS. Drop-in replacement for clsx + tailwind-merge.\\\" The CLI also carries a migration from clsx/tailwind-merge to cn. (Another UpFork product added on 2026-10-25 still got `@/lib/utils` imports, so the switch is recent.)\\n**The fix:** src/lib/utils.ts = `export { cn } from \\\"cn\\\"` (the registry's own file); removed clsx and tailwind-merge from dependencies.\\n**Evidence:** registry JSON fetched 2026-10-28; `pnpm remove clsx tailwind-merge` → `- clsx 2.1.1`, `- tailwind-merge 3.7.0`.\\n:::\\n\\n:::lesson{title=\\\"A scripted `shadcn add` answers its overwrite prompt “no”, stops there, and still prints Done with exit 0\\\" kind=\\\"tools\\\" by=\\\"ai\\\" packages=\\\"shadcn@4.23.0, pnpm@12.8.1\\\"}\\n**Rule:** If `shadcn add` run from a script prints \\\"Would you like to overwrite? (y/N)\\\" then Done, check every file: closed stdin answered no and the run stopped. Back up edited files, rerun with --overwrite.\\n**Snippet:**\\n```bash\\n# before: exits 0, but a declined overwrite ends the run\\npnpm dlx shadcn@4.23.0 add index --yes < /dev/null            # globals.css untouched\\n# after: keep your edited primitive aside, overwrite, put it back\\ncp src/components/ui/button.tsx /tmp/button.keep.tsx\\npnpm dlx shadcn@4.23.0 add sidebar calendar --yes --overwrite < /dev/null\\ncp /tmp/button.keep.tsx src/components/ui/button.tsx\\n```\\n**Check:** the second runs printed `✔ Created 3 files: input.tsx, skeleton.tsx, command.tsx` and `✔ Created 3 files: use-mobile.ts, calendar.tsx, sidebar.tsx … ℹ Updated 1 file: button.tsx`; `git diff --stat -- src/components/ui/button.tsx` empty after the restore; `pnpm typecheck` exit 0.\\n**Applies to:** shadcn@>=4.23 <5, other: pnpm dlx with no TTY\\n**I assumed:** `--yes` answers every prompt, and a declined overwrite skips only that one file while the rest are still written.\\n**What happened:** with one file: `pnpm dlx shadcn@4.23.0 add index --yes < /dev/null` installed the dependencies, printed `? The file utils.ts already exists. Would you like to overwrite? » (y/N)` followed by `Done in 40.6s using pnpm v12.8.1`, exit 0 — and src/app/globals.css was still the single line `@import \\\"tailwindcss\\\";`. With many: `pnpm dlx shadcn@4.23.0 add sidebar dropdown-menu popover command dialog breadcrumb tabs avatar collapsible drawer scroll-area sonner textarea kbd separator tooltip sheet --yes < /dev/null` printed `? The file button.tsx already exists. Would you like to overwrite? » (y/N)` (asked for sidebar's dependency `button`, which an earlier phase had edited) and `Done in 42.7s`, exit 0. 15 files were created; command.tsx, input.tsx, skeleton.tsx, sidebar.tsx and hooks/use-mobile.ts were not, and nothing said so.\\n**Why:** `--yes` only skips the confirmation prompt; the per-file overwrite question is separate, takes \\\"no\\\" when stdin is closed, and ends the run: the files still queued behind it are dropped.\\n**The fix:** `add index --yes --overwrite` wrote globals.css (`✔ Updating src\\\\app\\\\globals.css`); `add command input skeleton --yes` (no conflict) wrote the 3 missing files at once; for the sidebar, button.tsx was copied aside, `add sidebar calendar --yes --overwrite` run, and the file copied back.\\n**Evidence:** `ls src/components/ui` after the first multi-component run (15 files, no sidebar/command) and the two `add index` outputs, 2026-10-28.\\n:::\\n\\n:::lesson{title=\\\"A shadcn file copied from the registry JSON fails tsc: its imports use @/registry/new-york-v4/ui, which the CLI rewrites\\\" kind=\\\"tools\\\" by=\\\"ai\\\" packages=\\\"shadcn@4.23.0, recharts@3.10.1, typescript@6.0.3, next@16.4.3\\\"}\\n**Rule:** If you write a shadcn component from the registry JSON's files[].content (to skip the CLI), replace every \\\"@/registry/new-york-v4/ui/\\\" import with your ui alias, or tsc fails TS2307.\\n**Snippet:**\\n```bash\\ncurl -s https://ui.shadcn.com/r/styles/new-york-v4/alert-dialog.json -o alert-dialog.json\\nnode -e \\\"const j=require('./alert-dialog.json');require('fs').writeFileSync('src/components/ui/alert-dialog.tsx', j.files[0].content)\\\"\\nsed -i 's#@/registry/new-york-v4/ui/#@/components/ui/#g' src/components/ui/alert-dialog.tsx   # what the CLI would have done\\n```\\n**Check:** `pnpm exec tsc --noEmit` → no output, exit 0; `pnpm build` → \\\"✓ Compiled successfully\\\"; `grep -rn \\\"@/registry\\\" src` → nothing.\\n**Applies to:** shadcn@>=4.23 <5\\n**I assumed:** a registry item's `content` is the file the CLI would write, so saving it under components/ui is the same as `shadcn add`.\\n**What happened:** I wrote chart, form, checkbox, select, table, alert-dialog, switch, label, radio-group and card from https://ui.shadcn.com/r/styles/new-york-v4/<name>.json, because chart.json lists `\\\"dependencies\\\": [\\\"cn\\\", \\\"recharts@3.8.0\\\", \\\"lucide-react\\\"]` and `shadcn add chart` would have installed that pinned version over the project's recharts 3.10.1. `pnpm exec tsc --noEmit` then failed: `src/components/ui/alert-dialog.tsx(7,24): error TS2307: Cannot find module '@/registry/new-york-v4/ui/button' or its corresponding type declarations.` and the same for `src/components/ui/form.tsx(17,23)` with `'@/registry/new-york-v4/ui/label'`.\\n**Why:** the registry stores files with the registry's own import paths; the CLI rewrites them to the aliases in components.json when it installs. Items that depend on other items (alert-dialog → button, form → label) carry those paths.\\n**The fix:** `sed -i 's#@/registry/new-york-v4/ui/#@/components/ui/#g'` over the written files (2 imports in 10 files), then the project's usual token edits (`text-negative-text` for `text-destructive`, `bg-overlay` for `bg-black/50`).\\n**Evidence:** tsc output above; registry JSON fields as fetched with curl on 2026-10-28 (registry style new-york-v4).\\n:::\\n\\n## Shipping the release ZIP\\n\\nThe buyer's ZIP is `git archive` of the committed folder, without the build log, the lessons or the screenshots: `git -c core.autocrlf=false -c core.eol=lf archive --format=zip --prefix=meridian/ -o dist/meridian-1.0.0.zip HEAD:meridian`, run from the repository root, with `.gitattributes` keeping `docs/`, `qa/` and the listing scripts out. The 1.0.0 ZIP holds 337 files with 0 CRs and 0 NUL bytes, and an unzipped copy passes `pnpm install --frozen-lockfile` and `pnpm build`. Five lessons came out of getting there.\\n\\n:::lesson{title=\\\"The repo root's unanchored `build/` ignore rule swallowed a source folder: src/lib/data/build was never committed\\\" kind=\\\"tools\\\" by=\\\"ai\\\"}\\n**Rule:** If a source folder is named build/ (dist/, out/) and the repo root ignores `build/` unanchored, run `git ls-files <folder>`: empty means never committed; un-ignore it with `!/path/build/`.\\n**Snippet:**\\n```gitignore\\n# meridian/.gitignore\\n/build\\n+ # The repo root ignores every folder named build/; this one is source (the seeded data builders, §13)\\n+ !/src/lib/data/build/\\n```\\n**Check:** `git check-ignore -v meridian/src/lib/data/build/investments.ts` exits 1; `find meridian/src meridian/tests meridian/scripts meridian/public -type f | git check-ignore --stdin` prints nothing; and a `git archive` of the commit, built outside the repo, passes `pnpm install --frozen-lockfile` and `pnpm build`.\\n**Applies to:** other: git ignore rules in a monorepo (git@>=2)\\n**I assumed:** `git status` lists every new source file, so anything the phases built was either committed or visibly untracked.\\n**What happened:** `git check-ignore -v meridian/src/lib/data/build/investments.ts` → `.gitignore:6:build/ meridian/src/lib/data/build/investments.ts`, and `git ls-files meridian/src/lib/data/build` printed nothing: the eight seeded data builders written in phase 3 were on disk only. Every phase, and the review of every phase, was checked on the working tree, so for three phases origin/main lacked them while every check was green; a fresh clone or a `git archive` doesn't build.\\n**Why:** the repo-wide .gitignore (`build/`, meant for build output in every product) has no leading slash, so it matches a directory named build at any depth; ignored folders never show in `git status`.\\n**The fix:** meridian/.gitignore gained `!/src/lib/data/build/` (a deeper .gitignore's negation wins for its subtree) and the files were committed. And a release is checked the way the buyer gets it: the committed tree exported with `git archive` (or cloned) outside the repo, then a frozen install and a build there.\\n**Evidence:** `git ls-files` empty on origin/main b2f9d7f; the clean-copy build after 9929efc: 336 files, frozen install and build green (git 2.52.0).\\n:::\\n\\n:::lesson{title=\\\"git archive of a subfolder kept an empty docs/ and the tracked qa/README.md: export-ignore the folders themselves\\\" kind=\\\"not-in-docs\\\" by=\\\"ai\\\"}\\n**Rule:** If `git archive HEAD:<dir>` still lists an empty `docs/` after its files got export-ignore (or a tracked qa/README.md), put `export-ignore` on the folder: `docs export-ignore`.\\n**Snippet:**\\n```gitattributes\\n# before: the files go, an empty docs/ stays; qa/README.md ships\\ndocs/BRIEF.md export-ignore\\ndocs/PROGRESS.md export-ignore\\n# after: the folders are skipped whole\\ndocs export-ignore\\nqa export-ignore\\n```\\n**Check:** `git archive --worktree-attributes HEAD meridian | tar -t | grep -E \\\"^meridian/(qa|docs)\\\"` prints nothing before the commit; after it, `git archive HEAD:meridian | tar -t` lists no docs/ or qa/.\\n**Applies to:** other: git archive with .gitattributes (git@>=2)\\n**I assumed:** marking BRIEF.md, PROGRESS.md, LESSONS.md and palette-preview.png export-ignore kept docs/ out of the buyer's ZIP, and qa/ (gitignored but for its README) wasn't in it.\\n**What happened:** `git archive HEAD:meridian | tar -t` listed `docs/` (an empty folder entry), `qa/` and `qa/README.md`. Testing a fix with `git archive --worktree-attributes HEAD:meridian` changed nothing at all: even docs/BRIEF.md came back.\\n**Why:** export-ignore on files drops the files but not their folder's tree entry; a folder is skipped whole only when it has the attribute itself. `--worktree-attributes` matches the working tree's .gitattributes against the archive's paths, which for `HEAD:meridian` lack the `meridian/` prefix; test with `git archive --worktree-attributes HEAD meridian` instead.\\n**The fix:** meridian/.gitattributes adds `docs export-ignore` and `qa export-ignore` (the file lines stay).\\n**Evidence:** `git archive HEAD:meridian` gave 411 entries with `docs/`, `qa/` and `qa/README.md`; the worktree test of the fix gave 408, none of them, 2026-10-30.\\n:::\\n\\n:::lesson{title=\\\"`git archive … HEAD:meridian` run inside meridian/ made a 134-byte ZIP with no files\\\" kind=\\\"tools\\\" by=\\\"ai\\\"}\\n**Rule:** If `git archive <rev>:<folder>` gives an empty or tiny archive, run it from the repository root: from a subdirectory git also filters by that subdirectory's path inside the tree you named.\\n**Snippet:**\\n```bash\\n# before: run inside meridian/ — exit 0, a 134-byte ZIP with no files\\ngit -c core.autocrlf=false -c core.eol=lf archive --format=zip --prefix=meridian/ -o ../dist/meridian-1.0.0.zip HEAD:meridian\\n# after: the same command from the repository root\\ngit -c core.autocrlf=false -c core.eol=lf archive --format=zip --prefix=meridian/ -o dist/meridian-1.0.0.zip HEAD:meridian\\n```\\n**Check:** `unzip -Z1 dist/meridian-1.0.0.zip | grep -v '/$' | wc -l` → 337; clean copy: CR 0, NUL 0, frozen install and build exit 0.\\n**Applies to:** other: git archive with a `rev:path` tree-ish run from a subdirectory\\n**I assumed:** the recorded command, whose `-o ../dist/…` reads as if run from meridian/, works from there.\\n**What happened:** from upfork-products/meridian it exited 0 and wrote dist/meridian-1.0.0.zip at 134 bytes; `unzip -l` → \\\"0  1 file\\\". From the repository root with `-o dist/meridian-1.0.0.zip`: 828,529 bytes, 337 files.\\n**Why:** git archive limits itself to the current directory's path; inside the tree `HEAD:meridian` there is no meridian/ folder, so nothing matched.\\n**The fix:** run it from the repo root with `-o dist/meridian-1.0.0.zip`; the listing notes now say so.\\n**Evidence:** the two archives, 2026-10-30.\\n:::\\n\\n:::lesson{title=\\\"Two raw NUL characters made git store chart-utils.ts as binary, so its CRLFs shipped in the ZIP whatever core.eol said\\\" kind=\\\"not-in-docs\\\" by=\\\"ai\\\"}\\n**Rule:** If a release ZIP still holds CRs after `git -c core.eol=lf archive`, run `git ls-files --eol`: an `i/-text` source file is stored byte for byte; remove its NULs and commit it as LF.\\n**Snippet:**\\n```ts\\n// before: a literal U+0000 between the quotes, invisible in most editors\\nconst signature = keys.join(\\\"␀\\\")\\n// after\\nconst signature = keys.join(\\\"\\\\u0000\\\")\\n```\\n**Check:** the ZIP remade with `git -c core.autocrlf=false -c core.eol=lf archive …`: `find … | xargs -0 cat | tr -cd '\\\\r' | wc -c` → 0, and 0 NULs; typecheck, lint 0 problems, unit 63 passed, frozen install + build from the unzipped copy exit 0.\\n**Applies to:** other: Git for Windows git archive and ls-files --eol on TypeScript sources\\n**I assumed:** per UpFork lesson L-83P9, archiving with `-c core.eol=lf` would bring the CR count of the unzipped ZIP to 0.\\n**What happened:** the 1.0.0 ZIP made with `-c core.autocrlf=false` held 103 CRs, all in src/components/charts/chart-utils.ts. Archives made with core.eol=lf, crlf or unset were byte-identical; the other 334 text files were LF in every one. `git ls-files --eol` showed `i/-text w/-text` for that one file. In Git Bash, `cat -A` printed its lines without `^M` and `grep -l $'\\\\r'` matched 327 files; only `tr -cd '\\\\r' | wc -c` and Python counted right.\\n**Why:** `keys.join(\\\"␀\\\")` and `signature.split(\\\"␀\\\")` were typed with a literal NUL character. A NUL makes git treat the file as binary, so it is never converted on add or archive and its CRLF went in as committed. L-83P9 holds where files are text (Sponsor Bot has `* text=auto`); Meridian has no text attribute, so core.eol never applies to that file.\\n**The fix:** the two NULs became `\\\"\\\\u0000\\\"` escapes (the same string at runtime) and the file was committed as LF (d85598e); `git ls-files --eol` → `i/lf w/lf`.\\n**Evidence:** four archives (eol unset / lf / crlf / no -c) compared, 2026-10-30; commit d85598e.\\n:::\\n\\n:::lesson{title=\\\"Stopping a background `pnpm exec next start` left node serving the old build on Windows\\\" kind=\\\"tools\\\" by=\\\"ai\\\" packages=\\\"pnpm@12.8.1, next@16.4.3\\\"}\\n**Rule:** If you stop a background `pnpm exec next start` from Git Bash on Windows, check the port answers nothing before rebuilding; kill the node child by its command line if it still does.\\n**Snippet:**\\n```powershell\\n# the shell task is gone, but its node grandchild still serves the old build\\nGet-CimInstance Win32_Process | Where-Object { $_.CommandLine -match '<port>' } |\\n  ForEach-Object { Stop-Process -Id $_.ProcessId }\\n```\\n**Check:** the process query returned 0 matches; the fresh server served the new build (probe 0 faults).\\n**Applies to:** other: Git for Windows bash → pnpm.exe → cmd.exe → node process tree\\n**I assumed:** stopping the background shell task ended the server it had started.\\n**What happened:** after the stop, the port still answered 307 and `next build` ran under it. `Get-CimInstance Win32_Process` showed sh.exe → pnpm.exe → cmd.exe → node \\\"…\\\\next\\\\dist\\\\bin\\\\next\\\" start -p <port> still alive.\\n**Why:** on Windows the shell's termination doesn't reach the grandchild node process.\\n**The fix:** `Stop-Process` on the four PIDs whose command line matched the port, then rebuilt and restarted.\\n**Evidence:** the 307 and the process list, 2026-10-30 (git 2.52.0.windows.1).\\n:::\\n\\n## The listing pictures and the tour\\n\\nThe listing's pictures and its tour video were made by Playwright scripts (`scripts/listing-shots.mjs` and `listing-video.mjs`), so they can be retaken after any change. Two things went wrong on the way.\\n\\n:::lesson{title=\\\"A 1440×900 listing shot sliced the next row's text on 11 of 21 routes; scroll to where no edge cuts a line box\\\" kind=\\\"tools\\\" by=\\\"ai\\\" packages=\\\"@playwright/test@1.64.1, next@16.4.3\\\"}\\n**Rule:** If a listing screenshot's fold or sticky header runs through a line of text, scroll the panel to the nearest offset where no text line box or control crosses either edge (±4 px), not to a guessed one.\\n**Snippet:**\\n```js\\n// a line box is cut only when the edge runs well inside it (glyphs sit inside the box)\\nconst cuts = (edge) => bands.some(([a, b]) => a < edge - 4 && b > edge + 4)\\nconst clean = (at) => !cuts(at + scroller.clientHeight) && (at === 0 || !cuts(at + header))\\n```\\n**Check:** the seven desktop pictures at full size: no text line or control cut at either edge; #5099 back at offset 0 with its title.\\n**Applies to:** other: Playwright screenshots of a panel that scrolls under a sticky header\\n**I assumed:** as with an earlier template's listing (Wayline), each picture could be the page's first screen as it loads; a card running past the fold reads as \\\"more below\\\".\\n**What happened:** Cash flow's first screen cut \\\"$6,800 of $10,000\\\" and the \\\"Pay\\\" button in half; Equities cut the \\\"Geographical distribution\\\" and \\\"Dividends\\\" titles. A scan of every route's first screen found the fold inside text on 11 of 21 (Cash flow, Net worth, Transactions, Monthly analytics, Budget, Equities, F&O, Bonds, Insurance, Invoices, #5099).\\n**Why:** three rows of cards fill 900 px on only some pages; where a third row starts at ~750 px its title and first figures sit right on the fold. With a ±3 px margin #5099 failed on a line box that only grazed the fold, and the fit scrolled its title away.\\n**The fix:** scripts/listing-shots.mjs `fit()` collects Range.getClientRects() of every text node plus buttons, links, inputs, sliders and small SVGs in the shell's scroller, then tries offsets from 0 (or from a named card just under the header) until neither the fold nor the header's lower edge is more than 4 px inside any box. Sheets use their own scrolling body; Cash flow is framed from its chart card down.\\n**Evidence:** fold scan of 21 routes at 1440×900 (scratch script), 2026-10-30; the before/after Cash flow and Equities shots.\\n:::\\n\\n:::lesson{title=\\\"The tour showed “Refreshing…” and grey placeholders: the ready check passed on the old page before the route changed\\\" kind=\\\"my-mistake\\\" by=\\\"ai\\\" packages=\\\"@playwright/test@1.64.1, @tanstack/react-query@5.105.0, next@16.4.3\\\"}\\n**Rule:** If a recorded click or Enter navigates, do it under the cover and wait for the new pathname before checking for skeletons; click a page's toggles once off the record (staleTime Infinity keeps them).\\n**Snippet:**\\n```js\\n// before: returns at once — the old page is still \\\"ready\\\"\\nawait page.keyboard.press(\\\"Enter\\\")\\nawait settle(desk, 600)\\n// after: the navigation happens under the cover\\nawait cut(async () => {\\n  await page.keyboard.press(\\\"Enter\\\")\\n  await settle(desk, 600, \\\"/money-management/budget\\\")\\n})\\n```\\n**Check:** the third cut (92.1 s), every 2 s throughout and at 5 fps around both navigations: no skeleton, no placeholder bars and no \\\"Refreshing…\\\".\\n**Applies to:** other: Playwright recording of a Next.js app with TanStack Query placeholderData\\n**I assumed:** `settle()` (shell ready, no `[aria-busy]` or `[data-slot=skeleton]` in main) after pressing Enter in the palette meant the Budget page had loaded.\\n**What happened:** the first cut (91.8 s) showed, at 57 s, Budget with its \\\"Budgeted vs actual\\\" chart as grey placeholder bars and \\\"Refreshing…\\\" in the header line; Cash flow's Weekly switch and Net worth's 1Y switch each showed \\\"Refreshing…\\\" for about a second.\\n**Why:** right after Enter the frame still held the Loans page, which was ready, so the check returned before the client navigation began. The toggles change a query key; placeholderData keeps the old figures on screen while the new key fetches (250–600 ms of simulated latency), and UpdatedMeta says \\\"Refreshing…\\\" meanwhile.\\n**The fix:** scripts/listing-video.mjs: `settle(f, ms, \\\"/money-management/budget\\\")` waits for `f.waitForURL(...)` first and also for no \\\"Refreshing…\\\" in main; `warm(f, names)` clicks each toggle behind the cover before the scene, then the default again. The second cut still showed #5099's \\\"Loading the invoice…\\\" skeleton for about half a second: the click had started the route before the cover faded in. Now the tap's ripple plays on the link and the click itself happens inside `cut()` (cover on, recording paused).\\n**Evidence:** contact sheets of all three cuts, 2026-10-30.\\n:::\\n\\n## What we learned building it\\n\\n:::lesson{title=\\\"Claude Code's preview rejects an absolute `cwd` in launch.json for an app outside the project\\\" kind=\\\"tools\\\" by=\\\"ai\\\" packages=\\\"pnpm@12.8.1, next@16.4.3\\\"}\\n**Rule:** If preview_start says \\\"cwd must be a relative path within the project root\\\", the app lives outside the session's project: drop cwd and pass the folder to pnpm with \\\"--dir\\\".\\n**Snippet:**\\n```json\\n- { \\\"name\\\": \\\"meridian\\\", \\\"runtimeExecutable\\\": \\\"pnpm\\\", \\\"runtimeArgs\\\": [\\\"dev\\\"], \\\"cwd\\\": \\\"C:/path/to/meridian\\\", \\\"port\\\": 3000 }\\n+ { \\\"name\\\": \\\"meridian\\\", \\\"runtimeExecutable\\\": \\\"pnpm\\\", \\\"runtimeArgs\\\": [\\\"--dir\\\", \\\"C:/path/to/meridian\\\", \\\"dev\\\"], \\\"port\\\": 3000 }\\n```\\n**Check:** preview_start → `Server started on port …` (the app's dev port); server log `GET / 307`, `GET /dashboard/finance 200`; page title \\\"Finance overview · Meridian\\\"; no console errors.\\n**Applies to:** other: Claude Code desktop preview (.claude/launch.json)\\n**I assumed:** a launch entry `{ \\\"runtimeExecutable\\\": \\\"pnpm\\\", \\\"runtimeArgs\\\": [\\\"dev\\\"], \\\"cwd\\\": \\\"C:/…/meridian\\\" }` starts the app in that folder.\\n**What happened:** `Failed to start server: cwd must be a relative path within the project root.`\\n**Why:** the preview only runs commands from inside the session's project; `cwd` cannot leave it.\\n**The fix:** no `cwd`, and `\\\"runtimeArgs\\\": [\\\"--dir\\\", \\\"<the app's folder>\\\", \\\"dev\\\"]` (pnpm runs the folder's own `dev` script, so the port stays in package.json; the snippet's 3000 stands for that port).\\n**Evidence:** preview log, 2026-10-28.\\n:::\",\"bodySha256\":\"ab34ce08379d31bca2e242315106bb41bfd37a67cca999e01b7bbd0ff7a70517\",\"format\":2,\"kind\":\"tips-and-fixes\",\"product\":\"nextjs-finance-dashboard-template\",\"productRevision\":null,\"publishedAt\":\"2026-10-30T19:24:14.471Z\",\"revision\":1,\"site\":\"https://upfork.dev\",\"summary\":\"From TypeScript 7 outside typescript-eslint's range and scripted shadcn adds to a release ZIP with no files and CRLFs git wouldn't convert: 17 traps from building Meridian, each with its fix.\",\"title\":\"Setting up and shipping a Next.js 16 template: 17 traps with shadcn/ui, ESLint 10, pnpm 12 and git\",\"type\":\"article-version\",\"url\":\"https://upfork.dev/articles/setting-up-and-shipping-a-next-js-16-template-17-traps-with-shadcn-ui-eslint\"}",
    "recordHash": "c54336ddf98382a2f7e6dc3a6effa73256c8cec3f81a22fa56050d1d4bd90f37",
    "prevHash": "2a478710af40ac208f55cb242663516c3f4a91445721642770d2991b5b519e5a",
    "entryHash": "e5826487fdd08a073c37076dbf61cdc53a7f4a9dc58547df703b7c50c7c218db",
    "keyId": "k1",
    "signature": "gSyjIreIDy5eiIhuNKD4gpNB6TYRMugV3LJQcWomBw8vAIJbJqInU4B1+Pa8Zzg3CKLXINNEYNJZxWJxjkbcAw==",
    "recordedAt": "2026-10-30T19:24:24.92127+00:00",
    "publicKey": "VuHuHXAtO3XAqgnwvMenp0f/3vTV061hnAIizYvwh40=",
    "algorithm": "Ed25519"
  },
  "path": [
    {
      "seq": 597,
      "prevHash": "e5826487fdd08a073c37076dbf61cdc53a7f4a9dc58547df703b7c50c7c218db",
      "recordHash": "377f2b05beed23aecfedee3c5c88b9c4188d806989800d958c458606fcc98bb0",
      "entryHash": "a9a1419bee8e0e2fd5d4e14abb5026402391ebda1a0d0f6b56f44330fe69877b"
    },
    {
      "seq": 598,
      "prevHash": "a9a1419bee8e0e2fd5d4e14abb5026402391ebda1a0d0f6b56f44330fe69877b",
      "recordHash": "6ad5d8c01de50ea1f26cbd41ba14818c5212256181c79716093cab40564cea81",
      "entryHash": "89b259562be9d42dd426bca7d3d8096c72766c2dbe5a7e29c2b90708a7d04ef0"
    },
    {
      "seq": 599,
      "prevHash": "89b259562be9d42dd426bca7d3d8096c72766c2dbe5a7e29c2b90708a7d04ef0",
      "recordHash": "da9176ec5d356bb57b855dd7202ff172a18e600755958cf6ad4863df31e42e88",
      "entryHash": "37560fc41fa70a1e468ce946db74ae86c230b65766ca80cecd408c6cfcedeecb"
    },
    {
      "seq": 600,
      "prevHash": "37560fc41fa70a1e468ce946db74ae86c230b65766ca80cecd408c6cfcedeecb",
      "recordHash": "d6403216fa60f828aab11d0448deef2396f8d4e35f8d55f6dbf243fd271547ed",
      "entryHash": "024068186b367cc52b7e7100a3d0b97ed1fc08e00f82a649493a4bb87be6150c"
    },
    {
      "seq": 601,
      "prevHash": "024068186b367cc52b7e7100a3d0b97ed1fc08e00f82a649493a4bb87be6150c",
      "recordHash": "59bc375a9a743925f2b5ad7ffc674f96ab86211a3d1d46b97484702f7470279c",
      "entryHash": "911a7dd10f7052ba953ad0f861c0ec926fb606917a61db5e55be157d97dac15e"
    },
    {
      "seq": 602,
      "prevHash": "911a7dd10f7052ba953ad0f861c0ec926fb606917a61db5e55be157d97dac15e",
      "recordHash": "e1aa599bcf298558fe0c907b75d00ea6f2d9ea805791b6907d9abbabdca61e16",
      "entryHash": "d3631354a79bdcb055c4c8b84ab85a628c4636a5ef0601799b548c9ce9eae6b5"
    },
    {
      "seq": 603,
      "prevHash": "d3631354a79bdcb055c4c8b84ab85a628c4636a5ef0601799b548c9ce9eae6b5",
      "recordHash": "a3762930eeb23f600cda7cc29270d2015039aa83b25c3e1063122b489c08a08a",
      "entryHash": "8501030f4925860b8ab669e1f350446f884130fc070d3d09da0d33390aacd1fa"
    },
    {
      "seq": 604,
      "prevHash": "8501030f4925860b8ab669e1f350446f884130fc070d3d09da0d33390aacd1fa",
      "recordHash": "f41b19dd2147d65a0491d618fa6e72f7a7cad79769cb65a46a02c29db4ee9917",
      "entryHash": "f5ab802fa406225da9d4214df907dffa54706f3a00bcca4db52a630c941dd572"
    },
    {
      "seq": 605,
      "prevHash": "f5ab802fa406225da9d4214df907dffa54706f3a00bcca4db52a630c941dd572",
      "recordHash": "3bdd09cfb9fa59a1c96836b663b48bc42b3555a8f085d79e9d2c74403ed2c211",
      "entryHash": "1b3a698e4a03c650d323a1482f23a5a1cb6b7edb56db847d1e5ce509468ad29a"
    },
    {
      "seq": 606,
      "prevHash": "1b3a698e4a03c650d323a1482f23a5a1cb6b7edb56db847d1e5ce509468ad29a",
      "recordHash": "fdb6159e14111a60ec21f977f35f418536f46b07a435a44f8bcf10d209eb0974",
      "entryHash": "81cc3ffd7afd83d8f91109afc283b4f586e5528fc3b72bf88c0cdefd94a1ffb6"
    },
    {
      "seq": 607,
      "prevHash": "81cc3ffd7afd83d8f91109afc283b4f586e5528fc3b72bf88c0cdefd94a1ffb6",
      "recordHash": "9be799273c7facabeef51e5712453f540974b915f1667f3386c9fc8566adb82c",
      "entryHash": "2ee53a5ac8bf82ff44999ece9943dd0aaed0630406d5497dd169adc62418d8ad"
    },
    {
      "seq": 608,
      "prevHash": "2ee53a5ac8bf82ff44999ece9943dd0aaed0630406d5497dd169adc62418d8ad",
      "recordHash": "052f7e8ca5b7645dccf9684f68d4a3555c861370242c8f0aaac8fbec06842828",
      "entryHash": "e53672dca68d4bd7f65c439e11b8ac64436f1a693d718b41fb35668fce3f01b9"
    },
    {
      "seq": 609,
      "prevHash": "e53672dca68d4bd7f65c439e11b8ac64436f1a693d718b41fb35668fce3f01b9",
      "recordHash": "eca6f25eeca970daa14ebf71670368fecd76bab073b65b4d7aba3fb9c7f92bfa",
      "entryHash": "4930ded8a472f6a3f23f962fd123fc088536a1b93c6c311e1f40873998746807"
    },
    {
      "seq": 610,
      "prevHash": "4930ded8a472f6a3f23f962fd123fc088536a1b93c6c311e1f40873998746807",
      "recordHash": "a289472a8313782bcd5d1f0de2dfd5b6d52549ce3f3f5076032be151b33bb125",
      "entryHash": "139ca036a480478c73bf72419da3f8db56c56ea197e89d327ddcd09bab8a3ee7"
    },
    {
      "seq": 611,
      "prevHash": "139ca036a480478c73bf72419da3f8db56c56ea197e89d327ddcd09bab8a3ee7",
      "recordHash": "aa72fa63d404b7d12538d80c665855e19d57118a694746331f910e628a3b408e",
      "entryHash": "4821a0cb05448915bdfc012a702d60e5f443c02012adc7bd39f9d0bb469fcd8e"
    },
    {
      "seq": 612,
      "prevHash": "4821a0cb05448915bdfc012a702d60e5f443c02012adc7bd39f9d0bb469fcd8e",
      "recordHash": "c15f68e935f14214696eb1b9c9a8049cdd6c94ef344db7aa837d916d08c6c96c",
      "entryHash": "abf8332ba2d5628bae9c8fee18118bd85bca6774f34fd8d929826cb891cd7a5d"
    },
    {
      "seq": 613,
      "prevHash": "abf8332ba2d5628bae9c8fee18118bd85bca6774f34fd8d929826cb891cd7a5d",
      "recordHash": "2580e15403449b5007f6ccd595d3fbea8def29f77a6129969161a6a19e830bc8",
      "entryHash": "ee62bb540ba33f632dfdb36aa8a5c2a515c485493963ad4d944b534c09d5f214"
    },
    {
      "seq": 614,
      "prevHash": "ee62bb540ba33f632dfdb36aa8a5c2a515c485493963ad4d944b534c09d5f214",
      "recordHash": "57546e34296d8f48d4cf8bd92c7ebe8da7f642424e0217e88ee43cade04a53ff",
      "entryHash": "9efafea8bb8c6caf1fb9ddc1546c1759c8645ab17d0c5c151589e7430ed5b7ec"
    },
    {
      "seq": 615,
      "prevHash": "9efafea8bb8c6caf1fb9ddc1546c1759c8645ab17d0c5c151589e7430ed5b7ec",
      "recordHash": "204bae8dba8a9e40882104597ffaf2af6fe3b20e9b047baf3dbf1299d36cf36d",
      "entryHash": "217cd94fdeca6a0e1a1385d9856d3cc91bc598e6f730c9a861280d6f9d443093"
    },
    {
      "seq": 616,
      "prevHash": "217cd94fdeca6a0e1a1385d9856d3cc91bc598e6f730c9a861280d6f9d443093",
      "recordHash": "8124cc9256a42a694d13497cca432ac38c3ebe1e4b4469e8591232b4372b3943",
      "entryHash": "d8085e0f9912ddfa8398271ca5e47bf020f57190c4a3262d69d2b69c65908bc7"
    },
    {
      "seq": 617,
      "prevHash": "d8085e0f9912ddfa8398271ca5e47bf020f57190c4a3262d69d2b69c65908bc7",
      "recordHash": "190746a4d157c8cc19b1258084323921f559c54ae154c93cacfbbc4ae8fa0da8",
      "entryHash": "e367148ba25f502abd10e592ef62beef18934038f0ef86342e2ddc4453fc7589"
    },
    {
      "seq": 618,
      "prevHash": "e367148ba25f502abd10e592ef62beef18934038f0ef86342e2ddc4453fc7589",
      "recordHash": "cb35d5879befe072992b45c7c8ff4fc0f674289aca03c73bf5ffe1cd2b573e1f",
      "entryHash": "f843598a01c8767049bd3b355377dd335051425b0e61684ab92d54bcc5859dc4"
    },
    {
      "seq": 619,
      "prevHash": "f843598a01c8767049bd3b355377dd335051425b0e61684ab92d54bcc5859dc4",
      "recordHash": "c2502ea3535a39a6977cfcb6515fe113ab3a54d756ddff29587e4e28107e80e3",
      "entryHash": "77b0eb262adeaaae297cd217c75cd25739c933aa32102401c32d0e4a5bccaad5"
    },
    {
      "seq": 620,
      "prevHash": "77b0eb262adeaaae297cd217c75cd25739c933aa32102401c32d0e4a5bccaad5",
      "recordHash": "f33e283db2a1921755589dcdd53777992884b952cd79d5b6c405c133203e3dcc",
      "entryHash": "6445055c09fa80079e34e86a1332508bc6e9b2f93818537a655ee2b657d108db"
    },
    {
      "seq": 621,
      "prevHash": "6445055c09fa80079e34e86a1332508bc6e9b2f93818537a655ee2b657d108db",
      "recordHash": "044e6fb8ba6e25919a563701065ca2106d5f35f306c6be0e0606275fd2a60b0a",
      "entryHash": "8334e353988e222a23328f844996f3a37de917a918b462c4f1b500ba52c8524c"
    },
    {
      "seq": 622,
      "prevHash": "8334e353988e222a23328f844996f3a37de917a918b462c4f1b500ba52c8524c",
      "recordHash": "8796ab812bf9b481bd93b25f66bf1fb283d536e25c361b53e4bc27e5651d7bd4",
      "entryHash": "5bccac66bb29dca977530519e7993daa70cc019b810f170722b464ceadb0f246"
    },
    {
      "seq": 623,
      "prevHash": "5bccac66bb29dca977530519e7993daa70cc019b810f170722b464ceadb0f246",
      "recordHash": "7d41ea1718f6d0252358a825b44b8b969fec0986c591480b5d8c95acb1862dd5",
      "entryHash": "3d2af8edcf3a60e15997e53f0b3d7c20bb72e1fb4f9b12876991948a16af91b2"
    },
    {
      "seq": 624,
      "prevHash": "3d2af8edcf3a60e15997e53f0b3d7c20bb72e1fb4f9b12876991948a16af91b2",
      "recordHash": "9eb72d37985abc475868ce0c691787a58ffe6ba5437dd9e32645dd6a3fbc27be",
      "entryHash": "c386a1ad1fc384cdfeddc1c55e8d5caf6ddece0e827a1454263afdb17c4b509d"
    },
    {
      "seq": 625,
      "prevHash": "c386a1ad1fc384cdfeddc1c55e8d5caf6ddece0e827a1454263afdb17c4b509d",
      "recordHash": "ce8b23be0b90bb10fbd147c97a67cd99569f8dd8fd745671dbf1a8519cc41484",
      "entryHash": "3aa036d057edd6d429d3c16eceaaec2d35d4fd14a8e56b2dcde141c626b89084"
    },
    {
      "seq": 626,
      "prevHash": "3aa036d057edd6d429d3c16eceaaec2d35d4fd14a8e56b2dcde141c626b89084",
      "recordHash": "ce8dfe686c4e17766c06d5c57a25093a5d835c65bacbbb991410f0932e1ca4f0",
      "entryHash": "144c18132d18781a167af8acba04fa3e86d21d266811a24ee5073f3d257c2614"
    },
    {
      "seq": 627,
      "prevHash": "144c18132d18781a167af8acba04fa3e86d21d266811a24ee5073f3d257c2614",
      "recordHash": "11f917752013963509cca1984727e248d9bd3a8d700ebad65c4039ced5e2882f",
      "entryHash": "a0ee621700e7d6f3772101e318998a890eeb7424306b1e09199cc5de7e9dc9ba"
    },
    {
      "seq": 628,
      "prevHash": "a0ee621700e7d6f3772101e318998a890eeb7424306b1e09199cc5de7e9dc9ba",
      "recordHash": "c4d17b9ffc96d7ed747c10dc8c6c2cb49aec913e0906c653b3a24b9fdf947da6",
      "entryHash": "570f659041eb92a9508ffdaf33bec5bbba04391dd7feb2e97dfce90af174fad1"
    },
    {
      "seq": 629,
      "prevHash": "570f659041eb92a9508ffdaf33bec5bbba04391dd7feb2e97dfce90af174fad1",
      "recordHash": "0c144b2c359103cb698a0b83b3a73ffcd63441f16b4d0a67324a7452679d2c8c",
      "entryHash": "00fabb37a6e8824a3848b0c8a21bfd6766f5f7a763c5157b2abc56a5af73ce66"
    },
    {
      "seq": 630,
      "prevHash": "00fabb37a6e8824a3848b0c8a21bfd6766f5f7a763c5157b2abc56a5af73ce66",
      "recordHash": "fa028701d28d944263596ef13e4b2093ca28d37850a640d8ee64f693da14487a",
      "entryHash": "14566c723821fccfaff4f65aa5eeb89d03a4b9821c97199da90b3196463ba908"
    },
    {
      "seq": 631,
      "prevHash": "14566c723821fccfaff4f65aa5eeb89d03a4b9821c97199da90b3196463ba908",
      "recordHash": "33ae791342d6a60deeffef1479b8f9093c62a292dcad9edc463e03fed74f0139",
      "entryHash": "18e3b07d28f63eb8a7e3dcff2d78e33ea2e007438090d743999865fe4687c5b5"
    },
    {
      "seq": 632,
      "prevHash": "18e3b07d28f63eb8a7e3dcff2d78e33ea2e007438090d743999865fe4687c5b5",
      "recordHash": "1ab10e2e497d5369607c8297893382ac6c7a16d614b68d9bbc56e897c94f6c91",
      "entryHash": "df7fd3f60167937f7a7beacea8df43fea79d075f90ba8f5e15a51ca4282e5238"
    },
    {
      "seq": 633,
      "prevHash": "df7fd3f60167937f7a7beacea8df43fea79d075f90ba8f5e15a51ca4282e5238",
      "recordHash": "7d41803bafbb38d234fb67a7a61f15cfa739f3bd84e3e5056d56e97d49969250",
      "entryHash": "4d92322bad4ab8ef8427a9f6b525ec8684c1cf5a65a81e16e52f5aa96d096cab"
    },
    {
      "seq": 634,
      "prevHash": "4d92322bad4ab8ef8427a9f6b525ec8684c1cf5a65a81e16e52f5aa96d096cab",
      "recordHash": "f49cee7660617650773cc2d25997deb4a9146edde61508713e4762b65a0d7c38",
      "entryHash": "605999498714a9b21eff9eb2280a4653c2d7e4aae77ce009c651a53d0c208fb4"
    },
    {
      "seq": 635,
      "prevHash": "605999498714a9b21eff9eb2280a4653c2d7e4aae77ce009c651a53d0c208fb4",
      "recordHash": "5c001fc2d6579751adc07657a62cc436f0f8198448f3afdb1644546407bbe87e",
      "entryHash": "039a1f093d5638dabf22fa93cb647efaa87501660ffd8c25a421f9442d50932a"
    },
    {
      "seq": 636,
      "prevHash": "039a1f093d5638dabf22fa93cb647efaa87501660ffd8c25a421f9442d50932a",
      "recordHash": "e2011ec0147dd43681e9372d33fde65a651d962e8ac3b5b0942b4322facaf628",
      "entryHash": "c4c6afed115b56ac72a7a8adf84c1617e9e4cdb02c5f986e3e67eb5e497532c0"
    },
    {
      "seq": 637,
      "prevHash": "c4c6afed115b56ac72a7a8adf84c1617e9e4cdb02c5f986e3e67eb5e497532c0",
      "recordHash": "8b4789f70e6158fd8f915d364c16e3948ccfe56901dc13e366f2b1b7ad3a4462",
      "entryHash": "b29770299cfeea984521bef327d335ddc7f2a8e46920a64b0b80ebcad31a34d4"
    },
    {
      "seq": 638,
      "prevHash": "b29770299cfeea984521bef327d335ddc7f2a8e46920a64b0b80ebcad31a34d4",
      "recordHash": "1a9ea0f49c05071ca0b4bfadca3f3a5b1ded11035ac98917e60a560eb136fde4",
      "entryHash": "04406d6ad26a9e6069b93b9fec6bce9bedfd7c90580701a0d66d744ecfa385c5"
    },
    {
      "seq": 639,
      "prevHash": "04406d6ad26a9e6069b93b9fec6bce9bedfd7c90580701a0d66d744ecfa385c5",
      "recordHash": "3ef03d3eb99c02de4b1122251034b486dc55fa1349f40a8e2d4ab855d3328495",
      "entryHash": "c2d508e6a9891bb6f3e984661565dba5388885534954dc9a0263a0f18ca1bb8a"
    },
    {
      "seq": 640,
      "prevHash": "c2d508e6a9891bb6f3e984661565dba5388885534954dc9a0263a0f18ca1bb8a",
      "recordHash": "240156a22b5fec147bf2c56d9aad9579af48349d17e844f621059f5ce0b41624",
      "entryHash": "91666268ae803a50e4b0cb72595a4ef7e423fe50c1f500528c7cd4f0a98845e6"
    }
  ],
  "checkpoint": {
    "seq": 640,
    "kind": "checkpoint",
    "record": "{\"day\":\"2026-10-30\",\"entries\":639,\"head\":\"c2d508e6a9891bb6f3e984661565dba5388885534954dc9a0263a0f18ca1bb8a\",\"site\":\"https://upfork.dev\",\"type\":\"checkpoint\"}",
    "recordHash": "240156a22b5fec147bf2c56d9aad9579af48349d17e844f621059f5ce0b41624",
    "prevHash": "c2d508e6a9891bb6f3e984661565dba5388885534954dc9a0263a0f18ca1bb8a",
    "entryHash": "91666268ae803a50e4b0cb72595a4ef7e423fe50c1f500528c7cd4f0a98845e6",
    "keyId": "k1",
    "signature": "zgJ3XBAhPyJpFTFPRPjWt1hsNrnTid4E4bdlq1mpkUjMgWyA1evu4+12JDY5i3IunZB1YGZScVvl0MChvnOZBQ==",
    "recordedAt": "2026-10-30T20:22:37.85374+00:00",
    "publicKey": "VuHuHXAtO3XAqgnwvMenp0f/3vTV061hnAIizYvwh40=",
    "algorithm": "Ed25519"
  },
  "author": {
    "account": "a_9d470b566acab5f65bdc",
    "handle": "ramin-valadan",
    "former": false,
    "recordFormat": 2
  },
  "keys": "https://upfork.dev/ledger/keys.json"
}