All files / src/lib sort-phase.ts

100% Statements 19/19
100% Branches 13/13
100% Functions 7/7
100% Lines 14/14

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56                                                                24x 61x 38x 24x 24x 24x 12x         12x 21x 12x 21x     12x 24x 24x        
import type { CollectionPreferenceItem, DecisionEvent, Disposition, Item } from "@/types";
import { selectTriageQueue, type TriageQueueEntry } from "@/lib/triage-selection";
import { selectComparableItems } from "@/lib/compare-selection";
import { deriveOrder, toComparisonEvents } from "@/lib/insertion-ranking";
 
export interface SortProgressCount {
  done: number;
  total: number;
}
 
export type SortPhase =
  | { kind: "feelings"; queue: TriageQueueEntry[]; progress: SortProgressCount }
  | { kind: "pairs"; pair: [Item, Item]; progress: SortProgressCount }
  | { kind: "done"; progress: SortProgressCount };
 
export interface SortPhaseInput {
  /** The collection's live items. */
  items: Item[];
  preferences: CollectionPreferenceItem[];
  dispositionByItemId: Map<string, Disposition>;
  /** The curator's cross-session decision log (the one the recompute reads). */
  curatorDecisions: DecisionEvent[];
  /** This visit already walked the Not yet tail (the feelings phase handed off). */
  deferredOffered: boolean;
}
 
/**
 * Which phase the Sort screen shows (spec ยง4). Feelings first: every live piece
 * passes through once, Not yet pieces at the end; Not mine pieces never. Then
 * pairs over Love + Like. Progress never counts Not yet or Not mine pieces.
 */
export function chooseSortPhase(input: SortPhaseInput): SortPhase {
  const { items, preferences, dispositionByItemId, curatorDecisions, deferredOffered } = input;
  const liveIds = new Set(items.map((i) => i.id));
  const placed = new Set(preferences.filter((p) => liveIds.has(p.itemId)).map((p) => p.itemId));
  const queue = selectTriageQueue(items, placed, dispositionByItemId);
  const active = queue.filter((e) => !e.deferred).length;
  if (active > 0 || (queue.length > 0 && !deferredOffered)) {
    return { kind: "feelings", queue, progress: { done: placed.size, total: placed.size + active } };
  }
 
  // The same insertion engine and comparable set (Love + Like) the recompute
  // runs, so the screen asks the question the stored order is waiting on.
  const comparable = selectComparableItems(items, preferences);
  const byId = new Map(comparable.map((i) => [i.id, i]));
  const { order, nextPair } = deriveOrder(
    comparable.map((i) => ({ id: i.id, createdAt: i.createdAt })),
    toComparisonEvents(curatorDecisions),
  );
  const x = nextPair && byId.get(nextPair[0]);
  const pivot = nextPair && byId.get(nextPair[1]);
  return x && pivot
    ? { kind: "pairs", pair: [x, pivot], progress: { done: order.length, total: comparable.length } }
    : { kind: "done", progress: { done: comparable.length, total: comparable.length } };
}