12 KiB
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 trackerreports/— Individual evaluation reportsconfig/profile.yml— User profile (for recommendation context)modes/_profile.md— User archetypes and framingportals.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 with status beyond "Evaluated" (i.e., Applied, Responded, Interview, Offer, Rejected, Discarded, SKIP)?
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:
node analyze-patterns.mjs
Parse the JSON output. It contains:
| Key | Contents |
|---|---|
metadata |
Total entries, date range, analysis date, counts by outcome |
funnel |
Count per status stage (evaluated, applied, interview, offer, etc.) |
scoreComparison |
Avg/min/max score per outcome group (positive, negative, self_filtered, pending) |
archetypeBreakdown |
Per-archetype: total, positive, negative, self_filtered, conversion rate |
blockerAnalysis |
Most frequent hard blockers: geo-restriction, stack-mismatch, seniority, onsite |
remotePolicy |
Per-policy bucket: total, positive, negative, conversion rate |
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 minimum score + reasoning |
techStackGaps |
Most frequent tech gaps in negative outcomes |
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/Interview/Offer (a bare Applied does not count).
Motivation: Algorithmic Monocultures in Hiring (Bommasani et al., FAccT 2026, arXiv: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
recommendationsarray already contains thehigh-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
recommendationsarray already contains themedium-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.
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:
- 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). - 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 —
<!-- competency: ... -->per the convention ininterview-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. - 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.mdand (b) the distribution of roles actually applied to indata/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.ymltitle_filter.positivetoward 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
# Pattern Analysis -- {YYYY-MM-DD}
**Applications analyzed:** {total}
**Date range:** {from} to {to}
**Outcomes:** {positive} positive, {negative} negative, {self_filtered} self-filtered, {pending} pending
---
## 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 | ... | | | |
| Self-filtered | ... | | | |
| Pending | ... | | | |
## Archetype Performance
Table with each archetype, total applications, positive outcomes, conversion rate.
Highlight the best-performing archetype and the worst.
## Top Blockers
Frequency table of recurring hard blockers (geo-restriction, stack-mismatch, etc.).
Note the percentage of all applications affected by each.
## Remote Policy Patterns
Table showing conversion rate by remote policy bucket (global, regional, geo-restricted, hybrid/onsite).
## Tech Stack Gaps
List of most common missing skills in negative/self-filtered outcomes with frequency.
## Recommended Score Threshold
State the data-driven minimum score and reasoning.
## 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:
- One-line stat summary (X applications, Y% applied, Z% positive outcome)
- Top 3 findings (most impactful patterns)
- Link to full report
Example:
Pattern Analysis Complete (24 applications, Apr 7-8)
Key findings:
- Geo-restricted roles are 0% conversion (7 of 24) -- 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.ymlto filter out geo-restricted roles- Set a score threshold in
_profile.mdfor PDF generation- Adjust archetype targeting based on what's converting
- Realign targeting from the session signal — add the under-targeted archetype X to
modes/_profile.mdand reweightportals.ymltitle_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.ymlunder apatternskey
Outcome Classification
For reference, outcomes are classified as:
| Status | Outcome |
|---|---|
| Interview, Offer, Responded, Applied | Positive (invested effort or got traction) |
| Rejected, Discarded | Negative (company said no or offer closed) |
| SKIP, NO APLICAR | Self-filtered (user decided not to apply) |
| Evaluated | Pending (no action taken yet) |