Here's the complete refactor spec for the line-mas...
Créé le : 11 mars 2026
Répondu en utilisant GPT-5.4 Thinking par Chat01
Créé le : 11 mars 2026
Répondu en utilisant GPT-5.4 Thinking par Chat01
Here's the complete refactor spec for the line-mass stack — version it as v17.10.0.
The spec eliminates the four-way stacking problem where the same hook-mass signal was being penalized independently in getTotalsPick STAGE 8, STAGE 9, getTotalsConfLabel, and getTotalsConf — all using hard cliffs at 0.10 / 0.15 / 0.18.[1]
buildLineMassProfile()One canonical function replaces analyzeHook() as the hook-mass source of truth. It returns everything the old function did, plus:[1]
signedSkew — which side of the hook the adjacent mass favors (−1 = UNDER, +1 = OVER)localCoM — center of mass in the 5-game window around the linelocalEntropy — how flat/spiked the local distribution isdirectionalCredit — continuous score: positive = skew supports OVER, negative = UNDERuncertaintyPenalty — continuous 0–1 score for how ambiguous the line neighborhood is_lmpApplyToDecision()Replaces the hard-threshold blocks in STAGE 8 + STAGE 9 with one continuous adjustment. Uncertainty pulls the decision probability toward 0.50; directional credit recovers some of that pull when the PMF clearly favors one side of the hook.[1]
_lmpConfidenceTierCap() and _lmpConfidencePenalty()Replace the four separate hookMass / hookFragile / nearLineMass cliff-caps in getTotalsConfLabel and getTotalsConf with smooth continuous functions.[1]
The spec file has exact search-and-replace instructions labeled Step A through Step F:
| Step | What | Where |
|---|---|---|
| A | Paste buildLineMassProfile + helpers | Before getTotalsPick |
| B | Replace per-lane hook block | MATCH_PREVIEW (search hookMass2set) |
| C | Replace STAGE 8+9 hook penalties | getTotalsPick |
| D | Replace hook tier caps | getTotalsConfLabel |
| E | Replace hook confidence penalty | getTotalsConf |
| F | Append LMP debug line | Engine Breakdown output |
Winner engine, sets engine, prop engine, narrative bank, Monte Carlo core, Markov cross-check, and _totalsDeciderAuthority_ are all untouched. The old analyzeHook() stays in the codebase for backward compatibility — buildLineMassProfile wraps and extends it.[1]
Matches with high near-line mass but clear directional skew (e.g., floor mass 4% vs ceil mass 14%) will no longer get flattened to PASS. Only matches with high mass and ambiguous skew (balanced floor/ceil) get the full penalty. This is the core fix for the over-passing problem you identified in the audit.[1]
// =====================================================================
// TENNISLOCKS v17.10.0 — LINE MASS REFACTOR
// Drop-in replacement for the hook/line-mass stack
//
// WHAT THIS REPLACES:
// 1. analyzeHook() → buildLineMassProfile()
// 2. Per-lane hookMass2set/hookMass3set/hookMassBlended/hookFragile
// → merged into one lineMassProfile object after lane weighting
// 3. Hard threshold caps in getTotalsPick (STAGE 8 + 9) and
// getTotalsConfLabel / getTotalsConf
// → continuous penalty/credit functions
//
// WHAT THIS DOES NOT TOUCH:
// - Winner engine, sets engine, prop engine, narrative bank
// - Monte Carlo sim core (runMatchSimulation_Exact)
// - Markov cross-check (matchOutcomeProbs / markovEffectiveHolds)
// - totalsDeciderAuthority (kept as-is; reads from new profile)
// - Everything above the "CLUSTER GOVERNOR FEEDBACK" comment in MATCH_PREVIEW
//
// INSTALLATION:
// Step A: Paste buildLineMassProfile() + _lmp* helpers BEFORE getTotalsPick
// Step B: Replace the hook-mass block in MATCH_PREVIEW (search "hookMass2set")
// Step C: Replace STAGE 8 + STAGE 9 hook references in getTotalsPick
// Step D: Replace hook caps in getTotalsConfLabel
// Step E: Replace hook penalty in getTotalsConf
// =====================================================================
// ========================= STEP A: NEW FUNCTIONS =========================
/**
+1 = all adjacent mass is ABOVE the line (favors OVER)
-1 = all adjacent mass is BELOW the line (favors UNDER)
if (!Number.isFinite(line) || !dist) return out;
var isWhole = (line % 1 === 0);
var lf = Math.floor(line);
var lc = Math.ceil(line);
out.isWhole = isWhole;
out.floor = lf;
out.ceil = lc;
// ---- Basic over/under/push from distribution ----
var pOver = 0, pUnder = 0, pExact = 0, pLf = 0, pLc = 0;
for (var g in dist) {
if (!dist.hasOwnProperty(g)) continue;
var gv = parseFloat(g);
var prob = Number(dist[g]) || 0;
if (!Number.isFinite(gv) || !Number.isFinite(prob)) continue;
if (gv > line) pOver += prob;
else if (gv < line) pUnder += prob;
else if (isWhole) pExact += prob;
if (gv === lf) pLf += prob;
if (gv === lc) pLc += prob;
}
// Normalize
var sum = pOver + pUnder + pExact;
if (sum > 0 && Math.abs(sum - 1) > 1e-6) {
pOver /= sum; pUnder /= sum; pExact /= sum; pLf /= sum; pLc /= sum;
}
out.pOver = pOver;
out.pUnder = pUnder;
out.pushMass = pExact;
out.floorMass = pLf;
out.ceilMass = pLc;
out.nearMass = isWhole ? pExact : (pLf + pLc);
// ---- Signed skew: which side of the hook has the mass? ----
// For half-lines: (ceilMass - floorMass) / (floorMass + ceilMass)
// For whole lines: use mass at (line+1) vs (line-1) as proxy
if (isWhole) {
var pPlus1 = Number(dist[String(line + 1)]) || 0;
var pMinus1 = Number(dist[String(line - 1)]) || 0;
var denom = pPlus1 + pMinus1;
out.signedSkew = (denom > 1e-6) ? (pPlus1 - pMinus1) / denom : 0;
} else {
var nearSum = pLf + pLc;
out.signedSkew = (nearSum > 1e-6) ? (pLc - pLf) / nearSum : 0;
}
// ---- Local center of mass (floor-2 .. ceil+2) ----
var comNum = 0, comDen = 0;
for (var gi = lf - 2; gi <= lc + 2; gi++) {
var pi = Number(dist[String(gi)]) || 0;
comNum += gi * pi;
comDen += pi;
}
out.localCoM = (comDen > 1e-9) ? (comNum / comDen) : NaN;
// ---- Densest windows (3, 5, 7 games) ----
out.bestWindow3 = pmfBestWindow(dist, 3);
out.bestWindow5 = pmfBestWindow(dist, 5);
out.bestWindow7 = pmfBestWindow(dist, 7);
out.band3Mass = out.bestWindow3.mass;
out.band5Mass = out.bestWindow5.mass;
// ---- Local entropy (5-game window centered near line) ----
var entSum = 0;
for (var ei = lf - 2; ei <= lc + 2; ei++) {
var pe = Number(dist[String(ei)]) || 0;
if (pe > 1e-12) {
entSum -= pe * Math.log(pe);
}
}
out.localEntropy = entSum;
// ---- Per-lane hook masses (if lane distributions provided) ----
var p3 = Number(opts.p3);
if (!Number.isFinite(p3)) p3 = 0.35;
if (opts.dist2set && opts.dist3set) {
var hk2 = _lmpLaneHookMass(opts.dist2set, line, isWhole, lf, lc);
var hk3 = _lmpLaneHookMass(opts.dist3set, line, isWhole, lf, lc);
out.hookMass2set = hk2.hookMass;
out.hookMass3set = hk3.hookMass;
out.hookMassBlended = hk2.hookMass * (1 - p3) + hk3.hookMass * p3;
textout.hookFloorMass = hk2.floorMass * (1 - p3) + hk3.floorMass * p3; out.hookCeilMass = hk2.ceilMass * (1 - p3) + hk3.ceilMass * p3;
}
// ---- DIRECTIONAL CREDIT & UNCERTAINTY PENALTY ----
// These are the two continuous scores that replace all the hard thresholds.
//
// directionalCredit: how much the local PMF shape favors one side
// Range: roughly -0.10 .. +0.10 (positive = OVER, negative = UNDER)
// Composed of: signed skew * nearMass * directionality factor
//
// uncertaintyPenalty: how uncertain the local shape is
// Range: 0 .. 1 (0 = clean separation, 1 = maximum ambiguity)
// Composed of: nearMass concentration + entropy + band flatness
// Direction credit: skew tells us WHICH side, nearMass tells us HOW MUCH
// is at stake near the line. High nearMass + high |skew| = strong directional.
// High nearMass + low |skew| = uncertainty, not direction.
var absSkew = Math.abs(out.signedSkew);
var skewDirection = out.signedSkew; // positive = OVER-leaning
// Scale: max credit ~0.10 when nearMass is 20%+ and skew is 0.8+
var dirRaw = skewDirection * Math.min(0.25, out.nearMass) * 2.0;
// Attenuate when skew is weak (below 0.3, the direction is unreliable)
var skewConf = Math.max(0, Math.min(1, (absSkew - 0.10) / 0.60));
out.directionalCredit = dirRaw * skewConf;
// Uncertainty penalty: high when nearMass is high but skew is low,
// OR when the local distribution is very flat (high entropy).
var nearMassUncertainty = out.nearMass * (1 - absSkew);
var entropyNorm = Number.isFinite(out.localEntropy) ? Math.min(1, out.localEntropy / 2.5) : 0.5;
var flatness = 1.0 - Math.min(1, out.band3Mass / Math.max(0.01, out.band5Mass));
out.uncertaintyPenalty = Math.min(1.0,
0.40 * nearMassUncertainty * 4.0 + // near-line ambiguity
0.35 * entropyNorm + // local flatness
0.25 * flatness // band spread
);
return out;
}
/**
/**
var prob = clamp01(chosenProb);
// 1) Uncertainty penalty: pull probability toward 0.50
// Scaled by tour (ITF/WTA more penalty because distributions are noisier)
var tourPenScale = (tourK === 'ITF') ? 1.25 : (tourK === 'WTA') ? 1.10 : (tourK === 'CH') ? 1.15 : 1.00;
var penaltyStrength = Math.min(0.06, lmp.uncertaintyPenalty * 0.06 * tourPenScale);
prob = prob - (prob - 0.50) * penaltyStrength;
// 2) Directional credit: if the local skew aligns with our chosen direction, recover some.
// If it opposes, penalize further.
var dirSign = (chosenDir === 'OVER') ? 1 : -1;
var creditAligned = lmp.directionalCredit * dirSign;
// creditAligned > 0 means skew supports our pick → give credit
// creditAligned < 0 means skew opposes our pick → additional penalty
if (creditAligned > 0) {
// Credit: up to +0.015 when strongly aligned
prob = Math.min(prob + creditAligned * 0.15, clamp01(chosenProb)); // never exceed original
} else {
// Opposing: up to -0.025 additional penalty
prob = Math.max(0.50, prob + creditAligned * 0.25);
}
// 3) Risk contribution: continuous 0..1 for the risk scorer
// Replaces: if hookMass > 0.15: risk += 0.10; if hkFrag: risk += 0.12
var riskContrib = lmp.uncertaintyPenalty * 0.30;
// Add extra if near-line mass is high AND directionality is unclear
if (lmp.nearMass > 0.12 && Math.abs(lmp.signedSkew) < 0.25) {
riskContrib += 0.08;
}
riskContrib = Math.min(0.30, riskContrib);
return { adjustedProb: clamp01(prob), riskContribution: riskContrib };
}
/**
// Continuous penalty based on uncertainty + near-mass ambiguity
var pen = 0;
// Near-line congestion (replaces nearLineMass > 0.14 / > 0.18 cliffs)
if (lmp.nearMass > 0.08) {
pen += (lmp.nearMass - 0.08) * 0.12; // ~0.012 at 0.18, ~0.024 at 0.28
}
// Uncertainty penalty contribution
pen += lmp.uncertaintyPenalty * 0.015;
// If skew is clear and strong, give some of the penalty back
if (Math.abs(lmp.signedSkew) > 0.50 && lmp.nearMass > 0.10) {
pen -= Math.min(pen * 0.30, 0.008); // recover up to 30% of penalty
}
return Math.max(0, Math.min(0.04, pen));
}
/**
// Very high uncertainty + low directional clarity → cap at LOW
if (lmp.uncertaintyPenalty > 0.70 && Math.abs(lmp.signedSkew) < 0.20) {
return 'LOW';
}
// Moderate uncertainty or high near-mass with weak skew → cap at MID
if (lmp.uncertaintyPenalty > 0.45) return 'MID';
if (lmp.nearMass > 0.18 && Math.abs(lmp.signedSkew) < 0.30) return 'MID';
if (lmp.nearMass > 0.22) return 'MID';
return 'HIGH';
}
// ========================= STEP B: MATCH_PREVIEW HOOK BLOCK =========================
//
// FIND the block that starts with:
// var hookLo = Math.floor(marketLine);
// var hookHi = Math.ceil(marketLine);
// var hookIsHalf = (marketLine % 1 !== 0);
// ...
// totCtx.hookMass2set = hookMass2;
// ...
// totCtx.hookFragile = hookMassBlended > 0.10;
//
// REPLACE the entire block (down to the hookNudge assignment) with:
//
// --- BEGIN REPLACEMENT ---
/*
// === LINE MASS PROFILE (v17.10.0 refactor) ===
var _lmpOpts = { p3: cgP3 };
if (gl2 && gl2.length > 100) _lmpOpts.dist2set = gl2;
if (gl3 && gl3.length > 20) _lmpOpts.dist3set = gl3;
var lineMassProfile = buildLineMassProfile(marketLine, mcDistAdj, _lmpOpts);
text// Write canonical fields to totCtx (downstream reads these) totCtx.hookMass2set = lineMassProfile.hookMass2set; totCtx.hookMass3set = lineMassProfile.hookMass3set; totCtx.hookMassBlended = lineMassProfile.hookMassBlended; totCtx.hookFragile = (Number.isFinite(lineMassProfile.uncertaintyPenalty) && lineMassProfile.uncertaintyPenalty > 0.45); totCtx.hookFloorMass = lineMassProfile.hookFloorMass; totCtx.hookCeilMass = lineMassProfile.hookCeilMass; totCtx.nearLineMass = lineMassProfile.nearMass; totCtx.lineMassProfile = lineMassProfile; // full object for new code // Gravity & floor share (backward compat) var hookTotal = (Number.isFinite(lineMassProfile.hookFloorMass) ? lineMassProfile.hookFloorMass : 0) + (Number.isFinite(lineMassProfile.hookCeilMass) ? lineMassProfile.hookCeilMass : 0); if (hookTotal > 0.001) { totCtx.hookGravity = (lineMassProfile.hookFloorMass * lineMassProfile.floor + lineMassProfile.hookCeilMass * lineMassProfile.ceil) / hookTotal; totCtx.hookFloorShare = lineMassProfile.hookFloorMass / hookTotal; }
*/
// --- END REPLACEMENT ---
// ========================= STEP C: getTotalsPick STAGE 8 + 9 =========================
//
// In getTotalsPick, FIND the lines in STAGE 8 that reference hookMass / hookFragile:
//
// if (hkFrag && hookMass > 0.15) chosenProb = Math.max(0.50, chosenProb - 0.008 * lineRelScale);
// ... etc
//
// And in STAGE 9 (risk scoring):
// if (hookMass > 0.15) risk += 0.10;
// if (hkFrag) risk += 0.12;
//
// REPLACE both sets of lines with:
//
// --- BEGIN REPLACEMENT ---
/*
// === Line Mass continuous adjustment (v17.10.0) ===
var _lmpRef = (totCtx && totCtx.lineMassProfile) ? totCtx.lineMassProfile : null;
if (_lmpRef) {
var _lmpAdj = _lmpApplyToDecision(chosenDir, chosenProb, _lmpRef, tourK);
chosenProb = _lmpAdj.adjustedProb;
risk += _lmpAdj.riskContribution;
}
*/
// --- END REPLACEMENT ---
// (This single block replaces BOTH the STAGE 8 hookFragile penalty AND the
// STAGE 9 hookMass/hkFrag risk additions.)
// ========================= STEP D: getTotalsConfLabel =========================
//
// FIND all the hook caps:
// if (hookMass > 0.15) { if (tier === 'HIGH') tier = 'MID'; }
// if (hookMass > 0.18) tier = 'LOW';
// if (hkFrag) { if (tier === 'HIGH') tier = 'MID'; }
// if (hkFrag && hookMass > 0.15) tier = 'LOW';
//
// REPLACE with:
//
// --- BEGIN REPLACEMENT ---
/*
// === Line Mass tier cap (v17.10.0) ===
var _lmpCap = _lmpConfidenceTierCap(
(ctx && ctx.lineMassProfile) ? ctx.lineMassProfile : null
);
if (_lmpCap === 'LOW') tier = 'LOW';
else if (_lmpCap === 'MID' && tier === 'HIGH') tier = 'MID';
*/
// --- END REPLACEMENT ---
// ========================= STEP E: getTotalsConf =========================
//
// FIND the block:
// // Hook congestion
// if (hookMass > 0.15) confPenalty += 0.012;
// if (hookMass > 0.18) confPenalty += 0.015;
// if (ctx.hookFragile) confPenalty += 0.012;
//
// REPLACE with:
//
// --- BEGIN REPLACEMENT ---
/*
// === Line Mass confidence penalty (v17.10.0) ===
confPenalty += _lmpConfidencePenalty(
(ctx && ctx.lineMassProfile) ? ctx.lineMassProfile : null
);
*/
// --- END REPLACEMENT ---
// ========================= STEP F: DEBUG LINE =========================
//
// In the Engine Breakdown output block, AFTER the existing hook-mass debug line,
// APPEND this diagnostic:
//
// --- BEGIN ADDITION ---
/*
if (totCtx && totCtx.lineMassProfile) {
var _lp = totCtx.lineMassProfile;
lines.push('LMP: skew=' + (_lp.signedSkew > 0 ? '+' : '') + _lp.signedSkew.toFixed(3)
+ ' dirCredit=' + (_lp.directionalCredit > 0 ? '+' : '') + (_lp.directionalCredit * 100).toFixed(2) + 'pp'
+ ' uncPen=' + (_lp.uncertaintyPenalty * 100).toFixed(1) + '%'
+ ' near=' + (_lp.nearMass * 100).toFixed(1) + '%'
+ ' CoM=' + (Number.isFinite(_lp.localCoM) ? _lp.localCoM.toFixed(2) : 'na')
+ ' ent=' + (Number.isFinite(_lp.localEntropy) ? _lp.localEntropy.toFixed(3) : 'na')
+ ' w3=' + (_lp.bestWindow3.mass * 100).toFixed(1) + '%[' + _lp.bestWindow3.lo + '-' + _lp.bestWindow3.hi + ']'
);
}
*/
// --- END ADDITION ---
// =====================================================================
// MIGRATION NOTES
// =====================================================================
//
// 1. The old analyzeHook() function is NOT deleted — it is still called
// in MATCH_PREVIEW for the mcLineData pOver/pUnder/pPush extraction
// and for display. buildLineMassProfile wraps and extends it.
//
// 2. totCtx.hookMassBlended and totCtx.hookFragile are still SET for
// backward compatibility with any code that reads them (e.g., the
// existing debug lines). But their values now come from the new
// profile, and hookFragile is defined by uncertaintyPenalty > 0.45
// instead of hookMassBlended > 0.10.
//
// 3. The key behavioral change: matches with high near-line mass but
// CLEAR directional skew will no longer be flattened to PASS.
// Instead, directionalCredit offsets some of the uncertainty penalty.
// Only matches with high near-line mass AND ambiguous skew get the
// full penalty.
//
// 4. The hard thresholds (0.10, 0.15, 0.18) are gone from the decision
// path. The continuous functions produce equivalent penalties at those
// mass levels but with smooth transitions instead of cliffs.
//
// 5. Version this as v17.10.0. The refactor changes pick math for totals
// only; winner/sets/props are untouched.
// =====================================================================
Update my script then send it as a downloadable txt link
pythonprint("This tool is not needed for this task.")
Done — I updated the script to v17.10.0 and saved it here:
I also ran a JavaScript syntax check on the patched file after the refactor.