26 Commits
Author SHA1 Message Date
Oscar Madera e92f7d9065 feat(setup): add documents/projects/ ingestion for independent projects (Path A) (#468) 2026-09-16 06:47:15 +02:00
Oscar Madera 88d1b61047 feat(apply): verify posting source host against installed portals and known ATS apexes (#431) (#467)
- Add source host verification rule to apply.md Step 1 before drafting
- Check posting URL host against installed portals and six official ATS apexes
- Enforce fail-closed look-alike parsing (prefix, suffix, userinfo spoofing)
- Plainly flag unverified third-party hosts in evaluation output
- Add tests/test_apply_host_check.py and update CHANGELOG.md
2026-09-16 06:43:16 +02:00
Oscar Madera 6b07b13bd2 feat(commands): add GitHub repository project extraction to /expand (#437) 2026-09-07 18:43:59 +02:00
Oscar Madera 6176e6aaca feat(outcome): add stale sweep branch for batch-resolving quiet applications (#439)
* feat(outcome): add stale sweep branch for batch-resolving quiet applications

- Add /outcome stale [N] and /outcome sweep [N] to Step 0
- Offer stale sweep in Step 1.3 when rows are quiet 60+ days
- Add Step 2c Stale Sweep Branch with interactive all/select/skip confirmation
- Batch-resolve qualifying open applications to canonical no_response
- Update tracker status and append dated notes; update archive outcome.md
- Add Rule 9 to Important Rules
- Add tests/test_outcome_stale.py and update CHANGELOG.md

* test(outcome): drop auxiliary candidate filter from spec test per review
2026-09-07 18:41:16 +02:00
f8c606fb3d fix(jobnet-search): fallback to search endpoint on detail 404 for external ads (#432) (#435)
* fix(jobnet-search): fallback to search endpoint on detail 404 for external ads (#432)

* fix(jobnet-search): mark external detail fallback degraded and omit unknown fields

* changelog: add the #432 entry for the external-ad fallback

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013fqqLgQSnwgWkv98twQhHi

---------

Co-authored-by: Mads Lorentzen <madslorentzen17@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 18:38:15 +02:00
Oscar Madera db8312948a test(scraper): pin the /scrape Step 2 search-output contract across portal CLIs (#344)
The Step 2 contract ('Search output already includes title, company,
location, date, and URL') had no cross-portal regression net: a CLI that
quietly drops a contract field flags the portal as degraded on every
/scrape run while CI stays green. That failure class landed for real
(jobnet/jobdanmark/jobbank, fixed in #339/#340/#342). The contract fields
are derived from the SKILL.md sentence itself (never hardcoded), compared
against the real search output of every .agents/skills/*-search CLI, and
detail.ts is deliberately excluded - the contract is about what /scrape
consumes. Fails against pre-fix master on exactly the three portals the
fixes cover; passes with them applied.
2026-08-19 21:23:46 +02:00
Oscar Madera df7919cdf5 fix(jobbank-search): emit date key derived from posted in search output (#342)
jobbank search output was missing the shared contract's date field; it now emits date as YYYY-MM-DD derived from posted (kept unchanged), null when the feed item has no pubDate. The inline result mapping is extracted into an exported normalizeSearchItem (behavior-preserving) so the derivation is pinned by tests.

Co-authored-by: oscarbol09 <80536682+oscarbol09@users.noreply.github.com>
2026-08-19 08:32:49 +02:00
Oscar Madera 17bc698697 fix(jobdanmark-search): emit the /scrape contract fields in search output (#340)
Adds additive normalization so jobdanmark search output carries the cross-portal contract fields: company from companyName, location as the city after the postal code in companyAddress (null-safe - a missing or null address yields null instead of crashing the search), and date/deadline converted from DD-MM-YYYY to YYYY-MM-DD with safe passthrough on unexpected formats. Native fields unchanged.

Co-authored-by: oscarbol09 <80536682+oscarbol09@users.noreply.github.com>
2026-08-19 08:32:06 +02:00
Oscar Madera 04186b9a37 fix(jobnet-search): emit the /scrape contract fields in search output (#339)
Adds additive normalization so jobnet search output carries the cross-portal contract fields (company, location, date, deadline with the 1900-01-01 NotDisclosed sentinel mapped to null, and url). Emits the public /find-job/{jobAdId} route and corrects the skill's own stale /job/ documentation, which redirects anonymous visitors into the MitID login flow.

Co-authored-by: oscarbol09 <80536682+oscarbol09@users.noreply.github.com>
2026-08-19 08:20:23 +02:00
Oscar MaderaandJakob Stender Guldberg 762d3218ef fix(html-report): read and render the tracker deadline column (#325)
/html-report was the one tracker consumer #319's deadline column left behind:
Step 1 now parses every canonical column and Step 3 renders Deadline after Date.
The drift guard derives CANONICAL_HEADER from apply.md itself, so a future
column added elsewhere but missing here fails with the column named; legacy
13-field rows read as empty deadline, never dropped, never inferred. Includes
rule-6 sweep refinements and deadline-reconciliation rules authored by
jakob1379.

Co-authored-by: Jakob Stender Guldberg <17257805+jakob1379@users.noreply.github.com>
2026-08-16 20:02:09 +02:00
Oscar MaderaandJakob Stender Guldberg c855e11d22 fix(tracker): persist the application deadline end to end (#319) (#324)
The deadline is written at every moment it is provably in hand and survives
every write that follows: seen_jobs.json base field, /rank stored-value urgency
+ expiry sweep with Step 4 persistence, tracker 14th column with header-line-only
migration for existing files, /scrape-path extraction (assistant SKILL.md 1.3.2
-> 1.3.3), preserve-unparsed-fields in /outcome and /gmail-sync, notion-sync
deadline precedence. Design, scope analysis, and the folded refinements by
jakob1379 (#319, #328).

Co-authored-by: Jakob Stender Guldberg <17257805+jakob1379@users.noreply.github.com>
2026-08-16 19:56:45 +02:00
Oscar Maderaandcamcro0607 c696b60b77 fix(cv-template): compile main_example.tex on apt-packaged moderncv (#242) (#323)
Routes name styling through \namefont (present on every moderncv version) and
hands hyperref to the class via \AtEndPreamble, fixing both 2.3.1 compile
failures; pdfpagemode pinned to UseNone so the hook move cannot flip viewer
behavior. Diagnosis and fix design by camcro0607 (#242); verified on real
Debian bookworm apt moderncv 2.3.1 by ayobamiseun; modern-toolchain and PDF
catalog verification on 2.5.1 at review time.

Co-authored-by: camcro0607 <172529990+camcro0607@users.noreply.github.com>
2026-08-15 21:31:41 +02:00
Oscar Madera 621ce5ab39 fix(convert-salary-excel): reject ambiguous dot thousands separators (#326) 2026-08-14 10:51:06 +02:00
Oscar Madera e09d3eb37b fix(workflow): define tracker status enum once in /outcome, normalise readers (#299)
* fix(workflow): define tracker status enum once in /outcome, normalise readers (#298)

The tracker CSV status column had no single authoritative definition.
Six command files restated it with inconsistent spellings, producing two
concrete bugs:

- /outcome Step 4 wrote
o response and offer declined (spaces).
  /html-report normalised only the underscore forms, so those rows matched
  no bucket and were silently dropped from the rejection-rate denominator.
- /gmail-sync Step 2 hardcoded the final-status set with space spellings,
  so a row written with underscores was never recognised as final and the
  sync kept chasing closed applications.
- /html-report included interview_only in its tracker bucket map; that
  value belongs to the archive outcome.md Status: field, not the CSV
  status column.

Fix: add a '## Tracker status vocabulary' block in /outcome (the only
CSV writer) defining the canonical underscore spellings once.  Every
reader now references that block or explicitly lists both spelling forms
as read-tolerance for existing trackers.  /outcome Step 4 writes
no_response and offer_declined.  /html-report loses interview_only and
gains offer declined as a read-tolerance variant.  /notion-sync Step 3
Status select options are aligned to the canonical spellings.

Pinned by tests/test_tracker_status_vocab.py (9 new cases following the
DraftedMeansDraftedToEveryReader CASES-table pattern).  All 205 tests pass.

framework_version: 1.3.0 -> 1.3.1

* fix(workflow): address review findings on the tracker status enum (#298)

Follow-up to ca40df2, incorporating the maintainer and issue-author reviews.

Blockers fixed:
- CHANGELOG: the #298 entry had replaced the opening line of the #286 robots
  entry, leaving its body dangling under the new fork heads-up. Restored the
  deleted line and made the #298 entry self-contained above it (MadsLorentzen).
- /notion-sync Step 4 now normalises legacy space spellings to the canonical
  underscore forms before setting the Status property. A raw push would
  auto-create a separate Notion select option per unique string, splitting
  closed applications across two filter buckets in an existing database
  (MadsLorentzen).

Issue-author findings:
- The vocabulary block now states that the space spellings are the same
  values as the underscore forms, not separate statuses, equally Final.
  Previously a reader applying the Open/Final lists literally landed on
  "not Final, not Open, undefined" for `offer declined`, and /apply Step 6b
  would refresh a closed application's row instead of appending (jakob1379).
- The block moved below Step 1's closing --- as its own section: it was
  splitting Step 1's numbered list and silently truncating section-scoped
  reads of Step 1 to item 1 (jakob1379).
- Open is derived by exclusion from the one explicit Final list, so a new
  status needs updating in a single place (jakob1379).
- /html-report's bucket map gains a case-insensitive catch-all that maps
  unrecognised values to Rejected/Closed and names them once in the status
  breakdown - the #298 failure mode with a different input (jakob1379).
- /apply Step 6b and /interview Step 0 anchor their final/open decisions to
  the vocabulary block (jakob1379).
- /gmail-sync and /html-report drop their local restatements of the
  read-tolerance rule (jakob1379).

Tests: html-report bucket assertions scoped to the Step 1 section; new pins
for the equivalence clause, open-by-exclusion, block placement, the Notion
normalisation, and the apply/interview anchors.
2026-08-07 20:47:24 +02:00
Oscar Madera 16d441e74c feat(cli): identify jobnet and jobdanmark API requests with an honest User-Agent (#283)
* fix(cli): send User-Agent on jobnet and jobdanmark API requests

apiFetch/apiPost hit the portals' APIs without a User-Agent header, while every other Danish-portal CLI sends one on purpose (jobbank exports USER_AGENT and its tests assert it; jobindex sets it on htmlFetch). Requests without one are rejected by the portals' bot filters.

* fix(cli): satisfy strict typecheck in user-agent regression test

* refactor(cli): reframe user-agent tests as honest self-identification

* docs(changelog): entry for #283 user-agent self-identification
2026-08-06 08:00:11 +02:00
Oscar Madera ce60b08e81 fix(upstream-checker): report files missing from the upstream ref instead of silent OK (#282)
The per-file 'git show' failure was swallowed with a bare continue, so a file renamed or deleted upstream (or any unexpected git error) ended with a clean '[OK] All framework files are up to date' - a false all-clear.

Now the two failure modes are distinguished: files present locally but missing from the upstream ref are listed explicitly with a final [WARNING] instead of [OK], and unexpected git errors are added to the configuration errors with their stderr.

Adds UpstreamRefMissingFileTests, which simulates upstream dropping AGENTS.md while the fork keeps its copy: it fails on master and passes with the fix.
2026-08-05 06:28:23 +02:00
Oscar Madera eef9c47461 fix(cli): reject negative and fractional filter flags in Danish portal CLIs (#281)
Follow-up to #191: it tightened page/limit/per-page, but five filter flags still used bare z.coerce.number() and accepted negative and fractional values that were sent raw to the portals (e.g. --jobage=-5, --radius=2.5).

jobindex --jobage, jobnet --radius, jobdanmark --category/--jobtitle-id and jobbank --company now use .int().min(1), mirroring #191. Adds 8 network-free regression tests (a negative and a fractional case per flag) using the same validation-error pattern as the existing cli-flag-validation suites.
2026-08-05 06:27:43 +02:00
Oscar Madera 20d863044b docs(setup): show git remote add upstream in section 8 (#274)
Section 8 told users to 'git fetch upstream' but never showed how to add that remote, so forks that cloned from their own fork could not follow the steps. Spell out the one-time remote-add command (already printed by tools/check_upstream_updates.py) at the source.
2026-08-02 21:16:07 +02:00
Oscar Madera 0433f3e332 fix(check_upstream_updates): compare template repo URL case-insensitively (#273)
GitHub serves repo paths case-insensitively, so a direct clone from https://github.com/madslorentzen/ai-job-search (lowercased) triggered the fork-vs-self warning even though origin is the template repo itself. Lowercase both sides of the check.

New test clones with a lowercased URL: fails on the previous check, passes with this fix.
2026-08-02 21:15:28 +02:00
Oscar Madera 72bbe00529 fix(check_upstream_updates): warn when check falls back to a fork's own origin (#265)
On a fork without an 'upstream' remote, the checker silently fell back to 'origin' (the fork itself) and still printed '[OK] All framework files are up to date with upstream!', a false positive: the fork is always up to date with itself, so upstream updates were never reported. This is exactly the setup CONTRIBUTING.md recommends for forks.

Now, when the fallback remote does not point at the ai-job-search template repo, the script warns that the comparison is fork-vs-self and prints the command to add the template as a remote. The final OK line now names the ref it actually compared against.

Tests (new tests/test_check_upstream_updates.py, three scenarios) fail on master and pass with the fix.
2026-08-01 22:00:49 +02:00
Oscar Madera 3609f584b5 fix(convert_salary_excel): pair count/index columns by category name, not adjacency (#219)
The sequential scan assumed count/index pairs are always adjacent.
Interleaved columns like Count_A, Count_B, Index_A, Index_B produced
wrong pairings (Count_B ↔ Index_A), silently corrupting data.

Now columns are grouped by type, then matched by the category name
derived from stripping type words. Unmatched columns fall back to
standalone value columns using the original header name.
2026-07-22 20:34:48 +02:00
Oscar Madera 78281e8bea fix(jobdanmark): narrow soft-404 detection to avoid rejecting real postings (#206)
The jobdanmark detail parser flagged a soft-404 by testing whether the page title contained the substring '404' anywhere, so a legitimate posting titled e.g. 'Room 404 Cleaner' was wrongly rejected as NOT_FOUND. Narrows title matching to startsWith('404') plus specific error phrases ('page not found', Danish 'siden blev ikke fundet'), keeping the existing body-text backstop. Verified: strictly reduces false-positives, the real 404-page title ('404 | Jobdanmark') still detected, tests pass network-free.

By @oscarbol09.
2026-07-21 08:11:20 +02:00
Oscar Madera d3eea27b90 fix(portals): depth-track div extraction so nested job descriptions aren't truncated (#204)
The jobindex and linkedin detail parsers matched description containers with a non-greedy regex that stops at the first inner </div>, so any posting whose description contains nested divs was silently truncated (jobindex dropped later sections; linkedin dropped everything after the first block). Replaces the regex with a depth-tracked extractDivContent scanner that walks div open/close markers to the matching close. Verified: truncation bug reproduced against real markup fixtures, depth arithmetic correct (no off-by-one/infinite-loop), 28 tests pass network-free, no regression on non-nested divs. Malformed-HTML over-grabs rather than truncates - the safer failure, cleaned by downstream stripTags/decode.

By @oscarbol09.
2026-07-21 08:11:17 +02:00
Oscar Madera 73fb71587c fix(cli): catch unhandled promise rejections in self-contained CLIs (#203)
The freehire and linkedin CLIs called main().then(code => process.exit(code)) with no .catch(). If main() throws or returns a rejected promise, the .then() never runs, process.exit() is never called, and the runtime terminates with exit code 0 - a runtime failure becomes indistinguishable from success (and would suppress the Step 1c WebSearch fallback, which keys on non-zero exit). Adds a .catch() that writes the same JSON error shape the rest of each file already uses ({error, code: INTERNAL_ERROR} to stderr) and exits 1. Scoped to the two self-contained CLIs only; the bunli portals catch internally via defineCommand.

By @oscarbol09.
2026-07-20 21:54:10 +02:00
Oscar Madera b3b351605c fix(salary): detect city column from header token, not exact match (#201)
convert_salary_excel.py detected the city column via exact membership (h_lower in CITY_PATTERNS), so real headers like "City Name", "City/Kommune", or "Kommune <suffix>" never matched and every company was written with an empty city field. Switches to header_matches(h, CITY_PATTERNS) - the same whole-token matcher already used for the company, count, index, and ID columns. Same bug class as #151 (company column); bare "City"/"Kommune" inputs are unaffected. Regression test covers bare and suffixed headers.

By @oscarbol09.
2026-07-20 20:20:53 +02:00
Oscar Madera 669f5ac1ab docs(setup): add Basic MiKTeX setup instructions for Windows (#186)
Documents the Windows Basic MiKTeX path missing from SETUP.md's minimal-TeX section: enabling silent [MPM]AutoInstall so missing-package installs don't block on a GUI prompt in non-interactive terminals, an mpm pre-install alternative matching the macOS TinyTeX package list, and a PowerShell translation of the existing bash smoke tests. Nested under the LaTeX section as a peer of the TinyTeX/BasicTeX subsection.

By @oscarbol09 (first contribution). Package names, AutoInstall config, and the PowerShell smoke block empirically verified on Windows 11 + MiKTeX.
2026-07-20 18:40:50 +02:00