fix(reasoning): reconcile closure selection state

This commit is contained in:
2026-08-14 11:02:00 +01:00
parent a00112e157
commit 54e2e2186b
4 changed files with 386 additions and 3 deletions
+351 -2
View File
@@ -4558,8 +4558,6 @@ describe("60B.43 — terminal post-mutation target is cleared after valid decisi
expect(result.updatedSituationGraph.nodes.filter((node) => node.id === ids.productLaunchDecision)).toHaveLength(1);
expect(result.updatedSituationGraph.nodes.filter((node) => node.id === ids.launchThisYear)).toHaveLength(1);
expect(result.updatedSituationGraph.nodes.filter((node) => node.id === ids.waitTwelveMonths)).toHaveLength(1);
expect(result.updatedSituationGraph.activeUnknownNodeId).toBeNull();
expect(result.selectedQuestion).toBeNull();
});
it("discards a proposal-selected target that becomes known and falls back to another genuine unresolved candidate", () => {
@@ -4659,3 +4657,354 @@ describe("60B.43 — terminal post-mutation target is cleared after valid decisi
expect(result.selectedQuestion).toBeNull();
});
});
describe("60B.49 — reconciles updated-to-resolved unknown into resolvedUnknownNodeIds", () => {
it("accepts the 60B.47-shaped proposal by auto-adding the missing resolved unknown ID", () => {
const productLaunchDecision = makeNode({
id: "n_product_launch_decision",
label: "Which option leaves us better off overall?",
description:
"Uncertainty about which of the two product-launch timing options provides superior net value for the organisation.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const launchThisYear = makeNode({
id: "opt_launch_this_year",
label: "Launch this year",
description: "Launch the new software product this year.",
kind: "option",
status: "known",
confidence: "high",
});
const waitTwelveMonths = makeNode({
id: "opt_wait_twelve_months",
label: "Wait twelve months",
description: "Wait twelve months before launching the product.",
kind: "option",
status: "known",
confidence: "high",
});
const enterpriseCustomerSigning = makeNode({
id: "n_enterprise_customer_signing",
label: "Prospective enterprise customer signing status",
description:
"Unknown whether one prospective enterprise customer will sign if we launch this year.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement:
"We are evaluating two product-launch timing options: launching this year or waiting twelve months.",
nodes: [
productLaunchDecision,
launchThisYear,
waitTwelveMonths,
enterpriseCustomerSigning,
],
edges: [
makeEdge({
id: "e-opt-launch-to-dec",
fromNodeId: launchThisYear.id,
toNodeId: productLaunchDecision.id,
relationship: "contained_in",
confidence: "high",
description: "Launch option belongs to the decision.",
}),
makeEdge({
id: "e-opt-wait-to-dec",
fromNodeId: waitTwelveMonths.id,
toNodeId: productLaunchDecision.id,
relationship: "contained_in",
confidence: "high",
description: "Wait option belongs to the decision.",
}),
],
activeUnknownNodeId: enterpriseCustomerSigning.id,
resolvedNodeIds: [],
currentSummary: "Customer signing is the active uncertainty.",
});
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: enterpriseCustomerSigning.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer confirmed they will not sign this year.",
reason: "Negative outcome resolves the customer factor.",
},
{
nodeId: productLaunchDecision.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Waiting twelve months is now the resolved decision.",
reason: "Negative customer outcome resolves the timing decision.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [enterpriseCustomerSigning.id],
affectedNodeIds: [productLaunchDecision.id],
selectedQuestion: {
nodeId: productLaunchDecision.id,
question: "What outcome would demonstrate enough value to justify launching?",
reason: "Closure-shaped proposal contract.",
},
},
});
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.resolvedNodeIds).toEqual(
expect.arrayContaining([
enterpriseCustomerSigning.id,
productLaunchDecision.id,
]),
);
expect(
result.updatedSituationGraph.resolvedNodeIds.filter(
(id) => id === productLaunchDecision.id,
),
).toHaveLength(1);
expect(
result.updatedSituationGraph.resolvedNodeIds.filter(
(id) => id === enterpriseCustomerSigning.id,
),
).toHaveLength(1);
expect(
result.updatedSituationGraph.nodes.find(
(node) => node.id === enterpriseCustomerSigning.id,
)?.status,
).toBe("resolved");
expect(
result.updatedSituationGraph.nodes.find(
(node) => node.id === productLaunchDecision.id,
)?.status,
).toBe("resolved");
expect(result.updatedSituationGraph.nodes).toHaveLength(4);
expect(result.updatedSituationGraph.edges).toHaveLength(2);
expect(result.updatedSituationGraph.activeUnknownNodeId).toBeNull();
expect(result.selectedQuestion).toBeNull();
});
it("clears a stale same-turn resolved selectedQuestion and falls back to another genuine unresolved unknown", () => {
const productLaunchDecision = makeNode({
id: "n_product_launch_decision",
label: "Which option leaves us better off overall?",
description: "Uncertainty about which option provides superior net value.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const enterpriseCustomerSigning = makeNode({
id: "n_enterprise_customer_signing",
label: "Prospective enterprise customer signing status",
description: "Unknown whether the customer will sign.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const fallbackUnknown = makeNode({
id: "n_remaining_unknown",
label: "Remaining unresolved factor",
description: "Need remaining unresolved market evidence.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const graph = makeGraph({
centralStatement: "Product-launch negative outcome with remaining unresolved factor.",
nodes: [
productLaunchDecision,
enterpriseCustomerSigning,
fallbackUnknown,
],
edges: [],
activeUnknownNodeId: enterpriseCustomerSigning.id,
resolvedNodeIds: [],
currentSummary: "Customer signing is active but another unresolved factor exists.",
});
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: enterpriseCustomerSigning.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer will not sign.",
reason: "Customer factor resolved negatively.",
},
{
nodeId: productLaunchDecision.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Decision resolves to wait.",
reason: "Selected target closes in the same proposal.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [enterpriseCustomerSigning.id],
affectedNodeIds: [productLaunchDecision.id],
selectedQuestion: {
nodeId: productLaunchDecision.id,
question: "What outcome would demonstrate enough value to justify launching?",
reason: "This target becomes stale because it resolves in the same proposal.",
},
},
});
expect(result.success).toBe(true);
expect(result.selectedQuestion?.nodeId).toBe(fallbackUnknown.id);
expect(result.updatedSituationGraph.activeUnknownNodeId).toBe(
fallbackUnknown.id,
);
});
it("preserves forward reconciliation when resolvedUnknownNodeIds is present without an explicit updatedNode", () => {
const fixture = makeComparabilityUpdateFixture();
const result = applyValidatedProposal({
situationGraph: fixture.graph,
proposal: {
addedNodes: [],
updatedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [fixture.comparabilityUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
},
});
expect(result.success).toBe(true);
expect(
result.updatedSituationGraph.nodes.find(
(node) => node.id === fixture.comparabilityUnknownId,
)?.status,
).toBe("resolved");
expect(result.updatedSituationGraph.resolvedNodeIds).toContain(
fixture.comparabilityUnknownId,
);
});
it("does not duplicate already-consistent resolved unknown IDs", () => {
const fixture = makeComparabilityUpdateFixture();
const result = applyValidatedProposal({
situationGraph: fixture.graph,
proposal: fixture.proposal,
});
expect(result.success).toBe(true);
expect(
result.updatedSituationGraph.resolvedNodeIds.filter(
(id) => id === fixture.comparabilityUnknownId,
),
).toHaveLength(1);
});
it("does not auto-add non-unknown nodes updated to resolved", () => {
const stateNode = makeNode({
id: "n_state_node",
label: "State node",
description: "A state node that becomes resolved-like in status only.",
kind: "state",
status: "provisional",
confidence: "medium",
});
const graph = makeGraph({
centralStatement: "Non-unknown resolution graph",
nodes: [stateNode],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "Testing non-unknown reconciliation boundary.",
});
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: stateNode.id,
previousStatus: "provisional",
newStatus: "resolved",
previousValue: null,
newValue: "State resolved.",
reason: "Non-unknown node update.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: null,
},
});
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.resolvedNodeIds).not.toContain(stateNode.id);
});
it("does not auto-add non-resolved statuses for unknown nodes", () => {
const statuses = ["known", "unknown", "contradicted"];
for (const status of statuses) {
const unknownNode = makeNode({
id: `n_status_${status}`,
label: `Unknown ${status}`,
description: `Unknown node updated to ${status}.`,
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement: `Graph for ${status}`,
nodes: [unknownNode],
edges: [],
activeUnknownNodeId: unknownNode.id,
resolvedNodeIds: [],
currentSummary: `Testing ${status} non-addition.`,
});
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: unknownNode.id,
previousStatus: "unknown",
newStatus: status,
previousValue: null,
newValue: `${status} value`,
reason: `Update to ${status}.`,
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: null,
},
});
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.resolvedNodeIds).not.toContain(
unknownNode.id,
);
}
});
});
+9
View File
@@ -905,6 +905,15 @@ describe("60B.11 — prompt prerequisite-aware targeting", () => {
expect(prompt).toContain("depends_on");
});
it("clarifies selectedQuestion must remain unresolved after applying the same proposal", () => {
expect(prompt).toContain(
"remains unresolved after applying this same proposal",
);
expect(prompt).toContain(
"If the proposal resolves all consequential unknowns, selectedQuestion must be null.",
);
});
it("does NOT duplicate the materiality scoring rule", () => {
// The prompt should clarify targeting but not redefine materiality
expect(prompt).not.toContain("materiality score");