mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-04 05:52:13 +03:00
Compare commits
2 Commits
fix/v3851-
...
fix/v3851-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abb65b42da | ||
|
|
8870cd1bc7 |
@@ -19,53 +19,15 @@ import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { diffAgainstBaseline, parseTscOutput } from "./typecheckBaseline.mjs";
|
||||
|
||||
export { diffAgainstBaseline, parseTscOutput } from "./typecheckBaseline.mjs";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const TSCONFIG = path.join(ROOT, "tsconfig.typecheck-api.json");
|
||||
const BASELINE_PATH = path.join(ROOT, "config/quality/api-typecheck-baseline.json");
|
||||
const UPDATE = process.argv.includes("--update");
|
||||
|
||||
const TSC_ERROR_LINE = /^(.+?)\((\d+),(\d+)\): error (TS\d+):/;
|
||||
|
||||
export function parseTscOutput(raw) {
|
||||
const counts = {};
|
||||
for (const line of String(raw).split("\n")) {
|
||||
const match = TSC_ERROR_LINE.exec(line);
|
||||
if (!match) continue;
|
||||
const [, file, , , code] = match;
|
||||
if (!counts[file]) counts[file] = {};
|
||||
counts[file][code] = (counts[file][code] || 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function diffAgainstBaseline(live, baseline) {
|
||||
const regressions = [];
|
||||
const improvements = [];
|
||||
|
||||
for (const [file, codes] of Object.entries(live)) {
|
||||
for (const [code, liveCount] of Object.entries(codes)) {
|
||||
const baselineCount = (baseline[file] && baseline[file][code]) || 0;
|
||||
if (liveCount > baselineCount) {
|
||||
regressions.push({ file, code, liveCount, baselineCount });
|
||||
} else if (liveCount < baselineCount) {
|
||||
improvements.push({ file, code, liveCount, baselineCount });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [file, codes] of Object.entries(baseline)) {
|
||||
for (const [code, baselineCount] of Object.entries(codes)) {
|
||||
const liveCount = (live[file] && live[file][code]) || 0;
|
||||
if (liveCount === 0 && baselineCount > 0) {
|
||||
improvements.push({ file, code, liveCount: 0, baselineCount });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { regressions, improvements };
|
||||
}
|
||||
|
||||
function runTsc() {
|
||||
try {
|
||||
return execFileSync(
|
||||
@@ -117,7 +79,9 @@ function main() {
|
||||
`[api-typecheck] ${improvements.length} baselined error(s) no longer present ` +
|
||||
`— run 'node scripts/check/check-api-typecheck.mjs --update' to ratchet the baseline down:\n` +
|
||||
improvements
|
||||
.map((i) => ` - ${i.file} ${i.code} (baseline ${i.baselineCount} -> live ${i.liveCount})`)
|
||||
.map(
|
||||
(i) => ` - ${i.file} ${i.code} (baseline ${i.baselineCount} -> live ${i.liveCount})`
|
||||
)
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,74 +22,15 @@ import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { diffAgainstBaseline, parseTscOutput } from "./typecheckBaseline.mjs";
|
||||
|
||||
export { diffAgainstBaseline, parseTscOutput } from "./typecheckBaseline.mjs";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const TSCONFIG = path.join(ROOT, "open-sse", "tsconfig.json");
|
||||
const BASELINE_PATH = path.join(ROOT, "config/quality/open-sse-typecheck-baseline.json");
|
||||
const UPDATE = process.argv.includes("--update");
|
||||
|
||||
// Matches tsc --pretty false output lines, e.g.:
|
||||
// src/app/api/v1/chat/route.ts(12,7): error TS2304: Cannot find name 'bar'.
|
||||
// open-sse/handlers/chatCore.ts(45,3): error TS7053: Element implicitly has an 'any'...
|
||||
const TSC_ERROR_LINE = /^(.+?)\((\d+),(\d+)\): error (TS\d+):/;
|
||||
|
||||
/**
|
||||
* Parses raw `tsc --pretty false` stdout into a nested count map:
|
||||
* { "<relative file path>": { "<TS code>": <count> } }
|
||||
*
|
||||
* Pure/exported for unit testing against synthetic tsc output — no child
|
||||
* process involved here.
|
||||
*/
|
||||
export function parseTscOutput(raw) {
|
||||
const counts = {};
|
||||
const lines = String(raw).split("\n");
|
||||
for (const line of lines) {
|
||||
const match = TSC_ERROR_LINE.exec(line);
|
||||
if (!match) continue;
|
||||
const [, file, , , code] = match;
|
||||
if (!counts[file]) counts[file] = {};
|
||||
counts[file][code] = (counts[file][code] || 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares live (file, TS code) error counts against a frozen baseline.
|
||||
* Returns `{ regressions, improvements }`:
|
||||
* - regressions: entries where live count > baselined count (or the pair is
|
||||
* entirely new/unbaselined) — these fail the gate.
|
||||
* - improvements: entries where live count < baselined count — informational,
|
||||
* do not fail (use --update to ratchet the baseline down).
|
||||
*
|
||||
* Exported for unit testing.
|
||||
*/
|
||||
export function diffAgainstBaseline(live, baseline) {
|
||||
const regressions = [];
|
||||
const improvements = [];
|
||||
|
||||
for (const [file, codes] of Object.entries(live)) {
|
||||
for (const [code, liveCount] of Object.entries(codes)) {
|
||||
const baselineCount = (baseline[file] && baseline[file][code]) || 0;
|
||||
if (liveCount > baselineCount) {
|
||||
regressions.push({ file, code, liveCount, baselineCount });
|
||||
} else if (liveCount < baselineCount) {
|
||||
improvements.push({ file, code, liveCount, baselineCount });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [file, codes] of Object.entries(baseline)) {
|
||||
for (const [code, baselineCount] of Object.entries(codes)) {
|
||||
const liveCount = (live[file] && live[file][code]) || 0;
|
||||
if (liveCount === 0 && baselineCount > 0) {
|
||||
improvements.push({ file, code, liveCount: 0, baselineCount });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { regressions, improvements };
|
||||
}
|
||||
|
||||
function runTsc() {
|
||||
try {
|
||||
const stdout = execFileSync(
|
||||
@@ -143,7 +84,9 @@ function main() {
|
||||
`[open-sse-typecheck] ${improvements.length} baselined error(s) no longer present ` +
|
||||
`— run 'node scripts/check/check-open-sse-typecheck.mjs --update' to ratchet the baseline down:\n` +
|
||||
improvements
|
||||
.map((i) => ` - ${i.file} ${i.code} (baseline ${i.baselineCount} -> live ${i.liveCount})`)
|
||||
.map(
|
||||
(i) => ` - ${i.file} ${i.code} (baseline ${i.baselineCount} -> live ${i.liveCount})`
|
||||
)
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
91
scripts/check/typecheckBaseline.mjs
Normal file
91
scripts/check/typecheckBaseline.mjs
Normal file
@@ -0,0 +1,91 @@
|
||||
// Shared parsing and frozen-baseline comparison for the scoped TypeScript gates.
|
||||
|
||||
const TSC_ERROR_LINE = /^(.+?)\((\d+),(\d+)\): error (TS\d+):/;
|
||||
const TS_CODE = /^TS\d+$/;
|
||||
const UNSAFE_PROPERTY_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
||||
|
||||
function isPlainObject(value) {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return prototype === Object.prototype || prototype === null;
|
||||
}
|
||||
|
||||
function normalizeDiagnosticCounts(value, label) {
|
||||
if (!isPlainObject(value)) {
|
||||
throw new TypeError(`${label} must be a plain object`);
|
||||
}
|
||||
|
||||
const normalized = Object.create(null);
|
||||
for (const [file, codes] of Object.entries(value)) {
|
||||
if (UNSAFE_PROPERTY_KEYS.has(file)) {
|
||||
throw new TypeError(`${label} contains unsupported property key "${file}"`);
|
||||
}
|
||||
if (file.startsWith("_")) continue;
|
||||
if (!isPlainObject(codes)) {
|
||||
throw new TypeError(`${label} entry "${file}" must be a plain object`);
|
||||
}
|
||||
|
||||
const normalizedCodes = Object.create(null);
|
||||
for (const [code, count] of Object.entries(codes)) {
|
||||
if (!TS_CODE.test(code)) {
|
||||
throw new TypeError(`${label} entry "${file}" has invalid TypeScript code "${code}"`);
|
||||
}
|
||||
if (!Number.isFinite(count) || !Number.isInteger(count) || count < 0) {
|
||||
throw new TypeError(
|
||||
`${label} entry "${file}" code "${code}" must be a finite nonnegative integer`
|
||||
);
|
||||
}
|
||||
normalizedCodes[code] = count;
|
||||
}
|
||||
normalized[file] = normalizedCodes;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/** Parse `tsc --pretty false` output into per-file/per-code diagnostic counts. */
|
||||
export function parseTscOutput(raw) {
|
||||
const counts = {};
|
||||
for (const line of String(raw).split("\n")) {
|
||||
const match = TSC_ERROR_LINE.exec(line);
|
||||
if (!match) continue;
|
||||
const [, file, , , code] = match;
|
||||
if (!counts[file]) counts[file] = {};
|
||||
counts[file][code] = (counts[file][code] || 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare live diagnostic counts with a frozen baseline.
|
||||
*
|
||||
* Underscore-prefixed top-level keys are reserved for baseline metadata and
|
||||
* never participate in the diagnostic comparison.
|
||||
*/
|
||||
export function diffAgainstBaseline(live, baseline) {
|
||||
const liveCounts = normalizeDiagnosticCounts(live, "live diagnostics");
|
||||
const baselineCounts = normalizeDiagnosticCounts(baseline, "typecheck baseline");
|
||||
const regressions = [];
|
||||
const improvements = [];
|
||||
|
||||
for (const [file, codes] of Object.entries(liveCounts)) {
|
||||
for (const [code, liveCount] of Object.entries(codes)) {
|
||||
const baselineCount = baselineCounts[file]?.[code] ?? 0;
|
||||
if (liveCount > baselineCount) {
|
||||
regressions.push({ file, code, liveCount, baselineCount });
|
||||
} else if (liveCount < baselineCount) {
|
||||
improvements.push({ file, code, liveCount, baselineCount });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [file, codes] of Object.entries(baselineCounts)) {
|
||||
for (const [code, baselineCount] of Object.entries(codes)) {
|
||||
const liveCodes = liveCounts[file];
|
||||
if (!Object.hasOwn(liveCodes ?? {}, code) && baselineCount > 0) {
|
||||
improvements.push({ file, code, liveCount: 0, baselineCount });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { regressions, improvements };
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
parseTscOutput,
|
||||
diffAgainstBaseline,
|
||||
} from "../../../scripts/check/check-api-typecheck.mjs";
|
||||
import { diffAgainstBaseline as diffOpenSseAgainstBaseline } from "../../../scripts/check/check-open-sse-typecheck.mjs";
|
||||
|
||||
test("parseTscOutput: parses an API-route TS2554 regression", () => {
|
||||
const raw =
|
||||
@@ -85,3 +86,130 @@ test("diffAgainstBaseline: reports a disappeared diagnostic as an improvement",
|
||||
assert.equal(improvements[0].liveCount, 0);
|
||||
assert.equal(improvements[0].baselineCount, 2);
|
||||
});
|
||||
|
||||
test("diffAgainstBaseline: ignores underscore-prefixed baseline metadata", () => {
|
||||
const baseline = {
|
||||
_relax_velocity_2026_08_30:
|
||||
"per-file TS diagnostic counts raised by 20% (289 -> 455); velocity phase",
|
||||
"src/app/api/foo/route.ts": { TS2339: 1 },
|
||||
};
|
||||
const live = { "src/app/api/foo/route.ts": { TS2339: 1 } };
|
||||
|
||||
for (const compare of [diffAgainstBaseline, diffOpenSseAgainstBaseline]) {
|
||||
assert.deepEqual(compare(live, baseline), {
|
||||
regressions: [],
|
||||
improvements: [],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("diffAgainstBaseline: rejects a string in place of a real file diagnostic map", () => {
|
||||
const malformedBaseline = {
|
||||
"src/app/api/foo/route.ts": "TS2339: 1",
|
||||
};
|
||||
|
||||
assert.throws(
|
||||
() => diffAgainstBaseline({}, malformedBaseline),
|
||||
/src\/app\/api\/foo\/route\.ts.*plain object/
|
||||
);
|
||||
assert.throws(
|
||||
() => diffOpenSseAgainstBaseline({}, malformedBaseline),
|
||||
/src\/app\/api\/foo\/route\.ts.*plain object/
|
||||
);
|
||||
});
|
||||
|
||||
test("diffAgainstBaseline: rejects non-plain roots and file maps", () => {
|
||||
const inheritedRoot = Object.create({
|
||||
"src/app/api/inherited/route.ts": { TS2339: 1 },
|
||||
});
|
||||
const inheritedFileMap = Object.create({ TS2339: 1 });
|
||||
|
||||
for (const malformedBaseline of [[], "not an object", null, inheritedRoot]) {
|
||||
assert.throws(
|
||||
() => diffAgainstBaseline({}, malformedBaseline),
|
||||
/typecheck baseline must be a plain object/
|
||||
);
|
||||
}
|
||||
for (const malformedFileMap of [[], null, inheritedFileMap]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
diffAgainstBaseline(
|
||||
{},
|
||||
{
|
||||
"src/app/api/foo/route.ts": malformedFileMap,
|
||||
}
|
||||
),
|
||||
/src\/app\/api\/foo\/route\.ts.*plain object/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("diffAgainstBaseline: rejects prototype property keys", () => {
|
||||
const malformedBaseline = JSON.parse('{"__proto__":{"TS2339":1}}');
|
||||
|
||||
assert.throws(
|
||||
() => diffAgainstBaseline({}, malformedBaseline),
|
||||
/unsupported property key "__proto__"/
|
||||
);
|
||||
});
|
||||
|
||||
test("diffAgainstBaseline: rejects non-TypeScript diagnostic keys", () => {
|
||||
for (const code of ["2339", "TSX2339", "TS23x", "constructor"]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
diffAgainstBaseline(
|
||||
{},
|
||||
{
|
||||
"src/app/api/foo/route.ts": { [code]: 1 },
|
||||
}
|
||||
),
|
||||
/invalid TypeScript code/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("diffAgainstBaseline: rejects invalid diagnostic counts", () => {
|
||||
for (const count of [Number.NaN, Number.POSITIVE_INFINITY, -1, 1.5, "1"]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
diffAgainstBaseline(
|
||||
{},
|
||||
{
|
||||
"src/app/api/foo/route.ts": { TS2339: count },
|
||||
}
|
||||
),
|
||||
/finite nonnegative integer/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("diffAgainstBaseline: validates live diagnostics with the same schema", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
diffAgainstBaseline(
|
||||
{ "src/app/api/foo/route.ts": { TS2339: -1 } },
|
||||
{ "src/app/api/foo/route.ts": { TS2339: 1 } }
|
||||
),
|
||||
/live diagnostics.*finite nonnegative integer/
|
||||
);
|
||||
});
|
||||
|
||||
test("diffAgainstBaseline: accepts zero counts without fabricating a second improvement", () => {
|
||||
assert.deepEqual(
|
||||
diffAgainstBaseline(
|
||||
{ "src/app/api/foo/route.ts": { TS2339: 0 } },
|
||||
{ "src/app/api/foo/route.ts": { TS2339: 1 } }
|
||||
),
|
||||
{
|
||||
regressions: [],
|
||||
improvements: [
|
||||
{
|
||||
file: "src/app/api/foo/route.ts",
|
||||
code: "TS2339",
|
||||
liveCount: 0,
|
||||
baselineCount: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user