diff --git a/.dockerignore b/.dockerignore index 8cd20554e1..bcf6d2cc7d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,7 +12,7 @@ dist-ssr coverage .git .claude -# The self-host bundle's only entry point is src/server.ts (scripts/build-selfhost.mjs) and `npm ci` +# The self-host bundle's only entry point is src/server.ts (scripts/build-selfhost.ts) and `npm ci` # only ever sees the root package*.json (copied before the rest of the tree) — the loopover-ui # workspace app and the test suite are never read during the image build, so keep both out of the # build context entirely (measured: ~11MB of this repo's ~22MB tracked-file footprint). @@ -26,7 +26,7 @@ auth.json **/auth.json # The review-enrichment service (REES) is a separate Railway service with its own Dockerfile — keep it out of # the engine image. EXCEPT analyzer-metadata.json: the main engine's own code (src/review/enrichment-analyzers- -# taxonomy.ts) imports it directly, so excluding it wholesale breaks `scripts/build-selfhost.mjs`'s esbuild +# taxonomy.ts) imports it directly, so excluding it wholesale breaks `scripts/build-selfhost.ts`'s esbuild # bundle (module resolution failure at build time, not a runtime gap -- caught by the "build + boot smoke test" # workflow, which is path-gated and doesn't run on every PR). review-enrichment diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdc225b786..adb70df6d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,14 +160,14 @@ jobs: # and the pack check only inspects the tarball. Root src/ cannot # affect it, so it is intentionally NOT a trigger here. - 'packages/loopover-mcp/**' - - 'scripts/check-mcp-package.mjs' + - 'scripts/check-mcp-package.ts' - 'package-lock.json' engine: - 'packages/loopover-engine/**' - 'package-lock.json' miner: - 'packages/loopover-miner/**' - - 'scripts/check-miner-package.mjs' + - 'scripts/check-miner-package.ts' - 'package-lock.json' # No dedicated build/pack script (unlike mcp/engine/miner above) -- discovery-index is a normal # vitest-covered workspace package (packages/discovery-index/src/**/*.ts is in vitest.config.ts's @@ -217,7 +217,7 @@ jobs: - 'control-plane/**' - '.github/workflows/ci.yml' # Both miner-package test files are self-contained w.r.t. root src/**, the same trust boundary as - # mcpCliHarness above: check-miner-package.test.ts only spawns scripts/check-miner-package.mjs as a + # mcpCliHarness above: check-miner-package.test.ts only spawns scripts/check-miner-package.ts as a # real subprocess (node:child_process + vitest, nothing else), and miner-calibration-types.test.ts # only imports the scaffolded packages/loopover-miner/lib/calibration.ts -- neither ever loads # root src/ in-process. Mirrors mcpCliHarness's filter shape for the same reason (tooling/config @@ -927,7 +927,7 @@ jobs: # across every push-to-main run). key never actually matches (no save ever uses this literal # string); it exists only so the step always falls through to the restore-keys prefix match, # picking whatever the most recent refresh wrote. A cache MISS here is always safe: see - # scripts/compute-test-shards.mjs's fallback -- it splits evenly across shards when no timing + # scripts/compute-test-shards.ts's fallback -- it splits evenly across shards when no timing # data is available for a file (or none at all), the same balance vitest's own --shard already # gives today, so this can only make shard balance better than today's baseline, never worse. - name: Restore test timing cache @@ -1036,11 +1036,11 @@ jobs: # shards, every run sampled. Deliberately not applied to the scoped-selection branch above: # that file set isn't known until vitest resolves --changed itself, so a precomputed # assignment can't cover it without duplicating vitest's own dependency-graph resolution here. - # compute-test-shards.mjs enforces its own hard invariant (the union of all 3 shards' files + # compute-test-shards.ts enforces its own hard invariant (the union of all 3 shards' files # must exactly equal the discovered file set, no file missing or duplicated) and refuses to # write output at all if that's ever violated, so a bug here fails this step loudly rather # than silently dropping a test file from CI. - node scripts/compute-test-shards.mjs --shards=3 --timing=test-timing.json --output=shard-assignment.json + node --experimental-strip-types scripts/compute-test-shards.ts --shards=3 --timing=test-timing.json --output=shard-assignment.json mapfile -t SHARD_FILES < <(node -e "console.log(JSON.parse(require('fs').readFileSync('shard-assignment.json','utf8'))['${{ matrix.shard }}'].join('\n'))") npm run test:coverage -- --maxWorkers=4 "${SHARD_FILES[@]}" --reporter=default --reporter=blob --reporter=junit --outputFile.blob=blob-report/report-${{ matrix.shard }}.blob --outputFile.junit=reports/junit/vitest.xml "${EXCLUDE_ARGS[@]}" fi diff --git a/.github/workflows/orb-beta-release.yml b/.github/workflows/orb-beta-release.yml index af6630cfe8..94bceb8cdf 100644 --- a/.github/workflows/orb-beta-release.yml +++ b/.github/workflows/orb-beta-release.yml @@ -1,6 +1,6 @@ # Automated ORB (self-host container image, ghcr.io/jsonbored/loopover-selfhost) beta channel. # Daily (or on demand via workflow_dispatch), checks whether any image-relevant commit has landed -# since the last orb-v tag (scripts/check-orb-release-due.mjs / scripts/orb-release-core.ts) and, +# since the last orb-v tag (scripts/check-orb-release-due.ts / scripts/orb-release-core.ts) and, # if so, cuts the next `orb-vX.Y.Z-beta.N` tag and dispatches release-selfhost.yml to build + publish # it -- fully unattended: that workflow's `environment:` routes an actual beta version to # `release-beta` (no required reviewers), while a stable/rc version still requires the human-gated @@ -40,11 +40,11 @@ jobs: - name: Check whether an ORB beta is due id: report - # check-orb-release-due.mjs imports orb-release-core.ts directly, so it needs tsx (not plain node) to - # resolve that local .ts import. + # check-orb-release-due.ts imports orb-release-core.ts directly via a `.js` specifier, so it needs tsx + # (not plain node) to resolve that local .ts import. run: | set -euo pipefail - npx tsx scripts/check-orb-release-due.mjs --json --output orb-release-due.json + npx tsx scripts/check-orb-release-due.ts --json --output orb-release-due.json node <<'NODE' const fs = require("node:fs"); const report = JSON.parse(fs.readFileSync("orb-release-due.json", "utf8")); diff --git a/.github/workflows/orb-stable-release-pr.yml b/.github/workflows/orb-stable-release-pr.yml index 9ff1041133..2cb21dc53d 100644 --- a/.github/workflows/orb-stable-release-pr.yml +++ b/.github/workflows/orb-stable-release-pr.yml @@ -4,7 +4,7 @@ # doesn't fit release-please's directory-component model the way packages/loopover-mcp and # packages/loopover-engine do (see mcp-release-please.yml). Same UX contract as those, hand-rolled: on the # same schedule (or on demand), (re)compute the next stable version from conventional commits since the last -# STABLE orb-v tag (scripts/check-orb-stable-release-due.mjs / orb-release-core.ts's buildOrbStableReleaseReport) +# STABLE orb-v tag (scripts/check-orb-stable-release-due.ts / orb-release-core.ts's buildOrbStableReleaseReport) # and keep a standing `release-orb-stable` branch + PR in sync with that proposal. Nothing ships until a # maintainer reviews and merges it -- see orb-stable-release-tag.yml for what happens then. Never touches the # daily fully-unattended beta channel (orb-beta-release.yml). @@ -39,11 +39,11 @@ jobs: - name: Check whether a stable ORB release is due id: report - # check-orb-stable-release-due.mjs imports orb-release-core.ts directly, so it needs tsx (not plain - # node) to resolve that local .ts import. + # check-orb-stable-release-due.ts imports orb-release-core.ts directly via a `.js` specifier, so it + # needs tsx (not plain node) to resolve that local .ts import. run: | set -euo pipefail - npx tsx scripts/check-orb-stable-release-due.mjs --json --output orb-stable-release-due.json + npx tsx scripts/check-orb-stable-release-due.ts --json --output orb-stable-release-due.json node <<'NODE' const fs = require("node:fs"); const report = JSON.parse(fs.readFileSync("orb-stable-release-due.json", "utf8")); diff --git a/.github/workflows/publish-miner.yml b/.github/workflows/publish-miner.yml index 5d0b9d625a..83e2f15b5e 100644 --- a/.github/workflows/publish-miner.yml +++ b/.github/workflows/publish-miner.yml @@ -97,7 +97,7 @@ jobs: run: npm run build --workspace @loopover/miner # Reuses the exact allowlist/required-files/forbidden-content check test:ci already runs on - # every PR (scripts/check-miner-package.mjs) -- a dry-run pack, so it doesn't produce the real + # every PR (scripts/check-miner-package.ts) -- a dry-run pack, so it doesn't produce the real # tarball this job packs+uploads below. - name: Validate packed file list run: npm run test:miner-pack diff --git a/.github/workflows/release-selfhost.yml b/.github/workflows/release-selfhost.yml index fcc628304e..114035f61c 100644 --- a/.github/workflows/release-selfhost.yml +++ b/.github/workflows/release-selfhost.yml @@ -135,7 +135,7 @@ jobs: run: npm run build --workspace @loopover/engine - name: Build self-host bundle for release - run: node scripts/build-selfhost.mjs --all + run: node --experimental-strip-types scripts/build-selfhost.ts --all - name: Validate release source map run: node --experimental-strip-types scripts/validate-selfhost-sourcemap.ts diff --git a/.github/workflows/selfhost.yml b/.github/workflows/selfhost.yml index 5b8c344809..53bb7bb9b3 100644 --- a/.github/workflows/selfhost.yml +++ b/.github/workflows/selfhost.yml @@ -1,6 +1,6 @@ # Self-host stack CI (#980/#982). Provides integration coverage the main CI can't: # 1. Postgres integration test — needs a real PG service container -# 2. Self-host bundle build validation (build-selfhost.mjs) +# 2. Self-host bundle build validation (build-selfhost.ts) # 3. Docker image build + container smoke test (/health, /ready, /metrics) # Unit tests and typecheck are NOT duplicated here — the main CI validate job covers them. name: self-host @@ -11,7 +11,7 @@ on: paths: - "src/selfhost/**" - "src/server.ts" - - "scripts/build-selfhost.mjs" + - "scripts/build-selfhost.ts" - "scripts/validate-selfhost-sourcemap.ts" - "Dockerfile" - "docker-compose.yml" @@ -27,7 +27,7 @@ on: paths: - "src/selfhost/**" - "src/server.ts" - - "scripts/build-selfhost.mjs" + - "scripts/build-selfhost.ts" - "scripts/validate-selfhost-sourcemap.ts" - "Dockerfile" - "docker-compose.yml" @@ -97,7 +97,7 @@ jobs: run: PG_TEST_URL=postgres://postgres:devpw@localhost:5432/loopover npx vitest run test/integration/selfhost-pg.test.ts - name: Build the self-host bundle - run: node scripts/build-selfhost.mjs + run: node --experimental-strip-types scripts/build-selfhost.ts - name: Validate self-host source map run: node scripts/validate-selfhost-sourcemap.ts diff --git a/.github/workflows/test-timing-refresh.yml b/.github/workflows/test-timing-refresh.yml index 8d1446b2ad..15fcfcf422 100644 --- a/.github/workflows/test-timing-refresh.yml +++ b/.github/workflows/test-timing-refresh.yml @@ -2,7 +2,7 @@ name: Test timing refresh # Pulls per-test-file historical duration data from Codecov's Test Analytics API (see # scripts/fetch-test-timing.ts) and caches it for validate-tests' duration-aware shard bin-packer -# (scripts/compute-test-shards.mjs, ci.yml's "Test with coverage" step) to consume. Runs on a schedule +# (scripts/compute-test-shards.ts, ci.yml's "Test with coverage" step) to consume. Runs on a schedule # rather than per-PR: Codecov doesn't publish a numeric rate limit for this read endpoint, and this # repo's PR volume (hundreds/day) makes "query fresh on every PR" a real risk of hitting one, for data # that doesn't meaningfully change run-to-run anyway. diff --git a/Dockerfile b/Dockerfile index 68f491cb82..b7db9948bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN npm ci --ignore-scripts RUN npm --workspace @loopover/engine run build # --all: bundle every dependency into one self-contained dist/server.mjs, so the runtime image needs no # node_modules (≈10× smaller). The bundle has zero `cloudflare:*` imports (stubbed at build), so no loader. -RUN node scripts/build-selfhost.mjs --all +RUN node --experimental-strip-types scripts/build-selfhost.ts --all RUN node --experimental-strip-types scripts/validate-selfhost-sourcemap.ts # --- runtime base: slim, non-root ----------------------------------------------------------------------- @@ -69,7 +69,7 @@ ARG INSTALL_VISUAL_REVIEW=false COPY package*.json ./ RUN if [ "$INSTALL_VISUAL_REVIEW" = "true" ]; then npm install puppeteer-core@22.13.1 --ignore-scripts; fi # sharp (#4370): esbuild marks it `external` in the --all bundle (a native per-platform binary can't be -# bundled into dist/server.mjs, see scripts/build-selfhost.mjs), so it must be installed separately here, +# bundled into dist/server.mjs, see scripts/build-selfhost.ts), so it must be installed separately here, # same reason as puppeteer-core above -- but unconditional (not behind an opt-in build-arg): it's a core # dependency of the vision-image-downscale path, not an optional external-sidecar feature. --ignore-scripts # is safe here: sharp's own platform binary ships as an npm `optionalDependencies` entry diff --git a/apps/loopover-ui/content/docs/self-hosting-operations.mdx b/apps/loopover-ui/content/docs/self-hosting-operations.mdx index 98e599b61b..5a653282ad 100644 --- a/apps/loopover-ui/content/docs/self-hosting-operations.mdx +++ b/apps/loopover-ui/content/docs/self-hosting-operations.mdx @@ -1136,7 +1136,7 @@ scripts above pointed at an older target: `deploy-selfhost-prebuilt.sh`. - This repo has no down-migration convention — `scripts/check-migrations.mjs` only enforces a + This repo has no down-migration convention — `scripts/check-migrations.ts` only enforces a contiguous, non-colliding numbering, not a reverse path. If a migration has already run forward against the live database, rolling back the app code is **not safe in general**: older code can break against a newer schema (a dropped/renamed column, a NOT NULL column it never writes, a diff --git a/apps/loopover-ui/content/docs/self-hosting-releases.mdx b/apps/loopover-ui/content/docs/self-hosting-releases.mdx index 071afffe59..2b30e49426 100644 --- a/apps/loopover-ui/content/docs/self-hosting-releases.mdx +++ b/apps/loopover-ui/content/docs/self-hosting-releases.mdx @@ -104,7 +104,7 @@ docker compose up -d loopover`} There is no dedicated rollback command. Roll back by re-running `scripts/deploy-selfhost-image.sh` pinned to the prior image tag or digest (or `scripts/deploy-selfhost-prebuilt.sh` against an older checkout) — the same script you upgrade with, pointed backward. - This repo has no down-migration convention (`scripts/check-migrations.mjs` and `migrations/` only + This repo has no down-migration convention (`scripts/check-migrations.ts` and `migrations/` only ever add forward). If a migration already ran forward before you need to roll back, reverting the app image does not revert the schema — the rolled-back code now runs against a newer schema than it expects. Keep backups and read release notes for migration changes before upgrading a live diff --git a/apps/loopover-ui/src/lib/selfhost-env-reference.ts b/apps/loopover-ui/src/lib/selfhost-env-reference.ts index d5fefd140a..4405ff4ba2 100644 --- a/apps/loopover-ui/src/lib/selfhost-env-reference.ts +++ b/apps/loopover-ui/src/lib/selfhost-env-reference.ts @@ -307,11 +307,11 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [ }, { name: "OBSERVABILITY_SMOKE_POLL_MS", - firstReference: "scripts/smoke-observability-traces.mjs", + firstReference: "scripts/smoke-observability-traces.ts", }, { name: "OBSERVABILITY_SMOKE_TIMEOUT_MS", - firstReference: "scripts/smoke-observability-traces.mjs", + firstReference: "scripts/smoke-observability-traces.ts", }, { name: "OLLAMA_AI_API_KEY", @@ -499,11 +499,11 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [ }, { name: "SELFHOST_BUNDLE_ALL", - firstReference: "scripts/build-selfhost.mjs", + firstReference: "scripts/build-selfhost.ts", }, { name: "SELFHOST_SERVICE", - firstReference: "scripts/smoke-observability-traces.mjs", + firstReference: "scripts/smoke-observability-traces.ts", }, { name: "SELFHOST_SETUP_TOKEN", @@ -617,8 +617,8 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [ "| `MAINTENANCE_ADMISSION_MAX_LIVE_PENDING` | `src/selfhost/maintenance-admission.ts` |", "| `MAINTENANCE_ADMISSION_MAX_PENDING` | `src/selfhost/maintenance-admission.ts` |", "| `MIGRATIONS_DIR` | `src/server.ts` |", - "| `OBSERVABILITY_SMOKE_POLL_MS` | `scripts/smoke-observability-traces.mjs` |", - "| `OBSERVABILITY_SMOKE_TIMEOUT_MS` | `scripts/smoke-observability-traces.mjs` |", + "| `OBSERVABILITY_SMOKE_POLL_MS` | `scripts/smoke-observability-traces.ts` |", + "| `OBSERVABILITY_SMOKE_TIMEOUT_MS` | `scripts/smoke-observability-traces.ts` |", "| `OLLAMA_AI_API_KEY` | `src/selfhost/ai.ts` |", "| `OLLAMA_AI_BASE_URL` | `src/selfhost/ai.ts` |", "| `OLLAMA_AI_MODEL` | `src/selfhost/ai.ts` |", @@ -665,8 +665,8 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [ "| `REVIEW_AUDIT_S3_ENDPOINT` | `src/server.ts` |", "| `REVIEW_AUDIT_S3_REGION` | `src/server.ts` |", "| `REVIEW_AUDIT_S3_SECRET_ACCESS_KEY` | `src/server.ts` |", - "| `SELFHOST_BUNDLE_ALL` | `scripts/build-selfhost.mjs` |", - "| `SELFHOST_SERVICE` | `scripts/smoke-observability-traces.mjs` |", + "| `SELFHOST_BUNDLE_ALL` | `scripts/build-selfhost.ts` |", + "| `SELFHOST_SERVICE` | `scripts/smoke-observability-traces.ts` |", "| `SELFHOST_SETUP_TOKEN` | `src/selfhost/preflight.ts` |", "| `SENTRY_DSN` | `src/selfhost/sentry.ts` |", "| `SENTRY_ENVIRONMENT` | `src/selfhost/otel.ts` |", diff --git a/package.json b/package.json index d784f16654..0381793a73 100644 --- a/package.json +++ b/package.json @@ -34,21 +34,21 @@ "drizzle:generate": "drizzle-kit generate", "build:mcp": "npm --workspace @loopover/mcp run build", "build:miner": "turbo run build --filter=@loopover/engine && npm --workspace @loopover/miner run build", - "test:mcp-pack": "tsx scripts/check-mcp-package.mjs", - "test:miner-pack": "tsx scripts/check-miner-package.mjs", + "test:mcp-pack": "tsx scripts/check-mcp-package.ts", + "test:miner-pack": "tsx scripts/check-miner-package.ts", "test:miner-deployment-docs-audit": "tsx scripts/check-miner-deployment-docs.ts", "rees:install": "npm ci --prefix review-enrichment --prefer-offline --no-audit --no-fund", "rees:test": "npm run rees:install && npm --prefix review-enrichment test", "rees:metadata": "npm --prefix review-enrichment run metadata", "rees:metadata:check": "npm --prefix review-enrichment run metadata:check", "rees:validate-sourcemaps": "npm --prefix review-enrichment run validate:sourcemaps", - "rees:coverage": "node scripts/rees-coverage.mjs", + "rees:coverage": "node --experimental-strip-types scripts/rees-coverage.ts", "control-plane:install": "npm ci --prefix control-plane --prefer-offline --no-audit --no-fund", "control-plane:test": "npm run control-plane:install && npm --prefix control-plane test", "control-plane:coverage": "node scripts/control-plane-coverage.mjs", - "db:migrations:check": "tsx scripts/check-migrations.mjs", + "db:migrations:check": "tsx scripts/check-migrations.ts", "db:schema-drift:check": "tsx scripts/check-schema-drift.ts", - "actionlint": "node scripts/actionlint.mjs", + "actionlint": "node --experimental-strip-types scripts/actionlint.ts", "lint:composite-actions": "node --experimental-strip-types scripts/lint-composite-actions.ts", "ui:dev": "npm run ui:preview", "extension:build": "tsx scripts/build-extension.ts", @@ -84,11 +84,11 @@ "changelog": "npm run changelog:root && npm run changelog:mcp", "changelog:root": "git-cliff --config cliff.toml --output CHANGELOG.md", "changelog:mcp": "tsx scripts/generate-mcp-changelog.ts --output packages/loopover-mcp/CHANGELOG.md", - "changelog:check": "node scripts/check-changelog.mjs", - "changelog:check:root": "node scripts/check-changelog.mjs --root", - "changelog:check:mcp": "node scripts/check-changelog.mjs --mcp", + "changelog:check": "node --experimental-strip-types scripts/check-changelog.ts", + "changelog:check:root": "node --experimental-strip-types scripts/check-changelog.ts --root", + "changelog:check:mcp": "node --experimental-strip-types scripts/check-changelog.ts --mcp", "mcp:release-due": "tsx scripts/check-mcp-release-due.ts --json", - "mcp:release-candidate": "tsx scripts/check-mcp-release-candidate.mjs", + "mcp:release-candidate": "tsx scripts/check-mcp-release-candidate.ts", "typecheck": "tsc --noEmit", "check-node-version": "node --experimental-strip-types scripts/check-node-version.ts", "pretest": "npm run check-node-version", @@ -105,10 +105,10 @@ "pretest:coverage": "npm run check-node-version", "test:coverage": "vitest run --coverage --pool=forks", "test:smoke:production": "tsx scripts/smoke-production.ts", - "test:smoke:observability": "node scripts/smoke-observability-traces.mjs", - "test:smoke:observability:metrics": "node scripts/smoke-observability-metrics.mjs", + "test:smoke:observability": "node --experimental-strip-types scripts/smoke-observability-traces.ts", + "test:smoke:observability:metrics": "node --experimental-strip-types scripts/smoke-observability-metrics.ts", "test:smoke:browser:install": "playwright install chromium", - "test:smoke:browser": "node scripts/smoke-ui-browser.mjs", + "test:smoke:browser": "node --experimental-strip-types scripts/smoke-ui-browser.ts", "pretest:ci": "npm run check-node-version", "test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run test --workspace @loopover/engine && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run test:miner-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build", "test:release": "npm run test:ci && npm run changelog:check", diff --git a/packages/loopover-engine/src/miner/deny-hooks.ts b/packages/loopover-engine/src/miner/deny-hooks.ts index 30a0dc0e7c..69f88d9d52 100644 --- a/packages/loopover-engine/src/miner/deny-hooks.ts +++ b/packages/loopover-engine/src/miner/deny-hooks.ts @@ -13,7 +13,7 @@ // string-shaped input field — for flag-shaped needles like `-f`, where a substring test would also fire on // `--follow-tags`. // A rule with none of these constraints fires on the matcher alone. The built-in DEFAULT_DENY_RULES mirror the -// forbidden-path patterns enforced in `scripts/check-mcp-package.mjs` plus a conservative git force-push guard. +// forbidden-path patterns enforced in `scripts/check-mcp-package.ts` plus a conservative git force-push guard. export type DenyRule = { /** Tool-name glob (`*` = any within a segment, `**` across segments) or an exact tool name. */ @@ -146,7 +146,7 @@ function ruleMatches(rule: DenyRule, toolName: unknown, inputStrings: string[]): /** * The built-in house-rule deny set — a non-empty starting example a later phase can extend or replace. Mirrors the - * forbidden-path regex in `scripts/check-mcp-package.mjs` (CI workflows, env files, secret-bearing paths, private + * forbidden-path regex in `scripts/check-mcp-package.ts` (CI workflows, env files, secret-bearing paths, private * key material) and adds conservative git force-push guards (a command carrying `push` plus a force flag). */ export const DEFAULT_DENY_RULES: DenyRule[] = [ diff --git a/scripts/actionlint.mjs b/scripts/actionlint.ts similarity index 68% rename from scripts/actionlint.mjs rename to scripts/actionlint.ts index 89cf5556b6..9044e4b47a 100644 --- a/scripts/actionlint.mjs +++ b/scripts/actionlint.ts @@ -3,9 +3,15 @@ import { createRequire } from "node:module"; import { join } from "node:path"; import { setTimeout as delay } from "node:timers/promises"; import { resolveActionlintDownloadAttempts } from "./lib/actionlint-download-attempts.mjs"; +import type { ActionlintOptions, ActionlintResult } from "github-actionlint"; +import type { Result } from "@tktco/node-actionlint/build/types.js"; +// require(), not import: github-actionlint ships no "type": "module"/exports map (plain CJS), and the WASM +// fallback below is deliberately require()'d lazily inside its own function rather than imported at module +// scope, so that heavier dependency is only ever loaded on the (uncommon) path where the primary binary +// setup fails. const require = createRequire(import.meta.url); -const { actionlint } = require("github-actionlint"); +const { actionlint }: typeof import("github-actionlint") = require("github-actionlint"); const workflowDir = ".github/workflows"; const files = readdirSync(workflowDir) @@ -21,15 +27,15 @@ const maxAttempts = resolveActionlintDownloadAttempts(process.env.ACTIONLINT_DOW const retryDelaysMs = [1000, 3000, 7000]; const retryableSetupError = /Download failed: (?:408|425|429|5\d\d)\b|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|socket hang up/i; -function errorMessage(error) { +function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } -async function runWasmActionlint(reason) { +async function runWasmActionlint(reason: string): Promise<{ code: number }> { console.warn(`official actionlint setup unavailable, using WASM fallback: ${reason}`); - const { getLintLog, runLint } = require("@tktco/node-actionlint"); + const { getLintLog, runLint }: typeof import("@tktco/node-actionlint") = require("@tktco/node-actionlint"); const fileData = files.map((file) => ({ path: file, data: readFileSync(file, "utf8") })); - const results = ( + const results: Result[] = ( await Promise.all( fileData.map(async (file) => { const lintResults = await runLint(file.data, file.path); @@ -47,19 +53,20 @@ async function runWasmActionlint(reason) { return { code: 0 }; } -async function runActionlint() { +async function runActionlint(): Promise { if (process.env.ACTIONLINT_FORCE_WASM_FALLBACK === "1") { return runWasmActionlint("forced by ACTIONLINT_FORCE_WASM_FALLBACK"); } for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { try { - return await actionlint({ args: files, spawnOptions: { stdio: "inherit" } }); + const options: ActionlintOptions = { args: files, spawnOptions: { stdio: "inherit" } }; + return await actionlint(options); } catch (error) { const message = errorMessage(error); if (!retryableSetupError.test(message)) throw error; if (attempt >= maxAttempts) return runWasmActionlint(message); - const delayMs = retryDelaysMs[Math.min(attempt - 1, retryDelaysMs.length - 1)]; + const delayMs = retryDelaysMs[Math.min(attempt - 1, retryDelaysMs.length - 1)]!; console.warn(`actionlint setup failed, retrying (${attempt}/${maxAttempts}): ${message}`); await delay(delayMs); } diff --git a/scripts/build-selfhost.mjs b/scripts/build-selfhost.ts similarity index 99% rename from scripts/build-selfhost.mjs rename to scripts/build-selfhost.ts index 77a536ac06..119731fb1c 100644 --- a/scripts/build-selfhost.mjs +++ b/scripts/build-selfhost.ts @@ -26,7 +26,7 @@ await esbuild.build({ // sharp (#4370) is ALWAYS external even in --all mode: it ships a native per-platform binary esbuild // cannot bundle, unlike sharp's own JS glue code -- the Dockerfile installs it separately into the // runtime image (see the runtime-base stage). - ...(bundleAll ? { external: ["sharp"] } : { packages: "external" }), + ...(bundleAll ? { external: ["sharp"] } : { packages: "external" as const }), // Bundling CJS deps into an ESM output needs require/__dirname/__filename shimmed (some deps call them). ...(bundleAll ? { diff --git a/scripts/check-changelog.mjs b/scripts/check-changelog.ts similarity index 73% rename from scripts/check-changelog.mjs rename to scripts/check-changelog.ts index 8725e96c45..c12a3f3a40 100644 --- a/scripts/check-changelog.mjs +++ b/scripts/check-changelog.ts @@ -5,6 +5,14 @@ import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { spawnSync } from "node:child_process"; +type ChangelogCheck = { + label: string; + output: string; + command: string; + selector: string; + runner: () => string; +}; + function main() { const requestedChecks = new Set(process.argv.slice(2)); const validArgs = new Set(["--root", "--mcp"]); @@ -18,7 +26,7 @@ function main() { const tempDir = mkdtempSync(join(tmpdir(), "loopover-changelog-")); try { - const checks = [ + const checks: ChangelogCheck[] = [ { label: "root changelog", output: "CHANGELOG.md", @@ -39,13 +47,17 @@ function main() { const generatedPath = join(tempDir, "MCP_CHANGELOG.md"); const version = JSON.parse(readFileSync("packages/loopover-mcp/package.json", "utf8")).version; writeFileSync(generatedPath, readFileSync("packages/loopover-mcp/CHANGELOG.md", "utf8")); - run(["node", "scripts/generate-mcp-changelog.mjs", "--output", generatedPath, "--version", version], "MCP package changelog"); + // generate-mcp-changelog.ts imports mcp-release-core.ts directly, so it needs tsx (not plain node) to + // resolve that local .ts import -- same reason test/unit/check-schema-drift-script.test.ts spawns tsx + // directly via node_modules/.bin rather than through an npm script. + const tsxBin = join(process.cwd(), "node_modules", ".bin", "tsx"); + run([tsxBin, "scripts/generate-mcp-changelog.ts", "--output", generatedPath, "--version", version], "MCP package changelog"); return generatedPath; }, }, ].filter((check) => requestedChecks.size === 0 || requestedChecks.has(check.selector)); - const failures = []; + const failures: string[] = []; for (const check of checks) { const generatedPath = check.runner(); const expected = readFileSync(generatedPath, "utf8"); @@ -68,20 +80,24 @@ function main() { * injectable purely for testability; every real caller uses the defaults. When the command cannot even * launch (`status` is null, e.g. the binary is not on PATH), `result.error` holds the actual ENOENT/EACCES * reason -- surface its message (#7772) instead of the generic `${label} failed`, which wastes debugging time. */ -export function run(command, label, { spawn = spawnSync, onFailure = defaultOnFailure } = {}) { - const result = spawn(command[0], command.slice(1), { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); +export function run( + command: readonly string[], + label: string, + { spawn = spawnSync, onFailure = defaultOnFailure }: { spawn?: typeof spawnSync; onFailure?: (message: string, code: number) => void } = {}, +): void { + const result = spawn(command[0]!, command.slice(1), { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); if (result.status !== 0) { const message = result.stderr || result.stdout || (result.error ? `${label}: ${result.error.message}\n` : `${label} failed`); onFailure(message, result.status ?? 1); } } -function defaultOnFailure(message, code) { +function defaultOnFailure(message: string, code: number): never { process.stderr.write(message); process.exit(code); } -function normalize(value) { +function normalize(value: string): string { return value.replace(/\r\n/g, "\n").trimEnd(); } diff --git a/scripts/check-mcp-package.mjs b/scripts/check-mcp-package.ts similarity index 76% rename from scripts/check-mcp-package.mjs rename to scripts/check-mcp-package.ts index e0abe2ece8..08f380e457 100644 --- a/scripts/check-mcp-package.mjs +++ b/scripts/check-mcp-package.ts @@ -9,24 +9,25 @@ import { MCP_PACKAGE_ALLOWED_FILE_PATTERNS } from "./mcp-package-allowlist.js"; const FORBIDDEN_PATH = /(^|\/)(\.dev\.vars|\.env|\.npmrc|.*\.pem|.*private.*key.*|.*secret.*)$/i; const STALE_PACKAGE_TEXT = /(private beta|zeronode\.workers\.dev|preview URL)/i; -export function validateMcpPackFileList(files, readContent) { +type PackedFile = string | { path: string }; +type ReadContentFn = (file: string) => string; + +export function validateMcpPackFileList(files: readonly PackedFile[], readContent: ReadContentFn): string[] { const paths = files.map((file) => (typeof file === "string" ? file : file.path)).sort(); for (const file of paths) { if (FORBIDDEN_PATH.test(file)) throw new Error(`Forbidden file in MCP package: ${file}`); - if (!MCP_PACKAGE_ALLOWED_FILE_PATTERNS.some((pattern) => pattern.test(file))) - throw new Error(`Unexpected file in MCP package: ${file}`); + if (!MCP_PACKAGE_ALLOWED_FILE_PATTERNS.some((pattern) => pattern.test(file))) throw new Error(`Unexpected file in MCP package: ${file}`); const content = readContent(file); if (FORBIDDEN_CONTENT.test(content)) throw new Error(`Secret-like content found in MCP package file: ${file}`); - if (file === "README.md" && STALE_PACKAGE_TEXT.test(content)) - throw new Error(`Stale public-package wording found in MCP package file: ${file}`); + if (file === "README.md" && STALE_PACKAGE_TEXT.test(content)) throw new Error(`Stale public-package wording found in MCP package file: ${file}`); } return paths; } -export function runMcpPackCheck(options = {}) { +export function runMcpPackCheck(options: { pack?: { files: PackedFile[] }; packageRoot?: string; readContent?: ReadContentFn } = {}): string { const pack = options.pack ?? loadMcpPackFromNpm(); const packageRoot = options.packageRoot ?? join(process.cwd(), "packages/loopover-mcp"); - const readContent = + const readContent: ReadContentFn = options.readContent ?? ((file) => { if (process.env.CHECK_MCP_PACK_TEST_CONTENT !== undefined) return process.env.CHECK_MCP_PACK_TEST_CONTENT; @@ -36,9 +37,9 @@ export function runMcpPackCheck(options = {}) { return `MCP package dry-run ok: ${paths.join(", ")}\n`; } -function loadMcpPackFromNpm() { +function loadMcpPackFromNpm(): { files: PackedFile[] } { if (process.env.CHECK_MCP_PACK_TEST_FILES) { - const paths = JSON.parse(process.env.CHECK_MCP_PACK_TEST_FILES); + const paths: string[] = JSON.parse(process.env.CHECK_MCP_PACK_TEST_FILES); return { files: paths.map((path) => ({ path })) }; } const result = spawnSync("npm", ["pack", "--workspace", "@loopover/mcp", "--dry-run", "--json"], { diff --git a/scripts/check-mcp-release-candidate.mjs b/scripts/check-mcp-release-candidate.ts similarity index 83% rename from scripts/check-mcp-release-candidate.mjs rename to scripts/check-mcp-release-candidate.ts index a5c1796225..b752781dcf 100644 --- a/scripts/check-mcp-release-candidate.mjs +++ b/scripts/check-mcp-release-candidate.ts @@ -2,40 +2,33 @@ import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { spawnSync } from "node:child_process"; -import { - buildReleaseCandidateReport, - checkTag, - checkTarball, - checkTokenlessPublish, - expectedReleaseTag, - redactSensitive, -} from "./mcp-release-candidate-core.js"; +import { spawnSync, type SpawnSyncOptions } from "node:child_process"; +import { buildReleaseCandidateReport, checkTag, checkTarball, checkTokenlessPublish, expectedReleaseTag, redactSensitive, type CheckResult } from "./mcp-release-candidate-core.js"; const PACKAGE_DIR = "packages/loopover-mcp"; const WORKSPACE = "@loopover/mcp"; const PUBLISH_WORKFLOW = ".github/workflows/publish-mcp.yml"; const onWindows = process.platform === "win32"; -function arg(name) { +function arg(name: string): string | null { const flag = `--${name}`; const index = process.argv.indexOf(flag); - if (index !== -1 && index + 1 < process.argv.length) return process.argv[index + 1]; + if (index !== -1 && index + 1 < process.argv.length) return process.argv[index + 1]!; return null; } const wantsJson = process.argv.includes("--json"); -function run(command, args, options = {}) { +function run(command: string, args: string[], options: SpawnSyncOptions = {}) { // shell:true on Windows so `npm`/`npx` (.cmd shims) resolve; output is captured, never streamed raw. return spawnSync(command, args, { encoding: "utf8", shell: onWindows, ...options }); } -function readMaybe(path) { +function readMaybe(path: string): string | null { return existsSync(path) ? readFileSync(path, "utf8") : null; } -function packageVersion() { +function packageVersion(): string | null { try { return JSON.parse(readFileSync(join(PACKAGE_DIR, "package.json"), "utf8")).version ?? null; } catch { @@ -43,13 +36,13 @@ function packageVersion() { } } -function tarballFileCheck() { +function tarballFileCheck(): { check: CheckResult } { const result = run("npm", ["pack", "--workspace", WORKSPACE, "--dry-run", "--json"]); if (result.status !== 0 || !result.stdout) { return { check: { ok: false, code: "tarball_unsafe", message: "Could not compute the package file list via npm pack --dry-run." } }; } - const files = JSON.parse(result.stdout)[0].files.map((file) => file.path); - const contentsByFile = {}; + const files: string[] = JSON.parse(result.stdout)[0].files.map((file: { path: string }) => file.path); + const contentsByFile: Record = {}; for (const file of files) { const full = join(PACKAGE_DIR, file); if (existsSync(full)) contentsByFile[file] = readFileSync(full, "utf8"); @@ -57,7 +50,7 @@ function tarballFileCheck() { return { check: checkTarball({ files, contentsByFile }) }; } -function packedCliSmoke() { +function packedCliSmoke(): CheckResult { const build = run("npm", ["run", "build:mcp"]); if (build.status !== 0) { return { ok: false, code: "cli_smoke_failed", message: "npm run build:mcp failed before the packed CLI smoke." }; @@ -68,7 +61,7 @@ function packedCliSmoke() { } const filename = JSON.parse(pack.stdout)[0].filename; const tarball = join(process.cwd(), filename); - let temp = null; + let temp: string | null = null; try { temp = mkdtempSync(join(tmpdir(), "mcp-rc-")); if (run("npm", ["--prefix", temp, "init", "-y"]).status !== 0) { @@ -90,7 +83,7 @@ function packedCliSmoke() { } } -function emit(line) { +function emit(line: string): void { process.stdout.write(`${redactSensitive(line)}\n`); } diff --git a/scripts/check-migrations.mjs b/scripts/check-migrations.ts similarity index 92% rename from scripts/check-migrations.mjs rename to scripts/check-migrations.ts index 2613d5b437..daf29d84db 100644 --- a/scripts/check-migrations.mjs +++ b/scripts/check-migrations.ts @@ -30,14 +30,14 @@ // merged independently from the same base and were both applied to production before the collision // surfaced; bare ADD COLUMN statements, same grandfather reasoning as 0074/0090. import { readdirSync, readFileSync } from "node:fs"; -import { detectMigrationCollisions, extractMigrationNumber, KNOWN_MIGRATION_DUPLICATES, MIGRATION_FILENAME_PATTERN } from "../src/db/migration-collisions.ts"; -import { detectColumnCollisions } from "../src/db/migration-column-extraction.ts"; +import { detectMigrationCollisions, extractMigrationNumber, KNOWN_MIGRATION_DUPLICATES, MIGRATION_FILENAME_PATTERN } from "../src/db/migration-collisions.js"; +import { detectColumnCollisions } from "../src/db/migration-column-extraction.js"; const DIR = process.env.CHECK_MIGRATIONS_DIR || "migrations"; const NAME = MIGRATION_FILENAME_PATTERN; const KNOWN_DUPLICATES = KNOWN_MIGRATION_DUPLICATES; -const fail = (message) => { +const fail = (message: string): never => { process.stderr.write(`check-migrations: ${message}\n`); process.exit(1); }; @@ -51,7 +51,7 @@ const fail = (message) => { // `BEGIN`/`END` and mid-statement words don't trip. // The anchored patterns use a variable-length lookbehind (`(?<=(?:^|;)\s*)`, supported by V8/Node) so the // match starts on the keyword itself — reported line numbers point at the statement, not the preceding `;`. -const D1_FORBIDDEN = [ +const D1_FORBIDDEN: Array<[RegExp, string]> = [ [/create\s+(?:temp(?:orary)?\b|(?:unique\s+)?(?:table|index|view|trigger)\s+(?:if\s+not\s+exists\s+)?temp\s*\.)/gi, "temporary object (CREATE TEMP/TEMPORARY or temp schema) — D1 rejects temp tables/triggers/views/indexes; rewrite without one (e.g. DELETE the losers, then UPDATE the survivors)"], [/(?<=(?:^|;)\s*)attach\b/gi, "ATTACH is not supported on D1"], [/(?<=(?:^|;)\s*)detach\b/gi, "DETACH is not supported on D1"], @@ -73,7 +73,7 @@ const D1_FORBIDDEN = [ // by a `.`; an ordinary name or value never is. Peeking past the closing quote for one, uniformly across // all four quoting styles, distinguishes "used as a schema qualifier" from "used as a name or value" // without a real SQL parser. -function cleanSql(sql) { +function cleanSql(sql: string): string { let out = ""; for (let i = 0; i < sql.length; ) { const c = sql[i]; @@ -117,7 +117,7 @@ function cleanSql(sql) { j += 1; } let k = j + 1; - while (k < sql.length && /\s/.test(sql[k])) k += 1; + while (k < sql.length && /\s/.test(sql[k]!)) k += 1; const usedAsSchemaQualifier = k < sql.length && sql[k] === "."; out += " "; for (const ch of content) out += usedAsSchemaQualifier ? ch : ch === "\n" ? "\n" : " "; @@ -141,14 +141,16 @@ if (malformed.length > 0) { fail(`migration filenames must be NNNN_snake_case.sql (4-digit zero-padded number): ${malformed.join(", ")}`); } -const filesByNumber = new Map(); +// Every file is guaranteed NAME-conforming past the malformed check above (fail() exits the process), so +// extractMigrationNumber's null case -- reserved for a non-conforming filename -- can't occur here. +const filesByNumber = new Map(); for (const file of files) { - const number = extractMigrationNumber(file); + const number = extractMigrationNumber(file)!; if (!filesByNumber.has(number)) filesByNumber.set(number, []); - filesByNumber.get(number).push(file); + filesByNumber.get(number)!.push(file); } -const nextFree = () => { +const nextFree = (): string => { let n = Math.max(...filesByNumber.keys()) + 1; while (filesByNumber.has(n)) n += 1; return String(n).padStart(4, "0"); @@ -159,20 +161,20 @@ const nextFree = () => { // is the identical import, so CI and the Worker can never silently disagree about what counts as a collision. const collisions = detectMigrationCollisions(files, KNOWN_DUPLICATES); if (collisions.length > 0) { - const { paddedNumber, files: group } = collisions[0]; + const { paddedNumber, files: group } = collisions[0]!; fail(`duplicate migration number ${paddedNumber}: ${group.map((f) => `"${f}"`).join(", ")}. Two PRs grabbed the same number — renumber the newest to the next free number (${nextFree()}).`); } const numbers = [...filesByNumber.keys()].sort((a, b) => a - b); for (let i = 1; i < numbers.length; i += 1) { - if (numbers[i] !== numbers[i - 1] + 1) { + if (numbers[i] !== numbers[i - 1]! + 1) { const prev = String(numbers[i - 1]).padStart(4, "0"); const curr = String(numbers[i]).padStart(4, "0"); fail(`migration number gap: ${prev} -> ${curr}. Migrations must be a contiguous sequence (no skipped numbers).`); } } -const sqlViolations = []; +const sqlViolations: string[] = []; for (const file of files) { const cleaned = cleanSql(readFileSync(`${DIR}/${file}`, "utf8")); for (const [re, why] of D1_FORBIDDEN) { @@ -198,9 +200,9 @@ if (sqlViolations.length > 0) { // which detectColumnCollisions requires so a documented DROP TABLE + CREATE TABLE recreate (e.g. // migrations/0060_orb_fleet_collector.sql's orb_signals) correctly clears the table it replaces instead of // reading as a collision with it. -const columnCollisions = detectColumnCollisions(files.map((file) => [file, readFileSync(`${DIR}/${file}`, "utf8")])); +const columnCollisions = detectColumnCollisions(files.map((file): [string, string] => [file, readFileSync(`${DIR}/${file}`, "utf8")])); if (columnCollisions.length > 0) { - const { table, column, files: group } = columnCollisions[0]; + const { table, column, files: group } = columnCollisions[0]!; fail( `duplicate column ${table}.${column} defined by more than one migration: ${group.map((f) => `"${f}"`).join(", ")}. Two migrations independently added the same column under different numbers — this passes CI and shows a clean merge state, but fails at "wrangler d1 migrations apply" deploy time. Rename or remove the newer migration's column (or confirm the table is DROPped and recreated before it).`, ); diff --git a/scripts/check-miner-package.mjs b/scripts/check-miner-package.ts similarity index 83% rename from scripts/check-miner-package.mjs rename to scripts/check-miner-package.ts index 28a4684047..db41830402 100644 --- a/scripts/check-miner-package.mjs +++ b/scripts/check-miner-package.ts @@ -28,19 +28,21 @@ const REQUIRED = [ ]; const FORBIDDEN_PATH = /(^|\/)(\.dev\.vars|\.env|\.npmrc|.*\.pem|.*private.*key.*|.*secret.*)$/i; // Stale public-package wording the published README must never ship with (#7013). The sibling -// check-mcp-package.mjs has always guarded its README against this; the miner-package check did not, so a +// check-mcp-package.ts has always guarded its README against this; the miner-package check did not, so a // pre-release "private beta"/"preview URL" phrasing could ship in the public `@loopover/miner` README unnoticed. const STALE_PACKAGE_TEXT = /(private beta|zeronode\.workers\.dev|preview URL)/i; -export function validateMinerPackFileList(files, readContent) { +type PackedFile = string | { path: string }; +type ReadContentFn = (file: string) => string; + +export function validateMinerPackFileList(files: readonly PackedFile[], readContent: ReadContentFn): string[] { const paths = files.map((file) => (typeof file === "string" ? file : file.path)).sort(); for (const file of paths) { if (FORBIDDEN_PATH.test(file)) throw new Error(`Forbidden file in miner package: ${file}`); if (!ALLOWED.some((pattern) => pattern.test(file))) throw new Error(`Unexpected file in miner package: ${file}`); const content = readContent(file); if (FORBIDDEN_CONTENT.test(content)) throw new Error(`Secret-like content found in miner package file: ${file}`); - if (file === "README.md" && STALE_PACKAGE_TEXT.test(content)) - throw new Error(`Stale public-package wording found in miner package file: ${file}`); + if (file === "README.md" && STALE_PACKAGE_TEXT.test(content)) throw new Error(`Stale public-package wording found in miner package file: ${file}`); } for (const required of REQUIRED) { if (!paths.includes(required)) throw new Error(`Miner package is missing required file: ${required}`); @@ -54,10 +56,10 @@ export function validateMinerPackFileList(files, readContent) { return paths; } -export function runMinerPackCheck(options = {}) { +export function runMinerPackCheck(options: { pack?: { files: PackedFile[] }; packageRoot?: string; readContent?: ReadContentFn } = {}): string { const pack = options.pack ?? loadMinerPackFromNpm(); const packageRoot = options.packageRoot ?? join(process.cwd(), "packages/loopover-miner"); - const readContent = + const readContent: ReadContentFn = options.readContent ?? ((file) => { if (process.env.CHECK_MINER_PACK_TEST_CONTENT !== undefined) return process.env.CHECK_MINER_PACK_TEST_CONTENT; @@ -67,9 +69,9 @@ export function runMinerPackCheck(options = {}) { return `Miner package dry-run ok: ${paths.join(", ")}\n`; } -function loadMinerPackFromNpm() { +function loadMinerPackFromNpm(): { files: PackedFile[] } { if (process.env.CHECK_MINER_PACK_TEST_FILES) { - const paths = JSON.parse(process.env.CHECK_MINER_PACK_TEST_FILES); + const paths: string[] = JSON.parse(process.env.CHECK_MINER_PACK_TEST_FILES); return { files: paths.map((path) => ({ path })) }; } const result = spawnSync("npm", ["pack", "--workspace", "@loopover/miner", "--dry-run", "--json"], { diff --git a/scripts/check-orb-release-due.mjs b/scripts/check-orb-release-due.ts similarity index 76% rename from scripts/check-orb-release-due.mjs rename to scripts/check-orb-release-due.ts index f84f3c785a..9ada2229e7 100644 --- a/scripts/check-orb-release-due.mjs +++ b/scripts/check-orb-release-due.ts @@ -4,14 +4,16 @@ // hidden inside this script. See scripts/orb-release-core.ts for the underlying logic and rationale. import { execFileSync } from "node:child_process"; import { readFileSync, writeFileSync } from "node:fs"; -import { buildOrbReleaseReport, latestOrbTag, latestStableOrbTag } from "./orb-release-core.js"; +import { buildOrbReleaseReport, latestOrbTag, latestStableOrbTag, type OrbReleaseCommit } from "./orb-release-core.js"; const MANIFEST_PATH = "orb-manifest.json"; function main() { const args = parseArgs(process.argv.slice(2)); const manifestVersion = JSON.parse(readFileSync(MANIFEST_PATH, "utf8")).version; - const tags = git(["tag", "--list", "orb-v*"]).split("\n").filter(Boolean); + const tags = git(["tag", "--list", "orb-v*"]) + .split("\n") + .filter(Boolean); const stableTagName = latestStableOrbTag(tags)?.tag ?? null; const anyTagName = latestOrbTag(tags)?.tag ?? null; @@ -32,14 +34,16 @@ function main() { } } -function parseArgs(argv) { - const args = { json: false, output: null }; +type ParsedArgs = { json: boolean; output: string | null }; + +function parseArgs(argv: readonly string[]): ParsedArgs { + const args: ParsedArgs = { json: false, output: null }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--json") { args.json = true; } else if (arg === "--output") { - args.output = argv[++index]; + args.output = argv[++index]!; } else { throw new Error(`Unknown option: ${arg}`); } @@ -47,7 +51,7 @@ function parseArgs(argv) { return args; } -function readCommits(revisionRange) { +function readCommits(revisionRange: string): OrbReleaseCommit[] { const format = "%x1e%H%x1f%s%x1f%B"; const logOutput = git(["log", "--reverse", "--no-merges", `--format=${format}`, revisionRange]); return logOutput @@ -56,15 +60,17 @@ function readCommits(revisionRange) { .filter(Boolean) .map((entry) => { const [sha, subject, ...bodyParts] = entry.split("\x1f"); - return { sha, subject: subject?.split("\n")[0] ?? "", body: bodyParts.join("\x1f"), files: readCommitFiles(sha) }; + return { sha: sha!, subject: subject?.split("\n")[0] ?? "", body: bodyParts.join("\x1f"), files: readCommitFiles(sha!) }; }); } -function readCommitFiles(sha) { - return git(["diff-tree", "--no-commit-id", "--name-only", "-r", sha]).split("\n").filter(Boolean); +function readCommitFiles(sha: string): string[] { + return git(["diff-tree", "--no-commit-id", "--name-only", "-r", sha]) + .split("\n") + .filter(Boolean); } -function git(args) { +function git(args: readonly string[]): string { return execFileSync("git", args, { encoding: "utf8", maxBuffer: 1024 * 1024 * 200 }); } diff --git a/scripts/check-orb-stable-release-due.mjs b/scripts/check-orb-stable-release-due.ts similarity index 68% rename from scripts/check-orb-stable-release-due.mjs rename to scripts/check-orb-stable-release-due.ts index be16c53129..d9f55728cf 100644 --- a/scripts/check-orb-stable-release-due.mjs +++ b/scripts/check-orb-stable-release-due.ts @@ -1,15 +1,17 @@ // Computes whether a new STABLE (non-beta) ORB release is due, and what its proposed version would be. -// Read-only / side-effect-free by design, mirroring check-orb-release-due.mjs's own reasoning: the actual +// Read-only / side-effect-free by design, mirroring check-orb-release-due.ts's own reasoning: the actual // orb-manifest.json bump + `git tag` + PR create/update (the consequential actions) happen as explicit, // auditable steps in .github/workflows/orb-stable-release-pr.yml, not hidden inside this script. See // scripts/orb-release-core.ts's buildOrbStableReleaseReport for the underlying logic and rationale. import { execFileSync } from "node:child_process"; import { writeFileSync } from "node:fs"; -import { buildOrbStableReleaseReport } from "./orb-release-core.js"; +import { buildOrbStableReleaseReport, type OrbReleaseCommit } from "./orb-release-core.js"; function main() { const args = parseArgs(process.argv.slice(2)); - const tags = git(["tag", "--list", "orb-v*"]).split("\n").filter(Boolean); + const tags = git(["tag", "--list", "orb-v*"]) + .split("\n") + .filter(Boolean); const stableTagName = latestStableTagName(tags); const report = buildOrbStableReleaseReport({ @@ -24,25 +26,27 @@ function main() { } } -// Same tag-selection concern as check-orb-release-due.mjs's latestStableTagName -- kept here (not exported +// Same tag-selection concern as check-orb-release-due.ts's latestStableTagName -- kept here (not exported // from the core module) since it's a git-log concern, not a pure-logic one. -function latestStableTagName(tags) { +function latestStableTagName(tags: readonly string[]): string | null { const stable = tags.filter((tag) => /^orb-v\d+\.\d+\.\d+$/.test(tag)); return stable.sort(compareTagsDesc)[0] ?? null; } -function compareTagsDesc(left, right) { +function compareTagsDesc(left: string, right: string): number { return right.localeCompare(left, undefined, { numeric: true }); } -function parseArgs(argv) { - const args = { json: false, output: null }; +type ParsedArgs = { json: boolean; output: string | null }; + +function parseArgs(argv: readonly string[]): ParsedArgs { + const args: ParsedArgs = { json: false, output: null }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--json") { args.json = true; } else if (arg === "--output") { - args.output = argv[++index]; + args.output = argv[++index]!; } else { throw new Error(`Unknown option: ${arg}`); } @@ -50,7 +54,7 @@ function parseArgs(argv) { return args; } -function readCommits(revisionRange) { +function readCommits(revisionRange: string): OrbReleaseCommit[] { const format = "%x1e%H%x1f%s%x1f%B"; const logOutput = git(["log", "--reverse", "--no-merges", `--format=${format}`, revisionRange]); return logOutput @@ -59,15 +63,17 @@ function readCommits(revisionRange) { .filter(Boolean) .map((entry) => { const [sha, subject, ...bodyParts] = entry.split("\x1f"); - return { sha, subject: subject?.split("\n")[0] ?? "", body: bodyParts.join("\x1f"), files: readCommitFiles(sha) }; + return { sha: sha!, subject: subject?.split("\n")[0] ?? "", body: bodyParts.join("\x1f"), files: readCommitFiles(sha!) }; }); } -function readCommitFiles(sha) { - return git(["diff-tree", "--no-commit-id", "--name-only", "-r", sha]).split("\n").filter(Boolean); +function readCommitFiles(sha: string): string[] { + return git(["diff-tree", "--no-commit-id", "--name-only", "-r", sha]) + .split("\n") + .filter(Boolean); } -function git(args) { +function git(args: readonly string[]): string { return execFileSync("git", args, { encoding: "utf8", maxBuffer: 1024 * 1024 * 200 }); } diff --git a/scripts/check-schema-drift.ts b/scripts/check-schema-drift.ts index c83685eaa5..d18d34452b 100644 --- a/scripts/check-schema-drift.ts +++ b/scripts/check-schema-drift.ts @@ -1,6 +1,6 @@ #!/usr/bin/env tsx // #2565: src/db/schema.ts (Drizzle ORM sqliteTable declarations) is a single shared file where two -// independently-valid PRs can each add a new column to the SAME table. scripts/check-migrations.mjs's +// independently-valid PRs can each add a new column to the SAME table. scripts/check-migrations.ts's // detectColumnCollisions (#2551) only catches a git-merge-race collision between two DIFFERENT migration // files that both add the same (table, column) pair -- it never reads src/db/schema.ts at all, so it cannot // see the DIFFERENT gap this check closes: schema.ts's DECLARED shape (what Drizzle thinks a table's columns @@ -15,7 +15,7 @@ // drizzle-orm's getTableColumns (keyed by each column's .name -- the actual DB column name, not the JS // property name). Diff the two column-name sets per table. // -// Run via `tsx` (not plain `node`) for the same reason as check-migrations.mjs and +// Run via `tsx` (not plain `node`) for the same reason as check-migrations.ts and // check-openapi-settings-parity.ts: this script imports src/db/schema.ts (a .ts module) directly, and a bare // `node` invocation can't resolve a `.ts` import without an experimental flag CI's pinned Node isn't // guaranteed to support. diff --git a/scripts/compute-test-shards.mjs b/scripts/compute-test-shards.ts similarity index 86% rename from scripts/compute-test-shards.mjs rename to scripts/compute-test-shards.ts index aec4616bab..18c6b34874 100644 --- a/scripts/compute-test-shards.mjs +++ b/scripts/compute-test-shards.ts @@ -19,7 +19,7 @@ // rather than risk it: a hard failure here (a broken CI step everyone sees) is a wildly better outcome // than a silent one (missing coverage nobody notices until something ships broken). -import { readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs"; +import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; const TEST_ROOT = "test"; @@ -30,8 +30,8 @@ const timingArg = process.argv.find((a) => a.startsWith("--timing="))?.split("=" const outputArg = process.argv.find((a) => a.startsWith("--output="))?.split("=")[1]; if (!outputArg) throw new Error("--output= is required"); -function discoverTestFiles(dir) { - const results = []; +function discoverTestFiles(dir: string): string[] { + const results: string[] = []; for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = join(dir, entry.name); if (entry.isDirectory()) { @@ -44,20 +44,22 @@ function discoverTestFiles(dir) { return results; } -function loadTimingData(path) { +function loadTimingData(path: string | undefined): Record { if (!path || !existsSync(path)) return {}; const parsed = JSON.parse(readFileSync(path, "utf8")); return parsed.averageSecondsByFile ?? {}; } -function median(values) { +function median(values: readonly number[]): number { if (values.length === 0) return 0; const sorted = [...values].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); - return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]; + return sorted.length % 2 === 0 ? (sorted[mid - 1]! + sorted[mid]!) / 2 : sorted[mid]!; } -function packShards(files, durationByFile, shardCount) { +type Shard = { index: number; files: string[]; total: number }; + +function packShards(files: readonly string[], durationByFile: Record, shardCount: number): Shard[] { // New/untracked files (no historical row -- a file added since the last timing refresh, or the // refresh workflow hasn't run yet at all) get the median of known files' durations rather than 0: // treating an unknown file as free would let a burst of newly-added heavy test files land in the @@ -68,11 +70,9 @@ function packShards(files, durationByFile, shardCount) { const knownDurations = Object.values(durationByFile); const fallback = median(knownDurations); - const weighted = files - .map((file) => ({ file, duration: durationByFile[file] ?? fallback })) - .sort((a, b) => b.duration - a.duration); + const weighted = files.map((file) => ({ file, duration: durationByFile[file] ?? fallback })).sort((a, b) => b.duration - a.duration); - const shards = Array.from({ length: shardCount }, (_unused, index) => ({ index, files: [], total: 0 })); + const shards: Shard[] = Array.from({ length: shardCount }, (_unused, index) => ({ index, files: [], total: 0 })); // Picking the lightest shard via a plain reduce always resolves ties in favor of the FIRST shard // (shard.total < min.total is never true between two equal totals, so the running minimum never // moves off its starting candidate) -- harmless when durations vary, but catastrophic whenever many @@ -85,9 +85,9 @@ function packShards(files, durationByFile, shardCount) { // across all shards instead, regardless of whether the tied weight happens to be zero or not. let tiebreakStart = 0; for (const { file, duration } of weighted) { - let lightest = shards[tiebreakStart]; + let lightest = shards[tiebreakStart]!; for (let offset = 1; offset < shardCount; offset += 1) { - const candidate = shards[(tiebreakStart + offset) % shardCount]; + const candidate = shards[(tiebreakStart + offset) % shardCount]!; if (candidate.total < lightest.total) lightest = candidate; } lightest.files.push(file); @@ -97,10 +97,10 @@ function packShards(files, durationByFile, shardCount) { return shards; } -function assertInvariant(files, shards) { +function assertInvariant(files: readonly string[], shards: readonly Shard[]): void { const original = new Set(files); - const seen = new Set(); - const duplicates = []; + const seen = new Set(); + const duplicates: string[] = []; for (const shard of shards) { for (const file of shard.files) { if (seen.has(file)) duplicates.push(file); @@ -132,7 +132,7 @@ const durationByFile = loadTimingData(timingArg); const shards = packShards(files, durationByFile, shardsArg); assertInvariant(files, shards); -const assignment = {}; +const assignment: Record = {}; shards.forEach((shard, index) => { assignment[String(index + 1)] = shard.files; }); diff --git a/scripts/deploy-selfhost-prebuilt.sh b/scripts/deploy-selfhost-prebuilt.sh index 451de04cbf..225ef34539 100755 --- a/scripts/deploy-selfhost-prebuilt.sh +++ b/scripts/deploy-selfhost-prebuilt.sh @@ -36,7 +36,7 @@ run_node_build() { -v "$PWD:/work" \ -w /work \ "$NODE_IMAGE" \ - sh -lc 'npm ci --ignore-scripts && npm --workspace @loopover/engine run build && node scripts/build-selfhost.mjs --all && node --experimental-strip-types scripts/validate-selfhost-sourcemap.ts' + sh -lc 'npm ci --ignore-scripts && npm --workspace @loopover/engine run build && node --experimental-strip-types scripts/build-selfhost.ts --all && node --experimental-strip-types scripts/validate-selfhost-sourcemap.ts' } run_sentry_upload() { diff --git a/scripts/fetch-test-timing.ts b/scripts/fetch-test-timing.ts index d190fd8d3b..f9cfda6115 100644 --- a/scripts/fetch-test-timing.ts +++ b/scripts/fetch-test-timing.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node // Fetches per-test-file historical duration data from Codecov's Test Analytics API and aggregates it -// into a per-file average, for the test-shard bin-packer (scripts/compute-test-shards.mjs) to consume. +// into a per-file average, for the test-shard bin-packer (scripts/compute-test-shards.ts) to consume. // Codecov already ingests a JUnit report per shard on every push to main (see ci.yml's coverage-upload // steps, report_type: test_results) and pools it across runs -- this reads that pooled history back // out instead of this repo tracking its own duration history from scratch. @@ -8,7 +8,7 @@ // Filtered to branch=main deliberately: a PR's own JUnit upload is override_branch'd to that PR's own // branch name (see ci.yml's upload steps), not "main" -- so branch=main naturally selects only // push-triggered, full-unscoped-suite runs, which is exactly the population the shard bin-packer needs -// (duration-aware sharding only applies to the full-suite case; see compute-test-shards.mjs). +// (duration-aware sharding only applies to the full-suite case; see compute-test-shards.ts). // // Requires a Codecov personal API access token (Codecov Settings -> Access -> Generate Token), NOT the // existing CODECOV_TOKEN secret -- that one is an upload-only token and doesn't authenticate this read diff --git a/scripts/forbidden-content.ts b/scripts/forbidden-content.ts index e1cdcbcf6b..343466d1c1 100644 --- a/scripts/forbidden-content.ts +++ b/scripts/forbidden-content.ts @@ -1,6 +1,6 @@ // Single source of truth for the miner package's secret-shape detector. // -// scripts/check-miner-package.mjs + scripts/check-mcp-package.mjs use this to reject any packed miner/mcp file +// scripts/check-miner-package.ts + scripts/check-mcp-package.ts use this to reject any packed miner/mcp file // that embeds a secret-like value, and the AMS MCP contract test (test/unit/miner-mcp-contract.test.ts) reuses // the SAME pattern to assert no MCP tool response ever leaks one — importing it here rather than hand-duplicating // the regex keeps every consumer byte-for-byte in sync instead of relying on manual vigilance. diff --git a/scripts/gen-selfhost-env-reference.ts b/scripts/gen-selfhost-env-reference.ts index 61ce74f245..b7ef99573e 100644 --- a/scripts/gen-selfhost-env-reference.ts +++ b/scripts/gen-selfhost-env-reference.ts @@ -16,9 +16,9 @@ export const DEFAULT_SOURCE_ROOTS = [ "src/services/ai-review.ts", "src/queue/ai-review-orchestration.ts", "src/queue/processors.ts", - "scripts/build-selfhost.mjs", + "scripts/build-selfhost.ts", "scripts/migrate-selfhost-sqlite-to-postgres.ts", - "scripts/smoke-observability-traces.mjs", + "scripts/smoke-observability-traces.ts", ]; const ENV_NAME_RE = /^[A-Z][A-Z0-9_]*$/; diff --git a/scripts/lint-composite-actions.ts b/scripts/lint-composite-actions.ts index 82197322ad..9630de9cd5 100644 --- a/scripts/lint-composite-actions.ts +++ b/scripts/lint-composite-actions.ts @@ -5,7 +5,7 @@ // missing one in a composite action is a silent hard failure at actual run time, not a parse-time // error). // -// actionlint (this repo's usual workflow linter, scripts/actionlint.mjs) does NOT support action.yml +// actionlint (this repo's usual workflow linter, scripts/actionlint.ts) does NOT support action.yml // files at all -- confirmed this is a genuine, long-standing upstream limitation // (github.com/rhysd/actionlint/issues/46 and /issues/401, open since 2021), not a configuration gap on // this repo's side: even the raw actionlint binary, invoked directly with no wrapper, treats any file diff --git a/scripts/mcp-package-allowlist.ts b/scripts/mcp-package-allowlist.ts index 79cd455d6f..fe95c4009f 100644 --- a/scripts/mcp-package-allowlist.ts +++ b/scripts/mcp-package-allowlist.ts @@ -1,4 +1,4 @@ -// Canonical MCP published-tarball allowlist (#6291). Shared by check-mcp-package.mjs and +// Canonical MCP published-tarball allowlist (#6291). Shared by check-mcp-package.ts and // mcp-release-candidate-core.ts so the dry-run gate and the release-candidate tarball check // cannot drift (the previous duplicated lists already missed shipped lib/*.js files). diff --git a/scripts/mcp-release-candidate-core.ts b/scripts/mcp-release-candidate-core.ts index 037cb7e272..ecaf0ee5f8 100644 --- a/scripts/mcp-release-candidate-core.ts +++ b/scripts/mcp-release-candidate-core.ts @@ -16,7 +16,7 @@ import { MCP_PACKAGE_ALLOWED_FILE_PATTERNS } from "./mcp-package-allowlist.js"; export const RELEASE_TAG_PATTERN = /^mcp-v(\d+)\.(\d+)\.(\d+)$/; -// Canonical allowlist lives in mcp-package-allowlist.ts (shared with check-mcp-package.mjs). +// Canonical allowlist lives in mcp-package-allowlist.ts (shared with check-mcp-package.ts). export { MCP_PACKAGE_ALLOWED_FILE_PATTERNS }; const ALLOWED_FILE_PATTERNS = MCP_PACKAGE_ALLOWED_FILE_PATTERNS; const FORBIDDEN_PATH_PATTERN = /(^|\/)(\.dev\.vars|\.env|\.npmrc|.*\.pem|.*private.*key.*|.*secret.*)$/i; diff --git a/scripts/orb-release-core.ts b/scripts/orb-release-core.ts index 19d4a4fb4f..cb656ae7f6 100644 --- a/scripts/orb-release-core.ts +++ b/scripts/orb-release-core.ts @@ -20,7 +20,7 @@ const IMAGE_RELEVANT_PREFIXES = [ "migrations/", "Dockerfile", "docker-compose.yml", - "scripts/build-selfhost.mjs", + "scripts/build-selfhost.ts", "scripts/deploy-selfhost-image.sh", "scripts/deploy-selfhost-prebuilt.sh", "scripts/lib/selfhost-deploy-common.sh", diff --git a/scripts/rees-coverage.d.mts b/scripts/rees-coverage.d.mts deleted file mode 100644 index be8d3a98ac..0000000000 --- a/scripts/rees-coverage.d.mts +++ /dev/null @@ -1,6 +0,0 @@ -export type NormalizeLcovSfPathsOptions = { - readFile?: (path: string) => string; - writeFile?: (path: string, content: string) => void; -}; - -export function normalizeLcovSfPaths(lcovPath: string, options?: NormalizeLcovSfPathsOptions): void; diff --git a/scripts/rees-coverage.mjs b/scripts/rees-coverage.ts similarity index 85% rename from scripts/rees-coverage.mjs rename to scripts/rees-coverage.ts index 0b527967a8..5752f065ce 100644 --- a/scripts/rees-coverage.mjs +++ b/scripts/rees-coverage.ts @@ -2,14 +2,17 @@ // Runs c8 from the monorepo root so source-map remapping yields `review-enrichment/src/**` paths // (not bare `src/**`), and expands the test list in-process so Windows/npm quoting cannot drop the suite. import { spawnSync } from "node:child_process"; -import { readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { readFileSync, readdirSync, writeFileSync } from "node:fs"; import { join, relative } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, URL } from "node:url"; /** Normalize c8's SF: paths to forward slashes for Codecov. Swallows only a missing report * (ENOENT on read) — CI's "Verify REES coverage report exists" step fails closed downstream. * Any other read/write error propagates so a real lcov post-process failure is not masked. */ -export function normalizeLcovSfPaths(lcovPath, { readFile = readFileSync, writeFile = writeFileSync } = {}) { +export function normalizeLcovSfPaths( + lcovPath: string, + { readFile = readFileSync, writeFile = writeFileSync }: { readFile?: (path: string, encoding: "utf8") => string; writeFile?: (path: string, data: string) => void } = {}, +): void { try { const raw = readFile(lcovPath, "utf8"); writeFile( @@ -22,7 +25,7 @@ export function normalizeLcovSfPaths(lcovPath, { readFile = readFileSync, writeF } } -function collectTests(dir, out = []) { +function collectTests(dir: string, out: string[] = []): string[] { for (const ent of readdirSync(dir, { withFileTypes: true })) { const path = join(dir, ent.name); if (ent.isDirectory()) collectTests(path, out); diff --git a/scripts/smoke-observability-metrics.mjs b/scripts/smoke-observability-metrics.ts similarity index 92% rename from scripts/smoke-observability-metrics.mjs rename to scripts/smoke-observability-metrics.ts index 6d1b61a1f2..cfa4a81950 100644 --- a/scripts/smoke-observability-metrics.mjs +++ b/scripts/smoke-observability-metrics.ts @@ -10,12 +10,15 @@ const metricName = `loopover_selfhost_smoke_${Date.now()}_total`; await main(); -async function main() { +async function main(): Promise { if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("OBSERVABILITY_SMOKE_TIMEOUT_MS must be a positive number"); if (!Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) throw new Error("OBSERVABILITY_SMOKE_POLL_MS must be a positive number"); + // Executed by `docker compose exec ... node -e`, inside the container -- a separate JS runtime/process + // from this script's own, so its content is plain interpolated text, not something this file's own + // TypeScript can (or should) type-check. const script = ` const metricName = ${JSON.stringify(metricName)}; const now = BigInt(Date.now()) * 1000000n; diff --git a/scripts/smoke-observability-traces.mjs b/scripts/smoke-observability-traces.ts similarity index 90% rename from scripts/smoke-observability-traces.mjs rename to scripts/smoke-observability-traces.ts index f3aa0e7d11..b8c17eda67 100644 --- a/scripts/smoke-observability-traces.mjs +++ b/scripts/smoke-observability-traces.ts @@ -12,12 +12,15 @@ const spanId = randomBytes(8).toString("hex"); await main(); -async function main() { +async function main(): Promise { if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("OBSERVABILITY_SMOKE_TIMEOUT_MS must be a positive number"); if (!Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) throw new Error("OBSERVABILITY_SMOKE_POLL_MS must be a positive number"); + // Executed by `docker compose exec ... node -e`, inside the container -- a separate JS runtime/process + // from this script's own, so its content is plain interpolated text, not something this file's own + // TypeScript can (or should) type-check. const script = ` const traceId = ${JSON.stringify(traceId)}; const spanId = ${JSON.stringify(spanId)}; diff --git a/scripts/smoke-ui-browser.mjs b/scripts/smoke-ui-browser.ts similarity index 95% rename from scripts/smoke-ui-browser.mjs rename to scripts/smoke-ui-browser.ts index 6854d6deaf..7fea417ed6 100644 --- a/scripts/smoke-ui-browser.mjs +++ b/scripts/smoke-ui-browser.ts @@ -7,11 +7,11 @@ if (!playwright) { process.exit(1); } -const browser = await playwright.chromium.launch({ headless: process.env.HEADFUL !== "1" }).catch((error) => { +const browser = await playwright.chromium.launch({ headless: process.env.HEADFUL !== "1" }).catch((error: unknown) => { throw new Error(`Chromium launch failed. Run npm run test:smoke:browser:install first. ${error instanceof Error ? error.message : String(error)}`); }); const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }); -const consoleErrors = []; +const consoleErrors: string[] = []; page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); }); diff --git a/src/db/migration-collisions.ts b/src/db/migration-collisions.ts index 8c12dfdb52..9be43179a3 100644 --- a/src/db/migration-collisions.ts +++ b/src/db/migration-collisions.ts @@ -1,8 +1,8 @@ -// Pure, fs-free migration-collision detection (#2550), shared by scripts/check-migrations.mjs (CI, reads the +// Pure, fs-free migration-collision detection (#2550), shared by scripts/check-migrations.ts (CI, reads the // local filesystem) and the live premerge recheck (src/queue/processors.ts, reads a GitHub-API-fetched // filename list) — a single source of truth so the two never drift apart. -/** Matches scripts/check-migrations.mjs's NAME regex exactly. */ +/** Matches scripts/check-migrations.ts's NAME regex exactly. */ export const MIGRATION_FILENAME_PATTERN = /^(\d{4})_[a-z0-9]+(?:_[a-z0-9]+)*\.sql$/; export type MigrationCollision = { @@ -20,7 +20,7 @@ export function extractMigrationNumber(filename: string): number | null { } /** The pairs already merged AND applied in production before the collision was noticed (see - * scripts/check-migrations.mjs's own header comment for why these can never be renumbered). Kept in lockstep + * scripts/check-migrations.ts's own header comment for why these can never be renumbered). Kept in lockstep * with that script's KNOWN_DUPLICATES — both must list the exact same grandfathered sets. */ export const KNOWN_MIGRATION_DUPLICATES: ReadonlyMap> = new Map([ [15, new Set(["0015_github_agent_command_feedback.sql", "0015_product_usage_events.sql"])], @@ -32,7 +32,7 @@ export const KNOWN_MIGRATION_DUPLICATES: ReadonlyMap /** * Group filenames by their migration number and return every number with more than one file, minus any - * EXACT-set match against `knownDuplicates` (same grandfather semantics as scripts/check-migrations.mjs: + * EXACT-set match against `knownDuplicates` (same grandfather semantics as scripts/check-migrations.ts: * the group must be the identical size and every file in it must be in the allowed set — a third file at an * already-grandfathered number, or a substitution, is still flagged). Non-conforming filenames are ignored — * malformed-filename detection is a separate, CI-only concern. Pure, no I/O. diff --git a/src/db/migration-column-extraction.ts b/src/db/migration-column-extraction.ts index 6ab2b4e16d..930c4a705a 100644 --- a/src/db/migration-column-extraction.ts +++ b/src/db/migration-column-extraction.ts @@ -1,5 +1,5 @@ // Pure, fs-free (table, column) collision detection across migration files (#2551), shared by -// scripts/check-migrations.mjs's cross-migration collision check. Sufficient for this repo's actual migration +// scripts/check-migrations.ts's cross-migration collision check. Sufficient for this repo's actual migration // corpus -- verified by direct inspection: no CREATE TRIGGER statements (so no trigger-body-aware semicolon // handling is needed, unlike src/selfhost/migrate.ts's statement splitter) and every identifier is a bare // lowercase snake_case name (no quoted/bracketed identifiers anywhere) -- not a general-purpose SQL parser. @@ -198,7 +198,7 @@ export type ColumnCollision = { table: string; column: string; files: string[] } * Replay every migration file's schema events IN MIGRATION-NUMBER ORDER and return every (table, column) * pair DEFINED by more than one file -- a same-table/same-column collision across differently-numbered, * individually-valid migrations (#2551). `orderedFileContents` must already be sorted ascending by migration - * number (the same order `scripts/check-migrations.mjs` reads the directory in); a `drop_table` event clears + * number (the same order `scripts/check-migrations.ts` reads the directory in); a `drop_table` event clears * every column tracked for that table so far, so a documented DROP+CREATE recreate never reads as a * collision with the table it replaces. * diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 46add1566f..fbdff23404 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -2022,7 +2022,7 @@ export function changedPathsForGuardrail( } /** - * Live premerge migrations/** collision recheck (#2550). `check-migrations.mjs` (CI) only validates against + * Live premerge migrations/** collision recheck (#2550). `check-migrations.ts` (CI) only validates against * THIS PR's own branch snapshot at the time CI ran — it can never see a sibling PR that merged a * same-numbered migration file to `baseRef` in the meantime. This does the live check right before the * merge-decision moment: fetch the base branch's CURRENT migration filenames, drop any filename THIS PR's @@ -2031,7 +2031,7 @@ export function changedPathsForGuardrail( * this PR merges), union what's left with THIS PR's own new migration filenames (the live tree never * contains this PR's own not-yet-merged files, so checking main alone could never detect a collision from * this PR's perspective — the union is load-bearing, not optional), then run the SAME collision-detection - * function scripts/check-migrations.mjs uses. + * function scripts/check-migrations.ts uses. * * Deliberately scoped to a collision involving THIS PR's own migration number(s) only (via `prNumbers`) — a * pre-existing collision between two OTHER already-merged files (which would mean `main` itself is already diff --git a/src/review/visual/image-downscale.ts b/src/review/visual/image-downscale.ts index e507c3cb81..29b6febaa4 100644 --- a/src/review/visual/image-downscale.ts +++ b/src/review/visual/image-downscale.ts @@ -2,7 +2,7 @@ // // The real downscale uses a native image-resizing binding that can't run on the Cloudflare Workers runtime // — that's why `capture.ts` (which IS Worker-reachable) imports ONLY this file, never the real dependency -// directly. Mirrors the pixel-diff.ts seam exactly: `scripts/build-selfhost.mjs`'s esbuild plugin swaps +// directly. Mirrors the pixel-diff.ts seam exactly: `scripts/build-selfhost.ts`'s esbuild plugin swaps // this specifier for a real implementation (`src/selfhost/stubs/image-downscale.ts`) when bundling the // self-host entry (`src/server.ts`). The Worker's own (wrangler) bundle never applies that swap, so hosted // mode always returns the input unchanged — zero behavior change, zero added cost. diff --git a/src/review/visual/pixel-diff.ts b/src/review/visual/pixel-diff.ts index 42bd6c27ad..ced72e08ae 100644 --- a/src/review/visual/pixel-diff.ts +++ b/src/review/visual/pixel-diff.ts @@ -5,7 +5,7 @@ // guarantee — that's why `test/unit/worker-entry-boundary.test.ts` forbids importing (or even naming, in // worker-reachable file content) that module from the Worker entry (`src/index.ts`). This file is the seam: // `capture.ts` (which IS Worker-reachable) imports ONLY this file, never the self-host module directly. -// `scripts/build-selfhost.mjs`'s esbuild plugin swaps this exact specifier for a real implementation when +// `scripts/build-selfhost.ts`'s esbuild plugin swaps this exact specifier for a real implementation when // bundling the self-host entry (`src/server.ts`) — the SAME module-substitution pattern already used for // `@cloudflare/puppeteer` in that same build. The Worker's own (wrangler) bundle never applies that swap, so // hosted mode always uses this no-op — zero behavior change, zero added cost, until a Workers-compatible diff --git a/src/review/visual/scroll-gif.ts b/src/review/visual/scroll-gif.ts index 5f21de6610..e7fa16e688 100644 --- a/src/review/visual/scroll-gif.ts +++ b/src/review/visual/scroll-gif.ts @@ -7,7 +7,7 @@ // in ./pixel-diff — depends on Node's `Buffer` and a native-leaning image-decode step the Cloudflare Workers // runtime doesn't guarantee. `test/unit/worker-entry-boundary.test.ts` enforces the same boundary here as it // does for the pixel-diff module. `capture.ts` (Worker-reachable) imports ONLY this file; never the self-host -// module directly. `scripts/build-selfhost.mjs`'s esbuild plugin swaps this exact specifier for a real +// module directly. `scripts/build-selfhost.ts`'s esbuild plugin swaps this exact specifier for a real // implementation when bundling the self-host entry (`src/server.ts`) — the same module-substitution pattern // already used for pixel-diff and `@cloudflare/puppeteer` in that same build. The Worker's own (wrangler) // bundle never applies that swap, so hosted mode always uses this no-op — zero behavior change, zero added diff --git a/src/selfhost/stubs/image-downscale.ts b/src/selfhost/stubs/image-downscale.ts index 84d44de9d9..7795546bfc 100644 --- a/src/selfhost/stubs/image-downscale.ts +++ b/src/selfhost/stubs/image-downscale.ts @@ -1,5 +1,5 @@ // Self-host replacement for src/review/visual/image-downscale.ts (#4370). Swapped in by -// scripts/build-selfhost.mjs's esbuild plugin, the same mechanism used for @cloudflare/puppeteer and +// scripts/build-selfhost.ts's esbuild plugin, the same mechanism used for @cloudflare/puppeteer and // ./pixel-diff — this file is only ever bundled into dist/server.mjs, never the Worker entry, so it's safe // to depend on sharp (a native binding) here. Unlike puppeteer-core, sharp is marked `external` in the // --all esbuild bundle (a native binding can't be bundled) and installed separately into the runtime Docker diff --git a/src/selfhost/stubs/pixel-diff.ts b/src/selfhost/stubs/pixel-diff.ts index 993a7dd6fb..84e4d11a1b 100644 --- a/src/selfhost/stubs/pixel-diff.ts +++ b/src/selfhost/stubs/pixel-diff.ts @@ -1,5 +1,5 @@ // Self-host replacement for src/review/visual/pixel-diff.ts (#3674). Swapped in by -// scripts/build-selfhost.mjs's esbuild plugin, the same mechanism used for @cloudflare/puppeteer — this +// scripts/build-selfhost.ts's esbuild plugin, the same mechanism used for @cloudflare/puppeteer — this // file is only ever bundled into dist/server.mjs, never the Worker entry, so it's safe to depend on // pixelmatch/pngjs (Node `Buffer` + PNG decode) here. Unlike puppeteer-core, pixelmatch/pngjs are // unconditional package.json dependencies (no INSTALL_VISUAL_REVIEW-style opt-in), so a plain static diff --git a/src/selfhost/stubs/scroll-gif.ts b/src/selfhost/stubs/scroll-gif.ts index 926b80abbf..81d2e64cbd 100644 --- a/src/selfhost/stubs/scroll-gif.ts +++ b/src/selfhost/stubs/scroll-gif.ts @@ -1,5 +1,5 @@ // Self-host replacement for src/review/visual/scroll-gif.ts (#3612). Swapped in by -// scripts/build-selfhost.mjs's esbuild plugin, the same mechanism used for pixel-diff and +// scripts/build-selfhost.ts's esbuild plugin, the same mechanism used for pixel-diff and // @cloudflare/puppeteer — this file is only ever bundled into dist/server.mjs, never the Worker entry, so // it's safe to depend on pngjs (Node `Buffer` + PNG decode) and gifenc (pure-JS GIF encode, no ffmpeg/native // dependency — Workers-safe by itself, but useless here without the PNG-decode step next to it) here. diff --git a/test/unit/check-changelog-script.test.ts b/test/unit/check-changelog-script.test.ts index 3cdca387c9..13ab54756b 100644 --- a/test/unit/check-changelog-script.test.ts +++ b/test/unit/check-changelog-script.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -// @ts-expect-error -- plain .mjs script with no type declarations -import { run } from "../../scripts/check-changelog.mjs"; +import { run } from "../../scripts/check-changelog.js"; /** * #7772: when a command can't even launch (binary missing from PATH), spawnSync returns `status: null` with diff --git a/test/unit/check-manifest-drift-script.test.ts b/test/unit/check-manifest-drift-script.test.ts index 714b5fc81c..8556ae7c09 100644 --- a/test/unit/check-manifest-drift-script.test.ts +++ b/test/unit/check-manifest-drift-script.test.ts @@ -5,7 +5,7 @@ import { checkManifestDrift } from "../../scripts/check-manifest-drift.js"; import { LOOPOVER_REPO_FOCUS_MANIFEST_YAML } from "../../src/config/loopover-repo-focus-manifest"; // The script imports src/config/loopover-repo-focus-manifest.ts (a .ts module), so -- like -// check-schema-drift.ts, check-migrations.mjs, and check-openapi-settings-parity.ts -- it must run via +// check-schema-drift.ts, check-migrations.ts, and check-openapi-settings-parity.ts -- it must run via // `tsx`, the same binary package.json's manifest:drift-check uses, rather than plain `node`. const TSX_BIN = join(process.cwd(), "node_modules", ".bin", "tsx"); diff --git a/test/unit/check-mcp-package.test.ts b/test/unit/check-mcp-package.test.ts index 673045b399..512957254e 100644 --- a/test/unit/check-mcp-package.test.ts +++ b/test/unit/check-mcp-package.test.ts @@ -2,13 +2,13 @@ import { execFileSync } from "node:child_process"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -// tsx, not plain node: check-mcp-package.mjs imports forbidden-content.ts and mcp-package-allowlist.ts +// tsx, not plain node: check-mcp-package.ts imports forbidden-content.ts and mcp-package-allowlist.ts // directly, so plain node can't resolve those local .ts imports. const TSX_BIN = join(process.cwd(), "node_modules", ".bin", "tsx"); function runChecker(env: Record = {}): { status: number; out: string } { try { - const stdout = execFileSync(TSX_BIN, ["scripts/check-mcp-package.mjs"], { + const stdout = execFileSync(TSX_BIN, ["scripts/check-mcp-package.ts"], { encoding: "utf8", env: { ...process.env, ...env }, }); diff --git a/test/unit/check-migrations-script.test.ts b/test/unit/check-migrations-script.test.ts index 26b1932547..1b910458bb 100644 --- a/test/unit/check-migrations-script.test.ts +++ b/test/unit/check-migrations-script.test.ts @@ -15,14 +15,14 @@ afterEach(() => { // support. const TSX_BIN = join(process.cwd(), "node_modules", ".bin", "tsx"); -// Run scripts/check-migrations.mjs over a throwaway fixture dir (via CHECK_MIGRATIONS_DIR) and normalize the +// Run scripts/check-migrations.ts over a throwaway fixture dir (via CHECK_MIGRATIONS_DIR) and normalize the // pass/fail into { status, out }. On a non-zero exit execFileSync throws; the violation text is on stderr. function runCheck(files: Record): { status: number; out: string } { const dir = mkdtempSync(join(tmpdir(), "gtmig-check-")); tmpDirs.push(dir); for (const [name, body] of Object.entries(files)) writeFileSync(join(dir, name), body); try { - const stdout = execFileSync(TSX_BIN, ["scripts/check-migrations.mjs"], { + const stdout = execFileSync(TSX_BIN, ["scripts/check-migrations.ts"], { encoding: "utf8", env: { ...process.env, CHECK_MIGRATIONS_DIR: dir }, }); @@ -35,7 +35,7 @@ function runCheck(files: Record): { status: number; out: string describe("check-migrations script", () => { it("reports every grandfathered duplicate migration number in the success summary", () => { - const output = execFileSync(TSX_BIN, ["scripts/check-migrations.mjs"], { encoding: "utf8" }); + const output = execFileSync(TSX_BIN, ["scripts/check-migrations.ts"], { encoding: "utf8" }); expect(output).toContain("(5 grandfathered duplicates: 0015, 0017, 0074, 0090, 0156)"); }); diff --git a/test/unit/check-miner-package.test.ts b/test/unit/check-miner-package.test.ts index ff3e653ecf..35b97527cb 100644 --- a/test/unit/check-miner-package.test.ts +++ b/test/unit/check-miner-package.test.ts @@ -2,13 +2,13 @@ import { execFileSync } from "node:child_process"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -// tsx, not plain node: check-miner-package.mjs imports forbidden-content.ts directly, so plain node can't +// tsx, not plain node: check-miner-package.ts imports forbidden-content.ts directly, so plain node can't // resolve that local .ts import. const TSX_BIN = join(process.cwd(), "node_modules", ".bin", "tsx"); function runChecker(env: Record = {}): { status: number; out: string } { try { - const stdout = execFileSync(TSX_BIN, ["scripts/check-miner-package.mjs"], { + const stdout = execFileSync(TSX_BIN, ["scripts/check-miner-package.ts"], { encoding: "utf8", env: { ...process.env, ...env }, }); diff --git a/test/unit/check-schema-drift-script.test.ts b/test/unit/check-schema-drift-script.test.ts index 386d3cbfb6..478719840a 100644 --- a/test/unit/check-schema-drift-script.test.ts +++ b/test/unit/check-schema-drift-script.test.ts @@ -15,7 +15,7 @@ import { } from "../../scripts/check-schema-drift.js"; import * as realSchema from "../../src/db/schema"; -// #2565: the script imports src/db/schema.ts (a .ts module), so -- like check-migrations.mjs and +// #2565: the script imports src/db/schema.ts (a .ts module), so -- like check-migrations.ts and // check-openapi-settings-parity.ts -- it must run via `tsx`, the same binary package.json's // db:schema-drift:check uses, rather than plain `node`. const TSX_BIN = join(process.cwd(), "node_modules", ".bin", "tsx"); diff --git a/test/unit/ci-engine-miner-filters.test.ts b/test/unit/ci-engine-miner-filters.test.ts index 2e0e061c09..baad3c6a88 100644 --- a/test/unit/ci-engine-miner-filters.test.ts +++ b/test/unit/ci-engine-miner-filters.test.ts @@ -9,7 +9,7 @@ describe("CI engine/miner path filters", () => { const ci = readFileSync(CI_PATH, "utf8"); expect(ci).toMatch(/engine:\s*\n\s*- 'packages\/loopover-engine\/\*\*'/); expect(ci).toMatch(/miner:\s*\n\s*- 'packages\/loopover-miner\/\*\*'/); - expect(ci).toContain("scripts/check-miner-package.mjs"); + expect(ci).toContain("scripts/check-miner-package.ts"); expect(ci).toContain("needs.changes.outputs.engine"); expect(ci).toContain("needs.changes.outputs.miner"); expect(ci).toContain("name: Build engine package"); diff --git a/test/unit/codecov-policy.test.ts b/test/unit/codecov-policy.test.ts index 4cf740eea1..58108cd4a1 100644 --- a/test/unit/codecov-policy.test.ts +++ b/test/unit/codecov-policy.test.ts @@ -202,8 +202,8 @@ describe("Codecov policy", () => { expect(vitestConfig).not.toMatch(/review-enrichment\/src\/analyzers\/codeowners\.ts/); const rootPkg = JSON.parse(readFileSync("package.json", "utf8")) as { scripts: Record }; - expect(rootPkg.scripts["rees:coverage"]).toBe("node scripts/rees-coverage.mjs"); - const reesCoverageScript = readFileSync("scripts/rees-coverage.mjs", "utf8"); + expect(rootPkg.scripts["rees:coverage"]).toBe("node --experimental-strip-types scripts/rees-coverage.ts"); + const reesCoverageScript = readFileSync("scripts/rees-coverage.ts", "utf8"); expect(reesCoverageScript).toContain("c8"); expect(reesCoverageScript).toContain("review-enrichment"); expect(reesCoverageScript).toContain("coverage"); diff --git a/test/unit/compute-test-shards.test.ts b/test/unit/compute-test-shards.test.ts index 4660a6ee08..e4dd73f31e 100644 --- a/test/unit/compute-test-shards.test.ts +++ b/test/unit/compute-test-shards.test.ts @@ -4,10 +4,10 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -const SCRIPT = join(process.cwd(), "scripts/compute-test-shards.mjs"); +const SCRIPT = join(process.cwd(), "scripts/compute-test-shards.ts"); function run(args: string[]) { - return spawnSync("node", [SCRIPT, ...args], { cwd: process.cwd(), encoding: "utf8" }); + return spawnSync("node", ["--experimental-strip-types", SCRIPT, ...args], { cwd: process.cwd(), encoding: "utf8" }); } function discoverRealTestFiles(): string[] { @@ -34,7 +34,7 @@ afterEach(() => { if (tmpDir) rmSync(tmpDir, { recursive: true, force: true }); }); -describe("compute-test-shards.mjs", () => { +describe("compute-test-shards.ts", () => { it("with no timing data, splits the real repo's test files evenly across shards (round-robin fallback)", () => { tmpDir = mkdtempSync(join(tmpdir(), "shard-test-")); const outputPath = join(tmpDir, "assignment.json"); diff --git a/test/unit/docs-selfhost-update-rollback.test.ts b/test/unit/docs-selfhost-update-rollback.test.ts index d47b87ba0e..83c320df41 100644 --- a/test/unit/docs-selfhost-update-rollback.test.ts +++ b/test/unit/docs-selfhost-update-rollback.test.ts @@ -37,7 +37,7 @@ describe("self-host update + rollback docs (#1823)", () => { // never triggers that build on its own, so anything that imports the engine (e.g. // packages/loopover-miner) fails to resolve during the --all bundle unless this runs first. const engineBuildIndex = prebuiltScript.indexOf("@loopover/engine run build"); - const bundleIndex = prebuiltScript.indexOf("build-selfhost.mjs --all"); + const bundleIndex = prebuiltScript.indexOf("build-selfhost.ts --all"); expect(engineBuildIndex).toBeGreaterThan(-1); expect(bundleIndex).toBeGreaterThan(-1); expect(engineBuildIndex).toBeLessThan(bundleIndex); diff --git a/test/unit/forbidden-content.test.ts b/test/unit/forbidden-content.test.ts index 959a66680b..9f5b4c7a75 100644 --- a/test/unit/forbidden-content.test.ts +++ b/test/unit/forbidden-content.test.ts @@ -5,17 +5,17 @@ import { describe, expect, it } from "vitest"; import { FORBIDDEN_CONTENT } from "../../scripts/forbidden-content.js"; // forbidden-content.ts calls itself the single source of truth for the packaged secret-shape detector, but -// nothing enforced it: check-mcp-package.mjs re-declared the regex as its own local constant and the two could +// nothing enforced it: check-mcp-package.ts re-declared the regex as its own local constant and the two could // drift apart unnoticed (#6290). These assertions pin both halves of the claim -- the structural one (each // checker imports the constant rather than owning a copy) and the behavioral one (each checker actually rejects // what the shared detector matches). -const PACKAGE_CHECKERS = ["scripts/check-miner-package.mjs", "scripts/check-mcp-package.mjs"]; +const PACKAGE_CHECKERS = ["scripts/check-miner-package.ts", "scripts/check-mcp-package.ts"]; // A minimal file list that passes each checker's path/allowlist/required-file guards, so the run reaches the // shared secret-content read. Mirrors the file lists each checker's own "rejects secret-like content" test uses. const REACHABLE_FILES: Record = { - "scripts/check-miner-package.mjs": ["package.json", "bin/loopover-miner.js", "lib/cli.js"], - "scripts/check-mcp-package.mjs": ["package.json", "bin/loopover-mcp.js"], + "scripts/check-miner-package.ts": ["package.json", "bin/loopover-miner.js", "lib/cli.js"], + "scripts/check-mcp-package.ts": ["package.json", "bin/loopover-mcp.js"], }; // Assembled from fragments so this file never itself contains a credential-shaped literal -- the same @@ -25,7 +25,7 @@ const SECRET_SHAPED_PROBE = ["PROBE", "_", "SECRET", "=", "value"].join(""); // Run a checker as a subprocess (never import it): both scripts run `npm pack` at import time, and neither has a // .d.mts, so importing them from TS would also break the typecheck gate. Their env seams let a single file drive // the whole file list + content. Run via tsx, not plain node: both scripts import forbidden-content.ts (and -// check-mcp-package.mjs also imports mcp-package-allowlist.ts) directly, so plain node can't resolve those +// check-mcp-package.ts also imports mcp-package-allowlist.ts) directly, so plain node can't resolve those // local .ts imports. const TSX_BIN = join(process.cwd(), "node_modules", ".bin", "tsx"); @@ -68,10 +68,10 @@ describe("FORBIDDEN_CONTENT is the single source of truth (#6290)", () => { // Scoped to the MCP checker: the miner one layers required-file / lib-artifact / docs guards on top of a // minimal file list, so a clean-content pass there would be asserting its allowlist rather than the shared // detector. The reject case above already proves the miner checker runs content through the shared constant. - it("scripts/check-mcp-package.mjs accepts content the shared detector leaves alone", () => { + it("scripts/check-mcp-package.ts accepts content the shared detector leaves alone", () => { const result = runChecker( - "scripts/check-mcp-package.mjs", - REACHABLE_FILES["scripts/check-mcp-package.mjs"]!, + "scripts/check-mcp-package.ts", + REACHABLE_FILES["scripts/check-mcp-package.ts"]!, "export const answer = 42;", ); expect(result.status).toBe(0); diff --git a/test/unit/migration-collisions.test.ts b/test/unit/migration-collisions.test.ts index 65a249b6bd..ea4fd1e810 100644 --- a/test/unit/migration-collisions.test.ts +++ b/test/unit/migration-collisions.test.ts @@ -72,7 +72,7 @@ describe("detectMigrationCollisions (#2550)", () => { }); describe("KNOWN_MIGRATION_DUPLICATES (#2550)", () => { - it("stays byte-identical to scripts/check-migrations.mjs's grandfathered list", () => { + it("stays byte-identical to scripts/check-migrations.ts's grandfathered list", () => { // A drift here would mean the CI script and the live premerge recheck disagree about what's grandfathered // — this pins the exact set so a future addition to one side without the other is caught immediately. expect([...KNOWN_MIGRATION_DUPLICATES.keys()].sort((a, b) => a - b)).toEqual([15, 17, 74, 90, 156]); diff --git a/test/unit/observability-release-fetch-timeout.test.ts b/test/unit/observability-release-fetch-timeout.test.ts index 83fdb9636e..351b35e70b 100644 --- a/test/unit/observability-release-fetch-timeout.test.ts +++ b/test/unit/observability-release-fetch-timeout.test.ts @@ -15,7 +15,7 @@ it("check-mcp-release-due's githubRequest fetch carries an AbortSignal timeout", }); describe("smoke-observability scripts (#7014): every generated fetch is timeout-guarded", () => { - for (const path of ["scripts/smoke-observability-traces.mjs", "scripts/smoke-observability-metrics.mjs"]) { + for (const path of ["scripts/smoke-observability-traces.ts", "scripts/smoke-observability-metrics.ts"]) { it(`${path} bounds every fetch with AbortSignal.timeout`, () => { const src = readFileSync(path, "utf8"); const fetchCount = (src.match(/\bawait fetch\(/g) ?? []).length; diff --git a/test/unit/orb-release.test.ts b/test/unit/orb-release.test.ts index ab016f1038..27b3a165bd 100644 --- a/test/unit/orb-release.test.ts +++ b/test/unit/orb-release.test.ts @@ -108,7 +108,7 @@ describe("semver helpers", () => { // #6294: a same-version beta must never outrank its stable promotion. A lexicographic sort puts // "orb-v1.2.0-beta.10" ahead of "orb-v1.2.0" (the beta suffix string-sorts after its own prefix), which - // would pick a stale beta as check-orb-release-due.mjs's git-log boundary once a beta and its stable + // would pick a stale beta as check-orb-release-due.ts's git-log boundary once a beta and its stable // promotion point at different commits. Beta tags of a shipped version live in the tag list forever, so // this stays reachable indefinitely. it("latestOrbTag ranks a stable tag ahead of its same-version betas, not lexicographically (#6294)", () => { diff --git a/test/unit/rees-coverage-script.test.ts b/test/unit/rees-coverage-script.test.ts index fca31047a0..0f90e02ecd 100644 --- a/test/unit/rees-coverage-script.test.ts +++ b/test/unit/rees-coverage-script.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { normalizeLcovSfPaths } from "../../scripts/rees-coverage.mjs"; +import { normalizeLcovSfPaths } from "../../scripts/rees-coverage.js"; describe("rees-coverage script", () => { describe("normalizeLcovSfPaths", () => { diff --git a/test/unit/selfhost-image-downscale-stub.test.ts b/test/unit/selfhost-image-downscale-stub.test.ts index b13274e9fb..2882d04b17 100644 --- a/test/unit/selfhost-image-downscale-stub.test.ts +++ b/test/unit/selfhost-image-downscale-stub.test.ts @@ -1,5 +1,5 @@ // Tests for the self-host vision-image-downscale stub (#4370). This module is never bundled into the -// Worker entry (scripts/build-selfhost.mjs swaps it in only when building src/server.ts — see +// Worker entry (scripts/build-selfhost.ts swaps it in only when building src/server.ts — see // test/unit/worker-entry-boundary.test.ts for the enforced side of that), so it's safe to depend on sharp // / real PNG fixtures here, mirroring test/unit/selfhost-pixel-diff-stub.test.ts's own fixture style. import sharp from "sharp"; diff --git a/test/unit/selfhost-observability-config.test.ts b/test/unit/selfhost-observability-config.test.ts index 6784b38185..7c7edf28b7 100644 --- a/test/unit/selfhost-observability-config.test.ts +++ b/test/unit/selfhost-observability-config.test.ts @@ -79,7 +79,7 @@ describe("self-host observability trace config", () => { it("ships an operator smoke probe that verifies collector to Tempo retrieval", () => { const script = readFileSync( - join(process.cwd(), "scripts/smoke-observability-traces.mjs"), + join(process.cwd(), "scripts/smoke-observability-traces.ts"), "utf8", ); @@ -91,7 +91,7 @@ describe("self-host observability trace config", () => { it("ships an operator smoke probe that verifies collector-to-Prometheus-exporter metrics retrieval, plus the app's own /metrics shape (2026-07 fix)", () => { const script = readFileSync( - join(process.cwd(), "scripts/smoke-observability-metrics.mjs"), + join(process.cwd(), "scripts/smoke-observability-metrics.ts"), "utf8", ); @@ -103,7 +103,7 @@ describe("self-host observability trace config", () => { const packageJson = JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf8")); expect(packageJson.scripts["test:smoke:observability:metrics"]).toBe( - "node scripts/smoke-observability-metrics.mjs", + "node --experimental-strip-types scripts/smoke-observability-metrics.ts", ); }); diff --git a/test/unit/selfhost-pixel-diff-stub.test.ts b/test/unit/selfhost-pixel-diff-stub.test.ts index 246821cce0..5ee2e3a207 100644 --- a/test/unit/selfhost-pixel-diff-stub.test.ts +++ b/test/unit/selfhost-pixel-diff-stub.test.ts @@ -1,5 +1,5 @@ // Tests for the self-host pixel-diff stub (#3674). This module is never bundled into the Worker entry -// (scripts/build-selfhost.mjs swaps it in only when building src/server.ts — see +// (scripts/build-selfhost.ts swaps it in only when building src/server.ts — see // test/unit/worker-entry-boundary.test.ts for the enforced side of that), so it's safe to depend on real // PNG fixtures / Buffer here, mirroring test/unit/visual-diff.test.ts's own fixture style. import { PNG } from "pngjs"; diff --git a/test/unit/selfhost-scroll-gif-stub.test.ts b/test/unit/selfhost-scroll-gif-stub.test.ts index 819e30d85e..476215c456 100644 --- a/test/unit/selfhost-scroll-gif-stub.test.ts +++ b/test/unit/selfhost-scroll-gif-stub.test.ts @@ -1,5 +1,5 @@ // Tests for the self-host scroll-through-GIF stub (#3612). This module is never bundled into the Worker -// entry (scripts/build-selfhost.mjs swaps it in only when building src/server.ts — see +// entry (scripts/build-selfhost.ts swaps it in only when building src/server.ts — see // test/unit/worker-entry-boundary.test.ts for the enforced side of that), so it's safe to depend on real // PNG fixtures / Buffer here, mirroring test/unit/selfhost-pixel-diff-stub.test.ts's own fixture style. import { PNG } from "pngjs"; diff --git a/test/unit/selfhost-sentry-release.test.ts b/test/unit/selfhost-sentry-release.test.ts index 10734c6048..ed50cd6f12 100644 --- a/test/unit/selfhost-sentry-release.test.ts +++ b/test/unit/selfhost-sentry-release.test.ts @@ -71,7 +71,7 @@ describe("self-host Sentry release wiring", () => { ); for (const path of [ - "scripts/build-selfhost.mjs", + "scripts/build-selfhost.ts", "Dockerfile", ".github/workflows/selfhost.yml", ]) {