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
118 lines
6.3 KiB
Python
118 lines
6.3 KiB
Python
"""Record applications without quotas; preserve outcome events and interview history."""
|
|
from __future__ import annotations
|
|
import argparse
|
|
import hashlib
|
|
import application_records as records
|
|
|
|
FIT_CLASSES = ("core", "adjacent", "stretch", "no-go")
|
|
CHANNELS = ("strong", "moderate", "weak")
|
|
|
|
|
|
def summary(data: dict, all_applications: bool = False) -> None:
|
|
cohort_id = data["cohort_metadata"]["cohort_id"]
|
|
applications = [row for row in data["applications"] if all_applications or row.get("cohort_id") == cohort_id]
|
|
print(f"Application record: {'all' if all_applications else cohort_id} ({len(applications)} applications; no quota)")
|
|
for field, values in (("fit_class", FIT_CLASSES), ("channel", CHANNELS)):
|
|
for value in values:
|
|
print(f" {field} {value}: {sum(row.get(field) == value for row in applications)}")
|
|
unknown = sum(row.get(field) is None for row in applications)
|
|
if unknown:
|
|
print(f" {field} unrecorded: {unknown}")
|
|
outcomes = sorted({row["outcome"] for row in applications})
|
|
print(" outcomes: " + ", ".join(f"{value}={sum(row['outcome'] == value for row in applications)}" for value in outcomes))
|
|
print(f" interview reached (including later withdrawal/rejection): {sum(records.interview_reached(row) for row in applications)}")
|
|
|
|
|
|
def add_application(data: dict, args: argparse.Namespace) -> None:
|
|
records.valid_date(args.date)
|
|
identity = args.id or "app-" + hashlib.sha256(f"{args.company.casefold()}|{args.job_id or args.role.casefold()}|{args.date}".encode()).hexdigest()[:12]
|
|
candidate = {
|
|
"id": identity, "company": args.company, "role": args.role,
|
|
"label": f"{args.company} — {args.role}", "application_date": args.date,
|
|
"fit_class": args.fit_class, "evidence_fit": args.evidence_fit,
|
|
"hard_gate": args.hard_gate, "channel": args.channel, "outcome": args.outcome,
|
|
"cohort_id": data["cohort_metadata"]["cohort_id"], "job_id": args.job_id,
|
|
"url": args.url, "fit_override_reason": args.fit_override_reason,
|
|
"rubric_version": args.rubric_version,
|
|
"next_action": args.next_action, "current_summary": args.outcome,
|
|
"events": [{"type": args.outcome, "date": args.date}],
|
|
}
|
|
for existing in data["applications"]:
|
|
same_job = args.job_id and existing.get("job_id") == args.job_id and existing["company"].casefold() == args.company.casefold()
|
|
same_url = args.url and existing.get("url") == args.url
|
|
same_application = existing["company"].casefold() == args.company.casefold() and existing["role"].casefold() == args.role.casefold() and existing.get("application_date") == args.date
|
|
if existing["id"] == identity or same_job or same_url or same_application:
|
|
if all(existing.get(key) == value for key, value in candidate.items() if key not in {"events", "id", "current_summary", "next_action", "outcome"}):
|
|
return
|
|
raise ValueError(f"Application already exists ({existing['id']}); use update instead")
|
|
data["applications"].append(candidate)
|
|
records.validate(data)
|
|
|
|
|
|
def update_application(data: dict, args: argparse.Namespace) -> None:
|
|
records.valid_date(args.date)
|
|
matches = [row for row in data["applications"] if row["id"] == args.id]
|
|
if not matches:
|
|
raise ValueError(f"Unknown application ID: {args.id}")
|
|
row = matches[0]
|
|
if row.get("application_date") and args.date < row["application_date"]:
|
|
raise ValueError("event date precedes application date")
|
|
outcome_dates = [event["date"] for event in row.get("events", []) if event.get("outcome") and event.get("date")]
|
|
if args.outcome and outcome_dates and args.date < max(outcome_dates):
|
|
raise ValueError("older outcome cannot replace current state; record it as a historical note")
|
|
event = {"type": args.event or args.outcome or "note", "date": args.date}
|
|
if args.note:
|
|
event["note"] = args.note
|
|
if args.outcome:
|
|
event["outcome"] = args.outcome
|
|
if event not in row.setdefault("events", []):
|
|
row["events"].append(event)
|
|
if args.outcome:
|
|
row["outcome"] = args.outcome
|
|
row["current_summary"] = args.note or args.outcome
|
|
row["next_action"] = args.next_action or ("Await response." if args.outcome in {"applied", "interviewing", "open"} else "Review recorded outcome before further action.")
|
|
elif args.next_action:
|
|
row["next_action"] = args.next_action
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
commands = parser.add_subparsers(dest="command", required=True)
|
|
view = commands.add_parser("summary")
|
|
view.add_argument("--all", action="store_true")
|
|
add = commands.add_parser("add")
|
|
for field in ("company", "role", "date"):
|
|
add.add_argument("--" + field, required=True)
|
|
add.add_argument("--id")
|
|
add.add_argument("--job-id")
|
|
add.add_argument("--url")
|
|
add.add_argument("--fit-class", choices=FIT_CLASSES, required=True)
|
|
add.add_argument("--evidence-fit", type=float, required=True)
|
|
add.add_argument("--channel", choices=CHANNELS, required=True)
|
|
add.add_argument("--hard-gate", choices=("pass", "fail", "unknown"), required=True)
|
|
add.add_argument("--outcome", choices=sorted(records.OUTCOMES), default="applied")
|
|
add.add_argument("--fit-override-reason")
|
|
add.add_argument("--rubric-version", default="critique-framework-2026-09-11")
|
|
add.add_argument("--next-action", default="Await response.")
|
|
update = commands.add_parser("update")
|
|
update.add_argument("--id", required=True)
|
|
update.add_argument("--date", required=True, help="Date of this event")
|
|
update.add_argument("--outcome", choices=sorted(records.OUTCOMES))
|
|
update.add_argument("--event", choices=("interview_invited", "interview_held", "assessment_passed", "contact_attempted", "contact_conversation", "referral_received", "note"))
|
|
update.add_argument("--note")
|
|
update.add_argument("--next-action")
|
|
args = parser.parse_args()
|
|
try:
|
|
if args.command == "summary":
|
|
summary(records.load(), args.all)
|
|
return
|
|
with records.transaction() as data:
|
|
(add_application if args.command == "add" else update_application)(data, args)
|
|
summary(data)
|
|
except (ValueError, OSError) as error:
|
|
parser.exit(2, f"ERROR: {error}\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|