feat: classify observation relationships after comparability

This commit is contained in:
2026-08-02 16:40:24 +01:00
parent 0c7558d31f
commit c97f5f7303
5 changed files with 316 additions and 50 deletions
+177 -1
View File
@@ -210,6 +210,139 @@ export function assessComparability(graph) {
};
}
function extractObservationConcepts(profile) {
const concepts = new Set();
const text = profile.normalised;
const conceptPatterns = [
["sales", /\bsales\b/],
["revenue", /\brevenue\b/],
["cash", /\bcash\b/],
["complaints", /\bcomplaints?\b/],
["production", /\bproduction\b/],
["delivery_time", /\bdelivery time\b|\baverage delivery time\b/],
["cancellations", /\bcancellations?\b/],
["satisfaction", /\bsatisfaction\b/],
["temperature", /\btemperature\b/],
["ice", /\bice\b/],
["traffic", /\btraffic\b/],
["defects", /\bdefects?\b/],
["quality", /\bquality\b/],
["staffing", /\bstaff(ing)?\b/],
["availability", /\bavailable|availability|unavailable\b/],
["service", /\bservice\b/],
];
for (const [name, pattern] of conceptPatterns) {
if (pattern.test(text)) concepts.add(name);
}
return [...concepts];
}
function extractObservationDirection(profile) {
const text = profile.normalised;
if (/\bunavailable\b/.test(text)) return "unavailable";
if (/\b(increase|increased|rose|up|doubled)\b/.test(text)) return "up";
if (/\b(decrease|decreased|fell|down|halved)\b/.test(text)) return "down";
if (/\b(remained unchanged|unchanged|same)\b/.test(text)) return "flat";
if (/\bavailable\b/.test(text)) return "available";
if (/\bmelted\b/.test(text)) return "melted";
return "unknown";
}
export function classifyObservationRelationship(graph) {
const observations = collectObservationNodes(graph);
const profiles = observations.map((node) =>
analyseObservationText(`${node.label} ${node.description}`),
);
if (profiles.length < 2) {
return {
relationshipStatus: "insufficient_information",
reason:
"Fewer than two supported observations are available for comparison.",
contradictionReasoningAllowed: false,
questionRequired: false,
questionSuppressedReason:
"Not enough observations to classify a relationship.",
};
}
if (
profiles.every((profile) => profile.normalised === profiles[0].normalised)
) {
return {
relationshipStatus: "duplicate",
reason: "The observations repeat the same measurement and direction.",
contradictionReasoningAllowed: false,
questionRequired: false,
questionSuppressedReason:
"Duplicate observations do not justify a follow-up question.",
};
}
const conceptSets = profiles.map((profile) =>
extractObservationConcepts(profile),
);
const sharedConcepts = conceptSets.reduce((shared, concepts, index) => {
if (index === 0) return new Set(concepts);
return new Set(concepts.filter((concept) => shared.has(concept)));
}, new Set());
const directions = profiles.map((profile) =>
extractObservationDirection(profile),
);
if (
sharedConcepts.size > 0 &&
directions.includes("available") &&
directions.includes("unavailable")
) {
return {
relationshipStatus: "contradictory",
reason:
"The observations assert mutually incompatible states about the same subject.",
contradictionReasoningAllowed: true,
questionRequired: true,
};
}
if (
sharedConcepts.size > 0 &&
directions.every((direction) => direction !== "unknown")
) {
return {
relationshipStatus: "potentially_related",
reason:
"The observations concern the same subject but do not assert a direct contradiction.",
contradictionReasoningAllowed: false,
questionRequired: true,
};
}
if (
sharedConcepts.size === 0 &&
directions.every((direction) => direction !== "unknown")
) {
return {
relationshipStatus: "compatible",
reason:
"The observations can coexist without asserting incompatible states about the same subject.",
contradictionReasoningAllowed: false,
questionRequired: false,
questionSuppressedReason:
"Compatible observations do not justify a contradiction investigation.",
};
}
return {
relationshipStatus: "insufficient_information",
reason:
"There is not enough structure to classify the relationship safely.",
contradictionReasoningAllowed: false,
questionRequired: true,
};
}
function buildComparabilityQuestion(graph, assessment) {
const centralText = normaliseText(graph?.centralStatement || "");
const mentionsPeriod =
@@ -258,6 +391,46 @@ export function formulateTieResolutionQuestion({ graph }) {
comparabilityReason: comparability.reason,
contradictionReasoningAllowed:
comparability.contradictionReasoningAllowed,
relationshipStatus: "insufficient_information",
relationshipReason:
"Relationship classification is deferred until comparability is established.",
questionRequired: true,
};
}
const relationship = classifyObservationRelationship(graph);
if (!relationship.questionRequired) {
return {
question: null,
reason: relationship.reason,
strategy: null,
investigationStrategy: null,
selectionStatus: "ambiguous",
comparabilityStatus: comparability.comparabilityStatus,
comparabilityReason: comparability.reason,
relationshipStatus: relationship.relationshipStatus,
relationshipReason: relationship.reason,
contradictionReasoningAllowed: relationship.contradictionReasoningAllowed,
questionRequired: relationship.questionRequired,
questionSuppressedReason: relationship.questionSuppressedReason,
};
}
if (relationship.relationshipStatus === "potentially_related") {
return {
question:
"What connection, if any, should we check between these observations?",
reason:
"Formulated as a neutral relationship question because the observations may be related without being contradictory.",
strategy: null,
investigationStrategy: null,
selectionStatus: "ambiguous",
comparabilityStatus: comparability.comparabilityStatus,
comparabilityReason: comparability.reason,
relationshipStatus: relationship.relationshipStatus,
relationshipReason: relationship.reason,
contradictionReasoningAllowed: relationship.contradictionReasoningAllowed,
questionRequired: relationship.questionRequired,
};
}
@@ -278,7 +451,10 @@ export function formulateTieResolutionQuestion({ graph }) {
selectionStatus: "ambiguous",
comparabilityStatus: comparability.comparabilityStatus,
comparabilityReason: comparability.reason,
contradictionReasoningAllowed: comparability.contradictionReasoningAllowed,
relationshipStatus: relationship.relationshipStatus,
relationshipReason: relationship.reason,
contradictionReasoningAllowed: relationship.contradictionReasoningAllowed,
questionRequired: relationship.questionRequired,
};
}