refactor: shared agent workflows (F01) and one application registry (F02)
Checkpoint for the 2026-09-11 project review remediation. Work by a previous agent that ran out of context; committed unchanged as a rollback point before the remaining findings land on top of it. F01 — the two agent directories had drifted into different processes - Six shared workflows under `resume_builder/reference/workflows/`; both `.agents/skills/` and `.claude/skills/` are now thin entrypoints that route to the same file instead of carrying full copies. - The `.claude` copies were the stale side: fixed bullet line variants and character budgets that the newer process had already dropped. Removing them is the point of the change, not collateral. - Parity/link regression test added; corrected the top-level file map, which pointed at a directory that does not exist. F02 — application status was stored in four places and disagreed - New registry `job_scout/state/applications.json` (26 records) with `application_records.py` (schema, validation, transactional write) and `application_tracker.py` (add/update, no quota). - AGENTS.md and CLAUDE.md no longer keep their own status tables; both point at `current_applications.md`, generated from the registry. - Interview-reached is now independent of outcome, so a withdrawal after an interview can record both facts. The old tables are preserved verbatim in `job_scout/state/history/application_status_before_2026-09-11.json`. Verification: `python -m unittest discover -s tests -v` — 18/18 pass. Spot-checked that the detailed SBB and Kdo Cy notes survived the migration into the registry rather than being flattened. Known unresolved, deliberately not invented (see FIX_PROGRESS.md): Aker BP's application date and its Adjacent-despite-79 classification. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
This commit is contained in:
1 parent
0d999ab21b
commit
afdef7eb94
33 files changed
+1558
-1768
No files matched your search
@@ -0,0 +1,96 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "resume_builder/helpers"))
|
||||
import application_records as records
|
||||
import application_tracker as tracker
|
||||
from argparse import Namespace
|
||||
import copy
|
||||
|
||||
|
||||
class ApplicationRecordTests(unittest.TestCase):
|
||||
def test_generated_views_are_current(self):
|
||||
self.assertEqual([], records.render(records.load(), check=True))
|
||||
|
||||
def test_imported_outcomes_preserve_interview_and_closed_state(self):
|
||||
applications = records.load()["applications"]
|
||||
self.assertEqual(len(applications), len({row["id"] for row in applications}))
|
||||
sbb = next(row for row in applications if row["company"] == "SBB")
|
||||
self.assertEqual("withdrawn_by_candidate", sbb["outcome"])
|
||||
self.assertTrue(records.interview_reached(sbb))
|
||||
for token in ("200040836", "JR106747", "JRQ$540-19848"):
|
||||
row = next(row for row in applications if token in row["label"])
|
||||
self.assertEqual("rejected_no_interview", row["outcome"])
|
||||
|
||||
def test_both_agents_link_to_one_view(self):
|
||||
for name in ("AGENTS.md", "CLAUDE.md"):
|
||||
text = (ROOT / name).read_text(encoding="utf-8")
|
||||
section = text.split("## Active Sessions", 1)[1].split("## KB Corrections", 1)[0]
|
||||
self.assertIn("resume_builder/reference/current_applications.md", section)
|
||||
self.assertNotIn("| Session |", section)
|
||||
|
||||
def test_atomic_write_leaves_no_shared_temp_file(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "state.json"
|
||||
records.atomic_write(path, '{"value": 1}\n')
|
||||
self.assertEqual({"value": 1}, json.loads(path.read_text()))
|
||||
self.assertEqual([path], list(Path(directory).iterdir()))
|
||||
|
||||
def args(self, **overrides):
|
||||
fields = dict(id=None, company="Example", role="Engineer", date="2026-09-11", job_id="req-1", url=None,
|
||||
fit_class="core", evidence_fit=79, hard_gate="pass", channel="weak", outcome="applied",
|
||||
fit_override_reason=None, rubric_version="test", next_action="Await response.")
|
||||
fields.update(overrides)
|
||||
return Namespace(**fields)
|
||||
|
||||
def test_more_than_ten_applications_and_idempotent_retry(self):
|
||||
data = records.load()
|
||||
for index in range(11):
|
||||
tracker.add_application(data, self.args(job_id=f"new-{index}", role=f"Engineer {index}"))
|
||||
count = len(data["applications"])
|
||||
tracker.add_application(data, self.args(job_id="new-10", role="Engineer 10"))
|
||||
self.assertEqual(count, len(data["applications"]))
|
||||
|
||||
def test_bad_dates_scores_and_unexplained_classification_are_rejected(self):
|
||||
for fields in ({"date":"2026-99-11"}, {"evidence_fit":101}, {"evidence_fit":float("nan")},
|
||||
{"fit_class":"adjacent"}, {"hard_gate":"fail"}):
|
||||
with self.subTest(fields=fields), self.assertRaises(ValueError):
|
||||
tracker.add_application(records.load(), self.args(**fields))
|
||||
|
||||
def test_outcome_update_preserves_interview_event_and_is_idempotent(self):
|
||||
data = records.load()
|
||||
tracker.add_application(data, self.args())
|
||||
row = data["applications"][-1]
|
||||
invited = Namespace(id=row["id"], date="2026-09-12", event="interview_invited", outcome=None, note=None, next_action=None)
|
||||
tracker.update_application(data, invited)
|
||||
withdrawn = Namespace(id=row["id"], date="2026-09-13", event=None, outcome="withdrawn_by_candidate", note="Decision", next_action=None)
|
||||
tracker.update_application(data, withdrawn)
|
||||
tracker.update_application(data, withdrawn)
|
||||
self.assertEqual(3, len(row["events"]))
|
||||
self.assertTrue(records.interview_reached(row))
|
||||
self.assertEqual("withdrawn_by_candidate", row["outcome"])
|
||||
tracker.add_application(data, self.args())
|
||||
self.assertEqual("withdrawn_by_candidate", row["outcome"])
|
||||
|
||||
def test_transaction_failure_does_not_write_registry(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "applications.json"
|
||||
initial = json.dumps(records.load())
|
||||
path.write_text(initial, encoding="utf-8")
|
||||
with patch.object(records, "REGISTRY", path):
|
||||
with self.assertRaises(ValueError), records.transaction() as data:
|
||||
data["applications"][0]["outcome"] = "made-up"
|
||||
self.assertEqual(initial, path.read_text(encoding="utf-8"))
|
||||
self.assertFalse(path.with_suffix(".lock").exists())
|
||||
path.with_suffix(".lock").write_text("another writer")
|
||||
with self.assertRaises(ValueError), records.transaction():
|
||||
pass
|
||||
Reference in new issue
Block a user