Feature/product platform foundation v0.62 #1
+170
-42
@@ -2038,6 +2038,137 @@ function selectDecompositionChildCandidate(
|
||||
};
|
||||
}
|
||||
|
||||
function buildSelectedQuestionResult({
|
||||
updatedSituationGraph,
|
||||
deterministicSelection,
|
||||
}) {
|
||||
const selectedNode =
|
||||
deterministicSelection?.status === "selected"
|
||||
? findNodeById(updatedSituationGraph, deterministicSelection.nodeId)
|
||||
: null;
|
||||
const formulatedQuestion = selectedNode
|
||||
? formulateQuestion({
|
||||
node: selectedNode,
|
||||
graph: updatedSituationGraph,
|
||||
context: { selectionState: deterministicSelection },
|
||||
})
|
||||
: null;
|
||||
|
||||
const selectedQuestion =
|
||||
deterministicSelection?.status === "ambiguous"
|
||||
? {
|
||||
id: "q_tie_resolution",
|
||||
...formulateTieResolutionQuestion({ graph: updatedSituationGraph }),
|
||||
nodeId: null,
|
||||
tiedCandidateIds: deterministicSelection.tiedCandidateIds,
|
||||
}
|
||||
: deterministicSelection?.status === "selected" && formulatedQuestion
|
||||
? {
|
||||
nodeId: deterministicSelection.nodeId,
|
||||
question:
|
||||
formulatedQuestion.question || deterministicSelection.question,
|
||||
reason: formulatedQuestion.reason,
|
||||
strategy: formulatedQuestion.strategy,
|
||||
investigationStrategy: formulatedQuestion.investigationStrategy,
|
||||
reasoningPattern: formulatedQuestion.reasoningPattern,
|
||||
reasoningPatternReason: formulatedQuestion.reasoningPatternReason,
|
||||
questionFamily: formulatedQuestion.questionFamily,
|
||||
allowedQuestionFamilies: formulatedQuestion.allowedQuestionFamilies,
|
||||
rejectedQuestionFamilies:
|
||||
formulatedQuestion.rejectedQuestionFamilies,
|
||||
selectedQuestionTemplate:
|
||||
formulatedQuestion.selectedQuestionTemplate,
|
||||
questionComplexity: formulatedQuestion.questionComplexity,
|
||||
plainLanguageNormalisations:
|
||||
formulatedQuestion.plainLanguageNormalisations,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
selectedNode,
|
||||
formulatedQuestion,
|
||||
selectedQuestion,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAmbiguousGraphBackedSelection({
|
||||
graphSnapshot,
|
||||
updatedSituationGraph,
|
||||
deterministicSelection,
|
||||
}) {
|
||||
const orderedCandidateIds =
|
||||
deterministicSelection?.displayOrder ||
|
||||
deterministicSelection?.tiedCandidateIds ||
|
||||
[];
|
||||
|
||||
for (const candidateNodeId of orderedCandidateIds) {
|
||||
if (
|
||||
!isSelectableUnresolvedUnknown(updatedSituationGraph, candidateNodeId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidateResult = runDeterministicDecomposition({
|
||||
graphSnapshot,
|
||||
proposalSnapshot: {
|
||||
addedNodes: [],
|
||||
updatedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: null,
|
||||
},
|
||||
updatedSituationGraph: cloneJsonSafe(updatedSituationGraph),
|
||||
reasoningResolution: { reasoningStateOverride: {} },
|
||||
deterministicSelection: {
|
||||
status: "selected",
|
||||
nodeId: candidateNodeId,
|
||||
reason:
|
||||
"Selected this tied candidate for deterministic decomposition-based reselection.",
|
||||
},
|
||||
});
|
||||
|
||||
if (!candidateResult.success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextGraph = candidateResult.updatedSituationGraph;
|
||||
nextGraph.reasoningState = buildReasoningState(nextGraph);
|
||||
const nextSelection = isSelectableUnresolvedUnknown(
|
||||
nextGraph,
|
||||
candidateResult.selectedChildNodeId,
|
||||
)
|
||||
? {
|
||||
status: "selected",
|
||||
nodeId: candidateResult.selectedChildNodeId,
|
||||
reason:
|
||||
"Selected the preserved decomposition child after resolving an initial tie.",
|
||||
}
|
||||
: candidateResult.deterministicSelection;
|
||||
|
||||
const questionResult = buildSelectedQuestionResult({
|
||||
updatedSituationGraph: nextGraph,
|
||||
deterministicSelection: nextSelection,
|
||||
});
|
||||
|
||||
if (questionResult.selectedQuestion?.question) {
|
||||
nextGraph.activeUnknownNodeId =
|
||||
nextSelection?.status === "selected" ? nextSelection.nodeId : null;
|
||||
nextGraph.currentSummary = describeGraph(nextGraph);
|
||||
|
||||
return {
|
||||
...candidateResult,
|
||||
updatedSituationGraph: nextGraph,
|
||||
deterministicSelection: nextSelection,
|
||||
...questionResult,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function determineGraphBackedQuestion({ situationGraph }) {
|
||||
const graphSnapshot = cloneJsonSafe(situationGraph);
|
||||
let updatedSituationGraph = cloneJsonSafe(situationGraph);
|
||||
@@ -2090,52 +2221,48 @@ export function determineGraphBackedQuestion({ situationGraph }) {
|
||||
: null;
|
||||
updatedSituationGraph.currentSummary = describeGraph(updatedSituationGraph);
|
||||
|
||||
const selectedNode =
|
||||
deterministicSelection?.status === "selected"
|
||||
? findNodeById(updatedSituationGraph, deterministicSelection.nodeId)
|
||||
: null;
|
||||
const formulatedQuestion = selectedNode
|
||||
? formulateQuestion({
|
||||
node: selectedNode,
|
||||
graph: updatedSituationGraph,
|
||||
context: { selectionState: deterministicSelection },
|
||||
})
|
||||
: null;
|
||||
let questionResult = buildSelectedQuestionResult({
|
||||
updatedSituationGraph,
|
||||
deterministicSelection,
|
||||
});
|
||||
|
||||
if (
|
||||
deterministicSelection?.status === "ambiguous" &&
|
||||
!questionResult.selectedQuestion?.question
|
||||
) {
|
||||
const reselectionResult = resolveAmbiguousGraphBackedSelection({
|
||||
graphSnapshot,
|
||||
updatedSituationGraph,
|
||||
deterministicSelection,
|
||||
});
|
||||
|
||||
if (reselectionResult) {
|
||||
updatedSituationGraph = reselectionResult.updatedSituationGraph;
|
||||
deterministicSelection = reselectionResult.deterministicSelection;
|
||||
questionResult = reselectionResult;
|
||||
}
|
||||
}
|
||||
|
||||
const noQuestionReason = questionResult.selectedQuestion?.question
|
||||
? null
|
||||
: deterministicSelection?.status === "ambiguous"
|
||||
? questionResult.selectedQuestion?.questionSuppressedReason ||
|
||||
questionResult.selectedQuestion?.reason ||
|
||||
"Eligible unresolved candidates remain tied after initial graph-backed selection."
|
||||
: (updatedSituationGraph.nodes || []).some(
|
||||
(node) =>
|
||||
node.kind === "unknown" &&
|
||||
!["resolved", "contradicted"].includes(node.status) &&
|
||||
!(updatedSituationGraph.resolvedNodeIds || []).includes(node.id),
|
||||
)
|
||||
? "Compatible unresolved candidates remain, but none produced a valid graph-backed question."
|
||||
: "No unresolved unknown candidates remain after initial graph construction.";
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updatedSituationGraph,
|
||||
deterministicSelection,
|
||||
selectedQuestion:
|
||||
deterministicSelection?.status === "ambiguous"
|
||||
? {
|
||||
id: "q_tie_resolution",
|
||||
...formulateTieResolutionQuestion({ graph: updatedSituationGraph }),
|
||||
nodeId: null,
|
||||
tiedCandidateIds: deterministicSelection.tiedCandidateIds,
|
||||
}
|
||||
: deterministicSelection?.status === "selected" && formulatedQuestion
|
||||
? {
|
||||
nodeId: deterministicSelection.nodeId,
|
||||
question:
|
||||
formulatedQuestion.question || deterministicSelection.question,
|
||||
reason: formulatedQuestion.reason,
|
||||
strategy: formulatedQuestion.strategy,
|
||||
investigationStrategy: formulatedQuestion.investigationStrategy,
|
||||
reasoningPattern: formulatedQuestion.reasoningPattern,
|
||||
reasoningPatternReason: formulatedQuestion.reasoningPatternReason,
|
||||
questionFamily: formulatedQuestion.questionFamily,
|
||||
allowedQuestionFamilies:
|
||||
formulatedQuestion.allowedQuestionFamilies,
|
||||
rejectedQuestionFamilies:
|
||||
formulatedQuestion.rejectedQuestionFamilies,
|
||||
selectedQuestionTemplate:
|
||||
formulatedQuestion.selectedQuestionTemplate,
|
||||
questionComplexity: formulatedQuestion.questionComplexity,
|
||||
plainLanguageNormalisations:
|
||||
formulatedQuestion.plainLanguageNormalisations,
|
||||
}
|
||||
: null,
|
||||
selectedQuestion: questionResult.selectedQuestion,
|
||||
atomicityAssessment: decompositionResult.atomicityAssessment,
|
||||
answerabilityAssessment: decompositionResult.answerabilityAssessment,
|
||||
independentlyAnswerable:
|
||||
@@ -2160,7 +2287,8 @@ export function determineGraphBackedQuestion({ situationGraph }) {
|
||||
selectedUnknownBefore: decompositionResult.selectedUnknownBefore,
|
||||
selectedUnknownAfter: deterministicSelection?.nodeId ?? null,
|
||||
questionComplexityAssessment:
|
||||
formulatedQuestion?.questionComplexity ?? null,
|
||||
questionResult.formulatedQuestion?.questionComplexity ?? null,
|
||||
noQuestionReason,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ function buildDiagnostics({
|
||||
compatibilityFailures,
|
||||
replacementActions,
|
||||
graphReasoningIntegrity,
|
||||
noQuestionReason,
|
||||
}) {
|
||||
return {
|
||||
promptVersion: analysis?.promptVersion ?? null,
|
||||
@@ -112,6 +113,28 @@ function buildDiagnostics({
|
||||
compatibilityFailures: compatibilityFailures ?? [],
|
||||
replacementActions: replacementActions ?? [],
|
||||
graphReasoningIntegrity: graphReasoningIntegrity ?? null,
|
||||
noQuestionReason: noQuestionReason ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function fallbackStartCaseReasoningPatternValidation(
|
||||
selectedQuestion,
|
||||
existingValidation,
|
||||
) {
|
||||
if (existingValidation) {
|
||||
return existingValidation;
|
||||
}
|
||||
|
||||
if (!selectedQuestion?.reasoningPattern) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
activePattern: selectedQuestion.reasoningPattern,
|
||||
valid: Boolean(selectedQuestion.question),
|
||||
reason: selectedQuestion.question
|
||||
? "Initial graph-backed selection produced a reasoning-pattern-compatible question."
|
||||
: "Initial graph-backed selection did not produce a valid question for the inferred reasoning pattern.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -443,8 +466,10 @@ export async function startCase(body) {
|
||||
reasoningPatternReason:
|
||||
initialQuestionResult.selectedQuestion?.reasoningPatternReason ??
|
||||
null,
|
||||
reasoningPatternValidation:
|
||||
initialQuestionResult.reasoningPatternValidation ?? null,
|
||||
reasoningPatternValidation: fallbackStartCaseReasoningPatternValidation(
|
||||
initialQuestionResult.selectedQuestion,
|
||||
initialQuestionResult.reasoningPatternValidation,
|
||||
),
|
||||
patternCompatibleNodeCount:
|
||||
initialQuestionResult.patternCompatibleNodeCount ?? 0,
|
||||
incompatibleNodeIds: initialQuestionResult.incompatibleNodeIds ?? [],
|
||||
@@ -453,6 +478,7 @@ export async function startCase(body) {
|
||||
replacementActions: initialQuestionResult.replacementActions ?? [],
|
||||
graphReasoningIntegrity:
|
||||
initialQuestionResult.graphReasoningIntegrity ?? null,
|
||||
noQuestionReason: initialQuestionResult.noQuestionReason ?? null,
|
||||
}),
|
||||
validationErrors: graphReferenceValidation.errors,
|
||||
statusCode: 500,
|
||||
@@ -509,8 +535,10 @@ export async function startCase(body) {
|
||||
null,
|
||||
reasoningPatternReason:
|
||||
initialQuestionResult.selectedQuestion?.reasoningPatternReason ?? null,
|
||||
reasoningPatternValidation:
|
||||
initialQuestionResult.reasoningPatternValidation ?? null,
|
||||
reasoningPatternValidation: fallbackStartCaseReasoningPatternValidation(
|
||||
initialQuestionResult.selectedQuestion,
|
||||
initialQuestionResult.reasoningPatternValidation,
|
||||
),
|
||||
patternCompatibleNodeCount:
|
||||
initialQuestionResult.patternCompatibleNodeCount ?? 0,
|
||||
incompatibleNodeIds: initialQuestionResult.incompatibleNodeIds ?? [],
|
||||
@@ -518,6 +546,7 @@ export async function startCase(body) {
|
||||
replacementActions: initialQuestionResult.replacementActions ?? [],
|
||||
graphReasoningIntegrity:
|
||||
initialQuestionResult.graphReasoningIntegrity ?? null,
|
||||
noQuestionReason: initialQuestionResult.noQuestionReason ?? null,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,6 +92,49 @@ function makeCommercialAnalysisResult(overrides = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function makeCommercialTieAnalysisResult(overrides = {}) {
|
||||
return makeAnalysisResult({
|
||||
reconstruction: {
|
||||
summary:
|
||||
"A new reasoning method may become a commercial product, but multiple broad decision unknowns remain unresolved.",
|
||||
actors: [],
|
||||
systemsOrObjects: [],
|
||||
expectedStates: [],
|
||||
observedStates: [],
|
||||
differences: [],
|
||||
knownTransitions: [],
|
||||
unexplainedTransitions: [],
|
||||
contradictions: [],
|
||||
importantUnknowns: [
|
||||
{
|
||||
id: "unk-fit-pay",
|
||||
label:
|
||||
"Evidence of genuine problem-solution fit and actual willingness to pay among target users",
|
||||
description:
|
||||
"Need to know whether there is real problem-solution fit and willingness to pay among target users before continuing development.",
|
||||
confidence: "high",
|
||||
},
|
||||
{
|
||||
id: "unk-distinction",
|
||||
label:
|
||||
"Clear, measurable distinction between the method and existing AI tools that justifies separate commercial value",
|
||||
description:
|
||||
"Need to know whether there is a clear measurable distinction from existing AI tools before continuing development.",
|
||||
confidence: "high",
|
||||
},
|
||||
],
|
||||
plausibleInterpretations: [],
|
||||
},
|
||||
nextQuestion: {
|
||||
id: "q-commercial-tie",
|
||||
question:
|
||||
"What specific validation metrics, pilot feedback, or competitive benchmarking results have you collected?",
|
||||
reason: "Model-proposed broad validation question",
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function makeCommercialUpdateGraph() {
|
||||
const parent = makeNode({
|
||||
id: "n-commercial-parent",
|
||||
@@ -512,6 +555,39 @@ describe("lib/graph/orchestrator startCase", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("reselects and decomposes a tied commercial start-case candidate instead of returning a silent null question", async () => {
|
||||
mockAnalyseScenario.mockResolvedValue(makeCommercialTieAnalysisResult());
|
||||
const { startCase } = await import("@/lib/graph/orchestrator.js");
|
||||
|
||||
const result = await startCase({
|
||||
scenario:
|
||||
"I have developed a new reasoning method that aims to help people determine whether they have enough justified confidence to make a decision. I believe it could become a commercial product, but I do not yet know whether it solves a genuine problem, whether people would value it enough to pay for it, or whether it is fundamentally different from existing AI tools. Before investing significant time and money into building it further, I want to determine whether continuing development is commercially justified.",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(
|
||||
result.situationGraph.nodes.some((node) => node.kind === "unknown"),
|
||||
).toBe(true);
|
||||
expect(result.selectedQuestion).not.toBeNull();
|
||||
expect(result.selectedQuestion?.question).toBe(
|
||||
"Who experiences this problem?",
|
||||
);
|
||||
expect(result.selectedQuestion?.nodeId).toBe(
|
||||
result.situationGraph.activeUnknownNodeId,
|
||||
);
|
||||
expect(result.diagnostics.noQuestionReason).toBeNull();
|
||||
expect(result.diagnostics.selectedContainerUnknown).toBeTruthy();
|
||||
expect(result.diagnostics.selectedChildUnknown).toBeTruthy();
|
||||
expect(result.diagnostics.decompositionApplied).toBe(true);
|
||||
expect(result.diagnostics.reasoningPatternValidation).toMatchObject({
|
||||
activePattern: "decision",
|
||||
valid: true,
|
||||
});
|
||||
expect(result.selectedQuestion?.question.toLowerCase()).not.toMatch(
|
||||
/pay|price|pricing|budget|benchmark/,
|
||||
);
|
||||
});
|
||||
|
||||
it("includes compatibility diagnostics when provided by analysis", async () => {
|
||||
mockAnalyseScenario.mockResolvedValue(
|
||||
makeAnalysisResult({
|
||||
|
||||
@@ -472,6 +472,44 @@ describe("graph-backed UI rendering", () => {
|
||||
expect(html).not.toContain("Update situation");
|
||||
});
|
||||
|
||||
it("renders the initial graph-backed commercial question when start-case reselection succeeds", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<ScenarioResultPanels
|
||||
result={makeGraphResult({
|
||||
situationGraph: {
|
||||
...makeGraphResult().situationGraph,
|
||||
activeUnknownNodeId: "n-who",
|
||||
nodes: [
|
||||
...makeGraphResult().situationGraph.nodes,
|
||||
{
|
||||
id: "n-who",
|
||||
label: "Who experiences this problem",
|
||||
description:
|
||||
"Need to know who experiences this problem, because that must be clear before deciding whether it is commercially justified.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
parentId: "n-parent",
|
||||
},
|
||||
],
|
||||
},
|
||||
selectedQuestion: {
|
||||
nodeId: "n-who",
|
||||
question: "Who experiences this problem?",
|
||||
reason: "Graph-backed commercial child selection.",
|
||||
},
|
||||
diagnostics: {
|
||||
...makeGraphResult().diagnostics,
|
||||
noQuestionReason: null,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).toContain("Who experiences this problem?");
|
||||
expect(html).toContain("Selected Question");
|
||||
});
|
||||
|
||||
it("renders diagnostics", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<DiagnosticsView result={makeGraphResult()} />,
|
||||
|
||||
Reference in New Issue
Block a user