diff --git a/AGENTS.md b/AGENTS.md index 2911c9b407..e14d3474df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,9 +51,17 @@ behind human approval. ## Working conventions - `bun run check` (typecheck, lint, test, structural checks) must pass - before every commit. CI's `structural` job runs the same - `check:structural` list as local, including `check:report-error` and - `check:error-envelope`, so the two cannot drift. + before every commit. `check:structural` is a single runner + (`scripts/checks/run.ts`) that discovers and runs every + `scripts/checks/*.ts` file with an `import.meta.main` entry point; + CI's `structural` job runs that same `bun run check:structural`, so + local and CI can never enumerate a different set of checks. Run one + check on its own with `bun run check:structural ` (e.g. `bun run +check:structural report-error`), forwarding flags after it (e.g. + `bun run check:structural report-error -- --write-baseline`). There is + no standalone `check:` script anymore — `check:` in prose + anywhere in this repo (this file included) means `bun run +check:structural `. - Commit sequence per change: tests first ("Add tests for X"), then implementation ("X: what changed"), then docs ("Update docs: X"). One logical change per commit; commit messages are written for a public @@ -119,22 +127,22 @@ behind human approval. A rule a check enforces has zero violations; a rule stated only in prose drifts the moment it's inconvenient — everything above this section is -prose. Prefer these over remembering the rule; each is backed by a `bun run -check:*` script, so a violation fails CI rather than waiting for review. +prose. Prefer these over remembering the rule; each is backed by a +`scripts/checks/*.ts` file that `check:structural` (`scripts/checks/run.ts`) +discovers and runs, so a violation fails CI rather than waiting for review. - Report every caught error through `reportError` from `@corbits/error-sink` — never a bare `catch {}`, never a toast alone. It attaches operation/tenant/room/agent context and a `refId` a person can quote to support, and redacts secrets before anything reaches a log sink. Unlike every other rule in this section, this one does not yet - hold retroactively: `check:report-error` fails a catch clause that + hold retroactively: the report-error check fails a catch clause that neither calls `reportError` nor rethrows only when the catch is new or the current change's diff touches its line; a pre-existing catch is instead recorded in `scripts/checks/report-error-baseline.txt`, a debt ledger — not an allowlist — of 280 violations as of this check landing, - each one a real bug still to fix. The same script runs in CI via - `check:structural`. Regenerate it with `bun run -scripts/checks/report-error.ts --write-baseline` after fixing (or + each one a real bug still to fix. Regenerate it with `bun run +check:structural report-error -- --write-baseline` after fixing (or newly opting out) entries; a stale entry with no matching violation fails the check, so the ledger can only shrink. A violation already tracked on its own ticket opts out instead with a `report-error-ignore: @@ -142,22 +150,26 @@ scripts/checks/report-error.ts --write-baseline` after fixing (or actively being fixed, never a way to clear a baseline entry quietly. - A package's `browser-safe` subpath (e.g. `@corbits/inbox/client`) may never import a server-only dependency (`postgres`, `drizzle-orm`, - `hono`, any `@intx/*`) — `check:browser-safe-subpaths` walks the real + `hono`, any `@intx/*`) — the browser-safe-subpaths check walks the real import graph from each declared entry point. - A `{ name, version }` tool-package pin literal must match the pinned - package's own `package.json` version — `check:tool-package-pins` catches - the mismatch. A tool package's `src/` changing without a version bump is - the other half of that class, caught separately by - `check:tool-package-freshness`: it diffs against the base ref (a - GitHub `pull_request` job reads `pull_request.base.sha` from + package's own `package.json` version — the tool-package-pins check + catches the mismatch. A tool package's `src/` changing without a version + bump is the other half of that class, caught separately by + tool-package-freshness: it diffs against the base ref (a GitHub + `pull_request` job reads `pull_request.base.sha` from `GITHUB_EVENT_PATH`; locally it falls back to the merge base with `origin/main`) and flags any tool package whose `src/` differs from that base while its version stayed the same. With no base ref available (no `origin/main`, no pull-request event) it no-ops, deferring to CI as the authoritative run. - Every package needs a `LICENSE` file (canonical LGPL-2.1-or-later text, - copy from any existing `packages/*/LICENSE`) — `check:licenses` fails + copy from any existing `packages/*/LICENSE`) — the licenses check fails without one. +- A dependency declared once in the root `catalog` (bun's workspace + catalog) must be consumed as `catalog:` everywhere — the catalog-pins + check fails a literal range for a catalogued dependency, which is how + the same package silently ends up pinned two ways. - `tsconfig.base.json` sets `exactOptionalPropertyTypes: true`: an absent key and an explicit `{ foo: undefined }` are different types. Assigning `undefined` to an optional field the compiler expects omitted is a diff --git a/apps/hub/package.json b/apps/hub/package.json index 0b402142c1..40c1e0e8f9 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -69,10 +69,10 @@ "@workbench/onboarding": "workspace:*", "@workbench/templates": "workspace:*", "arktype": "catalog:", - "better-auth": "^1.4.18", - "drizzle-orm": "0.45.2", - "hono": "^4.11.9", - "postgres": "^3.4.8" + "better-auth": "catalog:", + "drizzle-orm": "catalog:", + "hono": "catalog:", + "postgres": "catalog:" }, "devDependencies": { "@types/bun": "catalog:", diff --git a/apps/web/package.json b/apps/web/package.json index 47ef955892..6d4d0e9f3b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -58,7 +58,7 @@ "@tailwindcss/vite": "^4.3.3", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "catalog:", "@vitejs/plugin-react": "^5.0.0", "sonner": "^2.0.7", "tailwindcss": "^4.3.3", diff --git a/bun.lock b/bun.lock index 640a270af4..054936bb28 100644 --- a/bun.lock +++ b/bun.lock @@ -90,10 +90,10 @@ "@workbench/onboarding": "workspace:*", "@workbench/templates": "workspace:*", "arktype": "catalog:", - "better-auth": "^1.4.18", - "drizzle-orm": "0.45.2", - "hono": "^4.11.9", - "postgres": "^3.4.8", + "better-auth": "catalog:", + "drizzle-orm": "catalog:", + "hono": "catalog:", + "postgres": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", @@ -180,7 +180,7 @@ "@tailwindcss/vite": "^4.3.3", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "catalog:", "@vitejs/plugin-react": "^5.0.0", "sonner": "^2.0.7", "tailwindcss": "^4.3.3", @@ -198,7 +198,7 @@ "@intx/hub-api": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", }, "devDependencies": { @@ -225,7 +225,7 @@ "@workbench/templates": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "isomorphic-git": "catalog:", "postgres": "catalog:", }, @@ -295,7 +295,7 @@ "devDependencies": { "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:", }, @@ -310,7 +310,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", }, "devDependencies": { "@intx/hub-common": "0.3.0", @@ -333,7 +333,7 @@ "devDependencies": { "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:", }, @@ -353,7 +353,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", @@ -370,7 +370,7 @@ "@intx/hub-api": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", }, "devDependencies": { @@ -450,7 +450,7 @@ "@intx/workflow-deploy": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", }, "devDependencies": { @@ -483,7 +483,7 @@ "@intx/inference": "workspace:*", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "catalog:", "typescript": "catalog:", }, }, @@ -527,7 +527,7 @@ "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:", }, @@ -539,7 +539,7 @@ "@corbits/error-sink": "workspace:*", "@intx/hub-api": "workspace:*", "arktype": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", @@ -565,7 +565,7 @@ "@modelcontextprotocol/sdk": "catalog:", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", "simple-icons": "^16.28.0", }, @@ -600,7 +600,7 @@ "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:", }, @@ -681,7 +681,7 @@ "@workbench/templates": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", }, "devDependencies": { @@ -840,7 +840,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", }, "devDependencies": { @@ -875,7 +875,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", }, "devDependencies": { @@ -975,7 +975,7 @@ "@corbits/error-sink": "workspace:*", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "arktype": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", @@ -1063,7 +1063,7 @@ "@workbench/templates": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", }, "devDependencies": { @@ -1088,7 +1088,7 @@ "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:", }, @@ -1103,7 +1103,7 @@ "@intx/hub-api": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", }, "devDependencies": { @@ -1118,7 +1118,7 @@ "@corbits/error-sink": "workspace:*", "@intx/hub-api": "workspace:*", "arktype": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "y-protocols": "^1.0.7", "yjs": "^13.6.32", }, @@ -1176,7 +1176,7 @@ "@intx/log": "0.3.0", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", }, "devDependencies": { @@ -1195,7 +1195,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", @@ -1276,7 +1276,7 @@ "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:", }, @@ -1291,7 +1291,7 @@ "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:", }, @@ -1306,7 +1306,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", @@ -1323,7 +1323,7 @@ "@intx/hub-sessions": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "isomorphic-git": "catalog:", "postgres": "catalog:", }, @@ -1357,7 +1357,7 @@ "chat": "^4.34.0", "corbits-tag": "github:corbitsdev/corbits-tag#92eabf4f7bf0a9663799c07db6c3f72a4275f894", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", }, "devDependencies": { @@ -1456,7 +1456,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", }, "devDependencies": { @@ -1493,7 +1493,7 @@ "arktype": "catalog:", "cronstrue": "^3.24.0", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", }, "devDependencies": { @@ -2010,11 +2010,12 @@ "@modelcontextprotocol/sdk": "^1.30.0", "@tanstack/react-query": "^5.101.4", "@types/bun": "^1.3.9", + "@types/react-dom": "^19.2.2", "@types/semver": "^7.7.1", "@types/ssri": "^7.1.5", "arktype": "^2.2.0", "better-auth": "^1.4.18", - "drizzle-orm": "^0.45.1", + "drizzle-orm": "^0.45.2", "hono": "^4.11.9", "isomorphic-git": "^1.27.2", "postgres": "^3.4.8", @@ -3550,14 +3551,6 @@ "@corbits/artifacts-hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], - "@corbits/chat-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/context-menu/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/plugins-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/settings-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -3582,8 +3575,6 @@ "@workbench/hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], - "@workbench/web/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], diff --git a/package.json b/package.json index 099d56801d..ee7497a2ad 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ ], "scripts": { "check": "bun run typecheck && bun run lint && bun run test && bun run check:structural", - "check:all": "bun run check && bun run check:packages", "typecheck": "bun run scripts/typecheck.ts", "lint": "prettier --check --cache . && eslint --cache .", "format": "prettier --write .", @@ -26,26 +25,7 @@ "setup": "bun scripts/db-setup.ts", "setup:memory": "bun run scripts/setup-memory.ts", "reset": "bun scripts/reset.ts", - "check:structural": "bun test scripts/checks/test && bun run check:deletion && bun run check:killdates && bun run check:licenses && bun run check:db-gate && bun run check:no-product-tenancy && bun run check:browser-safe-subpaths && bun run check:web-utilities && bun run check:tailwind-source && bun run check:ui-vocabulary && bun run check:react-ui-drift && bun run check:react-ui-pin && bun run check:tool-package-pins && bun run check:tool-package-freshness && bun run check:hub-git-safety && bun run check:report-error && bun run check:error-envelope && bun run check:routine-target-inference && bun run check:tsconfig-references", - "check:deletion": "bun run scripts/checks/deletion.ts", - "check:report-error": "bun run scripts/checks/report-error.ts", - "check:error-envelope": "bun run scripts/checks/error-envelope.ts", - "check:killdates": "bun run scripts/checks/killdates.ts", - "check:packages": "bun run scripts/checks/packages.ts", - "check:licenses": "bun run scripts/checks/licenses.ts", - "check:db-gate": "bun run scripts/checks/db-gate.ts", - "check:no-product-tenancy": "bun run scripts/checks/no-product-tenancy.ts", - "check:browser-safe-subpaths": "bun run scripts/checks/browser-safe-subpaths.ts", - "check:web-utilities": "bun run scripts/checks/web-tailwind-utilities.ts", - "check:tailwind-source": "bun run scripts/checks/tailwind-source.ts", - "check:ui-vocabulary": "bun run scripts/checks/ui-vocabulary.ts", - "check:react-ui-drift": "bun run scripts/checks/react-ui-drift.ts", - "check:react-ui-pin": "bun run scripts/checks/react-ui-pin.ts", - "check:tool-package-pins": "bun run scripts/checks/tool-package-pins.ts", - "check:tool-package-freshness": "bun run scripts/checks/tool-package-freshness.ts", - "check:hub-git-safety": "bun run scripts/checks/hub-git-safety.ts", - "check:routine-target-inference": "bun run scripts/checks/routine-target-inference.ts", - "check:tsconfig-references": "bun run scripts/generate-tsconfig-references.ts --check", + "check:structural": "bun run scripts/checks/run.ts", "build:sidecar-image": "docker build -f apps/sidecar/Dockerfile -t corbits-sidecar:dev .", "eval": "bun run scripts/evals-run.ts" }, @@ -81,14 +61,15 @@ "@types/semver": "^7.7.1", "@tanstack/react-query": "^5.101.4", "better-auth": "^1.4.18", - "drizzle-orm": "^0.45.1", + "drizzle-orm": "^0.45.2", "hono": "^4.11.9", "isomorphic-git": "^1.27.2", "postgres": "^3.4.8", "semver": "^7.7.2", "ssri": "^12.0.0", "tar": "^7.5.1", - "@types/ssri": "^7.1.5" + "@types/ssri": "^7.1.5", + "@types/react-dom": "^19.2.2" }, "trustedDependencies": [ "@corbits/react-ui", diff --git a/packages/access-policy/package.json b/packages/access-policy/package.json index 3c3d74cbe7..557ae865c2 100644 --- a/packages/access-policy/package.json +++ b/packages/access-policy/package.json @@ -20,7 +20,7 @@ "@corbits/hub-api-client": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:" }, "devDependencies": { diff --git a/packages/agent-directory/package.json b/packages/agent-directory/package.json index a633aa1652..f7b4971aa9 100644 --- a/packages/agent-directory/package.json +++ b/packages/agent-directory/package.json @@ -29,7 +29,7 @@ "@intx/workflow": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "isomorphic-git": "catalog:", "postgres": "catalog:" }, diff --git a/packages/api-query/package.json b/packages/api-query/package.json index e1f35e84d7..186086c9b7 100644 --- a/packages/api-query/package.json +++ b/packages/api-query/package.json @@ -21,7 +21,7 @@ "devDependencies": { "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:" } diff --git a/packages/approvals/package.json b/packages/approvals/package.json index 3c18a0aa30..36133b0d7e 100644 --- a/packages/approvals/package.json +++ b/packages/approvals/package.json @@ -20,7 +20,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9" + "hono": "catalog:" }, "devDependencies": { "@intx/hub-common": "0.3.0", diff --git a/packages/artifact-ui/package.json b/packages/artifact-ui/package.json index 916ad7d5a4..017c5aaab0 100644 --- a/packages/artifact-ui/package.json +++ b/packages/artifact-ui/package.json @@ -26,7 +26,7 @@ "devDependencies": { "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:" } diff --git a/packages/artifacts-hub/package.json b/packages/artifacts-hub/package.json index b3fb0574d2..5b6bc29bee 100644 --- a/packages/artifacts-hub/package.json +++ b/packages/artifacts-hub/package.json @@ -24,7 +24,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9" + "hono": "catalog:" }, "devDependencies": { "@types/bun": "catalog:", diff --git a/packages/bench/package.json b/packages/bench/package.json index 6c7709cccf..c35b066900 100644 --- a/packages/bench/package.json +++ b/packages/bench/package.json @@ -21,7 +21,7 @@ "@intx/hub-api": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:" }, "devDependencies": { diff --git a/packages/chat-ui/package.json b/packages/chat-ui/package.json index d75b8b1a5e..6aed0ebba0 100644 --- a/packages/chat-ui/package.json +++ b/packages/chat-ui/package.json @@ -36,7 +36,7 @@ "@intx/inference": "workspace:*", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "catalog:", "typescript": "catalog:" } } diff --git a/packages/chat/package.json b/packages/chat/package.json index 10457bd2bc..2889baa10a 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -53,7 +53,7 @@ "@corbits/connections": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:" }, "devDependencies": { diff --git a/packages/command-palette/package.json b/packages/command-palette/package.json index 047e156c87..98466e918b 100644 --- a/packages/command-palette/package.json +++ b/packages/command-palette/package.json @@ -20,7 +20,7 @@ "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:" } diff --git a/packages/commands/package.json b/packages/commands/package.json index 055c7c1acc..5c8d9a2fe0 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -16,7 +16,7 @@ "@corbits/error-sink": "workspace:*", "@intx/hub-api": "workspace:*", "arktype": "catalog:", - "hono": "^4.11.9" + "hono": "catalog:" }, "devDependencies": { "@types/bun": "catalog:", diff --git a/packages/connections/package.json b/packages/connections/package.json index 845c8cbfa1..13244673d3 100644 --- a/packages/connections/package.json +++ b/packages/connections/package.json @@ -44,7 +44,7 @@ "@corbits/seeding": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:", "simple-icons": "^16.28.0" }, diff --git a/packages/context-menu/package.json b/packages/context-menu/package.json index c396d694bb..d43cb296a9 100644 --- a/packages/context-menu/package.json +++ b/packages/context-menu/package.json @@ -20,7 +20,7 @@ "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:" } diff --git a/packages/evals/package.json b/packages/evals/package.json index cd718f850d..a5bccd85bb 100644 --- a/packages/evals/package.json +++ b/packages/evals/package.json @@ -33,7 +33,7 @@ "@workbench/onboarding": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:" }, "devDependencies": { diff --git a/packages/inference-catalog/package.json b/packages/inference-catalog/package.json index 4264b46859..a83a87b887 100644 --- a/packages/inference-catalog/package.json +++ b/packages/inference-catalog/package.json @@ -27,7 +27,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:" }, "devDependencies": { diff --git a/packages/insights/package.json b/packages/insights/package.json index bfe01927f2..43ce5ba48a 100644 --- a/packages/insights/package.json +++ b/packages/insights/package.json @@ -25,7 +25,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:" }, "devDependencies": { diff --git a/packages/memory-hub/package.json b/packages/memory-hub/package.json index d22c652783..65c013522c 100644 --- a/packages/memory-hub/package.json +++ b/packages/memory-hub/package.json @@ -17,7 +17,7 @@ "@corbits/error-sink": "workspace:*", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "arktype": "catalog:", - "hono": "^4.11.9" + "hono": "catalog:" }, "devDependencies": { "@types/bun": "catalog:", diff --git a/packages/onboarding/package.json b/packages/onboarding/package.json index 787c87987b..605f0cdd93 100644 --- a/packages/onboarding/package.json +++ b/packages/onboarding/package.json @@ -26,7 +26,7 @@ "@workbench/templates": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:" }, "devDependencies": { diff --git a/packages/plugins-ui/package.json b/packages/plugins-ui/package.json index 25e85d7836..4dc1e4c190 100644 --- a/packages/plugins-ui/package.json +++ b/packages/plugins-ui/package.json @@ -27,7 +27,7 @@ "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:" } diff --git a/packages/preferences/package.json b/packages/preferences/package.json index 9a319ca3cb..d129353036 100644 --- a/packages/preferences/package.json +++ b/packages/preferences/package.json @@ -21,7 +21,7 @@ "@intx/hub-api": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:" }, "devDependencies": { diff --git a/packages/presence/package.json b/packages/presence/package.json index 5041ac0832..f2cc0cf431 100644 --- a/packages/presence/package.json +++ b/packages/presence/package.json @@ -18,7 +18,7 @@ "@corbits/error-sink": "workspace:*", "@intx/hub-api": "workspace:*", "arktype": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "y-protocols": "^1.0.7", "yjs": "^13.6.32" }, diff --git a/packages/run-key-history/package.json b/packages/run-key-history/package.json index 8e8e53ac6c..93f2042bbd 100644 --- a/packages/run-key-history/package.json +++ b/packages/run-key-history/package.json @@ -20,7 +20,7 @@ "@intx/log": "0.3.0", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:" }, "devDependencies": { diff --git a/packages/run-scope/package.json b/packages/run-scope/package.json index 5fd7c6432d..9b80246abc 100644 --- a/packages/run-scope/package.json +++ b/packages/run-scope/package.json @@ -20,7 +20,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9" + "hono": "catalog:" }, "devDependencies": { "@types/bun": "catalog:", diff --git a/packages/settings-ui/package.json b/packages/settings-ui/package.json index 3a0fa19363..82220db1ac 100644 --- a/packages/settings-ui/package.json +++ b/packages/settings-ui/package.json @@ -35,7 +35,7 @@ "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:" } diff --git a/packages/shell-layout/package.json b/packages/shell-layout/package.json index 027948b9d7..b6f1b1f2b4 100644 --- a/packages/shell-layout/package.json +++ b/packages/shell-layout/package.json @@ -19,7 +19,7 @@ "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", + "@types/react-dom": "catalog:", "react-dom": "^19.2.0", "typescript": "catalog:" } diff --git a/packages/sidecar-placement/package.json b/packages/sidecar-placement/package.json index c34efb985c..d3e8de6dd3 100644 --- a/packages/sidecar-placement/package.json +++ b/packages/sidecar-placement/package.json @@ -19,7 +19,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9" + "hono": "catalog:" }, "devDependencies": { "@types/bun": "catalog:", diff --git a/packages/skills/package.json b/packages/skills/package.json index a666838b6c..6d8acf44ec 100644 --- a/packages/skills/package.json +++ b/packages/skills/package.json @@ -20,7 +20,7 @@ "@intx/hub-sessions": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "isomorphic-git": "catalog:", "postgres": "catalog:" }, diff --git a/packages/slack-tag/package.json b/packages/slack-tag/package.json index 5f98f88078..9fa6e16bf8 100644 --- a/packages/slack-tag/package.json +++ b/packages/slack-tag/package.json @@ -23,7 +23,7 @@ "chat": "^4.34.0", "corbits-tag": "github:corbitsdev/corbits-tag#92eabf4f7bf0a9663799c07db6c3f72a4275f894", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:" }, "devDependencies": { diff --git a/packages/webhook-triggers/package.json b/packages/webhook-triggers/package.json index bc1fe4620c..5f5bebe75d 100644 --- a/packages/webhook-triggers/package.json +++ b/packages/webhook-triggers/package.json @@ -25,7 +25,7 @@ "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:" }, "devDependencies": { diff --git a/packages/workflows/package.json b/packages/workflows/package.json index 00277de7fb..b10f906a40 100644 --- a/packages/workflows/package.json +++ b/packages/workflows/package.json @@ -25,7 +25,7 @@ "arktype": "catalog:", "cronstrue": "^3.24.0", "drizzle-orm": "catalog:", - "hono": "^4.11.9", + "hono": "catalog:", "postgres": "catalog:" }, "devDependencies": { diff --git a/scripts/checks/catalog-pins.ts b/scripts/checks/catalog-pins.ts new file mode 100644 index 0000000000..adb348a622 --- /dev/null +++ b/scripts/checks/catalog-pins.ts @@ -0,0 +1,168 @@ +// check:catalog-pins — a dependency declared in the root `catalog` (or a +// named catalog under `catalogs`) must be consumed as `catalog:` (or +// `catalog:`) everywhere. A literal range for a catalogued +// dependency is how the same package ends up pinned two ways (CL-7442 +// found drizzle-orm, hono, postgres, and @types/react-dom drifted this +// way) — one workspace bumps its own literal, the rest don't, and the +// version actually installed depends on hoisting order. +// +// vendor/intx/*/package.json is excluded: those manifests are governed +// by the vendoring ledger (VENDORED.md), not by this repo's catalog. +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { Glob } from "bun"; +import { type } from "arktype"; +import { + emptyReport, + reportAndExit, + rootFromArgs, + type CheckReport, +} from "./lib/repo"; + +const DEPENDENCY_FIELDS = [ + "dependencies", + "devDependencies", + "peerDependencies", +] as const; + +const PackageJson = type({ + "dependencies?": "Record", + "devDependencies?": "Record", + "peerDependencies?": "Record", + "catalog?": "Record", + "catalogs?": "Record>", + "workspaces?": "unknown", +}); + +type PackageJson = typeof PackageJson.infer; + +export const WORKSPACE_GLOBS = [ + "apps/*/package.json", + "packages/*/package.json", + "tools/*/package.json", + "templates/package.json", + "workflows/*/package.json", +]; + +function parsePackageJson(raw: string, source: string): PackageJson { + const parsed = PackageJson(JSON.parse(raw)); + if (parsed instanceof type.errors) { + throw new Error(`${source}: invalid package.json — ${parsed.summary}`); + } + return parsed; +} + +/** Resolves the root's default catalog plus any named catalogs, either + * from a top-level `catalog`/`catalogs` or from `workspaces.catalog` / + * `workspaces.catalogs` (bun accepts both shapes). */ +export function resolveCatalogs(rootPackageJson: PackageJson): { + readonly default: Readonly>; + readonly named: Readonly>>>; +} { + const workspaces = rootPackageJson.workspaces; + const workspacesObject = + workspaces && typeof workspaces === "object" && !Array.isArray(workspaces) + ? (workspaces as { + catalog?: Record; + catalogs?: Record>; + }) + : undefined; + + return { + default: rootPackageJson.catalog ?? workspacesObject?.catalog ?? {}, + named: rootPackageJson.catalogs ?? workspacesObject?.catalogs ?? {}, + }; +} + +export function auditCatalogPins( + catalog: Readonly>, + workspaces: readonly { dir: string; packageJson: PackageJson }[], + namedCatalogs: Readonly< + Record>> + > = {}, +): CheckReport { + const report = emptyReport(); + const catalogued = new Set(Object.keys(catalog)); + const cataloguedByName = new Map( + Object.entries(namedCatalogs).map(([catalogName, deps]) => [ + catalogName, + new Set(Object.keys(deps)), + ]), + ); + + for (const { dir, packageJson } of workspaces) { + for (const field of DEPENDENCY_FIELDS) { + const deps = packageJson[field]; + if (!deps) continue; + for (const [name, range] of Object.entries(deps)) { + if (range.startsWith("catalog:")) continue; + if (catalogued.has(name)) { + report.violations.push( + `${dir}/package.json: "${name}" is declared in the root ` + + `catalog but pinned here as a literal ("${range}") instead ` + + `of "catalog:" — either use "catalog:" or drop the entry ` + + `from the root catalog if this package intentionally needs ` + + `a different version.`, + ); + continue; + } + for (const [catalogName, names] of cataloguedByName) { + if (!names.has(name)) continue; + report.violations.push( + `${dir}/package.json: "${name}" is declared in the ` + + `"${catalogName}" catalog but pinned here as a literal ` + + `("${range}") instead of "catalog:${catalogName}" — either ` + + `use "catalog:${catalogName}" or drop the entry from that ` + + `catalog if this package intentionally needs a different ` + + `version.`, + ); + break; + } + } + } + } + return report; +} + +async function listWorkspaces( + root: string, +): Promise<{ dir: string; packageJson: PackageJson }[]> { + const workspaces: { dir: string; packageJson: PackageJson }[] = []; + for (const pattern of WORKSPACE_GLOBS) { + const glob = new Glob(pattern); + for await (const manifestPath of glob.scan(root)) { + const dir = path.dirname(manifestPath); + const packageJson = parsePackageJson( + readFileSync(path.join(root, manifestPath), "utf8"), + manifestPath, + ); + workspaces.push({ dir, packageJson }); + } + } + return workspaces; +} + +async function main(): Promise { + const root = rootFromArgs(Bun.argv.slice(2)); + const rootPackageJsonPath = path.join(root, "package.json"); + const rootPackageJson = existsSync(rootPackageJsonPath) + ? parsePackageJson( + readFileSync(rootPackageJsonPath, "utf8"), + "package.json", + ) + : ({} as PackageJson); + const { default: catalog, named: namedCatalogs } = + resolveCatalogs(rootPackageJson); + const workspaces = await listWorkspaces(root); + const report = auditCatalogPins(catalog, workspaces, namedCatalogs); + if (workspaces.length === 0) { + report.notes.push("no workspace packages yet."); + } + report.notes.push( + "vendor/intx/*/package.json is excluded — vendored manifests are " + + "governed by VENDORED.md, not this repo's catalog.", + ); + reportAndExit("check:catalog-pins", report); +} + +if (import.meta.main) await main(); diff --git a/scripts/checks/run.ts b/scripts/checks/run.ts new file mode 100644 index 0000000000..ad1e8a8b96 --- /dev/null +++ b/scripts/checks/run.ts @@ -0,0 +1,127 @@ +// check:structural — the single check runner. +// +// Every structural check used to get its own root `package.json` script +// (`check:report-error`, `check:licenses`, ...) plus a line in +// `check:structural`'s chain, so adding a check meant touching both. This +// runner instead discovers every `scripts/checks/*.ts` file that has an +// `if (import.meta.main)` entry point — that is the same signal each check +// already carries to run standalone during development — and runs it. A +// file with no such entry point (a shared lib, an allowlist data module, a +// `*.test.ts`) is not a check and is skipped. The runner itself +// (`run.ts`, or anything whose resolved real path points at it — a +// rename or a symlink included) is excluded even though it carries the +// same entry point, so it never discovers and recurses into itself. +// +// `scripts/generate-tsconfig-references.ts --check` lives outside this +// directory (it doubles as the reference generator), so it is added +// explicitly rather than discovered. +// +// `check:packages` (packed-consumption audit) was never part of the full +// `check:structural` chain before CL-7442 — it spawns a subprocess per +// workspace package to prove each one only imports what it packs, which +// takes minutes rather than seconds. It stays discoverable and runnable on +// its own (`bun run check:structural packages`), but is excluded from the +// full run so `check:structural`'s wall-clock does not regress. +// +// An optional name argument runs just that one check (by its filename +// without `.ts`, or `tsconfig-references`), forwarding any further +// arguments to it — e.g. `bun run check:structural report-error -- +// --write-baseline`. +import { readFileSync, readdirSync, realpathSync } from "node:fs"; +import path from "node:path"; + +const CHECKS_DIR = path.resolve(import.meta.dir); +const ROOT = path.resolve(CHECKS_DIR, "..", ".."); + +export interface Check { + readonly name: string; + readonly command: readonly string[]; +} + +const MAIN_ENTRY_POINT = /if\s*\(\s*import\.meta\.main\s*\)/; + +/** Discoverable and independently runnable, but excluded from a full run. */ +const EXCLUDED_FROM_FULL_RUN = new Set(["packages"]); + +// The runner itself lives in this directory and also has an +// `import.meta.main` entry point — without excluding it, it would +// discover itself as a check and spawn an unbounded recursion of itself. +// Excluded by resolved real path (not by filename) so a rename or a +// symlink pointing at this file is still recognized as "the runner" +// rather than slipping in as a discovered check. +export function discoverCheckFiles( + checksDir: string, + readdir: (dir: string) => string[] = (dir) => readdirSync(dir), + readFile: (file: string) => string = (file) => readFileSync(file, "utf8"), + runnerRealPath: string = realpathSync(import.meta.path), + resolveRealPath: (file: string) => string = (file) => realpathSync(file), +): string[] { + return readdir(checksDir) + .filter((entry) => entry.endsWith(".ts") && !entry.endsWith(".test.ts")) + .filter( + (entry) => + resolveRealPath(path.join(checksDir, entry)) !== runnerRealPath, + ) + .filter((entry) => + MAIN_ENTRY_POINT.test(readFile(path.join(checksDir, entry))), + ) + .sort(); +} + +export function buildChecks(checksDir: string, root: string): Check[] { + const checks: Check[] = discoverCheckFiles(checksDir).map((file) => ({ + name: file.slice(0, -".ts".length), + command: ["bun", "run", path.join(checksDir, file)], + })); + checks.push({ + name: "tsconfig-references", + command: [ + "bun", + "run", + path.join(root, "scripts", "generate-tsconfig-references.ts"), + "--check", + ], + }); + return checks.sort((a, b) => a.name.localeCompare(b.name)); +} + +async function run(command: readonly string[]): Promise { + const proc = Bun.spawn([...command], { + cwd: ROOT, + stdout: "inherit", + stderr: "inherit", + }); + return proc.exited; +} + +async function main(): Promise { + const [only, ...rest] = Bun.argv.slice(2); + const checks = buildChecks(CHECKS_DIR, ROOT); + + if (only !== undefined) { + const check = checks.find((candidate) => candidate.name === only); + if (!check) { + console.error( + `check:structural: no check named "${only}". Known checks: ` + + checks.map((candidate) => candidate.name).join(", "), + ); + process.exit(1); + } + process.exit(await run([...check.command, ...rest])); + } + + let failed = false; + + console.log("--- scripts/checks/test ---"); + if ((await run(["bun", "test", "scripts/checks/test"])) !== 0) failed = true; + + for (const check of checks) { + if (EXCLUDED_FROM_FULL_RUN.has(check.name)) continue; + console.log(`--- ${check.name} ---`); + if ((await run(check.command)) !== 0) failed = true; + } + + process.exit(failed ? 1 : 0); +} + +if (import.meta.main) await main(); diff --git a/scripts/checks/test/catalog-pins.test.ts b/scripts/checks/test/catalog-pins.test.ts new file mode 100644 index 0000000000..b90c01e79c --- /dev/null +++ b/scripts/checks/test/catalog-pins.test.ts @@ -0,0 +1,115 @@ +import { expect, test } from "bun:test"; +import { + WORKSPACE_GLOBS, + auditCatalogPins, + resolveCatalogs, +} from "../catalog-pins"; + +test("a literal range for a catalogued dependency is a violation", () => { + const report = auditCatalogPins({ hono: "^4.11.9" }, [ + { + dir: "packages/inbox", + packageJson: { dependencies: { hono: "^4.11.9" } }, + }, + ]); + expect(report.violations).toHaveLength(1); + expect(report.violations[0]).toContain("packages/inbox/package.json"); + expect(report.violations[0]).toContain("hono"); +}); + +test("catalog: for a catalogued dependency passes", () => { + const report = auditCatalogPins({ hono: "^4.11.9" }, [ + { + dir: "packages/inbox", + packageJson: { dependencies: { hono: "catalog:" } }, + }, + ]); + expect(report.violations).toEqual([]); +}); + +test("a literal for a dependency not in the catalog is fine", () => { + const report = auditCatalogPins({ hono: "^4.11.9" }, [ + { + dir: "packages/inbox", + packageJson: { dependencies: { react: "^19.2.0" } }, + }, + ]); + expect(report.violations).toEqual([]); +}); + +test("checks devDependencies and peerDependencies too", () => { + const report = auditCatalogPins({ postgres: "^3.4.8" }, [ + { + dir: "packages/a", + packageJson: { devDependencies: { postgres: "^3.4.8" } }, + }, + { + dir: "packages/b", + packageJson: { peerDependencies: { postgres: "^3.4.8" } }, + }, + ]); + expect(report.violations).toHaveLength(2); +}); + +test("a literal range for a dependency in a named catalog is a violation", () => { + const report = auditCatalogPins( + {}, + [ + { + dir: "packages/inbox", + packageJson: { dependencies: { react: "^19.2.0" } }, + }, + ], + { react19: { react: "^19.2.0" } }, + ); + expect(report.violations).toHaveLength(1); + expect(report.violations[0]).toContain("packages/inbox/package.json"); + expect(report.violations[0]).toContain("react"); + expect(report.violations[0]).toContain("catalog:react19"); +}); + +test("catalog: for a dependency in that named catalog passes", () => { + const report = auditCatalogPins( + {}, + [ + { + dir: "packages/inbox", + packageJson: { dependencies: { react: "catalog:react19" } }, + }, + ], + { react19: { react: "^19.2.0" } }, + ); + expect(report.violations).toEqual([]); +}); + +test("resolveCatalogs reads a top-level catalog and catalogs", () => { + const resolved = resolveCatalogs({ + catalog: { hono: "^4.11.9" }, + catalogs: { react19: { react: "^19.2.0" } }, + }); + expect(resolved.default).toEqual({ hono: "^4.11.9" }); + expect(resolved.named).toEqual({ react19: { react: "^19.2.0" } }); +}); + +test("resolveCatalogs falls back to workspaces.catalog / workspaces.catalogs", () => { + const resolved = resolveCatalogs({ + workspaces: { + catalog: { hono: "^4.11.9" }, + catalogs: { react19: { react: "^19.2.0" } }, + }, + }); + expect(resolved.default).toEqual({ hono: "^4.11.9" }); + expect(resolved.named).toEqual({ react19: { react: "^19.2.0" } }); +}); + +test("resolveCatalogs defaults to empty when neither location is present", () => { + const resolved = resolveCatalogs({ workspaces: ["packages/*"] }); + expect(resolved.default).toEqual({}); + expect(resolved.named).toEqual({}); +}); + +test("vendored manifests are excluded from workspace discovery", () => { + expect(WORKSPACE_GLOBS.some((glob) => glob.startsWith("vendor/"))).toBe( + false, + ); +}); diff --git a/scripts/checks/test/run.test.ts b/scripts/checks/test/run.test.ts new file mode 100644 index 0000000000..828cdc5d1e --- /dev/null +++ b/scripts/checks/test/run.test.ts @@ -0,0 +1,163 @@ +import { expect, test } from "bun:test"; +import path from "node:path"; +import { buildChecks, discoverCheckFiles } from "../run"; + +const REAL_CHECKS_DIR = path.resolve(import.meta.dir, ".."); + +// The full, explicit roster of checks this repo currently ships, kept +// deliberately out of sync with the filesystem: a check whose file grows +// an `import.meta.main` entry point but whose name is missing here (or +// vice versa) fails this test loudly, instead of the runner silently +// dropping it the way `web-tailwind-utilities.ts` did before it gained +// its `if (import.meta.main)` guard. +const EXPECTED_CHECK_NAMES = [ + "browser-safe-subpaths", + "catalog-pins", + "db-gate", + "deletion", + "error-envelope", + "hub-git-safety", + "killdates", + "licenses", + "no-product-tenancy", + "packages", + "react-ui-drift", + "react-ui-pin", + "report-error", + "routine-target-inference", + "tailwind-source", + "tool-package-freshness", + "tool-package-pins", + "tsconfig-references", + "ui-vocabulary", + "web-tailwind-utilities", +].sort(); + +const files: Record = { + "deletion.ts": "if (import.meta.main) main();", + "deletion.test.ts": "if (import.meta.main) main();", + "lib-only.ts": "export function helper() {}", + "allowlist.ts": "export const ALLOWLIST = [];", + "run.ts": "if (import.meta.main) main();", +}; + +function fakeReaddir(_dir: string): string[] { + return Object.keys(files); +} + +function fakeReadFile(file: string): string { + const name = file.split("/").pop() ?? ""; + return files[name] ?? ""; +} + +/** Identity resolver for fake, non-existent test files. */ +const identityRealPath = (file: string): string => file; + +function discover(runnerRealPath: string): string[] { + return discoverCheckFiles( + "/checks", + fakeReaddir, + fakeReadFile, + runnerRealPath, + identityRealPath, + ); +} + +test("discovers only .ts files with an import.meta.main entry point", () => { + const found = discover("/checks/run.ts"); + expect(found).toEqual(["deletion.ts"]); +}); + +test("skips .test.ts files even if they have a main entry point", () => { + expect(discover("/checks/run.ts")).not.toContain("deletion.test.ts"); +}); + +test("excludes the runner by resolved real path, not by filename", () => { + expect(discover("/checks/run.ts")).not.toContain("run.ts"); +}); + +test("excludes a renamed copy of the runner when its real path matches", () => { + const { "run.ts": _runner, ...withoutRunner } = files; + const renamed: Record = { + ...withoutRunner, + "structural.ts": files["run.ts"] ?? "", + }; + const found = discoverCheckFiles( + "/checks", + () => Object.keys(renamed), + (file) => renamed[file.split("/").pop() ?? ""] ?? "", + // The renamed file resolves to the same real path as the runner — + // e.g. it's the runner itself under a new name. + "/checks/structural.ts", + identityRealPath, + ); + expect(found).not.toContain("structural.ts"); + expect(found).toEqual(["deletion.ts"]); +}); + +test("excludes a symlink pointing at the runner", () => { + const symlinked: Record = { + ...files, + "run-link.ts": files["run.ts"] ?? "", + }; + const found = discoverCheckFiles( + "/checks", + () => Object.keys(symlinked), + (file) => symlinked[file.split("/").pop() ?? ""] ?? "", + "/checks/run.ts", + // A symlink resolves to the runner's real path regardless of its + // own name. + (file) => + file.endsWith("run-link.ts") || file.endsWith("run.ts") + ? "/checks/run.ts" + : file, + ); + expect(found).not.toContain("run-link.ts"); + expect(found).toEqual(["deletion.ts"]); +}); + +test("buildChecks always includes tsconfig-references alongside discovered checks", () => { + const checks = buildChecks( + REAL_CHECKS_DIR, + path.resolve(REAL_CHECKS_DIR, "..", ".."), + ); + const names = checks.map((check) => check.name); + expect(names).toContain("tsconfig-references"); + expect(names).toContain("licenses"); + expect(names).toContain("catalog-pins"); + expect(names).not.toContain("react-ui-drift-allowlist"); +}); + +test("buildChecks names are sorted", () => { + const checks = buildChecks( + REAL_CHECKS_DIR, + path.resolve(REAL_CHECKS_DIR, "..", ".."), + ); + const names = checks.map((check) => check.name); + expect(names).toEqual([...names].sort()); +}); + +test("never discovers itself (run.ts) as a check — that would recurse without bound", () => { + const checks = buildChecks( + REAL_CHECKS_DIR, + path.resolve(REAL_CHECKS_DIR, "..", ".."), + ); + expect(checks.map((check) => check.name)).not.toContain("run"); +}); + +test("buildChecks still surfaces packages as a runnable, individually-named check", () => { + const checks = buildChecks( + REAL_CHECKS_DIR, + path.resolve(REAL_CHECKS_DIR, "..", ".."), + ); + expect(checks.map((check) => check.name)).toContain("packages"); +}); + +test("buildChecks' discovered set matches the explicit expected roster exactly", () => { + const checks = buildChecks( + REAL_CHECKS_DIR, + path.resolve(REAL_CHECKS_DIR, "..", ".."), + ); + const names = checks.map((check) => check.name).sort(); + expect(names).toEqual(EXPECTED_CHECK_NAMES); +}); diff --git a/scripts/checks/web-tailwind-utilities.ts b/scripts/checks/web-tailwind-utilities.ts index 5acb1cc1a0..2a388066d0 100644 --- a/scripts/checks/web-tailwind-utilities.ts +++ b/scripts/checks/web-tailwind-utilities.ts @@ -155,4 +155,4 @@ async function main(): Promise { reportAndExit("check:web-utilities", report); } -await main(); +if (import.meta.main) await main();