← Files Career Command CenterARCHIVED FILE
scripts/question_cli.py
16.3 KB · Oct 2, 2026 · 00:29 UTC
#!/usr/bin/env python3
"""Manage source-specific evidence questions for a Career Command Center workspace."""
from __future__ import annotations
import argparse
import json
import os
import re
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
QUESTION_RELATIVE_PATH = Path("Evidence_Bank/personalized_questions.json")
VALID_PRIORITIES = {"critical", "high", "medium"}
VALID_CATEGORIES = {
"metric",
"ownership",
"outcome",
"method",
"timeline",
"contradiction",
"eligibility",
"direction",
"other",
}
RESPONSE_STATUSES = {"answered", "unable_to_verify", "not_applicable"}
TERMINAL_STATUSES = {"resolved", "not_applicable", "superseded"}
VALID_STATUSES = {"open"} | RESPONSE_STATUSES | TERMINAL_STATUSES
VALID_AUDIT_STATUSES = {"not_started", "current", "needs_refresh"}
ID_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._-]{4,119}$")
GENERIC_QUESTION_PATTERNS = [
re.compile(r"^tell me more\b", re.IGNORECASE),
re.compile(r"^can you elaborate\b", re.IGNORECASE),
re.compile(r"^please explain\b", re.IGNORECASE),
re.compile(r"^describe (?:your|the) (?:project|role|experience)\b", re.IGNORECASE),
re.compile(r"^what (?:did you do|are your strengths)\b", re.IGNORECASE),
]
def timestamp() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def clean_text(value: Any) -> str:
if not isinstance(value, str):
return ""
return " ".join(value.split())
def empty_bank() -> dict[str, Any]:
return {
"version": 1,
"generation_id": "",
"audit_status": "not_started",
"source_change_note": "",
"generated_at": "",
"updated_at": timestamp(),
"questions": [],
}
def question_path(workspace: Path) -> Path:
return workspace / QUESTION_RELATIVE_PATH
def load_json(path: Path) -> Any:
try:
return json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid JSON in {path}: {exc}") from exc
def load_bank(workspace: Path) -> dict[str, Any]:
path = question_path(workspace)
if not path.exists():
bank = empty_bank()
write_json(path, bank)
return bank
payload = load_json(path)
if not isinstance(payload, dict):
raise ValueError(f"{path} must contain a JSON object")
validate_bank(payload)
return payload
def write_json(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
data = json.dumps(value, indent=2, ensure_ascii=True, sort_keys=True) + "\n"
handle, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
with os.fdopen(handle, "w", encoding="utf-8") as stream:
stream.write(data)
os.replace(temporary_name, path)
except Exception:
try:
os.unlink(temporary_name)
except FileNotFoundError:
pass
raise
def ensure_text(value: Any, field: str, minimum: int, maximum: int) -> str:
text = clean_text(value)
if len(text) < minimum or len(text) > maximum:
raise ValueError(f"{field} must contain {minimum}-{maximum} characters")
return text
def normalize_source(raw: Any, index: int, workspace: Path | None = None) -> dict[str, str]:
if not isinstance(raw, dict):
raise ValueError(f"source_refs[{index}] must be an object")
path_value = ensure_text(raw.get("path"), f"source_refs[{index}].path", 1, 500)
source_path = Path(path_value)
if source_path.is_absolute() or path_value.startswith("~") or ".." in source_path.parts:
raise ValueError(f"source_refs[{index}].path must be workspace-relative")
if workspace is not None and not (workspace / source_path).is_file():
raise ValueError(f"source_refs[{index}].path does not exist in the workspace: {path_value}")
return {
"path": source_path.as_posix(),
"label": ensure_text(raw.get("label"), f"source_refs[{index}].label", 2, 160),
"locator": ensure_text(raw.get("locator"), f"source_refs[{index}].locator", 2, 160),
"context": ensure_text(raw.get("context"), f"source_refs[{index}].context", 10, 400),
}
def normalize_question(raw: Any, generated_at: str, workspace: Path) -> dict[str, Any]:
if not isinstance(raw, dict):
raise ValueError("Each question must be an object")
identifier = clean_text(raw.get("id"))
if not ID_PATTERN.fullmatch(identifier):
raise ValueError(
"Question id must be 5-120 lower-case letters, numbers, dots, underscores, or hyphens"
)
priority = clean_text(raw.get("priority")).lower()
if priority not in VALID_PRIORITIES:
raise ValueError(f"Question {identifier} has invalid priority: {priority}")
category = clean_text(raw.get("category")).lower()
if category not in VALID_CATEGORIES:
raise ValueError(f"Question {identifier} has invalid category: {category}")
question = ensure_text(raw.get("question"), f"Question {identifier}.question", 20, 500)
if not question.endswith("?"):
raise ValueError(f"Question {identifier}.question must end with a question mark")
if any(pattern.search(question) for pattern in GENERIC_QUESTION_PATTERNS):
raise ValueError(f"Question {identifier} is generic; tie it to the cited source ambiguity")
why = ensure_text(raw.get("why_it_matters"), f"Question {identifier}.why_it_matters", 10, 400)
sources = raw.get("source_refs")
if not isinstance(sources, list) or not sources:
raise ValueError(f"Question {identifier} requires at least one source reference")
evidence_ids = raw.get("related_evidence_ids") or []
if not isinstance(evidence_ids, list) or not all(isinstance(item, str) for item in evidence_ids):
raise ValueError(f"Question {identifier}.related_evidence_ids must be an array of strings")
return {
"id": identifier,
"priority": priority,
"category": category,
"question": question,
"why_it_matters": why,
"source_refs": [
normalize_source(source, index, workspace) for index, source in enumerate(sources)
],
"related_evidence_ids": [clean_text(item) for item in evidence_ids if clean_text(item)],
"status": "open",
"answer": "",
"generated_at": generated_at,
"answered_at": "",
"reviewed_at": "",
"review_note": "",
}
def validate_bank(bank: dict[str, Any]) -> None:
if bank.get("version") != 1:
raise ValueError("personalized_questions.json must use version 1")
if bank.get("audit_status", "not_started") not in VALID_AUDIT_STATUSES:
raise ValueError("personalized_questions.json has an invalid audit_status")
questions = bank.get("questions")
if not isinstance(questions, list):
raise ValueError("personalized_questions.json questions must be an array")
seen: set[str] = set()
for index, question in enumerate(questions):
if not isinstance(question, dict):
raise ValueError(f"questions[{index}] must be an object")
identifier = question.get("id")
if not isinstance(identifier, str) or not ID_PATTERN.fullmatch(identifier):
raise ValueError(f"questions[{index}] has an invalid id")
if identifier in seen:
raise ValueError(f"Duplicate question id: {identifier}")
seen.add(identifier)
if question.get("priority") not in VALID_PRIORITIES:
raise ValueError(f"Question {identifier} has an invalid priority")
if question.get("category") not in VALID_CATEGORIES:
raise ValueError(f"Question {identifier} has an invalid category")
if question.get("status") not in VALID_STATUSES:
raise ValueError(f"Question {identifier} has an invalid status")
if not clean_text(question.get("question")).endswith("?"):
raise ValueError(f"Question {identifier} must end with a question mark")
sources = question.get("source_refs")
if not isinstance(sources, list) or not sources:
raise ValueError(f"Question {identifier} requires source_refs")
for source_index, source in enumerate(sources):
normalize_source(source, source_index)
if question.get("status") == "answered" and not clean_text(question.get("answer")):
raise ValueError(f"Answered question {identifier} has an empty answer")
def summary(bank: dict[str, Any]) -> dict[str, Any]:
counts = {status: 0 for status in sorted(VALID_STATUSES)}
priorities = {priority: 0 for priority in sorted(VALID_PRIORITIES)}
for question in bank["questions"]:
counts[question["status"]] += 1
if question["status"] in {"open", "answered", "unable_to_verify"}:
priorities[question["priority"]] += 1
return {
"path": str(bank.get("_path", "")),
"generation_id": bank.get("generation_id", ""),
"audit_status": bank.get("audit_status", "not_started"),
"total": len(bank["questions"]),
"status_counts": counts,
"active_priority_counts": priorities,
"needs_user_answer": counts["open"],
"awaiting_codex_review": counts["answered"] + counts["unable_to_verify"],
}
def generate_questions(workspace: Path, input_path: Path, maximum: int) -> dict[str, Any]:
payload = load_json(input_path)
if isinstance(payload, list):
raw_questions = payload
generation_id = f"audit-{timestamp()}"
elif isinstance(payload, dict):
raw_questions = payload.get("questions")
generation_id = clean_text(payload.get("generation_id")) or f"audit-{timestamp()}"
else:
raise ValueError("Generation input must be an array or an object with a questions array")
if not isinstance(raw_questions, list):
raise ValueError("Generation input questions must be an array")
if maximum < 1 or maximum > 50:
raise ValueError("max-questions must be between 1 and 50")
if len(raw_questions) > maximum:
raise ValueError(f"Generation input contains {len(raw_questions)} questions; maximum is {maximum}")
now = timestamp()
incoming = [normalize_question(raw, now, workspace) for raw in raw_questions]
incoming_ids = [question["id"] for question in incoming]
if len(set(incoming_ids)) != len(incoming_ids):
raise ValueError("Generation input contains duplicate question ids")
bank = load_bank(workspace)
existing = {question["id"]: question for question in bank["questions"]}
merged: list[dict[str, Any]] = []
for question in incoming:
previous = existing.get(question["id"])
if previous and previous.get("status") not in {"open", "superseded"}:
merged.append(previous)
else:
merged.append(question)
for previous in bank["questions"]:
if previous["id"] in incoming_ids:
continue
retained = dict(previous)
if retained.get("status") == "open":
retained["status"] = "superseded"
retained["reviewed_at"] = now
retained["review_note"] = f"Superseded by question generation {generation_id}."
merged.append(retained)
bank.update(
{
"version": 1,
"generation_id": generation_id,
"audit_status": "current",
"source_change_note": "",
"generated_at": now,
"updated_at": now,
"questions": merged,
}
)
validate_bank(bank)
write_json(question_path(workspace), bank)
bank["_path"] = str(question_path(workspace))
return summary(bank)
def respond(workspace: Path, identifier: str, status: str, answer: str) -> dict[str, Any]:
if status not in RESPONSE_STATUSES:
raise ValueError(f"Response status must be one of: {', '.join(sorted(RESPONSE_STATUSES))}")
answer = answer.strip()
if status == "answered" and not answer:
raise ValueError("An answered question requires a non-empty answer")
bank = load_bank(workspace)
question = next((item for item in bank["questions"] if item["id"] == identifier), None)
if question is None:
raise ValueError(f"Question not found: {identifier}")
question["status"] = status
question["answer"] = answer
question["answered_at"] = timestamp()
question["reviewed_at"] = ""
question["review_note"] = ""
bank["updated_at"] = timestamp()
validate_bank(bank)
write_json(question_path(workspace), bank)
bank["_path"] = str(question_path(workspace))
return summary(bank)
def review_questions(workspace: Path, input_path: Path) -> dict[str, Any]:
payload = load_json(input_path)
reviews = payload.get("reviews") if isinstance(payload, dict) else None
if not isinstance(reviews, list) or not reviews:
raise ValueError("Review input must contain a non-empty reviews array")
bank = load_bank(workspace)
by_id = {question["id"]: question for question in bank["questions"]}
now = timestamp()
for index, review in enumerate(reviews):
if not isinstance(review, dict):
raise ValueError(f"reviews[{index}] must be an object")
identifier = clean_text(review.get("id"))
if identifier not in by_id:
raise ValueError(f"Review question not found: {identifier}")
status = clean_text(review.get("status")).lower()
if status not in {"resolved", "not_applicable"}:
raise ValueError(f"Review {identifier} status must be resolved or not_applicable")
note = ensure_text(review.get("review_note"), f"Review {identifier}.review_note", 5, 500)
evidence_ids = review.get("related_evidence_ids") or []
if not isinstance(evidence_ids, list) or not all(isinstance(item, str) for item in evidence_ids):
raise ValueError(f"Review {identifier}.related_evidence_ids must be an array of strings")
question = by_id[identifier]
question["status"] = status
question["reviewed_at"] = now
question["review_note"] = note
question["related_evidence_ids"] = [clean_text(item) for item in evidence_ids if clean_text(item)]
bank["updated_at"] = now
validate_bank(bank)
write_json(question_path(workspace), bank)
bank["_path"] = str(question_path(workspace))
return summary(bank)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--workspace", type=Path, required=True)
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("init")
subparsers.add_parser("validate")
subparsers.add_parser("summary")
generate_parser = subparsers.add_parser("generate")
generate_parser.add_argument("--input", type=Path, required=True)
generate_parser.add_argument("--max-questions", type=int, default=12)
respond_parser = subparsers.add_parser("respond")
respond_parser.add_argument("--id", required=True)
respond_parser.add_argument("--status", required=True, choices=sorted(RESPONSE_STATUSES))
respond_parser.add_argument("--answer", default="")
review_parser = subparsers.add_parser("review")
review_parser.add_argument("--input", type=Path, required=True)
return parser.parse_args()
def main() -> int:
args = parse_args()
workspace = args.workspace.expanduser().resolve()
if args.command == "init":
bank = load_bank(workspace)
bank["_path"] = str(question_path(workspace))
result = summary(bank)
elif args.command == "validate":
bank = load_bank(workspace)
bank["_path"] = str(question_path(workspace))
result = {"valid": True, **summary(bank)}
elif args.command == "summary":
bank = load_bank(workspace)
bank["_path"] = str(question_path(workspace))
result = summary(bank)
elif args.command == "generate":
result = generate_questions(workspace, args.input.expanduser().resolve(), args.max_questions)
elif args.command == "respond":
result = respond(workspace, args.id, args.status, args.answer)
elif args.command == "review":
result = review_questions(workspace, args.input.expanduser().resolve())
else:
raise AssertionError(f"Unhandled command: {args.command}")
print(json.dumps(result, indent=2, ensure_ascii=True))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except ValueError as error:
raise SystemExit(str(error)) from error
SHA-256: 795693b5a2dac735f4f1ec3cb7e0047f3022d1c3c0b9ad9b46f8604453017c69