Introduce a liveGroupingScoring engine that combines existing membership signals (normalized name, brand, number, metadata tokens, language, category/source when relevant) with a bounded Laya contribution: Laya raises or lowers confidence instead of acting as a final judge, with guard-rails against obvious contradictions. Sanitize Laya batch identity inputs in layaService and wire the weighted scoring into the live projection service. Weights are not exposed in the Nodecast UI. Cover signal combination, bounds and contradiction guard-rails with tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
113 lines
4.2 KiB
JavaScript
113 lines
4.2 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const {
|
|
MODE_WEIGHTS,
|
|
hardBlock,
|
|
scoreCandidate
|
|
} = require('../server/services/liveGroupingScoring');
|
|
const {
|
|
collectIdentityHints,
|
|
normalizeChannelName,
|
|
projectCatalog
|
|
} = require('../server/services/liveProjectionService');
|
|
|
|
const channel = (id, name, extra = {}) => ({
|
|
id,
|
|
source_id: 1,
|
|
item_id: id,
|
|
name,
|
|
category_id: 'sports',
|
|
fingerprint: normalizeChannelName(name),
|
|
...extra
|
|
});
|
|
|
|
test('weights match the validated architecture and remain bounded', () => {
|
|
assert.deepEqual(MODE_WEIGHTS.algo, { algorithm: 80, structure: 15, metadata: 5, laya: 0 });
|
|
assert.deepEqual(MODE_WEIGHTS.hybrid, { algorithm: 70, structure: 15, metadata: 5, laya: 10 });
|
|
assert.deepEqual(MODE_WEIGHTS.laya, { algorithm: 55, structure: 15, metadata: 5, laya: 25 });
|
|
});
|
|
|
|
test('exact algorithm match merges without Laya', () => {
|
|
const left = channel('a', 'beIN Sports 1 HD');
|
|
const right = channel('b', 'beIN Sports 1 4K');
|
|
assert.equal(scoreCandidate(left, right, { mode: 'algo' }).matched, true);
|
|
});
|
|
|
|
test('exact algorithm match survives a contradictory Laya opinion', () => {
|
|
const left = channel('a', 'beIN Sports 1 HD');
|
|
const right = channel('b', 'beIN Sports 1 4K');
|
|
const score = scoreCandidate(left, right, {
|
|
mode: 'hybrid',
|
|
layaHint: { sameIdentityProbability: 0, confidence: 1, number: '2' }
|
|
});
|
|
assert.equal(score.matched, true);
|
|
assert.equal(score.signals.laya, 0);
|
|
});
|
|
|
|
test('different channel numbers block even when Laya proposes a merge', () => {
|
|
const left = channel('a', 'beIN Sports 1');
|
|
const right = channel('b', 'beIN Sports 2');
|
|
const score = scoreCandidate(left, right, {
|
|
mode: 'laya', layaHint: { sameIdentityProbability: 1, confidence: 1 }
|
|
});
|
|
assert.equal(score.matched, false);
|
|
assert.equal(score.blocker, 'number_mismatch');
|
|
});
|
|
|
|
test('protected max token blocks beIN Sports Max 10 from beIN Sports 1', () => {
|
|
assert.equal(hardBlock(channel('a', 'beIN Sports Max 10'), channel('b', 'beIN Sports 1')), 'protected_token_mismatch');
|
|
assert.equal(projectCatalog([
|
|
channel('a', 'beIN Sports Max 10'), channel('b', 'beIN Sports 1')
|
|
], { mode: 'laya' }).length, 2);
|
|
});
|
|
|
|
test('different strong families are blocked before weighted scoring', () => {
|
|
assert.equal(hardBlock(channel('a', 'beIN Sports 1'), channel('b', 'Sky Sports 1')), 'strong_family_mismatch');
|
|
});
|
|
|
|
test('low-confidence Laya hint contributes zero', () => {
|
|
const score = scoreCandidate(
|
|
channel('a', 'beIN Sports 1 Event'),
|
|
channel('b', 'beIN Sports 1'),
|
|
{ mode: 'hybrid', layaHint: { sameIdentityProbability: 1, confidence: 0.64 } }
|
|
);
|
|
assert.equal(score.signals.laya, 0);
|
|
assert.equal(score.matched, false);
|
|
});
|
|
|
|
test('No Laya mode never calls the identity client', async () => {
|
|
let calls = 0;
|
|
const hints = await collectIdentityHints([
|
|
channel('a', 'beIN Sports 1 Event'), channel('b', 'beIN Sports 1')
|
|
], 'algo', { identityHintBatch: async () => { calls++; return []; } });
|
|
assert.equal(calls, 0);
|
|
assert.equal(hints.size, 0);
|
|
});
|
|
|
|
test('Laya only calls Laya but still respects blockers', async () => {
|
|
let calls = 0;
|
|
const items = [channel('a', 'beIN Sports 1 Event'), channel('b', 'beIN Sports 1')];
|
|
const hints = await collectIdentityHints(items, 'laya', {
|
|
identityHintBatch: async pairs => {
|
|
calls++;
|
|
return pairs.map(pair => ({ id: pair.id, sameIdentityProbability: 1, confidence: 1 }));
|
|
}
|
|
});
|
|
assert.equal(calls, 1);
|
|
assert.equal(projectCatalog(items, { mode: 'laya', identityHints: hints }).length, 1);
|
|
assert.equal(projectCatalog([
|
|
channel('1', 'beIN Sports 1'), channel('2', 'beIN Sports 2')
|
|
], { mode: 'laya', identityHints: hints }).length, 2);
|
|
});
|
|
|
|
test('hybrid mode caps the positive Laya contribution at weight 10', () => {
|
|
const score = scoreCandidate(
|
|
channel('a', 'beIN Sports 1 Event'),
|
|
channel('b', 'beIN Sports 1'),
|
|
{ mode: 'hybrid', layaHint: { sameIdentityProbability: 1, confidence: 5 } }
|
|
);
|
|
assert.equal(score.signals.laya, 1);
|
|
assert.equal(score.total, 72);
|
|
assert.equal(score.matched, true);
|
|
});
|