Input and output cannot collide
outputPath() handles .json case-insensitively, appends a suffix when no extension exists, resolves both paths, and asserts that they differ.
The triage now guarantees a distinct output path, checkpoints every completed email, resumes interrupted work, supplies calendar age to Jev, and validates each structured answer before saving it.
Merged to main · 19 September 2026The upgrade concentrates on data safety, interrupted-run recovery, time-aware decisions, and repeatable measurement.
outputPath() handles .json case-insensitively, appends a suffix when no extension exists, resolves both paths, and asserts that they differ.
Full runs write a separate .partial file after every email. --resume skips completed IDs and continues the interrupted run.
Each checkpoint uses a temporary file and rename. The final output is promoted only after all selected emails succeed, then the partial file is removed.
The state includes today and computed age_days. The importance rubric explicitly says an expired deadline is not urgent.
Every input field must be a string. Category and importance must be valid, and both boolean probabilities must be finite values from zero through one.
Security, receipts, service notices, newsletters, reminders, business inquiries, sponsorship, phishing, and an expired deadline share one JSONL result contract.
| Before | Failure mode | Now |
|---|---|---|
Only lowercase .json was replaced in the filename. | An uppercase extension or extensionless input could become its own output and be overwritten. | Output naming is case-insensitive, suffix-safe, resolved, and asserted distinct. |
| Results were written after the last email. | A late failure discarded every earlier paid decision. | Each successful row is atomically checkpointed and resumable. |
| Jev received the email date without the current date. | It could treat a months-old phrase such as “due tomorrow” as urgent today. | The model receives today and age_days; the old-deadline test scored 0.19 importance. |
| Answer fields were read directly. | A missing or malformed probability could silently become an unusable result. | Invalid structured responses stop before persistence. |
Parse and validate the JSON array and command-line options.
Load the partial file and build a set of completed message IDs.
Compute today and age_days before calling Jev.
Validate the answer and atomically save accumulated rows.
Write the complete final output, then remove the partial checkpoint.
npm test
npm run eval:live
node --env-file=.env triage.mjs emails.json
node --env-file=.env triage.mjs emails.json --resume
node --env-file=.env triage.mjs emails.json --limit 5 --raw
| Signal | Measured result | Interpretation |
|---|---|---|
| Offline checks | 12/12 plus focused path and age assertions | The runner, safety checks, evaluation contract, and age calculation are deterministic. |
| Live Jev evaluation | 12/12, 0 errors | All reviewed categories, score ranges, and boolean decisions passed. |
| Expired deadline | Importance 0.19; confidence 0.87 | The new clock context corrected the known stale-urgency failure mode. |
| Phishing | Scam 0.98; importance confidence 0.00 | The scam decision was strong, while the importance score remained intrinsically uncertain. |
| Sponsorship | Brand deal 0.98; scam 0.49 | The case passed, but the scam value sits one point below the operational threshold and deserves monitoring. |