fix(collision): treat case/whitespace-variant identities as one person

Vision reports a display name ("Karti") while the git watcher reports a handle ("karti"); the detectors compared engineerId case-sensitively, so the same human collided with themselves. Canonicalize identity at the decision point in both the file and research detectors, keeping the display-cased name for the card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kartikeya
2026-06-28 10:34:16 -07:00
parent d8eaf8d2e2
commit 79620bccaa
2 changed files with 25 additions and 3 deletions
+18 -2
View File
@@ -19,6 +19,15 @@ function fileKey(raw?: string): string | undefined {
return base.toLowerCase(); return base.toLowerCase();
} }
/**
* Canonicalize an engineer identity for case/whitespace-insensitive matching.
* Vision reports a display name ("Karti") while the git watcher reports a handle
* ("karti"); without this the same human collides with themselves.
*/
function canonicalName(raw: string): string {
return raw.trim().toLowerCase();
}
interface Touch { interface Touch {
engineerId: string; engineerId: string;
unpushed: boolean; unpushed: boolean;
@@ -64,8 +73,15 @@ export function detectCollisions(
const out: Collision[] = []; const out: Collision[] = [];
for (const [, touches] of byFile) { for (const [, touches] of byFile) {
const engineers = [...new Set(touches.map((t) => t.engineerId))]; // Distinct PEOPLE, case/whitespace-insensitive — one display name per person.
if (engineers.length < 2) continue; // need two distinct people on one file // Vision "Karti" and git "karti" are the same human, not a collision.
const byPerson = new Map<string, string>(); // canonical id -> display name
for (const t of touches) {
const id = canonicalName(t.engineerId);
if (id && !byPerson.has(id)) byPerson.set(id, t.engineerId);
}
if (byPerson.size < 2) continue; // need two distinct people on one file
const engineers = [...byPerson.values()];
const anyUnpushed = touches.some((t) => t.unpushed) || github.unpushed === true; const anyUnpushed = touches.some((t) => t.unpushed) || github.unpushed === true;
if (!anyUnpushed) continue; // the crux GitHub alone cannot answer if (!anyUnpushed) continue; // the crux GitHub alone cannot answer
+7 -1
View File
@@ -24,6 +24,12 @@ function stripGitPrefix(raw: string): string {
return raw.trim().replace(/^(\?\?|[MADRCU!]{1,2})\s+/, ''); return raw.trim().replace(/^(\?\?|[MADRCU!]{1,2})\s+/, '');
} }
/** Case/whitespace-insensitive identity so vision "Karti" and git "karti" are
* recognized as the same person and never flagged researching-vs-editing self. */
function canonicalName(raw: string): string {
return raw.trim().toLowerCase();
}
function fileStem(raw: string): string { function fileStem(raw: string): string {
const base = stripGitPrefix(raw).split(/[\\/]/).pop()?.trim().toLowerCase() ?? ''; const base = stripGitPrefix(raw).split(/[\\/]/).pop()?.trim().toLowerCase() ?? '';
return base.replace(/\.[^.]+$/, ''); return base.replace(/\.[^.]+$/, '');
@@ -104,7 +110,7 @@ export async function detectResearchOverlaps(
const researchText = [topic, source].filter(Boolean).join(' '); const researchText = [topic, source].filter(Boolean).join(' ');
for (const editor of editorFiles) { for (const editor of editorFiles) {
if (editor.engineerId === researcher.engineerId) continue; if (canonicalName(editor.engineerId) === canonicalName(researcher.engineerId)) continue;
const stem = fileStem(editor.file); const stem = fileStem(editor.file);
if (!stem) continue; if (!stem) continue;