#!/usr/bin/env python3 """Export the first user prompt from each Nucleic transcript for purpose labeling.""" from __future__ import annotations import argparse import hashlib import json import unicodedata from collections import Counter from datetime import datetime, timezone from pathlib import Path from typing import Any LABELS = ( "planning", "backendImpl", "frontendImpl", "quickFix", "refactor", "debugging", "review", "writing", ) LABEL_DEFINITIONS = { "planning": "Architecture, design, migration strategy, or multi-step project planning.", "backendImpl": "Server, API, data, algorithm, systems, or CLI implementation.", "frontendImpl": "UI, views, styling, components, layout, or animation implementation.", "quickFix": "A typo, one-liner, config tweak, or small contained bug fix.", "refactor": "Restructuring, renaming, extraction, or cleanup without behavior change.", "debugging": "Diagnosing a crash, failure, regression, or unexplained behavior.", "review": "Reviewing, explaining, auditing, or answering questions about existing code.", "writing": "Documentation, commit messages, summaries, formatting, or other prose work.", } class ExportError(RuntimeError): """The transcript export could not be completed safely.""" def normalized_prompt(prompt: str) -> str: """Match the classifier dataset's stable prompt normalization.""" return " ".join(unicodedata.normalize("NFKC", prompt).split()) def prompt_hash(prompt: str) -> str: return hashlib.sha256(normalized_prompt(prompt).casefold().encode("utf-8")).hexdigest() def canonical_json(value: dict[str, Any]) -> str: return json.dumps(value, ensure_ascii=False, separators=(",", ":")) def read_session(transcript: Path) -> tuple[dict[str, Any], str | None]: header: dict[str, Any] | None = None first_prompt: str | None = None with transcript.open(encoding="utf-8") as handle: for line_number, line in enumerate(handle, start=1): try: value = json.loads(line) except json.JSONDecodeError as error: raise ExportError( f"{transcript}:{line_number}: malformed JSON: {error.msg}" ) from error if not isinstance(value, dict): raise ExportError(f"{transcript}:{line_number}: expected a JSON object") if line_number == 1: header = value continue kind = value.get("kind") if isinstance(kind, dict) and kind.get("type") == "userText": text = kind.get("text") if not isinstance(text, str) or not text.strip(): raise ExportError( f"{transcript}:{line_number}: first userText has no prompt text" ) first_prompt = text break if header is None: raise ExportError(f"{transcript}: transcript is empty") return header, first_prompt def session_sort_key(record: dict[str, Any]) -> tuple[str, str]: return str(record.get("createdAt") or ""), str(record["sessionID"]) def export(sessions_dir: Path, output: Path, manifest_path: Path) -> dict[str, Any]: if not sessions_dir.is_dir(): raise ExportError(f"sessions directory does not exist: {sessions_dir}") records: list[dict[str, Any]] = [] omitted: list[dict[str, Any]] = [] backend_counts: Counter[str] = Counter() session_directories = sorted(path for path in sessions_dir.iterdir() if path.is_dir()) for session_dir in session_directories: transcript = session_dir / "transcript.jsonl" if not transcript.is_file(): raise ExportError(f"missing transcript: {transcript}") header, prompt = read_session(transcript) session_id = header.get("sessionID") if session_id != session_dir.name: raise ExportError( f"{transcript}: header sessionID {session_id!r} does not match directory" ) backend = str(header.get("backend") or "unknown") backend_counts[backend] += 1 common = { "sessionID": session_id, "createdAt": header.get("createdAt"), "backend": backend, } if prompt is None: omitted.append({**common, "reason": "no_user_text"}) continue records.append( { "schemaVersion": 1, **common, "promptHash": prompt_hash(prompt), "prompt": prompt, "purpose": None, "labelNotes": None, } ) records.sort(key=session_sort_key) omitted.sort(key=session_sort_key) output.parent.mkdir(parents=True, exist_ok=True) output.write_text( "".join(f"{canonical_json(record)}\n" for record in records), encoding="utf-8", ) output_sha256 = hashlib.sha256(output.read_bytes()).hexdigest() duplicate_session_prompts = sum( count - 1 for count in Counter(record["promptHash"] for record in records).values() if count > 1 ) manifest = { "schemaVersion": 1, "generatedAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), "source": { "kind": "nucleic_transcript_history", "sessionsDirectory": str(sessions_dir), "selection": "first non-empty AgentEvent kind=userText in each transcript", "deduplicated": False, }, "output": { "path": str(output), "format": "jsonl", "sha256": output_sha256, }, "counts": { "sessionDirectories": len(session_directories), "exportedPrompts": len(records), "sessionsWithoutUserPrompt": len(omitted), "duplicateSessionPrompts": duplicate_session_prompts, "backends": dict(sorted(backend_counts.items())), }, "labelingContract": { "field": "purpose", "unlabeledValue": None, "allowedValues": list(LABELS), "generalIsNotALabel": True, "definitions": LABEL_DEFINITIONS, "instruction": ( "Assign exactly one allowed purpose to every JSONL record. " "Preserve all other fields and the original record order." ), }, "sessionsWithoutUserPrompt": omitted, } manifest_path.parent.mkdir(parents=True, exist_ok=True) manifest_path.write_text( json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) return manifest def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--sessions-dir", type=Path, required=True, help="Nucleic Application Support sessions directory", ) parser.add_argument("--output", type=Path, required=True, help="Unlabeled JSONL output") parser.add_argument( "--manifest", type=Path, required=True, help="JSON manifest describing the output and labeling contract", ) return parser.parse_args() def main() -> None: args = parse_args() manifest = export( args.sessions_dir.expanduser().resolve(), args.output.expanduser().resolve(), args.manifest.expanduser().resolve(), ) print(json.dumps(manifest["counts"], sort_keys=True)) if __name__ == "__main__": main()