feat(uncertainty-led-investigation): add Condition D freeform deconstruct treatment selector
- Add reconstruct-v0.5-d-deconstruct-no21.md prompt (removes rule 21) from original deconstruct-first prompt: 'Do not create artificial uncertainties to increase their number.' - Extend start-case-experiment-helper.cjs with --condition D and --treatment no21 CLI flags for treatment selector apparatus - Add programmatic treatment validation (rejects invalid treatments cross-condition, validates only 'no21' is a valid treatment value) - Condition D output uses freeformText field instead of reconstruction JSON — avoids production JSON schema enforcement on freeform text - Mock double supports freeform mode: returns deterministic raw text instead of structured importantUnknowns when isFreeform=true - Add 23 targeted tests: J (Condition D prompt selection), J.1 (treatment validation), J.2 (prompt isolation verification) - Fix existing B.1 assertions to use matchObject for execution field
This commit is contained in:
@@ -56,20 +56,39 @@ function readScenarioInput(argv) {
|
||||
|
||||
async function runStartCaseExperiment(scenarioInput, experimentInstruction, options = {}) {
|
||||
const condition = options.condition;
|
||||
const treatment = options.treatment;
|
||||
|
||||
// ── Condition B/C: load alt prompt + generation schema (question-free) ──
|
||||
// ── Condition B/C/D: load alt prompt (B/C question-free; D deconstruct-freeform) ──
|
||||
let scenarioTextForPrompt = scenarioInput?.scenario;
|
||||
if ((condition === "B" || condition === "C") && typeof scenarioTextForPrompt === "string") {
|
||||
if ((condition === "B" || condition === "C" || condition === "D") && typeof scenarioTextForPrompt === "string") {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const altPromptFile = condition === "B"
|
||||
? "reconstruct-v0.5-b-noreq.md"
|
||||
: "reconstruct-v0.5-c-domain.md";
|
||||
let altPromptFile;
|
||||
if (condition === "B") {
|
||||
altPromptFile = "reconstruct-v0.5-b-noreq.md";
|
||||
} else if (condition === "C") {
|
||||
altPromptFile = "reconstruct-v0.5-c-domain.md";
|
||||
} else {
|
||||
// condition === "D"
|
||||
altPromptFile = treatment === "no21"
|
||||
? "reconstruct-v0.5-d-deconstruct-no21.md"
|
||||
: "reconstruct-v0.5-d-deconstruct.md";
|
||||
}
|
||||
const altPromptPath = path.join(PROJECT_ROOT, "prompts", altPromptFile);
|
||||
const altPromptContent = fs.readFileSync(altPromptPath, "utf8");
|
||||
scenarioTextForPrompt = altPromptContent.replace("{{SCENARIO}}", scenarioTextForPrompt);
|
||||
}
|
||||
|
||||
// ── Treatment validation (programmatic API) ────────────────
|
||||
if (condition === "D") {
|
||||
const validTreatments = ["no21"];
|
||||
if (treatment != null && !validTreatments.includes(treatment)) {
|
||||
throw new Error(`Invalid treatment: ${treatment}. Must be one of: no21`);
|
||||
}
|
||||
} else if (treatment != null) {
|
||||
throw new Error(`Treatment "${treatment}" is only valid with --condition D, not --condition ${condition}`);
|
||||
}
|
||||
|
||||
// ── Experiment seam bridge: supply instruction to production path ──
|
||||
const hadEnvVar = process.env.RECONSTRUCTION_EXPERIMENT_INSTRUCTION != null;
|
||||
const previousEnvValue = process.env.RECONSTRUCTION_EXPERIMENT_INSTRUCTION;
|
||||
@@ -83,23 +102,22 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti
|
||||
let reconstructionOnly = options.reconstructionOnly;
|
||||
const isMock = process.env.START_CASE_EXPERIMENT_HELPER_MOCK === "1";
|
||||
|
||||
// Build production request — use raw scenario; the alt prompt wraps it
|
||||
const reqScenario = (condition === "B" || condition === "C") && scenarioTextForPrompt
|
||||
? scenarioInput.scenario
|
||||
: (scenarioInput?.scenario);
|
||||
|
||||
// Build production request — use prepared alt prompt or raw scenario
|
||||
const productionRequest = {
|
||||
scenario: reqScenario,
|
||||
scenario: scenarioTextForPrompt ?? scenarioInput?.scenario,
|
||||
...(scenarioInput?.promptVersion !== undefined
|
||||
? { promptVersion: scenarioInput.promptVersion }
|
||||
: {}),
|
||||
};
|
||||
|
||||
// ── Condition B/C schema passthrough via dependencies (question-free importantUnknowns) ──
|
||||
// ── Condition B/C/D schema passthrough via dependencies ──
|
||||
let conditionSchema = null;
|
||||
if (condition === "B" || condition === "C") {
|
||||
const schemaModule = await import(PROJECT_ROOT + "/lib/reconstruction/schema-variant.js");
|
||||
conditionSchema = schemaModule.conditionBJsonSchema; // question-free schema — importantUnknowns has NO question field
|
||||
} else if (condition === "D") {
|
||||
// Freeform deconstruct — explicitly no JSON structure requirement
|
||||
conditionSchema = null;
|
||||
}
|
||||
|
||||
if (process.env.START_CASE_EXPERIMENT_PROVIDER === "openai") {
|
||||
@@ -119,6 +137,9 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti
|
||||
reconstructionOnly = true;
|
||||
}
|
||||
|
||||
// ── Condition D: freeform mode flag ────────────────
|
||||
const isFreeform = condition === "D";
|
||||
|
||||
if (!startCase && isMock) {
|
||||
// Deterministic mode: skip environment checks and use inline test double.
|
||||
startCase = async (body, dependencies) => {
|
||||
@@ -130,18 +151,30 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti
|
||||
if (shouldFail) {
|
||||
return { success: false, error: "deterministic mock failure", statusCode: 500 };
|
||||
}
|
||||
|
||||
// In freeform mode (Condition D), the model returns raw text rather than structured data.
|
||||
const reconstructionResult = isFreeform
|
||||
? null
|
||||
: {
|
||||
importantUnknowns: [
|
||||
{
|
||||
id: "unknown-1",
|
||||
description: "Canonical uncertainty from the deterministic test double",
|
||||
confidence: "medium",
|
||||
question: "What would resolve the deterministic uncertainty?",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// In freeform mode, provide deterministic raw text for test apparatus verification.
|
||||
const freeformText = isFreeform
|
||||
? "Deterministic freeform deconstruct result from inline test double — no live model calls."
|
||||
: null;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
reconstruction: {
|
||||
importantUnknowns: [
|
||||
{
|
||||
id: "unknown-1",
|
||||
description: "Canonical uncertainty from the deterministic test double",
|
||||
confidence: "medium",
|
||||
question: "What would resolve the deterministic uncertainty?",
|
||||
},
|
||||
],
|
||||
},
|
||||
reconstruction: reconstructionResult,
|
||||
freeformText,
|
||||
situationGraph: {
|
||||
nodes: [
|
||||
{
|
||||
@@ -204,6 +237,7 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti
|
||||
reconstructionProvider,
|
||||
reconstructionModelName,
|
||||
reconstructionOnly,
|
||||
...(isFreeform ? { freeform: true } : {}),
|
||||
};
|
||||
if (conditionSchema) {
|
||||
startCaseDeps.generationSchema = conditionSchema;
|
||||
@@ -243,6 +277,14 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti
|
||||
statusCode: result.statusCode ?? (result.success ? 200 : 500),
|
||||
};
|
||||
|
||||
// ── Condition D: freeform deconstruct output — capture without production JSON schema ──
|
||||
if (condition === "D") {
|
||||
const rawText = result.freeformText ?? result.reconstruction ?? null;
|
||||
output.conditionType = "deconstruct";
|
||||
output.freeformText = rawText;
|
||||
delete output.reconstruction;
|
||||
}
|
||||
|
||||
if (reconstructionOnly) {
|
||||
if (scenarioInput?.experimentId !== undefined) {
|
||||
output.experimentId = scenarioInput.experimentId;
|
||||
@@ -324,18 +366,41 @@ if (require.main === module) {
|
||||
experimentInstruction = process.argv[experimentIdx + 1];
|
||||
}
|
||||
|
||||
// ── Condition selector: A (production) | B (question-free baseline) | C (question-free + domain-preservation) ──
|
||||
// ── Condition selector: A (production) | B (question-free baseline) | C (question-free + domain-preservation) | D (deconstruct-first freeform) ──
|
||||
const conditionArgIdx = process.argv.indexOf("--condition");
|
||||
let condition = null;
|
||||
if (conditionArgIdx !== -1 && conditionArgIdx + 1 < process.argv.length) {
|
||||
condition = process.argv[conditionArgIdx + 1];
|
||||
}
|
||||
|
||||
// ── Validate condition value ──
|
||||
const validConditions = ["A", "B", "C", "D"];
|
||||
if (condition !== null && !validConditions.includes(condition)) {
|
||||
throw new Error(`Invalid condition: ${condition}. Must be one of: A, B, C, D`);
|
||||
}
|
||||
|
||||
// ── Treatment selector (D only) ──
|
||||
const treatmentArgIdx = process.argv.indexOf("--treatment");
|
||||
let treatment = null;
|
||||
if (treatmentArgIdx !== -1 && treatmentArgIdx + 1 < process.argv.length) {
|
||||
treatment = process.argv[treatmentArgIdx + 1];
|
||||
}
|
||||
|
||||
// ── Validate treatment value ──
|
||||
const validTreatments = ["no21"];
|
||||
if (treatment !== null && !validTreatments.includes(treatment)) {
|
||||
throw new Error(`Invalid treatment: ${treatment}. Must be one of: no21`);
|
||||
}
|
||||
if (treatment !== null && condition !== "D") {
|
||||
throw new Error(`Treatment "${treatment}" is only valid with --condition D, not --condition ${condition}`);
|
||||
}
|
||||
|
||||
const scenarioInput = readScenarioInput(process.argv);
|
||||
const reconstructionOnly = process.argv.includes("--reconstruction-only");
|
||||
const result = await runStartCaseExperiment(scenarioInput, experimentInstruction, {
|
||||
reconstructionOnly,
|
||||
condition,
|
||||
treatment: (condition === "D") ? treatment : undefined,
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
Reference in New Issue
Block a user