From 393d65629fddd72538f89a4c7a7f58680e637350 Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 19:45:56 -0700 Subject: [PATCH 01/15] Add Node + TypeScript scripting scaffolding Mirrors the `npm run script scripts/.ts` convention from app-monorepo-template/apps/web so future repo automation has a uniform entry point. The existing Python script under scripts/ is left as-is. Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 611 ++++++++++++++++++++++++++++++++++++ package.json | 19 ++ scripts/runScriptWithEnv.sh | 10 + tsconfig.json | 14 + 4 files changed, 654 insertions(+) create mode 100644 package-lock.json create mode 100644 package.json create mode 100755 scripts/runScriptWithEnv.sh create mode 100644 tsconfig.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..2a6c796b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,611 @@ +{ + "name": "liquid-docs", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "liquid-docs", + "version": "0.0.1", + "devDependencies": { + "@types/node": "^22.0.0", + "commander": "^14.0.0", + "husky": "^9.1.0", + "tsx": "^4.21.0", + "typescript": "^5.9.0", + "yaml": "^2.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/tsx": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.0.tgz", + "integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..b02db198 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "liquid-docs", + "version": "0.0.1", + "private": true, + "scripts": { + "script": "scripts/runScriptWithEnv.sh", + "snapshot:update": "./scripts/runScriptWithEnv.sh scripts/generateLinkSnapshot.ts --update", + "snapshot:check": "./scripts/runScriptWithEnv.sh scripts/generateLinkSnapshot.ts --check", + "prepare": "husky" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "commander": "^14.0.0", + "husky": "^9.1.0", + "tsx": "^4.21.0", + "typescript": "^5.9.0", + "yaml": "^2.7.0" + } +} diff --git a/scripts/runScriptWithEnv.sh b/scripts/runScriptWithEnv.sh new file mode 100755 index 00000000..da294e38 --- /dev/null +++ b/scripts/runScriptWithEnv.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Minimal tsx-based script runner. +# +# Mirrors the calling convention of +# ~/git/app-monorepo-template/apps/web/scripts/runScriptWithEnv.sh +# (`npm run script scripts/.ts -- --flag`) but without the env-file +# selection, since this repo currently has no runtime env to load. Expand +# toward the template's shape if env-aware scripts get added later. +set -euo pipefail +exec npx tsx "$@" diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..5392bffa --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["scripts/**/*.ts"] +} From 6f1cc8e02d43eaa786cbebacb7e2bbc18f022b44 Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 19:46:03 -0700 Subject: [PATCH 02/15] Add link snapshot contract `link-snapshot.yaml` is an append-only record of every URL the docs site has ever served (nav pages + literal redirect sources + on-disk .mdx/.md files under lfm/, leap/, examples/, deployment/). The companion script derives URLs from `docs.json`, supports `--update` and `--check`, and verifies that each `active` URL still resolves via the current nav, disk pages, or redirect chain. Entries under `deleted:` are intentionally retired and skipped by the check. Co-Authored-By: Claude Opus 4.7 (1M context) --- link-snapshot.yaml | 167 ++++++++++++++++++++ scripts/generateLinkSnapshot.ts | 261 ++++++++++++++++++++++++++++++++ 2 files changed, 428 insertions(+) create mode 100644 link-snapshot.yaml create mode 100644 scripts/generateLinkSnapshot.ts diff --git a/link-snapshot.yaml b/link-snapshot.yaml new file mode 100644 index 00000000..a6cd0247 --- /dev/null +++ b/link-snapshot.yaml @@ -0,0 +1,167 @@ +# Auto-managed by scripts/generateLinkSnapshot.ts. +# `active` is append-only — pre-commit adds new URLs but never removes them. +# To intentionally retire a URL without a redirect, move it from `active` to +# `deleted` with a reason. The CI check skips entries listed under `deleted`. +active: + - /customization/finetuning-frameworks/datasets + - /customization/finetuning-frameworks/leap-finetune + - /customization/finetuning-frameworks/trl + - /customization/finetuning-frameworks/unsloth + - /customization/getting-started/connect-ai-tools + - /customization/getting-started/welcome + - /customization/tools/workbench + - /deployment/getting-started/connect-ai-tools + - /deployment/getting-started/welcome + - /deployment/gpu-inference/baseten + - /deployment/gpu-inference/fal + - /deployment/gpu-inference/modal + - /deployment/gpu-inference/sglang + - /deployment/gpu-inference/transformers + - /deployment/gpu-inference/vllm + - /deployment/on-device/android/advanced-features + - /deployment/on-device/android/ai-agent-usage-guide + - /deployment/on-device/android/android-quick-start-guide + - /deployment/on-device/android/cloud-ai-comparison + - /deployment/on-device/android/constrained-generation + - /deployment/on-device/android/conversation-generation + - /deployment/on-device/android/function-calling + - /deployment/on-device/android/messages-content + - /deployment/on-device/android/model-loading + - /deployment/on-device/android/openai-client + - /deployment/on-device/android/utilities + - /deployment/on-device/android/voice-assistant + - /deployment/on-device/ios/advanced-features + - /deployment/on-device/ios/ai-agent-usage-guide + - /deployment/on-device/ios/cloud-ai-comparison + - /deployment/on-device/ios/constrained-generation + - /deployment/on-device/ios/conversation-generation + - /deployment/on-device/ios/function-calling + - /deployment/on-device/ios/ios-quick-start-guide + - /deployment/on-device/ios/messages-content + - /deployment/on-device/ios/model-loading + - /deployment/on-device/ios/openai-client + - /deployment/on-device/ios/utilities + - /deployment/on-device/ios/voice-assistant + - /deployment/on-device/leap-sdk-changelog + - /deployment/on-device/llama-cpp + - /deployment/on-device/lm-studio + - /deployment/on-device/mlx + - /deployment/on-device/ollama + - /deployment/on-device/onnx + - /deployment/on-device/sdk/advanced-features + - /deployment/on-device/sdk/ai-agent-usage-guide + - /deployment/on-device/sdk/cloud-ai-comparison + - /deployment/on-device/sdk/constrained-generation + - /deployment/on-device/sdk/conversation-generation + - /deployment/on-device/sdk/desktop-platforms + - /deployment/on-device/sdk/function-calling + - /deployment/on-device/sdk/messages-content + - /deployment/on-device/sdk/model-loading + - /deployment/on-device/sdk/openai-client + - /deployment/on-device/sdk/quick-start + - /deployment/on-device/sdk/utilities + - /deployment/on-device/sdk/voice-assistant + - /deployment/tools/model-bundling/authentication + - /deployment/tools/model-bundling/bundle-creation + - /deployment/tools/model-bundling/bundle-management + - /deployment/tools/model-bundling/changelog + - /deployment/tools/model-bundling/configuration + - /deployment/tools/model-bundling/data-privacy + - /deployment/tools/model-bundling/download + - /deployment/tools/model-bundling/quick-start + - /deployment/tools/model-bundling/reference + - /docs/fine-tuning/datasets + - /docs/fine-tuning/trl + - /docs/fine-tuning/unsloth + - /docs/fine-tuning/workbench + - /docs/getting-started/connect-ai-tools + - /docs/getting-started/welcome + - /docs/inference/baseten-deployment + - /docs/inference/fal-deployment + - /docs/inference/llama-cpp + - /docs/inference/lm-studio + - /docs/inference/mlx + - /docs/inference/modal-deployment + - /docs/inference/ollama + - /docs/inference/onnx + - /docs/inference/sglang + - /docs/inference/transformers + - /docs/inference/vllm + - /examples/android/leap-koog-agent + - /examples/android/recipe-generator-constrained-output + - /examples/android/slogan-generator + - /examples/android/vision-language-model-example + - /examples/android/web-content-summarizer + - /examples/connect-ai-tools + - /examples/customize-models/car-maker-identification + - /examples/customize-models/home-assistant + - /examples/customize-models/satellite-vlm + - /examples/customize-models/wildfire-prevention + - /examples/index + - /examples/laptop-examples/audio-car-cockpit + - /examples/laptop-examples/audio-to-text-in-real-time + - /examples/laptop-examples/browser-control + - /examples/laptop-examples/flight-search-assistant + - /examples/laptop-examples/invoice-extractor-tool-with-liquid-nanos + - /examples/laptop-examples/lfm2-english-to-korean + - /examples/laptop-examples/meeting-summarization + - /examples/web/audio-webgpu-demo + - /examples/web/hand-voice-racer + - /examples/web/vl-webgpu-demo + - /leap/edge-sdk/overview + - /lfm/fine-tuning + - /lfm/fine-tuning/datasets + - /lfm/fine-tuning/trl + - /lfm/fine-tuning/unsloth + - /lfm/getting-started/connect-ai-tools + - /lfm/getting-started/model-license + - /lfm/getting-started/welcome + - /lfm/help/connect-ai-tools + - /lfm/help/contributing + - /lfm/help/faqs + - /lfm/help/model-license + - /lfm/help/troubleshooting + - /lfm/inference + - /lfm/inference/llama-cpp + - /lfm/inference/lm-studio + - /lfm/inference/mlx + - /lfm/inference/ollama + - /lfm/inference/transformers + - /lfm/inference/vllm + - /lfm/key-concepts/chat-template + - /lfm/key-concepts/text-generation-and-prompting + - /lfm/key-concepts/tool-use + - /lfm/models/audio-models + - /lfm/models/complete-library + - /lfm/models/lfm2-1.2b + - /lfm/models/lfm2-1.2b-extract + - /lfm/models/lfm2-1.2b-rag + - /lfm/models/lfm2-1.2b-tool + - /lfm/models/lfm2-2.6b + - /lfm/models/lfm2-2.6b-exp + - /lfm/models/lfm2-2.6b-transcript + - /lfm/models/lfm2-24b-a2b + - /lfm/models/lfm2-350m + - /lfm/models/lfm2-350m-enjp-mt + - /lfm/models/lfm2-350m-extract + - /lfm/models/lfm2-350m-math + - /lfm/models/lfm2-350m-pii-extract-jp + - /lfm/models/lfm2-700m + - /lfm/models/lfm2-8b-a1b + - /lfm/models/lfm2-audio-1.5b + - /lfm/models/lfm2-colbert-350m + - /lfm/models/lfm2-vl-1.6b + - /lfm/models/lfm2-vl-3b + - /lfm/models/lfm2-vl-450m + - /lfm/models/lfm25-1.2b-base + - /lfm/models/lfm25-1.2b-instruct + - /lfm/models/lfm25-1.2b-jp + - /lfm/models/lfm25-1.2b-thinking + - /lfm/models/lfm25-350m + - /lfm/models/lfm25-audio-1.5b + - /lfm/models/lfm25-vl-1.6b + - /lfm/models/lfm25-vl-450m + - /lfm/models/liquid-nanos + - /lfm/models/text-models + - /lfm/models/vision-models +deleted: [] diff --git a/scripts/generateLinkSnapshot.ts b/scripts/generateLinkSnapshot.ts new file mode 100644 index 00000000..4ce37e38 --- /dev/null +++ b/scripts/generateLinkSnapshot.ts @@ -0,0 +1,261 @@ +import { Command } from 'commander'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as YAML from 'yaml'; + +const REPO_ROOT = path.resolve(__dirname, '..'); +const DOCS_JSON = path.join(REPO_ROOT, 'docs.json'); +const SNAPSHOT_FILE = path.join(REPO_ROOT, 'link-snapshot.yaml'); + +const SNAPSHOT_HEADER = `# Auto-managed by scripts/generateLinkSnapshot.ts. +# \`active\` is append-only — pre-commit adds new URLs but never removes them. +# To intentionally retire a URL without a redirect, move it from \`active\` to +# \`deleted\` with a reason. The CI check skips entries listed under \`deleted\`. +`; + +// Directories whose .mdx/.md files map to docs URLs. snippets/ is excluded +// because it holds reusable fragments, not pages. +const PAGE_DIRS = ['lfm', 'leap', 'examples', 'deployment']; + +interface DocsJson { + navigation?: { tabs?: NavNode[] }; + redirects?: { source: string; destination: string }[]; +} + +type NavNode = + | string + | { + tab?: string; + group?: string; + root?: string; + pages?: NavNode[]; + groups?: NavNode[]; + tabs?: NavNode[]; + }; + +interface DeletedEntry { + url: string; + reason?: string; + retired_at?: string; +} + +interface Snapshot { + active: string[]; + deleted: DeletedEntry[]; +} + +function loadDocsJson(): DocsJson { + return JSON.parse(fs.readFileSync(DOCS_JSON, 'utf8')); +} + +function* walkPages(node: NavNode): Generator { + if (typeof node === 'string') { + yield '/' + node; + return; + } + if (!node || typeof node !== 'object') return; + if (typeof node.root === 'string') yield '/' + node.root; + for (const list of [node.pages, node.groups, node.tabs] as (NavNode[] | undefined)[]) { + if (Array.isArray(list)) { + for (const child of list) yield* walkPages(child); + } + } +} + +function navUrls(docs: DocsJson): Set { + const urls = new Set(); + for (const tab of docs.navigation?.tabs ?? []) { + for (const url of walkPages(tab)) urls.add(url); + } + return urls; +} + +function redirectSources(docs: DocsJson): string[] { + // Skip wildcard sources (e.g. "/docs/models/:slug*") — they're patterns, not + // URLs that anyone visits directly. They stay in docs.json and still match + // incoming requests via matchesRedirectSource at check time. + return (docs.redirects ?? []) + .map((r) => normalizeUrl(r.source)) + .filter((src) => !src.includes(':')); +} + +function normalizeUrl(url: string): string { + if (!url.startsWith('/')) return '/' + url; + return url; +} + +function diskPageUrls(): Set { + const urls = new Set(); + for (const dir of PAGE_DIRS) { + const abs = path.join(REPO_ROOT, dir); + if (!fs.existsSync(abs)) continue; + walkDir(abs, (file) => { + if (file.endsWith('.mdx') || file.endsWith('.md')) { + const rel = path.relative(REPO_ROOT, file).replace(/\\/g, '/'); + const noExt = rel.replace(/\.(mdx|md)$/, ''); + urls.add('/' + noExt); + } + }); + } + return urls; +} + +function walkDir(dir: string, visit: (file: string) => void): void { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walkDir(full, visit); + else visit(full); + } +} + +function loadSnapshot(): Snapshot { + if (!fs.existsSync(SNAPSHOT_FILE)) return { active: [], deleted: [] }; + const raw = fs.readFileSync(SNAPSHOT_FILE, 'utf8'); + const parsed = YAML.parse(raw) ?? {}; + return { + active: Array.isArray(parsed.active) ? parsed.active.map(String) : [], + deleted: Array.isArray(parsed.deleted) ? parsed.deleted : [], + }; +} + +function serializeSnapshot(snap: Snapshot): string { + const body = YAML.stringify( + { + active: [...snap.active].sort(), + deleted: snap.deleted, + }, + { lineWidth: 0 }, + ); + return SNAPSHOT_HEADER + body; +} + +function computeUpdatedSnapshot(docs: DocsJson, prev: Snapshot): Snapshot { + const fromNav = navUrls(docs); + const fromRedirects = redirectSources(docs); + const fromDisk = diskPageUrls(); + const deletedUrls = new Set(prev.deleted.map((d) => d.url)); + // Start from prior active, minus anything the user has since moved to `deleted`. + const merged = new Set([...prev.active].filter((url) => !deletedUrls.has(url))); + for (const url of fromNav) if (!deletedUrls.has(url)) merged.add(url); + for (const url of fromRedirects) if (!deletedUrls.has(url)) merged.add(url); + for (const url of fromDisk) if (!deletedUrls.has(url)) merged.add(url); + return { + active: [...merged].sort(), + deleted: prev.deleted, + }; +} + +// Match a `:slug*`-style wildcard source against a candidate URL. +function matchesRedirectSource(source: string, candidate: string): boolean { + if (source === candidate) return true; + const wildcardIdx = source.indexOf(':'); + if (wildcardIdx === -1) return false; + const prefix = source.slice(0, wildcardIdx); + return candidate.startsWith(prefix); +} + +function resolveDestination(source: string, destination: string, candidate: string): string { + const wildcardIdx = source.indexOf(':'); + if (wildcardIdx === -1) return destination; + const prefix = source.slice(0, wildcardIdx); + const tail = candidate.slice(prefix.length); + // destination typically ends in `/:slug*`; strip that and append the tail. + const destWildcardIdx = destination.indexOf(':'); + const destPrefix = destWildcardIdx === -1 ? destination : destination.slice(0, destWildcardIdx); + return destPrefix + tail; +} + +interface ResolveContext { + navSet: Set; + diskSet: Set; + redirects: { source: string; destination: string }[]; +} + +function urlResolves(url: string, ctx: ResolveContext, visited = new Set(), depth = 0): boolean { + if (depth > 5) return false; + if (visited.has(url)) return false; + visited.add(url); + if (ctx.navSet.has(url)) return true; + if (ctx.diskSet.has(url)) return true; + for (const r of ctx.redirects) { + const normSource = normalizeUrl(r.source); + if (matchesRedirectSource(normSource, url)) { + const dest = normalizeUrl(resolveDestination(normSource, normalizeUrl(r.destination), url)); + if (urlResolves(dest, ctx, visited, depth + 1)) return true; + } + } + return false; +} + +function checkContract(docs: DocsJson, snap: Snapshot): { ok: boolean; failures: string[] } { + const ctx: ResolveContext = { + navSet: navUrls(docs), + diskSet: diskPageUrls(), + redirects: (docs.redirects ?? []).map((r) => ({ + source: normalizeUrl(r.source), + destination: normalizeUrl(r.destination), + })), + }; + const deleted = new Set(snap.deleted.map((d) => d.url)); + const failures: string[] = []; + for (const url of snap.active) { + if (deleted.has(url)) continue; + if (!urlResolves(url, ctx)) failures.push(url); + } + return { ok: failures.length === 0, failures }; +} + +function main(): void { + const program = new Command(); + program + .option('--update', 'Append new URLs to link-snapshot.yaml') + .option('--check', 'Verify snapshot contract; non-zero on failure') + .parse(process.argv); + const opts = program.opts<{ update?: boolean; check?: boolean }>(); + + if (!opts.update && !opts.check) { + console.error('Pass --update or --check.'); + process.exit(2); + } + + const docs = loadDocsJson(); + const prev = loadSnapshot(); + const next = computeUpdatedSnapshot(docs, prev); + const serialized = serializeSnapshot(next); + const onDisk = fs.existsSync(SNAPSHOT_FILE) ? fs.readFileSync(SNAPSHOT_FILE, 'utf8') : ''; + + if (opts.update) { + if (serialized !== onDisk) { + fs.writeFileSync(SNAPSHOT_FILE, serialized); + console.log(`Updated ${path.relative(REPO_ROOT, SNAPSHOT_FILE)} (${next.active.length} active, ${next.deleted.length} deleted).`); + } else { + console.log('Snapshot already up to date.'); + } + return; + } + + // --check mode + const stale = serialized !== onDisk; + const { ok, failures } = checkContract(docs, prev); + + if (!ok) { + console.error('Link snapshot contract violation. The following URLs no longer resolve:'); + for (const url of failures) console.error(` - ${url}`); + console.error(''); + console.error('Remediation options for each URL:'); + console.error(' 1. Add a redirect entry under `redirects` in docs.json pointing to a current page.'); + console.error(' 2. Keep the underlying .mdx file on disk but remove it from docs.json navigation'); + console.error(' (the URL stays served but undiscoverable — mark the page as deprecated).'); + console.error(' 3. Move the URL from `active` to `deleted` in link-snapshot.yaml with a reason.'); + process.exit(1); + } + if (stale) { + console.error('link-snapshot.yaml is out of date relative to docs.json + on-disk pages.'); + console.error('Run `npm run snapshot:update` and commit the result.'); + process.exit(1); + } + console.log(`Snapshot OK: ${prev.active.length} active URLs verified.`); +} + +main(); From ba0084580eaff2764a61fc04e4cd44d34aa807d2 Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 19:46:19 -0700 Subject: [PATCH 03/15] Add husky pre-commit hook to refresh link snapshot Regenerates link-snapshot.yaml and re-stages it whenever docs.json or any .mdx/.md file is in the commit. Installed automatically by "npm install" via the husky prepare script. Co-Authored-By: Claude Opus 4.7 (1M context) --- .husky/pre-commit | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100755 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..e85391ff --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +# Regenerate link-snapshot.yaml when docs structure may have changed. +if git diff --cached --name-only | grep -qE '^(docs\.json|.*\.mdx?$)'; then + npm run snapshot:update --silent || exit 1 + git add link-snapshot.yaml +fi From f0ca7a9eeb51c0242d751cf2fe2a6fac5c096e58 Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 19:46:28 -0700 Subject: [PATCH 04/15] Add CI workflow to enforce link snapshot contract Runs `npm run snapshot:check` on every PR against main and on pushes to main. The check fails the build if any URL recorded in `link-snapshot.yaml#active` no longer resolves under the current `docs.json` + on-disk pages, prompting the contributor to add a redirect, deprecate the page in place, or move the URL to `deleted:`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/check-link-snapshot.yaml | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/check-link-snapshot.yaml diff --git a/.github/workflows/check-link-snapshot.yaml b/.github/workflows/check-link-snapshot.yaml new file mode 100644 index 00000000..b598a29a --- /dev/null +++ b/.github/workflows/check-link-snapshot.yaml @@ -0,0 +1,23 @@ +name: Check link snapshot + +on: + pull_request: + branches: + - main + push: + branches: + - main + workflow_dispatch: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + - run: npm ci + - name: Verify snapshot contract + run: npm run snapshot:check From 0270bfbbd7618882ea5dd9679a7265f311edaa5a Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 19:46:36 -0700 Subject: [PATCH 05/15] Document link snapshot workflow in README Explains the one-time `npm install` to wire the pre-commit hook, the three remediation paths when CI flags a missing URL (redirect / keep as deprecated / move to `deleted:`), and the manual commands to regenerate or verify the snapshot. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index fcd0b218..c68cc8a0 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ This is the **official documentation repository** for Liquid AI. It contains com - [Making Changes](#making-changes) - [Submitting Changes](#submitting-changes) - [Link Check](#link-check) + - [Link Snapshot](#link-snapshot) - [License](#license) --- @@ -173,6 +174,31 @@ For more details on Mintlify setup and configuration, visit the [official Mintli The [`check-docs.yaml`](.github/workflows/check-docs.yaml) workflow has a `check-link` job that examine markdown links. Customize the config in [`link-check.json`](./link-check.json). If a link cannot be accessed (e.g. Github private repo), add the URL pattern to the `ignorePatterns` array. +### Link Snapshot + +[`link-snapshot.yaml`](./link-snapshot.yaml) is an append-only record of every URL the docs site has ever served (pages from `docs.json` navigation plus redirect sources). The [`check-link-snapshot.yaml`](.github/workflows/check-link-snapshot.yaml) workflow fails any PR whose changes would cause one of those URLs to stop resolving. + +One-time setup so the pre-commit hook keeps the snapshot fresh: + +```bash +npm install +``` + +The hook regenerates the snapshot whenever `docs.json` or any `.mdx`/`.md` file is staged. + +When the CI check fails on a URL, pick one of three remediations: + +1. **Add a redirect** under `redirects` in `docs.json` pointing to a current page. +2. **Keep the page on disk but remove it from `docs.json` navigation** — the URL stays served but undiscoverable. Add a deprecation note at the top of the page. +3. **Move the URL** from `active` to `deleted` in `link-snapshot.yaml` with a `reason` and `retired_at` date. Use this only when no good substitute exists. + +To regenerate or verify manually: + +```bash +npm run snapshot:update # regenerate link-snapshot.yaml +npm run snapshot:check # verify the contract (what CI runs) +``` + --- ## License From f19c5e7494bd2433301858f8d373906d2df694fc Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 19:47:45 -0700 Subject: [PATCH 06/15] Update ci workflows --- .github/workflows/check-docs.yaml | 2 +- .github/workflows/check-link-snapshot.yaml | 6 +++--- .github/workflows/check-notebooks.yaml | 2 +- .github/workflows/run-notebooks.yaml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/check-docs.yaml b/.github/workflows/check-docs.yaml index bca45dd6..8e198a3f 100644 --- a/.github/workflows/check-docs.yaml +++ b/.github/workflows/check-docs.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Check markdown links uses: gaurav-nelson/github-action-markdown-link-check@v1 # use to skip next line diff --git a/.github/workflows/check-link-snapshot.yaml b/.github/workflows/check-link-snapshot.yaml index b598a29a..d9f69847 100644 --- a/.github/workflows/check-link-snapshot.yaml +++ b/.github/workflows/check-link-snapshot.yaml @@ -13,10 +13,10 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: - node-version: "20" + node-version: "22" cache: "npm" - run: npm ci - name: Verify snapshot contract diff --git a/.github/workflows/check-notebooks.yaml b/.github/workflows/check-notebooks.yaml index 6641df63..cfa311d9 100644 --- a/.github/workflows/check-notebooks.yaml +++ b/.github/workflows/check-notebooks.yaml @@ -27,7 +27,7 @@ jobs: working-directory: notebooks steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/run-notebooks.yaml b/.github/workflows/run-notebooks.yaml index 0312ae06..b6ac4d76 100644 --- a/.github/workflows/run-notebooks.yaml +++ b/.github/workflows/run-notebooks.yaml @@ -22,7 +22,7 @@ jobs: notebooks: ${{ steps.list.outputs.notebooks }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: List notebooks id: list @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v5 From 2945fb12134c44ae28b372f861c09695f8c5e8a8 Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 20:10:50 -0700 Subject: [PATCH 07/15] Show a concrete deleted-entry example in CI diagnostics The previous contract-violation message told contributors to "move the URL to deleted with a reason" but did not show the YAML shape, so it was unclear whether reason was a quoted string, required, or how to add a retired_at field. Two changes: 1. Both the contract-violation message and a new "malformed deleted entry" check now print a fenced YAML example. 2. Each deleted entry is now validated to have a non-empty reason field; bare URLs under deleted are rejected at check time with the same example. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/generateLinkSnapshot.ts | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/scripts/generateLinkSnapshot.ts b/scripts/generateLinkSnapshot.ts index 4ce37e38..8a018cba 100644 --- a/scripts/generateLinkSnapshot.ts +++ b/scripts/generateLinkSnapshot.ts @@ -13,6 +13,13 @@ const SNAPSHOT_HEADER = `# Auto-managed by scripts/generateLinkSnapshot.ts. # \`deleted\` with a reason. The CI check skips entries listed under \`deleted\`. `; +const DELETED_EXAMPLE = ` deleted: + # Minimal form — git history is the canonical record of why this URL was retired: + - /lfm/old/experimental-thing + # Or, if you want the reason inline: + - url: /lfm/another-old-thing + reason: "Page retired in DOC-12; no substitute exists."`; + // Directories whose .mdx/.md files map to docs URLs. snippets/ is excluded // because it holds reusable fragments, not pages. const PAGE_DIRS = ['lfm', 'leap', 'examples', 'deployment']; @@ -33,10 +40,13 @@ type NavNode = tabs?: NavNode[]; }; -interface DeletedEntry { - url: string; - reason?: string; - retired_at?: string; +// A deleted entry is either a bare URL string (minimal form — commit history +// is the record of why) or an object with `url` plus optional `reason` / +// `retired_at` fields if the contributor wants the rationale inline. +type DeletedEntry = string | { url: string; reason?: string; retired_at?: string }; + +function deletedUrl(entry: DeletedEntry): string { + return typeof entry === 'string' ? entry : entry.url; } interface Snapshot { @@ -115,7 +125,7 @@ function loadSnapshot(): Snapshot { const parsed = YAML.parse(raw) ?? {}; return { active: Array.isArray(parsed.active) ? parsed.active.map(String) : [], - deleted: Array.isArray(parsed.deleted) ? parsed.deleted : [], + deleted: Array.isArray(parsed.deleted) ? (parsed.deleted as DeletedEntry[]) : [], }; } @@ -134,7 +144,7 @@ function computeUpdatedSnapshot(docs: DocsJson, prev: Snapshot): Snapshot { const fromNav = navUrls(docs); const fromRedirects = redirectSources(docs); const fromDisk = diskPageUrls(); - const deletedUrls = new Set(prev.deleted.map((d) => d.url)); + const deletedUrls = new Set(prev.deleted.map(deletedUrl)); // Start from prior active, minus anything the user has since moved to `deleted`. const merged = new Set([...prev.active].filter((url) => !deletedUrls.has(url))); for (const url of fromNav) if (!deletedUrls.has(url)) merged.add(url); @@ -197,7 +207,7 @@ function checkContract(docs: DocsJson, snap: Snapshot): { ok: boolean; failures: destination: normalizeUrl(r.destination), })), }; - const deleted = new Set(snap.deleted.map((d) => d.url)); + const deleted = new Set(snap.deleted.map(deletedUrl)); const failures: string[] = []; for (const url of snap.active) { if (deleted.has(url)) continue; @@ -247,7 +257,12 @@ function main(): void { console.error(' 1. Add a redirect entry under `redirects` in docs.json pointing to a current page.'); console.error(' 2. Keep the underlying .mdx file on disk but remove it from docs.json navigation'); console.error(' (the URL stays served but undiscoverable — mark the page as deprecated).'); - console.error(' 3. Move the URL from `active` to `deleted` in link-snapshot.yaml with a reason.'); + console.error(' 3. Move the URL from `active` to `deleted` in link-snapshot.yaml. The'); + console.error(' minimal form is just the URL string; add an inline `reason` if you want'); + console.error(' it embedded next to the entry (otherwise the commit history is the'); + console.error(' record). Example:'); + console.error(''); + console.error(DELETED_EXAMPLE); process.exit(1); } if (stale) { From 9251fe59b662d287bb8b8fe69dca7374f990ad2a Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 20:13:03 -0700 Subject: [PATCH 08/15] Update workflows --- .github/workflows/check-notebooks.yaml | 2 +- .github/workflows/run-notebooks.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-notebooks.yaml b/.github/workflows/check-notebooks.yaml index cfa311d9..76429735 100644 --- a/.github/workflows/check-notebooks.yaml +++ b/.github/workflows/check-notebooks.yaml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' diff --git a/.github/workflows/run-notebooks.yaml b/.github/workflows/run-notebooks.yaml index b6ac4d76..3d4171a3 100644 --- a/.github/workflows/run-notebooks.yaml +++ b/.github/workflows/run-notebooks.yaml @@ -38,7 +38,7 @@ jobs: uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' @@ -63,10 +63,10 @@ jobs: notebook: ${{ fromJSON(needs.discover-notebooks.outputs.notebooks) }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' From ccb83fa3763bf3ae97f4c51892b504fe08926191 Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 20:16:36 -0700 Subject: [PATCH 09/15] Move link snapshot docs from README to CLAUDE.md README now carries a one-paragraph orientation pointing readers at CLAUDE.md for the full mechanics. CLAUDE.md gains a new "Link Snapshot Contract" section covering the file format, the resolution algorithm CI uses to decide whether an `active` URL is satisfied, the three remediation paths in priority order, and the common pitfalls (forgetting redirects on path renames, skipping `npm install`, manually editing `active`). Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 23 +-------------------- 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d5206beb..93b27d29 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -182,6 +182,68 @@ grep -r '/>' --include="*.mdx" --include="*.md" . 3. Check if you need to target light/dark mode specifically 4. For icons using mask-image, set `background-color` instead of `color` +## Link Snapshot Contract + +`link-snapshot.yaml` at the repo root is the **contract** of every URL the docs site has promised to keep resolving. It is checked into git, regenerated by a pre-commit hook, and enforced by [`check-link-snapshot.yaml`](.github/workflows/check-link-snapshot.yaml) on every PR. + +### Why this exists + +Mintlify's URL space is derived from `docs.json` navigation plus `redirects[]`. Removing a page or a redirect silently breaks any external blog post, search-engine result, or shared bookmark that pointed at the old URL. The snapshot is an append-only ledger so that deletions become a deliberate decision rather than an accidental side effect. + +### File shape + +```yaml +active: + - /deployment/gpu-inference/baseten + - /docs/getting-started/welcome # served via a redirect + - /lfm/getting-started/welcome + # ...sorted, one URL per line... +deleted: + # Bare URL — git history is the canonical record of why this was retired: + - /lfm/old/experimental-thing + # Or, if you want the reason inline: + - url: /lfm/another-old-thing + reason: "Retired in DOC-12; no substitute exists." +``` + +- `active` is **append-only**. The pre-commit hook adds new URLs (nav pages, literal redirect sources, on-disk `.mdx`/`.md` files under `lfm/`, `leap/`, `examples/`, `deployment/`) but never silently removes them. +- `deleted` is hand-edited. Either bare-string entries or `{url, reason?, retired_at?}` objects are accepted; the serializer preserves whichever shape you wrote. +- Wildcard redirect sources (e.g. `/docs/models/:slug*`) are **not** snapshotted as `active` entries — they're patterns, not URLs anyone visits directly. The check still resolves URLs through them. + +### Tooling + +| Command | What it does | +| --- | --- | +| `npm install` | One-time. Installs deps and wires up the husky pre-commit hook via the `prepare` script. | +| `npm run snapshot:update` | Regenerates `link-snapshot.yaml` (append-only union with existing `active`). The pre-commit hook runs this automatically when `docs.json` or any `.mdx`/`.md` file is staged, then `git add`s the snapshot. | +| `npm run snapshot:check` | What CI runs. Verifies every `active` URL still resolves and fails non-zero with a per-URL diagnostic otherwise. | + +Both subcommands invoke `scripts/generateLinkSnapshot.ts` through `scripts/runScriptWithEnv.sh`, mirroring the `npm run script scripts/.ts` convention from the wider monorepo. New TypeScript scripts should follow that same pattern. + +### Remediation when CI flags a URL + +Pick exactly one path per flagged URL. In rough order of preference: + +1. **Add a redirect.** Append a `{source, destination}` entry under `redirects` in `docs.json`. Best when there's a clear substitute page — preserves the URL contract for external linkers and search engines. +2. **Deprecate in place.** Remove the page from the `docs.json` navigation tree, but leave the `.mdx` file on disk. The URL keeps resolving (the disk scan picks it up) but the page is undiscoverable from the nav. Add a `` banner at the top of the page indicating deprecation. Use this when the content is still accurate but no longer something we want to surface. +3. **Mark as deleted.** Move the URL from `active` to `deleted` in `link-snapshot.yaml`. Use only when the page is gone, no substitute exists, and we accept that external links to it will 404. The minimal form is just the URL string; commit history is the record of why. Add an inline `reason` if you want the rationale next to the entry. + +### Resolution algorithm (for understanding CI failures) + +A URL in `active` is considered resolved if any of these are true: + +1. It matches a string-or-`root` entry in the `docs.json` navigation tree. +2. A corresponding `.mdx` or `.md` file exists on disk under `lfm/`, `leap/`, `examples/`, or `deployment/`. +3. It matches a `redirects[*].source` (literal or `:slug*` wildcard prefix), and the destination itself resolves recursively (max depth 5, with a cycle guard). + +If none of those hold, CI fails and prints the URL plus the three remediation options. + +### Common pitfalls + +- **Editing a page's path in `docs.json` without adding a redirect.** This is the most common cause of CI failure. Renaming `lfm/foo` to `lfm/bar` removes `/lfm/foo` from navigation; add a redirect from `/lfm/foo` to `/lfm/bar`. +- **Skipping `npm install` on a fresh clone.** Without the husky hook, the pre-commit step doesn't run and the snapshot drifts. CI catches this — `--check` re-runs the update logic in dry-run mode and fails if the snapshot would change — but the fix is still to run `npm install`. +- **Manually deleting entries from `active`.** Don't. The snapshot is append-only by design. To retire a URL, move it to `deleted`; the next `snapshot:update` (or pre-commit hook firing) will drop it from `active` automatically. + ## Git Commits - Never create a single monolithic commit for multi-step work. Break commits into logical units (e.g., infrastructure/scaffolding, feature A, feature B, fixes). diff --git a/README.md b/README.md index c68cc8a0..d9ddebf8 100644 --- a/README.md +++ b/README.md @@ -176,28 +176,7 @@ The [`check-docs.yaml`](.github/workflows/check-docs.yaml) workflow has a `check ### Link Snapshot -[`link-snapshot.yaml`](./link-snapshot.yaml) is an append-only record of every URL the docs site has ever served (pages from `docs.json` navigation plus redirect sources). The [`check-link-snapshot.yaml`](.github/workflows/check-link-snapshot.yaml) workflow fails any PR whose changes would cause one of those URLs to stop resolving. - -One-time setup so the pre-commit hook keeps the snapshot fresh: - -```bash -npm install -``` - -The hook regenerates the snapshot whenever `docs.json` or any `.mdx`/`.md` file is staged. - -When the CI check fails on a URL, pick one of three remediations: - -1. **Add a redirect** under `redirects` in `docs.json` pointing to a current page. -2. **Keep the page on disk but remove it from `docs.json` navigation** — the URL stays served but undiscoverable. Add a deprecation note at the top of the page. -3. **Move the URL** from `active` to `deleted` in `link-snapshot.yaml` with a `reason` and `retired_at` date. Use this only when no good substitute exists. - -To regenerate or verify manually: - -```bash -npm run snapshot:update # regenerate link-snapshot.yaml -npm run snapshot:check # verify the contract (what CI runs) -``` +[`link-snapshot.yaml`](./link-snapshot.yaml) records every URL the docs site has served. The [`check-link-snapshot.yaml`](.github/workflows/check-link-snapshot.yaml) workflow fails any PR that would make a recorded URL stop resolving — add a redirect, leave the page on disk but drop it from navigation, or move the URL to `deleted` in the snapshot. Run `npm install` once to install the pre-commit hook that keeps the snapshot in sync. See [CLAUDE.md](./CLAUDE.md#link-snapshot-contract) for details. --- From 3fff5b71afa7597c65dd25013bee653c7c6813ca Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 20:30:51 -0700 Subject: [PATCH 10/15] Include deleted-entry examples in the snapshot file header The header on link-snapshot.yaml already explained that contributors move URLs from active to deleted; it did not show what either form looks like. Two commented examples now ride along with the file so that someone editing it directly does not need to cross-reference CLAUDE.md or trigger a CI failure to discover the shape. Co-Authored-By: Claude Opus 4.7 (1M context) --- link-snapshot.yaml | 11 ++++++++++- scripts/generateLinkSnapshot.ts | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/link-snapshot.yaml b/link-snapshot.yaml index a6cd0247..17b17f99 100644 --- a/link-snapshot.yaml +++ b/link-snapshot.yaml @@ -1,7 +1,16 @@ # Auto-managed by scripts/generateLinkSnapshot.ts. # `active` is append-only — pre-commit adds new URLs but never removes them. # To intentionally retire a URL without a redirect, move it from `active` to -# `deleted` with a reason. The CI check skips entries listed under `deleted`. +# `deleted`. The CI check skips entries listed under `deleted`. +# +# Examples of `deleted` entries (uncomment and adapt): +# +# deleted: +# # Minimal form — git history is the canonical record of why this was retired: +# - /lfm/old/experimental-thing +# # Or, if you want the reason inline: +# - url: /lfm/another-old-thing +# reason: "Page retired in DOC-12; no substitute exists." active: - /customization/finetuning-frameworks/datasets - /customization/finetuning-frameworks/leap-finetune diff --git a/scripts/generateLinkSnapshot.ts b/scripts/generateLinkSnapshot.ts index 8a018cba..7037eb7c 100644 --- a/scripts/generateLinkSnapshot.ts +++ b/scripts/generateLinkSnapshot.ts @@ -10,7 +10,16 @@ const SNAPSHOT_FILE = path.join(REPO_ROOT, 'link-snapshot.yaml'); const SNAPSHOT_HEADER = `# Auto-managed by scripts/generateLinkSnapshot.ts. # \`active\` is append-only — pre-commit adds new URLs but never removes them. # To intentionally retire a URL without a redirect, move it from \`active\` to -# \`deleted\` with a reason. The CI check skips entries listed under \`deleted\`. +# \`deleted\`. The CI check skips entries listed under \`deleted\`. +# +# Examples of \`deleted\` entries (uncomment and adapt): +# +# deleted: +# # Minimal form — git history is the canonical record of why this was retired: +# - /lfm/old/experimental-thing +# # Or, if you want the reason inline: +# - url: /lfm/another-old-thing +# reason: "Page retired in DOC-12; no substitute exists." `; const DELETED_EXAMPLE = ` deleted: From 9ea782b3cfe60aaad6addfe6d75925425697656b Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 20:31:46 -0700 Subject: [PATCH 11/15] Drop the shell wrapper and invoke tsx directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runScriptWithEnv.sh wrapper was a single exec call with no env-file handling — invoking tsx straight from package.json is one fewer indirection and removes the asymmetry with how scripts are typically run elsewhere. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 2 +- package.json | 5 ++--- scripts/runScriptWithEnv.sh | 10 ---------- 3 files changed, 3 insertions(+), 14 deletions(-) delete mode 100755 scripts/runScriptWithEnv.sh diff --git a/CLAUDE.md b/CLAUDE.md index 93b27d29..ffafeff9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -218,7 +218,7 @@ deleted: | `npm run snapshot:update` | Regenerates `link-snapshot.yaml` (append-only union with existing `active`). The pre-commit hook runs this automatically when `docs.json` or any `.mdx`/`.md` file is staged, then `git add`s the snapshot. | | `npm run snapshot:check` | What CI runs. Verifies every `active` URL still resolves and fails non-zero with a per-URL diagnostic otherwise. | -Both subcommands invoke `scripts/generateLinkSnapshot.ts` through `scripts/runScriptWithEnv.sh`, mirroring the `npm run script scripts/.ts` convention from the wider monorepo. New TypeScript scripts should follow that same pattern. +Both subcommands invoke `scripts/generateLinkSnapshot.ts` directly via `tsx`. New TypeScript scripts should follow the same pattern — a thin `tsx scripts/.ts` entry under `scripts` in `package.json`. ### Remediation when CI flags a URL diff --git a/package.json b/package.json index b02db198..58ebdd8c 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,8 @@ "version": "0.0.1", "private": true, "scripts": { - "script": "scripts/runScriptWithEnv.sh", - "snapshot:update": "./scripts/runScriptWithEnv.sh scripts/generateLinkSnapshot.ts --update", - "snapshot:check": "./scripts/runScriptWithEnv.sh scripts/generateLinkSnapshot.ts --check", + "snapshot:update": "tsx scripts/generateLinkSnapshot.ts --update", + "snapshot:check": "tsx scripts/generateLinkSnapshot.ts --check", "prepare": "husky" }, "devDependencies": { diff --git a/scripts/runScriptWithEnv.sh b/scripts/runScriptWithEnv.sh deleted file mode 100755 index da294e38..00000000 --- a/scripts/runScriptWithEnv.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# Minimal tsx-based script runner. -# -# Mirrors the calling convention of -# ~/git/app-monorepo-template/apps/web/scripts/runScriptWithEnv.sh -# (`npm run script scripts/.ts -- --flag`) but without the env-file -# selection, since this repo currently has no runtime env to load. Expand -# toward the template's shape if env-aware scripts get added later. -set -euo pipefail -exec npx tsx "$@" From fe875d4eb4193a0a40a0f40e1a42278a292c297a Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 20:32:45 -0700 Subject: [PATCH 12/15] Move the deleted-entry examples next to the deleted field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the examples sat at the top of the file they were easy to skim past — by the time a contributor scrolled down to `deleted:` they had to remember a YAML shape from screens earlier. Splitting the serializer so it emits `active:` first, then the example block, then `deleted:` keeps the examples adjacent to the field they describe. Co-Authored-By: Claude Opus 4.7 (1M context) --- link-snapshot.yaml | 17 ++++++++--------- scripts/generateLinkSnapshot.ts | 16 ++++++---------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/link-snapshot.yaml b/link-snapshot.yaml index 17b17f99..7b89d11c 100644 --- a/link-snapshot.yaml +++ b/link-snapshot.yaml @@ -2,15 +2,6 @@ # `active` is append-only — pre-commit adds new URLs but never removes them. # To intentionally retire a URL without a redirect, move it from `active` to # `deleted`. The CI check skips entries listed under `deleted`. -# -# Examples of `deleted` entries (uncomment and adapt): -# -# deleted: -# # Minimal form — git history is the canonical record of why this was retired: -# - /lfm/old/experimental-thing -# # Or, if you want the reason inline: -# - url: /lfm/another-old-thing -# reason: "Page retired in DOC-12; no substitute exists." active: - /customization/finetuning-frameworks/datasets - /customization/finetuning-frameworks/leap-finetune @@ -173,4 +164,12 @@ active: - /lfm/models/liquid-nanos - /lfm/models/text-models - /lfm/models/vision-models +# Examples of `deleted` entries (uncomment and adapt): +# +# deleted: +# # Minimal form — git history is the canonical record of why this was retired: +# - /lfm/old/experimental-thing +# # Or, if you want the reason inline: +# - url: /lfm/another-old-thing +# reason: "Page retired in DOC-12; no substitute exists." deleted: [] diff --git a/scripts/generateLinkSnapshot.ts b/scripts/generateLinkSnapshot.ts index 7037eb7c..cb93fa4a 100644 --- a/scripts/generateLinkSnapshot.ts +++ b/scripts/generateLinkSnapshot.ts @@ -11,8 +11,9 @@ const SNAPSHOT_HEADER = `# Auto-managed by scripts/generateLinkSnapshot.ts. # \`active\` is append-only — pre-commit adds new URLs but never removes them. # To intentionally retire a URL without a redirect, move it from \`active\` to # \`deleted\`. The CI check skips entries listed under \`deleted\`. -# -# Examples of \`deleted\` entries (uncomment and adapt): +`; + +const DELETED_PREAMBLE = `# Examples of \`deleted\` entries (uncomment and adapt): # # deleted: # # Minimal form — git history is the canonical record of why this was retired: @@ -139,14 +140,9 @@ function loadSnapshot(): Snapshot { } function serializeSnapshot(snap: Snapshot): string { - const body = YAML.stringify( - { - active: [...snap.active].sort(), - deleted: snap.deleted, - }, - { lineWidth: 0 }, - ); - return SNAPSHOT_HEADER + body; + const activeYaml = YAML.stringify({ active: [...snap.active].sort() }, { lineWidth: 0 }); + const deletedYaml = YAML.stringify({ deleted: snap.deleted }, { lineWidth: 0 }); + return SNAPSHOT_HEADER + activeYaml + DELETED_PREAMBLE + deletedYaml; } function computeUpdatedSnapshot(docs: DocsJson, prev: Snapshot): Snapshot { From 00235c7ddd7571ab32f384776fd4f83a2fcc2c60 Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 20:36:09 -0700 Subject: [PATCH 13/15] Trim the link-snapshot section in CLAUDE.md Compressed from 60+ lines to ~17. Cut the file-shape YAML block (the snapshot file now carries its own example comments), the tooling table (folded into prose), and the common-pitfalls section. Kept the resolution algorithm and the three remediation paths since those are what a contributor needs to act on a CI failure. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 65 +++++++++---------------------------------------------- README.md | 3 ++- 2 files changed, 12 insertions(+), 56 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ffafeff9..8ea7d63d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -184,65 +184,20 @@ grep -r '/>' --include="*.mdx" --include="*.md" . ## Link Snapshot Contract -`link-snapshot.yaml` at the repo root is the **contract** of every URL the docs site has promised to keep resolving. It is checked into git, regenerated by a pre-commit hook, and enforced by [`check-link-snapshot.yaml`](.github/workflows/check-link-snapshot.yaml) on every PR. - -### Why this exists - -Mintlify's URL space is derived from `docs.json` navigation plus `redirects[]`. Removing a page or a redirect silently breaks any external blog post, search-engine result, or shared bookmark that pointed at the old URL. The snapshot is an append-only ledger so that deletions become a deliberate decision rather than an accidental side effect. - -### File shape - -```yaml -active: - - /deployment/gpu-inference/baseten - - /docs/getting-started/welcome # served via a redirect - - /lfm/getting-started/welcome - # ...sorted, one URL per line... -deleted: - # Bare URL — git history is the canonical record of why this was retired: - - /lfm/old/experimental-thing - # Or, if you want the reason inline: - - url: /lfm/another-old-thing - reason: "Retired in DOC-12; no substitute exists." -``` - -- `active` is **append-only**. The pre-commit hook adds new URLs (nav pages, literal redirect sources, on-disk `.mdx`/`.md` files under `lfm/`, `leap/`, `examples/`, `deployment/`) but never silently removes them. -- `deleted` is hand-edited. Either bare-string entries or `{url, reason?, retired_at?}` objects are accepted; the serializer preserves whichever shape you wrote. -- Wildcard redirect sources (e.g. `/docs/models/:slug*`) are **not** snapshotted as `active` entries — they're patterns, not URLs anyone visits directly. The check still resolves URLs through them. - -### Tooling - -| Command | What it does | -| --- | --- | -| `npm install` | One-time. Installs deps and wires up the husky pre-commit hook via the `prepare` script. | -| `npm run snapshot:update` | Regenerates `link-snapshot.yaml` (append-only union with existing `active`). The pre-commit hook runs this automatically when `docs.json` or any `.mdx`/`.md` file is staged, then `git add`s the snapshot. | -| `npm run snapshot:check` | What CI runs. Verifies every `active` URL still resolves and fails non-zero with a per-URL diagnostic otherwise. | - -Both subcommands invoke `scripts/generateLinkSnapshot.ts` directly via `tsx`. New TypeScript scripts should follow the same pattern — a thin `tsx scripts/.ts` entry under `scripts` in `package.json`. - -### Remediation when CI flags a URL - -Pick exactly one path per flagged URL. In rough order of preference: - -1. **Add a redirect.** Append a `{source, destination}` entry under `redirects` in `docs.json`. Best when there's a clear substitute page — preserves the URL contract for external linkers and search engines. -2. **Deprecate in place.** Remove the page from the `docs.json` navigation tree, but leave the `.mdx` file on disk. The URL keeps resolving (the disk scan picks it up) but the page is undiscoverable from the nav. Add a `` banner at the top of the page indicating deprecation. Use this when the content is still accurate but no longer something we want to surface. -3. **Mark as deleted.** Move the URL from `active` to `deleted` in `link-snapshot.yaml`. Use only when the page is gone, no substitute exists, and we accept that external links to it will 404. The minimal form is just the URL string; commit history is the record of why. Add an inline `reason` if you want the rationale next to the entry. - -### Resolution algorithm (for understanding CI failures) - -A URL in `active` is considered resolved if any of these are true: +`link-snapshot.yaml` is an append-only ledger of every URL the docs site has promised to keep resolving. The pre-commit hook keeps it in sync; [`check-link-snapshot.yaml`](.github/workflows/check-link-snapshot.yaml) fails any PR that would make a recorded URL stop resolving. Run `npm install` once to install the hook. -1. It matches a string-or-`root` entry in the `docs.json` navigation tree. -2. A corresponding `.mdx` or `.md` file exists on disk under `lfm/`, `leap/`, `examples/`, or `deployment/`. -3. It matches a `redirects[*].source` (literal or `:slug*` wildcard prefix), and the destination itself resolves recursively (max depth 5, with a cycle guard). +A URL in `active` resolves if any of these are true: +1. It is in the `docs.json` navigation tree. +2. A matching `.mdx`/`.md` file exists under `lfm/`, `leap/`, `examples/`, or `deployment/`. +3. It matches a `redirects[*].source` (literal or `:slug*` prefix) whose destination itself resolves (recursive, max depth 5). -If none of those hold, CI fails and prints the URL plus the three remediation options. +When CI flags a URL, pick one (in order of preference): -### Common pitfalls +1. **Add a redirect** in `docs.json` — best when a substitute page exists. +2. **Deprecate in place** — remove the page from `docs.json` navigation but leave the `.mdx` on disk (the URL stays served, just undiscoverable). Add a `` deprecation banner. +3. **Mark deleted** — move the URL from `active` to `deleted` in `link-snapshot.yaml`. Bare URLs are fine; commit history is the record. Use only when no substitute exists. -- **Editing a page's path in `docs.json` without adding a redirect.** This is the most common cause of CI failure. Renaming `lfm/foo` to `lfm/bar` removes `/lfm/foo` from navigation; add a redirect from `/lfm/foo` to `/lfm/bar`. -- **Skipping `npm install` on a fresh clone.** Without the husky hook, the pre-commit step doesn't run and the snapshot drifts. CI catches this — `--check` re-runs the update logic in dry-run mode and fails if the snapshot would change — but the fix is still to run `npm install`. -- **Manually deleting entries from `active`.** Don't. The snapshot is append-only by design. To retire a URL, move it to `deleted`; the next `snapshot:update` (or pre-commit hook firing) will drop it from `active` automatically. +Don't edit `active` by hand — moving a URL to `deleted` causes the next `snapshot:update` (which the pre-commit hook runs automatically) to drop it from `active`. Manual commands: `npm run snapshot:update` to regenerate, `npm run snapshot:check` to run what CI runs. Both call `scripts/generateLinkSnapshot.ts` via `tsx`; new TypeScript scripts should follow the same `tsx scripts/.ts` pattern. ## Git Commits diff --git a/README.md b/README.md index d9ddebf8..35b3f7ba 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,7 @@ Navigate to the docs directory and start the development server: ```bash cd docs +npm i mintlify dev ``` @@ -182,4 +183,4 @@ The [`check-docs.yaml`](.github/workflows/check-docs.yaml) workflow has a `check ## License -This documentation is licensed under [Attribution-ShareAlike 4.0 International](./LICENSE). \ No newline at end of file +This documentation is licensed under [Attribution-ShareAlike 4.0 International](./LICENSE). From e13688c076775bab3d093db823c9881f3023ebe0 Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 20:42:23 -0700 Subject: [PATCH 14/15] Add a bulleted explanation of the link-snapshot mechanism to README The previous one-paragraph orientation said what the snapshot was but not how the append-only invariant survives the pre-commit hook regenerating the file on every commit. Four bullets now make the mechanism explicit so contributors do not have to follow the link to CLAUDE.md to understand why a missing redirect breaks CI. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 35b3f7ba..76b617ec 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,16 @@ The [`check-docs.yaml`](.github/workflows/check-docs.yaml) workflow has a `check ### Link Snapshot -[`link-snapshot.yaml`](./link-snapshot.yaml) records every URL the docs site has served. The [`check-link-snapshot.yaml`](.github/workflows/check-link-snapshot.yaml) workflow fails any PR that would make a recorded URL stop resolving — add a redirect, leave the page on disk but drop it from navigation, or move the URL to `deleted` in the snapshot. Run `npm install` once to install the pre-commit hook that keeps the snapshot in sync. See [CLAUDE.md](./CLAUDE.md#link-snapshot-contract) for details. +[`link-snapshot.yaml`](./link-snapshot.yaml) records every URL the docs site has served. The [`check-link-snapshot.yaml`](.github/workflows/check-link-snapshot.yaml) workflow fails any PR that would make a recorded URL stop resolving. Run `npm install` once to install the pre-commit hook that keeps the snapshot in sync. + +How it works: + +- The pre-commit hook regenerates the snapshot whenever `docs.json` or any `.mdx`/`.md` file is staged. +- `active:` is **append-only** — new URLs are added, but existing entries are never silently dropped, even after you delete the underlying page. +- CI re-checks every `active` URL on each PR. A URL is satisfied if it's still in the navigation, still on disk, or reachable via a redirect chain. +- The only way to retire a URL is to move it to `deleted:` explicitly. This forces every removal to be a deliberate choice — redirect, deprecate-but-keep, or formally retire. + +See [CLAUDE.md](./CLAUDE.md#link-snapshot-contract) for the remediation paths and the full resolution algorithm. --- From 70c4d806237a098d1544db1c2609d80abc927c2c Mon Sep 17 00:00:00 2001 From: Liren Tu Date: Fri, 15 May 2026 20:43:26 -0700 Subject: [PATCH 15/15] Clarify what `active:` refers to in the README bullets A leading bullet now names `active:` and `deleted:` as the two sections of link-snapshot.yaml so the append-only invariant in the following bullets reads as "the active field stays append-only," not "active is append-only" without antecedent. Also pulled the hand-edit clue into the retire-a-URL bullet. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 76b617ec..955ffc03 100644 --- a/README.md +++ b/README.md @@ -181,10 +181,11 @@ The [`check-docs.yaml`](.github/workflows/check-docs.yaml) workflow has a `check How it works: -- The pre-commit hook regenerates the snapshot whenever `docs.json` or any `.mdx`/`.md` file is staged. -- `active:` is **append-only** — new URLs are added, but existing entries are never silently dropped, even after you delete the underlying page. -- CI re-checks every `active` URL on each PR. A URL is satisfied if it's still in the navigation, still on disk, or reachable via a redirect chain. -- The only way to retire a URL is to move it to `deleted:` explicitly. This forces every removal to be a deliberate choice — redirect, deprecate-but-keep, or formally retire. +- `link-snapshot.yaml` has two sections: `active:` (URLs the site is currently committed to keep serving) and `deleted:` (URLs intentionally retired). +- The pre-commit hook regenerates the file whenever `docs.json` or any `.mdx`/`.md` file is staged. +- The `active:` section is **append-only** — new URLs are added when pages are created, but existing entries are never silently dropped, even after you delete the underlying page. +- CI re-checks every URL in `active:` on each PR. A URL is satisfied if it's still in the navigation, still on disk, or reachable via a redirect chain. +- The only way to retire a URL is to move it from `active:` to `deleted:` by hand. This forces every removal to be a deliberate choice — redirect, deprecate-but-keep, or formally retire. See [CLAUDE.md](./CLAUDE.md#link-snapshot-contract) for the remediation paths and the full resolution algorithm.