Files
vyndr/tests/unit/ledgerAggregateScope.test.js
T
builtbykev 89a2977f57 Item 7 — public accuracy reads the CLEAN ledger; BEAT CLOSE hidden until C4
Kev's call: the 30D accuracy surfaces must read TRUTH, not a cache that can't be
filtered. My earlier degraded-row exclusion only touched getModelAggregate
(Postgres); the public buckets/badge still read outcomeService (Redis outcome
log), which counts degraded projection-0 outcomes and has no field to filter on.

- /api/accuracy (AccuracyBadge) + /api/ledger/accuracy (buckets/ModelRecord)
  now source from the clean Postgres ledger aggregate via new
  ledgerService.getAccuracyView + accuracyBucketsFromAgg (model_value > 0
  excludes degraded rows). Same response shapes → no frontend change. Redis
  outcome log is now read by nothing public; it can age out or be rebuilt.

- BEAT CLOSE is a MEASURED-WRONG ZERO: captureClosing re-records the locked line
  as the "closing" line, so clv is flat on the whole sample and beat_close reads
  0% (comparing a number to itself). Full write-up: specs/audit-data/
  clv-capture-broken.md (the fix belongs to C4). Until then, beat_close_pct +
  clv_distribution are SUPPRESSED at the source (getModelAggregate, gated by
  clvCaptureReliable() / CLV_CAPTURE_RELIABLE=1). Every public surface already
  renders BEAT CLOSE only when non-null, so they all hide it now — no wrong zero
  anywhere. HIT RATE (real) is unaffected.

Suite 271/3261 green, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:08:18 -04:00

80 lines
3.5 KiB
JavaScript

// A1 Session 10 — getModelAggregate scoping. The public default MUST stay
// `.is('user_id', null)` (the model record); the new `userId` option swaps
// it for `.eq('user_id', uid)` (a user's own public-profile aggregate).
// Same window, same n≥20 gate in both scopes.
const ledgerService = require('../../src/services/ledgerService');
function makeSb(captured, rows) {
return {
from() {
const b = { _filters: [] };
const rec = (op) => (...args) => { b._filters.push([op, ...args]); return b; };
b.select = () => b;
b.eq = rec('eq');
b.is = rec('is');
b.not = rec('not');
b.gte = rec('gte');
b.gt = rec('gt');
b.order = () => b;
b.limit = () => { captured.push(b._filters); return Promise.resolve({ data: rows, error: null, count: 0 }); };
b.then = (resolve, reject) => {
captured.push(b._filters);
return Promise.resolve({ data: rows, error: null, count: 2 }).then(resolve, reject);
};
return b;
},
};
}
describe('getModelAggregate scoping', () => {
test('default (no userId) scopes BOTH queries to the public record (user_id IS NULL)', async () => {
const captured = [];
const agg = await ledgerService.getModelAggregate({ sb: makeSb(captured, []) });
expect(captured.length).toBe(2); // settled + pending
for (const filters of captured) {
expect(filters).toContainEqual(['is', 'user_id', null]);
expect(filters.some((f) => f[0] === 'eq' && f[1] === 'user_id')).toBe(false);
}
expect(agg.hit_pct).toBeNull();
expect(agg.min_sample).toBe(ledgerService.MIN_AGG_SAMPLE);
// 2026-07 — projection<=0 grades (the degradation blast radius) are excluded
// from the public record on BOTH queries (kept in the append-only ledger,
// just never counted). `.gt('model_value', 0)` also drops NULL model_value.
for (const filters of captured) {
expect(filters).toContainEqual(['gt', 'model_value', 0]);
}
});
test('userId scopes BOTH queries to that user and never touches the public scope', async () => {
const captured = [];
await ledgerService.getModelAggregate({ sb: makeSb(captured, []), userId: 'u-42' });
expect(captured.length).toBe(2);
for (const filters of captured) {
expect(filters).toContainEqual(['eq', 'user_id', 'u-42']);
expect(filters.some((f) => f[0] === 'is' && f[1] === 'user_id')).toBe(false);
}
});
test('user scope keeps the n≥20 gate — no percentage on a small sample', async () => {
const rows = Array.from({ length: 5 }, () => ({ outcome: 'hit', clv_result: 'beat', grade: 'A' }));
const agg = await ledgerService.getModelAggregate({ sb: makeSb([], rows), userId: 'u-42' });
expect(agg.settled).toBe(5);
expect(agg.hit_pct).toBeNull();
expect(agg.beat_close_pct).toBeNull();
});
test('user scope renders percentages at n≥20 like the public record', async () => {
process.env.CLV_CAPTURE_RELIABLE = '1'; // exercise the CLV math (suppressed by default, item 7)
const rows = [
...Array.from({ length: 14 }, () => ({ outcome: 'hit', clv_result: 'beat', grade: 'A' })),
...Array.from({ length: 7 }, () => ({ outcome: 'miss', clv_result: 'faded', grade: 'B' })),
];
const agg = await ledgerService.getModelAggregate({ sb: makeSb([], rows), userId: 'u-42' });
expect(agg.settled).toBe(21);
expect(agg.hit_pct).toBe(Math.round((14 / 21) * 100));
expect(agg.beat_close_pct).toBe(Math.round((14 / 21) * 100));
delete process.env.CLV_CAPTURE_RELIABLE;
});
});