mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
fix(ingestion): calculate confidence per resolution tier for heritage/MRO edges (#412)
Instead of hardcoding confidence: 1.0, compute it at ingestion time using
the same resolution tier system that CALLS edges already use.
Heritage edges (EXTENDS, IMPLEMENTS):
- resolveHeritageId now returns { id, confidence } using TIER_CONFIDENCE
- Same-file → 0.95, import-scoped → 0.9, global → 0.5
- Edge confidence = geometric mean of source and target confidence
(principled for partially-correlated cross-scope estimates, per
Dillig et al. POPL 2011 and Dempster-Shafer theory)
MRO edges (OVERRIDES):
- MRO-ordered → 0.9, class method wins → 0.95
- Single interface → 0.85, ambiguous/unresolved → 0.5
IMPORTS and CONTAINS intentionally keep 1.0 (deterministic).
Closes #412
This commit is contained in:
parent
b75e380bd0
commit
d206bf6772
5 changed files with 251 additions and 53 deletions
|
|
@ -25,6 +25,7 @@ import { SupportedLanguages } from '../../config/supported-languages.js';
|
|||
import { getTreeSitterBufferSize } from './constants.js';
|
||||
import type { ExtractedHeritage } from './workers/parse-worker.js';
|
||||
import type { ResolutionContext } from './resolution-context.js';
|
||||
import { TIER_CONFIDENCE } from './resolution-context.js';
|
||||
|
||||
/** C#/Java convention: interfaces start with I followed by an uppercase letter */
|
||||
const INTERFACE_NAME_RE = /^I[A-Z]/;
|
||||
|
|
@ -66,22 +67,28 @@ const resolveExtendsType = (
|
|||
* Resolve a symbol ID for heritage, with fallback to generated ID.
|
||||
* Uses ctx.resolve() → pick first candidate's nodeId → generate synthetic ID.
|
||||
*/
|
||||
interface ResolvedHeritage {
|
||||
readonly id: string;
|
||||
readonly confidence: number;
|
||||
}
|
||||
|
||||
const resolveHeritageId = (
|
||||
name: string,
|
||||
filePath: string,
|
||||
ctx: ResolutionContext,
|
||||
fallbackLabel: string,
|
||||
fallbackKey?: string,
|
||||
): string => {
|
||||
): ResolvedHeritage => {
|
||||
const resolved = ctx.resolve(name, filePath);
|
||||
if (resolved && resolved.candidates.length > 0) {
|
||||
// For global with multiple candidates, refuse (a wrong edge is worse than no edge)
|
||||
if (resolved.tier === 'global' && resolved.candidates.length > 1) {
|
||||
return generateId(fallbackLabel, fallbackKey ?? name);
|
||||
return { id: generateId(fallbackLabel, fallbackKey ?? name), confidence: TIER_CONFIDENCE['global'] };
|
||||
}
|
||||
return resolved.candidates[0].nodeId;
|
||||
return { id: resolved.candidates[0].nodeId, confidence: TIER_CONFIDENCE[resolved.tier] };
|
||||
}
|
||||
return generateId(fallbackLabel, fallbackKey ?? name);
|
||||
// Unresolved: use global-tier confidence as fallback
|
||||
return { id: generateId(fallbackLabel, fallbackKey ?? name), confidence: TIER_CONFIDENCE['global'] };
|
||||
};
|
||||
|
||||
export const processHeritage = async (
|
||||
|
|
@ -163,16 +170,16 @@ export const processHeritage = async (
|
|||
|
||||
const { type: relType, idPrefix } = resolveExtendsType(parentClassName, file.path, ctx, language);
|
||||
|
||||
const childId = resolveHeritageId(className, file.path, ctx, 'Class', `${file.path}:${className}`);
|
||||
const parentId = resolveHeritageId(parentClassName, file.path, ctx, idPrefix);
|
||||
const child = resolveHeritageId(className, file.path, ctx, 'Class', `${file.path}:${className}`);
|
||||
const parent = resolveHeritageId(parentClassName, file.path, ctx, idPrefix);
|
||||
|
||||
if (childId && parentId && childId !== parentId) {
|
||||
if (child.id && parent.id && child.id !== parent.id) {
|
||||
graph.addRelationship({
|
||||
id: generateId(relType, `${childId}->${parentId}`),
|
||||
sourceId: childId,
|
||||
targetId: parentId,
|
||||
id: generateId(relType, `${child.id}->${parent.id}`),
|
||||
sourceId: child.id,
|
||||
targetId: parent.id,
|
||||
type: relType,
|
||||
confidence: 1.0,
|
||||
confidence: Math.sqrt(child.confidence * parent.confidence),
|
||||
reason: '',
|
||||
});
|
||||
}
|
||||
|
|
@ -183,16 +190,16 @@ export const processHeritage = async (
|
|||
const className = captureMap['heritage.class'].text;
|
||||
const interfaceName = captureMap['heritage.implements'].text;
|
||||
|
||||
const classId = resolveHeritageId(className, file.path, ctx, 'Class', `${file.path}:${className}`);
|
||||
const interfaceId = resolveHeritageId(interfaceName, file.path, ctx, 'Interface');
|
||||
const cls = resolveHeritageId(className, file.path, ctx, 'Class', `${file.path}:${className}`);
|
||||
const iface = resolveHeritageId(interfaceName, file.path, ctx, 'Interface');
|
||||
|
||||
if (classId && interfaceId) {
|
||||
if (cls.id && iface.id) {
|
||||
graph.addRelationship({
|
||||
id: generateId('IMPLEMENTS', `${classId}->${interfaceId}`),
|
||||
sourceId: classId,
|
||||
targetId: interfaceId,
|
||||
id: generateId('IMPLEMENTS', `${cls.id}->${iface.id}`),
|
||||
sourceId: cls.id,
|
||||
targetId: iface.id,
|
||||
type: 'IMPLEMENTS',
|
||||
confidence: 1.0,
|
||||
confidence: Math.sqrt(cls.confidence * iface.confidence),
|
||||
reason: '',
|
||||
});
|
||||
}
|
||||
|
|
@ -203,16 +210,16 @@ export const processHeritage = async (
|
|||
const structName = captureMap['heritage.class'].text;
|
||||
const traitName = captureMap['heritage.trait'].text;
|
||||
|
||||
const structId = resolveHeritageId(structName, file.path, ctx, 'Struct', `${file.path}:${structName}`);
|
||||
const traitId = resolveHeritageId(traitName, file.path, ctx, 'Trait');
|
||||
const strct = resolveHeritageId(structName, file.path, ctx, 'Struct', `${file.path}:${structName}`);
|
||||
const trait = resolveHeritageId(traitName, file.path, ctx, 'Trait');
|
||||
|
||||
if (structId && traitId) {
|
||||
if (strct.id && trait.id) {
|
||||
graph.addRelationship({
|
||||
id: generateId('IMPLEMENTS', `${structId}->${traitId}`),
|
||||
sourceId: structId,
|
||||
targetId: traitId,
|
||||
id: generateId('IMPLEMENTS', `${strct.id}->${trait.id}`),
|
||||
sourceId: strct.id,
|
||||
targetId: trait.id,
|
||||
type: 'IMPLEMENTS',
|
||||
confidence: 1.0,
|
||||
confidence: Math.sqrt(strct.confidence * trait.confidence),
|
||||
reason: 'trait-impl',
|
||||
});
|
||||
}
|
||||
|
|
@ -256,44 +263,44 @@ export const processHeritageFromExtracted = async (
|
|||
if (!fileLanguage) continue;
|
||||
const { type: relType, idPrefix } = resolveExtendsType(h.parentName, h.filePath, ctx, fileLanguage);
|
||||
|
||||
const childId = resolveHeritageId(h.className, h.filePath, ctx, 'Class', `${h.filePath}:${h.className}`);
|
||||
const parentId = resolveHeritageId(h.parentName, h.filePath, ctx, idPrefix);
|
||||
const child = resolveHeritageId(h.className, h.filePath, ctx, 'Class', `${h.filePath}:${h.className}`);
|
||||
const parent = resolveHeritageId(h.parentName, h.filePath, ctx, idPrefix);
|
||||
|
||||
if (childId && parentId && childId !== parentId) {
|
||||
if (child.id && parent.id && child.id !== parent.id) {
|
||||
graph.addRelationship({
|
||||
id: generateId(relType, `${childId}->${parentId}`),
|
||||
sourceId: childId,
|
||||
targetId: parentId,
|
||||
id: generateId(relType, `${child.id}->${parent.id}`),
|
||||
sourceId: child.id,
|
||||
targetId: parent.id,
|
||||
type: relType,
|
||||
confidence: 1.0,
|
||||
confidence: Math.sqrt(child.confidence * parent.confidence),
|
||||
reason: '',
|
||||
});
|
||||
}
|
||||
} else if (h.kind === 'implements') {
|
||||
const classId = resolveHeritageId(h.className, h.filePath, ctx, 'Class', `${h.filePath}:${h.className}`);
|
||||
const interfaceId = resolveHeritageId(h.parentName, h.filePath, ctx, 'Interface');
|
||||
const cls = resolveHeritageId(h.className, h.filePath, ctx, 'Class', `${h.filePath}:${h.className}`);
|
||||
const iface = resolveHeritageId(h.parentName, h.filePath, ctx, 'Interface');
|
||||
|
||||
if (classId && interfaceId) {
|
||||
if (cls.id && iface.id) {
|
||||
graph.addRelationship({
|
||||
id: generateId('IMPLEMENTS', `${classId}->${interfaceId}`),
|
||||
sourceId: classId,
|
||||
targetId: interfaceId,
|
||||
id: generateId('IMPLEMENTS', `${cls.id}->${iface.id}`),
|
||||
sourceId: cls.id,
|
||||
targetId: iface.id,
|
||||
type: 'IMPLEMENTS',
|
||||
confidence: 1.0,
|
||||
confidence: Math.sqrt(cls.confidence * iface.confidence),
|
||||
reason: '',
|
||||
});
|
||||
}
|
||||
} else if (h.kind === 'trait-impl' || h.kind === 'include' || h.kind === 'extend' || h.kind === 'prepend') {
|
||||
const structId = resolveHeritageId(h.className, h.filePath, ctx, 'Struct', `${h.filePath}:${h.className}`);
|
||||
const traitId = resolveHeritageId(h.parentName, h.filePath, ctx, 'Trait');
|
||||
const strct = resolveHeritageId(h.className, h.filePath, ctx, 'Struct', `${h.filePath}:${h.className}`);
|
||||
const trait = resolveHeritageId(h.parentName, h.filePath, ctx, 'Trait');
|
||||
|
||||
if (structId && traitId) {
|
||||
if (strct.id && trait.id) {
|
||||
graph.addRelationship({
|
||||
id: generateId('IMPLEMENTS', `${structId}->${traitId}:${h.kind}`),
|
||||
sourceId: structId,
|
||||
targetId: traitId,
|
||||
id: generateId('IMPLEMENTS', `${strct.id}->${trait.id}:${h.kind}`),
|
||||
sourceId: strct.id,
|
||||
targetId: trait.id,
|
||||
type: 'IMPLEMENTS',
|
||||
confidence: 1.0,
|
||||
confidence: Math.sqrt(strct.confidence * trait.confidence),
|
||||
reason: h.kind,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ function c3Linearize(
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
type MethodDef = { classId: string; className: string; methodId: string };
|
||||
type Resolution = { resolvedTo: string | null; reason: string };
|
||||
type Resolution = { resolvedTo: string | null; reason: string; confidence: number };
|
||||
|
||||
/** Resolve by MRO order — first ancestor in linearized order wins. */
|
||||
function resolveByMroOrder(
|
||||
|
|
@ -224,10 +224,11 @@ function resolveByMroOrder(
|
|||
return {
|
||||
resolvedTo: match.methodId,
|
||||
reason: `${reasonPrefix}: ${match.className}::${methodName}`,
|
||||
confidence: 0.9, // MRO-ordered resolution
|
||||
};
|
||||
}
|
||||
}
|
||||
return { resolvedTo: defs[0].methodId, reason: `${reasonPrefix} fallback: first definition` };
|
||||
return { resolvedTo: defs[0].methodId, reason: `${reasonPrefix} fallback: first definition`, confidence: 0.7 };
|
||||
}
|
||||
|
||||
function resolveCsharpJava(
|
||||
|
|
@ -251,6 +252,7 @@ function resolveCsharpJava(
|
|||
return {
|
||||
resolvedTo: classDefs[0].methodId,
|
||||
reason: `class method wins: ${classDefs[0].className}::${methodName}`,
|
||||
confidence: 0.95, // Class method is authoritative
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -258,6 +260,7 @@ function resolveCsharpJava(
|
|||
return {
|
||||
resolvedTo: null,
|
||||
reason: `ambiguous: ${methodName} defined in multiple interfaces: ${interfaceDefs.map(d => d.className).join(', ')}`,
|
||||
confidence: 0.5,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -265,10 +268,11 @@ function resolveCsharpJava(
|
|||
return {
|
||||
resolvedTo: interfaceDefs[0].methodId,
|
||||
reason: `single interface default: ${interfaceDefs[0].className}::${methodName}`,
|
||||
confidence: 0.85, // Single interface, unambiguous
|
||||
};
|
||||
}
|
||||
|
||||
return { resolvedTo: null, reason: 'no resolution found' };
|
||||
return { resolvedTo: null, reason: 'no resolution found', confidence: 0.5 };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -376,6 +380,7 @@ export function computeMRO(graph: KnowledgeGraph): MROResult {
|
|||
resolution = {
|
||||
resolvedTo: null,
|
||||
reason: `Rust requires qualified syntax: <Type as Trait>::${methodName}()`,
|
||||
confidence: 0.5,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
|
|
@ -402,7 +407,7 @@ export function computeMRO(graph: KnowledgeGraph): MROResult {
|
|||
sourceId: classId,
|
||||
targetId: resolution.resolvedTo,
|
||||
type: 'OVERRIDES',
|
||||
confidence: 1.0,
|
||||
confidence: resolution.confidence,
|
||||
reason: resolution.reason,
|
||||
});
|
||||
overrideEdges++;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,44 @@ export const VALID_NODE_LABELS = new Set([
|
|||
/** Valid relation types for impact analysis filtering */
|
||||
export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']);
|
||||
|
||||
/**
|
||||
* Per-relation-type confidence floor for impact analysis.
|
||||
*
|
||||
* When the graph stores a relation with a confidence value, that stored
|
||||
* value is used as-is (it reflects resolution-tier accuracy from analysis
|
||||
* time). This map provides the floor for each edge type when no stored
|
||||
* confidence is available, and is also used for display / tooltip hints.
|
||||
*
|
||||
* Rationale:
|
||||
* CALLS / IMPORTS – direct, strongly-typed references → 0.9
|
||||
* EXTENDS – class hierarchy, statically verifiable → 0.85
|
||||
* IMPLEMENTS – interface contract, statically verifiable → 0.85
|
||||
* OVERRIDES – method override, statically verifiable → 0.85
|
||||
* HAS_METHOD – structural containment → 0.95
|
||||
* HAS_PROPERTY – structural containment → 0.95
|
||||
* ACCESSES – field read/write, may be indirect → 0.8
|
||||
* CONTAINS – folder/file containment → 0.95
|
||||
* (unknown type) – conservative fallback → 0.5
|
||||
*/
|
||||
export const IMPACT_RELATION_CONFIDENCE: Readonly<Record<string, number>> = {
|
||||
CALLS: 0.9,
|
||||
IMPORTS: 0.9,
|
||||
EXTENDS: 0.85,
|
||||
IMPLEMENTS: 0.85,
|
||||
OVERRIDES: 0.85,
|
||||
HAS_METHOD: 0.95,
|
||||
HAS_PROPERTY: 0.95,
|
||||
ACCESSES: 0.8,
|
||||
CONTAINS: 0.95,
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the confidence floor for a given relation type.
|
||||
* Falls back to 0.5 for unknown types so they are not silently elevated.
|
||||
*/
|
||||
const confidenceForRelType = (relType: string | undefined): number =>
|
||||
IMPACT_RELATION_CONFIDENCE[relType ?? ''] ?? 0.5;
|
||||
|
||||
/** Regex to detect write operations in user-supplied Cypher queries */
|
||||
export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i;
|
||||
|
||||
|
|
@ -1445,14 +1483,22 @@ export class LocalBackend {
|
|||
if (!visited.has(relId)) {
|
||||
visited.add(relId);
|
||||
nextFrontier.push(relId);
|
||||
const storedConfidence = rel.confidence ?? rel[6];
|
||||
const relationType = rel.relType || rel[5];
|
||||
// Prefer the stored confidence from the graph (set at analysis time);
|
||||
// fall back to the per-type floor for edges without a stored value.
|
||||
const effectiveConfidence =
|
||||
typeof storedConfidence === 'number' && storedConfidence > 0
|
||||
? storedConfidence
|
||||
: confidenceForRelType(relationType);
|
||||
impacted.push({
|
||||
depth,
|
||||
id: relId,
|
||||
name: rel.name || rel[2],
|
||||
type: rel.type || rel[3],
|
||||
filePath,
|
||||
relationType: rel.relType || rel[5],
|
||||
confidence: rel.confidence || rel[6] || 1.0,
|
||||
relationType,
|
||||
confidence: effectiveConfidence,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ describe('processHeritageFromExtracted', () => {
|
|||
expect(rels).toHaveLength(1);
|
||||
expect(rels[0].sourceId).toBe('Class:src/admin.ts:AdminUser');
|
||||
expect(rels[0].targetId).toBe('Class:src/user.ts:User');
|
||||
expect(rels[0].confidence).toBe(1.0);
|
||||
expect(rels[0].confidence).toBeCloseTo(0.689202); // geometric mean: sqrt(same-file * global)
|
||||
});
|
||||
|
||||
it('uses generated ID when class not in symbol table', async () => {
|
||||
|
|
|
|||
140
gitnexus/test/unit/impact-confidence.test.ts
Normal file
140
gitnexus/test/unit/impact-confidence.test.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/**
|
||||
* Unit Tests: Impact confidence per relation type (#412)
|
||||
*
|
||||
* Tests IMPACT_RELATION_CONFIDENCE and confidenceForRelType (tested
|
||||
* indirectly) to verify that:
|
||||
*
|
||||
* 1. Each known relation type maps to the expected confidence floor.
|
||||
* 2. Unknown / undefined relation types fall back to 0.5 (conservative).
|
||||
* 3. Stored graph confidence is preferred over the type-based floor.
|
||||
* 4. Confidence values are in the valid 0–1 range.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { IMPACT_RELATION_CONFIDENCE, VALID_RELATION_TYPES } from '../../src/mcp/local/local-backend.js';
|
||||
|
||||
// ─── IMPACT_RELATION_CONFIDENCE — value assertions ────────────────────────
|
||||
|
||||
describe('IMPACT_RELATION_CONFIDENCE', () => {
|
||||
it('CALLS has confidence 0.9 (direct reference)', () => {
|
||||
expect(IMPACT_RELATION_CONFIDENCE['CALLS']).toBe(0.9);
|
||||
});
|
||||
|
||||
it('IMPORTS has confidence 0.9 (direct reference)', () => {
|
||||
expect(IMPACT_RELATION_CONFIDENCE['IMPORTS']).toBe(0.9);
|
||||
});
|
||||
|
||||
it('EXTENDS has confidence 0.85 (statically verifiable inheritance)', () => {
|
||||
expect(IMPACT_RELATION_CONFIDENCE['EXTENDS']).toBe(0.85);
|
||||
});
|
||||
|
||||
it('IMPLEMENTS has confidence 0.85 (statically verifiable contract)', () => {
|
||||
expect(IMPACT_RELATION_CONFIDENCE['IMPLEMENTS']).toBe(0.85);
|
||||
});
|
||||
|
||||
it('OVERRIDES has confidence 0.85 (statically verifiable override)', () => {
|
||||
expect(IMPACT_RELATION_CONFIDENCE['OVERRIDES']).toBe(0.85);
|
||||
});
|
||||
|
||||
it('HAS_METHOD has confidence 0.95 (structural containment)', () => {
|
||||
expect(IMPACT_RELATION_CONFIDENCE['HAS_METHOD']).toBe(0.95);
|
||||
});
|
||||
|
||||
it('HAS_PROPERTY has confidence 0.95 (structural containment)', () => {
|
||||
expect(IMPACT_RELATION_CONFIDENCE['HAS_PROPERTY']).toBe(0.95);
|
||||
});
|
||||
|
||||
it('ACCESSES has confidence 0.8 (may be indirect read/write)', () => {
|
||||
expect(IMPACT_RELATION_CONFIDENCE['ACCESSES']).toBe(0.8);
|
||||
});
|
||||
|
||||
it('CONTAINS has confidence 0.95 (folder/file structural containment)', () => {
|
||||
expect(IMPACT_RELATION_CONFIDENCE['CONTAINS']).toBe(0.95);
|
||||
});
|
||||
|
||||
it('all defined confidence values are in the valid [0, 1] range', () => {
|
||||
for (const [type, confidence] of Object.entries(IMPACT_RELATION_CONFIDENCE)) {
|
||||
expect(confidence, `${type} confidence out of range`).toBeGreaterThanOrEqual(0);
|
||||
expect(confidence, `${type} confidence out of range`).toBeLessThanOrEqual(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── confidenceForRelType — fallback semantics ────────────────────────────
|
||||
//
|
||||
// confidenceForRelType is not exported, so we replicate its logic here to
|
||||
// verify the semantics that the production code must uphold.
|
||||
|
||||
const confidenceForRelType = (relType: string | undefined): number =>
|
||||
IMPACT_RELATION_CONFIDENCE[relType ?? ''] ?? 0.5;
|
||||
|
||||
describe('confidenceForRelType', () => {
|
||||
it('returns the correct floor for known types', () => {
|
||||
expect(confidenceForRelType('CALLS')).toBe(0.9);
|
||||
expect(confidenceForRelType('IMPORTS')).toBe(0.9);
|
||||
expect(confidenceForRelType('EXTENDS')).toBe(0.85);
|
||||
expect(confidenceForRelType('IMPLEMENTS')).toBe(0.85);
|
||||
expect(confidenceForRelType('OVERRIDES')).toBe(0.85);
|
||||
expect(confidenceForRelType('HAS_METHOD')).toBe(0.95);
|
||||
expect(confidenceForRelType('HAS_PROPERTY')).toBe(0.95);
|
||||
expect(confidenceForRelType('ACCESSES')).toBe(0.8);
|
||||
expect(confidenceForRelType('CONTAINS')).toBe(0.95);
|
||||
});
|
||||
|
||||
it('returns 0.5 for unknown relation types', () => {
|
||||
expect(confidenceForRelType('UNKNOWN_EDGE')).toBe(0.5);
|
||||
expect(confidenceForRelType('SOME_FUTURE_TYPE')).toBe(0.5);
|
||||
expect(confidenceForRelType('')).toBe(0.5);
|
||||
});
|
||||
|
||||
it('returns 0.5 for undefined relation type', () => {
|
||||
expect(confidenceForRelType(undefined)).toBe(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Effective confidence selection — stored value wins ───────────────────
|
||||
//
|
||||
// Verify the priority logic: stored graph confidence beats the type floor.
|
||||
|
||||
describe('effective confidence selection (stored vs type-floor)', () => {
|
||||
const pickConfidence = (storedConfidence: number | undefined, relationType: string): number => {
|
||||
return typeof storedConfidence === 'number' && storedConfidence > 0
|
||||
? storedConfidence
|
||||
: confidenceForRelType(relationType);
|
||||
};
|
||||
|
||||
it('uses stored confidence when it is a positive number', () => {
|
||||
expect(pickConfidence(0.95, 'CALLS')).toBe(0.95);
|
||||
expect(pickConfidence(0.7, 'EXTENDS')).toBe(0.7);
|
||||
expect(pickConfidence(0.3, 'ACCESSES')).toBe(0.3);
|
||||
});
|
||||
|
||||
it('falls back to type floor when stored confidence is undefined', () => {
|
||||
expect(pickConfidence(undefined, 'CALLS')).toBe(0.9);
|
||||
expect(pickConfidence(undefined, 'EXTENDS')).toBe(0.85);
|
||||
expect(pickConfidence(undefined, 'UNKNOWN')).toBe(0.5);
|
||||
});
|
||||
|
||||
it('falls back to type floor when stored confidence is 0 (not a valid confidence)', () => {
|
||||
// 0 means "no confidence stored", not "zero confidence"
|
||||
expect(pickConfidence(0, 'CALLS')).toBe(0.9);
|
||||
expect(pickConfidence(0, 'IMPLEMENTS')).toBe(0.85);
|
||||
});
|
||||
|
||||
it('stored confidence can be lower than the type floor (respects analysis result)', () => {
|
||||
// If analysis determined a low-confidence match, honour it
|
||||
expect(pickConfidence(0.5, 'HAS_METHOD')).toBe(0.5); // floor is 0.95, stored wins
|
||||
});
|
||||
});
|
||||
|
||||
// ─── VALID_RELATION_TYPES consistency ─────────────────────────────────────
|
||||
|
||||
describe('IMPACT_RELATION_CONFIDENCE vs VALID_RELATION_TYPES', () => {
|
||||
it('every key in IMPACT_RELATION_CONFIDENCE except CONTAINS is in VALID_RELATION_TYPES', () => {
|
||||
// CONTAINS is a graph-internal structural type, not exposed in impact filters
|
||||
const skipInValid = new Set(['CONTAINS']);
|
||||
for (const type of Object.keys(IMPACT_RELATION_CONFIDENCE)) {
|
||||
if (skipInValid.has(type)) continue;
|
||||
expect(VALID_RELATION_TYPES.has(type), `${type} missing from VALID_RELATION_TYPES`).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue