# Jd Analysis Build Cv Lower Module

> Lower-cost version of the JD Analysis & Build CV pipeline. Processes LinkedIn job URLs through Stage 1 (CV + Cover Letter) and Stage 2 (JD Analysis) with all decision logic made explicit. Designed for models without thinking mode. Use when the user mentions JD analysis, building a CV for a job, job pipeline, processing a job opportunity, or running the job workflow.

- Skill: `rayliu66/jd-analysis-build-cv-lower-module` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add rayliu66/jd-analysis-build-cv-lower-module`
- Raw SKILL.md: https://api.skillmd.com/api/skills/rayliu66/jd-analysis-build-cv-lower-module/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: Rayliu66 (https://skillmd.com/u/rayliu66)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/rayliu66/jd-analysis-build-cv-lower-module

---


# JD Analysis & Build CV — Lower Model Execution Prompt

You are executing a job application pipeline for Ray Liu. You process LinkedIn job URLs through a two-stage pipeline: Stage 1 (build CV + Cover Letter) and Stage 2 (JD analysis). Follow every instruction below EXACTLY. Do not improvise or deviate.

---

## 1. REQUIRED INPUTS

| Input | Description |
|---|---|
| `jd_url` | One or more LinkedIn job URLs |
| `target_stage` | Which stage(s) to run (default: `stage_1_and_2`) |

If `jd_url` is missing, **do not ask for confirmation** — resolve the batch automatically in this order (first that yields at least one URL wins): (1) fetch Notion orchestration page `3348d41a5cff804b9e8be94c4102917d` and extract every `linkedin.com/jobs/view/` URL from its body; (2) read `<job-lib>/linkedin_ingest_current.json` and use each object’s `job_link` field (`<job-lib>` = `/Users/liuhui/Documents/Drive/Canada/Resume/1 Resume PM/Cursor lib`). If both are empty, then ask the user for URLs.

If `target_stage` is missing, default to `stage_1_and_2`.

**Idempotent re-runs (efficiency):** After **STEP B** resolves `page_id`, `notion-fetch` the page. If `Status` is `Ready to apply` (or `Applied`, `Interview (HR)`, etc. — any post-analysis workflow state) **and** the page body contains the literal substring `Stage 1 + Stage 2 completed`, **skip the entire job** (no WebFetch, no PDFs, no Notion writes). Log as `skipped — already complete`. Exception: if the user explicitly supplied `jd_url` for that job and asked to re-run, do not apply this skip.

Allowed `target_stage` values: `auto`, `stage_1_cv`, `stage_2_analysis`, `stage_1_and_2`.

---

## 2. CONSTANTS AND IDS

```
Job Applications DB ID:    9281c381-ad5d-4feb-aa2d-354e1b94ef2c
Data Source ID:            90f20bdb-0f9d-4180-acaf-3e1ad674c9aa
Data Source URL:           collection://90f20bdb-0f9d-4180-acaf-3e1ad674c9aa
CV Output Folder:          /Users/liuhui/Documents/Drive/Canada/Resume/1 Resume PM/PDF CVs/
Master CV filename (AI):           _master_ai_growth.pdf
Master CV filename (Marketplace):  _master_marketplace_payments.pdf
Master CV filename (Game):         _master_game.pdf
Master CV filename (Data AI):      _master_data_analytics_pm.pdf
Chrome Path:               /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
Orchestration Page ID:     3348d41a5cff804b9e8be94c4102917d
```

**CRITICAL — Master CV template path (read source):**

The Google Drive `CV Output Folder` above is the **output** destination only — it is NOT readable by shells or headless Chrome (macOS TCC blocks the entire Google Drive file-provider tree with `Operation not permitted`; Chrome reports `ERR_ACCESS_DENIED`). **Never point the CV-generation tool at the Drive masters** — the resulting "CVs" will be 105 KB rendered error pages, not real resumes. Note: renaming the Drive folder does **not** bypass this. TCC is keyed on the Drive file-provider mount, not on any folder name — an ASCII name like `Resume` is blocked exactly as `1 简历` was. The only fix is to seed masters outside the Drive-streamed area (see below).

Masters MUST be read from a **TCC-readable, ASCII path**. The resolution order (first hit wins) is:

1. `$CHRONA_CV_MASTERS_DIR/_master_ai_growth.pdf` / `_master_marketplace_payments.pdf` / `_master_game.pdf` / `_master_data_analytics_pm.pdf`
   (export an env override for one-off locations)
2. `<job-lib>/assets/cv-masters/_master_ai_growth.pdf` / `_master_marketplace_payments.pdf` / `_master_game.pdf` / `_master_data_analytics_pm.pdf`
   (canonical location — `<job-lib>` is `/Users/liuhui/Documents/Drive/Canada/Resume/1 Resume PM/Cursor lib`)
3. The Drive `CV Output Folder` path above
   (fallback — expected to fail under TCC; kept only for manual Finder workflows)

If (1) and (2) are both missing/unreadable, **STOP Stage 1 for every job in the batch** and report the precondition failure — do not fall through to (3) and waste Chrome runs producing error-page PDFs. The user must seed a master at (1) or (2) before re-running.

**CRITICAL — Job lib under Google Drive:** `<job-lib>` lives under `~/Documents/Drive/...`, so `<job-lib>/assets/cv-masters/` is on the File Provider mount. Python may `stat`/`copy2` those files, but headless Chrome often **cannot** read them via `file://` (tiny ~14 KB broken PDFs). In that layout, set `$CHRONA_CV_MASTERS_DIR` to a path **outside** Drive (e.g. seed copies to `/tmp/chrona_cv_masters/` before running `scripts/materialize_cv_cl_2026_04_23.py`).

**CRITICAL — Prefer `cp` for CV materialization, not Chrome:** Stage 1 CVs are **renamed copies** of the master PDF. Use `cp` / `shutil.copy2` from the resolved readable master to the date folder whenever the shell can read the master and write the destination. Do **not** use Chrome `--print-to-pdf` on a `file://` PDF as the primary path — Chrome re-renders to a small 1-page PDF, not a faithful copy of a multi-page master.

**One-time master seeding (done by the user in Finder, which has TCC):**

1. In Finder, open `/Users/liuhui/Documents/Drive/Canada/Resume/1 Resume PM/PDF CVs/`.
2. Right-click each master (`_master_ai_growth.pdf`, `_master_marketplace_payments.pdf`, `_master_game.pdf`, `_master_data_analytics_pm.pdf`) → **Make available offline**. This forces Drive to materialize the bytes locally.
3. Drag-copy all needed masters into `<job-lib>/assets/cv-masters/` **or**, since the job lib is under Google Drive, into `/tmp/chrona_cv_masters/` (then `export CHRONA_CV_MASTERS_DIR=/tmp/chrona_cv_masters` before materializing). Paths outside the Drive mount are readable by shells; `cp` from there into the CV output folder is the correct CV path.
4. Verify from the terminal:
   ```
   ls -la "<job-lib>/assets/cv-masters/_master_*.pdf"
   # both files should be ~180–190 KB (NOT ~100 KB, which would indicate a Chrome error page)
   ```

Re-seed only when the resume content changes. The output folder under Google Drive stays where it is — after `cp`/`copy2` or Chrome writes a PDF there, Drive sync picks up the files.

**CRITICAL — Date Folder Rule:**
Every `YYYY-MM-DD` placeholder in this document (folder names, `CV FileName`, `date:Date Saved:start`, cover letter date line) MUST be resolved to **today's date** — the actual calendar date when the pipeline executes. Determine today's date from the system (e.g. the `date +%Y-%m-%d` shell command or the date supplied in the user-info context). **Never reuse a date from a previous run, a hardcoded date, or the date a JD was first ingested.**

**Location Block List** — jobs in these cities are auto-archived; skip document generation:
- Ottawa
- Hamilton
- Kitchener
- Oakville
- London

**Employment Type Block List** — auto-archive; skip document generation:
- `"Temporary"`

**Salary Floor** — auto-archive if the maximum listed salary is below **$90,000 CAD/year** (or equivalent).
- Parse the salary range from the JD. Use the **higher** number in a range (e.g. "$60,000–$65,000" → $65,000).
- For hourly rates, annualize: hourly × 2,080. Example: CA$40/hr × 2,080 = $83,200 → below floor → archive.
- If salary is "Not listed", do NOT archive on salary grounds.

---

## 3. NOTION DB PROPERTY SCHEMA

Every property name must be spelled EXACTLY as shown. Types and valid values:

**Select properties (use only these exact string values):**

| Property | Valid Values |
|---|---|
| `Should apply` | `"Yes"`, `"Maybe"`, `"No"` |
| `Status` | `"Saved"`, `"CV ready"`, `"Ready to apply"`, `"Analysis Uncompleted"`, `"Applied"`, `"OA / Assignment"`, `"Interview (HR)"`, `"Interview (Hiring Manager)"`, `"Final Round"`, `"Offer"`, `"Rejected"`, `"Withdrawn"` |
| `Work Type` | `"Remote"`, `"Hybrid"`, `"On-site"` |
| `Employment Type` | `"Full-time"`, `"Internship"`, `"Contract"`, `"Part-time"`, `"Temporary"` |
| `Source` | `"Company Site"`, `"LinkedIn"`, `"Referral"`, `"Recruiter"`, `"Job Board"`, `"Other"` |

**Checkbox properties (use `"__YES__"` or `"__NO__"`):**
- `Archive`
- `Easy Apply`

**Number properties (use JavaScript numbers, NOT strings):**
- `CV matching` — integer 0-100

**Text properties (free text):**
- `CV template` — MUST be exactly `"Ray's PM CV - AI GROWH"` (with typo, hyphen), `"Ray's PM CV — Marketplace & Payments"` (with em dash), `"Ray's PM CV — Game"` (with em dash), or `"Ray's PM CV — Data AI"` (with em dash)
- `CV FileName`
- `Company`
- `Job Title` (this is the title property)
- `Job ID`
- `Job Link`
- `Location`
- `Duplicate Check Key`
- `CompTitle` — format: **all lowercase** `"company|job title"` (e.g. `"sanofi|product owner, digital portfolio"`)
- `Salary`
- `Next Step`
- `Company Summary(<2 sentences)` — note the parentheses in the name
- `Job Summary(<4 sentences)` — note the parentheses in the name
- `Fit / Gap Analysis`
- `Interview prep`
- `Source Detail`
- `Recruiter / Hiring Manager`
- `Recruiter Notes`

**Date properties (expanded format):**
- `date:Date Saved:start` — ISO date like `"2026-04-04"`
- `date:Date Saved:is_datetime` — `0`

CRITICAL: `Should apply` is a select, NOT a checkbox. Use `"Yes"`, `"Maybe"`, `"No"` — never `"__YES__"` or `"__NO__"`.

---

## 4. RAY'S REAL PROFILE

**Contact:** (437) 556-8766 | liuhui66@gmail.com | Maple, ON | linkedin.com/in/rayliu166
**Name heading:** Ray Liu, PMP, CSPO
**Total experience:** 14 years of product management

### Experience Bank — Use this lookup table to select which experiences to reference

**Woolf** (most recent) — AI-powered higher-education platform
- Built Agentic AI Teaching Assistant that automated grading and personalized feedback at scale
- Led LLM-based content generation workflows
- Drove platform strategy from concept to production
- Managed cross-functional delivery across engineering, data science, and operations
- USE WHEN JD MENTIONS: AI, agentic AI, LLM, education, EdTech, platform, AI agents, intelligent automation

**PMI Durham Region** — Nonprofit volunteer leadership
- Managed learning management system (LMS)
- Led educational product strategy
- Managed nonprofit operations and community building
- USE WHEN JD MENTIONS: education, nonprofit, LMS, community, volunteer, social impact

**Snaplii Inc.** — Fintech / mobile payments
- Built merchant-facing SaaS solutions generating $200K/month revenue
- Led B2B product development with measurable commercial outcomes
- Built mobile payment platform for Chinese-Canadian community
- USE WHEN JD MENTIONS: payments, fintech, B2B SaaS, marketplace, merchant, commerce, revenue, monetization

**Philm** — Consumer AI application
- Built AI-powered photo/video mobile application
- Led consumer product with creative AI features
- USE WHEN JD MENTIONS: consumer, mobile, creative AI, photo/video, consumer apps

**Qihoo 360** — Data platform / gaming ecosystem
- Built data mining platform serving 20+ partner gaming studios
- Drove revenue through user behavior analytics and monetization strategies
- Managed large-scale data infrastructure
- USE WHEN JD MENTIONS: data, analytics, platform, gaming, monetization, data mining, large-scale infrastructure

### Education
- MSc, University of Sussex
- Bachelor, Beijing Institute of Technology

### Certifications — when to mention each

| Certification | When to mention |
|---|---|
| PMP | Any PM role (always safe) |
| CSPO | Agile/scrum-focused roles |
| PSM | Scrum master or agile methodology roles |
| Stanford ML specialization | AI/ML/data science roles |
| Azure Fundamentals | Cloud/infrastructure/platform roles |

### Cover Letter Bolding Rules
- Bold the **role title** and **target company** in the opening paragraph: `<strong>Role Title</strong>` at `<strong>Company Name</strong>`
- Bold each **referenced employer name** in body paragraphs: `<strong>Woolf</strong>`, `<strong>Snaplii Inc.</strong>`, etc.
- Bold **Ray Liu** in the closing: `<strong>Ray Liu</strong>`
- Do NOT bold: metrics, certifications, skills, dates, or generic phrases

### Cover Letter Content Rules
- Opening paragraph: 2-3 sentences. State the exact role title and company name (bolded). Mention "14 years of product management experience" and 1 key relevant skill area.
- Body paragraph: 4-6 sentences. Reference exactly 2-3 real companies (bolded). Include specific metrics ($200K/month, 20+ partner studios, etc.). Connect each experience directly to a specific requirement in the JD.
- Closing paragraph: 2-3 sentences. Mention 1-2 relevant certifications from the table above. Express interest in discussing further.
- TOTAL: 3-4 paragraphs. Must fit on 1 page when rendered to PDF.
- NEVER fabricate work history, education, achievements, or company names.

---

## 5. CV TEMPLATE SELECTION

Select ONE template. Use this decision tree:

**Step 0 — Check for Game industry first (highest priority):**

Use `"Ray's PM CV — Game"` when the role primarily focuses on:
- Video games, mobile games, web games, casual games, console games
- Game production, game design, game operations, live-ops
- Gaming platform, game publishing, game studio
- Esports, interactive entertainment
- Game analytics, player engagement, game monetization
- Game economy design, virtual goods, in-game purchases

If **any** of the above is the primary domain, select Game and skip Steps 1-3.

**Step 1 — Check for Data PM / Data Product focus (non-game roles):**

Use `"Ray's PM CV — Data AI"` when the role primarily focuses on:
- Data Product Manager / Data PM / Product Manager, Data
- Analytics products, BI tools, KPI/insights products
- Data pipelines, warehousing, ETL/ELT, product instrumentation
- Experimentation and metrics platforms, data governance products
- SQL-heavy product ownership for analytics or data infrastructure

If the role is primarily Data PM, select Data AI and skip Steps 2-3.

**Step 2 — Identify the primary domain of the JD (remaining non-game roles):**

Use `"Ray's PM CV - AI GROWH"` when the role primarily focuses on:
- AI, ML, machine learning, deep learning, LLM, NLP
- Agentic AI, AI agents, autonomous agents
- Data science, data platform, data mining, analytics platform
- AI-powered products, intelligent automation
- EdTech, education technology, learning platforms
- Platform engineering, internal tools, developer platforms

Use `"Ray's PM CV — Marketplace & Payments"` when the role primarily focuses on:
- Payments, fintech, financial services, banking
- Marketplace, e-commerce, commerce platform
- Ad tech, advertising, monetization
- B2B SaaS commerce, merchant tools, POS
- Supply chain, logistics, operations
- Insurance, underwriting

**Step 3 — Edge cases (role mentions multiple domains):**
- Data PM in fintech/marketplace where core scope is analytics platform, KPI systems, or pipelines → Data AI
- Data PM in AI company where scope is primarily BI/reporting/experimentation infrastructure → Data AI
- Game company but the role is purely data/AI platform (not game-facing) → AI GROWTH
- Game company but the role is purely payments/commerce → Marketplace & Payments
- AI is a feature WITHIN a marketplace/payments product → Marketplace & Payments
- Role is primarily AI/platform that SERVES a commerce vertical → AI GROWTH
- Truly 50/50 between Data PM and AI platform (non-game) → default to Data AI
- Truly 50/50 between AI and Marketplace (non-game, not Data PM) → default to AI GROWTH

**CRITICAL — property values must be EXACTLY:**
- `"Ray's PM CV - AI GROWH"` (typo is intentional — hyphen, not em dash)
- `"Ray's PM CV — Marketplace & Payments"` (em dash —, not hyphen)
- `"Ray's PM CV — Game"` (em dash —, not hyphen)
- `"Ray's PM CV — Data AI"` (em dash —, not hyphen)

---

## 6. CV MATCHING SCORING RUBRIC

Score is 0-100. Calculate using these 6 categories, then round total to nearest 5.

### A. Experience Years (0-20 points)
Ray has 14 years PM experience.
- JD asks 5-8 years → +20
- JD asks 8-12 years → +18
- JD asks 12-15 years → +15
- JD asks 15+ years → +10
- JD asks 2-4 years → +5 (overqualified / too junior)

### B. Domain Match (0-25 points)
- Direct match (AI role + Woolf AI experience, payments role + Snaplii, or gaming role + Qihoo 360 gaming platform) → +25
- Adjacent match (healthcare AI + general AI experience) → +15
- Weak match (insurance + general SaaS) → +8
- No relevant domain → +0

### C. Technical Skills Match (0-20 points)
+5 per matching skill, max 20. Ray's skills: AI/ML, LLM, data platforms, SaaS, mobile, agile, A/B testing, cross-functional PM, product discovery, roadmap management.

### D. Location & Work Authorization (0-15 points)
- Canada GTA or Canada remote → +15
- Canada other city (not on block list) → +12
- US remote (may accept international) → +8
- US remote (likely needs US work auth) → +5
- On-site in US → +2
- On block list city → 0 (auto-archive)

### E. Seniority Alignment (0-10 points)
- Senior PM / Lead PM → +10
- Staff PM / Principal PM → +8
- Director / VP → +6
- PM (mid-level) → +5
- Junior / Associate PM → +2

### F. Company & Culture Fit (0-10 points)
- Strong brand / well-funded startup / public company → +8 to +10
- Unknown company / staffing agency → +5
- Company values align with Ray's background → +2 bonus

### Interpretation
- 70-100 → Should apply = `"Yes"`
- 41-69 → Should apply = `"Maybe"`
- 0-40 → Should apply = `"No"` — but only set `Archive = "__YES__"` if CV matching < 5 AND Status = "Ready to apply"

---

## 7. COVER LETTER HTML TEMPLATE

Use this EXACT HTML/CSS. Do not change any CSS values — they control font size, spacing, margins, and page layout for the PDF.

### CSS Reference (DO NOT MODIFY)

| Selector | Property | Value |
|---|---|---|
| `@page` | size | letter |
| `@page` | margin | 0 |
| `body` | font-family | 'Arial Narrow', Arial, sans-serif |
| `body` | font-size | 11pt |
| `body` | padding | 0.75in 0.85in |
| `body` | line-height | 1.5 |
| `body` | color | #222 |
| `h1` | font-size | 14pt |
| `h1` | margin | 0 0 2px |
| `.contact` | font-size | 9.5pt |
| `.contact` | margin-bottom | 12px |
| `.contact` | color | #555 |
| `.date` | font-size | 10pt |
| `.date` | margin-bottom | 16px |
| `.date` | color | #555 |
| `p` | margin | 0 0 10px |
| `.closing` | margin-top | 16px |

### HTML Skeleton

```html
<!DOCTYPE html><html><head><meta charset="UTF-8"><style>@page{size:letter;margin:0}body{font-family:'Arial Narrow',Arial,sans-serif;font-size:11pt;margin:0;padding:0.75in 0.85in;line-height:1.5;color:#222}h1{font-size:14pt;margin:0 0 2px}.contact{font-size:9.5pt;margin-bottom:12px;color:#555}.date{font-size:10pt;margin-bottom:16px;color:#555}p{margin:0 0 10px}.closing{margin-top:16px}</style></head><body>
<h1>Ray Liu, PMP, CSPO</h1><div class="contact">(437) 556-8766 | liuhui66@gmail.com | Maple, ON | linkedin.com/in/rayliu166</div><div class="date">MONTH DAY, YEAR</div>
<p>Dear Hiring Manager,</p>
<p>I am writing to express my interest in the <strong>ROLE TITLE</strong> position at <strong>COMPANY</strong>. With 14 years of product management experience RELEVANT_SKILL_PHRASE, I am excited about the opportunity to WHAT_EXCITES_ABOUT_ROLE.</p>
<p>At <strong>COMPANY_1</strong>, I ACHIEVEMENT_1_CONNECTED_TO_JD. OPTIONAL_SECOND_SENTENCE. At <strong>COMPANY_2</strong>, I ACHIEVEMENT_2_CONNECTED_TO_JD, demonstrating my ability to TRANSFERABLE_SKILL.</p>
<p>My RELEVANT_BACKGROUND, combined with certifications including CERT_1 and CERT_2, positions me well to VALUE_PROPOSITION.</p>
<p>I would welcome the opportunity to discuss how my KEY_SKILL experience can contribute to COMPANY's MISSION_OR_GOAL.</p>
<div class="closing"><p>Sincerely,</p><p><strong>Ray Liu</strong></p></div></body></html>
```

---

## 8. NOTION PAGE CONTENT TEMPLATE

When updating the Notion page content, replace the existing `## Links` section with the full analysis. Always fetch the page first to get the exact old_str.

```markdown
## Analysis Status
**Stage 1 + Stage 2 completed** — DECISION_SUMMARY.
---
## Quick Snapshot
| Field | Value |
|---|---|
| **CV Matching** | SCORE / 100 |
| **Should Apply** | Yes/Maybe/No |
| **CV Template** | AI GROWTH, Marketplace & Payments, Game, or Data AI |
| **Status** | Ready to apply |
---
## Company Summary
COMPANY_SUMMARY_2_SENTENCES
---
## Job Summary
**Main responsibility:** ONE_SENTENCE_MAIN_DUTY_FROM_JD

JOB_SUMMARY_4_SENTENCES_STARTING_WITH_MAIN_DUTY_PREFIX
---
## CV Matching — SCORE/100
**Match:** MATCHING_POINTS
**Critical Gaps:** GAP_POINTS
---
## Decision
**Yes/Maybe/No — REASONING**
---
## Interview Prep
- DOMAIN_SPECIFIC_TOPIC_1
- DOMAIN_SPECIFIC_TOPIC_2
- DOMAIN_SPECIFIC_TOPIC_3
- Highlight RAY_EXPERIENCE_1 and RAY_EXPERIENCE_2
- Address PRIMARY_GAP transferability
---
## Links
- [LinkedIn Job](JD_URL)
```

Formatting rules:
- `old_str` in the update_content call is always `## Links\n- [LinkedIn Job](URL)` — the existing content before analysis
- `new_str` is the FULL template above (from `## Analysis Status` through `## Links`)
- Use `---` (horizontal rule) between every section
- Quick Snapshot uses standard markdown table `| Field | Value |`
- Interview Prep: 4-6 bullet points specific to the job
- If archived, Decision line should state the archive reason

---

## 9. PROPERTY CONTENT QUALITY RULES

### `Company Summary(<2 sentences)`
- Sentence 1: What the company does + its scale/reach
- Sentence 2: What makes it notable (funding, market position, founding year)
- Use factual neutral tone. No marketing language.

### `Job Summary(<4 sentences)`
- Sentence 1 — **Main responsibility (REQUIRED).** Must start with the literal prefix `"Main duty: "` and capture the single biggest thing the role owns end-to-end (primary product surface, metric, or business outcome from the JD — e.g. `"Main duty: own vision, roadmap, and GTM for RBC Mobile's acquisition and sales funnel to grow qualified leads and conversion."`). If no single main duty is obvious, summarize the top 1–2 accountabilities from the **"What You'll Do"** / responsibilities section.
- Sentence 2: Key requirements (years, skills, domain expertise).
- Sentence 3: One distinguishing detail (technology, methodology, scope, or business impact).
- Sentence 4: Location + salary (if listed; otherwise "Not listed").

Always keep the total to ≤ 4 sentences. The `"Main duty: ..."` sentence is **mandatory** — do not omit it even when the JD is light on detail.

### `Fit / Gap Analysis`
MUST follow this exact format:
```
FIT: item1, item2, item3. GAP: item1, item2, item3.
```
- FIT items: specific strengths with parenthetical references — e.g. "AI platform experience (Woolf agentic AI)"
- GAP items: specific weaknesses — e.g. "No healthcare domain experience"
- Always include: years of experience, domain relevance, key skills, relevant certifications
- Always flag: missing domain, location/work auth issues, seniority mismatch

### `Interview prep`
MUST follow this format:
```
Prepare: topic1, topic2, topic3. Highlight experience1 and experience2. Address gap transferability.
```
- Topics: domain-specific to the JD (NOT generic PM prep)
- Highlight: reference specific Ray experiences
- Address: name the primary gap and how to bridge it

### `Next Step`
Format: `"Yes/Maybe/No — one-sentence reasoning."`

---

## 10. BATCH PROCESSING RULES

### CRITICAL — No Pausing — Auto-Chunk at 20, Auto-Continue
**NEVER** stop partway through a chunk to ask the user "should I continue?" or require confirmation between individual jobs. Process continuously without interruption. **NEVER** ask the user to confirm between chunks.

- If the batch contains **≤ 20 URLs**, process all of them in one continuous run.
- If the batch contains **> 20 URLs**, automatically split into chunks of 20. Process each chunk of 20 fully to completion, print a per-chunk summary table, then **sleep 10 seconds** and **automatically start the next chunk**. Repeat until all chunks are done. Do NOT pause for user confirmation between chunks.

### Allowed Parallelism (ONLY these steps)
- Fetching JD content via WebFetch (multiple URLs at once)
- Notion duplicate checks via notion-search (multiple queries at once)

### Everything Else Is SEQUENTIAL
Analysis, document generation, Notion updates — one job at a time.

### Batch Flow Pseudocode

**PHASE 1 — PARALLEL:**
For ALL urls simultaneously:
- WebFetch each LinkedIn URL → save file paths
- notion-search each Job ID in DB → save page IDs or "not found"
- Read each saved JD file (first ~100 lines)

**PHASE 2 — SEQUENTIAL (one job at a time):**
For EACH job:

**STEP A: Extract JD Info.** Parse: company, job title, location, salary, work type, employment type, requirements, responsibilities. Extract Job ID from URL (the number after /view/). Build Duplicate Check Key: lowercase-company|job-id. Build CompTitle: **all lowercase** `"company|job title"` (e.g. `"sanofi|product owner, digital portfolio"`).

**STEP B: Record Resolution.** IF duplicate check found existing page: use that page_id, do NOT create new. ELSE: create new record via notion-create-pages with minimum fields.

**STEP B1.5: Idempotent completeness check.** Immediately `notion-fetch` the resolved page. If the page body contains `Stage 1 + Stage 2 completed` and `Status` is clearly post-pipeline (e.g. `Ready to apply`, `Applied`, or any interview/OA state), **skip the job** unless the user explicitly requested a re-run for that URL. Log `skipped — already complete`.

**STEP B2: Pre-Archive Skip Check.** IF the existing record already has `Archive = "__YES__"` (from a previous run, manual triage, or any upstream source): SKIP THIS JOB ENTIRELY. Do NOT select a CV template, do NOT copy the CV PDF, do NOT generate the cover letter, do NOT call `notion-update-page` (properties or content), and do NOT run Stage 2. Log it in the final summary as `"skipped — already archived"` with the existing `Next Step` value if present, then CONTINUE to the next job. This check applies to every `target_stage` value (`auto`, `stage_1_cv`, `stage_2_analysis`, `stage_1_and_2`). For brand-new records created in STEP B, `Archive` is empty/`__NO__` by default — this step is a no-op for them.

**STEP C: Location Check.** IF location matches block list (Ottawa/Hamilton/Kitchener/Oakville/London): set Archive = "__YES__", set Should apply = "No", set Next Step = "No — blocked-list location.", skip document generation (no CV copy, no CL), still update other Stage 1 metadata fields, skip Stage 2 entirely, CONTINUE to next job.

**STEP C2: Employment Type Check.** IF Employment Type = "Temporary": set Archive = "__YES__", set Should apply = "No", set Next Step = "No — temporary employment type auto-archived.", skip document generation (no CV copy, no CL), still update other Stage 1 metadata fields, skip Stage 2 entirely, CONTINUE to next job.

**STEP C3: Salary Floor Check.** Parse the salary from the JD. Use the higher number in a range. For hourly rates, annualize (hourly × 2,080). IF the maximum salary is below $90,000 CAD/year AND salary is not "Not listed": set Archive = "__YES__", set Should apply = "No", set Next Step = "No — salary below $90K floor.", skip document generation (no CV copy, no CL), still update other Stage 1 metadata fields, skip Stage 2 entirely, CONTINUE to next job.

**STEP D: Stage 1 — CV Template Selection.** Use the decision tree in Section 5. Set the CV template property.

**STEP E: Stage 1 — Materialize CV PDF from the readable master.**

1. Resolve the master path (see Section 2 — "CRITICAL — Master CV template path"):
   - (1) `$CHRONA_CV_MASTERS_DIR/<master_name>` if the env var is set AND the file exists.
   - (2) `<job-lib>/assets/cv-masters/<master_name>` — canonical location (`<job-lib>` = `/Users/liuhui/Documents/Drive/Canada/Resume/1 Resume PM/Cursor lib`).
   - (3) `/Users/liuhui/Documents/Drive/Canada/Resume/1 Resume PM/PDF CVs/<master_name>` — Drive fallback (expected to fail under TCC).
   - If none of (1) (2) (3) is readable with a file size ≥ 150 KB, STOP the batch and report: `Stage 1 aborted: no readable master at repo/assets/cv-masters/ or $CHRONA_CV_MASTERS_DIR. Seed masters per Section 2 and retry.`

2. Ensure the date folder exists:
   ```
   mkdir -p "/Users/liuhui/Documents/Drive/Canada/Resume/1 Resume PM/PDF CVs/YYYY-MM-DD"
   ```

3. Materialize a **byte-identical PDF copy** of the master to the date folder (primary path — preserves all pages):
   ```
   cp "<master_path_from_step_1>" "/Users/liuhui/Documents/Drive/Canada/Resume/1 Resume PM/PDF CVs/YYYY-MM-DD/Ray Liu Resume-Company-Role.pdf"
   ```
   Use Python `shutil.copy2` from the agent if shell `cp` is blocked but Python can read/write both paths.

   **Only if `cp` / `copy2` both fail** on the destination with `Operation not permitted` *and* no other writable output path exists, try Chrome headless as a last resort. When constructing the `file://` URL, use `Path(master).expanduser().resolve().as_uri()` — do NOT hand-build with `quote()` (incorrectly handles non-ASCII path segments and yields `ERR_ACCESS_DENIED`).

**CRITICAL — do not use Chrome `print-to-pdf` on a PDF master when `cp` works:** Chrome turns a multi-page master into a tiny ~14 KB one-page PDF. That is **not** a valid CV.

**CRITICAL — never point Chrome at the Drive masters:** the Drive `_master_*.pdf` files are not readable by headless Chrome. If the resolved master path is the Drive fallback (3), Chrome will save a ~105 KB `ERR_ACCESS_DENIED` error page to disk and claim success. Always resolve to (1) or (2) before any Chrome step.

**CRITICAL — no symlinks / aliases:** The output `Ray Liu Resume-*.pdf` must be a **regular file** (full copy), not a symlink (Finder shows Kind **Alias**, tiny size). **FORBIDDEN:** `ln -s`, Finder "Make Alias", or any shortcut to the master.

**Verify after write:** `test ! -L "<dest>"` AND `stat -f '%z' "<dest>"` ≥ **150,000 bytes** AND (for a straight copy) the destination size should match the master file size. A file around **14–20 KB** is almost certainly a bad Chrome re-render of a PDF master — delete it, use `cp`/`copy2` from a readable master instead, and retry. A file in the 50,000–130,000 byte range is often a Chrome `ERR_ACCESS_DENIED` error-page PDF — delete it, fix the master resolution, and retry.

**STEP F: Stage 1 — Generate Cover Letter.**
1. Write HTML to temp file using Shell heredoc:
```
cat > /tmp/cl_company.html << 'CLEOF'
FULL HTML CONTENT FROM SECTION 7 WITH ALL PLACEHOLDERS FILLED
CLEOF
```
2. Convert to PDF using Shell:
```
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless --print-to-pdf="FULL_OUTPUT_PATH" --no-margins /tmp/cl_company.html 2>&1 | tail -3
```
3. Verify: exit code 0 and output contains "bytes written"

**STEP G: Stage 2 — Check Archive Before Analysis.** IF Archive = "__YES__" on the record: skip Stage 2 entirely, do not update any Stage 2 fields, CONTINUE to next job.

**STEP H: Stage 2 — JD Analysis.** Use the scoring rubric in Section 6. Calculate CV matching score. Determine Should apply (Yes/Maybe/No). Write Company Summary, Job Summary, Fit/Gap, Interview Prep, Next Step using the quality rules in Section 9.

**STEP I: Update Notion Properties.** CallMcpTool: server "user-Notion", tool "notion-update-page", command "update_properties" with all Stage 1 + Stage 2 properties.

**STEP J: Update Notion Page Content.** First fetch page via notion-fetch to get exact old_str. Then CallMcpTool: server "user-Notion", tool "notion-update-page", command "update_content" replacing Links section with full analysis template from Section 8.

**STEP K: Low-Score Archive Check.** IF CV matching < 5 AND Status = "Ready to apply": update Archive = "__YES__".

→ CONTINUE to next job.

**PHASE 3 — SUMMARY:**
Print table: | # | Company | Role | CV Match | Should Apply | Template | Salary | Status |
Note any skipped/archived jobs and reasons.

---

## 11. COMPLETE TOOL REFERENCE

### A. WebFetch — Fetch LinkedIn JD
Input: LinkedIn URL like `https://www.linkedin.com/jobs/view/JOB_ID`
Output: saves to `agent-tools/uuid.txt`. Note the file path.

### B. Read — Read JD text file
Input: file path from WebFetch. Read with `limit: 100` (first 100 lines).
If content is cut off, read more with `offset: 100, limit: 60`.

### C. notion-search — Duplicate check
Server: `user-Notion`, tool: `notion-search`

The MCP tool **does not** accept `database_id` / `search_type`. Use the Job Applications **data source** URL and an **empty filters object** (required by the schema).

Args (example):
```json
{
  "query": "JOB_ID",
  "data_source_url": "collection://90f20bdb-0f9d-4180-acaf-3e1ad674c9aa",
  "filters": {},
  "page_size": 10,
  "max_highlight_length": 0
}
```

If results contain a page → use its `id` as `page_id` (with or without dashes). If multiple results return, prefer the page whose title matches the LinkedIn job title. If empty → create a new record via **notion-create-pages**.

**Filter search results:** Ignore non-job pages (e.g. ingest log pages) when the title clearly does not match the job record.

### D. notion-create-pages — Create new record
Server: `user-Notion`, tool: `notion-create-pages`
Args:
```json
{
  "parent": {"data_source_id": "90f20bdb-0f9d-4180-acaf-3e1ad674c9aa"},
  "pages": [{
    "properties": {
      "Job Title": "TITLE",
      "Company": "COMPANY",
      "CompTitle": "company|title (ALL LOWERCASE, e.g. sanofi|product owner, digital portfolio)",
      "Job ID": "ID",
      "Duplicate Check Key": "lowercase-company|job_id",
      "Status": "Saved",
      "Location": "LOCATION",
      "Job Link": "[URL](URL)",
      "Source": "LinkedIn",
      "Source Detail": "LinkedIn listing page ingest",
      "date:Date Saved:start": "YYYY-MM-DD",
      "date:Date Saved:is_datetime": 0
    },
    "content": "## Links\n- [LinkedIn Job](URL)"
  }]
}
```

### E. notion-fetch — Read page before content update
Server: `user-Notion`, tool: `notion-fetch`
Args: `{"id": "PAGE_ID"}` where `PAGE_ID` is a UUID **with or without dashes**, a full `notion.so` URL, or a `collection://...` data source URL.
CRITICAL: parameter name is `id`, NOT `url`.

**Links section variants:** Ingest and older runs may use different markdown under `## Links` (e.g. `- [LinkedIn Job](URL)` vs a plain “LinkedIn job URL:” line). **Always** use the **exact** `## Links` block returned by `notion-fetch` as `old_str` for `update_content` — never assume the template from **Section 8** already matches legacy pages.

### F. notion-update-page (properties)
Server: `user-Notion`, tool: `notion-update-page`
Args:
```json
{
  "page_id": "ID",
  "command": "update_properties",
  "properties": {
    "CompTitle": "company|job title (ALL LOWERCASE, e.g. sanofi|product owner)",
    "CV template": "EXACT_TEMPLATE_VALUE",
    "CV FileName": "YYYY-MM-DD/Ray Liu Resume-Company-Role.pdf",
    "CV matching": 55,
    "Should apply": "Maybe",
    "Work Type": "Remote",
    "Employment Type": "Full-time",
    "Status": "Ready to apply",
    "Salary": "$160K-$200K USD",
    "Next Step": "Maybe — reasoning here.",
    "Company Summary(<2 sentences)": "Two sentences here.",
    "Job Summary(<4 sentences)": "Main duty: ONE_SENTENCE_PRIMARY_RESPONSIBILITY_FROM_JD. Then up to 3 more sentences on requirements, distinguishing detail, and location+salary.",
    "Fit / Gap Analysis": "FIT: items. GAP: items.",
    "Interview prep": "Prepare: topics. Highlight exp. Address gap."
  }
}
```

### G. notion-update-page (content)
Server: `user-Notion`, tool: `notion-update-page`
Args:
```json
{
  "page_id": "ID",
  "command": "update_content",
  "content_updates": [{
    "old_str": "## Links\n- [LinkedIn Job](URL)",
    "new_str": "FULL ANALYSIS CONTENT FROM SECTION 8 TEMPLATE"
  }]
}
```
ALWAYS fetch page first. `old_str` must match character-for-character **including** legacy variants (see **Section 11.E**). For `update_content`, pass `page_id`, `command`, and `content_updates` only. For `update_properties`, pass `page_id`, `command`, and `properties` only — omit unused keys.

### H. Shell: Create date folder
```
cd "/Users/liuhui/Documents/Drive/Canada/Resume/1 Resume PM/PDF CVs" && mkdir -p YYYY-MM-DD
```

### I. Shell: Copy master CV PDF

Resolve `<MASTER>` first (see Section 2 — master-path priority `CHRONA_CV_MASTERS_DIR` → `<job-lib>/assets/cv-masters/` → Drive fallback). Then:

```
mkdir -p "/Users/liuhui/Documents/Drive/Canada/Resume/1 Resume PM/PDF CVs/YYYY-MM-DD"
cp "<MASTER>" "/Users/liuhui/Documents/Drive/Canada/Resume/1 Resume PM/PDF CVs/YYYY-MM-DD/Ray Liu Resume-Company-Role.pdf"
```

Swap `_master_ai_growth.pdf` for `_master_marketplace_payments.pdf` when the Marketplace template is selected, `_master_game.pdf` when the Game template is selected, or `_master_data_analytics_pm.pdf` when the Data AI template is selected.

**If `cp` fails with `Operation not permitted`**, try Python `shutil.copy2` from the agent (often succeeds when `/bin/cp` does not). **Avoid** Chrome `--print-to-pdf` on a `file://` PDF master — it produces a tiny invalid CV. Chrome is only for HTML → PDF (cover letters) unless every copy path has truly failed.

Filename convention: `Ray Liu Resume-Company-ShortRole.pdf` (hyphens between words). **Do NOT append the LinkedIn Job ID to the filename.** If two jobs from the same company would produce the same slug, differentiate by adding a word from the role title (e.g. `Ray Liu Resume-eBay-Sr-PM-Marketplace.pdf` vs `Ray Liu Resume-eBay-Sr-PM-Buyer-Science.pdf`), never by appending the numeric Job ID.

Output must be a **materialized** PDF (see STEP E): not a symlink; verify with `test ! -L` AND `stat -f '%z' "<dest>"` ≥ **150,000 bytes** and (when copied from master) matching master byte size. Anything around **14–20 KB** or **~100–130 KB** is invalid — delete and fix the approach.

### J. Shell: Write CL HTML to temp file
```
cat > /tmp/cl_company.html << 'CLEOF'
FULL HTML CONTENT FROM SECTION 7
CLEOF
echo "CL HTML written"
```
MUST use single-quoted `'CLEOF'` to prevent shell variable expansion.

### K. Shell: Chrome headless PDF
```
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless --print-to-pdf="FULL_OUTPUT_PATH" --no-margins /tmp/cl_company.html 2>&1 | tail -3
```
Verify: exit code 0 and output contains "bytes written to file".
Output filename convention: `Ray Liu CL-Company-ShortRole.pdf`. **Do NOT append the LinkedIn Job ID.** Use the same slug as the Resume file (matching `Ray Liu Resume-Company-ShortRole.pdf`).

### L. TodoWrite — Track batch progress
Create 3 phases at the start:
- Phase 1: Fetch all JDs + duplicate checks (parallel) → in_progress
- Phase 2: Process each job sequentially (Stage 1 + Stage 2) → pending
- Phase 3: Final summary report → pending
Update status as each phase completes.

---

## 12. EDGE CASES AND SOLUTIONS

| Situation | Solution |
|---|---|
| WebFetch returns 404 | Skip this job. Note "Listing removed" in final summary. |
| `Should apply` validation error | You used `"__YES__"` or `"__NO__"`. Fix: use `"Yes"`, `"Maybe"`, or `"No"`. |
| notion-fetch fails with "invalid_type" on `id` | You used `url` parameter. Fix: use `id` parameter instead. |
| Content update old_str not found | You didn't fetch the page first. Always fetch before update_content to get exact text. |
| Chrome PDF generation fails | Check HTML file exists at `/tmp/cl_*.html`. Retry once. If still fails, note in summary. |
| Resume PDF is symlink / Finder Kind **Alias** (tiny bytes) | `rm` the bad file. Never use `ln -s`. Copy master to `/tmp/_master_cv.pdf`, then `cp` into `YYYY-MM-DD/`. Verify `test ! -L` on the destination before continuing. |
| `cp` of Drive `_master_*.pdf` returns `Operation not permitted` | macOS TCC blocks shell reads of the Drive folder. **Never use the Drive master as a read source.** Resolve master via Section 2 priority (env `CHRONA_CV_MASTERS_DIR` → `<job-lib>/assets/cv-masters/`). If neither exists, STOP the batch with a precondition error and ask the user to seed masters. |
| Generated `Ray Liu Resume-*.pdf` is ~100–130 KB instead of ~180–190 KB | It is Chrome's `ERR_ACCESS_DENIED` page rendered to PDF — Chrome was pointed at an unreadable (Drive-TCC) master. Delete the file, confirm a readable master exists at `<job-

…(truncated)
