S1 (a1): feature-promise audit — no claim survives unverified

PROMISE-AUDIT.md: every /pricing claim → verified/built/reworded.
BUILT (was vapor): alt line ladder + edge ranking (same-features regrade
at shifted lines, Desk-gated at the API), quarter-Kelly (engine quantile
P(win) x real captured odds — either missing → no sizing), free-tier
kill-condition locked previews. FIXED (was false): analyst 15/day cap vs
the Founder 'Unlimited reads' promise → analyst unlimited; every '40+
factors' claim (real count: 22 named features) reworded truthfully in 7
files. VERIFIED: cascade alerts (real, wired), phi correlation,
leg history, cross-book comparison, WC soccer, real-time feed.
Locked by tests/unit/promiseAudit.test.js. Jest now ignores
.claude/worktrees (parallel agents' suites no longer leak into runs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-11 14:24:41 -04:00
parent e4d2e79f95
commit d242b11b4b
20 changed files with 296 additions and 30 deletions
+16 -1
View File
@@ -33,6 +33,9 @@ const TIERS = Object.freeze({
engine2: false,
stat_dashboard: true,
api_access: false,
// Session 62 (A1-S1) — Desk-only intelligence (pricing-page promises).
alt_line_ladder: false,
kelly_sizing: false,
}),
// Session 12 — $4.99/mo Africa tier. Slotted between free and analyst.
// Same intelligence surface as analyst (reasoning + kill conditions
@@ -49,9 +52,16 @@ const TIERS = Object.freeze({
engine2: false,
stat_dashboard: true,
api_access: false,
// Session 62 (A1-S1) — Desk-only intelligence (pricing-page promises).
alt_line_ladder: false,
kelly_sizing: false,
}),
analyst: Object.freeze({
scans_per_day: 15,
// Session 62 (A1-S1) — the Founder Access card promises "Unlimited
// reads" and the Nav shows an ∞ badge for every paid tier; the 15/day
// cap made both claims false. Analyst is unlimited; Desk differentiates
// on capabilities (alt ladder, Kelly, feed), not on rationing reads.
scans_per_day: Infinity,
grade_visible: true,
reasoning_visible: true,
kill_conditions_detail: true,
@@ -61,6 +71,9 @@ const TIERS = Object.freeze({
engine2: false,
stat_dashboard: true,
api_access: false,
// Session 62 (A1-S1) — Desk-only intelligence (pricing-page promises).
alt_line_ladder: false,
kelly_sizing: false,
}),
desk: Object.freeze({
scans_per_day: Infinity,
@@ -73,6 +86,8 @@ const TIERS = Object.freeze({
engine2: true, // LLM deep analysis
stat_dashboard: true,
api_access: false,
alt_line_ladder: true,
kelly_sizing: true,
}),
});
+4
View File
@@ -55,6 +55,10 @@ async function gradeBestSide(grade, prop, sport) {
line: prop.line,
sport,
book: prop.book,
// Session 62 (A1-S1) — real book odds ride into the engine so the
// quarter-Kelly sizing can compute from actual prices (or not at all).
over_odds: prop.over_odds ?? null,
under_odds: prop.under_odds ?? null,
};
const sides = await Promise.all([
Promise.resolve()
@@ -406,6 +406,50 @@ async function analyzeViaEngine1(rawProp = {}) {
// ledger model_value and rendered under the MODEL label on the card.
legacy.projection = projection;
// Session 62 (A1-S1) — ALT LINE LADDER + EDGE RANKING. The pricing page
// sold it; the adapter/card were already built for it; nothing produced
// it. Re-grade the SAME feature vector at shifted lines (zero extra I/O)
// and rank by edge. Attached for every graded result; the API tier gate
// strips it below Desk.
try {
const baseLine = Number(prop.line);
if (Number.isFinite(baseLine)) {
const ladder = [-1, -0.5, 0, 0.5, 1]
.map((shift) => Math.round((baseLine + shift) * 100) / 100)
.filter((ln) => ln > 0)
.map((ln) => {
const shiftedProp = { ...prop, line: ln };
const g = engine1.gradeProp({ features, trap, consistency, prop: shiftedProp });
const adapted = toLegacyShape(g, {
player: rawProp.player, stat_type: rawProp.stat_type,
line: ln, direction: prop.direction, book: rawProp.book || 'unknown', sport: meta.sport,
}, { edgePct: edgePctFor(features, { ...shiftedProp, stat_type: rawProp.stat_type, direction: prop.direction }) });
return { line: ln, grade: adapted.grade, edge_pct: adapted.edge_pct, base: ln === baseLine };
})
.sort((a, b) => (Number(b.edge_pct) || 0) - (Number(a.edge_pct) || 0));
if (ladder.length > 1) legacy.alt_lines = ladder;
}
} catch { /* the ladder is additive — never breaks the read */ }
// Session 62 (A1-S1) — QUARTER-KELLY. Real probability (quantile estimator
// over the actual game logs) × real book odds, or nothing. Never derived
// from confidence, never a default vig.
try {
const { estimateProbability } = require('./probabilityEstimator');
const est = estimateProbability({ gameLogs: meta.gameLogs, line: prop.line, statType: rawProp.stat_type, features });
const pWin = String(prop.direction || 'over').toLowerCase() === 'under'
? (Number.isFinite(est.p_over) ? 1 - est.p_over : null)
: (Number.isFinite(est.p_over) ? est.p_over : null);
if (pWin != null) legacy.p_win = Math.round(pWin * 1000) / 1000;
const sideOdds = String(prop.direction || 'over').toLowerCase() === 'under'
? rawProp.under_odds : rawProp.over_odds;
if (pWin != null && sideOdds != null) {
const { quarterKelly } = require('../../utils/kelly');
const k = quarterKelly(pWin, sideOdds);
if (k) legacy.kelly = { ...k, odds: String(sideOdds) };
}
} catch { /* sizing is additive — absent beats wrong */ }
return legacy;
}
+41
View File
@@ -0,0 +1,41 @@
'use strict';
/**
* Quarter-Kelly sizing (Session 62 / A1-S1 — the pricing page promised it;
* now it exists). Pure math, no I/O.
*
* DATA SEMANTICS: Kelly needs a REAL win probability and REAL book odds.
* p comes from the engine's quantile probability estimator (game-log based);
* odds come from the captured book row. Either missing → null — a sizing
* recommendation is never fabricated from confidence scores or defaults.
*/
function americanToDecimal(american) {
const a = Number(american);
if (!Number.isFinite(a) || a === 0) return null;
return a > 0 ? 1 + a / 100 : 1 + 100 / Math.abs(a);
}
/**
* quarterKelly(p, americanOdds) →
* { full, quarter, pct } — fractions of bankroll (pct = quarter × 100,
* rounded to 0.1) — or null when p/odds are unusable or the edge is
* non-positive (Kelly says: no bet).
*/
function quarterKelly(p, americanOdds) {
const prob = Number(p);
if (!Number.isFinite(prob) || prob <= 0 || prob >= 1) return null;
const dec = americanToDecimal(americanOdds);
if (dec == null || dec <= 1) return null;
const b = dec - 1;
const full = (b * prob - (1 - prob)) / b;
if (!Number.isFinite(full) || full <= 0) return null;
const quarter = full / 4;
return {
full: Math.round(full * 1000) / 1000,
quarter: Math.round(quarter * 1000) / 1000,
pct: Math.round(quarter * 1000) / 10, // % of bankroll at quarter-Kelly
};
}
module.exports = { quarterKelly, americanToDecimal };
+12 -3
View File
@@ -46,14 +46,23 @@ function lockKillConditions(list) {
*/
function applyTierGating(result, tierName) {
if (!result || typeof result !== 'object') return result;
// Session 62 (A1-S1) — Desk-only intelligence is stripped at the API, not
// just the adapter: alt line ladder + quarter-Kelly never leave the server
// for lower tiers. (The pricing page sells them as Desk-only; the gate
// makes that true at the wire.)
const deskGated = { ...result };
if (!canAccess(tierName, 'alt_line_ladder')) delete deskGated.alt_lines;
if (!canAccess(tierName, 'kelly_sizing')) delete deskGated.kelly;
if (canAccess(tierName, 'reasoning_visible')) {
// Paid tier — pass through unchanged.
return result;
// Paid tier — everything else passes through unchanged.
return deskGated;
}
// Free tier: keep grade + confidence + edge_pct (the hook), redact
// reasoning + kill condition explanations.
return {
...result,
...deskGated,
reasoning: lockReasoning(result.reasoning),
kill_conditions_triggered: lockKillConditions(result.kill_conditions_triggered),
tier_gated: true,