← Files Career CentreARCHIVED FILE
scripts/manage_passport.py
4.06 KB · Sep 30, 2026 · 23:13 UTC
#!/usr/bin/env python3
"""Atomic Career Passport updates for lifecycle events and user corrections."""
from __future__ import annotations
import argparse
import json
import os
import tempfile
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from contracts import APPLICATION_STAGES, load_json, validate_career_passport
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _atomic_write(path: Path, data: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
handle, temporary = tempfile.mkstemp(prefix=path.name, suffix=".tmp", dir=path.parent)
try:
with os.fdopen(handle, "w", encoding="utf-8") as stream:
json.dump(data, stream, indent=2)
stream.write("\n")
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def _set_path(data: dict[str, Any], dotted: str, value: Any) -> Any:
parts = [part for part in dotted.split(".") if part]
if not parts:
raise ValueError("field path cannot be empty")
target: dict[str, Any] = data
for part in parts[:-1]:
child = target.get(part)
if not isinstance(child, dict):
child = {}
target[part] = child
target = child
old = target.get(parts[-1])
target[parts[-1]] = value
return old
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("passport")
subparsers = parser.add_subparsers(dest="command", required=True)
event = subparsers.add_parser("append-event")
event.add_argument("--role-id", required=True)
event.add_argument("--stage", choices=sorted(APPLICATION_STAGES), required=True)
event.add_argument("--source", choices=["user", "posting", "system"], required=True)
event.add_argument("--note", default="")
correction = subparsers.add_parser("correct")
correction.add_argument("--field", required=True)
correction.add_argument("--value-json", required=True)
feedback = subparsers.add_parser("append-feedback")
feedback.add_argument("--category", choices=["search", "role", "document", "workflow"], required=True)
feedback.add_argument("--statement", required=True)
feedback.add_argument("--confirmed", action="store_true")
subparsers.add_parser("validate")
args = parser.parse_args()
path = Path(args.passport).resolve()
data = load_json(path)
if args.command == "append-event":
data.setdefault("application_events", []).append(
{
"event_id": f"APP-{uuid.uuid4().hex[:12].upper()}",
"role_id": args.role_id,
"stage": args.stage,
"recorded_at": _now(),
"source": args.source,
"note": args.note,
}
)
data["updated_at"] = _now()
elif args.command == "correct":
new_value = json.loads(args.value_json)
old_value = _set_path(data, args.field, new_value)
data.setdefault("corrections", []).append(
{
"field": args.field,
"old_value": old_value,
"new_value": new_value,
"recorded_at": _now(),
"source": "user",
}
)
data["updated_at"] = _now()
elif args.command == "append-feedback":
data.setdefault("feedback", []).append(
{
"feedback_id": f"FB-{uuid.uuid4().hex[:12].upper()}",
"category": args.category,
"statement": args.statement,
"confirmed": args.confirmed,
"recorded_at": _now(),
}
)
data["updated_at"] = _now()
errors = validate_career_passport(data)
report = {"passed": not errors, "errors": errors, "passport": str(path)}
if errors:
print(json.dumps(report, indent=2))
return 1
if args.command != "validate":
_atomic_write(path, data)
print(json.dumps(report, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 5eca2fe34bc50c3037b61ce1488ae1a4c65f77a0f3dac01b92c1c5e1726f830a