240 lines
12 KiB
Markdown
240 lines
12 KiB
Markdown
# 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 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:
|
|
|
|
```bash
|
|
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](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.
|
|
|
|
## 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 — `<!-- competency: ... -->` 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, {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:
|
|
1. One-line stat summary (X applications, Y% applied, Z% positive outcome)
|
|
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 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.yml` to filter out geo-restricted roles
|
|
> - Set a score threshold in `_profile.md` for 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.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, 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) |
|