feat(FN-4219): complete Step 1 — add METRIC parser

Fusion-Task-Id: FN-4219
Fusion-Task-Lineage: 8d9a9ba6-6729-4376-b935-549e28a7fa35
This commit is contained in:
Fusion
2026-05-14 00:10:25 -07:00
committed by gsxdsm
parent 9e9f78326f
commit 629451ba7d
2 changed files with 187 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
import { describe, expect, it } from "vitest";
import {
parseMetricLines,
PROTOTYPE_POLLUTION_DENYLIST,
} from "../experiment/metric-parser.js";
describe("parseMetricLines", () => {
it.each([
["METRIC accuracy=1", "accuracy", 1, undefined],
["METRIC loss=0.125", "loss", 0.125, undefined],
["METRIC score=1.2e-3", "score", 0.0012, undefined],
["METRIC delta=-42", "delta", -42, undefined],
["METRIC latency=123.4(ms)", "latency", 123.4, "ms"],
["METRIC speed=5.5 (req/s)", "speed", 5.5, "req/s"],
])(
"parses valid metric line: %s",
(line, name, value, unit) => {
const parsed = parseMetricLines(line);
expect(parsed.primary).toEqual({ name, value, unit });
expect(parsed.secondary).toEqual([]);
expect(parsed.warnings).toEqual([]);
},
);
it.each([
"METRIC bad=Infinity",
"METRIC bad=NaN",
"METRIC",
"not a metric",
"",
" ",
])("ignores malformed or invalid line: %s", (line) => {
const parsed = parseMetricLines(line);
expect(parsed.primary).toBeUndefined();
expect(parsed.secondary).toEqual([]);
});
it("warns and drops denylisted metric names", () => {
const denylisted = Array.from(PROTOTYPE_POLLUTION_DENYLIST);
const parsed = parseMetricLines(
denylisted.map((name) => `METRIC ${name}=1`).join("\n"),
);
expect(parsed.primary).toBeUndefined();
expect(parsed.secondary).toEqual([]);
expect(parsed.warnings).toHaveLength(denylisted.length);
for (const name of denylisted) {
expect(parsed.warnings.some((w) => w.includes(name))).toBe(true);
}
});
it("keeps first valid metric as primary and last-write-wins on duplicates", () => {
const parsed = parseMetricLines([
"METRIC accuracy=0.8",
"METRIC loss=0.3",
"METRIC accuracy=0.9",
"METRIC loss=0.1",
"METRIC f1=0.7",
].join("\n"));
expect(parsed.primary).toEqual({ name: "accuracy", value: 0.9, unit: undefined });
expect(parsed.secondary).toEqual([
{ name: "loss", value: 0.1, unit: undefined },
{ name: "f1", value: 0.7, unit: undefined },
]);
});
it("warns for non-finite metric values", () => {
const parsed = parseMetricLines("METRIC score=1\nMETRIC bad=Infinity");
expect(parsed.primary).toEqual({ name: "score", value: 1, unit: undefined });
expect(parsed.warnings).toEqual([
"Ignored non-finite metric value for bad: Infinity",
]);
});
it("parses multiple metrics preserving first-seen name order", () => {
const parsed = parseMetricLines([
"METRIC a=1",
"METRIC b=2",
"METRIC c=3",
].join("\n"));
expect(parsed.primary).toEqual({ name: "a", value: 1, unit: undefined });
expect(parsed.secondary).toEqual([
{ name: "b", value: 2, unit: undefined },
{ name: "c", value: 3, unit: undefined },
]);
});
});

View File

@@ -0,0 +1,97 @@
import type { ExperimentSecondaryMetric } from "@fusion/core";
export const METRIC_LINE_REGEX =
/^METRIC\s+([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*(?:\(([^)]+)\))?\s*$/;
export const PROTOTYPE_POLLUTION_DENYLIST = new Set([
"__proto__",
"constructor",
"prototype",
]);
export interface ParsedMetricLines {
primary?: { name: string; value: number; unit?: string };
secondary: ExperimentSecondaryMetric[];
warnings: string[];
}
interface ParsedMetricEntry {
name: string;
value: number;
unit?: string;
}
export function parseMetricLines(stdout: string): ParsedMetricLines {
const warnings: string[] = [];
const byName = new Map<string, ParsedMetricEntry>();
const firstSeenOrder: string[] = [];
for (const rawLine of stdout.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line) {
continue;
}
const match = METRIC_LINE_REGEX.exec(line);
if (!match) {
const looseMatch = /^METRIC\s+([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*([^\s(]+).*$/.exec(
line,
);
if (looseMatch) {
const [, looseName, looseRawValue] = looseMatch;
const looseValue = Number(looseRawValue);
if (!Number.isFinite(looseValue)) {
warnings.push(
`Ignored non-finite metric value for ${looseName}: ${looseRawValue}`,
);
}
}
continue;
}
const [, name, rawValue, rawUnit] = match;
if (PROTOTYPE_POLLUTION_DENYLIST.has(name)) {
warnings.push(`Ignored denylisted metric name: ${name}`);
continue;
}
const value = Number(rawValue);
if (!Number.isFinite(value)) {
warnings.push(`Ignored non-finite metric value for ${name}: ${rawValue}`);
continue;
}
if (!byName.has(name)) {
firstSeenOrder.push(name);
}
byName.set(name, {
name,
value,
unit: rawUnit?.trim() || undefined,
});
}
const ordered = firstSeenOrder
.map((name) => byName.get(name))
.filter((entry): entry is ParsedMetricEntry => Boolean(entry));
const primaryEntry = ordered[0];
const secondary = ordered.slice(1).map((entry) => ({
name: entry.name,
value: entry.value,
unit: entry.unit,
}));
return {
primary: primaryEntry
? {
name: primaryEntry.name,
value: primaryEntry.value,
unit: primaryEntry.unit,
}
: undefined,
secondary,
warnings,
};
}