mirror of
https://github.com/supabase/agent-skills.git
synced 2026-01-26 19:09:51 +08:00
Initial setup: PostgreSQL best practices repository
Skeleton structure for Supabase PostgreSQL experts to add performance optimization rules. Modeled after Vercel's react-best-practices-build. Includes: - Build system (parser, validator, builder) - Skill manifest and metadata - Rule templates and writing guidelines - CI workflow for validation - Getting started guide for Postgres team Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
590
packages/postgresql-best-practices-build/package-lock.json
generated
Normal file
590
packages/postgresql-best-practices-build/package-lock.json
generated
Normal file
@@ -0,0 +1,590 @@
|
||||
{
|
||||
"name": "postgresql-best-practices-build",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "postgresql-best-practices-build",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
||||
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
|
||||
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
|
||||
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
|
||||
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
|
||||
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
|
||||
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
|
||||
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
|
||||
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
|
||||
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
|
||||
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
|
||||
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
||||
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.2",
|
||||
"@esbuild/android-arm": "0.27.2",
|
||||
"@esbuild/android-arm64": "0.27.2",
|
||||
"@esbuild/android-x64": "0.27.2",
|
||||
"@esbuild/darwin-arm64": "0.27.2",
|
||||
"@esbuild/darwin-x64": "0.27.2",
|
||||
"@esbuild/freebsd-arm64": "0.27.2",
|
||||
"@esbuild/freebsd-x64": "0.27.2",
|
||||
"@esbuild/linux-arm": "0.27.2",
|
||||
"@esbuild/linux-arm64": "0.27.2",
|
||||
"@esbuild/linux-ia32": "0.27.2",
|
||||
"@esbuild/linux-loong64": "0.27.2",
|
||||
"@esbuild/linux-mips64el": "0.27.2",
|
||||
"@esbuild/linux-ppc64": "0.27.2",
|
||||
"@esbuild/linux-riscv64": "0.27.2",
|
||||
"@esbuild/linux-s390x": "0.27.2",
|
||||
"@esbuild/linux-x64": "0.27.2",
|
||||
"@esbuild/netbsd-arm64": "0.27.2",
|
||||
"@esbuild/netbsd-x64": "0.27.2",
|
||||
"@esbuild/openbsd-arm64": "0.27.2",
|
||||
"@esbuild/openbsd-x64": "0.27.2",
|
||||
"@esbuild/openharmony-arm64": "0.27.2",
|
||||
"@esbuild/sunos-x64": "0.27.2",
|
||||
"@esbuild/win32-arm64": "0.27.2",
|
||||
"@esbuild/win32-ia32": "0.27.2",
|
||||
"@esbuild/win32-x64": "0.27.2"
|
||||
}
|
||||
},
|
||||
"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/get-tsconfig": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
||||
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
18
packages/postgresql-best-practices-build/package.json
Normal file
18
packages/postgresql-best-practices-build/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "postgresql-best-practices-build",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsx src/build.ts",
|
||||
"build-agents": "tsx src/build.ts --agents-only",
|
||||
"validate": "tsx src/validate.ts",
|
||||
"extract-tests": "tsx src/extract-tests.ts",
|
||||
"dev": "npm run validate && npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
247
packages/postgresql-best-practices-build/src/build.ts
Normal file
247
packages/postgresql-best-practices-build/src/build.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { readdirSync, readFileSync, writeFileSync, existsSync } from "fs";
|
||||
import { join, basename } from "path";
|
||||
import { parseRuleFile } from "./parser.js";
|
||||
import { validateRuleFile } from "./validate.js";
|
||||
import { RULES_DIR, AGENTS_OUTPUT, METADATA_FILE } from "./config.js";
|
||||
import type { Rule, Metadata, Section } from "./types.js";
|
||||
|
||||
/**
|
||||
* Parse section definitions from _sections.md
|
||||
*/
|
||||
function parseSections(): Section[] {
|
||||
const sectionsFile = join(RULES_DIR, "_sections.md");
|
||||
if (!existsSync(sectionsFile)) {
|
||||
console.warn("Warning: _sections.md not found, using default sections");
|
||||
return getDefaultSections();
|
||||
}
|
||||
|
||||
const content = readFileSync(sectionsFile, "utf-8");
|
||||
const sections: Section[] = [];
|
||||
|
||||
const sectionMatches = content.matchAll(
|
||||
/##\s+(\d+)\.\s+([^\n(]+)\s*\((\w+)\)\s*\n\*\*Impact:\*\*\s*(\w+(?:-\w+)?)\s*\n\*\*Description:\*\*\s*([^\n]+)/g
|
||||
);
|
||||
|
||||
for (const match of sectionMatches) {
|
||||
sections.push({
|
||||
number: parseInt(match[1], 10),
|
||||
title: match[2].trim(),
|
||||
prefix: match[3].trim(),
|
||||
impact: match[4].trim() as Section["impact"],
|
||||
description: match[5].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
return sections.length > 0 ? sections : getDefaultSections();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default sections if _sections.md is missing or unparseable
|
||||
*/
|
||||
function getDefaultSections(): Section[] {
|
||||
return [
|
||||
{ number: 1, title: "Query Performance", prefix: "query", impact: "CRITICAL", description: "Slow queries, missing indexes, inefficient plans" },
|
||||
{ number: 2, title: "Connection Management", prefix: "conn", impact: "CRITICAL", description: "Pooling, limits, serverless strategies" },
|
||||
{ number: 3, title: "Schema Design", prefix: "schema", impact: "HIGH", description: "Table design, indexes, partitioning, data types" },
|
||||
{ number: 4, title: "Concurrency & Locking", prefix: "lock", impact: "MEDIUM-HIGH", description: "Transactions, isolation, deadlocks" },
|
||||
{ number: 5, title: "Security & RLS", prefix: "security", impact: "MEDIUM-HIGH", description: "Row-Level Security, privileges, auth patterns" },
|
||||
{ number: 6, title: "Data Access Patterns", prefix: "data", impact: "MEDIUM", description: "N+1 queries, batch operations, pagination" },
|
||||
{ number: 7, title: "Monitoring & Diagnostics", prefix: "monitor", impact: "LOW-MEDIUM", description: "pg_stat_statements, EXPLAIN, metrics" },
|
||||
{ number: 8, title: "Advanced Features", prefix: "advanced", impact: "LOW", description: "Full-text search, JSONB, extensions" },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load metadata from metadata.json
|
||||
*/
|
||||
function loadMetadata(): Metadata {
|
||||
if (!existsSync(METADATA_FILE)) {
|
||||
return {
|
||||
version: "0.1.0",
|
||||
organization: "Supabase",
|
||||
date: new Date().toLocaleDateString("en-US", { month: "long", year: "numeric" }),
|
||||
abstract: "PostgreSQL performance optimization guide for developers.",
|
||||
references: [],
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.parse(readFileSync(METADATA_FILE, "utf-8"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate anchor from title
|
||||
*/
|
||||
function toAnchor(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, "")
|
||||
.replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build AGENTS.md from all rule files
|
||||
*/
|
||||
function buildAgents(): void {
|
||||
console.log("Building AGENTS.md...\n");
|
||||
|
||||
// Load metadata and sections
|
||||
const metadata = loadMetadata();
|
||||
const sections = parseSections();
|
||||
|
||||
// Get all rule files
|
||||
const ruleFiles = readdirSync(RULES_DIR)
|
||||
.filter((f) => f.endsWith(".md") && !f.startsWith("_"))
|
||||
.map((f) => join(RULES_DIR, f));
|
||||
|
||||
if (ruleFiles.length === 0) {
|
||||
console.log("No rule files found. Generating empty AGENTS.md template.");
|
||||
}
|
||||
|
||||
// Parse and validate all rules
|
||||
const rules: Rule[] = [];
|
||||
|
||||
for (const file of ruleFiles) {
|
||||
const validation = validateRuleFile(file);
|
||||
if (!validation.valid) {
|
||||
console.error(`Skipping invalid file ${basename(file)}:`);
|
||||
validation.errors.forEach((e) => console.error(` - ${e}`));
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = parseRuleFile(file);
|
||||
if (result.success && result.rule) {
|
||||
rules.push(result.rule);
|
||||
}
|
||||
}
|
||||
|
||||
// Group rules by section and assign IDs
|
||||
const rulesBySection = new Map<number, Rule[]>();
|
||||
|
||||
for (const rule of rules) {
|
||||
const sectionRules = rulesBySection.get(rule.section) || [];
|
||||
sectionRules.push(rule);
|
||||
rulesBySection.set(rule.section, sectionRules);
|
||||
}
|
||||
|
||||
// Sort rules within each section and assign IDs
|
||||
for (const [sectionNum, sectionRules] of rulesBySection) {
|
||||
sectionRules.sort((a, b) => a.title.localeCompare(b.title));
|
||||
sectionRules.forEach((rule, index) => {
|
||||
rule.id = `${sectionNum}.${index + 1}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate markdown output
|
||||
const output: string[] = [];
|
||||
|
||||
// Header
|
||||
output.push("# PostgreSQL Best Practices\n");
|
||||
output.push(`**Version ${metadata.version}**`);
|
||||
output.push(`${metadata.organization}`);
|
||||
output.push(`${metadata.date}\n`);
|
||||
output.push("> This document is optimized for AI agents and LLMs. Rules are prioritized by performance impact.\n");
|
||||
output.push("---\n");
|
||||
|
||||
// Abstract
|
||||
output.push("## Abstract\n");
|
||||
output.push(`${metadata.abstract}\n`);
|
||||
output.push("---\n");
|
||||
|
||||
// Table of Contents
|
||||
output.push("## Table of Contents\n");
|
||||
|
||||
for (const section of sections) {
|
||||
const sectionRules = rulesBySection.get(section.number) || [];
|
||||
output.push(`${section.number}. [${section.title}](#${toAnchor(section.title)}) - **${section.impact}**`);
|
||||
|
||||
for (const rule of sectionRules) {
|
||||
output.push(` - ${rule.id} [${rule.title}](#${toAnchor(rule.id + "-" + rule.title)})`);
|
||||
}
|
||||
|
||||
output.push("");
|
||||
}
|
||||
|
||||
output.push("---\n");
|
||||
|
||||
// Sections and Rules
|
||||
for (const section of sections) {
|
||||
const sectionRules = rulesBySection.get(section.number) || [];
|
||||
|
||||
output.push(`## ${section.number}. ${section.title}\n`);
|
||||
output.push(`**Impact: ${section.impact}**\n`);
|
||||
output.push(`${section.description}\n`);
|
||||
|
||||
if (sectionRules.length === 0) {
|
||||
output.push("*No rules defined yet. See rules/_template.md for creating new rules.*\n");
|
||||
}
|
||||
|
||||
for (const rule of sectionRules) {
|
||||
output.push(`### ${rule.id} ${rule.title}\n`);
|
||||
|
||||
if (rule.impactDescription) {
|
||||
output.push(`**Impact: ${rule.impact} (${rule.impactDescription})**\n`);
|
||||
} else {
|
||||
output.push(`**Impact: ${rule.impact}**\n`);
|
||||
}
|
||||
|
||||
output.push(`${rule.explanation}\n`);
|
||||
|
||||
for (const example of rule.examples) {
|
||||
if (example.description) {
|
||||
output.push(`**${example.label} (${example.description}):**\n`);
|
||||
} else {
|
||||
output.push(`**${example.label}:**\n`);
|
||||
}
|
||||
|
||||
output.push("```" + (example.language || "sql"));
|
||||
output.push(example.code);
|
||||
output.push("```\n");
|
||||
|
||||
if (example.additionalText) {
|
||||
output.push(`${example.additionalText}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (rule.supabaseNotes) {
|
||||
output.push(`**Supabase Note:** ${rule.supabaseNotes}\n`);
|
||||
}
|
||||
|
||||
if (rule.references && rule.references.length > 0) {
|
||||
if (rule.references.length === 1) {
|
||||
output.push(`Reference: ${rule.references[0]}\n`);
|
||||
} else {
|
||||
output.push("References:");
|
||||
for (const ref of rule.references) {
|
||||
output.push(`- ${ref}`);
|
||||
}
|
||||
output.push("");
|
||||
}
|
||||
}
|
||||
|
||||
output.push("---\n");
|
||||
}
|
||||
}
|
||||
|
||||
// References section
|
||||
if (metadata.references && metadata.references.length > 0) {
|
||||
output.push("## References\n");
|
||||
for (const ref of metadata.references) {
|
||||
output.push(`- ${ref}`);
|
||||
}
|
||||
output.push("");
|
||||
}
|
||||
|
||||
// Write output
|
||||
writeFileSync(AGENTS_OUTPUT, output.join("\n"));
|
||||
console.log(`Generated: ${AGENTS_OUTPUT}`);
|
||||
console.log(`Total rules: ${rules.length}`);
|
||||
}
|
||||
|
||||
// Run build when executed directly
|
||||
const isMainModule = process.argv[1]?.endsWith("build.ts") || process.argv[1]?.endsWith("build.js");
|
||||
|
||||
if (isMainModule) {
|
||||
buildAgents();
|
||||
}
|
||||
|
||||
export { buildAgents };
|
||||
42
packages/postgresql-best-practices-build/src/config.ts
Normal file
42
packages/postgresql-best-practices-build/src/config.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Build package directory
|
||||
export const BUILD_DIR = join(__dirname, "..");
|
||||
|
||||
// Skill directory (relative to build package)
|
||||
export const SKILL_DIR = join(BUILD_DIR, "../../skills/postgresql-best-practices");
|
||||
|
||||
// Rules directory
|
||||
export const RULES_DIR = join(SKILL_DIR, "rules");
|
||||
|
||||
// Output files
|
||||
export const AGENTS_OUTPUT = join(SKILL_DIR, "AGENTS.md");
|
||||
export const METADATA_FILE = join(SKILL_DIR, "metadata.json");
|
||||
export const TEST_CASES_OUTPUT = join(BUILD_DIR, "test-cases.json");
|
||||
|
||||
// Section prefix to number mapping
|
||||
export const SECTION_MAP: Record<string, number> = {
|
||||
query: 1,
|
||||
conn: 2,
|
||||
connection: 2,
|
||||
schema: 3,
|
||||
lock: 4,
|
||||
security: 5,
|
||||
data: 6,
|
||||
monitor: 7,
|
||||
advanced: 8,
|
||||
};
|
||||
|
||||
// Valid impact levels in priority order
|
||||
export const IMPACT_LEVELS = [
|
||||
"CRITICAL",
|
||||
"HIGH",
|
||||
"MEDIUM-HIGH",
|
||||
"MEDIUM",
|
||||
"LOW-MEDIUM",
|
||||
"LOW",
|
||||
] as const;
|
||||
120
packages/postgresql-best-practices-build/src/extract-tests.ts
Normal file
120
packages/postgresql-best-practices-build/src/extract-tests.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { readdirSync, writeFileSync } from "fs";
|
||||
import { join, basename } from "path";
|
||||
import { parseRuleFile } from "./parser.js";
|
||||
import { validateRuleFile } from "./validate.js";
|
||||
import { RULES_DIR, TEST_CASES_OUTPUT } from "./config.js";
|
||||
import type { TestCase } from "./types.js";
|
||||
|
||||
/**
|
||||
* Check if an example label indicates a "bad" pattern
|
||||
*/
|
||||
function isBadExample(label: string): boolean {
|
||||
const lower = label.toLowerCase();
|
||||
return lower.includes("incorrect") || lower.includes("wrong") || lower.includes("bad");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an example label indicates a "good" pattern
|
||||
*/
|
||||
function isGoodExample(label: string): boolean {
|
||||
const lower = label.toLowerCase();
|
||||
return (
|
||||
lower.includes("correct") ||
|
||||
lower.includes("good") ||
|
||||
lower.includes("usage") ||
|
||||
lower.includes("implementation") ||
|
||||
lower.includes("example") ||
|
||||
lower.includes("recommended")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract test cases from all rule files
|
||||
*/
|
||||
function extractTestCases(): TestCase[] {
|
||||
const testCases: TestCase[] = [];
|
||||
|
||||
// Get all rule files
|
||||
const ruleFiles = readdirSync(RULES_DIR)
|
||||
.filter((f) => f.endsWith(".md") && !f.startsWith("_"))
|
||||
.map((f) => join(RULES_DIR, f));
|
||||
|
||||
// Track rule IDs by section for assignment
|
||||
const ruleCountBySection = new Map<number, number>();
|
||||
|
||||
for (const file of ruleFiles) {
|
||||
const validation = validateRuleFile(file);
|
||||
if (!validation.valid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = parseRuleFile(file);
|
||||
if (!result.success || !result.rule) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rule = result.rule;
|
||||
|
||||
// Assign rule ID
|
||||
const sectionCount = (ruleCountBySection.get(rule.section) || 0) + 1;
|
||||
ruleCountBySection.set(rule.section, sectionCount);
|
||||
const ruleId = `${rule.section}.${sectionCount}`;
|
||||
|
||||
// Extract test cases from examples
|
||||
for (const example of rule.examples) {
|
||||
if (!example.code || example.code.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let type: "bad" | "good" | null = null;
|
||||
|
||||
if (isBadExample(example.label)) {
|
||||
type = "bad";
|
||||
} else if (isGoodExample(example.label)) {
|
||||
type = "good";
|
||||
}
|
||||
|
||||
if (type) {
|
||||
testCases.push({
|
||||
ruleId,
|
||||
ruleTitle: rule.title,
|
||||
type,
|
||||
code: example.code,
|
||||
language: example.language || "sql",
|
||||
description: example.description || `${example.label} example for ${rule.title}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return testCases;
|
||||
}
|
||||
|
||||
// Run extraction when executed directly
|
||||
const isMainModule = process.argv[1]?.endsWith("extract-tests.ts") || process.argv[1]?.endsWith("extract-tests.js");
|
||||
|
||||
if (isMainModule) {
|
||||
console.log("Extracting test cases from rules...\n");
|
||||
|
||||
const testCases = extractTestCases();
|
||||
|
||||
if (testCases.length === 0) {
|
||||
console.log("No test cases extracted (no valid rules found).");
|
||||
console.log("This is expected for initial setup.\n");
|
||||
|
||||
// Write empty array
|
||||
writeFileSync(TEST_CASES_OUTPUT, JSON.stringify([], null, 2));
|
||||
console.log(`Generated: ${TEST_CASES_OUTPUT} (empty)`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Write test cases
|
||||
writeFileSync(TEST_CASES_OUTPUT, JSON.stringify(testCases, null, 2));
|
||||
|
||||
console.log(`Generated: ${TEST_CASES_OUTPUT}`);
|
||||
console.log(`Total test cases: ${testCases.length}`);
|
||||
console.log(` Bad examples: ${testCases.filter((t) => t.type === "bad").length}`);
|
||||
console.log(` Good examples: ${testCases.filter((t) => t.type === "good").length}`);
|
||||
}
|
||||
|
||||
export { extractTestCases };
|
||||
271
packages/postgresql-best-practices-build/src/parser.ts
Normal file
271
packages/postgresql-best-practices-build/src/parser.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { basename } from "path";
|
||||
import type { Rule, CodeExample, ImpactLevel, ParseResult } from "./types.js";
|
||||
import { SECTION_MAP, IMPACT_LEVELS } from "./config.js";
|
||||
|
||||
/**
|
||||
* Parse YAML-style frontmatter from markdown content
|
||||
*/
|
||||
function parseFrontmatter(content: string): {
|
||||
frontmatter: Record<string, string>;
|
||||
body: string;
|
||||
} {
|
||||
const frontmatter: Record<string, string> = {};
|
||||
|
||||
if (!content.startsWith("---")) {
|
||||
return { frontmatter, body: content };
|
||||
}
|
||||
|
||||
const endIndex = content.indexOf("---", 3);
|
||||
if (endIndex === -1) {
|
||||
return { frontmatter, body: content };
|
||||
}
|
||||
|
||||
const frontmatterContent = content.slice(3, endIndex).trim();
|
||||
const body = content.slice(endIndex + 3).trim();
|
||||
|
||||
for (const line of frontmatterContent.split("\n")) {
|
||||
const colonIndex = line.indexOf(":");
|
||||
if (colonIndex === -1) continue;
|
||||
|
||||
const key = line.slice(0, colonIndex).trim();
|
||||
let value = line.slice(colonIndex + 1).trim();
|
||||
|
||||
// Strip quotes
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
frontmatter[key] = value;
|
||||
}
|
||||
|
||||
return { frontmatter, body };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract section number from filename prefix
|
||||
*/
|
||||
function getSectionFromFilename(filename: string): number | null {
|
||||
const base = basename(filename, ".md");
|
||||
const prefix = base.split("-")[0];
|
||||
return SECTION_MAP[prefix] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract code examples from markdown body
|
||||
*/
|
||||
function extractExamples(body: string): CodeExample[] {
|
||||
const examples: CodeExample[] = [];
|
||||
const lines = body.split("\n");
|
||||
|
||||
let currentLabel = "";
|
||||
let currentDescription = "";
|
||||
let inCodeBlock = false;
|
||||
let codeBlockLang = "";
|
||||
let codeBlockContent: string[] = [];
|
||||
let additionalText: string[] = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Check for example label: **Label:** or **Label (description):**
|
||||
const labelMatch = line.match(/^\*\*([^*]+?)(?:\s*\(([^)]+)\))?\s*:\*\*\s*$/);
|
||||
if (labelMatch && !inCodeBlock) {
|
||||
// Save previous example if exists
|
||||
if (currentLabel && codeBlockContent.length > 0) {
|
||||
examples.push({
|
||||
label: currentLabel,
|
||||
description: currentDescription || undefined,
|
||||
code: codeBlockContent.join("\n"),
|
||||
language: codeBlockLang || undefined,
|
||||
additionalText: additionalText.length > 0 ? additionalText.join("\n").trim() : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
currentLabel = labelMatch[1].trim();
|
||||
currentDescription = labelMatch[2]?.trim() || "";
|
||||
codeBlockContent = [];
|
||||
codeBlockLang = "";
|
||||
additionalText = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for code block start
|
||||
if (line.startsWith("```") && !inCodeBlock) {
|
||||
inCodeBlock = true;
|
||||
codeBlockLang = line.slice(3).trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for code block end
|
||||
if (line.startsWith("```") && inCodeBlock) {
|
||||
inCodeBlock = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect code block content
|
||||
if (inCodeBlock) {
|
||||
codeBlockContent.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect additional text after code block (before next label)
|
||||
if (currentLabel && codeBlockContent.length > 0 && line.trim()) {
|
||||
// Stop collecting if we hit a heading or reference
|
||||
if (line.startsWith("#") || line.startsWith("Reference")) {
|
||||
continue;
|
||||
}
|
||||
additionalText.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Save last example
|
||||
if (currentLabel && codeBlockContent.length > 0) {
|
||||
examples.push({
|
||||
label: currentLabel,
|
||||
description: currentDescription || undefined,
|
||||
code: codeBlockContent.join("\n"),
|
||||
language: codeBlockLang || undefined,
|
||||
additionalText: additionalText.length > 0 ? additionalText.join("\n").trim() : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return examples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract title from first ## heading
|
||||
*/
|
||||
function extractTitle(body: string): string | null {
|
||||
const match = body.match(/^##\s+(.+)$/m);
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract explanation (content between title and first example)
|
||||
*/
|
||||
function extractExplanation(body: string): string {
|
||||
const lines = body.split("\n");
|
||||
const explanationLines: string[] = [];
|
||||
let foundTitle = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("## ")) {
|
||||
foundTitle = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!foundTitle) continue;
|
||||
|
||||
// Stop at first example label or code block
|
||||
if (line.match(/^\*\*[^*]+:\*\*/) || line.startsWith("```")) {
|
||||
break;
|
||||
}
|
||||
|
||||
explanationLines.push(line);
|
||||
}
|
||||
|
||||
return explanationLines.join("\n").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract references from body
|
||||
*/
|
||||
function extractReferences(body: string): string[] {
|
||||
const references: string[] = [];
|
||||
const lines = body.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
// Match "Reference: [text](url)" or "- [text](url)" after "References:"
|
||||
const refMatch = line.match(/Reference:\s*\[([^\]]+)\]\(([^)]+)\)/);
|
||||
if (refMatch) {
|
||||
references.push(refMatch[2]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match list items under References section
|
||||
const listMatch = line.match(/^-\s*\[([^\]]+)\]\(([^)]+)\)/);
|
||||
if (listMatch) {
|
||||
references.push(listMatch[2]);
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Supabase notes
|
||||
*/
|
||||
function extractSupabaseNotes(body: string): string | undefined {
|
||||
const match = body.match(/\*\*Supabase Note:\*\*\s*(.+?)(?=\n\n|\n\*\*|$)/s);
|
||||
return match ? match[1].trim() : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a rule file and return structured data
|
||||
*/
|
||||
export function parseRuleFile(filePath: string): ParseResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const { frontmatter, body } = parseFrontmatter(content);
|
||||
|
||||
// Extract section from filename
|
||||
const section = getSectionFromFilename(filePath);
|
||||
if (section === null) {
|
||||
errors.push(`Could not determine section from filename: ${basename(filePath)}`);
|
||||
return { success: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Get title from frontmatter or body
|
||||
const title = frontmatter.title || extractTitle(body);
|
||||
if (!title) {
|
||||
errors.push("Missing title in frontmatter or body");
|
||||
return { success: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Get impact level
|
||||
const impact = frontmatter.impact as ImpactLevel;
|
||||
if (!impact || !IMPACT_LEVELS.includes(impact)) {
|
||||
errors.push(`Invalid or missing impact level: ${impact}. Must be one of: ${IMPACT_LEVELS.join(", ")}`);
|
||||
return { success: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Extract other fields
|
||||
const explanation = extractExplanation(body);
|
||||
const examples = extractExamples(body);
|
||||
const references = extractReferences(body);
|
||||
const supabaseNotes = extractSupabaseNotes(body);
|
||||
const tags = frontmatter.tags?.split(",").map((t) => t.trim()) || [];
|
||||
|
||||
// Validation warnings
|
||||
if (!explanation || explanation.length < 20) {
|
||||
warnings.push("Explanation is very short or missing");
|
||||
}
|
||||
|
||||
if (examples.length === 0) {
|
||||
warnings.push("No code examples found");
|
||||
}
|
||||
|
||||
const rule: Rule = {
|
||||
id: "", // Will be assigned during build
|
||||
title,
|
||||
section,
|
||||
impact,
|
||||
impactDescription: frontmatter.impactDescription,
|
||||
explanation,
|
||||
examples,
|
||||
references: references.length > 0 ? references : undefined,
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
supabaseNotes,
|
||||
};
|
||||
|
||||
return { success: true, rule, errors, warnings };
|
||||
} catch (error) {
|
||||
errors.push(`Failed to parse file: ${error}`);
|
||||
return { success: false, errors, warnings };
|
||||
}
|
||||
}
|
||||
68
packages/postgresql-best-practices-build/src/types.ts
Normal file
68
packages/postgresql-best-practices-build/src/types.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export type ImpactLevel =
|
||||
| "CRITICAL"
|
||||
| "HIGH"
|
||||
| "MEDIUM-HIGH"
|
||||
| "MEDIUM"
|
||||
| "LOW-MEDIUM"
|
||||
| "LOW";
|
||||
|
||||
export interface CodeExample {
|
||||
label: string;
|
||||
description?: string;
|
||||
code: string;
|
||||
language?: string;
|
||||
additionalText?: string;
|
||||
}
|
||||
|
||||
export interface Rule {
|
||||
id: string;
|
||||
title: string;
|
||||
section: number;
|
||||
subsection?: number;
|
||||
impact: ImpactLevel;
|
||||
impactDescription?: string;
|
||||
explanation: string;
|
||||
examples: CodeExample[];
|
||||
references?: string[];
|
||||
tags?: string[];
|
||||
supabaseNotes?: string;
|
||||
}
|
||||
|
||||
export interface Section {
|
||||
number: number;
|
||||
title: string;
|
||||
prefix: string;
|
||||
impact: ImpactLevel;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Metadata {
|
||||
version: string;
|
||||
organization: string;
|
||||
date: string;
|
||||
abstract: string;
|
||||
references: string[];
|
||||
maintainers?: string[];
|
||||
}
|
||||
|
||||
export interface ParseResult {
|
||||
success: boolean;
|
||||
rule?: Rule;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface TestCase {
|
||||
ruleId: string;
|
||||
ruleTitle: string;
|
||||
type: "bad" | "good";
|
||||
code: string;
|
||||
language: string;
|
||||
description?: string;
|
||||
}
|
||||
186
packages/postgresql-best-practices-build/src/validate.ts
Normal file
186
packages/postgresql-best-practices-build/src/validate.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { readdirSync } from "fs";
|
||||
import { join, basename } from "path";
|
||||
import { parseRuleFile } from "./parser.js";
|
||||
import { RULES_DIR, IMPACT_LEVELS } from "./config.js";
|
||||
import type { ValidationResult } from "./types.js";
|
||||
|
||||
/**
|
||||
* Check if an example label indicates a "bad" pattern
|
||||
*/
|
||||
function isBadExample(label: string): boolean {
|
||||
const lower = label.toLowerCase();
|
||||
return lower.includes("incorrect") || lower.includes("wrong") || lower.includes("bad");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an example label indicates a "good" pattern
|
||||
*/
|
||||
function isGoodExample(label: string): boolean {
|
||||
const lower = label.toLowerCase();
|
||||
return (
|
||||
lower.includes("correct") ||
|
||||
lower.includes("good") ||
|
||||
lower.includes("usage") ||
|
||||
lower.includes("implementation") ||
|
||||
lower.includes("example") ||
|
||||
lower.includes("recommended")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a single rule file
|
||||
*/
|
||||
export function validateRuleFile(filePath: string): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const result = parseRuleFile(filePath);
|
||||
|
||||
// Add parser errors and warnings
|
||||
errors.push(...result.errors);
|
||||
warnings.push(...result.warnings);
|
||||
|
||||
if (!result.success || !result.rule) {
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
|
||||
const rule = result.rule;
|
||||
|
||||
// Validate title
|
||||
if (!rule.title || rule.title.trim().length === 0) {
|
||||
errors.push("Missing or empty title");
|
||||
}
|
||||
|
||||
// Validate explanation
|
||||
if (!rule.explanation || rule.explanation.trim().length === 0) {
|
||||
errors.push("Missing or empty explanation");
|
||||
} else if (rule.explanation.length < 50) {
|
||||
warnings.push("Explanation is shorter than 50 characters");
|
||||
}
|
||||
|
||||
// Validate examples
|
||||
if (rule.examples.length === 0) {
|
||||
errors.push("Missing examples (need at least one bad and one good example)");
|
||||
} else {
|
||||
const hasBad = rule.examples.some((e) => isBadExample(e.label));
|
||||
const hasGood = rule.examples.some((e) => isGoodExample(e.label));
|
||||
|
||||
if (!hasBad && !hasGood) {
|
||||
errors.push("Missing bad/incorrect and good/correct examples");
|
||||
} else if (!hasBad) {
|
||||
warnings.push("Missing bad/incorrect example (recommended for clarity)");
|
||||
} else if (!hasGood) {
|
||||
errors.push("Missing good/correct example");
|
||||
}
|
||||
|
||||
// Check for code in examples
|
||||
const hasCode = rule.examples.some((e) => e.code && e.code.trim().length > 0);
|
||||
if (!hasCode) {
|
||||
errors.push("Examples have no code");
|
||||
}
|
||||
|
||||
// Check for language specification
|
||||
for (const example of rule.examples) {
|
||||
if (example.code && !example.language) {
|
||||
warnings.push(`Example "${example.label}" missing language specification`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate impact level
|
||||
if (!IMPACT_LEVELS.includes(rule.impact)) {
|
||||
errors.push(`Invalid impact level: ${rule.impact}. Must be one of: ${IMPACT_LEVELS.join(", ")}`);
|
||||
}
|
||||
|
||||
// Warning for missing impact description
|
||||
if (!rule.impactDescription) {
|
||||
warnings.push("Missing impactDescription (recommended for quantifying benefit)");
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all rule files in the rules directory
|
||||
*/
|
||||
export function validateAllRules(): {
|
||||
totalFiles: number;
|
||||
validFiles: number;
|
||||
invalidFiles: number;
|
||||
results: Map<string, ValidationResult>;
|
||||
} {
|
||||
const results = new Map<string, ValidationResult>();
|
||||
let validFiles = 0;
|
||||
let invalidFiles = 0;
|
||||
|
||||
// Get all markdown files (excluding _ prefixed files)
|
||||
const files = readdirSync(RULES_DIR)
|
||||
.filter((f) => f.endsWith(".md") && !f.startsWith("_"))
|
||||
.map((f) => join(RULES_DIR, f));
|
||||
|
||||
for (const file of files) {
|
||||
const result = validateRuleFile(file);
|
||||
results.set(basename(file), result);
|
||||
|
||||
if (result.valid) {
|
||||
validFiles++;
|
||||
} else {
|
||||
invalidFiles++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalFiles: files.length,
|
||||
validFiles,
|
||||
invalidFiles,
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
// Run validation when executed directly
|
||||
const isMainModule = process.argv[1]?.endsWith("validate.ts") || process.argv[1]?.endsWith("validate.js");
|
||||
|
||||
if (isMainModule) {
|
||||
console.log("Validating PostgreSQL best practices rules...\n");
|
||||
|
||||
const { totalFiles, validFiles, invalidFiles, results } = validateAllRules();
|
||||
|
||||
if (totalFiles === 0) {
|
||||
console.log("No rule files found (this is expected for initial setup).");
|
||||
console.log("Create rule files in: skills/postgresql-best-practices/rules/");
|
||||
console.log("Use the _template.md as a starting point.\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let hasErrors = false;
|
||||
|
||||
for (const [filename, result] of results) {
|
||||
if (!result.valid || result.warnings.length > 0) {
|
||||
console.log(`\n${filename}:`);
|
||||
|
||||
for (const error of result.errors) {
|
||||
console.log(` ERROR: ${error}`);
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
for (const warning of result.warnings) {
|
||||
console.log(` WARNING: ${warning}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n${"=".repeat(50)}`);
|
||||
console.log(`Total: ${totalFiles} files | Valid: ${validFiles} | Invalid: ${invalidFiles}`);
|
||||
|
||||
if (hasErrors) {
|
||||
console.log("\nValidation failed. Please fix the errors above.");
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("\nValidation passed!");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
16
packages/postgresql-best-practices-build/tsconfig.json
Normal file
16
packages/postgresql-best-practices-build/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user