fix(reasoning): reconcile closure selection state
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user