feat: evaluate deterministic cross-branch corroboration

This commit is contained in:
2026-08-03 07:19:20 +01:00
parent 1d64144e01
commit b2ffc54964
6 changed files with 547 additions and 7 deletions
+179 -4
View File
@@ -463,6 +463,129 @@ function confidenceFromAssessment(assessment) {
return assessment?.conclusionConfidence ?? "medium";
}
function unique(values = []) {
return [...new Set(values.filter(Boolean))];
}
function branchEvidenceKeys(node) {
return unique([...(node?.evidenceIds || []), node?.value]);
}
function sharedMeaningfulTokens(aText, bText) {
const stop = new Set([
"the",
"and",
"for",
"that",
"this",
"with",
"from",
"because",
"need",
"unknown",
"possible",
]);
const a = splitSemanticTokens(aText).filter((token) => !stop.has(token));
const b = splitSemanticTokens(bText).filter((token) => !stop.has(token));
return [...new Set(a.filter((token) => b.includes(token)))];
}
function branchConflictSignature(node) {
return normaliseText(
`${node?.label || ""} ${node?.description || ""} ${node?.value || ""}`,
);
}
function branchesConflict(aNode, bNode) {
const aText = branchConflictSignature(aNode);
const bText = branchConflictSignature(bNode);
const oppositePolarity =
(aText.includes("correctly") && bText.includes("incorrectly")) ||
(aText.includes("incorrectly") && bText.includes("correctly")) ||
aNode?.status === "contradicted" ||
bNode?.status === "contradicted";
if (!oppositePolarity) return false;
return sharedMeaningfulTokens(aText, bText).length >= 2;
}
export function evaluateBranchInteractions({ parentNode, graph }) {
const directBranches = findDirectChildUnknowns(graph, parentNode.id).filter(
(node) => ["resolved", "provisional", "contradicted"].includes(node.status),
);
const duplicateEvidenceGroups = [];
const conflictingBranches = [];
const corroboratingBranches = [];
const duplicateBranchIds = new Set();
const conflictingBranchIds = new Set();
const evidenceGroups = new Map();
for (const branch of directBranches) {
for (const evidenceKey of branchEvidenceKeys(branch)) {
const ids = evidenceGroups.get(evidenceKey) || [];
ids.push(branch.id);
evidenceGroups.set(evidenceKey, ids);
}
}
for (const [evidenceKey, branchIds] of evidenceGroups.entries()) {
if (branchIds.length > 1) {
duplicateEvidenceGroups.push({
evidenceKey,
branchIds: unique(branchIds),
});
for (const id of branchIds) duplicateBranchIds.add(id);
}
}
for (let index = 0; index < directBranches.length; index += 1) {
for (let inner = index + 1; inner < directBranches.length; inner += 1) {
const aNode = directBranches[index];
const bNode = directBranches[inner];
if (branchesConflict(aNode, bNode)) {
conflictingBranches.push([aNode.id, bNode.id]);
conflictingBranchIds.add(aNode.id);
conflictingBranchIds.add(bNode.id);
continue;
}
const aEvidence = branchEvidenceKeys(aNode);
const bEvidence = branchEvidenceKeys(bNode);
const sharesEvidence = aEvidence.some((key) => bEvidence.includes(key));
if (
!sharesEvidence &&
aNode.status === "resolved" &&
bNode.status === "resolved"
) {
corroboratingBranches.push([aNode.id, bNode.id]);
}
}
}
const interactionBranchIds = new Set([
...duplicateBranchIds,
...conflictingBranchIds,
...corroboratingBranches.flat(),
]);
const independentBranches = directBranches
.map((branch) => branch.id)
.filter((id) => !interactionBranchIds.has(id));
return {
corroboratingBranches,
conflictingBranches,
duplicateEvidenceGroups,
independentBranches,
interactionSummary: {
corroboratingBranchCount: corroboratingBranches.length,
conflictingBranchCount: conflictingBranches.length,
duplicateEvidenceCount: duplicateEvidenceGroups.length,
independentBranchCount: independentBranches.length,
},
};
}
function upsertProposalNodeUpdate(proposalSnapshot, update) {
const existing = proposalSnapshot.updatedNodes.find(
(candidate) => candidate.nodeId === update.nodeId,
@@ -585,6 +708,13 @@ function computeParentProgressState(graph, parentNode) {
(child) => child.status !== "resolved",
).length;
const beforeAssessment = getNodeConfidenceAssessment(parentNode);
const interactions = evaluateBranchInteractions({ parentNode, graph });
const corroborationCount =
interactions.interactionSummary.corroboratingBranchCount;
const duplicateEvidenceCount =
interactions.interactionSummary.duplicateEvidenceCount;
const conflictingBranchCount =
interactions.interactionSummary.conflictingBranchCount;
if (totalChildren === 0) {
const nextAssessment = {
@@ -625,18 +755,32 @@ function computeParentProgressState(graph, parentNode) {
confidenceCapReason = "no_resolved_direct_children";
} else if (resolvedCount < totalChildren) {
nextAssessment = {
evidenceConfidence: "high",
evidenceConfidence: corroborationCount > 0 ? "high" : "medium",
completenessStatus: "partial",
conclusionConfidence: "medium",
};
confidenceCapReason = "unresolved_direct_children_cap_conclusion";
confidenceCapReason =
conflictingBranchCount > 0
? "conflicting_branches_cap_conclusion"
: duplicateEvidenceCount > 0
? "duplicate_evidence_no_extra_confidence"
: corroborationCount > 0
? "independent_corroboration_with_incomplete_parent"
: "unresolved_direct_children_cap_conclusion";
} else {
nextAssessment = {
evidenceConfidence: "high",
completenessStatus: "complete",
conclusionConfidence: "high",
conclusionConfidence: conflictingBranchCount > 0 ? "low" : "high",
};
confidenceCapReason = null;
confidenceCapReason =
conflictingBranchCount > 0
? "conflicting_branches_cap_conclusion"
: duplicateEvidenceCount > 0
? "duplicate_evidence_no_extra_confidence"
: corroborationCount > 0
? "independent_corroboration_supported_conclusion"
: null;
}
if (resolvedChildren.length === totalChildren) {
@@ -652,6 +796,7 @@ function computeParentProgressState(graph, parentNode) {
resolvedDirectChildren: resolvedCount,
unresolvedDirectChildren: unresolvedCount,
contradictoryDirectChildren: contradictoryChildren.length,
branchInteractions: interactions,
confidenceCapReason,
reason:
"All direct child unknowns are resolved, so the parent can now resolve deterministically.",
@@ -671,6 +816,7 @@ function computeParentProgressState(graph, parentNode) {
resolvedDirectChildren: resolvedCount,
unresolvedDirectChildren: unresolvedCount,
contradictoryDirectChildren: contradictoryChildren.length,
branchInteractions: interactions,
confidenceCapReason,
reason:
"At least one direct child has been progressed, so the parent becomes provisional but remains unresolved until all direct children are resolved.",
@@ -689,6 +835,7 @@ function computeParentProgressState(graph, parentNode) {
resolvedDirectChildren: resolvedCount,
unresolvedDirectChildren: unresolvedCount,
contradictoryDirectChildren: contradictoryChildren.length,
branchInteractions: interactions,
confidenceCapReason,
reason: "No direct child progress exists yet for the parent.",
};
@@ -807,6 +954,19 @@ export function propagateResolvedChildEvidence({
resolvedDirectChildren: progressState.resolvedDirectChildren,
unresolvedDirectChildren: progressState.unresolvedDirectChildren,
contradictoryDirectChildren: progressState.contradictoryDirectChildren,
corroboratingBranchCount:
progressState.branchInteractions.interactionSummary
.corroboratingBranchCount,
conflictingBranchCount:
progressState.branchInteractions.interactionSummary
.conflictingBranchCount,
duplicateEvidenceCount:
progressState.branchInteractions.interactionSummary
.duplicateEvidenceCount,
independentBranchCount:
progressState.branchInteractions.interactionSummary
.independentBranchCount,
interactionSummary: progressState.branchInteractions.interactionSummary,
confidenceCapReason: progressState.confidenceCapReason,
parentResolved: progressState.parentResolved,
reason: progressState.reason,
@@ -846,6 +1006,11 @@ export function propagateResolvedChildEvidence({
resolvedDirectChildren: firstEvent?.resolvedDirectChildren ?? 0,
unresolvedDirectChildren: firstEvent?.unresolvedDirectChildren ?? 0,
contradictoryDirectChildren: firstEvent?.contradictoryDirectChildren ?? 0,
corroboratingBranchCount: firstEvent?.corroboratingBranchCount ?? 0,
conflictingBranchCount: firstEvent?.conflictingBranchCount ?? 0,
duplicateEvidenceCount: firstEvent?.duplicateEvidenceCount ?? 0,
independentBranchCount: firstEvent?.independentBranchCount ?? 0,
interactionSummary: firstEvent?.interactionSummary ?? null,
confidenceCapReason: firstEvent?.confidenceCapReason ?? null,
ancestorPropagationStoppedReason,
affectedAncestorIds: [...affectedAncestorIds],
@@ -1915,6 +2080,11 @@ export function applyValidatedProposal({
const affectedAncestorIds = propagationResult.affectedAncestorIds;
const nextSelectedSibling = propagationResult.nextSelectedSibling;
const parentResolved = propagationResult.parentResolved;
const corroboratingBranchCount = propagationResult.corroboratingBranchCount;
const conflictingBranchCount = propagationResult.conflictingBranchCount;
const duplicateEvidenceCount = propagationResult.duplicateEvidenceCount;
const independentBranchCount = propagationResult.independentBranchCount;
const interactionSummary = propagationResult.interactionSummary;
const propagationReason = propagationResult.reason;
if (
@@ -2048,6 +2218,11 @@ export function applyValidatedProposal({
resolvedDirectChildren,
unresolvedDirectChildren,
contradictoryDirectChildren,
corroboratingBranchCount,
conflictingBranchCount,
duplicateEvidenceCount,
independentBranchCount,
interactionSummary,
confidenceCapReason,
ancestorPropagationStoppedReason,
affectedAncestorIds,