Feature/product platform foundation v0.62 #1
@@ -356,6 +356,31 @@ function reconcileResolutionSemantics(graph, proposal) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const update of nextProposal.updatedNodes) {
|
||||
const existingNode = graphNodeById.get(update.nodeId);
|
||||
if (
|
||||
existingNode?.kind === "unknown" &&
|
||||
update.newStatus === "resolved" &&
|
||||
!nextProposal.resolvedUnknownNodeIds.includes(update.nodeId)
|
||||
) {
|
||||
nextProposal.resolvedUnknownNodeIds.push(update.nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextProposal.selectedQuestion?.nodeId) {
|
||||
const selectedQuestionNodeId = nextProposal.selectedQuestion.nodeId;
|
||||
const selectedQuestionUpdate = updatedNodeById.get(selectedQuestionNodeId);
|
||||
const selectedQuestionResolvedByStatus =
|
||||
selectedQuestionUpdate?.newStatus === "resolved";
|
||||
const selectedQuestionResolvedById = nextProposal.resolvedUnknownNodeIds.includes(
|
||||
selectedQuestionNodeId,
|
||||
);
|
||||
|
||||
if (selectedQuestionResolvedByStatus || selectedQuestionResolvedById) {
|
||||
nextProposal.selectedQuestion = null;
|
||||
}
|
||||
}
|
||||
|
||||
for (const update of nextProposal.updatedNodes) {
|
||||
const existingNode = graphNodeById.get(update.nodeId);
|
||||
if (
|
||||
|
||||
@@ -117,7 +117,7 @@ The JSON object must contain exactly these top-level fields:
|
||||
14. Do not invent evidence.
|
||||
15. Do not create unsupported causal edges.
|
||||
16. When your proposal adds one or more new unresolved unknowns (status !== 'resolved'), you MUST include a selectedQuestion identifying one of those as a candidate unknown node. The engine validates your candidate and retains deterministic prerequisite ordering, fallback selection, and formulation authority; prefer nodes with no unresolved depends_on prerequisites from same-proposal additions. Your candidate does not need to be the highest-scoring unknown — it only needs to be a valid unresolved unknown that exists in the graph or in addedNodes.
|
||||
17. selectedQuestion.nodeId must reference an unresolved unknown node that exists either already in the graph or in addedNodes.
|
||||
17. selectedQuestion.nodeId must reference an unknown node that remains unresolved after applying this same proposal and that exists either already in the graph or in addedNodes. If the proposal resolves all consequential unknowns, selectedQuestion must be null.
|
||||
18. selectedQuestion.question must be one narrow non-compound question about that one unknown.
|
||||
19. Do not prioritise downstream implementation, pricing, optimisation, or speculative branches ahead of prerequisite definitions, actors, success criteria, constraints, measures, or terminology.
|
||||
20. Return selectedQuestion as null only when no consequential unresolved unknown remains.
|
||||
|
||||
@@ -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