Feature/product platform foundation v0.62 #1

Merged
robbond merged 683 commits from feature/product-platform-foundation-v0.62 into feature/emergent-unknowns-v0.5 2026-09-09 07:58:20 +01:00
7 changed files with 630 additions and 97 deletions
Showing only changes of commit 7d06cd3c47 - Show all commits
+70 -7
View File
@@ -10,6 +10,8 @@ import {
selectReasoningPattern,
} from "./question-formulator.js";
import {
answerResolutionGuidance,
answerSupportCategory,
graphUpdateSchema,
makeNodeId,
situationGraphSchema,
@@ -92,7 +94,10 @@ function validateAddedUnknowns(graph, proposal, answerMeaning) {
.join(" "),
);
return rawAnswerSupportsUnclassifiedMeaning(userSupportedMeaningText, unknownText);
return rawAnswerSupportsUnclassifiedMeaning(
userSupportedMeaningText,
unknownText,
);
}
if (addedUnknowns.length > 3) {
@@ -2929,13 +2934,43 @@ function deriveAnswerMeaningProfile(userSupportedMeaning) {
};
}
function getAnswerMeaningProfile(answerMeaning) {
if (!answerMeaning?.userSupportedMeaning) {
return {
category: null,
resolutionGuidance: null,
usedStructuredSupportCategory: false,
usedStructuredResolutionGuidance: false,
};
}
const derivedProfile = deriveAnswerMeaningProfile(
answerMeaning.userSupportedMeaning,
);
return {
category: answerMeaning.supportCategory ?? derivedProfile.category,
resolutionGuidance:
answerMeaning.resolutionGuidance ?? derivedProfile.resolutionGuidance,
usedStructuredSupportCategory: answerMeaning.supportCategory != null,
usedStructuredResolutionGuidance: answerMeaning.resolutionGuidance != null,
};
}
function validateAnswerMeaningCompatibilityWithRawAnswer({ answer, proposal }) {
if (!answer || !proposal.answerMeaning) return [];
if (
proposal.answerMeaning.supportCategory != null ||
proposal.answerMeaning.resolutionGuidance != null
) {
return [];
}
const errors = [];
const rawAnswerProfile = deriveAnswerMeaningProfile(answer);
const supportedMeaningProfile = deriveAnswerMeaningProfile(
proposal.answerMeaning.userSupportedMeaning,
const supportedMeaningProfile = getAnswerMeaningProfile(
proposal.answerMeaning,
);
const supportedMeaningText = normaliseSemanticText(
proposal.answerMeaning.userSupportedMeaning,
@@ -3006,9 +3041,33 @@ function validateAnswerMeaningAlignment(proposal) {
const { userSupportedMeaning } = proposal.answerMeaning;
const meaningText = normaliseSemanticText(userSupportedMeaning);
const { resolved, proposalText } = proposalResolutionSummary(proposal);
const derivedProfile = deriveAnswerMeaningProfile(userSupportedMeaning);
const supportCategory = derivedProfile.category;
const resolutionGuidance = derivedProfile.resolutionGuidance;
const profile = getAnswerMeaningProfile(proposal.answerMeaning);
const supportCategory = profile.category;
const resolutionGuidance = profile.resolutionGuidance;
const usesStructuredPath =
profile.usedStructuredSupportCategory ||
profile.usedStructuredResolutionGuidance;
if (usesStructuredPath) {
if (
resolutionGuidance === answerResolutionGuidance.must_remain_unresolved &&
resolved
) {
errors.push(
"Proposal resolves an unknown even though answerMeaning.resolutionGuidance is must_remain_unresolved.",
);
}
if (
supportCategory === answerSupportCategory.explicit_hard_constraint &&
resolutionGuidance === answerResolutionGuidance.must_resolve
) {
// Deferred: the current proposal structure does not safely identify which
// specific answered/targeted unknown must resolve in every case.
}
return errors;
}
if (resolutionGuidance === "must_remain_unresolved" && resolved) {
errors.push(
@@ -3249,7 +3308,11 @@ export function applyValidatedProposal({
...validateSemanticDuplicateUnknowns(situationGraph, validatedProposal),
);
proposalCompatibilityErrors.push(
...validateAddedUnknowns(situationGraph, validatedProposal, validatedProposal.answerMeaning),
...validateAddedUnknowns(
situationGraph,
validatedProposal,
validatedProposal.answerMeaning,
),
);
const selectedQuestionValidation = validateSelectedQuestion(
+14 -1
View File
@@ -1,5 +1,7 @@
import {
ConfidenceLevel,
answerResolutionGuidance,
answerSupportCategory,
SituationKind,
SituationRelationship,
SituationStatus,
@@ -33,6 +35,8 @@ export function buildGraphUpdatePrompt({
const nodeStatuses = formatEnumValues(SituationStatus);
const edgeRelationships = formatEnumValues(SituationRelationship);
const confidenceLevels = formatEnumValues(ConfidenceLevel);
const supportCategories = formatEnumValues(answerSupportCategory);
const resolutionGuidanceValues = formatEnumValues(answerResolutionGuidance);
return `You are proposing a graph update for Confidence Engine ${promptVersion}.
@@ -60,6 +64,12 @@ ${edgeRelationships}
## Allowed Confidence Values
${confidenceLevels}
## Allowed answerMeaning.supportCategory Values
${supportCategories}
## Allowed answerMeaning.resolutionGuidance Values
${resolutionGuidanceValues}
## Required JSON Field Names
The JSON object must contain exactly these top-level fields:
- addedNodes
@@ -116,10 +126,11 @@ The JSON object must contain exactly these top-level fields:
25. Do not replace the whole graph, and do not restate unchanged graph content inside the proposal.
26. answerMeaning.userSupportedMeaning must state only what the user's answer directly supports.
27. Put any stronger interpretation in answerMeaning.possibleInference, not in userSupportedMeaning.
28. supportCategory and resolutionGuidance are optional descriptive hints only; if you are unsure of the exact wording, leave them null rather than inventing rigid category labels.
28. When answerMeaning is present, populate supportCategory with one of the allowed values whenever the user's meaning fits an existing category. Use other when none of the protected categories applies. Do not leave supportCategory null merely because the wording is uncertain.
29. If the answer is conditional or qualified, preserve that qualification explicitly in userSupportedMeaning.
30. If the answer says the user is unsure or does not resolve the distinction, state that uncertainty directly in userSupportedMeaning.
31. If the answer explicitly states a hard constraint, state that directly in userSupportedMeaning.
32. Populate resolutionGuidance when the user's meaning genuinely implies must_remain_unresolved, may_resolve, or must_resolve. Keep it null only when no existing resolution state actually applies.
## Additional Guidance
- If the answer only clarifies an existing unknown, prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes.
@@ -132,6 +143,8 @@ The JSON object must contain exactly these top-level fields:
- 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.
- For answerMeaning.supportCategory, use only these exact values: ${supportCategories}. Use other when none of the protected categories applies.
- For answerMeaning.resolutionGuidance, use only these exact values: ${resolutionGuidanceValues}. Keep it null only when none of those existing resolution states genuinely applies.
## Example Constraint Reminder
${formatExampleAnswerBlock()}
+8 -2
View File
@@ -162,8 +162,14 @@ export const answerMeaningSchema = z
.object({
userSupportedMeaning: z.string().min(1),
possibleInference: z.string().nullable().optional(),
supportCategory: z.string().min(1).nullable().optional(),
resolutionGuidance: z.string().min(1).nullable().optional(),
supportCategory: z
.enum(Object.values(answerSupportCategory))
.nullable()
.optional(),
resolutionGuidance: z
.enum(Object.values(answerResolutionGuidance))
.nullable()
.optional(),
})
.strict();
+427 -51
View File
@@ -1161,8 +1161,8 @@ describe("applyValidatedProposal", () => {
"Risk is of greater relative importance than growth.",
possibleInference:
"This may imply caution, but does not establish whether risk is a hard constraint.",
supportCategory: "relative_priority_only",
resolutionGuidance: "must_remain_unresolved",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -1204,8 +1204,8 @@ describe("applyValidatedProposal", () => {
"Avoiding additional risk is a preference/trade-off rather than a hard constraint.",
possibleInference:
"The user prioritizes risk mitigation over aggressive growth strategies.",
supportCategory: "relative_priority_only",
resolutionGuidance: "must_remain_unresolved",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -1253,8 +1253,8 @@ describe("applyValidatedProposal", () => {
"The user would normally avoid more risk, but for the right opportunity might accept some.",
possibleInference:
"This may support eventual clarification, but the qualifying condition remains material.",
supportCategory: "conditional_tradeoff",
resolutionGuidance: "may_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -1264,7 +1264,7 @@ describe("applyValidatedProposal", () => {
expect(result.errors.join(" ")).toContain("conditional qualification");
});
it("accepts the Experiment 56A supportCategory wording variant and still applies the existing Regression B guard", () => {
it("rejects the Experiment 56A supportCategory wording variant at the schema boundary", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
@@ -1305,10 +1305,10 @@ describe("applyValidatedProposal", () => {
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("conditional qualification");
expect(result.errors.join(" ")).toContain("Invalid enum value");
});
it("accepts the Experiment 56B live wording variants and still applies the existing Regression B guard", () => {
it("rejects the Experiment 56B live wording variants at the schema boundary", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
@@ -1350,10 +1350,10 @@ describe("applyValidatedProposal", () => {
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("conditional qualification");
expect(result.errors.join(" ")).toContain("Invalid enum value");
});
it("B live-variant 1: negated hard-constraint mention stays conditional rather than explicit hard constraint", () => {
it("B live-variant 1: invalid live wording is rejected before semantic validation", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
@@ -1395,13 +1395,13 @@ describe("applyValidatedProposal", () => {
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("conditional qualification");
expect(result.errors.join(" ")).toContain("Invalid enum value");
expect(result.errors.join(" ")).not.toContain(
"explicitly stated hard constraint",
);
});
it("B live-variant 2: default preference plus override stays conditional rather than other", () => {
it("B live-variant 2: invalid free-text structured hints are rejected before semantic validation", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
@@ -1441,10 +1441,7 @@ describe("applyValidatedProposal", () => {
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("conditional qualification");
expect(result.errors.join(" ")).not.toContain(
"does not clearly establish one of the protected reasoning categories",
);
expect(result.errors.join(" ")).toContain("Invalid enum value");
});
it("Regression B: preserves conditional trade-off when userSupportedMeaning stays within the raw answer", () => {
@@ -1555,8 +1552,8 @@ describe("applyValidatedProposal", () => {
"Avoiding additional risk is a preference rather than a hard constraint.",
possibleInference:
"The user may be signaling caution and a willingness to trade off growth for lower risk.",
supportCategory: "other",
resolutionGuidance: "may_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -1717,7 +1714,7 @@ describe("applyValidatedProposal", () => {
"There is proven broad market demand for a product that delivers these savings.",
possibleInference:
"The savings target could imply broader applicability if others share similar overhead pressures.",
supportCategory: "other",
supportCategory: null,
resolutionGuidance: null,
},
},
@@ -1761,8 +1758,8 @@ describe("applyValidatedProposal", () => {
userSupportedMeaning:
"The user is not sure whether avoiding additional risk is a hard constraint or a preference/trade-off.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "must_remain_unresolved",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -1803,8 +1800,8 @@ describe("applyValidatedProposal", () => {
userSupportedMeaning:
"Avoiding additional risk is a hard constraint and the user does not want any increase in risk.",
possibleInference: null,
supportCategory: "explicit_hard_constraint",
resolutionGuidance: "must_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -1905,7 +1902,7 @@ describe("applyValidatedProposal", () => {
);
});
it("fails safely when answerMeaning does not clearly establish one of the protected categories", () => {
it("fails at the schema boundary when answerMeaning uses unsupported structured values", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
@@ -1944,9 +1941,7 @@ describe("applyValidatedProposal", () => {
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"unsupported stronger meaning than answerMeaning.userSupportedMeaning establishes",
);
expect(result.errors.join(" ")).toContain("Invalid enum value");
});
it("active unknown matches selected question node", () => {
@@ -2566,7 +2561,8 @@ describe("applyValidatedProposal", () => {
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "The user needs evidence for projected office savings realism.",
newValue:
"The user needs evidence for projected office savings realism.",
reason: "The answer directs focus to savings realism evidence.",
},
],
@@ -2583,8 +2579,8 @@ describe("applyValidatedProposal", () => {
userSupportedMeaning:
"I need evidence that projected office savings are realistic.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "may_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -2601,7 +2597,8 @@ describe("applyValidatedProposal", () => {
// 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.",
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: {
@@ -2617,7 +2614,8 @@ describe("applyValidatedProposal", () => {
}),
makeNode({
id: "n-retention-impact",
label: "Whether the move materially increases loss of key engineers",
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",
@@ -2648,8 +2646,8 @@ describe("applyValidatedProposal", () => {
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",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -2664,7 +2662,8 @@ describe("applyValidatedProposal", () => {
const result = applyValidatedProposal({
situationGraph: graph,
answer: "We are looking at this mainly for cost reduction — roughly £2M annual savings on office overhead.",
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: {
@@ -2695,7 +2694,8 @@ describe("applyValidatedProposal", () => {
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-office-paint-colour",
question: "What evidence supports the office paint colour hypothesis?",
question:
"What evidence supports the office paint colour hypothesis?",
reason: "Unrelated unknown — should be rejected.",
},
answerMeaning: {
@@ -2730,7 +2730,8 @@ describe("applyValidatedProposal", () => {
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.",
description:
"Need to check whether workforce stability matters, because that could impact scheduling.",
kind: "unknown",
status: "unknown",
confidence: "medium",
@@ -2747,8 +2748,10 @@ describe("applyValidatedProposal", () => {
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.",
userSupportedMeaning:
"The project timeline is critical to the decision.",
possibleInference:
"Workforce stability may matter for scheduling and continuity.",
supportCategory: null,
resolutionGuidance: null,
},
@@ -2802,7 +2805,8 @@ describe("applyValidatedProposal", () => {
toNodeId: "n-savings-realism-5",
relationship: "depends_on",
confidence: "medium",
description: "Savings realism is a dependency of commercial justification.",
description:
"Savings realism is a dependency of commercial justification.",
}),
],
removedEdgeIds: [],
@@ -2817,8 +2821,8 @@ describe("applyValidatedProposal", () => {
userSupportedMeaning:
"We need evidence that projected office savings are realistic.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "may_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -2884,6 +2888,376 @@ describe("applyValidatedProposal", () => {
expect(result.success).toBe(true);
});
it("Structured fidelity: raw unsure + structured uncertain does not false-reject on populated structured path", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am unsure whether the projected office savings from the relocation are realistic.",
proposal: {
...makeMeaningfulNoOpProposal(),
addedNodes: [
makeNode({
id: "n-savings-anchor",
label: "Savings realism investigation context",
description:
"Answer-derived context for savings realism because the commercial decision depends on it.",
kind: "state",
status: "known",
confidence: "medium",
}),
makeNode({
id: "n-savings-realism",
label: "Whether the projected office savings are realistic",
description:
"Need to know whether the projected office savings are realistic because that matters to whether continuing development is commercially justified.",
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
],
addedEdges: [
makeEdge({
id: "e-anchor-savings-realism",
fromNodeId: "n-savings-anchor",
toNodeId: "n-savings-realism",
relationship: "depends_on",
confidence: "medium",
description:
"The answer-derived savings context depends on savings realism.",
}),
],
selectedQuestion: {
nodeId: "n-savings-realism",
question:
"What evidence would clarify whether the projected office savings are realistic?",
reason:
"The structured uncertainty remains unresolved and is the next consequential unknown.",
},
answerMeaning: {
userSupportedMeaning:
"The user is currently uncertain whether the projected office savings from the relocation are realistic.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "must_remain_unresolved",
},
},
});
expect(result.success).toBe(true);
});
it("Structured fidelity: equivalent paraphrase wording does not change acceptance when structured category is populated", () => {
const graph = makeCommercialUpdateFixture();
const baseProposal = {
...makeMeaningfulNoOpProposal(),
addedNodes: [
makeNode({
id: "n-savings-anchor",
label: "Savings realism investigation context",
description:
"Answer-derived context for savings realism because the commercial decision depends on it.",
kind: "state",
status: "known",
confidence: "medium",
}),
makeNode({
id: "n-savings-realism",
label: "Whether the projected office savings are realistic",
description:
"Need to know whether the projected office savings are realistic because that matters to whether continuing development is commercially justified.",
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
],
addedEdges: [
makeEdge({
id: "e-anchor-savings-realism",
fromNodeId: "n-savings-anchor",
toNodeId: "n-savings-realism",
relationship: "depends_on",
confidence: "medium",
description:
"The answer-derived savings context depends on savings realism.",
}),
],
selectedQuestion: {
nodeId: "n-savings-realism",
question:
"What evidence would clarify whether the projected office savings are realistic?",
reason:
"The structured uncertainty remains unresolved and is the next consequential unknown.",
},
};
const answers = [
"The user is currently uncertain whether the projected office savings from the relocation are realistic.",
"The user remains unclear whether the projected office savings from the relocation are realistic.",
];
for (const userSupportedMeaning of answers) {
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am unsure whether the projected office savings from the relocation are realistic.",
proposal: {
...baseProposal,
answerMeaning: {
userSupportedMeaning,
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "must_remain_unresolved",
},
},
});
expect(result.success).toBe(true);
}
});
it("Structured fidelity: must_remain_unresolved rejects relevant resolution mutation", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am not sure whether avoiding additional risk is a hard constraint.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Avoiding additional risk is a hard constraint.",
reason:
"Incorrectly resolves an uncertainty that should remain unresolved.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"The user is not sure whether avoiding additional risk is a hard constraint or a preference/trade-off.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "must_remain_unresolved",
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain(
"answerMeaning.resolutionGuidance is must_remain_unresolved",
);
});
it("Structured fidelity: null structured fields retain existing lexical fallback", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am unsure whether avoiding additional risk is a hard constraint.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Avoiding additional risk is a hard constraint.",
reason:
"Incorrectly resolves an uncertainty that should remain unresolved.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"The user is not sure whether avoiding additional risk is a hard constraint or a preference/trade-off.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain("must remain unresolved");
});
it("Structured fidelity: populated conditional_tradeoff uses structured path without lexical verification", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I'd normally avoid more risk, but for the right opportunity I might accept some.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is flexible under certain conditions.",
reason: "The answer provides a conditional trade-off.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"Avoiding additional risk is flexible under certain conditions.",
possibleInference: null,
supportCategory: "conditional_tradeoff",
resolutionGuidance: "may_resolve",
},
},
});
expect(result.success).toBe(true);
});
it("Structured fidelity: populated explicit_hard_constraint uses structured path without lexical verification", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer: "It's a hard constraint. I don't want any increase in risk.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Avoiding additional risk is non-negotiable.",
reason: "The answer establishes a hard constraint.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning: "Avoiding additional risk is non-negotiable.",
possibleInference: null,
supportCategory: "explicit_hard_constraint",
resolutionGuidance: "must_resolve",
},
},
});
expect(result.success).toBe(true);
});
it("Structured fidelity: possibleInference remains non-authoritative", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer: "The project timeline is critical to the decision.",
proposal: {
...makeMeaningfulNoOpProposal(),
addedNodes: [
makeNode({
id: "n-workforce-stability",
label: "Workforce stability impact",
description:
"Need to know workforce stability impact because that could affect delivery confidence.",
kind: "unknown",
status: "unknown",
confidence: "low",
}),
],
addedEdges: [
makeEdge({
id: "e-parent-workforce-stability",
fromNodeId: "n-commercial-parent",
toNodeId: "n-workforce-stability",
relationship: "depends_on",
confidence: "low",
description:
"Commercial justification depends on workforce stability.",
}),
],
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.errors.join(" ")).toContain(
"explicitly related to an answer-derived node",
);
});
it("Structured fidelity: no new synonym or keyword logic was added to lexical fallback", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am unsure whether avoiding additional risk is a hard constraint or a preference/trade-off.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Avoiding additional risk is a hard constraint.",
reason:
"Incorrectly resolves an uncertainty that should remain unresolved.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"The user is currently uncertain whether avoiding additional risk is a hard constraint or a preference/trade-off.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain(
"overstates a raw answer that remains uncertain",
);
});
// ── Case 7 — no support and no linkage ──
it("Case 7: unknown with neither user support nor structural linkage is rejected", () => {
const graph = makeCommercialUpdateFixture();
@@ -2996,8 +3370,8 @@ describe("applyValidatedProposal", () => {
userSupportedMeaning:
"We need evidence that cost reduction is achievable through office overhead and savings.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "may_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -3059,8 +3433,8 @@ describe("applyValidatedProposal", () => {
userSupportedMeaning:
"We need cost reduction through office overhead savings.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "may_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
@@ -3180,7 +3554,8 @@ describe("applyValidatedProposal", () => {
toNodeId: "n-boundary-c",
relationship: "depends_on",
confidence: "medium",
description: "Parent depends on commercial justification verification.",
description:
"Parent depends on commercial justification verification.",
}),
],
removedEdgeIds: [],
@@ -3192,10 +3567,11 @@ describe("applyValidatedProposal", () => {
reason: "Consequential unknown verified by user-supported meaning.",
},
answerMeaning: {
userSupportedMeaning: "We are considering relocation for cost savings.",
userSupportedMeaning:
"We are considering relocation for cost savings.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "may_resolve",
supportCategory: null,
resolutionGuidance: null,
},
},
});
+58 -18
View File
@@ -122,12 +122,36 @@ describe("buildGraphUpdatePrompt", () => {
"Put any stronger interpretation in answerMeaning.possibleInference",
);
expect(prompt).toContain(
"supportCategory and resolutionGuidance are optional descriptive hints only",
"populate supportCategory with one of the allowed values whenever the user's meaning fits an existing category",
);
expect(prompt).toContain(
"leave them null rather than inventing rigid category labels",
"Do not leave supportCategory null merely because the wording is uncertain",
);
});
it("lists allowed structured semantic enum values", () => {
const prompt = buildGraphUpdatePrompt(makeContext());
expect(prompt).toContain("## Allowed answerMeaning.supportCategory Values");
expect(prompt).toContain(
"relative_priority_only | conditional_tradeoff | uncertain | explicit_hard_constraint | other",
);
expect(prompt).toContain(
"must_remain_unresolved | may_resolve | must_resolve",
);
});
it("strengthens resolutionGuidance population guidance without provider-specific wording", () => {
const prompt = buildGraphUpdatePrompt(makeContext());
expect(prompt).toContain(
"Populate resolutionGuidance when the user's meaning genuinely implies must_remain_unresolved, may_resolve, or must_resolve",
);
expect(prompt).toContain(
"Keep it null only when no existing resolution state actually applies",
);
expect(prompt).not.toContain("qwen");
});
});
// ── Semantic-to-mutation contract (57J.39) ──────────────
@@ -135,7 +159,9 @@ describe("buildGraphUpdatePrompt", () => {
describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => {
it("contains the explicit structural-materialization MUST rule", () => {
const prompt = buildGraphUpdatePrompt(makeContext());
expect(prompt).toContain("MUST express its effect through structural mutation");
expect(prompt).toContain(
"MUST express its effect through structural mutation",
);
});
it("rule permits update/refine of existing structure", () => {
@@ -162,9 +188,7 @@ describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => {
const prompt = buildGraphUpdatePrompt(makeContext());
// The rule should be silent about forcing new nodes — this is preserved by existing rule #7.
// Verify the MUST rule exists but doesn't contain "must add a new node" or similar.
const mustRuleMatch = prompt.match(
/6\..*?(?=\n7\.)/s,
);
const mustRuleMatch = prompt.match(/6\..*?(?=\n7\.)/s);
expect(mustRuleMatch).not.toBe(null);
expect(mustRuleMatch[0]).not.toContain("must add a new node");
});
@@ -172,9 +196,7 @@ describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => {
it("does not imply possibleInference alone triggers mutation", () => {
const prompt = buildGraphUpdatePrompt(makeContext());
// The rule must reference userSupportedMeaning specifically, not possibleInference as a trigger.
const mustRuleMatch = prompt.match(
/6\..*?(?=\n7\.)/s,
);
const mustRuleMatch = prompt.match(/6\..*?(?=\n7\.)/s);
expect(mustRuleMatch[0]).toContain("userSupportedMeaning");
});
@@ -234,7 +256,9 @@ describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => {
expect(prompt).toContain("duplicate unknowns");
// Additional Guidance preference for update over add:
const additionalGuidance = prompt.split("## Additional Guidance")[1];
expect(additionalGuidance).toContain("prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes");
expect(additionalGuidance).toContain(
"prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes",
);
});
it("PASS: update/refine route preserved — no new mandatory-add requirement", () => {
@@ -244,8 +268,12 @@ describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => {
expect(prompt).toContain("update/refinement of existing structure");
// Additional guidance still encourages preferring updates:
const additionalGuidance = prompt.split("## Additional Guidance")[1];
expect(additionalGuidance).toContain("prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes");
expect(additionalGuidance).toContain("update that node rather than creating only a parallel observation");
expect(additionalGuidance).toContain(
"prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes",
);
expect(additionalGuidance).toContain(
"update that node rather than creating only a parallel observation",
);
});
it("PASS: possibleInference separation preserved — not converted to mandatory mutation", () => {
@@ -263,7 +291,9 @@ describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => {
expect(additionalGuidance).not.toContain("possibleInference");
// Rule #27 exists in the prompt (separation preserved):
expect(fullPrompt).toContain("answerMeaning.possibleInference, not in userSupportedMeaning");
expect(fullPrompt).toContain(
"answerMeaning.possibleInference, not in userSupportedMeaning",
);
});
it("PASS: no action-selection machinery added — no keyword routing or node-kind decision table", () => {
@@ -293,7 +323,9 @@ describe("buildGraphUpdatePrompt — 57J.46 existing-first uncertainty fallback"
it("test 1: assembled prompt explicitly says to check for an equivalent unresolved unknown first", () => {
const prompt = buildGraphUpdatePrompt(makeContext());
const guidance = getAdditionalGuidance(prompt);
expect(guidance).toContain("first check whether an existing unresolved node");
expect(guidance).toContain(
"first check whether an existing unresolved node",
);
expect(guidance).toContain("represents the same uncertainty");
});
@@ -310,7 +342,9 @@ describe("buildGraphUpdatePrompt — 57J.46 existing-first uncertainty fallback"
const prompt = buildGraphUpdatePrompt(makeContext());
const guidance = getAdditionalGuidance(prompt);
expect(guidance).toContain("if no such node exists");
expect(guidance).toContain("add a new unknown that directly represents the unresolved uncertainty");
expect(guidance).toContain(
"add a new unknown that directly represents the unresolved uncertainty",
);
});
// Test 4 — ordered fallback means: existing first, otherwise add
@@ -361,9 +395,13 @@ describe("buildGraphUpdatePrompt — 57J.46 existing-first uncertainty fallback"
expect(prompt).toContain(
"include that existing node ID in resolvedUnknownNodeIds",
);
expect(prompt).toContain("update that node rather than creating only a parallel observation");
expect(prompt).toContain(
"update that node rather than creating only a parallel observation",
);
// Rule #5 (resolve answered unknown first) must still exist:
expect(prompt).toContain("Resolve the answered unknown first when the answer supports it");
expect(prompt).toContain(
"Resolve the answered unknown first when the answer supports it",
);
});
// Test 9 — duplicate validator/contract preserved
@@ -372,7 +410,9 @@ describe("buildGraphUpdatePrompt — 57J.46 existing-first uncertainty fallback"
expect(prompt).toContain("Do not add duplicate unknowns");
expect(prompt).toContain("genuinely new concepts");
const guidance = getAdditionalGuidance(prompt);
expect(guidance).toContain("prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes");
expect(guidance).toContain(
"prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes",
);
});
// Test 10 — scope remains uncertainty-only (not universal to all categories)
+30 -2
View File
@@ -4,6 +4,8 @@ import {
SituationStatus,
ConfidenceLevel,
SituationRelationship,
answerResolutionGuidance,
answerSupportCategory,
situationNodeSchema,
situationEdgeSchema,
situationGraphSchema,
@@ -212,13 +214,39 @@ describe("graphUpdateSchema", () => {
userSupportedMeaning: "Risk matters more to me.",
possibleInference:
"This may imply caution, but does not establish a hard constraint.",
supportCategory: "relative priority only",
resolutionGuidance: "leave unresolved",
supportCategory: answerSupportCategory.relative_priority_only,
resolutionGuidance: answerResolutionGuidance.must_remain_unresolved,
},
});
expect(result.success).toBe(true);
});
it("rejects invalid supportCategory values", () => {
const result = graphUpdateSchema.safeParse({
answerMeaning: {
userSupportedMeaning: "Risk matters more to me.",
possibleInference: null,
supportCategory: "relative priority only",
resolutionGuidance: answerResolutionGuidance.must_remain_unresolved,
},
});
expect(result.success).toBe(false);
});
it("rejects invalid resolutionGuidance values", () => {
const result = graphUpdateSchema.safeParse({
answerMeaning: {
userSupportedMeaning: "Risk matters more to me.",
possibleInference: null,
supportCategory: answerSupportCategory.relative_priority_only,
resolutionGuidance: "leave unresolved",
},
});
expect(result.success).toBe(false);
});
it("rejects update with invalid node kind in addedNodes", () => {
const invalid = graphUpdateSchema.safeParse({
addedNodes: [
+23 -16
View File
@@ -104,7 +104,7 @@ describe("parseGraphUpdateProposal", () => {
expect(result.proposal.addedNodes[0].id).toBe("n-new");
});
it("accepts live-style free-text answerMeaning hints without requiring enum tokens", () => {
it("rejects supportCategory values outside the existing enum", () => {
const result = parseGraphUpdateProposal({
...makeValidProposal(),
answerMeaning: {
@@ -118,15 +118,10 @@ describe("parseGraphUpdateProposal", () => {
},
});
expect(result.success).toBe(true);
expect(result.proposal.answerMeaning).toMatchObject({
supportCategory: "conditional_preference",
resolutionGuidance:
"Identify and quantify the threshold conditions that trigger risk acceptance.",
});
expect(result.success).toBe(false);
});
it("accepts the earlier live Regression B wording variant without special aliasing", () => {
it("rejects the earlier live Regression B wording variant when it is outside the existing enum", () => {
const result = parseGraphUpdateProposal({
...makeValidProposal(),
answerMeaning: {
@@ -139,10 +134,26 @@ describe("parseGraphUpdateProposal", () => {
},
});
expect(result.success).toBe(false);
});
it("accepts valid structured answerMeaning enum values", () => {
const result = parseGraphUpdateProposal({
...makeValidProposal(),
answerMeaning: {
userSupportedMeaning:
"I'd normally avoid more risk, but for the right opportunity I might accept some.",
possibleInference:
"This may support later clarification, but the condition remains material.",
supportCategory: "conditional_tradeoff",
resolutionGuidance: "may_resolve",
},
});
expect(result.success).toBe(true);
expect(result.proposal.answerMeaning).toMatchObject({
supportCategory: "conditional_qualification",
resolutionGuidance: "may resolve once the condition is clarified",
supportCategory: "conditional_tradeoff",
resolutionGuidance: "may_resolve",
});
});
@@ -289,7 +300,7 @@ describe("parseGraphUpdateProposal", () => {
expect(result.success).toBe(false);
});
it("does not reject unsupported free-text answerMeaning labels at parse time", () => {
it("rejects unsupported answerMeaning enum labels at parse time", () => {
const result = parseGraphUpdateProposal({
...makeValidProposal(),
answerMeaning: {
@@ -300,11 +311,7 @@ describe("parseGraphUpdateProposal", () => {
},
});
expect(result.success).toBe(true);
expect(result.proposal.answerMeaning).toMatchObject({
supportCategory: "constraint_preference_mix",
resolutionGuidance: "needs more nuance",
});
expect(result.success).toBe(false);
});
it("defaults missing selectedQuestion to null", () => {