feat: evaluate deterministic cross-branch corroboration
This commit is contained in:
+179
-4
@@ -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,
|
||||
|
||||
@@ -119,6 +119,11 @@ function buildUpdateDiagnostics({
|
||||
resolvedDirectChildren,
|
||||
unresolvedDirectChildren,
|
||||
contradictoryDirectChildren,
|
||||
corroboratingBranchCount,
|
||||
conflictingBranchCount,
|
||||
duplicateEvidenceCount,
|
||||
independentBranchCount,
|
||||
interactionSummary,
|
||||
confidenceCapReason,
|
||||
ancestorPropagationStoppedReason,
|
||||
affectedAncestorIds,
|
||||
@@ -183,6 +188,11 @@ function buildUpdateDiagnostics({
|
||||
resolvedDirectChildren: resolvedDirectChildren ?? 0,
|
||||
unresolvedDirectChildren: unresolvedDirectChildren ?? 0,
|
||||
contradictoryDirectChildren: contradictoryDirectChildren ?? 0,
|
||||
corroboratingBranchCount: corroboratingBranchCount ?? 0,
|
||||
conflictingBranchCount: conflictingBranchCount ?? 0,
|
||||
duplicateEvidenceCount: duplicateEvidenceCount ?? 0,
|
||||
independentBranchCount: independentBranchCount ?? 0,
|
||||
interactionSummary: interactionSummary ?? null,
|
||||
confidenceCapReason: confidenceCapReason ?? null,
|
||||
ancestorPropagationStoppedReason: ancestorPropagationStoppedReason ?? null,
|
||||
affectedAncestorIds: affectedAncestorIds ?? [],
|
||||
@@ -471,6 +481,11 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
||||
resolvedDirectChildren: 0,
|
||||
unresolvedDirectChildren: 0,
|
||||
contradictoryDirectChildren: 0,
|
||||
corroboratingBranchCount: 0,
|
||||
conflictingBranchCount: 0,
|
||||
duplicateEvidenceCount: 0,
|
||||
independentBranchCount: 0,
|
||||
interactionSummary: null,
|
||||
confidenceCapReason: null,
|
||||
ancestorPropagationStoppedReason: null,
|
||||
affectedAncestorIds: [],
|
||||
@@ -552,6 +567,11 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
||||
unresolvedDirectChildren: applicationResult.unresolvedDirectChildren,
|
||||
contradictoryDirectChildren:
|
||||
applicationResult.contradictoryDirectChildren,
|
||||
corroboratingBranchCount: applicationResult.corroboratingBranchCount,
|
||||
conflictingBranchCount: applicationResult.conflictingBranchCount,
|
||||
duplicateEvidenceCount: applicationResult.duplicateEvidenceCount,
|
||||
independentBranchCount: applicationResult.independentBranchCount,
|
||||
interactionSummary: applicationResult.interactionSummary,
|
||||
confidenceCapReason: applicationResult.confidenceCapReason,
|
||||
ancestorPropagationStoppedReason:
|
||||
applicationResult.ancestorPropagationStoppedReason,
|
||||
@@ -616,6 +636,11 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
||||
resolvedDirectChildren: 0,
|
||||
unresolvedDirectChildren: 0,
|
||||
contradictoryDirectChildren: 0,
|
||||
corroboratingBranchCount: 0,
|
||||
conflictingBranchCount: 0,
|
||||
duplicateEvidenceCount: 0,
|
||||
independentBranchCount: 0,
|
||||
interactionSummary: null,
|
||||
confidenceCapReason: null,
|
||||
ancestorPropagationStoppedReason: null,
|
||||
affectedAncestorIds: [],
|
||||
|
||||
Reference in New Issue
Block a user