Files
santifer--career-ops/modes/patterns.md
T
wehub-resource-sync d083df1fdb
CodeQL Analysis / Analyze (javascript-typescript) (push) Failing after 2s
Web CI / web typecheck + build (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
CodeQL Analysis / Analyze (go) (push) Failing after 16s
chore: import upstream snapshot with attribution
2026-07-13 12:02:43 +08:00

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 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:

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 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

# 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)