feat(confidence-engine): preserve initial reconstruction relationships
This commit is contained in:
@@ -9,6 +9,16 @@
|
||||
- **HEAD:** `37a9a12` — route design evolution provenance through archive index
|
||||
- **Working tree:** will be clean after v0.59a commit
|
||||
|
||||
## v0.5 initial reconstruction relationship contract
|
||||
|
||||
- v0.5 first-class initial reconstruction relationship contract implemented.
|
||||
- Reconstruction relationships use explicit `fromId`/`toId` references.
|
||||
- Declared relationships project deterministically to existing SituationGraph edges.
|
||||
- Invalid relationship references are skipped rather than inferred or repaired.
|
||||
- SituationGraph schema unchanged; v0.5 is the production default.
|
||||
- Targeted deterministic suite passed; live v0.5 semantic/reliability validation remains pending.
|
||||
- 502 structured-output reliability remains a separate unresolved issue.
|
||||
|
||||
## Current product architecture
|
||||
|
||||
Three distinct routes, not a single page:
|
||||
|
||||
+42
-9
@@ -22,6 +22,12 @@ export function buildInitialGraph(analysisData) {
|
||||
}
|
||||
|
||||
const nodeMap = new Map(); // label -> node
|
||||
const sourceIdToNodeId = new Map();
|
||||
|
||||
function mapSourceId(sourceId, node) {
|
||||
if (sourceId) sourceIdToNodeId.set(sourceId, node.id);
|
||||
return node;
|
||||
}
|
||||
|
||||
// ── Helper: register or get a node by label ────────────
|
||||
|
||||
@@ -97,14 +103,17 @@ export function buildInitialGraph(analysisData) {
|
||||
obs.confidence || "medium",
|
||||
);
|
||||
|
||||
if (obs.id) node.evidenceIds.push(obs.id);
|
||||
if (obs.id) {
|
||||
node.evidenceIds.push(obs.id);
|
||||
mapSourceId(obs.id, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actors as states/nodes
|
||||
if (reconstruction.actors) {
|
||||
for (const actor of reconstruction.actors) {
|
||||
ensureNode(
|
||||
mapSourceId(actor.id, ensureNode(
|
||||
actor.description || actor.label,
|
||||
"observation",
|
||||
"supported",
|
||||
@@ -112,13 +121,13 @@ export function buildInitialGraph(analysisData) {
|
||||
null,
|
||||
null,
|
||||
actor.confidence || "medium",
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (reconstruction.systemsOrObjects) {
|
||||
for (const sys of reconstruction.systemsOrObjects) {
|
||||
ensureNode(
|
||||
mapSourceId(sys.id, ensureNode(
|
||||
sys.description || sys.label,
|
||||
"metric",
|
||||
"known",
|
||||
@@ -126,7 +135,7 @@ export function buildInitialGraph(analysisData) {
|
||||
null,
|
||||
null,
|
||||
sys.confidence || "medium",
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +151,7 @@ export function buildInitialGraph(analysisData) {
|
||||
null,
|
||||
diff.confidence || "medium",
|
||||
);
|
||||
mapSourceId(diff.id, node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +167,7 @@ export function buildInitialGraph(analysisData) {
|
||||
null,
|
||||
c.confidence || "medium",
|
||||
);
|
||||
mapSourceId(c.id, node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +184,7 @@ export function buildInitialGraph(analysisData) {
|
||||
null,
|
||||
unk.confidence || "low",
|
||||
);
|
||||
mapSourceId(unk.id, node);
|
||||
unknownNodes.push(node);
|
||||
}
|
||||
}
|
||||
@@ -180,7 +192,7 @@ export function buildInitialGraph(analysisData) {
|
||||
// Plausible interpretations
|
||||
if (reconstruction.plausibleInterpretations) {
|
||||
for (const interp of reconstruction.plausibleInterpretations) {
|
||||
ensureNode(
|
||||
mapSourceId(interp.id, ensureNode(
|
||||
interp.description || interp.label,
|
||||
"assumption",
|
||||
"provisional",
|
||||
@@ -188,14 +200,14 @@ export function buildInitialGraph(analysisData) {
|
||||
null,
|
||||
null,
|
||||
interp.confidence || "low",
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Known transitions
|
||||
if (reconstruction.knownTransitions) {
|
||||
for (const trans of reconstruction.knownTransitions) {
|
||||
ensureNode(
|
||||
mapSourceId(trans.id, ensureNode(
|
||||
`${trans.entity}: ${trans.previousState} → ${trans.currentState}`,
|
||||
"transition",
|
||||
trans.explanationStatus === "confirmed" ? "known" : "provisional",
|
||||
@@ -204,7 +216,7 @@ export function buildInitialGraph(analysisData) {
|
||||
null,
|
||||
null,
|
||||
trans.confidence || "medium",
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +259,27 @@ export function buildInitialGraph(analysisData) {
|
||||
}
|
||||
}
|
||||
|
||||
const edgeIds = new Set(edges.map((edge) => edge.id));
|
||||
for (const relationship of reconstruction.relationships ?? []) {
|
||||
const fromNodeId = sourceIdToNodeId.get(relationship.fromId);
|
||||
const toNodeId = sourceIdToNodeId.get(relationship.toId);
|
||||
const id = `e-rel-${relationship.id}`;
|
||||
|
||||
if (!fromNodeId || !toNodeId || edgeIds.has(id)) continue;
|
||||
|
||||
edges.push(
|
||||
situationEdgeSchema.parse({
|
||||
id,
|
||||
fromNodeId,
|
||||
toNodeId,
|
||||
relationship: relationship.relationship,
|
||||
confidence: relationship.confidence,
|
||||
description: relationship.description,
|
||||
}),
|
||||
);
|
||||
edgeIds.add(id);
|
||||
}
|
||||
|
||||
return { nodes: nodeArr, edges };
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ const __dirname = dirname(__filename);
|
||||
const PROMPTS_DIR = join(__dirname, "../../prompts");
|
||||
|
||||
/** Available prompt versions */
|
||||
export const PROMPT_VERSIONS = ["v0.1", "v0.2", "v0.3", "v0.4"];
|
||||
export const PROMPT_VERSIONS = ["v0.1", "v0.2", "v0.3", "v0.4", "v0.5"];
|
||||
|
||||
/** Default prompt version (override via RECONSTRUCTION_PROMPT_VERSION env var) */
|
||||
const defaultVersionFromEnv = process.env.RECONSTRUCTION_PROMPT_VERSION;
|
||||
export const DEFAULT_PROMPT_VERSION =
|
||||
defaultVersionFromEnv && PROMPT_VERSIONS.includes(defaultVersionFromEnv)
|
||||
? defaultVersionFromEnv
|
||||
: "v0.4";
|
||||
: "v0.5";
|
||||
|
||||
/** Build a v0.1 (extraction-only) prompt inline for backward compatibility */
|
||||
function buildV1Prompt(scenario) {
|
||||
@@ -91,15 +91,29 @@ async function buildV4Prompt(scenario) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Load a versioned prompt from disk and substitute {{SCENARIO}} */
|
||||
async function buildV5Prompt(scenario) {
|
||||
try {
|
||||
const content = await fs.readFile(
|
||||
join(PROMPTS_DIR, "reconstruct-v0.5.md"),
|
||||
"utf-8",
|
||||
);
|
||||
return content.replace("{{SCENARIO}}", scenario);
|
||||
} catch {
|
||||
// Fall back to v0.4 prompt if v0.5 file is missing
|
||||
return buildV4Prompt(scenario);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an analysis prompt for the given version.
|
||||
* @param {string} scenario - The scenario text
|
||||
* @param {"v0.1" | "v0.2" | "v0.3" | "v0.4"} [version="v0.4"] - Prompt version
|
||||
* @param {"v0.1" | "v0.2" | "v0.3" | "v0.4" | "v0.5"} [version="v0.5"] - Prompt version
|
||||
* @param {object} [opts] - Optional experimental parameters
|
||||
* @param {string} [opts.experimentInstruction] - Bounded experimental instruction block appended to the base prompt (production prompt is never replaced)
|
||||
* @returns {Promise<{prompt: string, version: string}>}
|
||||
*/
|
||||
export async function buildPrompt(scenario, version = "v0.4", opts = {}) {
|
||||
export async function buildPrompt(scenario, version = "v0.5", opts = {}) {
|
||||
let prompt;
|
||||
switch (version) {
|
||||
case "v0.1":
|
||||
@@ -111,6 +125,9 @@ export async function buildPrompt(scenario, version = "v0.4", opts = {}) {
|
||||
case "v0.4":
|
||||
prompt = await buildV4Prompt(scenario);
|
||||
break;
|
||||
case "v0.5":
|
||||
prompt = await buildV5Prompt(scenario);
|
||||
break;
|
||||
default: // v0.3
|
||||
prompt = await buildV3Prompt(scenario);
|
||||
break;
|
||||
|
||||
@@ -126,6 +126,24 @@ const evidenceRecordSchema = z.object({
|
||||
importance: importanceEnum,
|
||||
});
|
||||
|
||||
const reconstructionRelationshipSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
fromId: z.string().min(1),
|
||||
toId: z.string().min(1),
|
||||
relationship: z.enum([
|
||||
"supports",
|
||||
"weakens",
|
||||
"contradicts",
|
||||
"depends_on",
|
||||
"causes",
|
||||
"may_cause",
|
||||
"compares_with",
|
||||
"other",
|
||||
]),
|
||||
description: z.string().min(1),
|
||||
confidence: confidenceEnum,
|
||||
});
|
||||
|
||||
const reconstructionSchemaV2 = z.object({
|
||||
summary: z.string().min(1),
|
||||
actors: z.array(itemSchemaV1),
|
||||
@@ -159,6 +177,7 @@ const reconstructionSchemaV2 = z.object({
|
||||
confidence: confidenceEnum,
|
||||
}),
|
||||
),
|
||||
relationships: z.array(reconstructionRelationshipSchema).optional().default([]),
|
||||
});
|
||||
|
||||
const inputClassificationSchema = z.object({
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
You are a neutral analyst performing evidence-based situation reconstruction.
|
||||
|
||||
## Rules
|
||||
|
||||
1. Do NOT invent facts, context or causes. Only include information present in the scenario. For decomposition structure — splitting a concept into child categories — each child's distinguishing semantic content must be directly supported by meaning supplied by the user in their text (semantic-equivalent paraphrase is permitted; literal word-for-word matching is not required). A decomposition child is NOT permitted if producing it requires adding a new actor, category, segment, mechanism, cause, process, subtype, condition, system behaviour, operational distinction, or equivalent semantic proposition that the user did not supply. The following are NOT sufficient to justify creating new decomposition structure: plausible, likely, domain-typical, possibly implied, clearly implied, worth investigating, could be relevant, might explain. Those standards may inform a provisional interpretation but never decomposition.
|
||||
|
||||
2. First determine what kind of input has been supplied. Use only these classification types:
|
||||
observed_problem, unexplained_change, contradiction, decision_request, causal_claim,
|
||||
reported_claim, fault_report, ambiguous_statement, question, desired_outcome,
|
||||
insufficient_context, other
|
||||
|
||||
3. Choose reasoning modes from:
|
||||
establish_baseline, identify_difference, reconstruct_transition, decompose_aggregate,
|
||||
validate_measurement, validate_claim, investigate_contradiction, clarify_meaning,
|
||||
decision_support, fault_investigation, identify_missing_information, test_possible_explanations, other
|
||||
|
||||
4. Look for anchors: actor, system or object, expected outcome, observed outcome,
|
||||
previous state, current state, difference between groups, change over time, measurement,
|
||||
evidence source, proposed action.
|
||||
|
||||
5. Identify meaningful differences (e.g., some succeed while others fail; revenue rises while cash falls).
|
||||
|
||||
6. Keep multiple plausible interpretations separate where the evidence does not distinguish them. Model-generated possible interpretations are permitted ONLY when they satisfy all of the following conditions: (a) supported by supplied evidence; (b) clearly represented in plausibleInterpretations; (c) explicitly marked as provisional; (d) their supporting evidence is identified; (e) NOT presented as observation, supplied unknown, transition, relationship, or established cause. A plausible interpretation must NEVER be smuggled into decomposition structure. Do NOT generate plausible interpretations merely to fill a list — return an empty array [] if the evidence does not support useful, distinct interpretations.
|
||||
|
||||
7. When the user explicitly names multiple distinct possible explanations, causes, constraints, or dependencies for the situation, preserve those user-stated alternatives as separate importantUnknowns when they can sensibly be investigated independently. Do not collapse them into one "which factor", "relative contribution", or equivalent umbrella unknown. Do not turn a user-stated possibility into an asserted plausible interpretation — preserve its uncertain status. Only split concepts when the user has presented materially distinct dimensions that each warrant independent investigation.
|
||||
|
||||
8. Distinguish: what was said / what it may mean / why it may have been said.
|
||||
|
||||
9. If input is too ambiguous or contains no useful operational anchors, say so and ask for the single piece of context that would best distinguish plausible interpretations.
|
||||
|
||||
10. Do not split concepts merely to increase the number of unknowns — only separate when the user has presented materially distinct dimensions worth independent investigation.
|
||||
|
||||
## Preserve supplied relationships
|
||||
|
||||
Reconstruction must preserve not only supplied semantic units but also relationships the user supplies between them. Supplied semantic units remain represented in the existing reconstruction arrays. Supplied relationships between those units MUST be represented in `reconstruction.relationships`, using `id`, `fromId`, `toId`, `relationship`, `description`, and `confidence`.
|
||||
|
||||
Each relationship `fromId` and `toId` MUST reference IDs of semantic units already present elsewhere in `reconstruction`. Do NOT invent a relationship endpoint merely to complete a relationship. Preserve unresolved relationships as unresolved or provisional when the user leaves them unresolved. Keep useful relationship prose in `summary` where appropriate, but summary prose is not the authoritative representation of graph-critical relationships. Do not introduce an action recommendation or steering.
|
||||
|
||||
For example, "I am deciding whether to spend about £120,000 on automated quality inspection now or wait until we understand whether there is actually a quality problem" contains an unresolved decision dependency. A valid reconstruction can represent an unknown such as whether the complaint increase reflects a quality problem automated inspection could address (`u1`) and an unknown such as whether spending £120,000 on automated inspection now is appropriate (`u2`), then declare:
|
||||
|
||||
```json
|
||||
{"id":"r1","fromId":"u2","toId":"u1","relationship":"depends_on","description":"Whether automated inspection is appropriate depends on whether the complaint increase reflects a quality problem that inspection could address.","confidence":"high"}
|
||||
```
|
||||
|
||||
This example illustrates the contract; do not reproduce its wording unless the supplied scenario supports it.
|
||||
|
||||
## Explicit stop boundary for decomposition
|
||||
|
||||
Once the supplied meaning of an observation, uncertainty, relationship or transition has been faithfully represented, STOP. Do not recursively decompose unless the supplied user material itself contains further distinct semantic structure. For each candidate decomposition child: identify the supplied meaning that supports its distinguishing content — if that content adds no new semantic proposition beyond what the user supplied, allow it; if it requires adding new semantic content, stop and do not create the child.
|
||||
|
||||
## Normalisation and rate reasoning (apply whenever applicable)
|
||||
|
||||
When the scenario mentions counts, totals, frequencies, or volumes alongside changes in scale, volume, exposure, time, population, or output:
|
||||
|
||||
- ALWAYS consider whether a denominator or exposure metric is needed to normalise the count.
|
||||
- Distinguish between absolute count (total number observed) and rate (count per unit of exposure).
|
||||
- Two metrics rising at similar percentages does NOT imply that quality, performance, or safety has worsened — production growth may outpace complaint growth, meaning the per-unit rate could be stable or even improved.
|
||||
- Identify the possible denominator explicitly (e.g., "per unit produced", "per customer served", "per hour of operation").
|
||||
- State clearly: "The absolute count changed by X%, but without knowing the denominator we cannot determine whether the rate per unit has worsened, stayed stable, or improved."
|
||||
- Avoid treating correlation between two rising counts as evidence of a causal relationship.
|
||||
|
||||
## Interpretation discipline
|
||||
|
||||
- Do NOT generate plausible interpretations merely to fill a list. If the evidence does not support useful, distinct interpretations, return an empty array [].
|
||||
- Only include an interpretation when there is specific evidence that makes it distinguishable from alternatives and worth evaluating further.
|
||||
- Rank all reconstruction details by importance:
|
||||
- critical: essential to resolving the situation; without it conclusions cannot be drawn
|
||||
- important: materially affects understanding of the situation
|
||||
- supporting: adds context but not critical
|
||||
- incidental: minor detail, unlikely to affect conclusions
|
||||
|
||||
## Next question discipline
|
||||
|
||||
- Generate exactly ONE next question. Do NOT combine multiple questions.
|
||||
- The first and only question should target the single most useful missing comparison or data point.
|
||||
- Prefer narrow, specific questions over broad compound questions.
|
||||
- When counts have changed alongside scale/exposure, the highest-value question typically targets the rate-per-unit or equivalent normalised metric.
|
||||
- Do NOT generate speculative interpretations merely to justify a question.
|
||||
|
||||
## Confidence scale
|
||||
|
||||
- low — weak evidence, speculation, or missing information
|
||||
- medium — reasonable inference from available evidence
|
||||
- high — strong evidence, direct observation, or confirmed fact
|
||||
|
||||
## Importance scale (evidence records)
|
||||
|
||||
- incidental — minor detail, unlikely to affect conclusions
|
||||
- supporting — adds context but not critical
|
||||
- important — materially affects understanding of the situation
|
||||
- critical — essential to resolving the situation; without it conclusions cannot be drawn
|
||||
|
||||
## Expected information value (next question)
|
||||
|
||||
- low — marginally useful even if answered
|
||||
- medium — meaningfully clarifies the situation
|
||||
- high — would significantly distinguish between plausible explanations or fill a gap in understanding
|
||||
|
||||
## Next question selection criteria
|
||||
|
||||
Prefer questions that:
|
||||
- clarify a major difference
|
||||
- establish a baseline
|
||||
- explain an important transition
|
||||
- test an unsupported claim
|
||||
- distinguish between plausible explanations
|
||||
- request measurable evidence
|
||||
- identify who or what is affected
|
||||
- establish timing
|
||||
|
||||
Avoid questions that:
|
||||
- have already been answered
|
||||
- assume a cause
|
||||
- jump to a solution
|
||||
- ask about motive before the observable situation is understood
|
||||
- focus on incidental wording
|
||||
- are too broad to produce useful information
|
||||
- combine many unrelated questions
|
||||
|
||||
## Output format — return this exact JSON structure
|
||||
|
||||
Return a JSON object with exactly these four top-level keys (use **camelCase**):
|
||||
|
||||
```json
|
||||
{
|
||||
"inputClassification": {
|
||||
"primaryType": "<one of: observed_problem, unexplained_change, contradiction, decision_request, causal_claim, reported_claim, fault_report, ambiguous_statement, question, desired_outcome, insufficient_context, other>",
|
||||
"secondaryTypes": ["<optional additional types from the same list>"],
|
||||
"reasoningModes": ["<one or more of: establish_baseline, identify_difference, reconstruct_transition, decompose_aggregate, validate_measurement, validate_claim, investigate_contradiction, clarify_meaning, decision_support, fault_investigation, identify_missing_information, test_possible_explanations, other>"],
|
||||
"classificationReason": "<brief explanation of why you chose the primary type>",
|
||||
"confidence": "<low | medium | high>"
|
||||
},
|
||||
"reconstruction": {
|
||||
"summary": "<one-sentence overview of the situation>",
|
||||
"actors": [{"id": "<any unique string>", "description": "...", "confidence": "<low|medium|high>"}],
|
||||
"systemsOrObjects": [{"id": "<any unique string>", "description": "...", "confidence": "<low|medium|high>"}],
|
||||
"expectedStates": [{"id": "...", "description": "...", "confidence": "<low|medium|high>"}],
|
||||
"observedStates": [{"id": "...", "description": "...", "confidence": "<low|medium|high>"}],
|
||||
"differences": [{"id": "...", "description": "...", "confidence": "<low|medium|high>"}],
|
||||
"knownTransitions": [{"id": "...", "description": "...", "confidence": "<low|medium|high>", "entity": "...", "previousState": "...", "currentState": "...", "explanationStatus": "..."}],
|
||||
"unexplainedTransitions": [{"id": "...", "description": "...", "confidence": "<low|medium|high>", "entity": "...", "previousState": "...", "currentState": "..."}],
|
||||
"contradictions": [{"id": "...", "description": "...", "confidence": "<low|medium|high>"}],
|
||||
"importantUnknowns": [{"id": "...", "description": "...", "confidence": "<low|medium|high>"}],
|
||||
"plausibleInterpretations": [{"id": "...", "description": "...", "supportingEvidenceIds": ["<ids that support this interpretation>"], "assumptionsRequired": [], "confidence": "<low|medium|high>"}],
|
||||
"relationships": [{"id": "...", "fromId": "<an ID from a supplied reconstruction item>", "toId": "<an ID from a supplied reconstruction item>", "relationship": "<supports | weakens | contradicts | depends_on | causes | may_cause | compares_with | other>", "description": "...", "confidence": "<low|medium|high>"}]
|
||||
},
|
||||
"evidence": [
|
||||
{"id": "<any unique string>", "description": "...", "evidenceType": "<direct_observation | reported_statement | interpretation | assumption | inferred_relationship>", "source": "<optional — who/where this came from>", "attribution": null, "confidence": "<low | medium | high>", "importance": "<incidental | supporting | important | critical>"}
|
||||
],
|
||||
"nextQuestion": {"id": "<any unique string>", "question": "<one precise question>", "targets": ["<what this question targets — e.g. 'actor', 'system', 'expectedOutcome'>"], "reason": "<why answering this is important>", "expectedInformationValue": "<low | medium | high>", "reasoningMode": "<optional reasoning mode from the list above>"}
|
||||
}
|
||||
```
|
||||
|
||||
`relationships` must be an array and may be empty (`[]`).
|
||||
|
||||
CRITICAL RULES for JSON output:
|
||||
1. Use **exactly** the key names shown above (camelCase, no snake_case).
|
||||
2. The four top-level keys must be: `inputClassification`, `reconstruction`, `evidence`, `nextQuestion`.
|
||||
3. Do NOT invent new top-level keys (no `anchors`, `confidence` at top level, `meaningful_differences`, etc.).
|
||||
4. Keep `actors`, `systemsOrObjects`, `expectedStates`, `observedStates`, `differences`, `contradictions`, `importantUnknowns`, and `relationships` as arrays even if empty: [].
|
||||
5. Keep `plausibleInterpretations` as an array (can be []), same for `knownTransitions` and `unexplainedTransitions`.
|
||||
6. Each object in reconstruction arrays must have at least `id`, `description`, `confidence`; each relationship must additionally have `fromId`, `toId`, and `relationship`.
|
||||
7. **evidenceType**: classify each evidence item clearly as either a direct observation, a reported statement, an interpretation, an assumption, or an inferred relationship. Do not treat raw counts as proof of causal relationships — they may be inferred relationships only when supported by explicit reasoning about denominators or rates.
|
||||
|
||||
Scenario:
|
||||
{{SCENARIO}}
|
||||
|
||||
Return ONLY the JSON object starting with { and ending with }. Do NOT include any text before the opening brace or after the closing brace. Do NOT wrap in markdown backticks.
|
||||
@@ -216,6 +216,98 @@ describe("buildInitialGraph", () => {
|
||||
expect(depEdges.length).toBe(2); // Two unknown nodes
|
||||
});
|
||||
|
||||
it("projects a supplied dependency between reconstruction source IDs", () => {
|
||||
const reconstruction = {
|
||||
...makeReconstructionFixture(),
|
||||
importantUnknowns: [
|
||||
{ id: "u_problem", description: "Whether a quality problem exists", confidence: "high" },
|
||||
{ id: "u_intervention", description: "Whether inspection is appropriate", confidence: "high" },
|
||||
],
|
||||
relationships: [{
|
||||
id: "r-dependency",
|
||||
fromId: "u_intervention",
|
||||
toId: "u_problem",
|
||||
relationship: "depends_on",
|
||||
description: "Inspection appropriateness depends on the quality problem.",
|
||||
confidence: "high",
|
||||
}],
|
||||
};
|
||||
|
||||
const result = buildInitialGraph({ reconstruction, evidence: [] });
|
||||
const intervention = result.nodes.find((node) => node.label === "Whether inspection is appropriate");
|
||||
const problem = result.nodes.find((node) => node.label === "Whether a quality problem exists");
|
||||
|
||||
expect(result.edges).toContainEqual(expect.objectContaining({
|
||||
id: "e-rel-r-dependency",
|
||||
fromNodeId: intervention.id,
|
||||
toNodeId: problem.id,
|
||||
relationship: "depends_on",
|
||||
}));
|
||||
});
|
||||
|
||||
it("preserves a supplied compares_with relationship type", () => {
|
||||
const reconstruction = {
|
||||
...makeReconstructionFixture(),
|
||||
observedStates: [
|
||||
{ id: "u_a", description: "Complaint rate before", confidence: "high" },
|
||||
{ id: "u_b", description: "Complaint rate after", confidence: "high" },
|
||||
],
|
||||
relationships: [{
|
||||
id: "r-compare",
|
||||
fromId: "u_a",
|
||||
toId: "u_b",
|
||||
relationship: "compares_with",
|
||||
description: "Compare complaint rates before and after.",
|
||||
confidence: "medium",
|
||||
}],
|
||||
};
|
||||
|
||||
const result = buildInitialGraph({ reconstruction, evidence: [] });
|
||||
expect(result.edges).toContainEqual(expect.objectContaining({
|
||||
id: "e-rel-r-compare",
|
||||
relationship: "compares_with",
|
||||
}));
|
||||
});
|
||||
|
||||
it("skips relationships with invalid source IDs without creating substitute nodes", () => {
|
||||
const reconstruction = {
|
||||
...makeReconstructionFixture(),
|
||||
relationships: [{
|
||||
id: "r-missing",
|
||||
fromId: "missing-source",
|
||||
toId: "obs-1",
|
||||
relationship: "depends_on",
|
||||
description: "This must be skipped.",
|
||||
confidence: "low",
|
||||
}],
|
||||
};
|
||||
|
||||
expect(() => buildInitialGraph({ reconstruction, evidence: [] })).not.toThrow();
|
||||
const result = buildInitialGraph({ reconstruction, evidence: [] });
|
||||
expect(result.edges.find((edge) => edge.id === "e-rel-r-missing")).toBeUndefined();
|
||||
expect(result.nodes.find((node) => node.label === "missing-source")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not infer direct dependencies between related descriptions without relationships", () => {
|
||||
const reconstruction = {
|
||||
...makeReconstructionFixture(),
|
||||
importantUnknowns: [
|
||||
{ id: "u_problem", description: "Whether a quality problem exists", confidence: "high" },
|
||||
{ id: "u_intervention", description: "Whether inspection is appropriate", confidence: "high" },
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildInitialGraph({ reconstruction, evidence: [] });
|
||||
const intervention = result.nodes.find((node) => node.label === "Whether inspection is appropriate");
|
||||
const problem = result.nodes.find((node) => node.label === "Whether a quality problem exists");
|
||||
|
||||
expect(result.edges).not.toContainEqual(expect.objectContaining({
|
||||
fromNodeId: intervention.id,
|
||||
toNodeId: problem.id,
|
||||
relationship: "depends_on",
|
||||
}));
|
||||
});
|
||||
|
||||
it("handles empty observedStates gracefully", () => {
|
||||
const reconstruction = {
|
||||
...makeReconstructionFixture(),
|
||||
|
||||
+133
-10
@@ -25,9 +25,9 @@ describe("v0.3 prompt", () => {
|
||||
expect(PROMPT_VERSIONS).toContain("v0.3");
|
||||
});
|
||||
|
||||
it("DEFAULT_PROMPT_VERSION was v0.3 on earlier branches (now v0.4)", () => {
|
||||
// This test documents that the old default was v0.3; the new default is v0.4
|
||||
expect(["v0.3", "v0.4"]).toContain(DEFAULT_PROMPT_VERSION);
|
||||
it("DEFAULT_PROMPT_VERSION was v0.3 on earlier branches (now v0.5)", () => {
|
||||
// This test documents that the old default was v0.3; the new default is v0.5
|
||||
expect(["v0.3", "v0.4", "v0.5"]).toContain(DEFAULT_PROMPT_VERSION);
|
||||
});
|
||||
|
||||
it("v0.2 remains available in PROMPT_VERSIONS", () => {
|
||||
@@ -201,6 +201,45 @@ describe("v0.2 backward compatibility", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("v0.5 prompt", () => {
|
||||
it("is the production default", async () => {
|
||||
const result = await buildPrompt("Default prompt scenario");
|
||||
|
||||
expect(DEFAULT_PROMPT_VERSION).toBe("v0.5");
|
||||
expect(PROMPT_VERSIONS).toContain("v0.5");
|
||||
expect(result.version).toBe("v0.5");
|
||||
expect(result.prompt).toContain("Default prompt scenario");
|
||||
});
|
||||
|
||||
it("keeps explicit v0.4 unchanged", async () => {
|
||||
const result = await buildPrompt("Explicit v0.4 scenario", "v0.4");
|
||||
|
||||
expect(result.version).toBe("v0.4");
|
||||
expect(result.prompt).toContain(
|
||||
"with no dedicated schema field MUST be preserved explicitly in summary",
|
||||
);
|
||||
});
|
||||
|
||||
it("loads the v0.5 relationship contract", async () => {
|
||||
const result = await buildPrompt("Explicit v0.5 scenario", "v0.5");
|
||||
|
||||
expect(result.version).toBe("v0.5");
|
||||
expect(result.prompt).toContain("reconstruction.relationships");
|
||||
expect(result.prompt).toContain("fromId");
|
||||
expect(result.prompt).toContain("toId");
|
||||
expect(result.prompt).toContain("MUST reference IDs of semantic units");
|
||||
});
|
||||
|
||||
it("retains the provenance stop boundary and interpretation separation", async () => {
|
||||
const { prompt } = await buildPrompt("Contract retention scenario", "v0.5");
|
||||
|
||||
expect(prompt).toContain("Explicit stop boundary for decomposition");
|
||||
expect(prompt).toContain("Once the supplied meaning");
|
||||
expect(prompt).toContain("Interpretation discipline");
|
||||
expect(prompt).toContain("plausibleInterpretations");
|
||||
});
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Schema validation tests for v0.3-shaped output
|
||||
// ──────────────────────────────────────────────
|
||||
@@ -344,6 +383,90 @@ describe("v0.3 schema validation", () => {
|
||||
|
||||
const result = reconstructionV2Schema.safeParse(input);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.reconstruction.relationships).toEqual([]);
|
||||
});
|
||||
|
||||
it("parses a valid reconstruction relationship", () => {
|
||||
const input = {
|
||||
inputClassification: {
|
||||
primaryType: "decision_request",
|
||||
classificationReason: "test",
|
||||
confidence: "medium",
|
||||
},
|
||||
reconstruction: {
|
||||
summary: "Decision depends on an unresolved quality question.",
|
||||
actors: [],
|
||||
systemsOrObjects: [],
|
||||
expectedStates: [],
|
||||
observedStates: [],
|
||||
differences: [],
|
||||
knownTransitions: [],
|
||||
unexplainedTransitions: [],
|
||||
contradictions: [],
|
||||
importantUnknowns: [
|
||||
{ id: "u1", description: "Whether a quality problem exists", confidence: "high" },
|
||||
{ id: "u2", description: "Whether inspection is appropriate", confidence: "high" },
|
||||
],
|
||||
plausibleInterpretations: [],
|
||||
relationships: [
|
||||
{
|
||||
id: "r1",
|
||||
fromId: "u2",
|
||||
toId: "u1",
|
||||
relationship: "depends_on",
|
||||
description: "Inspection appropriateness depends on the quality problem.",
|
||||
confidence: "high",
|
||||
},
|
||||
],
|
||||
},
|
||||
evidence: [],
|
||||
nextQuestion: {
|
||||
id: "q1",
|
||||
question: "What evidence establishes the quality problem?",
|
||||
targets: ["u1"],
|
||||
reason: "need evidence",
|
||||
expectedInformationValue: "high",
|
||||
},
|
||||
};
|
||||
|
||||
const result = reconstructionV2Schema.safeParse(input);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.reconstruction.relationships).toEqual(input.reconstruction.relationships);
|
||||
});
|
||||
|
||||
it("rejects invalid reconstruction relationship enums and missing endpoints", () => {
|
||||
const validRelationship = {
|
||||
id: "r1",
|
||||
fromId: "u2",
|
||||
toId: "u1",
|
||||
relationship: "depends_on",
|
||||
description: "u2 depends on u1",
|
||||
confidence: "high",
|
||||
};
|
||||
const base = {
|
||||
inputClassification: { primaryType: "other", classificationReason: "test", confidence: "low" },
|
||||
reconstruction: {
|
||||
summary: "test",
|
||||
actors: [], systemsOrObjects: [], expectedStates: [], observedStates: [], differences: [],
|
||||
knownTransitions: [], unexplainedTransitions: [], contradictions: [], importantUnknowns: [],
|
||||
plausibleInterpretations: [], relationships: [validRelationship],
|
||||
},
|
||||
evidence: [],
|
||||
nextQuestion: { id: "q1", question: "What next?", targets: [], reason: "test", expectedInformationValue: "low" },
|
||||
};
|
||||
|
||||
expect(reconstructionV2Schema.safeParse({
|
||||
...base,
|
||||
reconstruction: { ...base.reconstruction, relationships: [{ ...validRelationship, relationship: "updates" }] },
|
||||
}).success).toBe(false);
|
||||
expect(reconstructionV2Schema.safeParse({
|
||||
...base,
|
||||
reconstruction: { ...base.reconstruction, relationships: [{ ...validRelationship, fromId: undefined }] },
|
||||
}).success).toBe(false);
|
||||
expect(reconstructionV2Schema.safeParse({
|
||||
...base,
|
||||
reconstruction: { ...base.reconstruction, relationships: [{ ...validRelationship, toId: undefined }] },
|
||||
}).success).toBe(false);
|
||||
});
|
||||
|
||||
it("validates evidence distinguishing direct_observation from inferred_relationship", () => {
|
||||
@@ -676,8 +799,8 @@ describe("target scenario fixture validation", () => {
|
||||
|
||||
describe("diagnostics prompt version", () => {
|
||||
it("DEFAULT_PROMPT_VERSION is exported correctly", () => {
|
||||
// The current default is v0.4 (was v0.3)
|
||||
expect(["v0.3", "v0.4"]).toContain(DEFAULT_PROMPT_VERSION);
|
||||
// The current default is v0.5 (was v0.3)
|
||||
expect(["v0.3", "v0.4", "v0.5"]).toContain(DEFAULT_PROMPT_VERSION);
|
||||
});
|
||||
|
||||
it("PROMPT_VERSIONS includes both v0.2 and v0.3", () => {
|
||||
@@ -801,8 +924,8 @@ describe("Finding disposition toggle — state machine", () => {
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
describe("v0.4 prompt", () => {
|
||||
it("DEFAULT_PROMPT_VERSION is v0.4 on this branch", () => {
|
||||
expect(DEFAULT_PROMPT_VERSION).toBe("v0.4");
|
||||
it("DEFAULT_PROMPT_VERSION is v0.5 on this branch", () => {
|
||||
expect(DEFAULT_PROMPT_VERSION).toBe("v0.5");
|
||||
});
|
||||
|
||||
it("v0.4 is in PROMPT_VERSIONS", () => {
|
||||
@@ -879,12 +1002,12 @@ describe("v0.4 prompt", () => {
|
||||
expect(result.prompt).toMatch(/exactly.*one.*question|Do NOT combine/i);
|
||||
});
|
||||
|
||||
it("default buildPrompt (no version arg) resolves to v0.4", async () => {
|
||||
it("default buildPrompt (no version arg) resolves to v0.5", async () => {
|
||||
const result = await buildPrompt("Default version test");
|
||||
expect(result.version).toBe(DEFAULT_PROMPT_VERSION);
|
||||
expect(DEFAULT_PROMPT_VERSION).toBe("v0.4");
|
||||
expect(DEFAULT_PROMPT_VERSION).toBe("v0.5");
|
||||
// Should not contain v0.3 schema-specific differences array (it does have it)
|
||||
// but the prompt should be from the v0.4 file which has the stop boundary text
|
||||
// but the prompt should be from the v0.5 file which has the stop boundary text
|
||||
expect(result.prompt.toLowerCase()).toMatch(/stop|explicit.*stop/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user