experiment: improve semantic graph projection
Experiment 13 — Semantic Facilitator Translation - Classify nodes by semantic role (observation, question, explanation, scaffolding, relationship) rather than graph kind. Scaffolding suppressed entirely before section routing. - Three-tier filtering: scaffolding patterns > internal vocabulary > technical summary patterns. Prevents structural noise from contaminating user-facing sections. - Deduplicate by normalised text — merge duplicate observations expressing the same finding. - Route resolved unknowns and assumptions to known section with epistemic labels instead of treating them as unresolved questions. - Prefer concrete observations (numbers, change language, temporal refs) over abstract labels in ranking. - Closed Experiment 12 as confirmed. Added Experiment 13 documentation. - Updated UX guidelines with Semantic Projection principles. - 37 tests: filtering, classification, deduplication, ranking, framing, mock data integration, edge cases.
This commit is contained in:
@@ -11,6 +11,11 @@
|
||||
* 3. Possible explanations — assumptions and tentative causal claims
|
||||
* 4. Quiet reasoning summary — secondary counts from the same graph
|
||||
*
|
||||
* Key design: this adapter classifies nodes by *semantic role* rather than
|
||||
* simply projecting graph kinds. Internal graph concepts (metrics, systems,
|
||||
* scaffolding, technical summaries) are suppressed from the user-facing view.
|
||||
* Translation quality matters more than layout completeness.
|
||||
*
|
||||
* All filtering, deduplication and ranking is deterministic and uses only
|
||||
* existing graph fields. No new backend data or API contracts are required.
|
||||
*/
|
||||
@@ -59,6 +64,38 @@ const TECHNICAL_SUMMARY_PATTERNS = [
|
||||
/\b(?:node|edge|unknown|state)\s+count/i,
|
||||
];
|
||||
|
||||
// Patterns that flag content as scaffolding — structural glue the user does not need to see.
|
||||
const SCAFFOLDING_PATTERNS = [
|
||||
// Scenario summaries and setup descriptions
|
||||
/\bsummary\s*of\s*(?:scenario|situation|problem|context|background)\b/i,
|
||||
/(?:^|\s)summary\s*[:\.]?\s*/i,
|
||||
// Process labels — the user cares about findings, not processes
|
||||
/\b(?:process|approach|workflow|methodology|procedure)\s+describes?\b/i,
|
||||
// System/tool references that are implementation details
|
||||
/\b(?:system|tool|platform|interface|framework|engine|library|component)\s+(?:for|that|which|used|providing|supporting)\b/i,
|
||||
/(?:logging|measurement|reporting|tracking|monitoring)\s+(?:system|tool|mechanism|framework|approach)\b/i,
|
||||
// Metric object descriptions (the metric itself is fine; describing the *object* is not)
|
||||
/\b(?:metric|measure|indicator|KPI)\s+describes?\b/i,
|
||||
/\b(?:metric|measure|indicator)\s+(?:captures?|tracks?|quantifies?|represents?)\b/i,
|
||||
// Graph artefacts — nodes describing themselves or other graph elements
|
||||
/(?:graph|diagram|visualization)\s+(?:showing|depicting|illustrating|displaying)\b/i,
|
||||
/\bnodes?\s*representing?\b/i,
|
||||
// "Current situation" type labels that are pure scaffolding
|
||||
/\b(?:current\s+)?(?:situation|state|scenario|context)\b.*\b(describes?|is|represents?|shows)\b/i,
|
||||
// Vague state-of-play descriptions
|
||||
/\b(?:is\s+(?:a\s+)?(?:situation|case|scenario|context|problem))\b/i,
|
||||
];
|
||||
|
||||
// Patterns that flag content as implementation/technical vocabulary the user should not see.
|
||||
const INTERNAL_VOCAB_PATTERNS = [
|
||||
// Technical summary language
|
||||
/\b(?:total|overall)\s+(?:count|number|figure)\s+of\b/i,
|
||||
/(?:complaint|incident|issue)\s+logging\s+(?:system|tool|mechanism|process)\b/i,
|
||||
/(?:performance|quality|production)\s+(?:measurement|monitoring)\s+(?:tools?|systems?)\b/i,
|
||||
// "Scaffolding" kind of description masquerading as content
|
||||
/\b(?:summary|overview|background\s+context)\s+of\b/i,
|
||||
];
|
||||
|
||||
/**
|
||||
* Decide whether a raw graph text item should be included in the panel.
|
||||
* Returns { included, displayText, reason } where reason is null when accepted.
|
||||
@@ -68,16 +105,36 @@ function filterItem(raw) {
|
||||
const description = raw.description;
|
||||
const kind = raw.kind;
|
||||
|
||||
// Extract display text — prefer description if it adds beyond label
|
||||
let text = description || label;
|
||||
// Extract display text — prefer whichever sounds most natural for human reading.
|
||||
let text;
|
||||
if (description && typeof description === "string" && description.trim()) {
|
||||
// Prefer the longer, more informative text.
|
||||
if (description !== label) {
|
||||
text = description;
|
||||
} else {
|
||||
text = label;
|
||||
}
|
||||
} else {
|
||||
text = label || "";
|
||||
}
|
||||
if (!text || typeof text !== "string") return { included: false, reason: "empty" };
|
||||
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return { included: false, reason: "empty" };
|
||||
|
||||
// Depriorise items that are purely technical summaries
|
||||
for (var i = 0; i < TECHNICAL_SUMMARY_PATTERNS.length; i++) {
|
||||
if (TECHNICAL_SUMMARY_PATTERNS[i].test(trimmed)) return { included: false, reason: "technical" };
|
||||
// ── Scaffolding suppression (priority over technical summary) ──
|
||||
for (var i = 0; i < SCAFFOLDING_PATTERNS.length; i++) {
|
||||
if (SCAFFOLDING_PATTERNS[i].test(trimmed)) return { included: false, reason: "scaffolding" };
|
||||
}
|
||||
|
||||
// ── Internal vocabulary suppression ──
|
||||
for (var j = 0; j < INTERNAL_VOCAB_PATTERNS.length; j++) {
|
||||
if (INTERNAL_VOCAB_PATTERNS[j].test(trimmed)) return { included: false, reason: "internal-vocab" };
|
||||
}
|
||||
|
||||
// ── Technical summary suppression (existing) ──
|
||||
for (var k = 0; k < TECHNICAL_SUMMARY_PATTERNS.length; k++) {
|
||||
if (TECHNICAL_SUMMARY_PATTERNS[k].test(trimmed)) return { included: false, reason: "technical" };
|
||||
}
|
||||
|
||||
// Skip internal IDs — items whose text is just an ID or contains only one
|
||||
@@ -95,7 +152,7 @@ function filterItem(raw) {
|
||||
/* ── Node collection helpers ───────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Determine whether a node is resolved.
|
||||
* Determine whether a node is resolved (explicitly closed).
|
||||
*/
|
||||
function isResolved(node, resolvedIds) {
|
||||
return resolvedIds.has(node.id) || node.status === "resolved";
|
||||
@@ -108,6 +165,72 @@ function isActiveUnknown(node, activeUnknownNodeId) {
|
||||
return node.id === activeUnknownNodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a node's content represents established knowledge
|
||||
* (regardless of explicit resolution). Used for routing in Phase 1.
|
||||
* A kind=observation with status known is always an established observation.
|
||||
*/
|
||||
function isEstablished(node, resolvedIds) {
|
||||
if (node.kind === "observation" && node.status === "known") return true;
|
||||
// Already resolved nodes are also established (by ID or by status)
|
||||
if (resolvedIds.has(node.id)) return true;
|
||||
if (node.status === "resolved") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ── Semantic role classification ──────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Classify a node by its semantic role in the investigation rather than its graph kind.
|
||||
* Returns one of: "observation", "question", "explanation", "scaffolding", "relationship".
|
||||
*
|
||||
* This allows the adapter to route content based on *meaning* rather than *type*.
|
||||
* A node that says "Complaints increased by 35%" is an observation regardless of kind.
|
||||
* A node whose only value is describing a process or summarising the scenario is scaffolding.
|
||||
*/
|
||||
function classifySemanticRole(node, resolvedIds) {
|
||||
var text = (node.description || node.label || "").trim().toLowerCase();
|
||||
var kind = node.kind;
|
||||
|
||||
// If filterItem rejected it as scaffolding/internal-vocab, treat it as scaffolding here too
|
||||
var filtered = filterItem(node);
|
||||
if (!filtered.included) {
|
||||
return "scaffolding";
|
||||
}
|
||||
|
||||
// Check resolved/established first for semantic routing.
|
||||
// A resolved unknown or assumption is still a question/explanation in origin,
|
||||
// but its content is now known. We return "observation" here so that Phase 1
|
||||
// routing places it in the known section rather than investigating.
|
||||
if (resolvedIds && isEstablished(node, resolvedIds)) {
|
||||
return kind === "relationship" ? "relationship" : "observation";
|
||||
}
|
||||
|
||||
if (kind === "observation") return "observation";
|
||||
if (kind === "unknown") return "question";
|
||||
if (kind === "assumption") return "explanation";
|
||||
if (kind === "relationship") return "relationship";
|
||||
|
||||
// kind === "state" or "metric" — need to look at content
|
||||
var isConcrete = !!(
|
||||
/\b\d+/.test(text) || // contains numbers
|
||||
/\b(?:increased|decreased|rose|fell|changed|improved|worsened)\b/i.test(text) || // contains change language
|
||||
/\b(?:per|from |to |across|during|over\s+\d)/i.test(text) || // temporal/quantitative
|
||||
/\b(?:about|approximately|around|roughly|exactly)\b/i.test(text) ||
|
||||
/^\d/.test(text) // starts with a digit
|
||||
);
|
||||
|
||||
var isProcess = /describes?\s+(?:the\s+)?(?:current\s+)?(?:situation|state|scenario|problem|context)/i.test(text);
|
||||
var isSummary = /^summary/i.test(text) || /^(is\s+a\s+)?(situation|case|scenario|context)\b/i.test(text);
|
||||
|
||||
if (isConcrete) return "observation";
|
||||
if (isProcess || isSummary) return "scaffolding";
|
||||
|
||||
// Default: if it looks like a question or explanation from context, honour that.
|
||||
if (/\?$/.test(node.label || "")) return "question";
|
||||
return "scaffolding";
|
||||
}
|
||||
|
||||
/* ── Core adapter function ─────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
@@ -134,7 +257,7 @@ export function buildFacilitatorViewModel(_ref) {
|
||||
if (!edges) edges = [];
|
||||
if (!selectedQuestion) selectedQuestion = null;
|
||||
|
||||
// ── Phase 1: Categorise all nodes ───────────────────────────
|
||||
// ── Phase 1: Categorise all nodes by semantic role ──────────
|
||||
var known = [];
|
||||
var stillInvestigating = [];
|
||||
var possibleExplanations = [];
|
||||
@@ -142,14 +265,20 @@ export function buildFacilitatorViewModel(_ref) {
|
||||
for (var _i = 0; _i < nodes.length; _i++) {
|
||||
var node = nodes[_i];
|
||||
var resolved = isResolved(node, resolvedIds);
|
||||
var established = isEstablished(node, resolvedIds);
|
||||
var semanticRole = classifySemanticRole(node, resolvedIds);
|
||||
var displayResult = filterItem(node);
|
||||
|
||||
if (!displayResult.included) continue;
|
||||
|
||||
// Scaffold items are entirely suppressed from user-facing sections.
|
||||
if (semanticRole === "scaffolding") continue;
|
||||
|
||||
var entry = {
|
||||
text: displayResult.displayText,
|
||||
normalised: normaliseText(displayResult.displayText),
|
||||
kind: node.kind,
|
||||
semanticRole: semanticRole,
|
||||
confidence: node.confidence || null,
|
||||
isResolved: resolved,
|
||||
isActiveUnknown: isActiveUnknown(node, activeUnknownNodeId),
|
||||
@@ -158,37 +287,28 @@ export function buildFacilitatorViewModel(_ref) {
|
||||
priority: node.priority != null ? node.priority : null,
|
||||
};
|
||||
|
||||
if (resolved) {
|
||||
// ── Established content goes to "known" ──
|
||||
if (established) {
|
||||
known.push(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unresolved content routing by kind
|
||||
switch (node.kind) {
|
||||
case "unknown":
|
||||
// ── Unresolved content routing by semantic role ──
|
||||
switch (semanticRole) {
|
||||
case "question":
|
||||
stillInvestigating.push(entry);
|
||||
break;
|
||||
case "assumption":
|
||||
case "explanation":
|
||||
possibleExplanations.push(entry);
|
||||
break;
|
||||
case "state":
|
||||
case "metric":
|
||||
// Unresolved states/metrics with extra content go to investigating.
|
||||
if (node.description && node.description !== node.label) {
|
||||
stillInvestigating.push(entry);
|
||||
} else {
|
||||
known.push(entry);
|
||||
}
|
||||
break;
|
||||
case "observation":
|
||||
// Unresolved observations are uncertain — put in stillInvestigating.
|
||||
// Unresolved observations are uncertain — put in investigating.
|
||||
stillInvestigating.push(entry);
|
||||
break;
|
||||
case "conclusion":
|
||||
possibleExplanations.push(entry);
|
||||
case "relationship":
|
||||
known.push(entry);
|
||||
break;
|
||||
default:
|
||||
// Unknown kind — treat as unresolved unknown for safety.
|
||||
stillInvestigating.push(entry);
|
||||
}
|
||||
}
|
||||
@@ -242,7 +362,8 @@ export function buildFacilitatorViewModel(_ref) {
|
||||
|
||||
/**
|
||||
* Rank known items.
|
||||
* Order: supported/resolved > high-confidence > concise > connected to active > last added.
|
||||
* Order: observations > questions/resolved unknowns > explanations/resolved assumptions > relationships > other.
|
||||
* Within each group: high-confidence > medium > low > concise.
|
||||
*/
|
||||
function rankKnown(items) {
|
||||
var confidenceRank = {};
|
||||
@@ -251,23 +372,34 @@ export function buildFacilitatorViewModel(_ref) {
|
||||
confidenceRank["low"] = 2;
|
||||
confidenceRank["null"] = 3;
|
||||
|
||||
var rolePriority = {};
|
||||
rolePriority["observation"] = 0;
|
||||
rolePriority["question"] = 1;
|
||||
rolePriority["explanation"] = 2;
|
||||
rolePriority["relationship"] = 3;
|
||||
rolePriority["scaffolding"] = 4;
|
||||
|
||||
return items.slice().sort(function (a, b) {
|
||||
// Known items are all resolved. Prefer observations first.
|
||||
if (a.kind === "observation" && b.kind !== "observation") return -1;
|
||||
if (b.kind === "observation" && a.kind !== "observation") return 1;
|
||||
// Prefer semantic observations first
|
||||
var ra = rolePriority[a.semanticRole] != null ? rolePriority[a.semanticRole] : 4;
|
||||
var rb = rolePriority[b.semanticRole] != null ? rolePriority[b.semanticRole] : 4;
|
||||
if (ra !== rb) return ra - rb;
|
||||
|
||||
// Then by confidence
|
||||
var ca = confidenceRank[a.confidence] != null ? confidenceRank[a.confidence] : 3;
|
||||
var cb = confidenceRank[b.confidence] != null ? confidenceRank[b.confidence] : 3;
|
||||
if (ca !== cb) return ca - cb;
|
||||
|
||||
// Prefer concise items
|
||||
if (a.text.length !== b.text.length) return a.text.length - b.text.length;
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rank still-investigating items.
|
||||
* Order: active unknown > selected-question target > structurally eligible/high-priority > concise > remaining.
|
||||
* Order: active unknown > selected-question target > explicit priority > evidence-linked > concise > remaining.
|
||||
*/
|
||||
function rankUnknowns(items) {
|
||||
var sqText = selectedQuestion && selectedQuestion.question ? normaliseText(selectedQuestion.question) : null;
|
||||
@@ -289,8 +421,8 @@ export function buildFacilitatorViewModel(_ref) {
|
||||
// Prefer items linked to supported observations (has evidenceIds)
|
||||
var ae = a.evidenceIds ? a.evidenceIds.length : 0;
|
||||
var be = b.evidenceIds ? b.evidenceIds.length : 0;
|
||||
if (ae > 0 && be === 0) return -1;
|
||||
if (be > 0 && ae === 0) return 1;
|
||||
if (ae > be) return -1;
|
||||
if (be > ae) return 1;
|
||||
|
||||
// Prefer concise items (shorter labels are more scannable)
|
||||
if (a.text.length !== b.text.length) return a.text.length - b.text.length;
|
||||
@@ -439,3 +571,4 @@ export function buildFacilitatorViewModel(_ref) {
|
||||
}
|
||||
|
||||
export default buildFacilitatorViewModel;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user