From f342761f1ba087a0726d36e8f9a9104d06f375db Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 25 Jun 2026 17:06:08 -0700 Subject: [PATCH] feat: add test-free verify:fast (typecheck + build + boot-smoke) Adds scripts/verify-fast.mjs + root `pnpm verify:fast`, an opt-in, flake-free verification path that runs typecheck + build scoped to the changed packages (reusing test-changed.mjs git-diff / changed-package resolution) plus the existing boot smoke once, with no test suite. Each step is bounded by the shared runWithWatchdog (class "changed"); exits nonzero on the first failure. No default changed: pnpm test, the merge gate, and CI are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 3 + docs/testing.md | 4 + package.json | 1 + scripts/__tests__/verify-fast.test.mjs | 155 +++++++++++++++ scripts/test-changed.mjs | 14 +- scripts/verify-fast.mjs | 265 +++++++++++++++++++++++++ 6 files changed, 438 insertions(+), 4 deletions(-) create mode 100644 scripts/__tests__/verify-fast.test.mjs create mode 100644 scripts/verify-fast.mjs diff --git a/AGENTS.md b/AGENTS.md index ddf29832a5..a59723fdae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,6 +107,7 @@ The merge gate is thin and trusted: CI blocks PRs on exactly Lint, Typecheck, Bu pnpm test # gate suite + changed-only affected tests (bounded; never full-suite) pnpm test:gate # the merge gate: curated engine-core suite + CI-shape test pnpm smoke:boot # boot smoke: CLI --help + real serve /api/health +pnpm verify:fast # TEST-FREE verification: typecheck + build (scoped to changed packages) + boot smoke; recommended non-test verification/testCommand. Additive — changes no default pnpm test:velocity # weekly report-only test velocity baseline; use -- --measure --write-report to refresh pnpm test:full # full workspace suite — explicit opt-in only pnpm lint @@ -114,6 +115,8 @@ pnpm build pnpm verify:workspace # deep opt-in verification (lint -> test:full -> build); NOT the merge gate ``` +`pnpm verify:fast` is the recommended **test-free verification** path: typecheck + build scoped to the changed packages (it reuses `pnpm test`'s changed-package resolution) plus the boot smoke once, with **no test run**. It is deterministic and flake-free, suitable as a project `testCommand`/verification command when you want non-test verification; the full suite stays available and runs non-blocking. It is additive and does not change `pnpm test`, the gate, or CI. See `docs/testing.md`. + ### Standing Rule: Flaky Tests Are Quarantined on Sight (Deletion Ratchet) - A test observed failing without a corresponding real bug in the change is QUARANTINED ON SIGHT: add an entry to `scripts/lib/test-quarantine.json` (`file`, `reason` with a link to the failing run, `quarantinedAt`) AND a matching one-line `exclude` in that package's vitest config, in the same commit. diff --git a/docs/testing.md b/docs/testing.md index ce93d09a27..a62a6ad758 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -32,12 +32,16 @@ Use the narrowest command that exercises the behavior you changed, then broaden pnpm test # gate suite + changed-only affected tests (bounded; never full-suite) pnpm test:gate # the merge gate: curated engine-core suite + CI-shape test pnpm smoke:boot # boot smoke: CLI --help + real serve /api/health +pnpm verify:fast # TEST-FREE verification: typecheck + build (scoped to changed packages) + boot smoke pnpm test:full # full workspace suite — explicit opt-in only pnpm lint # lint all packages pnpm build # build workspace packages (excludes desktop/mobile) pnpm verify:workspace # deep opt-in verification: lint -> test:full -> build (NOT the merge gate) ``` + +`pnpm verify:fast` (`scripts/verify-fast.mjs`) is the recommended **test-free verification** command: it runs **typecheck + build scoped to the changed packages** (reusing the same git-diff / changed-package resolution as `pnpm test`) followed by the existing **boot smoke** once — and runs **no test suite**. It gives deterministic, flake-free signal in seconds, so it is a sound project `testCommand`/verification command when you want non-test verification. With no affected package (root/docs-only diff) it runs the boot smoke only. Each step is bounded by the shared `runWithWatchdog` (class `changed`) so a hang fails fast, and it exits nonzero on the first failing step. This is purely additive: it does not change `pnpm test`, the merge gate, or CI, and the full suite stays available (`pnpm test:full`, non-blocking on push to main). + `pnpm test:full` runs each package's default test script with capped worker fanout (`FUSION_TEST_TOTAL_WORKERS=4 FUSION_TEST_CONCURRENCY=2 pnpm -r --workspace-concurrency=2 test`). Do not casually raise worker counts; dashboard/jsdom and integration-heavy packages destabilize when oversubscribed. Use `VITEST_MAX_WORKERS=` only for targeted package-level investigation. diff --git a/package.json b/package.json index 229b5eb2b1..f0b7d6fcbb 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "build:exe": "pnpm build && pnpm --filter @runfusion/fusion build:exe", "build:exe:all": "pnpm build && pnpm --filter @runfusion/fusion build:exe:all", "test": "node scripts/test-changed.mjs", + "verify:fast": "node scripts/verify-fast.mjs", "test:scripts": "node scripts/run-script-tests.mjs", "test:workflow-release-check": "node scripts/workflow-reliability-release-check.mjs", "fn:cache-stats": "node scripts/cache-stats.mjs", diff --git a/scripts/__tests__/verify-fast.test.mjs b/scripts/__tests__/verify-fast.test.mjs new file mode 100644 index 0000000000..91dbbcd3b9 --- /dev/null +++ b/scripts/__tests__/verify-fast.test.mjs @@ -0,0 +1,155 @@ +/** + * Unit tests for scripts/verify-fast.mjs + * + * Runner: node --test scripts/__tests__/verify-fast.test.mjs + * + * These exercise the PURE planning / arg-construction logic only. They never + * spawn real tsc / build / vitest — the test-free verification command's value + * is its deterministic plan, so that is what we pin. + */ + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + buildTypecheckStep, + buildBuildStep, + buildBootSmokeStep, + buildVerifyPlan, + VERIFY_EXCLUDED_PACKAGES, +} from "../verify-fast.mjs"; + +import { resolveAffectedPackages } from "../test-changed.mjs"; + +const SMOKE = "/repo/scripts/boot-smoke.mjs"; +const NODE = "/usr/bin/node"; + +function stepIds(plan) { + return plan.steps.map((s) => s.id); +} +function stepByKind(plan, kind) { + return plan.steps.filter((s) => s.kind === kind); +} + +// --------------------------------------------------------------------------- +// buildTypecheckStep +// --------------------------------------------------------------------------- + +test("buildTypecheckStep: uses the package's typecheck script when present", () => { + const step = buildTypecheckStep("@fusion/engine", { hasTypecheck: true }); + assert.equal(step.command, "pnpm"); + assert.deepEqual(step.args, ["--filter", "@fusion/engine", "typecheck"]); + assert.equal(step.klass, "changed"); +}); + +test("buildTypecheckStep: falls back to scoped tsc --noEmit when no typecheck script", () => { + const step = buildTypecheckStep("@fusion/widget", { hasTypecheck: false }); + assert.deepEqual(step.args, ["--filter", "@fusion/widget", "exec", "tsc", "--noEmit", "-p", "."]); +}); + +test("buildTypecheckStep: defaults to the tsc fallback when meta omitted", () => { + const step = buildTypecheckStep("@fusion/widget"); + assert.deepEqual(step.args, ["--filter", "@fusion/widget", "exec", "tsc", "--noEmit", "-p", "."]); +}); + +// --------------------------------------------------------------------------- +// buildBuildStep / buildBootSmokeStep +// --------------------------------------------------------------------------- + +test("buildBuildStep: scoped pnpm build for the package", () => { + const step = buildBuildStep("@fusion/cli"); + assert.deepEqual(step.args, ["--filter", "@fusion/cli", "build"]); + assert.equal(step.kind, "build"); +}); + +test("buildBootSmokeStep: runs the boot-smoke script via node", () => { + const step = buildBootSmokeStep(SMOKE, NODE); + assert.equal(step.command, NODE); + assert.deepEqual(step.args, [SMOKE]); + assert.equal(step.kind, "boot-smoke"); +}); + +// --------------------------------------------------------------------------- +// buildVerifyPlan +// --------------------------------------------------------------------------- + +test("buildVerifyPlan: no packages -> boot smoke only", () => { + const plan = buildVerifyPlan({ packages: [], bootSmokeScriptPath: SMOKE, nodeBin: NODE }); + assert.deepEqual(stepIds(plan), ["boot-smoke"]); + assert.deepEqual(plan.eligiblePackages, []); +}); + +test("buildVerifyPlan: typecheck for all eligible, then builds, then boot smoke (ordered)", () => { + const packageMeta = new Map([ + ["@fusion/engine", { hasTypecheck: true, hasBuild: true }], + ["@fusion/core", { hasTypecheck: true, hasBuild: true }], + ]); + const plan = buildVerifyPlan({ packages: ["@fusion/engine", "@fusion/core"], packageMeta, bootSmokeScriptPath: SMOKE, nodeBin: NODE }); + assert.deepEqual(stepIds(plan), [ + "typecheck:@fusion/engine", + "typecheck:@fusion/core", + "build:@fusion/engine", + "build:@fusion/core", + "boot-smoke", + ]); +}); + +test("buildVerifyPlan: a package without a build script gets a typecheck step but no build step", () => { + const packageMeta = new Map([ + ["@fusion/engine", { hasTypecheck: true, hasBuild: true }], + ["@fusion/test-only", { hasTypecheck: false, hasBuild: false }], + ]); + const plan = buildVerifyPlan({ packages: ["@fusion/engine", "@fusion/test-only"], packageMeta, bootSmokeScriptPath: SMOKE, nodeBin: NODE }); + assert.deepEqual(stepIds(plan), [ + "typecheck:@fusion/engine", + "typecheck:@fusion/test-only", + "build:@fusion/engine", + "boot-smoke", + ]); + // The test-only package's typecheck uses the tsc fallback (no typecheck script). + const tc = stepByKind(plan, "typecheck").find((s) => s.pkg === "@fusion/test-only"); + assert.deepEqual(tc.args, ["--filter", "@fusion/test-only", "exec", "tsc", "--noEmit", "-p", "."]); +}); + +test("buildVerifyPlan: desktop/mobile are excluded from scoped steps but boot smoke still runs", () => { + const packageMeta = new Map([ + ["@fusion/engine", { hasTypecheck: true, hasBuild: true }], + ["@fusion/desktop", { hasTypecheck: true, hasBuild: true }], + ["@fusion/mobile", { hasTypecheck: true, hasBuild: true }], + ]); + const plan = buildVerifyPlan({ + packages: ["@fusion/engine", "@fusion/desktop", "@fusion/mobile"], + packageMeta, + bootSmokeScriptPath: SMOKE, + nodeBin: NODE, + }); + assert.deepEqual(plan.eligiblePackages, ["@fusion/engine"]); + assert.deepEqual(plan.excludedPackages.sort(), ["@fusion/desktop", "@fusion/mobile"]); + assert.deepEqual(stepIds(plan), ["typecheck:@fusion/engine", "build:@fusion/engine", "boot-smoke"]); +}); + +test("VERIFY_EXCLUDED_PACKAGES mirrors the root build/typecheck exclusions", () => { + assert.ok(VERIFY_EXCLUDED_PACKAGES.has("@fusion/desktop")); + assert.ok(VERIFY_EXCLUDED_PACKAGES.has("@fusion/mobile")); +}); + +// --------------------------------------------------------------------------- +// Integration: reuse test-changed's resolveAffectedPackages to scope the plan +// --------------------------------------------------------------------------- + +test("buildVerifyPlan: scopes to exactly the packages resolveAffectedPackages selects", () => { + // packageNameByDir as test-changed builds it (dir -> name, with a bare alias). + const packageNameByDir = new Map([ + ["packages/engine", "@fusion/engine"], + ["engine", "@fusion/engine"], + ["packages/dashboard", "@fusion/dashboard"], + ["dashboard", "@fusion/dashboard"], + ]); + const changedFiles = ["packages/engine/src/merger.ts", "docs/testing.md"]; + const affected = resolveAffectedPackages(changedFiles, packageNameByDir); + assert.deepEqual(affected, ["@fusion/engine"]); // docs/ change does not add a package + + const packageMeta = new Map([["@fusion/engine", { hasTypecheck: true, hasBuild: true }]]); + const plan = buildVerifyPlan({ packages: affected, packageMeta, bootSmokeScriptPath: SMOKE, nodeBin: NODE }); + assert.deepEqual(stepIds(plan), ["typecheck:@fusion/engine", "build:@fusion/engine", "boot-smoke"]); +}); diff --git a/scripts/test-changed.mjs b/scripts/test-changed.mjs index 7edb162dfe..d66038fc2b 100644 --- a/scripts/test-changed.mjs +++ b/scripts/test-changed.mjs @@ -407,7 +407,13 @@ function gitOutput(gitArgs) { return result.stdout.trim(); } -function getBaseBranch() { +// FNXC:TestInfrastructure 2026-06-25-00:00: +// Exported so the test-free verification path (scripts/verify-fast.mjs) reuses +// the SAME base-branch / comparison-base / changed-file / workspace-resolution +// logic instead of reinventing git-diff. verify:fast runs typecheck + build + +// boot-smoke (no test suite) and must scope to exactly the packages a +// changed-only run would scope to. +export function getBaseBranch() { const changesetConfigPath = path.join(rootDir, ".changeset", "config.json"); const changesetConfig = JSON.parse(readFileSync(changesetConfigPath, "utf8")); return changesetConfig.baseBranch || "main"; @@ -497,7 +503,7 @@ export function listWorkspacePackageInfos({ projectRoot = rootDir } = {}) { .sort((a, b) => a.dir.localeCompare(b.dir)); } -function listWorkspacePackages(workspacePackages = listWorkspacePackageInfos()) { +export function listWorkspacePackages(workspacePackages = listWorkspacePackageInfos()) { const packageNameByDir = new Map(); for (const workspacePackage of workspacePackages) { packageNameByDir.set(workspacePackage.dir, workspacePackage.name); @@ -665,7 +671,7 @@ export function isSharedInfraChange(changedFiles) { }); } -function detectComparisonBase(baseBranch) { +export function detectComparisonBase(baseBranch) { const candidates = [ `origin/${baseBranch}`, `refs/remotes/origin/${baseBranch}`, @@ -682,7 +688,7 @@ function detectComparisonBase(baseBranch) { return null; } -function changedFilesSince(baseSha) { +export function changedFilesSince(baseSha) { const diff = gitOutput(["diff", "--name-only", `${baseSha}...HEAD`]); if (diff === null) { return null; diff --git a/scripts/verify-fast.mjs b/scripts/verify-fast.mjs new file mode 100644 index 0000000000..f12de223cb --- /dev/null +++ b/scripts/verify-fast.mjs @@ -0,0 +1,265 @@ +#!/usr/bin/env node +/* +FNXC:TestInfrastructure 2026-06-25-00:00: +verify:fast is the opt-in, TEST-FREE verification command. It gives deterministic, +flake-free signal in seconds without running the test suite, by doing exactly: + 1. typecheck — scoped to the changed packages (their `typecheck` script, or + `pnpm --filter exec tsc --noEmit -p .` when none exists). + 2. build — scoped to the changed packages (`pnpm --filter build`). + 3. boot smoke — once (scripts/boot-smoke.mjs: CLI --help + real serve /api/health). + +Rationale: docs/testing.md observes the broad test gate "caught no recalled real +bugs while consuming ~70% of shipping time in flake triage." typecheck+build+boot +is fast and never flakes, so it is a sound project `testCommand`/verification +command when you want non-test verification. This command changes NO default — +`pnpm test`, the merge gate, and CI are untouched. The full suite stays available +(`pnpm test:full`) and runs non-blocking on push to main. + +Change-detection REUSES scripts/test-changed.mjs (getBaseBranch / +detectComparisonBase / changedFilesSince / resolveAffectedPackages / workspace +resolution) so verify:fast scopes to exactly the packages a changed-only test run +would, instead of reinventing git-diff. Each step is bounded by the existing +`runWithWatchdog` (class "changed") so a hung tsc/build/serve fails fast instead +of blocking forever, and we exit nonzero on the first failing step. +*/ + +import path from "node:path"; +import { readFileSync } from "node:fs"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +import { + getBaseBranch, + detectComparisonBase, + changedFilesSince, + listWorkspacePackageInfos, + listWorkspacePackages, + buildPackageDirByName, + resolveAffectedPackages, +} from "./test-changed.mjs"; +import { deriveBudgetMs, runWithWatchdog } from "./lib/run-vitest-watchdog.mjs"; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(scriptDir, ".."); +const bootSmokeScriptPath = path.join(scriptDir, "boot-smoke.mjs"); + +/* +FNXC:TestInfrastructure 2026-06-25-00:00: +@fusion/desktop and @fusion/mobile are excluded from the root `build`/`typecheck` +scripts (heavy native/electron + RN toolchains), so verify:fast mirrors that +policy and skips them with a note rather than failing on an unbuildable filter. +*/ +export const VERIFY_EXCLUDED_PACKAGES = new Set(["@fusion/desktop", "@fusion/mobile"]); + +/** + * Build the scoped typecheck step for a package. Prefers the package's own + * `typecheck` script (e.g. dashboard runs two tsc passes); falls back to a plain + * project tsc --noEmit when the package declares no typecheck script. + * + * @param {string} pkg workspace package name (e.g. "@fusion/engine") + * @param {{ hasTypecheck?: boolean }} [meta] + * @returns {{ id: string, kind: string, pkg: string, label: string, command: string, args: string[], klass: string }} + */ +export function buildTypecheckStep(pkg, meta = {}) { + const args = meta.hasTypecheck + ? ["--filter", pkg, "typecheck"] + : ["--filter", pkg, "exec", "tsc", "--noEmit", "-p", "."]; + return { id: `typecheck:${pkg}`, kind: "typecheck", pkg, label: `typecheck ${pkg}`, command: "pnpm", args, klass: "changed" }; +} + +/** + * Build the scoped build step for a package. + * + * @param {string} pkg + * @returns {{ id: string, kind: string, pkg: string, label: string, command: string, args: string[], klass: string }} + */ +export function buildBuildStep(pkg) { + return { id: `build:${pkg}`, kind: "build", pkg, label: `build ${pkg}`, command: "pnpm", args: ["--filter", pkg, "build"], klass: "changed" }; +} + +/** + * Build the single boot-smoke step (always last, after any builds, so it runs + * against freshly built artifacts). + * + * @param {string} smokeScriptPath + * @param {string} [nodeBin] + */ +export function buildBootSmokeStep(smokeScriptPath, nodeBin = process.execPath) { + return { + id: "boot-smoke", + kind: "boot-smoke", + pkg: null, + label: "boot smoke (CLI --help + real serve /api/health)", + command: nodeBin, + args: [smokeScriptPath], + klass: "changed", + }; +} + +/** + * Pure planner: turn the affected package set into an ordered step list. + * typecheck (all eligible) → build (eligible with a build script) → boot smoke. + * With no eligible packages this is just the boot-smoke step, satisfying the + * "no packages changed ⇒ boot smoke only" contract. + * + * @param {object} opts + * @param {string[]} [opts.packages] affected package names + * @param {Map} [opts.packageMeta] + * @param {string} opts.bootSmokeScriptPath + * @param {string} [opts.nodeBin] + * @returns {{ eligiblePackages: string[], excludedPackages: string[], steps: object[] }} + */ +export function buildVerifyPlan({ packages = [], packageMeta = new Map(), bootSmokeScriptPath: smokeScriptPath, nodeBin = process.execPath } = {}) { + const eligiblePackages = packages.filter((pkg) => !VERIFY_EXCLUDED_PACKAGES.has(pkg)); + const excludedPackages = packages.filter((pkg) => VERIFY_EXCLUDED_PACKAGES.has(pkg)); + + const steps = []; + for (const pkg of eligiblePackages) { + steps.push(buildTypecheckStep(pkg, packageMeta.get(pkg) ?? {})); + } + for (const pkg of eligiblePackages) { + const meta = packageMeta.get(pkg) ?? {}; + // Only build packages that declare a build script; pure test/config packages + // have nothing to emit and a `pnpm --filter build` would error. + if (meta.hasBuild !== false) steps.push(buildBuildStep(pkg)); + } + steps.push(buildBootSmokeStep(smokeScriptPath, nodeBin)); + return { eligiblePackages, excludedPackages, steps }; +} + +/** + * Read each affected package's package.json to learn which scripts it declares. + * + * @param {string[]} packages + * @param {Map} packageDirByName pkg name → repo-relative dir + * @param {string} [root] + * @returns {Map} + */ +export function readPackageMeta(packages, packageDirByName, root = repoRoot) { + const meta = new Map(); + for (const pkg of packages) { + const dir = packageDirByName.get(pkg); + let scripts = {}; + if (dir) { + try { + const pkgJson = JSON.parse(readFileSync(path.join(root, dir, "package.json"), "utf8")); + scripts = pkgJson.scripts ?? {}; + } catch { + // Missing/unreadable package.json: fall back to tsc default + attempt build. + } + } + meta.set(pkg, { + dir: dir ?? null, + hasTypecheck: typeof scripts.typecheck === "string", + hasBuild: typeof scripts.build === "string", + }); + } + return meta; +} + +/** + * Resolve the affected package set for the current working tree, reusing + * test-changed's git-diff + workspace resolution. Returns both the package list + * and a human note describing why the set is what it is (no base, no changes, + * unmappable path, etc.) so the CLI can explain a boot-smoke-only run. + * + * @returns {{ packages: string[], packageDirByName: Map, note: string }} + */ +export function resolveAffectedForVerify() { + const baseBranch = getBaseBranch(); + const comparisonBase = detectComparisonBase(baseBranch); + const workspacePackages = listWorkspacePackageInfos(); + const packageNameByDir = listWorkspacePackages(workspacePackages); + const packageDirByName = buildPackageDirByName(workspacePackages); + + if (!comparisonBase) { + return { packages: [], packageDirByName, note: `could not resolve merge-base with ${baseBranch}; running boot smoke only` }; + } + const changedFiles = changedFilesSince(comparisonBase); + if (changedFiles === null) { + return { packages: [], packageDirByName, note: "failed to read git diff; running boot smoke only" }; + } + if (changedFiles.length === 0) { + return { packages: [], packageDirByName, note: "no changes detected against base; running boot smoke only" }; + } + const affected = resolveAffectedPackages(changedFiles, packageNameByDir); + if (affected === null) { + return { packages: [], packageDirByName, note: "changed file did not map to a workspace package; running boot smoke only" }; + } + if (affected.length === 0) { + return { packages: [], packageDirByName, note: "no affected workspace package (root/docs-only changes); running boot smoke only" }; + } + return { packages: affected, packageDirByName, note: `affected packages: ${affected.join(", ")}` }; +} + +/** + * Run one step under the wall-clock watchdog (class "changed"). Streams the + * child's output (stdio inherit) and throws with an `.exitCode` on the first + * failure/timeout/signal so the caller exits nonzero immediately. + */ +export async function runStep(step, { spawnFn = spawn, log = console.log, errLog = console.error } = {}) { + const budgetMs = deriveBudgetMs({ klass: step.klass ?? "changed" }); + log(`\n[verify:fast] -> ${step.label}`); + log(`[verify:fast] ${step.command} ${step.args.join(" ")} (budget ${Math.round(budgetMs / 1000)}s)`); + const startedAt = Date.now(); + const { code, signal, timedOut } = await runWithWatchdog({ + command: step.command, + args: step.args, + env: process.env, + cwd: repoRoot, + budgetMs, + label: step.label, + log: errLog, + spawn: spawnFn, + }); + const elapsedS = ((Date.now() - startedAt) / 1000).toFixed(1); + if (timedOut || signal || code !== 0) { + const reason = timedOut ? `watchdog timeout (${budgetMs}ms)` : signal ? `signal ${signal}` : `exit code ${code}`; + const error = new Error(`[verify:fast] FAILED: ${step.label} (${reason}) after ${elapsedS}s`); + error.exitCode = timedOut ? 124 : signal ? 1 : code ?? 1; + throw error; + } + log(`[verify:fast] OK ${step.label} (${elapsedS}s)`); +} + +export async function main() { + const overallStart = Date.now(); + console.log("[verify:fast] test-free verification: typecheck + build (scoped to changed packages) + boot smoke."); + + const { packages, packageDirByName, note } = resolveAffectedForVerify(); + console.log(`[verify:fast] ${note}`); + + const packageMeta = readPackageMeta(packages, packageDirByName); + const { eligiblePackages, excludedPackages, steps } = buildVerifyPlan({ + packages, + packageMeta, + bootSmokeScriptPath, + }); + + if (excludedPackages.length > 0) { + console.log(`[verify:fast] skipping excluded packages (also excluded from root build/typecheck): ${excludedPackages.join(", ")}`); + } + if (eligiblePackages.length === 0) { + console.log("[verify:fast] no scoped packages to verify; running boot smoke only."); + } else { + console.log(`[verify:fast] scoped to: ${eligiblePackages.join(", ")}`); + } + console.log(`[verify:fast] plan: ${steps.map((s) => s.id).join(" -> ")}`); + + for (const step of steps) { + await runStep(step); + } + + const elapsedS = ((Date.now() - overallStart) / 1000).toFixed(1); + console.log(`\n[verify:fast] PASS — ${steps.length} step(s) green in ${elapsedS}s (no tests run).`); +} + +const currentFilePath = fileURLToPath(import.meta.url); +if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) { + main().catch((error) => { + if (error?.message) console.error(error.message); + if (error?.exitCode) process.exit(error.exitCode); + console.error(error); + process.exit(1); + }); +}