fix: handle unjustified unknown selection ties

This commit is contained in:
2026-08-02 16:09:00 +01:00
parent a1f6d0c2b9
commit 51ce356218
8 changed files with 731 additions and 145 deletions
+200 -62
View File
@@ -216,6 +216,119 @@ function buildScoreContributions(
return contributions;
}
function getMeaningfulSemanticContributions(contributions = []) {
return contributions
.filter(
(contribution) =>
contribution.rule !== "downstream_dependencies" &&
contribution.rule !== "unresolved_prerequisite_penalty" &&
contribution.delta !== 0,
)
.map((contribution) => ({
rule: contribution.rule,
delta: contribution.delta,
}));
}
function buildCandidateDisplayOrder(candidates) {
return [...candidates].sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
if (b.downstreamCount !== a.downstreamCount) {
return b.downstreamCount - a.downstreamCount;
}
if (a.unresolvedParentUnknownCount !== b.unresolvedParentUnknownCount) {
return a.unresolvedParentUnknownCount - b.unresolvedParentUnknownCount;
}
return a.label.localeCompare(b.label);
});
}
function semanticSignature(candidate) {
return JSON.stringify(
getMeaningfulSemanticContributions(candidate.contributions),
);
}
function classifyCandidateOrdering(candidates) {
const displayOrder = buildCandidateDisplayOrder(candidates);
const best = displayOrder[0] ?? null;
if (!best) {
return {
displayOrder,
best: null,
leadingCandidates: [],
status: "no_candidates",
tieType: "none",
usedAlphabeticalOrdering: false,
reason: "No unresolved unknown candidates remain.",
};
}
const topScoreCandidates = displayOrder.filter(
(candidate) => candidate.score === best.score,
);
if (topScoreCandidates.length === 1) {
return {
displayOrder,
best,
leadingCandidates: [best],
status: "selected",
tieType: "none",
usedAlphabeticalOrdering: false,
reason: `Clear winner by total score (${best.score}).`,
};
}
const topStructuralCandidates = topScoreCandidates.filter(
(candidate) =>
candidate.downstreamCount === best.downstreamCount &&
candidate.unresolvedParentUnknownCount ===
best.unresolvedParentUnknownCount,
);
if (topStructuralCandidates.length === 1) {
return {
displayOrder,
best,
leadingCandidates: [best],
status: "selected",
tieType: "structural_tie",
usedAlphabeticalOrdering: false,
reason:
"Score tie was resolved by downstream dependency count or prerequisite ordering.",
};
}
const topSemanticSignature = semanticSignature(best);
const semanticPeers = topStructuralCandidates.filter(
(candidate) => semanticSignature(candidate) === topSemanticSignature,
);
if (semanticPeers.length !== topStructuralCandidates.length) {
return {
displayOrder,
best: null,
leadingCandidates: topStructuralCandidates,
status: "ambiguous",
tieType: "semantic_tie",
usedAlphabeticalOrdering: false,
reason:
"Leading candidates remain tied after score and structural checks, but differ in semantic contribution patterns.",
};
}
return {
displayOrder,
best: null,
leadingCandidates: topStructuralCandidates,
status: "ambiguous",
tieType: "complete_unresolved_tie",
usedAlphabeticalOrdering: false,
reason: "No justified distinction between leading unknowns.",
};
}
export function scoreUnknownCandidate(graph, node, resolvedNodeIds = []) {
const text = collectNodeText(node);
const matches = classifyUnknownPriority(text);
@@ -490,21 +603,36 @@ export function selectActiveUnknownCandidate(graph, resolvedNodeIds) {
...scoreUnknownCandidate(graph, node, resolvedNodeIds),
}));
scoredCandidates.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
if (b.downstreamCount !== a.downstreamCount) {
return b.downstreamCount - a.downstreamCount;
}
if (a.unresolvedParentUnknownCount !== b.unresolvedParentUnknownCount) {
return a.unresolvedParentUnknownCount - b.unresolvedParentUnknownCount;
}
return a.node.label.localeCompare(b.node.label);
});
const selection = classifyCandidateOrdering(
scoredCandidates.map(({ node, ...candidate }) => ({
...candidate,
node,
})),
);
const best = scoredCandidates[0];
if (selection.status === "ambiguous") {
return {
selectedNode: null,
status: "ambiguous",
tieType: selection.tieType,
tiedCandidateIds: selection.leadingCandidates.map(
(candidate) => candidate.nodeId,
),
displayOrder: selection.displayOrder.map((candidate) => candidate.nodeId),
reason: selection.reason,
};
}
const best = selection.best;
if (!best) return null;
return {
selectedNode: {
nodeId: best.node.id,
label: best.node.label,
},
status: "selected",
tieType: selection.tieType,
nodeId: best.node.id,
label: best.node.label,
score: best.score,
@@ -522,7 +650,10 @@ export function explainUnknownSelection(graph, resolvedNodeIds = []) {
return {
selectedNodeId: null,
selectedNodeLabel: null,
status: "no_candidates",
tieType: "none",
resolvedNodeIds: [...resolvedNodeIds],
tiedCandidateIds: [],
candidates: [],
competitors: [],
tieBreakOrder: [
@@ -543,68 +674,75 @@ export function explainUnknownSelection(graph, resolvedNodeIds = []) {
...scoreUnknownCandidate(graph, node, resolvedNodeIds),
}));
candidates.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
if (b.downstreamCount !== a.downstreamCount) {
return b.downstreamCount - a.downstreamCount;
}
if (a.unresolvedParentUnknownCount !== b.unresolvedParentUnknownCount) {
return a.unresolvedParentUnknownCount - b.unresolvedParentUnknownCount;
}
return a.label.localeCompare(b.label);
});
const selected = candidates[0];
const competitors = candidates.slice(1).map((candidate) => ({
nodeId: candidate.nodeId,
label: candidate.label,
score: candidate.score,
downstreamCount: candidate.downstreamCount,
unresolvedParentUnknownCount: candidate.unresolvedParentUnknownCount,
matches: candidate.matches,
contributions: candidate.contributions,
outrankedBy: {
scoreDelta: selected.score - candidate.score,
downstreamDelta: selected.downstreamCount - candidate.downstreamCount,
unresolvedPrerequisiteDelta:
candidate.unresolvedParentUnknownCount -
selected.unresolvedParentUnknownCount,
labelOrderWinner:
selected.score === candidate.score &&
selected.downstreamCount === candidate.downstreamCount &&
selected.unresolvedParentUnknownCount ===
candidate.unresolvedParentUnknownCount
? selected.label.localeCompare(candidate.label) <= 0
? selected.label
: candidate.label
: null,
},
}));
const selection = classifyCandidateOrdering(candidates);
const orderedCandidates = selection.displayOrder;
const selected = selection.best;
const competitors = orderedCandidates
.filter((candidate) => candidate.nodeId !== selected?.nodeId)
.map((candidate) => ({
nodeId: candidate.nodeId,
label: candidate.label,
score: candidate.score,
downstreamCount: candidate.downstreamCount,
unresolvedParentUnknownCount: candidate.unresolvedParentUnknownCount,
matches: candidate.matches,
contributions: candidate.contributions,
outrankedBy: {
scoreDelta: (selected?.score ?? candidate.score) - candidate.score,
downstreamDelta:
(selected?.downstreamCount ?? candidate.downstreamCount) -
candidate.downstreamCount,
unresolvedPrerequisiteDelta:
candidate.unresolvedParentUnknownCount -
(selected?.unresolvedParentUnknownCount ??
candidate.unresolvedParentUnknownCount),
labelOrderWinner:
selected &&
selected.score === candidate.score &&
selected.downstreamCount === candidate.downstreamCount &&
selected.unresolvedParentUnknownCount ===
candidate.unresolvedParentUnknownCount
? selected.label.localeCompare(candidate.label) <= 0
? selected.label
: candidate.label
: null,
},
}));
return {
selectedNodeId: selected.nodeId,
selectedNodeLabel: selected.label,
selectedNodeId: selected?.nodeId ?? null,
selectedNodeLabel: selected?.label ?? null,
status: selection.status,
tieType: selection.tieType,
resolvedNodeIds: [...resolvedNodeIds],
tiedCandidateIds: selection.leadingCandidates.map(
(candidate) => candidate.nodeId,
),
tieBreakOrder: [
"score_desc",
"downstreamCount_desc",
"unresolvedParentUnknownCount_asc",
"label_asc",
],
candidates,
selected: {
nodeId: selected.nodeId,
label: selected.label,
score: selected.score,
downstreamCount: selected.downstreamCount,
unresolvedParentUnknownCount: selected.unresolvedParentUnknownCount,
matches: selected.matches,
contributions: selected.contributions,
},
alphabeticalUsedAsReasoning: false,
candidates: orderedCandidates,
selected: selected
? {
nodeId: selected.nodeId,
label: selected.label,
score: selected.score,
downstreamCount: selected.downstreamCount,
unresolvedParentUnknownCount: selected.unresolvedParentUnknownCount,
matches: selected.matches,
contributions: selected.contributions,
}
: null,
competitors,
summary: {
candidateCount: candidates.length,
selectedReason: `highest_score=${selected.score}; downstream=${selected.downstreamCount}; unresolved_prerequisites=${selected.unresolvedParentUnknownCount}`,
candidateCount: orderedCandidates.length,
selectedReason: selected
? `highest_score=${selected.score}; downstream=${selected.downstreamCount}; unresolved_prerequisites=${selected.unresolvedParentUnknownCount}`
: selection.reason,
},
};
}