From b762e997e7def82afd8b0ef37b514295d3af4d7b Mon Sep 17 00:00:00 2001 From: philluiz2323 Date: Sun, 21 Jun 2026 23:26:16 -0700 Subject: [PATCH] fix(scoring): parse underscores in the fractional part of upstream constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #969 constant parser added underscore support for the integer part and exponents, but the fractional digit classes (\d* / \.\d+) still rejected '_'. Per PEP 515 underscores are valid between digits in any part, so a valid upstream literal like 0.000_001 matched only '0.000' and was read as 0 — a silent truncation (and divide/multiply-by-zero hazard) that skews every score preview/breakdown. Allow '_' in the fractional digits too; Number() already strips them before parsing. Existing integer-underscore/exponent cases are unchanged. Closes #992 --- src/scoring/model.ts | 10 ++++++---- test/unit/scoring.test.ts | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/scoring/model.ts b/src/scoring/model.ts index c088a2abcd..f02557b68a 100644 --- a/src/scoring/model.ts +++ b/src/scoring/model.ts @@ -153,10 +153,12 @@ export async function getOrCreateScoringModelSnapshot(env: Env): Promise { const constants: Record = {}; for (const line of source.split("\n")) { - // Match Python numeric literals including underscore separators (1_000_000), floats, and exponents - // (1e-9, 5.8e1). The previous /[-+]?\d+(?:\.\d+)?/ stopped at `_`/`e`, truncating 1_000_000 -> 1 and - // 1e-9 -> 1, which silently misparsed any such upstream constant and polluted the unmodeled list (#810). - const match = line.match(/^([A-Z][A-Z0-9_]+)\s*=\s*([-+]?(?:\d[\d_]*\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)/); + // Match Python numeric literals including underscore separators (1_000_000, 0.000_001), floats, and + // exponents (1e-9, 5.8e1). PEP 515 allows underscores between digits in ANY part of the literal, so the + // fractional digits accept `_` too — the previous frac classes (`\d*`/`\.\d+`) stopped at the first + // fractional underscore, truncating 0.000_001 -> 0. The earlier /[-+]?\d+(?:\.\d+)?/ also stopped at + // `_`/`e` (1_000_000 -> 1, 1e-9 -> 1). Both silently misparsed upstream constants (#810). + const match = line.match(/^([A-Z][A-Z0-9_]+)\s*=\s*([-+]?(?:\d[\d_]*\.?[\d_]*|\.\d[\d_]*)(?:[eE][-+]?\d+)?)/); if (!match) continue; const name = match[1]!; const raw = match[2]!; diff --git a/test/unit/scoring.test.ts b/test/unit/scoring.test.ts index 1445bc85c4..da286690c7 100644 --- a/test/unit/scoring.test.ts +++ b/test/unit/scoring.test.ts @@ -86,6 +86,21 @@ OSS_EMISSION_SHARE = 0.90 expect(parsed.OSS_EMISSION_SHARE).toBe(0.9); }); + it("parses underscores in the FRACTIONAL part (PEP 515) without truncating to zero", () => { + const parsed = parsePythonNumberConstants( + ` +SMALL_RATE = 0.000_001 +PRECISE_SCALE = 3.14_15 +MIXED_GROUPED = 1_000.000_5 +`, + { knownOnly: false }, + ); + // Frac classes were \\d*/\\.\\d+ (no `_`), so 0.000_001 stopped at "0.000" -> 0 (a divide/multiply hazard). + expect(parsed.SMALL_RATE).toBe(0.000001); + expect(parsed.PRECISE_SCALE).toBe(3.1415); + expect(parsed.MIXED_GROUPED).toBe(1000.0005); + }); + it("flags only scoring snapshots older than the freshness window as stale (#810)", () => { const now = Date.parse("2026-06-21T12:00:00.000Z"); const justFresh = new Date(now - SCORING_SNAPSHOT_STALE_MS + 60_000).toISOString();