Ctrl K
ComunidadGuías…

src/lib/quiet.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * How long an application has been quiet, as a rule rather than a guess.
 *
 * The number was computed inside `diagnoseSearch` and never left it, so the
 * dashboard could tell you three things had gone quiet while the board they
 * were sitting on said nothing. It lives here — a pure module with no Prisma
 * import — because the board and the filter menu are client components, and
 * `src/lib/resume-text.ts` already records what happens when a function is
 * imported into the browser from the data layer: it drags the whole module,
 * Prisma and node:crypto included, into the bundle.
 */

export const DAY = 86_400_000;

/**
 * How long an application can sit in a stage before it has probably died.
 *
 * These are the numbers diagnoseSearch has always used, kept exactly, because
 * one rule means one set of thresholds — a dashboard that calls something
 * stalled while the card beside it looks fine is worse than either alone. A
 * wishlist entry is not quiet (nothing has happened because nothing was meant
 * to happen yet) so it has no threshold, and neither do the endings.
 */
export const STALE_AFTER: Partial<Record<string, number>> = {
  APPLIED: 21,
  // One number for the whole of interviewing, where there used to be three
  // (14 for a screen, 14 for an interview, 10 for a final round). Two of them
  // were the same anyway, and which of the three a given employer's process
  // belonged in was a guess — so the tighter of the two live numbers wins and
  // the rule stays one sentence: two weeks of silence mid-process is quiet.
  INTERVIEWING: 14,
  OFFER: 7,
};

/**
 * Whether silence at this stage means anything at all — the stages with a
 * threshold above. A caller that takes its own day count (morning_brief's
 * quiet_after_days, the weekly digest's fortnight) still has to ask this
 * first: filtering on days alone put never-sent wishlist rows at the top of
 * the brief as "silence this long usually means it is over".
 */
export function canGoQuiet(stage: string): boolean {
  return STALE_AFTER[stage] !== undefined;
}

/**
 * When a card starts saying how long it has been. Below this the number is
 * noise — everything is a few days quiet — and a badge on every card teaches
 * nothing.
 */
export const SHOW_QUIET_AFTER = 7;

/** Everything the rule needs to know about one application. */
export type QuietSubject = {
  stage: string;
  createdAt: Date;
  appliedAt?: Date | null;
  /** The most recent activity of any kind, if there is one. */
  lastActivityAt?: Date | null;
  /** When it last changed stage, if it ever has. */
  lastStageChangeAt?: Date | null;
};

/**
 * The last time anything actually happened, which is deliberately NOT
 * `updatedAt`: dragging a card past another one writes a sort order and would
 * otherwise reset the clock on an application nobody touched.
 */
export function lastTouchAt(subject: QuietSubject): Date {
  const candidates = [
    subject.lastActivityAt,
    subject.lastStageChangeAt,
    subject.appliedAt,
    subject.createdAt,
  ].filter(Boolean) as Date[];
  return candidates.reduce((latest, date) => (date > latest ? date : latest), candidates[0]);
}

/** Whole days since the last touch. Never negative, even with a future date. */
export function quietDaysFor(subject: QuietSubject, now: Date): number {
  return Math.max(0, Math.floor((now.getTime() - lastTouchAt(subject).getTime()) / DAY));
}

/**
 * Whether this one is worth chasing: past its stage's threshold, and still
 * live. A rejection that has sat untouched for a year is not a job to chase.
 */
export function hasGoneQuiet(stage: string, quietDays: number, terminal: readonly string[]): boolean {
  if (terminal.includes(stage)) return false;
  const threshold = STALE_AFTER[stage];
  return threshold !== undefined && quietDays >= threshold;
}