Score a PhD applicant's profile and rank candidate advisors using a connection-first 4.0-scale scoring system. Best-supported for physics / HEP and materials science (MSE), with the scoring engine extensible to chemistry, biology, CS, math, EE, ChemE, earth science (each with field-specific caveats — see references/journal_tiers.md). Use when the user wants to evaluate their PhD application chances, find matching advisors at top US programs, score a CV for graduate school, or compare candidate professors. Also triggers when the user mentions phdtaketaketake or its connection-first philosophy of valuing advisor network over h-index.
Installation
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Score a PhD applicant's profile and rank candidate advisors using a connection-first 4.0-scale scoring system. Best-supported for physics / HEP and materials science (MSE), with the scoring engine extensible to chemistry, biology, CS, math, EE, ChemE, earth science (each with field-specific caveats — see references/journal_tiers.md). Use when the user wants to evaluate their PhD application chances, find matching advisors at top US programs, score a CV for graduate school, or compare candidate professors. Also triggers when the user mentions phdtaketaketake or its connection-first philosophy of valuing advisor network over h-index.
Every connection edge, every candidate fact, every signal value MUST trace
back to a real source you actually fetched via web search. Fabrication is
strictly forbidden — students use these rankings to decide where they spend
years of their life. Made-up data is worse than no data.
The contract:
✅ Verified via web search → record value + structured EvidenceSource
(URL + source_type + claim + supports_fields)
✅ Searched but found nothing → leave the field empty / set signal to "missing"
❌ Guessed from training memory → NOT ALLOWED
❌ Inferred from name patterns / school proximity / "feels likely" → NOT ALLOWED
❌ Estimated without any web search → NOT ALLOWED
Two enforcement layers:
Risk-adjusted ranking — wide confidence bands move candidates
down the sort order. The agent literally cannot get a top rank with
unsourced claims; the band widens and risk_adjusted_strength = strength − band/2 drops below better-evidenced peers.
--strict-evidence flag — when run with this flag, scripts/match.py
rejects any candidate that has unsourced claims (a value set without an
EvidenceEntry). Missing signals (no value, no evidence) are still
allowed — they're honest "I couldn't verify" states. Use this when the
user is making real application decisions.
The matcher's confidence band (±0.2 / 0.4 / 0.6 / 0.8 — see §Confidence
calibration below) handles missing data gracefully, AND the risk-adjusted
ranking subtracts band/2 from the sort key — so wide bands move candidates
down the list. A wide band on real data is far more useful than a narrow
band on made-up data.
Full allowed-source list and forbidden-behavior catalog:
references/data_integrity.md. Read it
before doing any connection research.
This skill ranks candidate PhD advisors using a 5-layer deterministic
pipeline — each layer composes the layer below; every score traces
back to cited evidence:
Connection (C) — verified path between candidate PI ↔ student's
current advisor: small-team coauthor, big-collab paper overlap,
working group, analysis contact, genealogy, shared grant,
co-mentored student, committee/exam, same center, prior-institution
overlap, conference session. v2 aggregation: strongest + 0.10·second_strongest, capped at 1.0, scaled by recency.
Advisor influence (A) — PI reputation only (post-#6a):
0.40·influence + 0.30·elite_status + 0.30·grad_placement_quality.
Funding and recruiting moved to Opportunity (O).
Publication (P) — field-aware tier × author-role × status ×
recency × contribution-bonus, with big-collab and consortium
guardrails (min(0.10, n/100) cap on alphabetical co-authorship).
Program difficulty (D) — per-program penalty 0–0.8 from school-
tier admit rate + cohort size + admission model + funding structure
faculty count + international friendliness. Subtracted from
risk_adjusted_strength to form difficulty_adjusted_strength
(the primary sort key, replaces v1 tier_adj).
Research fit (R) — structured 6-axis tie-breaker: `0.30·topic
0.20·method + 0.15·system + 0.15·temporal + 0.10·grant +
0.10·background`. Never a 6th pillar; sorts ties only.
How users actually invoke this skill (natural language)
Users on QClaw / Claude Code / any agent platform will not write JSON
themselves. The expected entry shape is conversational:
"我是 2027 fall 申请 Physics PhD,方向是 ATLAS Higgs / detector ML。
本科 UCI,GPA 3.85/4.0,有两篇 ATLAS big-collab paper,导师是 Prof. X。
请帮我找美国 top 10–30 的匹配 PI,并按 phdtaketaketake 的 evidence-first
规则给出排序和申请策略。"
"I'm applying for biology PhDs this fall, focusing on cancer
immunology. Berkeley undergrad, GPA 3.9, one first-author Cell paper,
advisor is Prof. Y. Find me 8 advisors at top US programs."
Your job as the agent: translate this into the structured
StudentProfile + candidate-discovery workflow below. Do not ask
the user to fill JSON. Do not ask for the schema upfront. Ask for
missing facts in plain English, one round at a time.
Required information to ask for (if not given)
If the user's first message is missing any of these, ask before doing
deep research — running the pipeline without them produces low-confidence
output:
Field / subfield (e.g. "physics / HEP" → resolves to FieldProfile)
Research direction (1–2 sentences — the matcher uses this for research_fit)
Current advisor(s) (name + institution — drives the C pillar; without this, connection-first matching is degraded and the matcher prints a stderr warning)
Target school tier or list (top_10 / top_11_30 / top_31_60 / top_60_plus, OR a list of school names)
Optional but improves output quality
Papers: title, venue, status (published / accepted / submitted / preprint / in_prep), author position, total authors. Without this, P pillar floors out; user gets an honest "no publication evidence" rather than a guess.
Experiences: lab name, duration months, output (paper / poster / thesis). Without this, E pillar floors.
Specific candidate PIs: if the user already has a target list, skip Step 3 (discovery_plan) and feed candidates straight to collect_evidence. If not, run discovery_plan first.
Theory / experiment crossover preferences (physics-specific): affects research_fit.theory_experiment_fit signal.
International friendliness needs (visa / funding constraints): affects program_difficulty interpretation.
Even with no candidate list, the agent can run discovery-plan to
generate per-school search queries, then collect-evidence on
agent-discovered candidates, then match. Missing optional fields
widen the confidence band but do not crash.
Two-layer output contract
What the user sees vs what power users / strict-mode auditors get:
Per-candidate cards (rendered by you, the agent) — the human-
readable presentation. Format defined in §"How to present results
to the user" below.
Full match.json (raw MatchResult JSON) — kept as power-user
appendix; never the primary user-facing artifact.
Step 0 — Load the FieldProfile
Before running any deep-research, load the FieldProfile for the user's
discipline. This is the per-field calibration layer that tells you
which databases to search, how to bucket papers, and what caveats to
surface. Bundled profiles:
journal_first; co-first common; PubMed/bioRxiv; HHMI strong signal
chemistry
chem, organic, inorganic, physical chemistry
journal_first; senior author = last
math
mathematics, applied math, pure math, statistics
preprint_first; arXiv often canonical; Math Genealogy authoritative
The matcher resolves aliases (e.g., user types "hep" → loads
physics.yaml). The resolved profile flows into the result as
field_profile_id and field_caveats; always surface the caveats in
your result presentation — they are how the matcher tells the user
"this discipline has the following gotchas".
For an unbundled field (e.g., "materials_chemistry"), the matcher
returns field_profile_id: null — fall back to the cross-field guidance
in references/journal_tiers.md and your domain knowledge, and tell the
user explicitly "no profile bundled for this field".
Architecture: no static cache, always real-time research
There is no bundled candidate cache. PhD advisor data is too dynamic
(people change institutions, retire, take new students, pivot subfields) and
too vast (millions of PIs across STEM) for any static dataset to be useful.
Instead, the split:
You (the agent) — do the deep research. Use web search + page fetch +
whatever tools you have to find candidates and verify connection edges.
scripts/match.py — pure Python. Takes the profile + candidates you
built and runs the deterministic scoring (Pub/GPA/Experience/Connection
combination, tier-adaptive weights, application_strength with confidence band).
This makes the skill universal across STEM fields and always-fresh.
Workflow
Step 1 — Gather student profile
Required fields (must ask if missing):
field — any STEM string ("physics", "chemistry", "biology", etc.)
undergrad_institution
gpa_raw + gpa_scale
research_direction — short paragraph (≥30 words is best)
current_advisors[] — {id, name, institution}. Without this, the entire
Connection score collapses to candidate's field strength only.
papers[] — {title, journal, journal_tier, author_position, status, year}.
Without this, P score floors at 3.0.
Paper-status weights: published / accepted / in_press get full
credit; submitted / preprint get 0.7×; in_prep gets 0.3×. List
status honestly. Default is "published". Schema is strict —
unknown status values raise. Field-specific overrides apply (e.g.,
math sets preprint=0.9 because arXiv is often the canonical record).
Optional P1 fields (use when you can):
venue_type: journal / conference / workshop / preprint /
clinical_trial — useful for CS where conferences = top venues
author_role: first / co_first / middle / senior /
corresponding / consortium. When set, co_first /
corresponding / senior are scored as 1st-author equivalent
regardless of byline position. Use co_first for biology
"These authors contributed equally" cases.
total_authors: total author count on the paper. Use the
classify_coauthorship(total_authors, field_profile) helper to
bucket path_edge.small_team_coauthor_5y vs big_collab_papers_5y
with the right per-field threshold (physics 10, mse/cs 8,
biology/chemistry 6, math 4).
experiences[] — {lab_pi_name, lab_tier, duration_months, output_type}.
Without this, E score defaults to 2.0.
Source priority:
CV / resume pasted or attached → parse it, then show the inferred
profile back to the user for confirmation before continuing.
Existing profile JSON → use directly.
Prose description → ask brief targeted batches for missing fields. Don't
dump a 10-question list at once.
For mappings (gpa_scale, journal_tier, lab_tier, output_type,
author_position for big-collab papers): see references/profile_schema.md
and references/journal_tiers.md. When uncertain about a journal tier,
ask the user or default to tier 4.
Ask the user where they want to apply. Acceptable inputs:
Specific schools (e.g., MIT, Stanford, Princeton)
A tier ("top 10 physics", "top 30 chemistry")
Specific professors they have in mind ("I'm interested in Prof. X")
Open-ended ("show me the best matches")
Per the cardinal data-integrity rule, school tier is now a sourced
signal — not memorized. Fetch the field-appropriate ranking page.
The right source depends on field; use the loaded FieldProfile's
ranking_source_url_template first:
field
preferred ranking source
physics / mse / chemistry / biology / math
US News field-specific page (URL template in profile)
cs
CSRankings (https://csrankings.org/) — community-maintained, more reliable than US News for CS subfields
Generic US News science page is a fallback only when no profile applies.
Record the URL in evidence["school_tier"].items for every candidate
you generate, with supports_fields=["school_tier"]:
"evidence":{"school_tier":{"items":[{"url":"https://www.usnews.com/best-graduate-schools/...","source_type":"us_news","claim":"MIT physics ranked top 10 in 2024","supports_fields":["school_tier"]}]}}
Without claim-level evidence (or in strict mode without items at all),
school_tier counts as unverified and the candidate's confidence band
widens.
If the user gives a tier, use the fetched ranking to enumerate target
schools (~10–20). Don't enumerate from training memory — rankings change
year-to-year and your training data may be stale.
Step 2.5 — Generate per-field discovery plan (optional)
Before running the candidate-discovery deep-research step, run
scripts/build_discovery_plan.py to get a structured search plan
(per-field query recipes, primary databases, exclusion rules) — keeps
field coverage consistent and prevents "I forgot to search OpenReview
for ML papers" failure modes:
Output JSON includes per-school query recipes (Google Scholar / DBLP
/ INSPIRE / PubMed / NIH RePORTER / Math Genealogy / etc.), the
loaded FieldProfile.primary_databases, the field-specific
ranking_source_url, the field caveats, and universal exclusion
rules (skip emeriti, no PhD students, no recent papers, etc.). Use
the queries verbatim during the deep-research step.
Step 3 — Find candidate PIs (research direction match)
For each target program, web-search for active PIs whose research matches the
user's research_direction:
<school> <department> "<user research keywords>" faculty
For each PI you find, capture:
id — any unique string (e.g., cand_001)
name, institution
school_tier — based on the field-specific US News PhD ranking
(top_10 / top_11_30 / top_31_60 / top_60_plus)
field — same as student.field
research_areas — 3–5 short tags from their faculty profile / recent papers
Quality bar: PI should have ≥1 paper in last 3 years matching the direction.
Skip emeriti, deans, and people who've fully pivoted to admin / industry.
Aim for 10–30 candidates per query. The matcher caps at top-K anyway.
Step 4 — Compute connection edges (THE core IP)
For each candidate, search for verifiable connection signals to the
user's current_advisors. Re-read the cardinal rule above — every
edge must be backed by an actual web-search result, with a URL you can
cite. No guessing from training memory.
Direct co-authorship — DIFFERENTIATE small-team vs big-collab. This
distinction matters: co-authoring a 5-person condensed-matter paper is
real evidence of working together; co-name on an alphabetical 3000-author
ATLAS paper is just shared collaboration membership.
The threshold for "big collab" is field-specific — see the loaded
FieldProfile's big_collab_threshold:
field
threshold (>N authors → big collab)
physics
10 (ATLAS-aware)
mse / cs
8
biology / chemistry
6
math
4
Use the right threshold per field instead of always assuming 10.
Search the FieldProfile's primary_databases first (e.g., INSPIRE-HEP
for physics, DBLP for CS, PubMed for biology). Generic Google Scholar is
a fallback. Specifically:
- Google Scholar: "<advisor full name>" "<candidate full name>"
site:scholar.google.com
- OpenAlex API: https://api.openalex.org/works?filter=
authorships.author.id:<advisor_id>,
authorships.author.id:<candidate_id>
- INSPIRE-HEP: https://inspirehep.net/search?p=a+<advisor>+a+<candidate>
(preferred for physics)
- PubMed: for biology/medicine pairs
- Semantic Scholar: for CS pairs
For each co-authored paper found, check author count before tallying:
≤ field threshold → counts toward small_team_coauthor_5y (full strength, max 1.0 at n=5+)
field threshold → counts toward big_collab_papers_5y (very weak alone, cap 0.10)
Sprint-2-c1 added more edge types beyond co-authorship — record
when verified: shared_grant_count_5y (NSF/NIH/DOE shared grants),
co_mentored_student_count (jointly supervised students),
committee_or_exam_overlap (PhD committee / qualifying exam),
same_center_or_institute, prior_institution_overlap_years,
conference_session_overlap_5y. Also set
most_recent_connection_year (year of last interaction) — drives the
recency multiplier (0–2y → 1.0, 3–5y → 0.85, 6–10y → 0.60, 10y+ →
0.35, None → 0.75). See
references/connection_v2.md for the
full ladder, aggregation formula, and per-edge evidence guidance.
If the candidate-advisor relationship is in a big-collab field (HEP, large
clinical trials, BICEP / LIGO, etc.) and you find shared papers but they're
all big-collab, look for stronger evidence before claiming connection:
same_working_group: true if both are documented members of the same
ATLAS subgroup / convener team (verify via INSPIRE-HEP or working-group
page)
analysis_contact_overlap: true if both are listed as analysis contacts
on a specific paper / internal note (verify via published authorship page
or paper-specific contact list)
Record sources for every edge. Use the structured items field
(preferred) so each URL is bound to a specific claim:
"paths_to_advisors":{"adv_001":{"small_team_coauthor_5y":3,"big_collab_papers_5y":12,"same_working_group":true,"items":[{"url":"https://scholar.google.com/citations?user=<id>&...","source_type":"google_scholar","claim":"3 co-authored papers in 2022-2024 with ≤10 authors","supports_fields":["small_team_coauthor_5y"]},{"url":"https://inspirehep.net/authors/<candidate>/...","source_type":"inspire","claim":"12 ATLAS publications co-authored 2020-2024","supports_fields":["big_collab_papers_5y"]},{"url":"https://atlas-glance.cern.ch/atlas/analysis/<group>/conveners","source_type":"lab_page","claim":"both listed as H→cc̄ working group conveners 2021-2023","supports_fields":["same_working_group"]}],"note":"3 small-team co-authored papers, 12 ATLAS bulk, both H→cc̄ conveners"}}
The legacy sources: list[str] (bare URLs) is still accepted for
backward compatibility but prefer the structured items form — it
makes claims auditable: each URL is bound to a specific supports_fields
list, so a reviewer can verify each claim individually rather than
guessing which URL backs which signal.
Verify membership via the consortium's published author list, the
candidate's CV / lab page, or INSPIRE-HEP collaboration tracking — not
training memory. Estimate overlap years from documented join/leave
dates → collaboration_overlap_years (float).
Academic genealogy (PhD lineage shared):
- Mathematics Genealogy Project: https://www.genealogy.math.ndsu.nodak.edu/
(authoritative for physics, math, some bio)
- Faculty bios on the candidate's lab / department page
(often state "PhD under Prof. X, year")
Match types (Connection v2 strengths):
Same PhD advisor (academic siblings) → "same_advisor" (0.65)
Advisor is PhD sibling / nephew of candidate → "uncle_nephew" (0.50)
Genealogy is a meaningful historical signal, but weaker than verified
recent working contact. This is why v2 lowered same_advisor from
v1's 1.0 to 0.65 — a shared PhD advisor decades ago tells you less than
a small-team coauthored paper in the last 5 years (which saturates at
1.0). Don't over-weight lineage in your narrative to the user.
Don't infer from name patterns / institutional history alone. If
Mathematics Genealogy returns nothing and the faculty bio doesn't mention
the lineage, leave the genealogy edge empty.
Editorial / committee co-membership (weaker signal): only count when
you've found documented evidence (a journal masthead, NSF panel report,
conference PC list). → committee_co_member: true, same_period: bool
Take the MAX of these edges, do NOT sum. The matcher treats them as
mutually exclusive (avoids double-counting).
If no edge found via search, record what you searched with a
supports_fields=["path:<advisor_id>"] item — strict mode requires this
verified-empty form (bare URLs in sources won't pass strict):
"paths_to_advisors":{"adv_001":{"items":[{"url":"https://scholar.google.com/citations?user=...&q=Wang+candidate","source_type":"google_scholar","claim":"searched 2020-2024: 0 co-authored papers, no shared lineage","supports_fields":["path:adv_001"]}],"note":"also checked Math Genealogy Project — neither party in DB"}}
The C score reduces cleanly to field strength only when no edges are
found. An empty paths_to_advisors[adv_id] = {} is missing data
(silently penalized) — prefer the verified-empty form above so the
matcher can credit you for searching.
Step 5 — Advisor influence signals (per candidate, drive the A dimension)
Properties of the candidate themselves — not about your connection to
them. These feed the A pillar in the 5-dim CAPEG match formula
(roadmap #3). Three-state semantics: each field is either
verified-with-sources, verified-empty (value remains null/false
sources documenting the search), or omitted (no value, no sources →
counts as unverified).
The A composite, reputation-only post-roadmap-#6a (sums to 1.0):
active_funding_quality and pi_signal (recruiting health) are no
longer A components — they live on OpportunitySignal and feed the
O dimension (drives opportunity_adj, not the match score). See
Step 5.5 for where to put those fields.
Fields:
normalized_collab_top20pct (0–1, default null): proxy via candidate's
h-index from Google Scholar or OpenAlex. Formula: min(1.0, h_index / 50).
Cite the profile URL in evidence["normalized_collab_top20pct"].items
with supports_fields=["normalized_collab_top20pct"]:
collab_with_nas (bool, default null): three-state, strict about
semantics:
null (default) — you didn't search the NAS / HHMI directory; no
claim. The signal counts as missing.
false — you searched and confirmed no recent NAS / HHMI co-author.
Record evidence with supports_fields=["collab_with_nas"] citing
the searched directory pages; this counts as verified-empty.
true — you found a specific recent co-author in the official NAS
or HHMI directory. Cite the directory match in evidence.
grad_placement_quality (0–1, default null): only set if you read the
lab's "alumni" / "former students" section. Top faculty placements: 0.8+,
academia + industry mix: 0.5–0.7, mostly post-docs: 0.4. If no alumni
page exists, leave as null (do not fall back to 0.5 — that's a
fake default; the matcher widens the band on its own).
active_funding_quality — fill on opportunity_signal, not the top-
level candidate (see Step 5.5). The top-level field is kept for
backward compatibility but is no longer part of A. Field semantics
(0–1 scale, NIH RePORTER / NSF Award Search / DOE / ERC / DARPA
citations, R01+CAREER ≈ 0.85, single small grant ≈ 0.4, verified-
empty 0.0, null if not searched) apply to either location.
Discipline-specific elite signals (use collab_with_nas=true and cite):
Don't fill in fake defaults when you didn't check. A 0.5 written into
the JSON without sources counts as unverified — same as null without
sources — but pretends to be a real signal. The matcher's confidence band
will widen either way; honesty in the JSON helps the user read the result.
Step 5.5 — Opportunity signal (roadmap #6a — replaces pi_adj in app_strength)
After A's reputation signals are gathered, optionally fill
candidate.opportunity_signal with the time-sensitive admit-cycle
availability data. The matcher derives opportunity_adj from this and
uses it in place of the v1 pi_adj term inside application_strength.
Critical: post-roadmap-#6a, A is reputation-only. active_funding_quality
and pi_signal no longer feed A — they live on OpportunitySignal.
opportunity_signal fields:
pi_signal (mirrors legacy top-level — wins via field-by-field merge if !="missing")
Set fields need evidence with supports_fields=["opportunity:<field>"]
in opportunity_signal.evidence[<field>]. Legacy
evidence["pi_signal"] and evidence["active_funding_quality"]
forms still satisfy strict mode for migration.
When opportunity_signal is omitted entirely, the matcher takes the
v1 PI_ADJ legacy path on the top-level pi_signal only — preserving
exact old behavior. Full schema, formula, and ladder in
references/opportunity.md.
Step 6 — Recruiting signal (pi_signal)
Fetch the candidate's lab / faculty page (don't assume from memory).
Read the current-students list, "join the lab" page, or "applying" notes:
"strong" — page shows ≥2 new PhDs/yr in last 3 yrs (large turnover, growing group)
"normal" — 1–2/yr based on listed timeline
"shrinking" — <1/yr, or many recent graduations without new admits
"missing" — page didn't load, didn't have a students list, or status unclear
"not_recruiting" — explicitly stated on the page. Forces application_strength = 0.
Default to "missing" whenever you didn't actually fetch and read the
page. The matcher penalizes missing data slightly (−0.1) but never
makes up a status.
Optionally fill candidate.program_profile with program-level
difficulty signals. The matcher's primary sort key is now
difficulty_adjusted_strength = risk_adjusted_strength − program_difficulty_penalty, and the 5-tier label is applied to it
(not to raw application_strength).
The penalty (0–0.8) combines school_tier admit-rate, cohort size,
admission model (rotation vs direct-admit), funding structure, faculty
count in subfield, and international friendliness. Each set field
needs evidence with supports_fields=["program:<field>"]. Full schema,
formula, and components in
references/program_profile.md.
When program_profile is null, only the school_tier factor contributes
(top_10=0.70, top_11_30=0.50, top_31_60=0.30, top_60+=0.00) — this is
the v2 replacement for the v1 tier_adj term, which has been removed.
Step 6.5 — Research fit (roadmap #4 — tie-breaker, NOT a pillar)
After C / A / P / E / G fields are populated and before the matcher
is invoked, optionally compute a research_fit_score per candidate.
This is a 0–1 alignment between the student's research_direction and
the candidate's actual recent work.
It is not part of the match formula — it does not move match_score
or application_strength. It only breaks ties in the sort order when
two candidates land at the same risk_adjusted_strength. The connection-
first thesis is preserved.
The fit fields live on the same CandidateAdvisor JSON record alongside
the other signals, so they must be filled in before piping candidates to
scripts/match.py. Fields:
research_fit_score (0–1, or null if you didn't compute one)
research_fit_summary (short prose, e.g., "5 of last 8 papers on H→cc̄")
research_fit_axes (per-axis breakdown — see the field-axis table
below; values must be in [0, 1] or Pydantic rejects the candidate)
Use the loaded FieldProfile's research_fit_axes to decompose the score
honestly:
If a candidate uses an axis key not declared by the active FieldProfile,
the matcher emits a warning into input_warnings (the score still goes
through; it just flags the drift).
Schema:
{"research_fit_score":0.78,"research_fit_summary":"5 of last 8 papers on H→cc̄, primary detector matches","research_fit_axes":{"subfield":0.95,"detector_or_technique":1.0,"process_or_topic":0.8,"experiment_vs_theory":1.0,"collaboration":0.5},"evidence":{"research_fit":{"items":[{"url":"https://scholar.google.com/citations?user=...","source_type":"google_scholar","claim":"5 of last 8 papers in 2022-2024 on H→cc̄ analysis","supports_fields":["research_fit"]}]}}}
Strict mode: research_fit_score != null without
supports_fields=["research_fit"] evidence → rejected. Leaving
research_fit_score as null is allowed and is not counted in
evidence coverage — a null fit does not widen the confidence band; it
just shows as "not computed" in the result card. Don't write a fake
0.5 placeholder to "look complete" — that becomes an unsourced claim
and hurts the candidate.
Step 6.7 — Audit evidence quality (optional, before strict run)
Before invoking the matcher in --strict-evidence mode, run
scripts/audit_candidates.py to surface every fixable evidence gap in
one pass:
input_warnings — paper-role conventions and axis-key drift (same as in match output)
Use this when finalizing a school list — fix the high severity entries
first, then medium. The matcher's strict mode also produces these errors,
but the audit CLI lets the user see the full repair workload before
deciding whether to fix or fall back to default mode.
Before invoking audit_candidates.py and the matcher, run
scripts/collect_evidence.py to auto-fill structured evidence (paths,
research_areas, most_recent_connection_year) from external sources:
# Live OpenAlex enrichment (recommended for real applications):
python scripts/collect_evidence.py \
--profile-file /tmp/profile.json \
--candidates-file /tmp/cands.json \
--field <FIELD> \
--live --mailto <your-email> \
--out /tmp/enriched.json
# Then audit + match the enriched JSON:
python scripts/audit_candidates.py \
--candidates-file /tmp/enriched.json \
--field <FIELD> --strict-evidence
The collector uses OpenAlex (cross-STEM) and emits structured
EvidenceSource items with supports_fields so strict mode passes.
See references/evidence_collection.md
for the adapter interface, fixture format, and what v1 fills.
Step 7 — Run matcher
Two modes, depending on what the user is doing:
For real PhD-application decisions (recommended): use --strict-evidence.
Strict mode rejects any candidate with claim-level evidence missing — the
errors list which fields and where to cite. This is the right mode when
the user is finalizing a school list:
If strict fails, fix the evidence and re-run — don't fall back to
default mode silently. Tell the user which candidates couldn't be
strictly verified, then either gather the missing evidence or warn them
explicitly that those candidates' rankings are based on unverified claims.
For exploratory drafts (default mode): omit the flag. Default mode
accepts legacy bare URLs and missing signals; the confidence band widens
and risk_adjusted_strength drops accordingly. Useful for first-pass
brainstorming, but say so in the result presentation.
Output is a JSON list of MatchResult records (candidate, c/p/e/g sub-scores,
match_score, application_strength, confidence_band, strength_label,
risk_adjusted_strength, lower_bound, missing_signals, unsourced_signals,
total_signals, missing_signal_names, unsourced_signal_names, explanation).
Step 8 — Present results
This is the user-facing rendering layer. The matcher's JSON is the
source of truth; you (the agent) translate it into per-candidate
cards. Cards are short, scannable, and product-grade — not debug
dumps. Power users / strict-mode auditors can still inspect the full
match.json separately.
The per-candidate card format
Render each ranked candidate using exactly this template (markdown
headings, fixed field order):
# <rank>. <Name> — <Institution>
Label: <strength_label> Strategy: <apply_bucket> / <recommended_action>
difficulty_adjusted: <D> application_strength: <Y> ±<band>
Why ranked here:
- <2–4 bullets, each citing a real source>
Main risks:
- <plain-English missing/unsourced/blocked signals — see Step 8.5>
Next action:
<one sentence drawn from strategy.outreach_angle / evidence_to_fix /
recommended_action — concrete, actionable>
Real example (using the physics_hep_audit_demo output):
# 1. Prof. Alex Hartman — MIT
Label: Reach Strategy: target / contact_first
difficulty_adjusted: 2.20 application_strength: 3.30 ±0.80
Why ranked here:
- Strong connection: 3 small-team coauthored papers with your advisor
Prof. Wang in 2022–2024 (OpenAlex)
- Research direction overlaps ATLAS Higgs / detector ML — 5 of last 8
papers on H→cc̄ topics (OpenAlex recent_works)
- Top-10 program difficulty penalty is high (school_tier=top_10 +0.70)
Main risks:
- school_tier is unsourced (used as input, no ranking page citation)
- pi_signal / grad_placement_quality / active_funding_quality are
missing (lab and alumni pages not searched)
- Confidence band wide (±0.8) — lower_bound is 2.50
Next action:
Email Prof. Hartman. Lead with the shared small-team coauthorship
through Prof. Wang on the H→cc̄ work — that's your strongest verified
edge. Mention you're applying for fall 2027 and ask whether the lab
is actively recruiting; this fills the missing pi_signal.
Field-by-field rendering rules
Title line: # <rank>. <Name> — <Institution> (markdown H1; the rank is the post-#5 sort order).
Label / Strategy line: <strength_label> is applied to difficulty_adjusted_strength (not raw application_strength); <apply_bucket> is the strategy enum (priority / target / reach / only_if_space / drop); <recommended_action> is apply / contact_first / investigate_evidence / deprioritize / skip.
Numbers line: only show difficulty_adjusted (the actual sort key) and application_strength ±band. Do not dump c/a/p/e/g scores in the user-facing card — they go in the JSON appendix.
Why ranked here: 2–4 bullets, each one citing a real source (URL or named page). Pull from the matcher's explanation field but rewrite into product language. Skip pillars that are at floor (no evidence to cite).
Main risks: translate unsourced_signal_names / missing_signal_names / blocked-source attempts into plain English — see §"How to talk about missing signals to the user" for the canonical template. Always mention band width when ±band ≥ 0.6.
Next action: drawn from strategy.outreach_angle (when set) or strategy.evidence_to_fix (top item). Always concrete — name the person to contact, the page to fetch, the field to fill.
Power users invoke phdtaketaketake-match directly and read the JSON.
QClaw / Claude Code users see only the cards.
Top-of-output portfolio summary
Before the per-candidate cards, surface the portfolio rollup in one short
paragraph (drawn from strategy_summary.portfolio_notes):
N candidates analyzed: <p> priority · <t> target · <r> reach ·
<o> only_if_space · <d> drop. <one-sentence verdict on portfolio shape —
e.g., "no priority bucket fills yet — main blocker is missing pi_signal
across the board".>
If the run output's input_warnings is non-empty (co_first used in a
field that doesn't recognize the convention, profile-level data
issues), surface them right after the portfolio paragraph, before the
first candidate card.
Mandatory product-boundary footer
Every results render — even a one-candidate quick check — must end with this exact disclaimer (zh + en, since the audience spans both):
This is a 4.0-scale relative application-strength index, not an
admission probability. Missing or blocked sources widen the
confidence band instead of being guessed.
这是一个 4.0 制的相对申请强度指数,不是录取概率。证据无法验证或来源被拦截
时,confidence band 会变宽,而不是被猜测填充。
Do not drop this footer to save tokens. It is the product's most
important boundary statement — it stops users from misreading
Target / Reach / Match as actual admit probabilities, and it
explains why some candidates have wide bands.
Per-claim source requirement (still hard)
Every factual claim in the "Why ranked here" bullets must include its source. This requirement is unchanged from prior versions — the card structure is just a wrapper around evidence-cited prose.
Examples of good vs bad bullet phrasing:
✅ "co-authored 4 papers with Prof. Wang in 2022–2024 (OpenAlex; latest: PRL 130, 2023)"
✅ "co-PI on ATLAS Higgs subgroup since 2017 (INSPIRE-HEP collaboration tracking)"
✅ "academic siblings — both PhD'd under H. Georgi at Harvard (Math Genealogy Project)"
Re-run with --strict-evidence after fixing the unsourced claims?
Step 8.5 — How to talk about missing signals to the user
The matcher reports missing / unsourced / verified-empty signals as
namespaced JSON field names (path:adv_001, program:cohort_size_estimate,
pi_signal, etc.). Do not show those raw field names to QClaw / Claude
Code users. Translate them into plain English using the template below.
The four states (canonical)
Internal state
What happened
What to tell the user
Verified
searched, found a value, cited URL
(don't surface — implicit when bullet has source)
Verified-empty
searched, page said "no result"
"I confirmed via that there's no for this PI" — counts as evidence, narrows the band
Missing
didn't search this signal
"I haven't checked for yet — you can ask me to fill it in"
"I tried to verify but blocked the request — counts as missing; you can paste the page text or a screenshot"
The blocked state is not a system failure; it's expected — Google
Scholar throws CAPTCHAs, US News has a paywall, some school CDNs
(Cloudflare) refuse fetches. The skill is designed to handle this:
blocked sources widen the confidence band rather than getting filled
with guesses. See references/data_integrity.md
§"Blocked / timeout / CAPTCHA is not verified-empty" for the policy.
The Main risks template (canonical wording)
When rendering "Main risks:" in a per-candidate card (Step 8), use this
shape — translating namespaced signals to plain English, grouping by
state, and offering the recovery path:
Main risks:
- I couldn't verify the following signals — they're counted as missing
and widen the confidence band:
• school_tier — ranking page (US News graduate program ranking) was
blocked by a login wall
• pi_signal — lab page would not load (Cloudflare challenge / 403)
• grad_placement_quality — no alumni / "former students" page found
- This is not a negative conclusion; the matcher just has less to go on.
You can manually paste lab page text, an alumni list, or a screenshot
of the ranking page and I'll re-run with that as evidence.
Specific signal → plain-English mapping
Namespaced signal
Plain English
path:<adv_id>
"verified path between this PI and your advisor "
school_tier
"school's program ranking"
pi_signal
"whether the PI is actively recruiting (lab page check)"
grad_placement_quality
"alumni placement track record"
active_funding_quality
"PI's active grants (NIH RePORTER / NSF Award Search)"
collab_with_nas
"NAS / HHMI co-author"
normalized_collab_top20pct
"PI's h-index / collaboration percentile"
program:cohort_size_estimate
"incoming cohort size"
program:admission_model
"rotation vs direct-admit"
program:funding_structure
"funding model (guaranteed vs on-paper)"
program:faculty_count_in_area
"number of faculty in your subfield"
program:international_friendliness
"international-student friendliness"
research_fit
"research-direction overlap"
opportunity:lab_open_positions
"open PhD slots this cycle"
opportunity:application_contact_policy
"preferred contact channel (email / through program / do-not-contact)"
Three things to never say
❌ "Error 403 on lab.stanford.edu" (raw HTTP error — not user-facing)
❌ "evidence is a bit thin" (vague; the user can't act on it)
❌ "h_index unavailable, defaulting to 0.5" (invented default — never; the matcher does not do this)
Three things to always say when surfacing blocked sources
✅ name the signal in plain English (not the namespaced field)
✅ name the source that was blocked (so the user knows what to manually provide)
✅ name the recovery — paste page text, paste a screenshot, give a URL the agent can fetch
This is the canonical user-facing language. Use it consistently across
cards, portfolio rollup, and follow-up questions.
CV optimization (parallel workflow — Sprint-7)
Beyond advisor matching, the skill ships a LaTeX CV template + compile pipeline so the same conversation can produce a polished PhD-application CV. This is a parallel workflow to advisor matching — they share the same install but trigger independently.
When to invoke this workflow
Trigger on any of:
"帮我做 / 整理 / 优化 / 改进 我的 CV / 简历"
"make / improve / format my CV"
"tailor my CV for <target PI> / <target program>"
"我有一份 match.json,帮我针对前 3 个 PI 做 tailored CV"
The user pastes a CV (LaTeX or otherwise) and asks for review / optimization
Two trigger paths:
Generic CV — user wants a clean PhD-application CV, no specific target. Run the template-fill flow; skip tailoring.
Tailored CV — user provides a match.json (or asks to tailor against earlier matching output). Fill the template, then reorder + prune for top-bucket candidates.
If the user invokes "make my CV" without naming a target, ask once:
Do you have target advisors / programs in mind? If you've already run advisor matching, paste the match.json and I can tailor the CV for the top candidates. Otherwise I'll produce a general PhD-app version.
Don't re-ask. If they say no or skip, proceed with generic.
Step CV-1 — Read the bundled template
phdtaketaketake-cv-template --print > cv.tex
# or, just print the path so you can Read / Edit it directly:
phdtaketaketake-cv-template
# /…/phd_matcher/cv/templates/default.tex
Read it carefully before editing. Note the contract:
Preamble + custom commands (\resumeSubheading, \resumeItem, \resumeItemWithoutTitle, \resumeItemListStart/End, \resumeSubHeadingListStart/End) — never edit. The % DO NOT EDIT comment marks the boundary.
All sections present by default: Education / Research Experience / Publications & Presentations / Technical Skills / Teaching Experience / Leadership Experience / Honors and Awards. Delete any that don't apply (whole \section{...} block + its wrapper). Empty section headers look worse than missing sections.
OPTIONAL_BLOCK_START / END markers wrap content that's off by default — currently the Relevant Coursework table (under Education) and the multi-affiliation extra-emails block (under Header). Enable by removing the % prefix on each line between the markers.
<angle-bracket> placeholders mark every fill-in slot. The as-shipped file compiles to a "demo CV" with placeholders rendered as text — useful to verify your LaTeX install works before personalizing.
Step CV-2 — Gather user info
Ask in plain English, section by section, not all at once. Don't dump the template into the chat — that overwhelms the user. Probe like a CV consultant:
Header: full name, primary email, secondary emails (multi-affiliated users only), personal website (optional).
Education: institution(s), degree, GPA + scale, location, dates. Ask: "Do you want a Relevant Coursework table? It helps for physics / theory CS / applied math; less common for experimental bio / chem."
Research experience: per project — title, mentor, institution, dates, 2–4 bullets describing what you built / proved / measured + methods + tools + outcome / contribution. Probe for verbs: avoid "responsible for", prefer "implemented", "demonstrated", "produced", "developed".
Publications: split into Selected Papers (published / accepted), Work in Progress (drafted / in prep), Posters & Talks. If a sub-category is empty → delete it (don't leave empty headers).
Technical skills: 2–3 named categories (Programming / Field Software / Languages). Comma-separated lists.
Teaching / Leadership / Honors: ask if relevant; delete the section entirely if not.
The user may not give everything in one round. That's expected. Fill what you have, leave clearly-marked TODO comments in the .tex for empty fields, ask follow-ups for the most important gaps first (Research Experience > Publications > Skills > others).
Step CV-3 — Fill the template
⚠️ Source-of-truth invariant — read first
Every fact in the CV must trace to something the user explicitly provided in this conversation. This is the CV sub-skill's analogue of the matcher's evidence-first contract.
Allowed source for CV content
Example
✅ User typed it in conversation
"my advisor is Prof. Wang at Tsinghua" → Mentor: Prof. Wang, Tsinghua University
✅ User pasted from their old CV / Overleaf / ORCID
preserve as-is, just reformat
✅ User uploaded a PDF / screenshot and confirmed contents
quote from the user-supplied file
✅ Suggest deletion of demonstrably weak user-typed items (vague skill listings like "Microsoft Office", "various AI tools"; stale or off-topic experiences)
always flag the deletion in the handoff so the user can re-add. Never silently drop content.
❌ A CandidateAdvisor from match.json
target PIs are ranking signals, not CV content
❌ A research_areas field from a candidate record
this is the matcher's view of the target PI, not the user's experience
❌ Names / institutions surfaced by collect_evidence web research
these belong to candidate enrichment, not the CV
❌ Anything the agent infers from training memory
guessed publications, GPA scales, lab names
On the "suggest deletion" path: this is the one direction the agent
is allowed to trim user-supplied content (still strict no-invention
on adding). The trigger is content that any CV consultant would flag —
generic "Microsoft Office" / "All kinds of AI tools" / "Detail-oriented
team player" filler, or experiences clearly off-topic for a PhD
application. The non-negotiable rule: always tell the user what you
removed in the Step CV-6 handoff, in a form they can act on:
I removed these items because they read as filler for a PhD-application
CV — let me know if you want any of them back:
Skills line: "Microsoft Office, Google Docs"
Experience: "Cashier at Starbucks (Summer 2022)"
If anything here was actually load-bearing (e.g., the cashier role
was your funding source during a specific period), tell me and I'll
put it back.
Never silently drop content. The user should always be able to recover
their full input.
The most subtle violation: a target PI from match.json (e.g., Prof. Hartman at MIT) accidentally landing in the CV body as a co-author or mentor. Target PIs never appear inside cv.tex. They appear in match.json (the matcher's output) and they drive ordering decisions (Step CV-4), but their names, institutions, and research areas stay there.
If you find yourself about to write a person, institution, paper, or skill into cv.tex and you can't point at the user message that introduced it, stop and ask the user instead of writing it.
How to actually fill it
Use Edit / Write to produce a populated cv.tex. Replace every <placeholder> with content from the user (per the invariant above). LaTeX special-character escaping is required for user-typed text:
User text
LaTeX form
& (ampersand, e.g. "R&D")
\&
% (percent, e.g. "5% improvement")
\%
_ (underscore, e.g. file names)
\_
# (hash, e.g. "channel #1")
\#
$ (dollar, e.g. "$8110 award")
\$
{}
\{\}
~
\textasciitilde{}
^
\textasciicircum{}
\
\textbackslash{}
URLs inside \href{} and \url{} already escape correctly — no manual work needed there.
If the user wants the Relevant Coursework table: between OPTIONAL_BLOCK_START: Relevant Coursework and OPTIONAL_BLOCK_END: Relevant Coursework, remove the leading % from every line, then fill the courses. Keep the START / END marker comments themselves so the structure is recoverable.
If a section doesn't apply to the user (no Teaching, no Leadership), delete the entire \section{...} block including its \resumeSubHeadingListStart / End wrapper.
Step CV-4 — (Optional) Tailor for target PI(s)
Only when the user provided a match.json or asked for tailoring against earlier matching output. The full per-section playbook is in references/cv_optimization.md §"Tailoring playbook". Quick summary:
Research Experience order — read top-bucket candidates' research_areas. For each user experience, judge overlap. Strong-overlap projects move to the top; weak/none + > 2 years old → consider deleting (ask the user first).
Bullets within an experience — lead with the bullet whose methods / detector / dataset overlap target PI's recent papers.
Publications — same. Lead with paper most overlapping target's subject.
Technical skills — re-order each comma-separated list to put target-lab tools first (e.g. for a Geant4-heavy LDMX-style group, put Geant4 before MadGraph5).
Tailoring is reordering and user-approved pruning, never invention. Never add an experience, paper, or skill the user didn't actually do.
⚠️ Target PIs stay in match.json — they never enter cv.tex
The target PI from match.json (e.g. Prof. Hartman at MIT) is the recipient of the CV, not its content. Tailoring uses the target's research_areas / c_score / research_fit_axes as ranking signals to decide which of the user's own experiences and papers to surface — but the target's name, institution, and research areas stay inside match.json. They do not appear as text inside cv.tex.
The mentor / advisor / co-author / institution names insidecv.tex come exclusively from the user's own profile (StudentProfile.current_advisors, experiences[].mentor, paper author lists they typed) — same as in Step CV-3.
If you're tailoring for Hartman and the user's actual current advisor is Wang, the CV says "Mentor: Prof. Wang" (the user's reality); Hartman is who you're sending it to, not who you're listing on it.
Step CV-5 — Compile to PDF
phdtaketaketake-cv-compile cv.tex
# → cv.pdf in the same directory
The compile CLI prefers latexmk -pdf (handles cross-reference multi-pass internally); falls back to pdflatex running up to 3 passes. Three exit modes:
Exit code
Status
What to do
0
ok
PDF produced. Show the user the path.
1
failed (TeX install OK; .tex has an error)
The CLI prints diagnostic lines (! LaTeX Error:, l.<num>, "Undefined control sequence", etc.) extracted from the .log. Most common cause: an unescaped LaTeX special char in user text — re-check the \& / \% / \_ / \# / \$ escapes in Step CV-3 and re-run.
2
tex_not_installed
The CLI prints install hints for macOS / Debian / Fedora / Windows + a note that minimal TeX installs (BasicTeX, TinyTeX) may need tlmgr install titlesec enumitem. Always offer Overleaf as the universal fallback — paste the .tex, compile in-browser.
3
input error
File not found, etc. Verify the path.
On persistent compile failure, do not try to "fix" the LaTeX by guessing. Surface the diagnostic to the user, ask them to paste the offending source line back so you can escape it correctly, or hand them the raw cv.tex + Overleaf link.
Step CV-6 — Hand off to the user
Show:
The .pdf path (or, on compile failure, the .tex content + Overleaf link)
A 2–3 sentence summary of any tailoring decisions you made (the user needs to review and may push back on order / deletions)
A short review checklist:
GPA scale matches your records (3.85/4.0 vs 88/100)
Paper venues + author orders are current
Dates consistent (no overlap that doesn't make sense)
Email addresses spelled correctly
Standard product-boundary disclaimer (zh + en, same as advisor matching):
This is a CV format + reorder helper, not a content-quality / SoP / recommendation-letter assistant. The substance of your experiences and the strategic narrative of your application are your responsibility.
The matcher's confidence_band widens as the count of unverified signals
grows. A signal counts as verified iff it has at least one
EvidenceSource in items whose supports_fields includes that signal's
field name. Bare sources: list[str] URLs are accepted in default mode
(legacy compat) but rejected in --strict-evidence mode — only
structured items with matching supports_fields count.
Signal
Verified means
path:<adv.id>
for every non-default sub-field on PathEdge (small_team_coauthor_5y, big_collab_papers_5y, same_working_group, …), items contains an EvidenceSource with that field in its supports_fields
school_tier
evidence["school_tier"].items has an EvidenceSource with "school_tier" in supports_fields
research_areas
evidence["research_areas"].items has an EvidenceSource with "research_areas" in supports_fields
same per-field rule, signal name in supports_fields
pi_signal
value is non-"missing" AND evidence["pi_signal"].items has matching supports_fields
Unverified count
Confidence band
0
±0.2 (fully sourced)
1–2
±0.4
3–4
±0.6
5+
±0.8 (mostly unverified)
Per the cardinal rule, a non-default claim without claim-level evidence
is forbidden. Default mode tolerates it (wide band, low risk-adjusted
rank); strict mode rejects it outright. Don't game the band — the
explainer cites only items whose supports_fields matches the claim, so
attaching unrelated URLs doesn't help.
If you searched and verifiably found nothing, record that as evidence
with the right field bound:
"paths_to_advisors":{"adv_001":{"items":[{"url":"https://scholar.google.com/citations?user=...","source_type":"google_scholar","claim":"0 co-authored papers found in 2020–2024","supports_fields":["small_team_coauthor_5y","big_collab_papers_5y"]}],"note":"searched OpenAlex + Scholar; no overlap"}}
This counts as verified empty (0 unverified for that path) — strictly
better than no entry (1 unverified) or a bare-URL sources: [...] (which
fails strict mode).
Important constraints
NEVER FABRICATE. This is the cardinal rule (see top of file). If you
searched and didn't find a signal, mark it missing — never guess. See
references/data_integrity.md for the full forbidden-behavior catalog.
Cite sources in the explanation for every verified edge / signal.
Don't double-count school prestige. It's encoded in connection_score
and lab_tier (for student experiences). Don't add a separate "school
bonus" on top.
For big-collab papers (ATLAS / CMS / large clinical trials / consortia)
use the actual author position even if it's 100+. The 5+ rule handles
them correctly.
Don't refuse to run the match if some signals are missing. Run it,
surface the gaps in the explanation, widen the confidence band.
References
When the user asks deeper questions, read the relevant doc:
references/data_integrity.md — allowed sources + forbidden behaviors. Read this first if you're new to the skill.