Order Zero Phase 1b: redaction detection + reference-policy curve

Two corrections to the first pass, both of which would have produced a
false positive.

1) A non-empty body is NOT proof of access. PropLine's free tier returns
   the full STRUCTURE of tier-gated endpoints with values stripped plus an
   upgrade_url -- and the first pass classified /odds/closing and /movement
   as "works" on structure alone. detectRedaction() now counts actual
   prices and downgrades works -> partial when a body advertises an upgrade
   or carries outcomes with zero prices. Same class as the harness that
   returned a silent false, inverted.

2) One hard-coded reference set forces a yes/no on a question that is
   really a curve. reference_policy_curve reports strict eligibility
   (>=2 books, both sides, same line) under exchange_only /
   exchange_plus_sharp / exchange_plus_us / takeable_only, so the ruler
   decision is made on coverage-vs-quality rather than on a guess. DFS is
   absent from every policy by construction and a test asserts it.

Also probes /markets/resolution-summary: /exports/resolved-props being 403
tells us we cannot PULL settlements; resolution-summary tells us whether
they EXIST to be bought. Different questions.

19 unit tests, still hermetic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
This commit is contained in:
Kev
2026-07-31 23:37:23 -04:00
parent 2071b79456
commit 3c466d79cb
2 changed files with 131 additions and 3 deletions
+78 -2
View File
@@ -142,6 +142,21 @@ function analyseBreadth(raw, allowedBooks) {
if (r >= 3) eligible3 += 1;
}
// Same strict rule (two-sided, same line) under each candidate policy.
const policyCurve = {};
for (const [name, set] of Object.entries(REFERENCE_POLICIES)) {
const best = new Map();
for (const [lk, books] of byPropLine.entries()) {
let r = 0;
for (const b of set) if (books.has(b)) r += 1;
const pk = lk.slice(0, lk.lastIndexOf('|'));
best.set(pk, Math.max(best.get(pk) || 0, r));
}
let n2 = 0; let n3 = 0;
for (const r of best.values()) { if (r >= 2) n2 += 1; if (r >= 3) n3 += 1; }
policyCurve[name] = { n2, n3 };
}
const props = feedCounts.length;
const hist = (xs) => { const h = {}; for (const n of xs) h[n] = (h[n] || 0) + 1; return h; };
const mean = (xs) => (xs.length ? round2(xs.reduce((a, b) => a + b, 0) / xs.length) : null);
@@ -179,9 +194,62 @@ function analyseBreadth(raw, allowedBooks) {
consensus_eligible_n2_pct: pctOf(eligible2),
consensus_eligible_n3: eligible3,
consensus_eligible_n3_pct: pctOf(eligible3),
reference_policy_curve: Object.fromEntries(Object.entries(policyCurve).map(([k, v]) => [k, {
books: REFERENCE_POLICIES[k],
n2: v.n2, n2_pct: pctOf(v.n2), n3: v.n3, n3_pct: pctOf(v.n3),
}])),
};
}
/**
* REDACTION DETECTION. PropLine's free tier returns the full STRUCTURE of
* tier-gated endpoints with the values stripped, plus an `upgrade_url`. A
* non-empty body is therefore NOT proof of access — the first pass classified
* /odds/closing as "works" on structure alone. This counts actual prices.
*
* Returns { outcomes, priced, redacted } — `redacted: true` when the body
* advertises an upgrade or carries outcomes with no prices at all.
*/
function detectRedaction(body) {
if (!body || typeof body !== 'object') return null;
let outcomes = 0;
let priced = 0;
const walkBooks = (books) => {
for (const bm of books || []) {
for (const mk of (bm && bm.markets) || []) {
for (const oc of (mk && mk.outcomes) || []) {
outcomes += 1;
if (oc && oc.price != null) priced += 1;
}
}
}
};
walkBooks(body.bookmakers);
const advertised = body.upgrade_url != null || body.redacted === true
|| (typeof body.redacted === 'string' && body.redacted.length > 0);
return {
outcomes,
priced,
steam_count: Array.isArray(body.steam) ? body.steam.length : null,
advertises_upgrade: !!advertised,
redacted: advertised || (outcomes > 0 && priced === 0),
};
}
/**
* Consensus coverage under several candidate REFERENCE-SET policies, so the
* ruler decision is made on a coverage/quality curve rather than on one
* hard-coded set. DFS is absent from every policy by construction.
*/
const REFERENCE_POLICIES = Object.freeze({
exchange_only: ['novig', 'smarkets', 'kalshi', 'matchbook', 'polymarket'],
exchange_plus_sharp:['novig', 'smarkets', 'kalshi', 'matchbook', 'polymarket', 'pinnacle', 'bovada'],
exchange_plus_us: ['novig', 'smarkets', 'kalshi', 'matchbook', 'polymarket', 'pinnacle', 'bovada',
'draftkings', 'betmgm', 'betrivers', 'fanduel'],
takeable_only: ['draftkings', 'fanduel', 'betmgm', 'betrivers'],
});
/** One read-only probe. Never throws; classifies works / partial / no. */
async function probe(path, params, note, httpGet) {
const get = httpGet || axios.get;
@@ -216,8 +284,12 @@ async function probe(path, params, note, httpGet) {
verdict = 'partial'; // reachable, nothing on file for this target
}
const redaction = detectRedaction(body);
if (verdict === 'works' && redaction && redaction.redacted) verdict = 'partial';
return {
path, verdict, status, note,
...(redaction ? { redaction } : {}),
...(rows != null ? { csv_rows: rows } : {}),
...(Object.keys(headers).length ? { headers } : {}),
shape: scrubKeys(summarise(body)).slice(0, 700),
@@ -285,8 +357,12 @@ async function verify(opts = {}) {
}
}
// 4 — sport coverage (rewrites rollout economics if it settles NBA/soccer).
// 4 — sport coverage + graded VOLUME per sport. `/exports/resolved-props`
// being 403 tells us we can't PULL settlements; resolution-summary tells us
// whether they EXIST to be bought. Two different questions.
out.endpoints.push(await probe('/sports', {}, 'sport coverage — which sports PropLine carries', httpGet));
out.endpoints.push(await probe('/markets/resolution-summary', { days: 30 },
'does PropLine actually GRADE nba/soccer? (the $19/mo question)', httpGet));
// 3 — the four documented-but-unverified endpoints, against a REAL event.
for (const sport of sports) {
@@ -311,4 +387,4 @@ async function verify(opts = {}) {
return out;
}
module.exports = { verify, __internals: { analyseBreadth, scrubKeys, probe, summarise, REFERENCE_CANDIDATES, DFS_PLATFORMS, BASE } };
module.exports = { verify, __internals: { analyseBreadth, scrubKeys, probe, summarise, detectRedaction, REFERENCE_CANDIDATES, REFERENCE_POLICIES, DFS_PLATFORMS, BASE } };
+53 -1
View File
@@ -10,7 +10,7 @@
*/
const { __internals } = require('../../src/services/proplineVerify');
const { analyseBreadth, scrubKeys, probe, summarise, REFERENCE_CANDIDATES, DFS_PLATFORMS } = __internals;
const { analyseBreadth, scrubKeys, probe, summarise, detectRedaction, REFERENCE_CANDIDATES, REFERENCE_POLICIES, DFS_PLATFORMS } = __internals;
const ALLOWED = new Set(['draftkings', 'fanduel', 'betmgm', 'betrivers', 'pinnacle']);
@@ -144,3 +144,55 @@ describe('proplineVerify — breadth', () => {
expect(summarise([{ a: 1 }])).toContain('array(1)');
});
});
describe('proplineVerify — redaction detection', () => {
beforeEach(() => { process.env.PROPLINE_API_KEY_1 = 'k'; });
const priced = { bookmakers: [{ key: 'dk', markets: [{ outcomes: [{ price: -110 }, { price: -110 }] }] }] };
const stripped = { bookmakers: [{ key: 'dk', markets: [{ outcomes: [{ price: null }, {}] }] }], upgrade_url: 'https://x' };
it('flags a structure-only body with an upgrade_url as redacted', () => {
const r = detectRedaction(stripped);
expect(r.outcomes).toBe(2);
expect(r.priced).toBe(0);
expect(r.redacted).toBe(true);
});
it('does not flag a genuinely priced body', () => {
const r = detectRedaction(priced);
expect(r.priced).toBe(2);
expect(r.redacted).toBe(false);
});
it('DOWNGRADES a 200 redacted body from works to partial', async () => {
const httpGet = async () => ({ status: 200, data: stripped, headers: {} });
const r = await probe('/x', {}, '', httpGet);
expect(r.verdict).toBe('partial'); // a non-empty body is NOT proof of access
expect(r.redaction.redacted).toBe(true);
});
it('keeps works when the body carries real prices', async () => {
const httpGet = async () => ({ status: 200, data: priced, headers: {} });
expect((await probe('/x', {}, '', httpGet)).verdict).toBe('works');
});
});
describe('proplineVerify — reference policies', () => {
it('no policy admits a DFS platform', () => {
for (const books of Object.values(REFERENCE_POLICIES)) {
for (const dfs of DFS_PLATFORMS) expect(books).not.toContain(dfs);
}
});
it('the curve widens monotonically as the policy widens', () => {
const raw = [event('e1', {
novig: [{ player: 'A', point: 1.5, over: -104, under: -104 }],
draftkings: [{ player: 'A', point: 1.5, over: -110, under: -110 }],
betmgm: [{ player: 'A', point: 1.5, over: -112, under: -108 }],
})];
const c = analyseBreadth(raw, ALLOWED).reference_policy_curve;
expect(c.exchange_only.n2).toBe(0); // one exchange only
expect(c.exchange_plus_us.n2).toBe(1); // exchange + two US books
expect(c.takeable_only.n2).toBe(1);
});
});