# Mode: patterns -- Rejection Pattern Detector ## Purpose Analyze all tracked applications to find patterns in outcomes and surface actionable insights. Identifies what's working (archetypes, remote policies, score ranges) and what's wasting time (geo-restricted roles, stack mismatches, low-score applications). When interview sessions are available, it also reads *what the candidate actually says in the room* — a higher-resolution, lower-noise signal of role-fit than win/loss — to detect role **misfit**: when the candidate's strongest, most fluent answers point at a different role-type than the one they keep applying to (Step 1b). ## Inputs - `data/applications.md` — Application tracker - `reports/` — Individual evaluation reports - `config/profile.yml` — User profile (for recommendation context) - `modes/_profile.md` — User archetypes and framing - `portals.yml` — Portal config (for filter update recommendations) - `interview-prep/sessions/*.md` — Interview sessions (optional; drives Step 1b). Drop real-interview transcripts and mock-session files here. ## Minimum Threshold Before running analysis, check: does `data/applications.md` have at least 5 entries that were actually sent (Applied, Responded, Assessment, Interview, Offer, Hired, Rejected)? SKIP and Discarded rows are not submissions and do not count toward the floor. If not, tell the user: > "Not enough data yet -- {N}/5 applications have progressed beyond evaluation. Keep applying and come back when you have more outcomes to analyze." Exit gracefully. ## Step 1 — Run Analysis Script Execute: ```bash node analyze-patterns.mjs ``` Parse the JSON output. It contains: | Key | Contents | |-----|----------| | `metadata` | Total entries, date range, analysis date, counts by outcome, and `outcomeRates` — the whole tracker through the same `submitted` / `decided` denominators as every breakdown row | | `funnel` | Count per status stage (evaluated, applied, interview, offer, etc.) | | `scoreComparison` | Avg/min/max score per outcome group (positive, awaiting, negative, discarded, self_filtered, pending) | | `archetypeBreakdown` | Per-archetype: total, submitted, decided, positive, awaiting, negative, discarded, self_filtered, conversionRate, decidedRate | | `blockerAnalysis` | Most frequent hard blockers: geo-restriction, stack-mismatch, seniority, onsite; each `percentage` is a share of `blockerBase` | | `blockerBase` | Entries carrying a non-empty gaps array — the denominator for `blockerAnalysis[].percentage` | | `remotePolicy` | Per-policy bucket: same columns as `archetypeBreakdown` | | `companySizeBreakdown` | Per-size bucket: startup, scaleup, enterprise | | `vendorAnalysis` | ATS channel analysis: per-vendor advance rate + coverage (see below) | | `viaChannelAnalysis` | Via channel analysis (#1596): per-agency advance rate + agency-vs-direct aggregate (see below) | | `scoreThreshold` | `recommended` (null until `sufficientSample`), `observedMinimum`, `sampleSize`, `sufficientSample`, reasoning | | `techStackGaps` | Most frequent tech gaps in negative / discarded outcomes | | `discardReasonStats` | User-committed skip/discard reasons; each `percentage` is a share of `discardReasonBase` | | `discardReasonBase` | Self-filtered, discarded or negative entries — the denominator for `discardReasonStats[].percentage` and its recommendation threshold | | `recommendations` | Top 5 actionable items with reasoning and impact level | If the script returns `error`, display the error message and exit. ### `vendorAnalysis` — how to present it (IMPORTANT: causal humility) `vendorAnalysis` groups **submitted** applications by the ATS vendor detected from each report's `**URL:**` (community ATS with clean fingerprints only: Greenhouse, Lever, Ashby, Workday — white-labeled ATS are not URL-detectable and fall into an unreported `unknown` bucket). `advanceRate` = share that reached `Responded`/`Assessment`/`Interview`/`Offer` (a bare `Applied` does **not** count). Motivation: *Algorithmic Monocultures in Hiring* (Bommasani et al., FAccT 2026, [arXiv:2605.27371](https://arxiv.org/abs/2605.27371)) — rejections through a shared screener are correlated, not independent, so a concentrated dead channel has diminishing returns. When you narrate this to the user: - **Report channel yield, NOT discrimination.** A single tracker cannot separate "the vendor's algorithm filters me" from "that vendor skews toward a segment I fit poorly." Never claim bias. The honest, useful framing is: *"X% of your applications go through {vendor} and it's advancing far less than your other channels — route those companies through referral/direct contact instead."* - Respect `sufficientSample`: if false, mention the vendor only as an observation ("too few to conclude"), never as a recommendation. - Always state coverage (`coveragePct`) so the user knows the stats cover a subset. - The `recommendations` array already contains the `high`-impact channel action when one qualifies — surface it verbatim rather than inventing a stronger claim. ### `viaChannelAnalysis` — per-agency advance rate (#1596) Groups **submitted** applications by their `Via` channel (the recruiter/agency firm; requires the optional Via column, #1596 — trackers without it produce empty buckets and nothing is claimed). `—` rows count as `direct`; the `breakdown` lists each agency with total/advanced/`advanceRate`/`sufficientSample`. Submitted rows with an *empty* Via cell (legacy tracker or blank cell, as opposed to the explicit `—` direct marker) belong to neither bucket and are counted in `unknownVia` — when it's non-zero, state it so the user knows the agency/direct split covers a subset of submissions. In an agency-mediated search the highest-leverage decision is which recruiter relationships to invest in — this shows which ones actually convert. Same causal-humility rules as `vendorAnalysis`: report channel yield, never a causal claim; respect `sufficientSample`; a strong agency is *"prioritize roles via X — it converts"*, a weak one is an observation, not an accusation. - The `recommendations` array already contains the `medium`-impact best-converting-agency action when one qualifies — surface it verbatim rather than inventing a stronger claim. ### Salary lens (optional) If compensation observations exist (report `advertised_comp` keys or `data/salary-observations.tsv` lines), run `node salary-gap.mjs --summary` as an additional lens: advertised→actual haircut per (company, role) and per currency, plus desired-attainment. Zero tokens — never recompute these numbers manually. Respect its data-quality section the same way as `sufficientSample`: low sample sizes are observations, not recommendations. ### Company History lens (optional) Run `node company-history.mjs --summary` as an additional lens. Zero tokens. **Hygiene first, always.** The summary leads with any aged-Applied rows that look silent — present that list before any card. Tell the user to confirm real or update via `node set-status.mjs --row --on ` before drawing any conclusion from the cards below it — a stale tracker row produces the same signal as genuine silence. Then present the cards, sorted silent-first (the script already orders them this way). **Causal humility (mandatory, same posture as `vendorAnalysis`):** these cards are facts about *your own files* — a company reflecting `silent-on-you` means "no response recorded in your tracker/follow-ups," not "the company never responded." Innocent explanations are common: high-volume inboxes, evergreen requisitions, re-opened searches, or a response the candidate received but never logged. Present the facts (silent Nd since date, M follow-ups sent) and let the user judge — never phrase a card as a verdict about the company. ### Funnel Calibration lens (optional) If the tracker has Applied-or-beyond rows, run `node funnel-velocity.mjs --summary` as an additional lens: the candidate's funnel rates vs candidate-side market benchmark ranges, in-flight applications past the typical first-response window, and (once `data/status-log.tsv` has accrued transitions) median days per stage hop. Zero tokens — never recompute these numbers manually. Presentation rules (these are hard MUSTs, not style suggestions): - **Above-range rates**: encouragement is wanted — this lens exists to counter silence-anxiety — but MUST keep the script's selection-bias note (targeted applications are expected to beat mass-platform averages). Confirmed filter quality, not market ease. - **Below-range rates**: calibration plus exactly one concrete action (follow-up compliance via followup mode, or score-threshold review via patterns Step 2; skip the follow-up option when `follow_ups.enabled` is `false` in `config/profile.yml`). Never verdicts about the candidate. - **Every benchmark mention** carries its year and "directional" — the shipped baselines are recruiter-industry aggregates, not peer-reviewed statistics. - **No comparative multiplier claims below n=20 applied.** The script already suppresses them; do not reconstruct them manually from the raw numbers. - **Velocity medians**: always repeat the censored count the script prints ("n still waiting, excluded") — a median over answered applications only is survivorship-biased without it. ## Step 1b — Session-Content Targeting Signal (optional) Outcome data (Step 1) tells you *whether* you're winning. Interview sessions tell you *what role you're actually selling* in the room — a higher-resolution, lower-noise signal of role-fit than win/loss, which is confounded by comp, timing, headcount, and a dozen reasons unrelated to fit. **Run this step only if session data exists.** Check: `interview-prep/sessions/*.md` (excluding `README.md` and `.gitkeep`). If no sessions are present, **skip this step silently** and proceed with outcome-only analysis. This step is purely additive — the mode works fully without it, and gains resolution once sessions accumulate. If sessions exist, for each one: 1. Separate the candidate's answers from the interviewer's questions. If speaker labels are missing, infer them (turns tagged `**Interviewer:**` / `**Candidate:**` per the session format). 2. Determine the competency / role-signal each substantive answer demonstrates (e.g. *instructional-design*, *systems-architecture*, *data-analysis*, *stakeholder-management*, *people-leadership*). **Tags first, inference as fallback:** if the answer already carries an explicit competency tag — `` per the convention in `interview-prep/sessions/README.md`, whether written by hand or emitted by a debrief tool (e.g. `interview/debrief`) — use it directly. Only infer the competency yourself when no tag is present. 3. Mark whether the answer is **fluent and specific** (concrete metrics, named tools, real decisions) or **flat and generic** (hedged, vague, textbook). Then aggregate across all sessions: - **Where do the fluent/specific answers cluster?** That competency cluster is the role-type the candidate is *actually* strongest at — regardless of the title on their résumé. - Compare that cluster against (a) the archetypes in `modes/_profile.md` and (b) the distribution of roles actually applied to in `data/applications.md`. - **Surface the misfit:** if the strongest cluster (X) is under-represented in the roles applied to (Y), that is a targeting-correction signal: > "Your answers consistently light up around **X**, but you're mostly applying to **Y**. Consider adding archetype X and reweighting `portals.yml` `title_filter.positive` toward it." This is the difference between *"you're losing"* (Step 1, outcomes) and *"you're aiming at the wrong target"* (Step 1b, content). Feed the result into the Step 2 report and Step 4 recommendations. **Privacy:** sessions contain real interviewer names and companies. Read them locally only; **never quote a real name or company into a committed report.** Summarize the signal (competency clusters), never the content. ## Step 2 — Generate Report Write the report to `reports/pattern-analysis-{YYYY-MM-DD}.md`. ### Report Structure ```markdown # Pattern Analysis -- {YYYY-MM-DD} **Applications analyzed:** {total} **Date range:** {from} to {to} **Outcomes:** {positive} positive, {awaiting} awaiting a reply, {negative} negative, {self_filtered} self-filtered, {pending} not sent --- ## Conversion Funnel Show each status with count and percentage of total. Use a simple table: | Stage | Count | % | |-------|-------|---| | Evaluated | X | X% | | Applied | X | X% | | ... | | | ## Score vs Outcome | Outcome | Avg Score | Min | Max | Count | |---------|-----------|-----|-----|-------| | Positive | X.X/5 | X.X | X.X | X | | Negative | ... | | | | | Awaiting | ... | | | | | Self-filtered | ... | | | | | Pending | ... | | | | ## Archetype Performance Table with each archetype: `submitted`, `decided`, positive, `decidedRate` (lead with it) and `conversionRate`. Highlight the best-performing archetype and the worst. `conversionRate` = positives over everything sent, so a mostly-unanswered segment reads low by construction. `decidedRate` = positives over the applications with a recorded outcome, and is `null` while nothing is decided — report that as "no data yet", never 0%. Never divide by `metadata.total`: it counts evaluated rows that were never sent; the whole-tracker rates live in `metadata.outcomeRates`. ## Top Blockers Frequency table of recurring hard blockers (geo-restriction, stack-mismatch, etc.). Show each frequency against `blockerBase`; its percentage is the share of gap-bearing entries, not the share of all applications. ## Remote Policy Patterns Table showing conversion rate by remote policy bucket (global, regional, geo-restricted, hybrid/onsite). **Never call a segment bad on `conversionRate` alone** — an all-awaiting bucket shows a low rate and a `null` `decidedRate`, which is silence, not rejection. Call `0%` negative only when `positive` is 0 and `decided` >= 2, and name the decided count. ## Tech Stack Gaps List of most common missing skills in negative / discarded / self-filtered outcomes with frequency. ## Recommended Score Threshold State the data-driven minimum score and reasoning. When `sufficientSample` is false, `recommended` is `null`: report `observedMinimum` as an observation over `sampleSize` outcome(s) and do not offer to persist it. ## Targeting Signal (interview sessions) *Include this section only if Step 1b ran.* Summarize, in competency terms only (no real names/companies): - Which competency cluster the candidate's answers are strongest at (X) - Which role-types they're actually applying to (Y) - The misfit gap and the suggested realignment (add archetype X / reweight `portals.yml`) ## Recommendations Number the top recommendations (from the script output). For each: 1. **[IMPACT]** Action to take Reasoning behind the recommendation. ``` ## Step 3 — Present Summary Show the user a condensed version with: 1. One-line stat summary from `metadata.outcomeRates` (X sent, Y decided, Z% of decided advanced) — never a share of `metadata.total` 2. Top 3 findings (most impactful patterns) 3. Link to full report Example: > **Pattern Analysis Complete** (24 applications, Apr 7-8) > > Key findings: > - Geo-restricted roles: 0 of 7 **decided** applications advanced -- stop evaluating US/Canada-only postings > - Regional/global remote roles convert at 57-67% -- these are your sweet spot > - No positive outcomes below 4.2/5 -- consider this your score floor > > Full report: `reports/pattern-analysis-2026-04-08.md` ## Step 4 — Offer to Apply Recommendations Ask the user if they want to act on any recommendations: > "Want me to apply any of these recommendations? I can: > - Update `portals.yml` to filter out geo-restricted roles > - Set a score threshold in `_profile.md` for PDF generation (only when `scoreThreshold.sufficientSample` is true) > - Adjust archetype targeting based on what's converting > - Realign targeting from the session signal — add the under-targeted archetype X to `modes/_profile.md` and reweight `portals.yml` `title_filter.positive` (if Step 1b ran) > > Just say which ones, or 'all' to apply everything." If the user agrees: - For portal filter changes: edit `portals.yml` - For profile/archetype changes: edit `modes/_profile.md` (NEVER `_shared.md`) - For score threshold: add to `config/profile.yml` under a `patterns` key ## Outcome Classification For reference, outcomes are classified as: | Status | Outcome | |--------|---------| | Interview, Offer, Responded, Hired | **Positive** (the employer moved it forward) | | Applied | **Awaiting** (sent, no answer yet — counts in the conversion denominator, never as a success) | | Rejected | **Negative** (the employer said no) | | Discarded | **Discarded** (you withdrew, or the posting closed — not an employer decision, not a proven submission; counts in neither `submitted` nor `decided`) | | SKIP, NO APLICAR | **Self-filtered** (user decided not to apply) | | Evaluated | **Pending** (never sent) |