reasoning: admit verified user-supported unknowns without provenance edges
This commit is contained in:
@@ -75,12 +75,31 @@ function isCompoundQuestion(question) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function validateAddedUnknowns(graph, proposal) {
|
||||
function validateAddedUnknowns(graph, proposal, answerMeaning) {
|
||||
const errors = [];
|
||||
const addedUnknowns = proposal.addedNodes.filter(
|
||||
(node) => node.kind === "unknown",
|
||||
);
|
||||
|
||||
const userSupportedMeaningText = answerMeaning?.userSupportedMeaning;
|
||||
|
||||
function hasNodeLevelUserSupport(unknownNode) {
|
||||
if (!userSupportedMeaningText) return false;
|
||||
|
||||
const unknownText = normaliseSemanticText(
|
||||
[normaliseText(unknownNode.label), normaliseText(unknownNode.description)]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
);
|
||||
|
||||
const overlapRatio = semanticOverlapRatio(userSupportedMeaningText, unknownText);
|
||||
const overlappingTokens = semanticContentTokens(userSupportedMeaningText).filter(
|
||||
(token) => semanticContentTokens(unknownText).includes(token),
|
||||
).length;
|
||||
|
||||
return overlapRatio >= 0.4 || overlappingTokens >= 3;
|
||||
}
|
||||
|
||||
if (addedUnknowns.length > 3) {
|
||||
errors.push(
|
||||
`Proposal adds too many unknown nodes: ${addedUnknowns.length} (maximum 3)`,
|
||||
@@ -180,7 +199,10 @@ function validateAddedUnknowns(graph, proposal) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasExplicitAnswerDerivedRelationship(unknownNode)) {
|
||||
if (
|
||||
!hasNodeLevelUserSupport(unknownNode) &&
|
||||
!hasExplicitAnswerDerivedRelationship(unknownNode)
|
||||
) {
|
||||
errors.push(
|
||||
`New unknown must be explicitly related to an answer-derived node: "${unknownNode.id}"`,
|
||||
);
|
||||
@@ -3232,7 +3254,7 @@ export function applyValidatedProposal({
|
||||
...validateSemanticDuplicateUnknowns(situationGraph, validatedProposal),
|
||||
);
|
||||
proposalCompatibilityErrors.push(
|
||||
...validateAddedUnknowns(situationGraph, validatedProposal),
|
||||
...validateAddedUnknowns(situationGraph, validatedProposal, validatedProposal.answerMeaning),
|
||||
);
|
||||
|
||||
const selectedQuestionValidation = validateSelectedQuestion(
|
||||
|
||||
@@ -2537,4 +2537,408 @@ describe("applyValidatedProposal", () => {
|
||||
);
|
||||
expect(result.selectedQuestion?.reasoningPattern).toBe("decision");
|
||||
});
|
||||
|
||||
// ── Case 1 — direct user-supported uncertainty, no structural anchor ──
|
||||
it("Case 1: admitted unknown passes via node-level user support without structural linkage", () => {
|
||||
const graph = makeCommercialUpdateFixture();
|
||||
const parentId = graph.activeUnknownNodeId;
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
answer: "I need evidence that projected office savings are realistic.",
|
||||
previousQuestion:
|
||||
"What problem would this need to solve to justify continuing development?",
|
||||
proposal: {
|
||||
addedNodes: [
|
||||
makeNode({
|
||||
id: "n-savings-realism",
|
||||
label: "Whether projected office savings are realistic",
|
||||
description:
|
||||
"Need to verify whether the projected office savings are realistic, because that determines commercial justification.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
}),
|
||||
],
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: parentId,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "The user needs evidence for projected office savings realism.",
|
||||
reason: "The answer directs focus to savings realism evidence.",
|
||||
},
|
||||
],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [parentId],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: {
|
||||
nodeId: "n-savings-realism",
|
||||
question: "What evidence supports the projected office savings?",
|
||||
reason: "Consequential unknown verified by user-supported meaning.",
|
||||
},
|
||||
answerMeaning: {
|
||||
userSupportedMeaning:
|
||||
"I need evidence that projected office savings are realistic.",
|
||||
possibleInference: null,
|
||||
supportCategory: "uncertain",
|
||||
resolutionGuidance: "may_resolve",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
// ── Case 2 — two direct user-supported unknowns ──
|
||||
it("Case 2: two independently supported unknowns both pass via node-level user support (parent stays unresolved)", () => {
|
||||
const graph = makeCommercialUpdateFixture();
|
||||
const parentId = graph.activeUnknownNodeId;
|
||||
|
||||
// The parent remains unresolved — the answer adds two new unknowns
|
||||
// without resolving the original question. Both children are independently verified.
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
answer: "I need evidence the savings are realistic and evidence the move will not materially increase loss of key engineers.",
|
||||
previousQuestion:
|
||||
"What problem would this need to solve to justify continuing development?",
|
||||
proposal: {
|
||||
addedNodes: [
|
||||
makeNode({
|
||||
id: "n-savings-realism-2",
|
||||
label: "Whether projected savings are realistic",
|
||||
description:
|
||||
"Need to verify whether the savings figures are realistic, because that is needed for commercial evaluation.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
}),
|
||||
makeNode({
|
||||
id: "n-retention-impact",
|
||||
label: "Whether the move materially increases loss of key engineers",
|
||||
description:
|
||||
"Need to check whether the relocation increases risk of losing key engineers, because that matters for continuity.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
}),
|
||||
],
|
||||
updatedNodes: [],
|
||||
addedEdges: [
|
||||
makeEdge({
|
||||
id: "e-parent-to-savings",
|
||||
fromNodeId: parentId,
|
||||
toNodeId: "n-savings-realism-2",
|
||||
relationship: "depends_on",
|
||||
confidence: "medium",
|
||||
description: "Parent depends on savings verification.",
|
||||
}),
|
||||
],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: {
|
||||
nodeId: "n-savings-realism-2",
|
||||
question: "What evidence supports the projected office savings?",
|
||||
reason: "First of two independently verified consequential unknowns.",
|
||||
},
|
||||
answerMeaning: {
|
||||
userSupportedMeaning:
|
||||
"I need evidence the savings are realistic and evidence the move will not materially increase loss of key engineers.",
|
||||
possibleInference: null,
|
||||
supportCategory: "uncertain",
|
||||
resolutionGuidance: "may_resolve",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
// ── Case 3 — unsupported hallucinated unknown ──
|
||||
it("Case 3: unsupported hallucinated unknown is rejected without structural linkage", () => {
|
||||
const graph = makeCommercialUpdateFixture();
|
||||
const parentId = graph.activeUnknownNodeId;
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
answer: "We are looking at this mainly for cost reduction — roughly £2M annual savings on office overhead.",
|
||||
previousQuestion:
|
||||
"What problem would this need to solve to justify continuing development?",
|
||||
proposal: {
|
||||
addedNodes: [
|
||||
makeNode({
|
||||
id: "n-office-paint-colour",
|
||||
label: "Whether office paint colour affects morale",
|
||||
description:
|
||||
"Need to know whether paint colour in the current office affects team morale, because that is a potential factor.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "low",
|
||||
}),
|
||||
],
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: parentId,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "Cost reduction is the primary driver.",
|
||||
reason: "The answer identifies cost reduction as the main concern.",
|
||||
},
|
||||
],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [parentId],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: {
|
||||
nodeId: "n-office-paint-colour",
|
||||
question: "What evidence supports the office paint colour hypothesis?",
|
||||
reason: "Unrelated unknown — should be rejected.",
|
||||
},
|
||||
answerMeaning: {
|
||||
userSupportedMeaning:
|
||||
"The main reason for considering this is cost reduction, specifically about £2M in annual office-overhead savings.",
|
||||
possibleInference: null,
|
||||
supportCategory: "other",
|
||||
resolutionGuidance: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.stage).toBe("proposal_compatibility");
|
||||
expect(result.errors.join(" ")).toContain(
|
||||
"explicitly related to an answer-derived node",
|
||||
);
|
||||
});
|
||||
|
||||
// ── Case 4 — possibleInference only ──
|
||||
it("Case 4: unknown based on possibleInference only is not admitted through user-support path", () => {
|
||||
const graph = makeCommercialUpdateFixture();
|
||||
|
||||
// AnswerMeaning's userSupportedMeaning shares NO tokens with the unknown.
|
||||
// possibleInference does mention "workforce" but that must NOT establish support.
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
answer: "The project timeline is critical.",
|
||||
previousQuestion: "What problem would this need to solve?",
|
||||
proposal: {
|
||||
addedNodes: [
|
||||
makeNode({
|
||||
id: "n-workforce-stability",
|
||||
label: "Whether workforce stability affects the decision",
|
||||
description: "Need to check whether workforce stability matters, because that could impact scheduling.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
}),
|
||||
],
|
||||
updatedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: {
|
||||
nodeId: "n-workforce-stability",
|
||||
question: "How important is workforce stability to this decision?",
|
||||
reason: "Inferred consequential unknown — should be rejected.",
|
||||
},
|
||||
answerMeaning: {
|
||||
userSupportedMeaning: "The project timeline is critical to the decision.",
|
||||
possibleInference: "Workforce stability may matter for scheduling and continuity.",
|
||||
supportCategory: null,
|
||||
resolutionGuidance: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.stage).toBe("proposal_compatibility");
|
||||
expect(result.errors.join(" ")).toContain(
|
||||
"explicitly related to an answer-derived node",
|
||||
);
|
||||
});
|
||||
|
||||
// ── Case 5 — genuine structural edge ──
|
||||
it("Case 5: supported unknown with genuine structural edge passes without additional provenance edge", () => {
|
||||
const graph = makeCommercialUpdateFixture();
|
||||
const parentId = graph.activeUnknownNodeId;
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
answer: "We need evidence that projected office savings are realistic.",
|
||||
previousQuestion:
|
||||
"What problem would this need to solve to justify continuing development?",
|
||||
proposal: {
|
||||
addedNodes: [
|
||||
makeNode({
|
||||
id: "n-savings-realism-5",
|
||||
label: "Whether projected office savings are realistic",
|
||||
description:
|
||||
"Need to verify whether the savings figures are realistic, because that is needed for commercial evaluation.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
parentId,
|
||||
}),
|
||||
],
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: parentId,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "Savings realism evidence is needed.",
|
||||
reason: "The answer directs to savings verification.",
|
||||
},
|
||||
],
|
||||
addedEdges: [
|
||||
makeEdge({
|
||||
id: "e-parent-savings-realism-5",
|
||||
fromNodeId: parentId,
|
||||
toNodeId: "n-savings-realism-5",
|
||||
relationship: "depends_on",
|
||||
confidence: "medium",
|
||||
description: "Savings realism is a dependency of commercial justification.",
|
||||
}),
|
||||
],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [parentId],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: {
|
||||
nodeId: "n-savings-realism-5",
|
||||
question: "What evidence supports the projected office savings?",
|
||||
reason: "Both supported and structurally linked.",
|
||||
},
|
||||
answerMeaning: {
|
||||
userSupportedMeaning:
|
||||
"We need evidence that projected office savings are realistic.",
|
||||
possibleInference: null,
|
||||
supportCategory: "uncertain",
|
||||
resolutionGuidance: "may_resolve",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
// ── Case 6 — answerMeaning absent + valid structural linkage ──
|
||||
it("Case 6: no answerMeaning but valid structural linkage passes (existing behaviour unchanged)", () => {
|
||||
const graph = makeCommercialUpdateFixture();
|
||||
const parentId = graph.activeUnknownNodeId;
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
answer: "We need evidence that projected office savings are realistic.",
|
||||
previousQuestion:
|
||||
"What problem would this need to solve to justify continuing development?",
|
||||
proposal: {
|
||||
addedNodes: [
|
||||
makeNode({
|
||||
id: "n-savings-realism-6",
|
||||
label: "Whether projected office savings are realistic",
|
||||
description:
|
||||
"Need to verify whether the savings figures are realistic, because that is needed for commercial evaluation.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
parentId,
|
||||
}),
|
||||
],
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: parentId,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "Savings realism evidence is needed.",
|
||||
reason: "The answer directs to savings verification.",
|
||||
},
|
||||
],
|
||||
addedEdges: [
|
||||
makeEdge({
|
||||
id: "e-parent-savings-realism-6",
|
||||
fromNodeId: parentId,
|
||||
toNodeId: "n-savings-realism-6",
|
||||
relationship: "depends_on",
|
||||
confidence: "medium",
|
||||
description: "Savings realism is a dependency.",
|
||||
}),
|
||||
],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [parentId],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: {
|
||||
nodeId: "n-savings-realism-6",
|
||||
question: "What evidence supports the projected office savings?",
|
||||
reason: "Structurally linked without answerMeaning.",
|
||||
},
|
||||
answerMeaning: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
// ── Case 7 — no support and no linkage ──
|
||||
it("Case 7: unknown with neither user support nor structural linkage is rejected", () => {
|
||||
const graph = makeCommercialUpdateFixture();
|
||||
const parentId = graph.activeUnknownNodeId;
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
answer: "We need evidence that projected office savings are realistic.",
|
||||
previousQuestion:
|
||||
"What problem would this need to solve to justify continuing development?",
|
||||
proposal: {
|
||||
addedNodes: [
|
||||
makeNode({
|
||||
id: "n-hallucinated-unknown",
|
||||
label: "Whether the kitchen lighting affects productivity",
|
||||
description:
|
||||
"Need to know whether lighting matters, because it could impact outcomes.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "low",
|
||||
}),
|
||||
],
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: parentId,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "Savings realism is the focus.",
|
||||
reason: "Answer directs to savings verification.",
|
||||
},
|
||||
],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [parentId],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: {
|
||||
nodeId: "n-hallucinated-unknown",
|
||||
question: "Does kitchen lighting affect productivity?",
|
||||
reason: "No support, no linkage — should be rejected.",
|
||||
},
|
||||
answerMeaning: {
|
||||
userSupportedMeaning:
|
||||
"We need evidence that projected office savings are realistic.",
|
||||
possibleInference: null,
|
||||
supportCategory: "uncertain",
|
||||
resolutionGuidance: "may_resolve",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.stage).toBe("proposal_compatibility");
|
||||
expect(result.errors.join(" ")).toContain(
|
||||
"explicitly related to an answer-derived node",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user