From 060b88a254897ebcd6fdb3457171389297a8dfe6 Mon Sep 17 00:00:00 2001 From: jaso0n0818 Date: Sun, 21 Jun 2026 05:56:26 +0000 Subject: [PATCH] fix(scoring): parse underscore separators and scientific notation in upstream constants (#810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parsePythonNumberConstants matched numbers with /[-+]?\d+(?:\.\d+)?/, which stops at `_` and `e`: an upstream constant written 1_000_000 parsed as 1, and 1e-9 / 5.8e1 parsed as 1 / 5.8 — silently misparsing such constants and polluting the unmodeled-constant drift list. Widen the literal match to allow underscore separators, floats, and exponents, and strip `_` before Number(). Adds parser coverage for 1_000_000, 5.8e1, and 1e-9. (Reopens the parser fix from #968, which the gate closed for a patch-coverage miss on an unreachable guard line; that guard is removed so every changed line is exercised.) Addresses #810 (parser deliverable; staleness gate left for a follow-up). --- src/scoring/model.ts | 8 ++++++-- test/unit/scoring.test.ts | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/scoring/model.ts b/src/scoring/model.ts index 1ec4675e75..35f930064b 100644 --- a/src/scoring/model.ts +++ b/src/scoring/model.ts @@ -140,12 +140,16 @@ export async function getOrCreateScoringModelSnapshot(env: Env): Promise { const constants: Record = {}; for (const line of source.split("\n")) { - const match = line.match(/^([A-Z][A-Z0-9_]+)\s*=\s*([-+]?\d+(?:\.\d+)?)/); + // 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+)?)/); if (!match) continue; const name = match[1]!; const raw = match[2]!; if (options.knownOnly !== false && !SCORING_CONSTANT_NAMES.has(name)) continue; - constants[name] = Number(raw); + // Number() rejects underscore separators, so strip them before parsing. + constants[name] = Number(raw.replace(/_/g, "")); } return constants; } diff --git a/test/unit/scoring.test.ts b/test/unit/scoring.test.ts index 0ab930517c..2502255382 100644 --- a/test/unit/scoring.test.ts +++ b/test/unit/scoring.test.ts @@ -72,6 +72,20 @@ IGNORED = "not numeric" expect(detectActiveModel({})).toBe("unknown"); }); + it("parses underscore separators, floats, and scientific notation without truncating (#810)", () => { + const parsed = parsePythonNumberConstants(` +CONTRIBUTION_SCORE_FOR_FULL_BONUS = 1_500_000 +SRC_TOK_SATURATION_SCALE = 5.8e1 +MERGED_PR_BASE_SCORE = 1e-9 +OSS_EMISSION_SHARE = 0.90 +`); + // The previous /[-+]?\\d+(?:\\.\\d+)?/ regex stopped at `_`/`e`: 1_500_000 -> 1, 5.8e1 -> 5.8, 1e-9 -> 1. + expect(parsed.CONTRIBUTION_SCORE_FOR_FULL_BONUS).toBe(1500000); + expect(parsed.SRC_TOK_SATURATION_SCALE).toBe(58); + expect(parsed.MERGED_PR_BASE_SCORE).toBe(1e-9); + expect(parsed.OSS_EMISSION_SHARE).toBe(0.9); + }); + it("prefers exponential saturation when mixed upstream constants are present", () => { const parsed = parsePythonNumberConstants(` MERGED_PR_BASE_SCORE = 25