feat: classify observation relationships after comparability
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user