← Files Career Command CenterARCHIVED FILE

scripts/doctor.py

9.09 KB · Oct 2, 2026 · 00:29 UTC

↓ Download file

#!/usr/bin/env python3
"""Diagnose workspace and app readiness for Career Command Center."""

from __future__ import annotations

import argparse
import json
from datetime import datetime
from pathlib import Path


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("workspace", type=Path)
    parser.add_argument("--strict", action="store_true")
    args = parser.parse_args()
    workspace = args.workspace.expanduser().resolve()
    errors: list[str] = []
    warnings: list[str] = []

    required = [
        "Config/command_center_config.json",
        "State/cv_command_center_state.json",
        "Evidence_Bank/CV_GENERATION_STANDARD.md",
        "Evidence_Bank/WORKSPACE_CONTRACT.md",
        "Evidence_Bank/PERSONALIZED_QUESTION_STANDARD.md",
        "Evidence_Bank/approved_evidence.json",
        "Evidence_Bank/personalized_questions.json",
    ]
    for relative in required:
        if not (workspace / relative).exists():
            errors.append(f"Missing {relative}")

    config: dict = {}
    config_path = workspace / "Config/command_center_config.json"
    if config_path.exists():
        try:
            config = json.loads(config_path.read_text(encoding="utf-8"))
        except Exception as exc:
            errors.append(f"Invalid config JSON: {exc}")
    if config and not config.get("onboarding_completed"):
        errors.append("First-use onboarding is not complete")
    if config and not (config.get("profile") or {}).get("fullName"):
        errors.append("Candidate full name is missing")
    if config and not (config.get("search") or {}).get("countries"):
        errors.append("No target countries are configured")

    state_path = workspace / "State/cv_command_center_state.json"
    if state_path.exists():
        try:
            state = json.loads(state_path.read_text(encoding="utf-8"))
            if not isinstance(state.get("leads"), list):
                errors.append("State leads is not an array")
        except Exception as exc:
            errors.append(f"Invalid state JSON: {exc}")

    evidence_path = workspace / "Evidence_Bank/approved_evidence.json"
    masters: list[str] = []
    has_evidence_blocks = False
    if evidence_path.exists():
        try:
            evidence = json.loads(evidence_path.read_text(encoding="utf-8"))
            registered = evidence.get("approved_master_cvs") or {}
            masters = list(registered.values()) if isinstance(registered, dict) else list(registered)
            has_evidence_blocks = bool(evidence.get("evidence_blocks"))
            if not has_evidence_blocks:
                warnings.append("Evidence audit has not approved any evidence blocks")
        except Exception as exc:
            errors.append(f"Invalid evidence JSON: {exc}")
    if not masters:
        masters = list((config.get("cv") or {}).get("selectedMasterPaths") or [])
    missing_masters = [path for path in masters if not Path(path).expanduser().exists()]
    if not masters:
        warnings.append("No approved role-family master CVs are registered")
    elif missing_masters:
        warnings.append(f"{len(missing_masters)} registered master CV path(s) do not exist")

    ledger_path = workspace / "Evidence_Bank/Verified_Evidence_Ledger.md"
    if not ledger_path.exists() and not has_evidence_blocks:
        warnings.append("Narrative evidence ledger has not been created")

    document_files = [path for path in (workspace / "Documents").rglob("*") if path.is_file()]
    project_files = [path for path in (workspace / "Projects").rglob("*") if path.is_file()]
    document_count = len(document_files)
    project_count = len(project_files)
    if document_count == 0 and project_count == 0 and not has_evidence_blocks:
        warnings.append("No source documents or project files have been imported")

    question_counts = {
        "open": 0,
        "answered": 0,
        "unable_to_verify": 0,
        "not_applicable": 0,
        "resolved": 0,
        "superseded": 0,
    }
    question_audit_status = "not_started"
    question_generated_at = ""
    missing_active_sources = 0
    questions_path = workspace / "Evidence_Bank/personalized_questions.json"
    if questions_path.exists():
        try:
            question_bank = json.loads(questions_path.read_text(encoding="utf-8"))
            if question_bank.get("version") != 1:
                errors.append("Personalized evidence questions must use version 1")
            question_audit_status = question_bank.get("audit_status", "not_started")
            question_generated_at = question_bank.get("generated_at", "")
            if question_audit_status not in {"not_started", "current", "needs_refresh"}:
                errors.append("Personalized evidence questions have an invalid audit status")
            questions = question_bank.get("questions")
            if not isinstance(questions, list):
                errors.append("Personalized evidence questions must contain a questions array")
            else:
                for index, question in enumerate(questions):
                    if not isinstance(question, dict):
                        errors.append(f"Personalized evidence question {index} is not an object")
                        continue
                    status = question.get("status")
                    if status not in question_counts:
                        errors.append(f"Personalized evidence question {index} has invalid status {status!r}")
                        continue
                    question_counts[status] += 1
                    if status in {"open", "answered", "unable_to_verify"}:
                        source_refs = question.get("source_refs")
                        if not isinstance(source_refs, list) or not source_refs:
                            errors.append(
                                f"Personalized evidence question {index} has no source references"
                            )
                        else:
                            for source in source_refs:
                                raw_path = source.get("path") if isinstance(source, dict) else None
                                if not isinstance(raw_path, str):
                                    errors.append(
                                        f"Personalized evidence question {index} has an invalid source path"
                                    )
                                    continue
                                relative = Path(raw_path)
                                if relative.is_absolute() or ".." in relative.parts:
                                    errors.append(
                                        f"Personalized evidence question {index} source is not workspace-relative"
                                    )
                                elif not (workspace / relative).is_file():
                                    missing_active_sources += 1
        except Exception as exc:
            errors.append(f"Invalid personalized questions JSON: {exc}")

    if question_counts["open"]:
        warnings.append(
            f"Personalized evidence questions still need answers: {question_counts['open']}"
        )
    awaiting_review = question_counts["answered"] + question_counts["unable_to_verify"]
    if awaiting_review:
        warnings.append(
            f"Personalized evidence answers await Codex review: {awaiting_review}"
        )
    if document_count + project_count > 0 and question_audit_status != "current":
        warnings.append("Personalized evidence audit needs refresh for imported source material")
    source_files = document_files + project_files
    intake_path = workspace / "Evidence_Bank/intake_answers.md"
    if intake_path.is_file():
        source_files.append(intake_path)
    if question_audit_status == "current" and source_files:
        try:
            audited_at = datetime.fromisoformat(question_generated_at.replace("Z", "+00:00"))
            newest_source = max(path.stat().st_mtime for path in source_files)
            if newest_source - audited_at.timestamp() > 2:
                warnings.append("Personalized evidence audit needs refresh for changed source files")
        except Exception as exc:
            errors.append(f"Invalid personalized question generation timestamp: {exc}")
    if missing_active_sources:
        warnings.append(
            f"Personalized evidence questions reference missing source files: {missing_active_sources}"
        )

    ready = not errors and not any(
        text.startswith("Evidence audit")
        or text.startswith("No approved")
        or text.startswith("Personalized evidence")
        for text in warnings
    )
    result = {
        "workspace": str(workspace),
        "ready_for_automation": ready,
        "errors": errors,
        "warnings": warnings,
        "documents": document_count,
        "project_files": project_count,
        "approved_masters": len(masters) - len(missing_masters),
        "question_counts": question_counts,
        "question_audit_status": question_audit_status,
    }
    print(json.dumps(result, indent=2))
    if errors:
        return 2
    if args.strict and warnings:
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: 090175ec28fe385b9dc84b10126805baeb2e2ead1d6b48e931026b624a4a3a7b