import { findPartialPlaceholderStart, generateSecretPlaceholder } from "../constants/placeholders";
import type { ChatCompletionResponse, ChatMessage } from "../services/llm-client";
-import { resolveConflictsSimple } from "../utils/conflict-resolver";
+import { resolveOverlaps } from "../utils/conflict-resolver";
import { extractTextContent } from "../utils/content";
import type { SecretsRedaction } from "./detect";
}
// Resolve conflicts between overlapping redactions
- const resolved = resolveConflictsSimple(redactions);
+ const resolved = resolveOverlaps(redactions);
// First pass: sort by start position ascending to assign placeholders in order of appearance
const sortedByStart = [...resolved].sort((a, b) => a.start - b.start);
import {
type EntityWithScore,
resolveConflicts,
- resolveConflictsSimple,
+ resolveOverlaps,
} from "./conflict-resolver";
describe("resolveConflicts (Presidio-style)", () => {
});
});
-describe("resolveConflictsSimple (for secrets without scores)", () => {
+describe("resolveOverlaps (for secrets without scores)", () => {
test("returns empty array for empty input", () => {
- expect(resolveConflictsSimple([])).toEqual([]);
+ expect(resolveOverlaps([])).toEqual([]);
});
test("returns single entity unchanged", () => {
const entities = [{ start: 0, end: 5 }];
- expect(resolveConflictsSimple(entities)).toEqual(entities);
+ expect(resolveOverlaps(entities)).toEqual(entities);
});
test("keeps non-overlapping entities", () => {
{ start: 0, end: 5 },
{ start: 10, end: 15 },
];
- expect(resolveConflictsSimple(entities)).toEqual(entities);
+ expect(resolveOverlaps(entities)).toEqual(entities);
});
test("keeps adjacent entities", () => {
{ start: 0, end: 4 },
{ start: 4, end: 9 },
];
- expect(resolveConflictsSimple(entities)).toEqual(entities);
+ expect(resolveOverlaps(entities)).toEqual(entities);
});
test("keeps longer when same start position", () => {
{ start: 6, end: 10 },
{ start: 6, end: 12 },
];
- const result = resolveConflictsSimple(entities);
+ const result = resolveOverlaps(entities);
expect(result).toHaveLength(1);
expect(result[0].end).toBe(12);
});
{ start: 0, end: 10 },
{ start: 5, end: 15 },
];
- const result = resolveConflictsSimple(entities);
+ const result = resolveOverlaps(entities);
expect(result).toHaveLength(1);
expect(result[0].start).toBe(0);
});
{ start: 0, end: 14 },
{ start: 4, end: 8 },
];
- const result = resolveConflictsSimple(entities);
+ const result = resolveOverlaps(entities);
expect(result).toHaveLength(1);
expect(result[0].end).toBe(14);
});
-/**
- * Conflict resolution for overlapping entities
- *
- * Based on Microsoft Presidio's conflict resolution logic:
- * https://github.com/microsoft/presidio/blob/main/presidio-anonymizer/presidio_anonymizer/anonymizer_engine.py
- */
+// Conflict resolution based on Microsoft Presidio's logic
+// https://github.com/microsoft/presidio/blob/main/presidio-anonymizer/presidio_anonymizer/anonymizer_engine.py
export interface EntityWithScore {
start: number;
return groups;
}
-/**
- * Merge overlapping intervals. Returns new array (does not mutate input).
- */
function mergeOverlapping<T extends Interval>(
intervals: T[],
merge: (a: T, b: T) => T,
const last = result[result.length - 1];
if (overlaps(current, last)) {
- // Replace last with merged interval
result[result.length - 1] = merge(last, current);
} else {
result.push(current);
return result;
}
-/**
- * Remove entities that are contained in another or have same indices with lower score.
- */
function removeConflicting<T extends EntityWithScore>(entities: T[]): T[] {
if (entities.length <= 1) return [...entities];
- // Sort by start, then by score descending (higher score first)
const sorted = [...entities].sort((a, b) => {
if (a.start !== b.start) return a.start - b.start;
if (a.end !== b.end) return a.end - b.end;
return result;
}
-/**
- * Resolve conflicts between overlapping entities using Presidio's algorithm.
- *
- * Phase 1: Merge overlapping entities of the same type (expand boundaries, keep highest score)
- * Phase 2: Remove conflicting entities of different types (contained or same indices with lower score)
- */
+/** For PII entities with scores. Merges same-type overlaps, removes cross-type conflicts. */
export function resolveConflicts<T extends EntityWithScore>(entities: T[]): T[] {
if (entities.length <= 1) return [...entities];
return removeConflicting(afterMerge);
}
-/**
- * Simple overlap resolution for entities without scores.
- * Uses length as tiebreaker (longer wins). For secrets detection.
- */
-export function resolveConflictsSimple<T extends Interval>(entities: T[]): T[] {
+/** For secrets without scores. Keeps non-overlapping, longer wins ties. */
+export function resolveOverlaps<T extends Interval>(entities: T[]): T[] {
if (entities.length <= 1) return [...entities];
const sorted = [...entities].sort((a, b) => {