From 9620ab1990a3c4d219c84e70782032fabc9f97f6 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sun, 15 Feb 2026 19:34:13 -0800 Subject: [PATCH] Add transitive conflict detection for Must vs Must Not chains --- static/trip.html | 2 + static/trip.js | 96 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/static/trip.html b/static/trip.html index ba46957..c075c0e 100644 --- a/static/trip.html +++ b/static/trip.html @@ -63,6 +63,7 @@ .constraint-add select { font-size: 0.75rem; padding: 0.15rem; border: 1px solid var(--wa-color-neutral-300, #ccc); border-radius: 0.25rem; } #conflicts { margin-bottom: 0.5rem; } #mismatches { margin-bottom: 0.5rem; } + #hard-conflicts { margin-bottom: 0.5rem; } .conflict-row { margin-bottom: 0.2rem; } .conflict-icon { background: var(--wa-color-danger-50, #dc3545); color: white; border-radius: 0.15rem; padding: 0 0.15rem; font-size: 0.6rem; line-height: 1.2; vertical-align: middle; margin-right: 0.1rem; display: inline-block; } @@ -83,6 +84,7 @@
+
diff --git a/static/trip.js b/static/trip.js index 4cb1a34..252ec15 100644 --- a/static/trip.js +++ b/static/trip.js @@ -96,6 +96,77 @@ async function loadStudents() { } } + const studentName = {}; + for (const s of students) studentName[s.id] = s.name; + + const mustAdj = {}; + for (const s of students) { + for (const [bId, eff] of Object.entries(allOveralls[s.id])) { + if (eff.kind === 'must') { + const a = s.id, b = parseInt(bId); + if (!mustAdj[a]) mustAdj[a] = []; + mustAdj[a].push(b); + if (!mustAdj[b]) mustAdj[b] = []; + mustAdj[b].push(a); + } + } + } + + const ufParent = {}; + for (const s of students) ufParent[s.id] = s.id; + const ufFind = (x) => { + if (ufParent[x] !== x) ufParent[x] = ufFind(ufParent[x]); + return ufParent[x]; + }; + for (const s of students) { + for (const [bId, eff] of Object.entries(allOveralls[s.id])) { + if (eff.kind === 'must') { + const ra = ufFind(s.id), rb = ufFind(parseInt(bId)); + if (ra !== rb) ufParent[ra] = rb; + } + } + } + + const findMustPath = (from, to) => { + if (from === to) return [from]; + const visited = new Set([from]); + const queue = [[from]]; + while (queue.length > 0) { + const path = queue.shift(); + const curr = path[path.length - 1]; + for (const next of (mustAdj[curr] || [])) { + if (next === to) return [...path, next]; + if (!visited.has(next)) { + visited.add(next); + queue.push([...path, next]); + } + } + } + return null; + }; + + const hardConflictList = []; + for (const s of students) { + for (const [bId, eff] of Object.entries(allOveralls[s.id])) { + if (eff.kind !== 'must_not') continue; + const b = parseInt(bId); + if (ufFind(s.id) !== ufFind(b)) continue; + const path = findMustPath(b, s.id); + if (!path) continue; + const chain = []; + for (let i = 0; i < path.length - 1; i++) { + const x = path[i], y = path[i + 1]; + if (allOveralls[x]?.[y]?.kind === 'must') { + chain.push({ from: studentName[x], to: studentName[y], kind: 'must' }); + } else { + chain.push({ from: studentName[y], to: studentName[x], kind: 'must' }); + } + } + chain.push({ from: s.name, to: studentName[b], kind: 'must_not' }); + hardConflictList.push(chain); + } + } + const conflictsEl = document.getElementById('conflicts'); const conflictsWasOpen = conflictsEl.querySelector('wa-details')?.open; conflictsEl.innerHTML = ''; @@ -142,6 +213,31 @@ async function loadStudents() { mismatchesEl.appendChild(det); } + const hardConflictsEl = document.getElementById('hard-conflicts'); + const hardConflictsWasOpen = hardConflictsEl.querySelector('wa-details')?.open; + hardConflictsEl.innerHTML = ''; + if (hardConflictList.length > 0) { + const det = document.createElement('wa-details'); + det.summary = '\u26a0 Conflicts (' + hardConflictList.length + ')'; + if (hardConflictsWasOpen) det.open = true; + for (const chain of hardConflictList) { + const div = document.createElement('div'); + div.className = 'conflict-row'; + chain.forEach((link, i) => { + if (i === chain.length - 1 && chain.length > 1) { + div.appendChild(document.createTextNode(', but ')); + } else if (i > 0) { + div.appendChild(document.createTextNode(', ')); + } + div.appendChild(document.createTextNode(link.from + ' ')); + div.appendChild(kindSpan(link.kind)); + div.appendChild(document.createTextNode(' ' + link.to)); + }); + det.appendChild(div); + } + hardConflictsEl.appendChild(det); + } + const container = document.getElementById('students'); const openStates = {}; for (const card of container.children) {