feat: review project pseudonyms by category

This commit is contained in:
muena
2026-09-24 20:11:10 +02:00
parent 8b254eb87d
commit 226d230b6d
6 changed files with 242 additions and 127 deletions

66
src/lib/project-review.ts Normal file
View File

@@ -0,0 +1,66 @@
const SEPARATOR = "\n\n\n\n";
export const MAX_BATCH_CODEPOINTS = 24000;
export type BatchItem = { key: string; text: string };
export type BatchSegment = BatchItem & { start: number; end: number };
export type TextBatch = { text: string; segments: BatchSegment[] };
/** Keep file boundaries while reducing many small text/path API calls to a few batches. */
export function makeTextBatches(items: BatchItem[], limit = MAX_BATCH_CODEPOINTS): TextBatch[] {
const batches: TextBatch[] = [];
let segments: BatchSegment[] = [];
let text = "";
let length = 0;
const separatorLength = [...SEPARATOR].length;
const flush = () => {
if (segments.length) batches.push({ text, segments });
segments = [];
text = "";
length = 0;
};
for (const item of items) {
if (!item.text) continue;
const itemLength = [...item.text].length;
if (segments.length && length + separatorLength + itemLength > limit) flush();
if (segments.length) { text += SEPARATOR; length += separatorLength; }
segments.push({ ...item, start: length, end: length + itemLength });
text += item.text;
length += itemLength;
}
flush();
return batches;
}
export function spansBySegment<T extends { start: number; end: number }>(
batch: TextBatch, spans: T[],
): Map<string, T[]> {
const result = new Map<string, T[]>(batch.segments.map((segment) => [segment.key, []]));
let segmentIndex = 0;
for (const span of [...spans].sort((a, b) => a.start - b.start)) {
while (segmentIndex < batch.segments.length && span.start >= batch.segments[segmentIndex].end) segmentIndex++;
const segment = batch.segments[segmentIndex];
if (!segment || span.start < segment.start || span.end > segment.end) continue;
result.get(segment.key)!.push({ ...span, start: span.start - segment.start, end: span.end - segment.start });
}
return result;
}
export type GroupableHit = { id: string; type: string; original: string; placeholder: string };
export type ReviewGroup<T extends GroupableHit> = {
key: string; type: string; original: string; placeholder: string; hits: T[];
};
export function groupReviewHits<T extends GroupableHit>(hits: T[]): ReviewGroup<T>[] {
const groups = new Map<string, ReviewGroup<T>>();
for (const hit of hits) {
const key = JSON.stringify([hit.type, hit.original, hit.placeholder]);
let group = groups.get(key);
if (!group) {
group = { key, type: hit.type, original: hit.original, placeholder: hit.placeholder,
hits: [] };
groups.set(key, group);
}
group.hits.push(hit);
}
return [...groups.values()].sort((a, b) => a.type.localeCompare(b.type) || a.original.localeCompare(b.original));
}