mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-25 08:48:08 +02:00
108 lines
3.6 KiB
TypeScript
108 lines
3.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
calibrateWeightsFromSyntheticFixtures,
|
|
defaultKtxRelationshipScoreWeights,
|
|
normalizeKtxRelationshipScoreWeights,
|
|
scoreKtxRelationshipCandidate,
|
|
type KtxRelationshipSignalVector,
|
|
} from './relationship-scoring.js';
|
|
|
|
function signals(overrides: Partial<KtxRelationshipSignalVector> = {}): KtxRelationshipSignalVector {
|
|
return {
|
|
nameSimilarity: 0.5,
|
|
typeCompatibility: 1,
|
|
valueOverlap: 0,
|
|
embeddingSimilarity: 0,
|
|
profileUniqueness: 0.5,
|
|
profileNullRate: 0.5,
|
|
structuralPrior: 0.5,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('relationship scoring', () => {
|
|
it('scores stronger evidence higher without hard-gating on names', () => {
|
|
const weakNameStrongProfile = scoreKtxRelationshipCandidate(
|
|
signals({
|
|
nameSimilarity: 0.05,
|
|
typeCompatibility: 1,
|
|
valueOverlap: 0.7,
|
|
profileUniqueness: 1,
|
|
profileNullRate: 1,
|
|
structuralPrior: 0.7,
|
|
}),
|
|
);
|
|
const strongNameWeakProfile = scoreKtxRelationshipCandidate(
|
|
signals({
|
|
nameSimilarity: 0.95,
|
|
typeCompatibility: 1,
|
|
valueOverlap: 0,
|
|
profileUniqueness: 0.3,
|
|
profileNullRate: 0.4,
|
|
structuralPrior: 0.5,
|
|
}),
|
|
);
|
|
|
|
expect(weakNameStrongProfile.score).toBeGreaterThan(strongNameWeakProfile.score);
|
|
expect(weakNameStrongProfile.contributions.profileUniqueness).toBeGreaterThan(0);
|
|
expect(weakNameStrongProfile.contributions.nameSimilarity).toBeLessThan(0.02);
|
|
});
|
|
|
|
it('normalizes partial and invalid weights into a usable vector', () => {
|
|
const weights = normalizeKtxRelationshipScoreWeights({
|
|
nameSimilarity: 3,
|
|
typeCompatibility: -1,
|
|
valueOverlap: Number.POSITIVE_INFINITY,
|
|
profileUniqueness: 1,
|
|
});
|
|
|
|
const total = Object.values(weights).reduce((sum, value) => sum + value, 0);
|
|
expect(total).toBeCloseTo(1, 6);
|
|
expect(weights.nameSimilarity).toBeGreaterThan(weights.profileUniqueness);
|
|
expect(weights.typeCompatibility).toBe(0);
|
|
expect(weights.valueOverlap).toBe(0);
|
|
});
|
|
|
|
it('returns deterministic defaults as a defensive copy', () => {
|
|
const first = defaultKtxRelationshipScoreWeights();
|
|
const second = defaultKtxRelationshipScoreWeights();
|
|
|
|
expect(first).toEqual(second);
|
|
expect(first).not.toBe(second);
|
|
expect(Object.values(first).reduce((sum, value) => sum + value, 0)).toBeCloseTo(1, 6);
|
|
});
|
|
|
|
it('calibrates only from synthetic observations', () => {
|
|
expect(() =>
|
|
calibrateWeightsFromSyntheticFixtures([
|
|
{
|
|
fixtureId: 'chinook_with_declared_metadata',
|
|
origin: 'public',
|
|
expectedRelationship: true,
|
|
signals: signals({ nameSimilarity: 1 }),
|
|
},
|
|
]),
|
|
).toThrow(/synthetic/i);
|
|
});
|
|
|
|
it('calibrates deterministic weights from positive and negative synthetic observations', () => {
|
|
const weights = calibrateWeightsFromSyntheticFixtures([
|
|
{
|
|
fixtureId: 'synthetic_positive',
|
|
origin: 'synthetic',
|
|
expectedRelationship: true,
|
|
signals: signals({ nameSimilarity: 0.8, valueOverlap: 0.9, profileUniqueness: 1, profileNullRate: 1 }),
|
|
},
|
|
{
|
|
fixtureId: 'synthetic_negative',
|
|
origin: 'synthetic',
|
|
expectedRelationship: false,
|
|
signals: signals({ nameSimilarity: 0.2, valueOverlap: 0.1, profileUniqueness: 0.4, profileNullRate: 0.5 }),
|
|
},
|
|
]);
|
|
|
|
expect(Object.values(weights).reduce((sum, value) => sum + value, 0)).toBeCloseTo(1, 6);
|
|
expect(weights.valueOverlap).toBeGreaterThan(weights.structuralPrior);
|
|
expect(weights.profileUniqueness).toBeGreaterThan(weights.embeddingSimilarity);
|
|
});
|
|
});
|