diff --git a/.changeset/per-package-typecheck-coverage.md b/.changeset/per-package-typecheck-coverage.md new file mode 100644 index 0000000000..933d56f37d --- /dev/null +++ b/.changeset/per-package-typecheck-coverage.md @@ -0,0 +1,4 @@ +--- +--- + +ci(dx): every workspace package now either declares a `typecheck` script (`tsc --noEmit`, run by the new `turbo run typecheck` task in the lint workflow) or carries a measured DEBT/EXEMPT entry in `scripts/check-type-check-coverage.mjs`, whose coverage ratchet runs in CI (#4311). 66 of 77 packages build with tsup, which transpiles without type-checking, and `vitest run` does not type-check either — 48 packages were already clean and are locked in; the 29 failing ones are frozen in the ledger with their raw error counts and the issue's code/config/noise triage. Dev scripts and CI only; releases nothing. diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2cfc49c79d..fcf1aa38f3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -236,6 +236,18 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + # Coverage meta-gate (#4311): 66 of 77 workspace packages build with tsup + # (esbuild transpile, no type check) and `vitest run` does not type-check + # either, so a package without a `typecheck` script has NO tsc reading its + # src/ or its tests at all — 380 real errors had accumulated that way, + # invisible to every green build. This gate makes the hole itself the + # failure: every package either declares `typecheck` (run by the turbo + # step below) or carries a measured DEBT/EXEMPT entry in the script's + # ledger, reconciled in both directions so the ledger can only shrink. + # Reads package.json files only; no build, sub-second. + - name: Check every package is type-check covered or ledgered + run: pnpm check:type-check-coverage + - name: Type check (@objectstack/spec) run: pnpm --filter @objectstack/spec exec tsc --noEmit @@ -351,6 +363,19 @@ jobs: - name: Build workspace packages run: pnpm exec turbo run build --filter='./packages/*' --filter='./examples/*^...' + # The per-package `typecheck` scripts the coverage gate above requires + # (#4311). tsc resolves workspace imports through each dependency's built + # dist/*.d.ts, so the task carries `dependsOn: ^build` in turbo.json — + # which also builds the handful of nested packages (packages/plugins/*, + # packages/services/*, …) the build step's direct-children glob misses + # when no example depends on them. Three filters because the bare + # `./packages/*` glob only matches direct children (see the build step's + # comment): the nested group dirs and apps/ (docs) need their own globs. + # Examples and the downstream contract keep their dedicated steps below — + # they are distinct gates, not bulk coverage. + - name: Type check workspace packages + run: pnpm exec turbo run typecheck --filter='./packages/*' --filter='./packages/*/*' --filter='./apps/*' + - name: Type check example apps run: pnpm --filter './examples/*' run typecheck diff --git a/AGENTS.md b/AGENTS.md index 99706d9ecd..17e1a368dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,9 +20,15 @@ pnpm install # deps pnpm setup # first-time: install + build spec pnpm build # turbo build (excludes docs) pnpm test # turbo test +pnpm typecheck # turbo typecheck — per-package `tsc --noEmit`; tsup/vitest never type-check (#4311) pnpm docs:dev # docs site ``` +Type-check coverage is ratcheted (`pnpm check:type-check-coverage`, CI-gated): every +workspace package declares a `typecheck` script or carries a measured DEBT/EXEMPT entry +in `scripts/check-type-check-coverage.mjs`. New packages must arrive covered; a package +that graduates deletes its ledger entry in the same PR. + ### Running the dev server | Scenario | Command | Notes | @@ -408,7 +414,7 @@ composition with its real services, or do not claim an answer. ## Post-Task Checklist -1. `pnpm test` — verify nothing broke. +1. `pnpm test` — verify nothing broke. Touched a type-check-covered package? `pnpm typecheck` too. 2. **Land it — don't leave passing work in the working tree.** Once tests pass, create a feature branch, commit, push, open a PR, and merge it after remote CI is fully green (see Multi-agent discipline: never straight to `main`, diff --git a/apps/docs/package.json b/apps/docs/package.json index 2ace0268aa..a7aa62b29a 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -10,7 +10,8 @@ "site:start": "next start", "site:lint": "next lint", "types:check": "fumadocs-mdx && next typegen && tsc --noEmit", - "postinstall": "fumadocs-mdx" + "postinstall": "fumadocs-mdx", + "typecheck": "tsc --noEmit" }, "dependencies": { "fumadocs-core": "16.12.1", diff --git a/package.json b/package.json index 3a9fba8e7d..7538c24cc9 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "spec:rebuild": "turbo run build --filter=...@objectstack/spec", "test": "turbo run test", "test:e2e": "turbo run test:e2e", + "typecheck": "turbo run typecheck", "clean": "turbo run clean && rm -rf dist", "setup": "pnpm install && pnpm --filter @objectstack/spec build", "version": "changeset version && node scripts/sync-protocol-version.mjs && node scripts/sync-template-versions.mjs", @@ -41,7 +42,8 @@ "check:console-sha": "node scripts/check-console-sha.mjs", "check:release-notes": "node scripts/check-release-notes.mjs", "check:node-version": "node scripts/check-node-version.mjs", - "check:published-files": "node scripts/check-published-files.mjs --self-test && node scripts/check-published-files.mjs" + "check:published-files": "node scripts/check-published-files.mjs --self-test && node scripts/check-published-files.mjs", + "check:type-check-coverage": "node scripts/check-type-check-coverage.mjs --self-test && node scripts/check-type-check-coverage.mjs" }, "keywords": [ "objectstack", diff --git a/packages/apps/account/package.json b/packages/apps/account/package.json index a3d21658ea..135660b4f2 100644 --- a/packages/apps/account/package.json +++ b/packages/apps/account/package.json @@ -13,7 +13,8 @@ } }, "scripts": { - "build": "tsup" + "build": "tsup", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/platform-objects": "workspace:*", diff --git a/packages/apps/setup/package.json b/packages/apps/setup/package.json index d883b7c3b9..cd4691dfef 100644 --- a/packages/apps/setup/package.json +++ b/packages/apps/setup/package.json @@ -13,7 +13,8 @@ } }, "scripts": { - "build": "tsup" + "build": "tsup", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/platform-objects": "workspace:*", diff --git a/packages/apps/studio/package.json b/packages/apps/studio/package.json index 2404c711e4..ce77eed6fb 100644 --- a/packages/apps/studio/package.json +++ b/packages/apps/studio/package.json @@ -13,7 +13,8 @@ } }, "scripts": { - "build": "tsup" + "build": "tsup", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/platform-objects": "workspace:*", diff --git a/packages/cli/package.json b/packages/cli/package.json index 5bec4a9a13..87532fb77c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -11,7 +11,8 @@ "scripts": { "build": "if [ -n \"$OS_SKIP_DTS\" ]; then tsc -p tsconfig.build.json --noCheck --declaration false --declarationMap false; else tsc -p tsconfig.build.json; fi", "dev": "tsc -p tsconfig.build.json --watch", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "keywords": [ "objectstack", diff --git a/packages/client-react/package.json b/packages/client-react/package.json index 407147c97c..fa66036621 100644 --- a/packages/client-react/package.json +++ b/packages/client-react/package.json @@ -13,7 +13,8 @@ } }, "scripts": { - "build": "tsup src/index.tsx --config ../../tsup.config.ts" + "build": "tsup src/index.tsx --config ../../tsup.config.ts", + "typecheck": "tsc --noEmit" }, "peerDependencies": { "react": ">=18.0.0" diff --git a/packages/client/package.json b/packages/client/package.json index 095bfd6305..2a32634a36 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -15,7 +15,8 @@ "scripts": { "build": "tsup --config ../../tsup.config.ts", "test": "vitest run", - "test:integration": "vitest run --config vitest.integration.config.ts" + "test:integration": "vitest run --config vitest.integration.config.ts", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/connectors/connector-mcp/package.json b/packages/connectors/connector-mcp/package.json index 0787ce7cdf..dfed03662a 100644 --- a/packages/connectors/connector-mcp/package.json +++ b/packages/connectors/connector-mcp/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/packages/connectors/connector-openapi/package.json b/packages/connectors/connector-openapi/package.json index 73fa332edb..d572fbf609 100644 --- a/packages/connectors/connector-openapi/package.json +++ b/packages/connectors/connector-openapi/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/connectors/connector-rest/package.json b/packages/connectors/connector-rest/package.json index 02ca8059d1..758d5d3435 100644 --- a/packages/connectors/connector-rest/package.json +++ b/packages/connectors/connector-rest/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/connectors/connector-slack/package.json b/packages/connectors/connector-slack/package.json index 47752f801a..c4f42dcc4c 100644 --- a/packages/connectors/connector-slack/package.json +++ b/packages/connectors/connector-slack/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/formula/package.json b/packages/formula/package.json index 78cb50a4f4..6fdc44c591 100644 --- a/packages/formula/package.json +++ b/packages/formula/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../tsup.config.ts", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@marcbachmann/cel-js": "^8.0.0", diff --git a/packages/lint/package.json b/packages/lint/package.json index 0a7f1843bd..a6dcd2d3ae 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -16,7 +16,8 @@ "scripts": { "build": "tsup --config ../../tsup.config.ts", "dev": "tsc -w", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/formula": "workspace:*", diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 772047cd2e..b1fb5b8d40 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -15,7 +15,8 @@ }, "scripts": { "build": "tsup --config ../../tsup.config.ts", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/packages/metadata-core/package.json b/packages/metadata-core/package.json index 5181fe3dfd..033afee556 100644 --- a/packages/metadata-core/package.json +++ b/packages/metadata-core/package.json @@ -28,7 +28,8 @@ "dev": "tsc --watch", "clean": "rm -rf dist", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "typecheck": "tsc --noEmit" }, "keywords": [ "objectstack", diff --git a/packages/metadata-fs/package.json b/packages/metadata-fs/package.json index b2a86dd6b1..c72fb33fb2 100644 --- a/packages/metadata-fs/package.json +++ b/packages/metadata-fs/package.json @@ -23,7 +23,8 @@ "dev": "tsc --watch", "clean": "rm -rf dist", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "typecheck": "tsc --noEmit" }, "keywords": [ "objectstack", diff --git a/packages/objectql/package.json b/packages/objectql/package.json index 101e9b9769..5c178aeaa5 100644 --- a/packages/objectql/package.json +++ b/packages/objectql/package.json @@ -19,7 +19,8 @@ }, "scripts": { "build": "tsup", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/platform-objects/package.json b/packages/platform-objects/package.json index 9c5189dec8..7594a17952 100644 --- a/packages/platform-objects/package.json +++ b/packages/platform-objects/package.json @@ -64,7 +64,8 @@ }, "scripts": { "build": "tsup", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/metadata-core": "workspace:*", diff --git a/packages/plugins/driver-mongodb/package.json b/packages/plugins/driver-mongodb/package.json index 3f8b2ccbc7..1f303ee28b 100644 --- a/packages/plugins/driver-mongodb/package.json +++ b/packages/plugins/driver-mongodb/package.json @@ -15,7 +15,8 @@ "scripts": { "build": "tsup --config ../../../tsup.config.ts", "dev": "tsc -w", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/plugins/embedder-openai/package.json b/packages/plugins/embedder-openai/package.json index ed8f935f69..edca5beb20 100644 --- a/packages/plugins/embedder-openai/package.json +++ b/packages/plugins/embedder-openai/package.json @@ -15,7 +15,8 @@ "scripts": { "build": "tsup --config ../../../tsup.config.ts", "dev": "tsc -w", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/spec": "workspace:*" diff --git a/packages/plugins/knowledge-memory/package.json b/packages/plugins/knowledge-memory/package.json index 535b1c8439..0618f12556 100644 --- a/packages/plugins/knowledge-memory/package.json +++ b/packages/plugins/knowledge-memory/package.json @@ -15,7 +15,8 @@ "scripts": { "build": "tsup --config ../../../tsup.config.ts", "dev": "tsc -w", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/plugins/plugin-audit/package.json b/packages/plugins/plugin-audit/package.json index c4c1431e49..10c4a6d793 100644 --- a/packages/plugins/plugin-audit/package.json +++ b/packages/plugins/plugin-audit/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index 593e463a8f..ac424e37e8 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -15,7 +15,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@better-auth/core": "1.7.0-rc.2", diff --git a/packages/plugins/plugin-dev/package.json b/packages/plugins/plugin-dev/package.json index 94810d0e1f..27843c085c 100644 --- a/packages/plugins/plugin-dev/package.json +++ b/packages/plugins/plugin-dev/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/account": "workspace:^", diff --git a/packages/plugins/plugin-email/package.json b/packages/plugins/plugin-email/package.json index dd30a0f7a7..74f6c13dc6 100644 --- a/packages/plugins/plugin-email/package.json +++ b/packages/plugins/plugin-email/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/plugins/plugin-hono-server/package.json b/packages/plugins/plugin-hono-server/package.json index 70c807d604..d8cfe6f3ee 100644 --- a/packages/plugins/plugin-hono-server/package.json +++ b/packages/plugins/plugin-hono-server/package.json @@ -7,7 +7,8 @@ "types": "dist/index.d.ts", "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@hono/node-server": "^2.0.12", diff --git a/packages/plugins/plugin-pinyin-search/package.json b/packages/plugins/plugin-pinyin-search/package.json index 918b6a3042..a3f2607509 100644 --- a/packages/plugins/plugin-pinyin-search/package.json +++ b/packages/plugins/plugin-pinyin-search/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/plugins/plugin-reports/package.json b/packages/plugins/plugin-reports/package.json index 36177693b7..a85d7ae6a4 100644 --- a/packages/plugins/plugin-reports/package.json +++ b/packages/plugins/plugin-reports/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index 43509bf6cf..b24a527cc3 100644 --- a/packages/plugins/plugin-security/package.json +++ b/packages/plugins/plugin-security/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/plugins/plugin-sharing/package.json b/packages/plugins/plugin-sharing/package.json index 1198b63071..349189351a 100644 --- a/packages/plugins/plugin-sharing/package.json +++ b/packages/plugins/plugin-sharing/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/plugins/plugin-webhooks/package.json b/packages/plugins/plugin-webhooks/package.json index 6337cc14d8..5d64976315 100644 --- a/packages/plugins/plugin-webhooks/package.json +++ b/packages/plugins/plugin-webhooks/package.json @@ -20,7 +20,8 @@ }, "scripts": { "build": "tsup", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/qa/http-conformance/package.json b/packages/qa/http-conformance/package.json index 714a2562a2..3bc06d3a53 100644 --- a/packages/qa/http-conformance/package.json +++ b/packages/qa/http-conformance/package.json @@ -6,7 +6,8 @@ "description": "HTTP transport-port conformance gate (ADR-0076 D11/OQ#10, #2462) \u2014 a zero-dependency node:http reference implementation of IHttpServer plus a cross-adapter suite that boots the dispatcher bridge and REST generator on it AND on plugin-hono-server, pinning that the port stays free of framework-isms. Not published; validation instrument, not a product server.", "type": "module", "scripts": { - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*" diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index 5ba9319d3e..bf74b7fda3 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../tsup.config.ts", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "devDependencies": { "typescript": "^6.0.3", diff --git a/packages/services/service-cluster-redis/package.json b/packages/services/service-cluster-redis/package.json index 474679f263..659986bda9 100644 --- a/packages/services/service-cluster-redis/package.json +++ b/packages/services/service-cluster-redis/package.json @@ -15,7 +15,8 @@ }, "scripts": { "build": "tsup", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/service-cluster": "workspace:*", diff --git a/packages/services/service-package/package.json b/packages/services/service-package/package.json index 1720ac4bb7..b3c6221383 100644 --- a/packages/services/service-package/package.json +++ b/packages/services/service-package/package.json @@ -15,7 +15,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/services/service-sms/package.json b/packages/services/service-sms/package.json index 52a62a00c1..3b2d6d4272 100644 --- a/packages/services/service-sms/package.json +++ b/packages/services/service-sms/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/spec/package.json b/packages/spec/package.json index fb6045179f..9bdf810122 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -214,7 +214,8 @@ "gen:react-blocks": "tsx scripts/build-react-blocks-contract.ts", "check:react-blocks": "tsx scripts/build-react-blocks-contract.ts --check", "check:react-conformance": "tsx scripts/check-react-blocks-conformance.ts", - "check:skill-examples": "tsx scripts/check-skill-examples.ts" + "check:skill-examples": "tsx scripts/check-skill-examples.ts", + "typecheck": "tsc --noEmit" }, "keywords": [ "objectstack", diff --git a/packages/triggers/trigger-api/package.json b/packages/triggers/trigger-api/package.json index b72523bf54..21ed12f2ce 100644 --- a/packages/triggers/trigger-api/package.json +++ b/packages/triggers/trigger-api/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/triggers/trigger-record-change/package.json b/packages/triggers/trigger-record-change/package.json index ed973daa0c..893bfd1a92 100644 --- a/packages/triggers/trigger-record-change/package.json +++ b/packages/triggers/trigger-record-change/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/triggers/trigger-schedule/package.json b/packages/triggers/trigger-schedule/package.json index 7bccaf44dd..9fa828e894 100644 --- a/packages/triggers/trigger-schedule/package.json +++ b/packages/triggers/trigger-schedule/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/verify/package.json b/packages/verify/package.json index ba2c2789f8..f845d08ff6 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -16,7 +16,8 @@ "scripts": { "build": "tsup --config ../../tsup.config.ts", "dev": "tsc -w", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/vscode-objectstack/package.json b/packages/vscode-objectstack/package.json index 6928651159..62bece1b95 100644 --- a/packages/vscode-objectstack/package.json +++ b/packages/vscode-objectstack/package.json @@ -56,7 +56,8 @@ "scripts": { "build": "tsc -p ./tsconfig.json", "watch": "tsc -watch -p ./tsconfig.json", - "package": "vsce package" + "package": "vsce package", + "typecheck": "tsc --noEmit" }, "devDependencies": { "@types/vscode": "^1.125.0", diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs new file mode 100644 index 0000000000..535b084221 --- /dev/null +++ b/scripts/check-type-check-coverage.mjs @@ -0,0 +1,547 @@ +#!/usr/bin/env node +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// check-type-check-coverage -- every workspace package's TypeScript is read by +// tsc somewhere, or its absence is a recorded, tracked decision (#4311). +// +// 66 of 77 workspace packages build with tsup, which transpiles with esbuild +// and never type-checks. `vitest run` does not type-check either. And the CI +// typecheck job covered exactly four targets (spec, examples, +// downstream-contract, docs code blocks) -- so for most packages NOTHING read +// src/ or the tests with a type checker at all. #4311 measured the hole: +// 380 code-tier errors across 18 packages, 241 of them in driver-sql tests +// passing authored-shape literals into the freshly narrowed QueryAST. A green +// test suite no tsc has ever read is not evidence of a contract; this gate +// makes the coverage hole itself the failure, so it can only shrink. +// +// node scripts/check-type-check-coverage.mjs +// node scripts/check-type-check-coverage.mjs --self-test +// +// Invariants, per workspace package (the root workspace package included -- +// #4311's audit counted its top-level TypeScript like any other package's): +// +// COVERED the package declares a `typecheck` script, OR carries a DEBT +// entry (measured tsc error count + tracking issue) or an EXEMPT +// entry (why type-checking cannot apply) below. A new package +// must arrive covered -- the ledger is closed to new debt. +// REAL a declared `typecheck` script actually invokes tsc. A script +// that echoes, lints, or runs tests is not type coverage. +// RUNNABLE turbo.json declares the `typecheck` task, the root `typecheck` +// script aggregates it (`turbo run typecheck`, the build/test +// convention), and lint.yml invokes it -- a script CI never +// executes is not coverage either (#4203: gates that only run +// where nobody runs them, rot). +// RECONCILED in both directions: a DEBT/EXEMPT entry for a package that now +// declares `typecheck`, or that no longer exists, is an error. +// A ledger that can only accrete rots into a list nobody trusts. +// +// The root is the one asymmetry: its `typecheck` script is the workspace +// aggregator, so its OWN top-level TypeScript is covered by a `typecheck:root` +// script (tsc, invoked from lint.yml) or by a ledger entry like anyone else. +// +// DEBT is frozen debt, not a permission slip. Every entry below was measured +// by running the package's own `tsc --noEmit` on main (see the issue for the +// code-tier / config-tier / noise split -- raw counts here include all three). +// To onboard a package: fix (or config-fix) its errors, add +// `"typecheck": "tsc --noEmit"` to its package.json, and delete its entry +// here in the same PR. Deleting the entry without the script fails COVERED; +// keeping the entry alongside the script fails RECONCILED. + +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import { join, posix, resolve } from 'node:path'; + +// Anchored to the script, not to cwd: the verdict must not depend on where the +// guard was invoked from. +const ROOT = resolve(import.meta.dirname, '..'); +const SELF = 'scripts/check-type-check-coverage.mjs'; +const WORKSPACE_FILE = 'pnpm-workspace.yaml'; +const TRACKING_ISSUE = 'https://github.com/objectstack-ai/objectstack/issues/4311'; + +// Package name -> { errors, note? }. `errors` is the raw `tsc --noEmit` count +// measured per package on main @ e5a4d26 (2026-07-31, the #4311 audit re-run +// one commit later; aggregates matched exactly: 48 clean / 29 failing). +// Raw counts include all three of #4311's tiers -- code-tier (real defects), +// config-tier (the check itself misconfigured: TS2591/TS2584 missing +// `types:["node"]`, TS2835/TS2307 module resolution) and noise (TS7006 +// implicit-any params, TS6133 unused) -- so each note says what the pile is +// made of. Nobody should mistake a config-tier pile for real breakage, or -- +// worse -- the reverse: `core` at 91 raw has 3 real errors, while +// `driver-sql`'s 241 are ALL real. +const DEBT = { + '@objectstack/cloud-connection': { + errors: 13, + note: 'code-tier 11 (TS2493 tuple indexing) + 2 config-tier.', + }, + '@objectstack/core': { + errors: 91, + note: 'code-tier 3; the rest is config-tier (TS2835/TS2347 module resolution) and noise (TS7006).', + }, + '@objectstack/dogfood': { + errors: 12, + note: 'code-tier 8 (TS2322/TS2554) + 3 config-tier + 1 noise.', + }, + '@objectstack/driver-memory': { + errors: 23, + note: 'all code-tier: TS2345/TS2741/TS2339, the QueryAST authored-vs-parsed playbook.', + }, + '@objectstack/driver-sql': { + errors: 241, + note: 'ALL code-tier (TS2345 x123, TS2353 x118): tests pass authored-shape literals into the parsed QueryAST type across 8+ test files; #4196/#4286 narrowed QueryAST and no tsc ever read these tests. 63% of the whole audit; single playbook, fix as one batch.', + }, + '@objectstack/driver-sqlite-wasm': { + errors: 27, + note: 'all code-tier (TS2345/TS2353), same QueryAST playbook as driver-sql.', + }, + '@objectstack/hono': { + errors: 3, + note: 'all code-tier (TS2769/TS18046).', + }, + '@objectstack/knowledge-ragflow': { + errors: 4, + note: 'code-tier 3 (TS2353) + 1 config-tier (TS2550 lib).', + }, + '@objectstack/metadata': { + errors: 87, + note: 'code-tier 31 (TS2345/TS2353); the rest is config-tier (TS2835) and noise (TS7006).', + }, + '@objectstack/metadata-protocol': { + errors: 21, + note: 'code-tier 2; the rest is config-tier (TS2835) and noise (TS7006).', + }, + '@objectstack/observability': { + errors: 11, + note: 'all code-tier (TS2554 wrong arity x10, TS2552).', + }, + '@objectstack/plugin-approvals': { + errors: 1, + note: 'config-tier only (TS2550 lib); no code-tier finding in #4311.', + }, + '@objectstack/rest': { + errors: 2, + note: 'code-tier 2 (TS2345).', + }, + '@objectstack/runtime': { + errors: 18, + note: 'noise only (TS6133 unused); no code-tier finding in #4311.', + }, + '@objectstack/service-analytics': { + errors: 3, + note: 'code-tier 2 (TS7053) + 1 noise.', + }, + '@objectstack/service-automation': { + errors: 2, + note: 'code-tier 2 (TS2741: engine.test.ts misses resumeAuthority, the #4198 discovery that opened #4311).', + }, + '@objectstack/service-cache': { + errors: 3, + note: 'config-tier only (TS2835); no code-tier finding in #4311.', + }, + '@objectstack/service-cluster': { + errors: 1, + note: 'code-tier 1 (TS2322).', + }, + '@objectstack/service-i18n': { + errors: 14, + note: 'config-tier (TS2835) and noise (TS7006) only; no code-tier finding in #4311.', + }, + '@objectstack/service-job': { + errors: 6, + note: 'config-tier (TS2835) and noise only; no code-tier finding in #4311.', + }, + '@objectstack/service-knowledge': { + errors: 8, + note: 'code-tier 3 (TS2339/TS2352/TS2493); the rest config-tier and noise.', + }, + '@objectstack/service-messaging': { + errors: 1, + note: 'noise only (TS6133); no code-tier finding in #4311.', + }, + '@objectstack/service-queue': { + errors: 9, + note: 'config-tier (TS2835) and noise (TS7006) only; no code-tier finding in #4311.', + }, + '@objectstack/service-realtime': { + errors: 9, + note: 'config-tier (TS2835) and noise (TS7006) only; no code-tier finding in #4311.', + }, + '@objectstack/service-settings': { + errors: 44, + note: 'config-tier (TS2307/TS2835 module resolution) and noise (TS7006) only; no code-tier finding in #4311.', + }, + '@objectstack/service-storage': { + errors: 42, + note: 'code-tier 5 (TS2339/TS2347); the rest is config-tier (TS2835) and noise (TS7006).', + }, + '@objectstack/spec-monorepo': { + errors: 50, + note: 'the workspace root itself: code-tier 2 (TS2304); the rest is config-tier (TS2307/TS2591/TS2584 -- the root tsconfig has no `types:["node"]`) and noise.', + }, + '@objectstack/types': { + errors: 107, + note: 'config-tier only (TS2591/TS2584: missing `types:["node"]`/lib in the check config); no code-tier finding in #4311.', + }, + 'create-objectstack': { + errors: 5, + note: 'config-tier only (TS2307 module resolution of template files); no code-tier finding in #4311.', + }, +}; + +// Package name -> why running tsc over it is not applicable at all. An EXEMPT +// entry is a statement about the package's nature, not about its debt; if the +// nature changes, the RECONCILED direction forces this entry out. +const EXEMPT = { + '@objectstack/console': + 'Published objectui build artifact -- package.json/README/CHANGELOG plus a dist/ pulled in by `pnpm objectui:refresh`. No TypeScript sources, no tsconfig; the sources are type-checked in the objectui repo.', +}; + +/** + * The `packages:` globs from pnpm-workspace.yaml. Blank lines and comments are + * skipped rather than treated as the end of the list: stopping early would + * drop members from the scan and report a clean run over a partial workspace. + */ +function workspaceGlobs() { + const lines = readFileSync(join(ROOT, WORKSPACE_FILE), 'utf8').split(/\r?\n/); + const start = lines.findIndex((l) => /^packages\s*:\s*$/.test(l)); + if (start === -1) throw new Error(`${WORKSPACE_FILE}: no top-level \`packages:\` block`); + const globs = []; + for (let i = start + 1; i < lines.length; i++) { + const line = lines[i].replace(/#.*$/, '').trimEnd(); + if (!line.trim()) continue; + const m = line.match(/^\s+-\s+['"]?([^'"\s]+)['"]?\s*$/); + if (m) { + globs.push(m[1]); + continue; + } + if (/^\S/.test(line)) break; // the next top-level key ends the block + } + if (globs.length === 0) throw new Error(`${WORKSPACE_FILE}: \`packages:\` block is empty`); + return globs; +} + +/** Every workspace member as { name, dir, scripts, hasTsconfig }. */ +function workspacePackages() { + const dirs = []; + for (const glob of workspaceGlobs()) { + // Every pattern in this repo is `` or `/*`. Anything richer + // would silently resolve to nothing, so reject it rather than under-report. + const star = glob.endsWith('/*'); + const base = star ? glob.slice(0, -2) : glob; + if (base.includes('*')) { + throw new Error(`${WORKSPACE_FILE}: pattern "${glob}" is richer than or /*; extend ${SELF}`); + } + const abs = join(ROOT, base); + if (!existsSync(abs)) continue; + const candidates = star ? readdirSync(abs).map((e) => posix.join(base, e)) : [base]; + for (const c of candidates) { + if (existsSync(join(ROOT, c, 'package.json'))) dirs.push(c); + } + } + return dirs.sort().map((dir) => { + const manifest = JSON.parse(readFileSync(join(ROOT, dir, 'package.json'), 'utf8')); + return { + name: manifest.name ?? dir, + dir, + scripts: manifest.scripts ?? {}, + hasTsconfig: existsSync(join(ROOT, dir, 'tsconfig.json')), + }; + }); +} + +/** + * Pure verdict over an observed workspace state; the real run and the + * self-test both go through here, so the semantics the fixtures prove are the + * semantics the gate applies. + * + * @param {Array<{name: string, dir: string, scripts: Record, hasTsconfig: boolean}>} packages + * @param {{name: string, scripts: Record}} root + * @param {{ debt: Record, + * exempt: Record, + * turboHasTask: boolean, ciInvokesTask: boolean, ciInvokesRoot: boolean }} state + * @returns {string[]} problems, empty when the ratchet holds + */ +function evaluate(packages, root, state) { + const problems = []; + const byName = new Map(packages.map((p) => [p.name, p])); + byName.set(root.name, root); + + for (const pkg of packages) { + const script = pkg.scripts.typecheck; + const inDebt = Object.hasOwn(state.debt, pkg.name); + const inExempt = Object.hasOwn(state.exempt, pkg.name); + + if (script !== undefined) { + // REAL: the script must put tsc in front of the package's sources. + if (!/\btsc\b/.test(script)) { + problems.push( + `${pkg.name} (${pkg.dir}): \`typecheck\` script does not invoke tsc ("${script}") -- ` + + `a typecheck that never type-checks satisfies the letter of COVERED and nothing else.`, + ); + } + // RECONCILED: covered packages must not also sit in the ledger. + if (inDebt) { + problems.push( + `${pkg.name}: declares \`typecheck\` but still has a DEBT entry -- it graduated; ` + + `delete its entry from DEBT in ${SELF}.`, + ); + } + if (inExempt) { + problems.push( + `${pkg.name}: declares \`typecheck\` but still has an EXEMPT entry -- ` + + `delete its entry from EXEMPT in ${SELF}.`, + ); + } + continue; + } + + // COVERED: no script, so the ledger must own the gap -- with substance. + if (inDebt) { + const entry = state.debt[pkg.name]; + if (!entry || typeof entry.errors !== 'number' || entry.errors <= 0) { + problems.push( + `${pkg.name}: DEBT entry has no measured error count -- run its \`tsc --noEmit\`, ` + + `record the number, or onboard it outright.`, + ); + } + } else if (inExempt) { + if (!String(state.exempt[pkg.name] ?? '').trim()) { + problems.push(`${pkg.name}: EXEMPT entry has no reason -- say why tsc cannot apply, or onboard it.`); + } + } else { + problems.push( + `${pkg.name} (${pkg.dir}): no \`typecheck\` script and no ledger entry. ` + + `Add \`"typecheck": "tsc --noEmit"\` to its package.json (tsup/vitest never type-check, ` + + `so without it nothing reads this package's types at all -- see ${TRACKING_ISSUE}). ` + + `Only if the errors are too large to fix now: measure them and add a DEBT entry in ${SELF}.`, + ); + } + } + + // The root's own top-level TypeScript, covered via `typecheck:root` (its + // `typecheck` slot is the workspace aggregator, asserted under RUNNABLE). + const rootScript = root.scripts['typecheck:root']; + const rootInDebt = Object.hasOwn(state.debt, root.name); + const rootInExempt = Object.hasOwn(state.exempt, root.name); + if (rootScript !== undefined) { + if (!/\btsc\b/.test(rootScript)) { + problems.push(`${root.name}: \`typecheck:root\` does not invoke tsc ("${rootScript}").`); + } + if (rootInDebt) { + problems.push( + `${root.name}: declares \`typecheck:root\` but still has a DEBT entry -- it graduated; ` + + `delete its entry from DEBT in ${SELF}.`, + ); + } + if (rootInExempt) { + problems.push(`${root.name}: declares \`typecheck:root\` but still has an EXEMPT entry -- delete it from ${SELF}.`); + } + if (!state.ciInvokesRoot) { + problems.push( + `.github/workflows/lint.yml never invokes \`typecheck:root\` -- the root's own ` + + `TypeScript is declared covered but CI never reads it. Add the step.`, + ); + } + } else if (rootInDebt) { + const entry = state.debt[root.name]; + if (!entry || typeof entry.errors !== 'number' || entry.errors <= 0) { + problems.push(`${root.name}: DEBT entry has no measured error count.`); + } + } else if (rootInExempt) { + if (!String(state.exempt[root.name] ?? '').trim()) { + problems.push(`${root.name}: EXEMPT entry has no reason.`); + } + } else { + problems.push( + `${root.name} (workspace root): no \`typecheck:root\` script and no ledger entry -- ` + + `the root's own top-level TypeScript (tsup.config.ts and friends) is in the audit too (${TRACKING_ISSUE}).`, + ); + } + + // RECONCILED, other direction: ledger entries must point at live packages. + for (const name of Object.keys(state.debt)) { + if (!byName.has(name)) { + problems.push(`DEBT entry for "${name}" names no workspace package -- remove it from ${SELF}.`); + } + } + for (const name of Object.keys(state.exempt)) { + if (!byName.has(name)) { + problems.push(`EXEMPT entry for "${name}" names no workspace package -- remove it from ${SELF}.`); + } + } + + // RUNNABLE: coverage that nothing executes is not coverage. + if (!state.turboHasTask) { + problems.push( + `turbo.json does not declare a \`typecheck\` task -- \`turbo run typecheck\` runs nothing, ` + + `so every per-package script above is dead. Restore the task (dependsOn ^build).`, + ); + } + if (!/\bturbo run typecheck\b/.test(root.scripts.typecheck ?? '')) { + problems.push( + `the root \`typecheck\` script must aggregate the workspace (\`turbo run typecheck\`, ` + + `the build/test convention) so one command runs every declared check locally.`, + ); + } + if (!state.ciInvokesTask) { + problems.push( + `.github/workflows/lint.yml does not invoke \`turbo run typecheck\` -- the per-package ` + + `scripts exist but CI never runs them (#4203 is the history of exactly this). Restore the step.`, + ); + } + + return problems; +} + +/** The observed non-fixture state. */ +function observed() { + const turbo = JSON.parse(readFileSync(join(ROOT, 'turbo.json'), 'utf8')); + const lintYml = readFileSync(join(ROOT, '.github/workflows/lint.yml'), 'utf8'); + const rootManifest = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')); + return { + root: { name: rootManifest.name, scripts: rootManifest.scripts ?? {} }, + state: { + debt: DEBT, + exempt: EXEMPT, + turboHasTask: Object.hasOwn(turbo.tasks ?? {}, 'typecheck'), + ciInvokesTask: /turbo run typecheck/.test(lintYml), + ciInvokesRoot: /typecheck:root/.test(lintYml), + }, + }; +} + +/** + * The ledger semantics are the one part of this gate that can be wrong while + * every package is right -- an evaluate() that under-reports waves the next + * uncovered package through, silently. So each failure class is asserted + * against a fixture before the real run is allowed to say OK. + */ +function selfTest() { + const pkg = (name, extra = {}) => ({ name, dir: `packages/${name}`, scripts: {}, hasTsconfig: true, ...extra }); + const okRoot = { + name: 'root', + scripts: { typecheck: 'turbo run typecheck', 'typecheck:root': 'tsc --noEmit' }, + }; + const okState = { debt: {}, exempt: {}, turboHasTask: true, ciInvokesTask: true, ciInvokesRoot: true }; + const cases = [ + { + label: 'covered package passes', + packages: [pkg('a', { scripts: { typecheck: 'tsc --noEmit' } })], + root: okRoot, + state: okState, + expect: [], + }, + { + label: 'uncovered, unledgered package fails COVERED', + packages: [pkg('a')], + root: okRoot, + state: okState, + expect: [/no `typecheck` script and no ledger entry/], + }, + { + label: 'debt-ledgered package passes, but an empty measurement fails', + packages: [pkg('a'), pkg('b')], + root: okRoot, + state: { ...okState, debt: { a: { errors: 12 }, b: {} } }, + expect: [/b: DEBT entry has no measured error count/], + }, + { + label: 'exempt package passes only with a reason', + packages: [pkg('a'), pkg('b')], + root: okRoot, + state: { ...okState, exempt: { a: 'no sources', b: ' ' } }, + expect: [/b: EXEMPT entry has no reason/], + }, + { + label: 'a typecheck script that never runs tsc fails REAL', + packages: [pkg('a', { scripts: { typecheck: 'echo ok' } })], + root: okRoot, + state: okState, + expect: [/does not invoke tsc/], + }, + { + label: 'graduating without deleting the ledger entry fails RECONCILED', + packages: [pkg('a', { scripts: { typecheck: 'tsc --noEmit' } })], + root: okRoot, + state: { ...okState, debt: { a: { errors: 3 } } }, + expect: [/declares `typecheck` but still has a DEBT entry/], + }, + { + label: 'ledger entries for vanished packages fail RECONCILED', + packages: [pkg('a', { scripts: { typecheck: 'tsc --noEmit' } })], + root: okRoot, + state: { ...okState, debt: { gone: { errors: 1 } }, exempt: { also_gone: 'x' } }, + expect: [/DEBT entry for "gone"/, /EXEMPT entry for "also_gone"/], + }, + { + label: 'a missing turbo task or CI step fails RUNNABLE', + packages: [pkg('a', { scripts: { typecheck: 'tsc --noEmit' } })], + root: okRoot, + state: { ...okState, turboHasTask: false, ciInvokesTask: false }, + expect: [/turbo\.json does not declare/, /lint\.yml does not invoke/], + }, + { + label: 'an unledgered root without typecheck:root fails COVERED', + packages: [], + root: { name: 'root', scripts: { typecheck: 'turbo run typecheck' } }, + state: okState, + expect: [/root.*no `typecheck:root` script and no ledger entry/], + }, + { + label: 'a debt-ledgered root passes; graduating it stale-fails like anyone else', + packages: [], + root: { name: 'root', scripts: { typecheck: 'turbo run typecheck' } }, + state: { ...okState, debt: { root: { errors: 50 } } }, + expect: [], + }, + { + label: 'a root aggregator that does not run turbo fails RUNNABLE', + packages: [], + root: { name: 'root', scripts: { typecheck: 'tsc --noEmit', 'typecheck:root': 'tsc --noEmit' } }, + state: okState, + expect: [/root `typecheck` script must aggregate/], + }, + { + label: 'a covered root that CI never runs fails RUNNABLE', + packages: [], + root: okRoot, + state: { ...okState, ciInvokesRoot: false }, + expect: [/never invokes `typecheck:root`/], + }, + ]; + + const failures = []; + for (const c of cases) { + const got = evaluate(c.packages, c.root, c.state); + if (got.length !== c.expect.length || !c.expect.every((rx, i) => rx.test(got[i]))) { + failures.push(`${c.label}: expected ${c.expect.length} problem(s) matching ${c.expect}, got ${JSON.stringify(got)}`); + } + } + if (failures.length) { + console.error(`✗ check:type-check-coverage --self-test — ${failures.length} failure(s)\n`); + for (const f of failures) console.error(' • ' + f); + process.exit(1); + } + console.log(`✓ check:type-check-coverage --self-test — ${cases.length} semantic case(s) hold.`); +} + +if (process.argv.includes('--self-test')) { + selfTest(); + process.exit(0); +} + +const packages = workspacePackages(); +const { root, state } = observed(); +const problems = evaluate(packages, root, state); + +if (problems.length) { + console.error(`check-type-check-coverage: ${problems.length} problem(s)\n`); + for (const p of problems) console.error(' • ' + p); + process.exit(1); +} + +const covered = packages.filter((p) => p.scripts.typecheck !== undefined).length; +const debtTotal = Object.values(DEBT).reduce((sum, e) => sum + (e.errors ?? 0), 0); +console.log( + `check-type-check-coverage: OK — ${covered}/${packages.length} workspace packages type-checked ` + + `(plus the root), ${Object.keys(DEBT).length} in the DEBT ledger (${debtTotal} frozen raw errors, ` + + `${TRACKING_ISSUE}), ${Object.keys(EXEMPT).length} exempt.`, +); diff --git a/turbo.json b/turbo.json index 6b670ca659..9d6cb71bd0 100644 --- a/turbo.json +++ b/turbo.json @@ -12,6 +12,11 @@ "outputs": [], "inputs": ["$TURBO_DEFAULT$", "!dist/**", "!coverage/**", "!.turbo/**"] }, + "typecheck": { + "dependsOn": ["^build"], + "outputs": [], + "inputs": ["$TURBO_DEFAULT$", "!dist/**", "!coverage/**", "!.turbo/**"] + }, "@objectstack/metadata#test": { "dependsOn": ["build"], "outputs": [],