Step 3 gains rule 7: a posting whose stored posted_date is more than 30
days old at rank time carries a visible staleness marker with its age
spelled out alongside the score - FLAG treatment like location and
language, never an exclusion. No posted_date or null means no flag and no
guess (never inferred from first_seen), and rule 6's defensive-parse rule
applies wherever the stored value is compared. Age is re-derived each run
and never persisted. Four new spec pins in test_rank_command.py, each
verified to fail against the rule-less spec.
"location" meant a place in scraper output and a PASS/FAIL/FLAG verdict
in /rank's persistence - one key, two meanings, in the same store, with
ranking able to overwrite the commute-filter place with "PASS". The
verdict now lives in location_verdict; legacy entries are read
compatibly and migrated on re-write. Also completes the seen_jobs schema
enumeration (F27 Part A): the do-not-drop instruction now names
location_verdict/language_gate/language_note. Review finding F27
(2026-08-19), decision approved by Mads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rule 6's expiry sweep mutates status automatically from stored deadline
values, yet had no rule for the non-ISO shapes portals have shipped into
seen_jobs.json ("ASAP", DD.MM.YYYY, free text) - "ASAP" is incomparable
and "01.09.2026" is ambiguous between 1 Sep and 9 Jan. /outcome, which
merely displays dates, already carried the defensive-parse rule. A
non-YYYY-MM-DD stored value is now handled like an absent one (left
alone, never compared, never guessed at) and reported once with its
portal. Includes the F24-style coupling test. Review finding F17
(2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
04-job-evaluation.md still said the gate result "is not a field /scrape
or /rank track" - true when the gate was introduced, false since /rank
began persisting language_gate/language_note as shortlist veto fields
and /scrape began surfacing the flag. The authoritative framework file
taught agents the opposite of rank.md's own persistence rule. New
coupling test pins that the section names the tracked fields and never
reverts to the untracked claim. framework_version 1.2.3 -> 1.2.4.
Review finding F24 (2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/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>
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>
Follow-up suggested during PR #275's merge review: spec-pinning tests for
the language_gate/language_note fields, matching the pattern already used
for the sibling gaps/strengths fields in this same file.
Four new assertions: Step 2's scoring-agent JSON schema declares both
fields with the PASS/FAIL/FLAG enum and distinguishes them from the
pre-existing `language` field; Step 3 documents the Language veto rule
mirroring the existing Location veto; Step 4 persists both fields into
seen_jobs.json; Step 5's presentation rules document the FLAG marker.
The Step 4 persistence test is a real regression guard, not just
documentation: language_gate/language_note were computed in Step 2 and
used to decide Step 3's veto, but an earlier version of this spec never
instructed Step 4 to actually write them to seen_jobs.json - caught via
live debugging (a real /rank run showed language_gate: null on every
entry despite the run reporting genuine vetoes), fixed once already.
Verified live that this test fails against that exact regression and
passes against the current (fixed) spec text.
/rank's Step 2 scoring agents already return strengths and gaps per job,
but Step 4 only persisted rank_score/rank_verdict/rank_date - both arrays
were printed once in Step 5 and then discarded. Store them verbatim in
seen_jobs.json (replaced, not accumulated, on --all re-ranks) so downstream
consumers can read real triage findings instead of re-deriving them.
Discussed in #258.