fix(apply): record the drafted application in the tracker (#269) (#291)

/apply wrote a CV and a cover letter to disk and then wrote nothing to
job_search_tracker.csv, so a drafted and submitted application was
invisible to /gmail-sync, /html-report, /notion-sync, /interview,
/upskill aggregate mode, and to /rank's dedup exclusion. The safety net
that would have caught it - /gmail-sync - refuses to create missing
rows, so the failure it exists to catch is the one that disables it.
Nothing detected the loss afterwards.

Step 6b appends a drafted row carrying the two document paths, the fit
rating and the posting URL, reusing /outcome's exact header so the two
commands cannot diverge. It runs immediately after "Files Created" and
before the optional application-form offer, which ends the turn on a
question - anything placed after that offer would be skipped whenever
the user never answers, reproducing the bug. Re-running /apply updates
the row rather than duplicating it, and never moves a row that already
reached applied or beyond back to drafted. The step is mirrored into
job-application-assistant, which defers to it rather than restating it,
because /scrape Step 5 routes straight into the skill; /scrape Step 6
now defers to the same step instead of adding a row of its own.

seen_jobs.json is deliberately left alone: drafting is not applying, and
that file's vocabulary has no value for either. /rank builds its
exclusion set from company+role in the tracker regardless of status.

drafted is introduced into the status vocabulary, and every reader that
meant "submitted" is updated to say so. These readers define their open
set by exclusion from the final statuses, so a new non-final value would
otherwise have joined all of them silently: /outcome's follow-up branch
would have drafted a chase email to an employer who never received an
application, /gmail-sync would have searched for mail about it and then
flagged it as stale, /notion-sync would have published an "Applied on"
date for it, and /html-report would have counted it in the headline
application total. /outcome Step 4 also overwrites the draft date with
the submission date when a row leaves drafted, so the date column keeps
meaning "applied on". The wider vocabulary reconciliation - underscore
versus space, the separate archive enum - stays a separate concern.
This commit is contained in:
Jakob Stender Guldberg
2026-08-06 17:16:07 +02:00
committed by GitHub
parent cffacfdde0
commit 41b5fd857f
10 changed files with 296 additions and 20 deletions
+4 -4
View File
@@ -62,19 +62,19 @@ Validate the cheap, local precondition before creating anything external. A run
| Company | rich text | |
| Score | number | 0-100 from `rank_score` |
| Verdict | select | Strong Fit / Good Fit / Moderate Fit / Weak Fit / Poor Fit |
| Status | select | ranked / applied / interview / offer / hired / rejected / no response / withdrawn / expired |
| Status | select | ranked / drafted / applied / interview / offer / hired / rejected / no response / withdrawn / expired |
| Fit | select | high / medium / low (scraper quick-fit) |
| Deadline | date | omit when unknown |
| First seen | date | |
| Ranked | date | `rank_date` from `seen_jobs.json`; omit when not ranked |
| Applied on | date | tracker `date` column; omit when not in the tracker |
| Applied on | date | tracker `date` column; omit when not in the tracker, and omit when the status is `drafted` |
| Channel | select | tracker `channel` column (e.g. portal / email / referral); options grow as values appear |
| CV file | rich text | tracker `cv_file` column - the filename only, never document content |
| Cover letter | rich text | tracker `cover_letter_file` column - the filename only, never document content |
| URL | url | posting URL |
| Key | rich text | the job's key in `seen_jobs.json` - dedup anchor, never edited by hand |
The tracker-sourced properties (Applied on, Channel, CV file, Cover letter) stay empty for jobs that have no tracker row - they fill in once `/outcome` records the application. Only filenames ever sync; document contents stay local.
The tracker-sourced properties (Applied on, Channel, CV file, Cover letter) stay empty for jobs that have no tracker row. CV file and Cover letter fill in once `/apply` records the draft; Applied on stays empty until `/outcome` records the submission. Only filenames ever sync; document contents stay local.
4. **Existing database with missing properties:** if the located database predates a schema addition (a property from the table above does not exist), add the missing properties to the database before upserting. Never remove or retype existing properties.
5. Write `job_scraper/notion_sync.json` with the database id and URL. This file is personal state and is gitignored - never commit it.
@@ -98,7 +98,7 @@ Batch politely: if the MCP server rate-limits, back off and continue; report any
The page body is what makes a row worth clicking. Build it **only from stored data and actually fetched content**:
1. **Fit summary** - a short section from `seen_jobs.json` fields: score, verdict, quick-fit level, first-seen and ranked dates. If the job is in the tracker, add the application timeline (date applied, channel, current status, dated notes from the `notes` column) and name the submitted documents from `cv_file`/`cover_letter_file` (filenames only - the documents themselves never sync).
1. **Fit summary** - a short section from `seen_jobs.json` fields: score, verdict, quick-fit level, first-seen and ranked dates. If the job is in the tracker, add the application timeline (date applied, channel, current status, dated notes from the `notes` column) and name the submitted documents from `cv_file`/`cover_letter_file` (filenames only - the documents themselves never sync). **When the status is `drafted`, write "drafted YYYY-MM-DD, not yet submitted" instead of a date applied, and call the files drafts rather than submitted documents** (page bodies are write-once - Step 4.3).
2. **The posting** - WebFetch the job URL and write a readable digest: what the role is, key requirements, practical details (location, deadline, salary if stated). Retry a 403 with browser headers per `.claude/skills/job-application-assistant/09-web-research.md` first. If the fetch still fails or redirects to a listing page, write "Posting no longer available (checked YYYY-MM-DD)" - **never reconstruct a posting from memory**.
3. **Links** - the posting URL; if `documents/applications/<company>_<role>/` exists locally, name it as the local archive path (plain text - the destination cannot link into the filesystem).