diff --git a/docs/test-feedback-loop-baseline.md b/docs/test-feedback-loop-baseline.md new file mode 100644 index 0000000000..ce17e52ed9 --- /dev/null +++ b/docs/test-feedback-loop-baseline.md @@ -0,0 +1,48 @@ +# Test feedback-loop baseline + +> Publish this page's latest-cycle summary in #leads each week. The objective is signal-per-second: keep the merge gate thin, keep `pnpm test` flat or faster, and ratchet flaky/low-signal tests toward rescue or deletion. + +## Latest #leads summary + +- Cycle: **2026-W25** (2026-06-18T02:11:11.998Z) +- Gate suite wall-time: **7.2s** (trend: n/a) +- `pnpm test` wall-time: **36.9s** (trend: n/a) +- Flake/quarantine count: **5** ledger entries across **4** files +- Timing snapshot source: `scripts/test-timings.json` captured at **2026-06-03T23:45:49.672Z** + +## Slowest 20 test files + +| Rank | File | Package | Duration | +|---:|---|---|---:| +| 1 | `packages/engine/src/__tests__/reliability-interactions/shared-branch-group-lifecycle.test.ts` | @fusion/engine | 13.9s | +| 2 | `packages/core/src/__tests__/agent-store.test.ts` | @fusion/core | 11.6s | +| 3 | `packages/dashboard/src/__tests__/routes-agents.test.ts` | @fusion/dashboard | 11.2s | +| 4 | `packages/core/src/__tests__/mission-store.test.ts` | @fusion/core | 10.7s | +| 5 | `packages/core/src/__tests__/db.test.ts` | @fusion/core | 10.1s | +| 6 | `packages/dashboard/src/__tests__/routes-git.test.ts` | @fusion/dashboard | 9.4s | +| 7 | `packages/engine/src/__tests__/reliability-interactions/branch-group-automerge-precedence.test.ts` | @fusion/engine | 9.0s | +| 8 | `packages/engine/src/__tests__/merger-ai.test.ts` | @fusion/engine | 8.7s | +| 9 | `packages/engine/src/__tests__/reliability-interactions/branch-group-merge-routing.test.ts` | @fusion/engine | 8.4s | +| 10 | `packages/engine/src/__tests__/reliability-interactions/branch-group-promotion-gate.test.ts` | @fusion/engine | 8.4s | +| 11 | `packages/core/src/__tests__/task-documents.test.ts` | @fusion/core | 8.3s | +| 12 | `packages/engine/src/runtimes/__tests__/in-process-runtime.test.ts` | @fusion/engine | 7.8s | +| 13 | `packages/cli/src/__tests__/extension.test.ts` | @runfusion/fusion | 7.0s | +| 14 | `packages/core/src/__tests__/run-audit.test.ts` | @fusion/core | 6.9s | +| 15 | `packages/engine/src/__tests__/reliability-interactions/branch-group-promotion.test.ts` | @fusion/engine | 6.1s | +| 16 | `packages/dashboard/src/__tests__/routes-planning.test.ts` | @fusion/dashboard | 5.6s | +| 17 | `packages/core/src/__tests__/store-merge-queue.test.ts` | @fusion/core | 5.2s | +| 18 | `packages/dashboard/app/components/__tests__/FileEditor.test.tsx` | @fusion/dashboard | 5.1s | +| 19 | `packages/engine/src/__tests__/reliability-interactions/integration-worktree-state.test.ts` | @fusion/engine | 4.9s | +| 20 | `packages/engine/src/__tests__/self-healing-already-merged.real-git.test.ts` | @fusion/engine | 4.9s | + +## Trend + +| Cycle | Captured at | Gate suite | `pnpm test` | Quarantine entries | Quarantined files | +|---|---|---:|---:|---:|---:| +| 2026-W25 | 2026-06-18T02:11:11.998Z | 7.2s | 36.9s | 5 | 4 | + +## Operating rules + +- Record a new row weekly with `node scripts/test-feedback-baseline.mjs --record --gate-ms --test-ms ` after running `pnpm test:gate` and `pnpm test`. +- Use the slowest-file list as the candidate queue for FN-5048 rewrites or deletion-ratchet review; do not add coverage for its own sake. +- Quarantined tests remain on the 14-day rescue-or-delete clock in `scripts/lib/test-quarantine.json`; deleting a low-signal expired test is a valid positive outcome. diff --git a/docs/test-feedback-loop-baselines.json b/docs/test-feedback-loop-baselines.json new file mode 100644 index 0000000000..e71abe868a --- /dev/null +++ b/docs/test-feedback-loop-baselines.json @@ -0,0 +1,122 @@ +{ + "baselines": [ + { + "capturedAt": "2026-06-18T02:11:11.998Z", + "cycle": "2026-W25", + "gateWallTimeMs": 7200, + "pnpmTestWallTimeMs": 36900, + "timingSnapshotCapturedAt": "2026-06-03T23:45:49.672Z", + "slowest20": [ + { + "packageName": "@fusion/engine", + "file": "packages/engine/src/__tests__/reliability-interactions/shared-branch-group-lifecycle.test.ts", + "durationMs": 13900 + }, + { + "packageName": "@fusion/core", + "file": "packages/core/src/__tests__/agent-store.test.ts", + "durationMs": 11600 + }, + { + "packageName": "@fusion/dashboard", + "file": "packages/dashboard/src/__tests__/routes-agents.test.ts", + "durationMs": 11200 + }, + { + "packageName": "@fusion/core", + "file": "packages/core/src/__tests__/mission-store.test.ts", + "durationMs": 10700 + }, + { + "packageName": "@fusion/core", + "file": "packages/core/src/__tests__/db.test.ts", + "durationMs": 10100 + }, + { + "packageName": "@fusion/dashboard", + "file": "packages/dashboard/src/__tests__/routes-git.test.ts", + "durationMs": 9400 + }, + { + "packageName": "@fusion/engine", + "file": "packages/engine/src/__tests__/reliability-interactions/branch-group-automerge-precedence.test.ts", + "durationMs": 9000 + }, + { + "packageName": "@fusion/engine", + "file": "packages/engine/src/__tests__/merger-ai.test.ts", + "durationMs": 8700 + }, + { + "packageName": "@fusion/engine", + "file": "packages/engine/src/__tests__/reliability-interactions/branch-group-merge-routing.test.ts", + "durationMs": 8400 + }, + { + "packageName": "@fusion/engine", + "file": "packages/engine/src/__tests__/reliability-interactions/branch-group-promotion-gate.test.ts", + "durationMs": 8400 + }, + { + "packageName": "@fusion/core", + "file": "packages/core/src/__tests__/task-documents.test.ts", + "durationMs": 8300 + }, + { + "packageName": "@fusion/engine", + "file": "packages/engine/src/runtimes/__tests__/in-process-runtime.test.ts", + "durationMs": 7800 + }, + { + "packageName": "@runfusion/fusion", + "file": "packages/cli/src/__tests__/extension.test.ts", + "durationMs": 7000 + }, + { + "packageName": "@fusion/core", + "file": "packages/core/src/__tests__/run-audit.test.ts", + "durationMs": 6900 + }, + { + "packageName": "@fusion/engine", + "file": "packages/engine/src/__tests__/reliability-interactions/branch-group-promotion.test.ts", + "durationMs": 6100 + }, + { + "packageName": "@fusion/dashboard", + "file": "packages/dashboard/src/__tests__/routes-planning.test.ts", + "durationMs": 5600 + }, + { + "packageName": "@fusion/core", + "file": "packages/core/src/__tests__/store-merge-queue.test.ts", + "durationMs": 5200 + }, + { + "packageName": "@fusion/dashboard", + "file": "packages/dashboard/app/components/__tests__/FileEditor.test.tsx", + "durationMs": 5100 + }, + { + "packageName": "@fusion/engine", + "file": "packages/engine/src/__tests__/reliability-interactions/integration-worktree-state.test.ts", + "durationMs": 4900 + }, + { + "packageName": "@fusion/engine", + "file": "packages/engine/src/__tests__/self-healing-already-merged.real-git.test.ts", + "durationMs": 4900 + } + ], + "flakeCount": 5, + "uniqueQuarantinedFileCount": 4, + "quarantinedFiles": [ + "packages/core/src/__tests__/task-list-format.test.ts", + "packages/core/src/__tests__/test-project.test.ts", + "plugins/fusion-plugin-compound-engineering/src/__tests__/sync.test.ts", + "plugins/fusion-plugin-compound-engineering/src/__tests__/work-bridge.test.ts" + ], + "notes": "FN-6612 first-cycle publication artifact measured in this worktree: pnpm test:gate 7.2s; pnpm test 36.9s." + } + ] +} diff --git a/docs/testing.md b/docs/testing.md index a4bc7e2440..76ddcb840e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -10,6 +10,18 @@ CI blocks PRs on exactly four checks (`.github/workflows/pr-checks.yml`): **Lint Gate membership is the explicit allow-list in `packages/engine/vitest.config.ts` (`engine-core` project). Admission requires evidence of value (the test catches real regressions); tests never graduate in by default. A flaky gate test is evicted by deleting its allow-list line — the eviction PR does not need the flaky test to pass. The whole `engine-core` project must stay under ~60s wall-clock. +## Weekly signal-per-second baseline + +Refresh and publish the test feedback-loop baseline in #leads once per weekly cycle: + +```bash +pnpm test:gate # capture wall-time in ms +pnpm test # capture wall-time in ms +node scripts/test-feedback-baseline.mjs --record --gate-ms --test-ms --print-leads +``` + +The generated `docs/test-feedback-loop-baseline.md` is the publication artifact: it reports gate wall-time, `pnpm test` wall-time, the slowest 20 test files from `scripts/test-timings.json`, and the current quarantine/flake count from `scripts/lib/test-quarantine.json`. Keep the trend flat or net-negative; use the slowest-file list to drive FN-5048 rewrites and deletion-ratchet reviews instead of adding low-signal coverage. + **The gate's blind spot, stated honestly:** typecheck + build + boot smoke + curated suite does not run the union suite a merge creates. Logic regressions outside the curated set land non-blocking by design — that is the accepted trade: the old broad gate caught no recalled real bugs while consuming ~70% of shipping time in flake triage. ## Required workspace gates diff --git a/package.json b/package.json index b91f3b78de..88c3bae98c 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "test:scripts": "node --test scripts/__tests__/*.test.mjs", "fn:cache-stats": "node scripts/cache-stats.mjs", "test:full": "node scripts/test-changed.mjs --full --no-cache && pnpm --filter @fusion/engine test:slow", + "test:feedback-baseline": "node scripts/test-feedback-baseline.mjs", "test:ci:shard": "node scripts/ci-test-shard.mjs", "test:serial": "FUSION_TEST_CONCURRENCY=1 FUSION_TEST_WORKSPACE_CONCURRENCY=1 pnpm test:full", "test:fast": "FUSION_TEST_CONCURRENCY=4 FUSION_TEST_WORKSPACE_CONCURRENCY=4 pnpm test:full", diff --git a/scripts/__tests__/test-feedback-baseline.test.mjs b/scripts/__tests__/test-feedback-baseline.test.mjs new file mode 100644 index 0000000000..ab354a1cfb --- /dev/null +++ b/scripts/__tests__/test-feedback-baseline.test.mjs @@ -0,0 +1,90 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { mkdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { + collectFlakeSummary, + collectSlowestFiles, + createBaseline, + DEFAULT_BASELINES_PATH, + DEFAULT_MARKDOWN_PATH, + DEFAULT_QUARANTINE_PATH, + DEFAULT_TIMINGS_PATH, + main, + renderMarkdown, +} from "../test-feedback-baseline.mjs"; + +function writeJson(root, relativePath, value) { + const absolutePath = path.join(root, relativePath); + mkdirSync(path.dirname(absolutePath), { recursive: true }); + writeFileSync(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +test("collectSlowestFiles ranks timing snapshot entries across packages", () => { + const rows = collectSlowestFiles({ + packages: { + "@fusion/a": { files: { "a-fast.test.ts": 20, "a-slow.test.ts": 2000 } }, + "@fusion/b": { files: { "b-medium.test.ts": 1000 } }, + }, + }, 2); + + assert.deepEqual(rows, [ + { packageName: "@fusion/a", file: "a-slow.test.ts", durationMs: 2000 }, + { packageName: "@fusion/b", file: "b-medium.test.ts", durationMs: 1000 }, + ]); +}); + +test("collectFlakeSummary counts ledger entries and unique quarantined files", () => { + const summary = collectFlakeSummary({ entries: [ + { file: "one.test.ts" }, + { file: "one.test.ts" }, + { file: "two.test.ts" }, + ] }); + + assert.equal(summary.flakeCount, 3); + assert.equal(summary.uniqueQuarantinedFileCount, 2); + assert.deepEqual(summary.quarantinedFiles, ["one.test.ts", "two.test.ts"]); +}); + +test("renderMarkdown includes #leads summary, trend, and slowest files", () => { + const baseline = createBaseline({ + now: new Date("2026-06-17T18:00:00.000Z"), + gateWallTimeMs: 12_300, + pnpmTestWallTimeMs: 45_600, + timings: { capturedAt: "2026-06-17T17:00:00.000Z", packages: { "@fusion/core": { files: { "packages/core/src/__tests__/agent-store.test.ts": 11_600 } } } }, + quarantine: { entries: [{ file: "packages/core/src/__tests__/flake.test.ts" }] }, + }); + + const markdown = renderMarkdown([baseline]); + + assert.match(markdown, /Latest #leads summary/); + assert.match(markdown, /Gate suite wall-time: \*\*12\.3s\*\*/); + assert.match(markdown, /packages\/core\/src\/__tests__\/agent-store\.test\.ts/); + assert.match(markdown, /Quarantined tests remain on the 14-day rescue-or-delete clock/); +}); + +test("main records a baseline and writes the markdown publication artifact", async () => { + const root = mkdtempSync(path.join(tmpdir(), "fusion-test-feedback-baseline-")); + writeJson(root, DEFAULT_TIMINGS_PATH, { + capturedAt: "2026-06-17T17:00:00.000Z", + packages: { "@fusion/core": { files: { "slow.test.ts": 1500, "fast.test.ts": 100 } } }, + }); + writeJson(root, DEFAULT_QUARANTINE_PATH, { entries: [{ file: "slow.test.ts" }] }); + + const chunks = []; + const code = await main(["--record", "--gate-ms", "1000", "--test-ms", "2000", "--print-leads"], { + rootDir: root, + stdout: { write: (chunk) => chunks.push(String(chunk)) }, + stderr: { write: () => {} }, + }); + + assert.equal(code, 0); + assert.match(chunks.join(""), /gate 1\.0s, pnpm test 2\.0s/); + const store = JSON.parse(readFileSync(path.join(root, DEFAULT_BASELINES_PATH), "utf8")); + assert.equal(store.baselines.length, 1); + assert.equal(store.baselines[0].flakeCount, 1); + const markdown = readFileSync(path.join(root, DEFAULT_MARKDOWN_PATH), "utf8"); + assert.match(markdown, /slow\.test\.ts/); +}); diff --git a/scripts/test-feedback-baseline.mjs b/scripts/test-feedback-baseline.mjs new file mode 100644 index 0000000000..8a50ab8857 --- /dev/null +++ b/scripts/test-feedback-baseline.mjs @@ -0,0 +1,192 @@ +#!/usr/bin/env node + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const currentFilePath = fileURLToPath(import.meta.url); +const repoRoot = path.resolve(path.dirname(currentFilePath), ".."); + +export const DEFAULT_TIMINGS_PATH = "scripts/test-timings.json"; +export const DEFAULT_QUARANTINE_PATH = "scripts/lib/test-quarantine.json"; +export const DEFAULT_BASELINES_PATH = "docs/test-feedback-loop-baselines.json"; +export const DEFAULT_MARKDOWN_PATH = "docs/test-feedback-loop-baseline.md"; + +function readJson(relativePath, fallback = null, rootDir = repoRoot) { + const absolutePath = path.join(rootDir, relativePath); + if (!existsSync(absolutePath)) return fallback; + return JSON.parse(readFileSync(absolutePath, "utf8")); +} + +function writeJson(relativePath, value, rootDir = repoRoot) { + const absolutePath = path.join(rootDir, relativePath); + mkdirSync(path.dirname(absolutePath), { recursive: true }); + writeFileSync(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +function writeText(relativePath, value, rootDir = repoRoot) { + const absolutePath = path.join(rootDir, relativePath); + mkdirSync(path.dirname(absolutePath), { recursive: true }); + writeFileSync(absolutePath, value, "utf8"); +} + +function normalizeMs(value) { + if (value == null || value === "") return null; + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed < 0) { + throw new Error(`Expected a non-negative millisecond value, got ${value}`); + } + return Math.round(parsed); +} + +function formatDuration(ms) { + if (ms == null) return "pending measurement"; + const sign = ms < 0 ? "-" : ""; + const absoluteMs = Math.abs(ms); + if (absoluteMs < 1000) return `${sign}${absoluteMs}ms`; + const seconds = absoluteMs / 1000; + if (seconds < 60) return `${sign}${seconds.toFixed(1)}s`; + const minutes = Math.floor(seconds / 60); + const remaining = Math.round(seconds - minutes * 60); + return `${sign}${minutes}m ${String(remaining).padStart(2, "0")}s`; +} + +function isoWeek(date) { + const working = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); + const day = working.getUTCDay() || 7; + working.setUTCDate(working.getUTCDate() + 4 - day); + const yearStart = new Date(Date.UTC(working.getUTCFullYear(), 0, 1)); + const week = Math.ceil(((working - yearStart) / 86_400_000 + 1) / 7); + return `${working.getUTCFullYear()}-W${String(week).padStart(2, "0")}`; +} + +export function collectSlowestFiles(timings, limit = 20) { + const rows = []; + for (const [packageName, packageTiming] of Object.entries(timings?.packages ?? {})) { + for (const [file, durationMs] of Object.entries(packageTiming?.files ?? {})) { + rows.push({ packageName, file, durationMs: Number(durationMs) || 0 }); + } + } + + rows.sort((a, b) => b.durationMs - a.durationMs || a.file.localeCompare(b.file)); + return rows.slice(0, limit); +} + +export function collectFlakeSummary(quarantine) { + const entries = Array.isArray(quarantine?.entries) ? quarantine.entries : []; + const uniqueFiles = [...new Set(entries.map((entry) => entry.file).filter(Boolean))].sort(); + return { + flakeCount: entries.length, + uniqueQuarantinedFileCount: uniqueFiles.length, + quarantinedFiles: uniqueFiles, + }; +} + +export function createBaseline({ now = new Date(), gateWallTimeMs = null, pnpmTestWallTimeMs = null, timings, quarantine, notes = "" } = {}) { + const slowest20 = collectSlowestFiles(timings, 20); + const flakeSummary = collectFlakeSummary(quarantine); + return { + capturedAt: now.toISOString(), + cycle: isoWeek(now), + gateWallTimeMs: normalizeMs(gateWallTimeMs), + pnpmTestWallTimeMs: normalizeMs(pnpmTestWallTimeMs), + timingSnapshotCapturedAt: timings?.capturedAt ?? null, + slowest20, + ...flakeSummary, + notes, + }; +} + +function trendDelta(latest, previous, field) { + if (!previous || latest?.[field] == null || previous?.[field] == null) return "n/a"; + const delta = latest[field] - previous[field]; + const sign = delta > 0 ? "+" : ""; + return `${sign}${formatDuration(delta)}`; +} + +export function renderMarkdown(baselines) { + const sorted = [...baselines].sort((a, b) => String(a.capturedAt).localeCompare(String(b.capturedAt))); + const latest = sorted.at(-1); + const previous = sorted.at(-2); + + const slowRows = (latest?.slowest20 ?? []) + .map((row, index) => `| ${index + 1} | \`${row.file}\` | ${row.packageName} | ${formatDuration(row.durationMs)} |`) + .join("\n"); + const trendRows = sorted + .map((row) => `| ${row.cycle} | ${row.capturedAt} | ${formatDuration(row.gateWallTimeMs)} | ${formatDuration(row.pnpmTestWallTimeMs)} | ${row.flakeCount ?? 0} | ${row.uniqueQuarantinedFileCount ?? 0} |`) + .join("\n"); + + return `# Test feedback-loop baseline\n\n> Publish this page's latest-cycle summary in #leads each week. The objective is signal-per-second: keep the merge gate thin, keep \`pnpm test\` flat or faster, and ratchet flaky/low-signal tests toward rescue or deletion.\n\n## Latest #leads summary\n\n- Cycle: **${latest?.cycle ?? "none"}** (${latest?.capturedAt ?? "not captured"})\n- Gate suite wall-time: **${formatDuration(latest?.gateWallTimeMs)}** (trend: ${trendDelta(latest, previous, "gateWallTimeMs")})\n- \`pnpm test\` wall-time: **${formatDuration(latest?.pnpmTestWallTimeMs)}** (trend: ${trendDelta(latest, previous, "pnpmTestWallTimeMs")})\n- Flake/quarantine count: **${latest?.flakeCount ?? 0}** ledger entr${(latest?.flakeCount ?? 0) === 1 ? "y" : "ies"} across **${latest?.uniqueQuarantinedFileCount ?? 0}** file${(latest?.uniqueQuarantinedFileCount ?? 0) === 1 ? "" : "s"}\n- Timing snapshot source: \`${DEFAULT_TIMINGS_PATH}\` captured at **${latest?.timingSnapshotCapturedAt ?? "unknown"}**\n\n## Slowest 20 test files\n\n| Rank | File | Package | Duration |\n|---:|---|---|---:|\n${slowRows || "| — | — | — | — |"}\n\n## Trend\n\n| Cycle | Captured at | Gate suite | \`pnpm test\` | Quarantine entries | Quarantined files |\n|---|---|---:|---:|---:|---:|\n${trendRows || "| — | — | — | — | — | — |"}\n\n## Operating rules\n\n- Record a new row weekly with \`node scripts/test-feedback-baseline.mjs --record --gate-ms --test-ms \` after running \`pnpm test:gate\` and \`pnpm test\`.\n- Use the slowest-file list as the candidate queue for FN-5048 rewrites or deletion-ratchet review; do not add coverage for its own sake.\n- Quarantined tests remain on the 14-day rescue-or-delete clock in \`scripts/lib/test-quarantine.json\`; deleting a low-signal expired test is a valid positive outcome.\n`; +} + +function parseArgs(argv) { + const args = { record: false, printLeads: false, gateMs: null, testMs: null, notes: "" }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--record") args.record = true; + else if (arg === "--print-leads") args.printLeads = true; + else if (arg === "--gate-ms") args.gateMs = argv[++index]; + else if (arg === "--test-ms") args.testMs = argv[++index]; + else if (arg === "--notes") args.notes = argv[++index] ?? ""; + else if (arg === "--help" || arg === "-h") args.help = true; + else throw new Error(`Unknown argument: ${arg}`); + } + return args; +} + +function renderLeadsSummary(baselines) { + const latest = [...baselines].sort((a, b) => String(a.capturedAt).localeCompare(String(b.capturedAt))).at(-1); + if (!latest) return "No test feedback-loop baseline has been recorded yet."; + const topFive = (latest.slowest20 ?? []).slice(0, 5).map((row, index) => `${index + 1}. ${row.file} (${formatDuration(row.durationMs)})`).join("; "); + return `Test feedback-loop ${latest.cycle}: gate ${formatDuration(latest.gateWallTimeMs)}, pnpm test ${formatDuration(latest.pnpmTestWallTimeMs)}, quarantine ledger ${latest.flakeCount} entries/${latest.uniqueQuarantinedFileCount} files. Slowest files: ${topFive || "none"}.`; +} + +export async function main(argv = process.argv.slice(2), { rootDir = repoRoot, stdout = process.stdout, stderr = process.stderr } = {}) { + let args; + try { + args = parseArgs(argv); + } catch (err) { + stderr.write(`${err.message}\n`); + return 1; + } + + if (args.help) { + stdout.write("Usage: node scripts/test-feedback-baseline.mjs [--record --gate-ms --test-ms ] [--print-leads]\n"); + return 0; + } + + const store = readJson(DEFAULT_BASELINES_PATH, { baselines: [] }, rootDir); + const baselines = Array.isArray(store?.baselines) ? store.baselines : []; + + if (args.record || baselines.length === 0) { + const timings = readJson(DEFAULT_TIMINGS_PATH, { packages: {} }, rootDir); + const quarantine = readJson(DEFAULT_QUARANTINE_PATH, { entries: [] }, rootDir); + baselines.push(createBaseline({ + gateWallTimeMs: args.gateMs, + pnpmTestWallTimeMs: args.testMs, + timings, + quarantine, + notes: args.notes, + })); + writeJson(DEFAULT_BASELINES_PATH, { baselines }, rootDir); + } + + const markdown = renderMarkdown(baselines); + writeText(DEFAULT_MARKDOWN_PATH, markdown, rootDir); + + if (args.printLeads) { + stdout.write(`${renderLeadsSummary(baselines)}\n`); + } else { + stdout.write(`Updated ${DEFAULT_MARKDOWN_PATH}\n`); + } + return 0; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + /* + FNXC:TestFeedbackVelocity 2026-06-17-18:30: + The CEO mandate requires a weekly #leads-visible baseline for test signal-per-second, not more coverage. Keep this script stdlib-only so any engineer or scheduled job can refresh gate time, pnpm-test time, slowest files, and quarantine count without booting Fusion services. + */ + const exitCode = await main(); + process.exitCode = exitCode; +}