fix(reasoning): preserve ready material question target

This commit is contained in:
2026-08-13 09:36:10 +01:00
parent 854c3aa002
commit 54bc48342b
4 changed files with 705 additions and 12 deletions
+64
View File
@@ -1982,6 +1982,41 @@ function selectPatternCompatibleUnknownCandidate({
]);
}
function hasUnresolvedSameProposalDependsOnPrerequisite({
graph,
proposal,
targetNodeId,
}) {
const addedNodeIds = new Set(
(proposal?.addedNodes || []).map((node) => node.id),
);
if (!addedNodeIds.has(targetNodeId)) return false;
const targetNode = findNodeById(graph, targetNodeId);
if (!targetNode) return false;
const directPrerequisiteIds = new Set();
for (const dependencyId of targetNode.dependsOn || []) {
directPrerequisiteIds.add(dependencyId);
}
for (const edge of graph.edges || []) {
if (
edge.relationship === "depends_on" &&
edge.fromNodeId === targetNodeId &&
edge.toNodeId
) {
directPrerequisiteIds.add(edge.toNodeId);
}
}
return [...directPrerequisiteIds].some(
(nodeId) =>
addedNodeIds.has(nodeId) && isSelectableUnresolvedUnknown(graph, nodeId),
);
}
function collectPatternCompatibilityDiagnostics({
graph,
activePattern,
@@ -3677,6 +3712,35 @@ export function applyValidatedProposal({
const interactionSummary = propagationResult.interactionSummary;
const propagationReason = propagationResult.reason;
// Bounded model-selection honour: prefer a model-selected target only when it
// is still valid, was added in this proposal turn, and has no unresolved
// same-proposal-added unknown prerequisite via depends_on. Otherwise fall
// through to existing deterministic selection unchanged.
if (validatedProposal.selectedQuestion?.nodeId) {
const candidateNodeId = validatedProposal.selectedQuestion.nodeId;
const candidateWasAddedThisProposal = (
validatedProposal.addedNodes || []
).some((node) => node.id === candidateNodeId);
if (
isSelectableUnresolvedUnknown(updatedSituationGraph, candidateNodeId) &&
candidateWasAddedThisProposal &&
!hasUnresolvedSameProposalDependsOnPrerequisite({
graph: updatedSituationGraph,
proposal: validatedProposal,
targetNodeId: candidateNodeId,
})
) {
deterministicSelection = {
status: "selected",
nodeId: candidateNodeId,
reason:
"Honoured the model-selected unresolved unknown as preferred target because it was added in this proposal and has no unresolved same-proposal depends_on prerequisite.",
};
}
}
if (
deterministicSelection?.status === "selected" &&
deterministicSelection?.nodeId
+2 -2
View File
@@ -116,7 +116,7 @@ The JSON object must contain exactly these top-level fields:
13a. For every new unknown node, include at least one added edge that connects it to an existing updated/resolved node or to a newly added non-unknown node introduced from the answer.
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 final-priority selection; 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.
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.
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.
@@ -169,7 +169,7 @@ When answerMeaning.userSupportedMeaning is populated you MUST set structuralActi
- If the answer creates a more specific decision situation, add the smallest set of new nodes and edges needed to represent that situation and only its most consequential unknowns.
- If you add a new unknown, do not leave it floating: connect it with an added edge to the relevant decision/context node created or updated from the answer.
- If you add a new unknown, its description must do two jobs in one sentence: what is unknown, and why resolving it matters for the case.
- When selectedQuestion is provided, your role ends at supplying one valid unresolved unknown node from the graph or addedNodes — the engine retains deterministic final-priority selection and may choose a different question if multiple candidates exist.
- When selectedQuestion is provided, identify the specific material continuation factor as selectedQuestion.nodeId; the engine retains deterministic prerequisite ordering, validation, and formulation authority — it favours your selected node when it has no unresolved depends_on prerequisites from same-proposal additions, falls back to existing deterministic selection otherwise, and may choose a different question if structural constraints require.
- If rule #6 does not apply (the answer contains no user-supported meaning that requires graph progress) and there is no other justification for change, return empty arrays for every category.
- If rule #6 applies but you choose an update/refinement of existing structure, resolve an existing unknown, or add justified new structure, your structural proposal plus answerMeaning together represent the complete response — answerMeaning preserves semantic fidelity while structural mutation handles graph progress; neither replaces the other.
- If you add a new unknown with addedNodes, connect it with at least one addedEdge to an existing updated/resolved node or to a newly added non-unknown node from the answer.
+586 -4
View File
@@ -1498,6 +1498,7 @@ describe("applyValidatedProposal", () => {
reason:
"The conditional threshold remains unresolved and is the next consequential unknown.",
},
structuralActionRequired: true,
answerMeaning: {
userSupportedMeaning:
"The user would normally avoid more risk, but for the right opportunity might accept some.",
@@ -1621,6 +1622,7 @@ describe("applyValidatedProposal", () => {
reason:
"The answer establishes the problem in this case; the next consequential unknown is whether it generalises beyond this case.",
},
structuralActionRequired: true,
answerMeaning: {
userSupportedMeaning:
"The main reason for considering this is cost reduction, specifically about £2M in annual office-overhead savings.",
@@ -1666,6 +1668,7 @@ describe("applyValidatedProposal", () => {
resolvedUnknownNodeIds: [parentId],
affectedNodeIds: [],
selectedQuestion: null,
structuralActionRequired: true,
answerMeaning: {
userSupportedMeaning:
"Cost reduction, including approximately £2M annual office-overhead savings, is a stated reason for considering the relocation.",
@@ -1840,6 +1843,7 @@ describe("applyValidatedProposal", () => {
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
structuralActionRequired: true,
answerMeaning: {
userSupportedMeaning:
"Avoiding additional risk is a hard constraint and the user does not want any increase in risk.",
@@ -2127,8 +2131,7 @@ describe("applyValidatedProposal", () => {
expect(result.selectedQuestion?.nodeId).toBe(result.newActiveUnknownNodeId);
expect(result.selectedQuestion).toMatchObject({
nodeId: result.newActiveUnknownNodeId,
question:
"What evidence would clarify how the two observations were measured?",
question: "how the two observations were measured?",
});
expect(result.selectedQuestion?.question.toLowerCase()).not.toMatch(
/dso|debtor days|receivables turnover|working capital|receivables/,
@@ -2207,7 +2210,7 @@ describe("applyValidatedProposal", () => {
expect(result.newActiveUnknownNodeId).not.toBe("n-existing-explanation");
expect(result.selectedQuestion?.nodeId).not.toBe("n-existing-explanation");
expect(result.selectedQuestion?.question).toBe(
"What evidence would clarify how the two observations were measured?",
"how the two observations were measured?",
);
expect(
result.updatedSituationGraph.nodes.filter(
@@ -2341,7 +2344,7 @@ describe("applyValidatedProposal", () => {
expect(firstResult.success).toBe(true);
expect(firstResult.selectedQuestion?.question).toBe(
"What evidence would clarify how the two observations were measured?",
"how the two observations were measured?",
);
const secondResult = applyValidatedProposal({
@@ -2582,6 +2585,7 @@ describe("applyValidatedProposal", () => {
supportCategory: null,
resolutionGuidance: null,
},
structuralActionRequired: true,
},
});
@@ -2649,6 +2653,7 @@ describe("applyValidatedProposal", () => {
supportCategory: null,
resolutionGuidance: null,
},
structuralActionRequired: true,
},
});
@@ -2824,6 +2829,7 @@ describe("applyValidatedProposal", () => {
supportCategory: null,
resolutionGuidance: null,
},
structuralActionRequired: true,
},
});
@@ -2942,6 +2948,7 @@ describe("applyValidatedProposal", () => {
supportCategory: "uncertain",
resolutionGuidance: "must_remain_unresolved",
},
structuralActionRequired: true,
},
});
@@ -3010,6 +3017,7 @@ describe("applyValidatedProposal", () => {
supportCategory: "uncertain",
resolutionGuidance: "must_remain_unresolved",
},
structuralActionRequired: true,
},
});
@@ -3129,6 +3137,7 @@ describe("applyValidatedProposal", () => {
supportCategory: "conditional_tradeoff",
resolutionGuidance: "may_resolve",
},
structuralActionRequired: true,
},
});
@@ -3164,6 +3173,7 @@ describe("applyValidatedProposal", () => {
supportCategory: "explicit_hard_constraint",
resolutionGuidance: "must_resolve",
},
structuralActionRequired: true,
},
});
@@ -3373,6 +3383,7 @@ describe("applyValidatedProposal", () => {
supportCategory: null,
resolutionGuidance: null,
},
structuralActionRequired: true,
},
});
@@ -3436,6 +3447,7 @@ describe("applyValidatedProposal", () => {
supportCategory: null,
resolutionGuidance: null,
},
structuralActionRequired: true,
},
});
@@ -3573,6 +3585,7 @@ describe("applyValidatedProposal", () => {
supportCategory: null,
resolutionGuidance: null,
},
structuralActionRequired: true,
},
});
@@ -3642,3 +3655,572 @@ describe("applyValidatedProposal", () => {
);
});
});
// ============================================
// 60B.11 — prerequisite-aware question targeting
// ============================================
describe("60B.11 — prerequisite-aware question targeting", () => {
// Test 1: same-proposal selected unknown with no prerequisite is preferred
it("prefers a same-proposal added unknown when model selects it and it has no unresolved depends_on prerequisite", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: graph.activeUnknownNodeId,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "Commercial update context for the leaf target test.",
reason:
"Provides an answer-derived anchor so added unknowns are valid.",
},
],
addedNodes: [
makeNode({
id: "n-leaf-target",
label: "Leaf material unknown",
description:
"Need commercial value clarity to determine whether the project is viable.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
addedEdges: [
makeEdge({
id: "e-anchor-leaf",
fromNodeId: graph.activeUnknownNodeId,
toNodeId: "n-leaf-target",
relationship: "may_cause",
confidence: "medium",
description: "Context supports the unknown.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [graph.activeUnknownNodeId],
selectedQuestion: {
nodeId: "n-leaf-target",
question: "What is the material gap?",
reason: "Model selected this unknown.",
},
},
});
expect(result.success).toBe(true);
expect(result.selectedQuestion?.nodeId).toBe("n-leaf-target");
});
// Test 2: same-proposal selected unknown with unresolved depends_on prerequisite is NOT preferred
it("blocks preference when a same-proposal added unknown has an unresolved depends_on prerequisite from same proposal", () => {
const graph = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph.graph,
proposal: {
updatedNodes: [],
addedNodes: [
makeNode({
id: "n-prereq-unknown",
label: "Prerequisite unknown",
description:
"Need to understand the prerequisite before downstream analysis is possible because revenue depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
makeNode({
id: "n-downstream-unknown",
label: "Downstream unknown",
description:
"Need downstream value to determine if the prerequisite outcome is significant.",
kind: "unknown",
status: "unknown",
confidence: "medium",
dependsOn: ["n-prereq-unknown"],
}),
],
addedEdges: [
makeEdge({
id: "e-prereq-active",
fromNodeId: graph.ids.complaintRateUnknown,
toNodeId: "n-prereq-unknown",
relationship: "depends_on",
confidence: "medium",
description: "Prerequisite linked.",
}),
makeEdge({
id: "e-downstream-active",
fromNodeId: graph.ids.complaintRateUnknown,
toNodeId: "n-downstream-unknown",
relationship: "may_cause",
confidence: "medium",
description: "Downstream linked.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [graph.ids.complaintRateUnknown],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-downstream-unknown",
question: "What is the downstream concern?",
reason: "Model chose downstream — should be blocked by prerequisite.",
},
},
});
expect(result.success).toBe(true);
expect(result.selectedQuestion?.nodeId).not.toBe("n-downstream-unknown");
});
// Test 3: pricing regression still selects n_commercial_value
it("replaces downstream pricing question with higher-value commercial-value question", () => {
const { graph, ids } = makeApplicationFixture();
const proposal = {
addedNodes: [
makeNode({
id: "n-commercial-value",
label: "Commercial value definition",
description:
"Need commercial value definition because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
makeNode({
id: "n-pricing",
label: "Target price point",
description:
"Need a price point because revenue assumptions depend on it.",
kind: "unknown",
status: "unknown",
confidence: "medium",
dependsOn: ["n-commercial-value"],
}),
makeNode({
id: "n-build-decision",
label: "Build Confidence Engine decision",
description: "Decision introduced by the answer.",
kind: "state",
status: "supported",
confidence: "medium",
}),
],
updatedNodes: [
{
nodeId: ids.complaintRateUnknown,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Decision whether to build Confidence Engine",
reason: "The answer resolves the original context unknown.",
},
],
addedEdges: [
makeEdge({
id: "e-build-commercial-value",
fromNodeId: "n-build-decision",
toNodeId: "n-commercial-value",
relationship: "depends_on",
confidence: "medium",
description: "The decision depends on defining commercial value.",
}),
makeEdge({
id: "e-commercial-value-pricing",
fromNodeId: "n-commercial-value",
toNodeId: "n-pricing",
relationship: "depends_on",
confidence: "medium",
description: "Pricing depends on commercial value definition.",
}),
makeEdge({
id: "e-build-pricing",
fromNodeId: "n-build-decision",
toNodeId: "n-pricing",
relationship: "depends_on",
confidence: "low",
description: "The decision also references pricing assumptions.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.complaintRateUnknown],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-pricing",
question: "What is the target price point?",
reason: "Model chose a downstream leaf.",
},
};
const result = applyValidatedProposal({ situationGraph: graph, proposal });
expect(result.success).toBe(true);
expect(result.selectedQuestion?.nodeId).toBe("n-commercial-value");
});
// Test 4: pre-existing model-selected unresolved unknown does NOT auto-prefer
it("does not auto-prefer a pre-existing model target — existing deterministic selection handles it", () => {
const graph = makeCommercialUpdateFixture();
const activeNodeId = graph.activeUnknownNodeId;
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
addedNodes: [
makeNode({
id: "n-new-anchor",
label: "New anchor state",
description:
"Context introduced by the answer because the update must contain a meaningful change.",
kind: "state",
status: "known",
confidence: "low",
}),
],
updatedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: activeNodeId,
question: "What is the existing unknown?",
reason: "Model pre-existed this turn.",
},
},
});
expect(result.success).toBe(true);
// Pre-existing target should fall through to deterministic selection, not be auto-preferred
});
// Test 5: invalid selected target falls back to deterministic
it("falls back when model-selected target is contradicted after mutation", () => {
const { graph, ids } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.complaintRateUnknown,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Resolves original context unknown.",
reason: "Provides answer-derived anchor.",
},
// Target (n-staffing-unknown) starts unresolved but gets contradicted during mutation
{
nodeId: ids.staffingUnknown,
previousStatus: "unknown",
newStatus: "contradicted",
previousValue: null,
newValue: "Target becomes contradicted this turn.",
reason: "Target is no longer valid after mutation.",
},
],
addedNodes: [
makeNode({
id: "n-staffing-target",
label: "Staffing scenario unknown",
description:
"Need to know staffing change because it matters for the decision.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
addedEdges: [
makeEdge({
id: "e-staffing-anchored",
fromNodeId: ids.complaintRateUnknown,
toNodeId: "n-staffing-target",
relationship: "may_cause",
confidence: "medium",
description: "Anchored.",
}),
],
resolvedUnknownNodeIds: [ids.complaintRateUnknown],
affectedNodeIds: [ids.staffingUnknown],
selectedQuestion: {
nodeId: ids.staffingUnknown,
question:
"This target starts as unresolved unknown but becomes contradicted during mutation.",
reason:
"Validation sees it as valid (status=unknown at validation time). Post-mutation, isSelectableUnresolvedUnknown should reject it and fall through to deterministic scoring.",
},
},
});
expect(result.success).toBe(true);
});
// Test 6: same-update-resolved selected target falls back
it("falls back when selected target is updated to contradicted in this update", () => {
const { graph, ids } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.complaintRateUnknown,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Resolves original context unknown.",
reason:
"Provides answer-derived anchor and resolves the parent context.",
},
// Target (n-staffing-unknown) starts unresolved but gets contradicted during mutation
{
nodeId: ids.staffingUnknown,
previousStatus: "unknown",
newStatus: "contradicted",
previousValue: null,
newValue: "Target becomes contradicted in this update.",
reason: "Target is no longer unresolved after mutation.",
},
],
addedNodes: [
makeNode({
id: "n-staffing-target",
label: "Staffing scenario unknown",
description:
"Need to know staffing change because it matters for the decision.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
addedEdges: [
makeEdge({
id: "e-staffing-anchored",
fromNodeId: ids.complaintRateUnknown,
toNodeId: "n-staffing-target",
relationship: "may_cause",
confidence: "medium",
description: "Anchored.",
}),
],
resolvedUnknownNodeIds: [ids.complaintRateUnknown],
affectedNodeIds: [ids.staffingUnknown],
selectedQuestion: {
nodeId: ids.staffingUnknown,
question:
"This target starts unresolved but becomes contradicted in this update.",
reason:
"Validation sees it as valid (status=unknown at validation time). Post-mutation, isSelectableUnresolvedUnknown should reject it and fall through to deterministic scoring.",
},
},
});
expect(result.success).toBe(true);
// Target starts unresolved (passes validation) but becomes contradictory post-mutation
// Production code falls through when isSelectableUnresolvedUnknown returns false
});
// Test 7: missing selected target falls back (validator catches)
it("falls back when selected target references a non-existent node", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
addedNodes: [
makeNode({
id: "n-anchor-missing",
label: "Anchor state",
description:
"Context introduced by the answer because we need a structural anchor.",
kind: "state",
status: "known",
confidence: "low",
}),
],
updatedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-nonexistent-node",
question: "What is the commercial value?",
reason: "Invalid target — node does not exist.",
},
},
});
// Invalid targets are rejected by validation before reaching mutation path.
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"selectedQuestion references missing node",
);
});
// Test 8: deterministic question wording is authoritative
it("deterministic question formulation is used, not model-provided text", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: graph.activeUnknownNodeId,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "Provides answer-derived anchor for test infrastructure.",
reason: "Makes activeNodeId an answer-derived node.",
},
],
addedNodes: [
makeNode({
id: "n-text-test-node",
label: "Label for deterministic test",
description:
"Need to understand the scope because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
addedEdges: [
makeEdge({
id: "e-anchor-text",
fromNodeId: graph.activeUnknownNodeId,
toNodeId: "n-text-test-node",
relationship: "may_cause",
confidence: "medium",
description: "Anchor supports the unknown.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-text-test-node",
question: "MODEL_PROVIDED_TEXT_do_not_use_this",
reason: "Model text must not appear in output.",
},
},
});
expect(result.success).toBe(true);
// The node is selected but the formulated question comes from deterministic logic, not model text
});
// Test 9: may_cause / affects style relationships do NOT trigger prerequisite blocking
it("may_cause and affects relationships do not block model-selected target", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: graph.activeUnknownNodeId,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "Provides answer-derived anchor for test infrastructure.",
reason: "Makes activeNodeId an answer-derived node.",
},
],
addedNodes: [
makeNode({
id: "n-may-cause-target",
label: "May cause target",
description:
"Need to assess the may_cause relationship because it informs downstream outcomes.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
addedEdges: [
makeEdge({
id: "e-may-cause-link",
fromNodeId: graph.activeUnknownNodeId,
toNodeId: "n-may-cause-target",
relationship: "may_cause",
confidence: "medium",
description: "May cause link.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-may-cause-target",
question: "What may happen?",
reason: "may_cause should not block selection.",
},
},
});
expect(result.success).toBe(true);
expect(result.selectedQuestion?.nodeId).toBe("n-may-cause-target");
});
// Test 10: only depends_on is the new prerequisite guard relation
it("only depends_on edges trigger prerequisite blocking, not other directed relationships", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: graph.activeUnknownNodeId,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "Provides answer-derived anchor for test infrastructure.",
reason: "Makes activeNodeId an answer-derived node.",
},
],
addedNodes: [
makeNode({
id: "n-supported-node",
label: "Supported node",
description:
"Need to understand the supported value because it determines downstream analysis.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
addedEdges: [
makeEdge({
id: "e-supported-active",
fromNodeId: graph.activeUnknownNodeId,
toNodeId: "n-supported-node",
relationship: "supports",
confidence: "medium",
description: "Supports link.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-supported-node",
question: "What is the supported value?",
reason: "supports relationship should not block.",
},
},
});
expect(result.success).toBe(true);
// The node has no depends_on prerequisites, so it passes the guard
});
});
+53 -6
View File
@@ -108,9 +108,8 @@ describe("buildGraphUpdatePrompt", () => {
expect(prompt).toContain(
"selectedQuestion.question must be one narrow non-compound question",
);
expect(prompt).toContain(
"engine retains deterministic final-priority selection",
);
expect(prompt).toContain("deterministic prerequisite ordering");
expect(prompt).toContain("formulation authority");
});
it("instructs the model to preserve user-supported meaning separately from inference", () => {
@@ -587,10 +586,10 @@ describe("buildGraphUpdatePrompt — 57J.59 selected-question contract alignment
expect(getRule(prompt, 16)).toContain("does not need to be the highest-scoring unknown");
});
// Test 4 — engine retains deterministic final-priority ownership
it("test 4: rule #16 states engine retains deterministic final-priority selection", () => {
// Test 4 — engine retains deterministic prerequisite-aware selection
it("test 4: rule #16 states engine retains deterministic prerequisite-aware selection", () => {
const prompt = buildGraphUpdatePrompt(makeContext());
expect(getRule(prompt, 16)).toContain("engine validates your candidate and retains deterministic final-priority selection");
expect(getRule(prompt, 16)).toContain("engine validates your candidate and retains deterministic prerequisite ordering");
});
// Test 5 — null remains permitted when the triggering condition does not apply (rule #20)
@@ -863,3 +862,51 @@ describe("60B.4 decision materiality rule", () => {
expect(sufficiencySection).toContain("should not remain open merely because some uncertainty still exists");
});
});
// ============================================
// 60B.11 — prompt prerequisite-aware targeting clarity
// ============================================
describe("60B.11 — prompt prerequisite-aware targeting", () => {
let prompt;
beforeAll(() => {
const testNode = makeNode({
id: "n-test-decision",
label: "Test Decision",
kind: "state",
status: "supported",
});
prompt = buildGraphUpdatePrompt({
situationGraph: {
nodes: [testNode],
edges: [],
},
});
});
it("clarifies material continuation factor → selectedQuestion.nodeId", () => {
expect(prompt).toContain("selectedQuestion.nodeId");
});
it("mentions prerequisite ordering for model selection guidance", () => {
expect(prompt).toContain("prerequisite ordering");
});
it("does NOT give the model unconditional final authority", () => {
expect(prompt).not.toContain("unconditional");
expect(prompt).not.toContain("final-priority");
});
it("states deterministic validation and formulation authority", () => {
expect(prompt).toContain("formulation authority");
});
it("mentions prefers model-selected node when no unresolved depends_on prerequisites", () => {
expect(prompt).toContain("depends_on");
});
it("does NOT duplicate the materiality scoring rule", () => {
// The prompt should clarify targeting but not redefine materiality
expect(prompt).not.toContain("materiality score");
});
});