wip(confidence-engine): checkpoint open-question presentation work
This commit is contained in:
+12
-1
@@ -12,9 +12,12 @@ import {
|
||||
} from "../lib/reconstruction/prompt.js";
|
||||
import { normaliseAnalysisResponse } from "../lib/reconstruction/compatibility.js";
|
||||
import {
|
||||
reconstructionGenerationJsonSchema,
|
||||
reconstructionGenerationSchema,
|
||||
reconstructionV2Schema,
|
||||
reconstructionSchema as reconstructionV1Schema,
|
||||
} from "../lib/reconstruction/schema.js";
|
||||
import { validateReconstructionOpenQuestions } from "../lib/reconstruction/open-question-presentation.js";
|
||||
|
||||
const MAX_SCENARIO_LENGTH = 10000;
|
||||
|
||||
@@ -80,6 +83,8 @@ export async function analyseScenario(scenario, opts = {}) {
|
||||
const providerResult = await provider.generateReconstruction(
|
||||
promptObj.prompt,
|
||||
reconstructionModelName,
|
||||
reconstructionGenerationJsonSchema,
|
||||
{ outputZodSchema: reconstructionGenerationSchema },
|
||||
);
|
||||
if (
|
||||
providerResult &&
|
||||
@@ -201,6 +206,9 @@ function buildCompatibilityDiagnostics(compatibility) {
|
||||
}
|
||||
|
||||
function buildSuccessResultV2(data, model, duration, version, compatibility) {
|
||||
const presentationValidation = validateReconstructionOpenQuestions(
|
||||
data.reconstruction,
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
validationStatus: "valid",
|
||||
@@ -209,10 +217,13 @@ function buildSuccessResultV2(data, model, duration, version, compatibility) {
|
||||
rawResponse: JSON.stringify(data).slice(0, 3000),
|
||||
promptVersion: version,
|
||||
inputClassification: data.inputClassification,
|
||||
reconstruction: data.reconstruction,
|
||||
reconstruction: presentationValidation.reconstruction,
|
||||
evidence: data.evidence,
|
||||
nextQuestion: data.nextQuestion,
|
||||
errors: undefined,
|
||||
presentationValidationStatus:
|
||||
presentationValidation.issues.length > 0 ? "partial" : "valid",
|
||||
presentationValidationIssues: presentationValidation.issues,
|
||||
...buildCompatibilityDiagnostics(compatibility),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@ export function buildInitialGraph(analysisData) {
|
||||
null,
|
||||
unk.confidence || "low",
|
||||
);
|
||||
if (unk.question) node.openQuestion = unk.question;
|
||||
mapSourceId(unk.id, node);
|
||||
unknownNodes.push(node);
|
||||
}
|
||||
|
||||
@@ -88,6 +88,10 @@ function buildDiagnostics({
|
||||
compatibilityApplied: analysis?.compatibilityApplied ?? false,
|
||||
compatibilityChanges: analysis?.compatibilityChanges ?? [],
|
||||
compatibilityWarnings: analysis?.compatibilityWarnings ?? [],
|
||||
presentationValidationStatus:
|
||||
analysis?.presentationValidationStatus ?? "not_applicable",
|
||||
presentationValidationIssues:
|
||||
analysis?.presentationValidationIssues ?? [],
|
||||
unknownSelectionExplanation: unknownSelectionExplanation ?? null,
|
||||
reconstructionQuestion: reconstructionQuestion ?? null,
|
||||
reconstructionQuestionAccepted: reconstructionQuestionAccepted ?? null,
|
||||
|
||||
@@ -57,6 +57,7 @@ export const situationNodeSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
label: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
openQuestion: z.string().min(1).optional(),
|
||||
kind: z.enum(Object.values(SituationKind)),
|
||||
status: z.enum(Object.values(SituationStatus)),
|
||||
confidence: z.enum(Object.values(ConfidenceLevel)),
|
||||
@@ -259,6 +260,7 @@ export function makeNode(opts) {
|
||||
id,
|
||||
label: opts.label,
|
||||
description: opts.description ?? opts.label,
|
||||
openQuestion: opts.openQuestion,
|
||||
kind: opts.kind ?? "observation",
|
||||
status: opts.status ?? "unknown",
|
||||
confidence: opts.confidence ?? "medium",
|
||||
|
||||
+17
-5
@@ -546,10 +546,15 @@ class OpenAIReconstructionProvider {
|
||||
this.fetchImpl = fetchImpl;
|
||||
}
|
||||
|
||||
async generateReconstruction(prompt, modelName = "gpt-5.6-terra", outputSchema, { signal } = {}) {
|
||||
async generateReconstruction(
|
||||
prompt,
|
||||
modelName = "gpt-5.6-terra",
|
||||
outputSchema,
|
||||
{ signal, outputZodSchema } = {},
|
||||
) {
|
||||
if (!this.apiKey) throw new Error("OPENAI_API_KEY is not set");
|
||||
const structuredOutputSchema = outputSchema
|
||||
? createOpenAIStrictSchema(outputSchema, null)
|
||||
? createOpenAIStrictSchema(outputSchema, outputZodSchema ?? null)
|
||||
: openAIReconstructionJsonSchema;
|
||||
|
||||
const format = {
|
||||
@@ -594,10 +599,17 @@ class OpenAIReconstructionProvider {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const recoveredResponse = recoverJson(outputText);
|
||||
const normalisedResponse = outputSchema && !outputZodSchema
|
||||
? recoveredResponse
|
||||
: normaliseOpenAITransportResponse(
|
||||
recoveredResponse,
|
||||
outputZodSchema ?? reconstructionV2Schema,
|
||||
outputSchema ?? reconstructionJsonSchema,
|
||||
);
|
||||
|
||||
return {
|
||||
response: outputSchema
|
||||
? recoverJson(outputText)
|
||||
: normaliseOpenAITransportResponse(recoverJson(outputText)),
|
||||
response: normalisedResponse,
|
||||
providerApiPath: "/v1/responses",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -274,8 +274,14 @@ export function buildFacilitatorViewModel(_ref) {
|
||||
// Scaffold items are entirely suppressed from user-facing sections.
|
||||
if (semanticRole === "scaffolding") continue;
|
||||
|
||||
// An unknown's openQuestion is a model-authored presentation projection.
|
||||
// Its label and description remain the canonical uncertainty representation.
|
||||
var displayText =
|
||||
node.kind === "unknown" && !resolved && node.openQuestion
|
||||
? node.openQuestion.trim()
|
||||
: displayResult.displayText;
|
||||
var entry = {
|
||||
text: displayResult.displayText,
|
||||
text: displayText,
|
||||
normalised: normaliseText(displayResult.displayText),
|
||||
kind: node.kind,
|
||||
semanticRole: semanticRole,
|
||||
@@ -410,8 +416,10 @@ export function buildFacilitatorViewModel(_ref) {
|
||||
if (!a.isActiveUnknown && b.isActiveUnknown) return 1;
|
||||
|
||||
// Selected-question target: match by normalised text
|
||||
if (sqText && a.normalised === sqText && b.normalised !== sqText) return -1;
|
||||
if (sqText && a.normalised !== sqText && b.normalised === sqText) return 1;
|
||||
var aMatchesSelectedQuestion = sqText && normaliseText(a.text) === sqText;
|
||||
var bMatchesSelectedQuestion = sqText && normaliseText(b.text) === sqText;
|
||||
if (aMatchesSelectedQuestion && !bMatchesSelectedQuestion) return -1;
|
||||
if (!aMatchesSelectedQuestion && bMatchesSelectedQuestion) return 1;
|
||||
|
||||
// Explicit priority fields where available in the graph
|
||||
var pa = a.priority != null ? a.priority : null;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
const ISSUE_MESSAGES = {
|
||||
question_missing: "No user-facing Open Question was generated.",
|
||||
question_empty: "The generated Open Question was empty.",
|
||||
question_not_interrogative: "The generated Open Question was not a direct interrogative.",
|
||||
question_indirect_proposition:
|
||||
"The generated Open Question used an indirect uncertainty proposition.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Narrow presentation validation only. This does not attempt to prove semantic
|
||||
* equivalence with the canonical uncertainty and never rewrites either value.
|
||||
*/
|
||||
export function validateOpenQuestionCandidate(candidate) {
|
||||
if (candidate === undefined || candidate === null) {
|
||||
return { valid: false, category: "question_missing" };
|
||||
}
|
||||
|
||||
if (typeof candidate !== "string" || candidate.trim().length === 0) {
|
||||
return { valid: false, category: "question_empty" };
|
||||
}
|
||||
|
||||
const question = candidate.trim();
|
||||
if (/^whether\b/i.test(question)) {
|
||||
return { valid: false, category: "question_indirect_proposition" };
|
||||
}
|
||||
|
||||
if (!question.endsWith("?")) {
|
||||
return { valid: false, category: "question_not_interrogative" };
|
||||
}
|
||||
|
||||
return { valid: true, question };
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove invalid presentation candidates without changing canonical descriptions,
|
||||
* IDs, ordering, relationships, or any other reconstruction content.
|
||||
*/
|
||||
export function validateReconstructionOpenQuestions(reconstruction) {
|
||||
const issues = [];
|
||||
const importantUnknowns = (reconstruction?.importantUnknowns ?? []).map(
|
||||
(unknown, index) => {
|
||||
const validation = validateOpenQuestionCandidate(unknown.question);
|
||||
if (validation.valid) {
|
||||
return { ...unknown, question: validation.question };
|
||||
}
|
||||
|
||||
const { question: _invalidQuestion, ...canonicalUnknown } = unknown;
|
||||
issues.push({
|
||||
category: validation.category,
|
||||
path: ["reconstruction", "importantUnknowns", index, "question"],
|
||||
unknownId: unknown.id,
|
||||
message: ISSUE_MESSAGES[validation.category],
|
||||
severity: "nonfatal",
|
||||
});
|
||||
return canonicalUnknown;
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
reconstruction: { ...reconstruction, importantUnknowns },
|
||||
issues,
|
||||
};
|
||||
}
|
||||
@@ -144,6 +144,21 @@ const reconstructionRelationshipSchema = z.object({
|
||||
confidence: confidenceEnum,
|
||||
});
|
||||
|
||||
const importantUnknownSchema = itemSchemaV1.extend({
|
||||
// Canonical uncertainty meaning remains in description. question is an
|
||||
// optional, model-authored user-facing interrogative for the Open Questions UI.
|
||||
// Keep this tolerant for historical data and nonfatal presentation validation.
|
||||
question: z.string().optional(),
|
||||
});
|
||||
|
||||
const generatedImportantUnknownSchema = itemSchemaV1.extend({
|
||||
// New generation requests require a distinct presentation candidate. More
|
||||
// nuanced interrogative validation is intentionally performed locally after
|
||||
// canonical reconstruction validation rather than encoded as provider-specific
|
||||
// JSON Schema grammar.
|
||||
question: z.string().trim().min(1),
|
||||
});
|
||||
|
||||
const reconstructionSchemaV2 = z.object({
|
||||
summary: z.string().min(1),
|
||||
actors: z.array(itemSchemaV1),
|
||||
@@ -167,7 +182,7 @@ const reconstructionSchemaV2 = z.object({
|
||||
}),
|
||||
),
|
||||
contradictions: z.array(itemSchemaV1),
|
||||
importantUnknowns: z.array(itemSchemaV1),
|
||||
importantUnknowns: z.array(importantUnknownSchema),
|
||||
plausibleInterpretations: z.array(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
@@ -205,6 +220,19 @@ export const reconstructionV2Schema = z.object({
|
||||
nextQuestion: nextQuestionSchema,
|
||||
});
|
||||
|
||||
// Generation-only contract. Durable reconstruction parsing remains tolerant so
|
||||
// historical snapshots and otherwise valid canonical reconstructions do not fail
|
||||
// solely because presentation wording is absent or malformed.
|
||||
export const reconstructionGenerationSchema = reconstructionV2Schema.extend({
|
||||
reconstruction: reconstructionSchemaV2.extend({
|
||||
importantUnknowns: z.array(generatedImportantUnknownSchema),
|
||||
}),
|
||||
});
|
||||
|
||||
export const reconstructionGenerationJsonSchema = z.toJSONSchema(
|
||||
reconstructionGenerationSchema,
|
||||
);
|
||||
|
||||
// Outer wrapper for API return (includes diagnostics + v0.2 data)
|
||||
export const analyseResponseV2Schema = z.object({
|
||||
inputClassification: inputClassificationSchema.optional(),
|
||||
|
||||
Reference in New Issue
Block a user