#!/usr/bin/env python3 """ PPT Master - Automatic Workflow Transcript Internal runtime helper that records a project-scoped Python tool's command envelope and material text outcomes in an existing project workflow log. Dependencies: None (standard library only) """ from __future__ import annotations import atexit import json import os import sys import threading import time import uuid from collections import deque from collections.abc import Iterable from datetime import datetime, timezone from pathlib import Path from typing import TextIO WORKFLOW_LOG_RELATIVE_PATH = Path("validation/workflow.log") PROJECT_PATH_ENV = "PPT_MASTER_PROJECT_PATH" DISABLE_TRANSCRIPT_ENV = "PPT_MASTER_DISABLE_WORKFLOW_TRANSCRIPT" _EXCLUDED_ENTRYPOINTS = {"workflow_log.py"} _CRITICAL_MARKERS = ("[ERROR]", "[FAIL]") _ALWAYS_RETAIN_MARKERS = ( "[POSTFLIGHT]", "[PPTX]", "[REPORT]", "[Done]", "[OK] Done", ) _SUMMARY_MARKER = "[SUMMARY]" _SUMMARY_STOP_MARKERS = ("[TIP]",) _WARNING_MARKERS = ("[WARN]", "[WARNING]") _OK_MARKER = "[OK]" _WARNING_SAMPLE_LIMIT = 4 _OK_SAMPLE_LIMIT = 1 _STDERR_SAMPLE_LIMIT = 8 _STDERR_TAIL_LIMIT = 4 _CRITICAL_CONTEXT_LIMIT = 4 _SUMMARY_CONTEXT_LIMIT = 12 _ACTIVE_TRANSCRIPT: _AutomaticTranscript | None = None def _utc_timestamp() -> str: """Return a compact UTC timestamp.""" return ( datetime.now(timezone.utc) .isoformat(timespec="milliseconds") .replace("+00:00", "Z") ) def _candidate_directory(value: str) -> Path | None: """Resolve one CLI value to an existing directory candidate.""" if not value or value.startswith("-") or "://" in value: return None try: raw_path = Path(value) except (OSError, ValueError): return None if not raw_path.exists() and raw_path.parent == Path("."): return None path = raw_path if not path.is_absolute(): path = Path.cwd() / path while not path.exists() and path != path.parent: path = path.parent if path.is_file(): path = path.parent return path.resolve() def _find_project_root(argv: list[str]) -> Path | None: """Find the first ancestor that already owns a workflow log.""" explicit_project = os.environ.get(PROJECT_PATH_ENV, "").strip() values = [explicit_project, *argv[1:], str(Path.cwd())] for value in values: directory = _candidate_directory(value) if directory is None: continue for candidate in (directory, *directory.parents): if (candidate / WORKFLOW_LOG_RELATIVE_PATH).is_file(): return candidate return None class _AutomaticTranscript: """Append a bounded command/outcome audit without changing the owning tool.""" def __init__( self, handle: TextIO, original_stderr: TextIO, argv: list[str], ) -> None: self.handle = handle self.original_stderr = original_stderr self.argv = argv self.run_id = uuid.uuid4().hex[:12] self.started_clock = time.monotonic() self.pending = {"stdout": "", "stderr": ""} self.observed_lines = 0 self.retained_lines = 0 self.omitted_lines = 0 self.warning_samples = 0 self.ok_samples = 0 self.stderr_samples = 0 self.stderr_tail: deque[str] = deque(maxlen=_STDERR_TAIL_LIMIT) self.critical_context_remaining = 0 self.summary_context_remaining = 0 self.current_record_stream: str | None = None self.lock = threading.RLock() self.enabled = True def _disable(self, exc: OSError | UnicodeError | ValueError) -> None: """Stop transcript writes without changing tool execution.""" self.enabled = False try: self.original_stderr.write( f"[WARN] Workflow audit recording stopped: {exc}\n" ) self.original_stderr.flush() except (OSError, ValueError): pass def _write(self, text: str) -> None: """Append and flush one complete audit record.""" if not self.enabled: return try: self.handle.write(text) self.handle.flush() except (OSError, UnicodeError, ValueError) as exc: self._disable(exc) def start(self) -> None: """Write the Python-command envelope.""" self._write( f"\n=== {_utc_timestamp()} PYTHON run={self.run_id} ===\n" f"cwd: {Path.cwd()}\n" f"argv: {json.dumps(self.argv, ensure_ascii=False)}\n" ) @staticmethod def _is_decoration(line: str) -> bool: """Return whether a non-empty line is only visual separator glyphs.""" stripped = line.strip() return bool(stripped) and set(stripped) <= {"-", "=", "_"} def _should_retain(self, stream_name: str, line: str) -> bool: """Select a bounded set of explicit outcomes for the cold audit log.""" stripped = line.strip() if not stripped or self._is_decoration(stripped): return False if _SUMMARY_MARKER in stripped: self.summary_context_remaining = _SUMMARY_CONTEXT_LIMIT return True if any(marker in stripped for marker in _SUMMARY_STOP_MARKERS): self.summary_context_remaining = 0 return False if self.summary_context_remaining > 0: self.summary_context_remaining -= 1 return True if any(marker in stripped for marker in _CRITICAL_MARKERS): self.critical_context_remaining = _CRITICAL_CONTEXT_LIMIT return True if self.critical_context_remaining > 0: self.critical_context_remaining -= 1 return True if any(marker in stripped for marker in _ALWAYS_RETAIN_MARKERS): return True if any(marker in stripped for marker in _WARNING_MARKERS): if self.warning_samples < _WARNING_SAMPLE_LIMIT: self.warning_samples += 1 return True return False if _OK_MARKER in stripped: if self.ok_samples < _OK_SAMPLE_LIMIT: self.ok_samples += 1 return True return False if stream_name == "stderr" and self.stderr_samples < _STDERR_SAMPLE_LIMIT: self.stderr_samples += 1 return True return False def _record_line(self, stream_name: str, line: str) -> None: """Record one material logical line or account for its omission.""" if not line.strip(): return self.observed_lines += 1 if not self._should_retain(stream_name, line): self.omitted_lines += 1 if stream_name == "stderr" and not self._is_decoration(line): self.stderr_tail.append(line) return if stream_name == "stderr": self.stderr_tail.clear() self.retained_lines += 1 if self.current_record_stream != stream_name: self._write(f"{stream_name}:\n") self.current_record_stream = stream_name self._write(f" {line}\n") def write_stream(self, stream_name: str, text: str) -> None: """Buffer fragments and write complete logical lines.""" if not text or not self.enabled: return with self.lock: pending = self.pending[stream_name] + text while "\n" in pending: line, pending = pending.split("\n", 1) self._record_line(stream_name, line) self.pending[stream_name] = pending def flush_stream(self, stream_name: str) -> None: """Flush a partial logical line when the owning stream flushes.""" with self.lock: if not self.enabled: self.pending[stream_name] = "" return pending = self.pending[stream_name] if pending: self._record_line(stream_name, pending) self.pending[stream_name] = "" def close(self) -> None: """Flush pending output and close the command envelope.""" with self.lock: if not self.enabled: return self.flush_stream("stdout") self.flush_stream("stderr") if self.stderr_tail: self._write("stderr-tail:\n") for line in self.stderr_tail: self._write(f" {line}\n") self.retained_lines += len(self.stderr_tail) self.omitted_lines -= len(self.stderr_tail) elapsed_ms = int((time.monotonic() - self.started_clock) * 1000) self._write( f"=== {_utc_timestamp()} END run={self.run_id} " f"elapsed_ms={elapsed_ms} output_lines={self.observed_lines} " f"retained={self.retained_lines} omitted={self.omitted_lines} ===\n" ) self.enabled = False try: self.handle.close() except (OSError, ValueError) as exc: self._disable(exc) class _TeeTextIO: """Forward text writes while offering them to the audit recorder.""" def __init__( self, primary: TextIO, transcript: _AutomaticTranscript, stream_name: str, ) -> None: self.primary = primary self.transcript = transcript self.stream_name = stream_name self.lock = threading.RLock() def write(self, text: str) -> int: """Write to the original stream, then mirror the same text.""" with self.lock: written = self.primary.write(text) self.transcript.write_stream(self.stream_name, text) return written def writelines(self, lines: Iterable[str]) -> None: """Forward a sequence through the ordinary write path.""" for line in lines: self.write(line) def flush(self) -> None: """Flush the original stream without splitting a logical log line.""" with self.lock: self.primary.flush() def __getattr__(self, name: str) -> object: """Delegate the remaining text-stream interface.""" return getattr(self.primary, name) def install_auto_transcript(argv: list[str] | None = None) -> Path | None: """Record a project-scoped Python command when its project log exists.""" global _ACTIVE_TRANSCRIPT effective_argv = list(sys.argv if argv is None else argv) if _ACTIVE_TRANSCRIPT is not None: return None if os.environ.get(DISABLE_TRANSCRIPT_ENV): return None if Path(effective_argv[0]).name in _EXCLUDED_ENTRYPOINTS: return None project_root = _find_project_root(effective_argv) if project_root is None: return None log_path = project_root / WORKFLOW_LOG_RELATIVE_PATH original_stdout = sys.stdout original_stderr = sys.stderr try: handle = log_path.open( "a", encoding="utf-8", errors="replace", newline="", ) except OSError as exc: try: original_stderr.write( f"[WARN] Workflow audit unavailable ({log_path}): {exc}\n" ) original_stderr.flush() except (OSError, ValueError): pass return None transcript = _AutomaticTranscript( handle, original_stderr, [sys.executable, *effective_argv], ) transcript.start() _ACTIVE_TRANSCRIPT = transcript sys.stdout = _TeeTextIO(original_stdout, transcript, "stdout") sys.stderr = _TeeTextIO(original_stderr, transcript, "stderr") atexit.register(transcript.close) return log_path