From a854ba076988c67674467e67ed4f6c4b35c778d9 Mon Sep 17 00:00:00 2001 From: jaso0n0818 Date: Sun, 21 Jun 2026 05:03:48 +0000 Subject: [PATCH] fix(scoring): parse underscore separators and scientific notation in upstream constants (#810) 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. Such constants were silently misparsed and also polluted the unmodeled-constant drift list. Widen the literal match to allow underscore separators, floats, and exponents, strip underscores before Number(), and guard against non-finite results. Adds parser coverage for 1_000_000, 5.8e1, and 1e-9. Addresses #810 (constant-parser robustness; the staleness-gate deliverable is left for a focused follow-up). --- src/scoring/model.ts | 10 ++++++++-- test/unit/scoring.test.ts | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/scoring/model.ts b/src/scoring/model.ts index 1ec4675e75..bb32744cdb 100644 --- a/src/scoring/model.ts +++ b/src/scoring/model.ts @@ -140,12 +140,18 @@ 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. + const value = Number(raw.replace(/_/g, "")); + if (!Number.isFinite(value)) continue; + constants[name] = value; } return constants; } diff --git a/test/unit/scoring.test.ts b/test/unit/scoring.test.ts index 0ab930517c..e5ba262b6b 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