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:
97
packages/engine/src/experiment/metric-parser.ts
Normal file
97
packages/engine/src/experiment/metric-parser.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user