mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-12 22:50:52 +01:00
The transcript analyzer behind /compile-candidate lived only in the working tree. Untracked meant unlinted — pre-commit only inspects files handed to it — so nothing had ever checked it, and it had been abandoned mid-refactor with every parse raising AttributeError: '_Accumulator' object has no attribute 'first_timestamp'. The accumulator had been moved onto the grouped records dataclasses, but three increment sites and parse_session still read flat attributes that no longer existed. parse_session now hands over acc.meta and acc.counts directly; only subagent_count comes from elsewhere, since no transcript line carries it. Two modules broke the 500-line limit, so they were split along seams that were already there. signatures.py takes the text-signature helpers — traces.py was importing them from parse.py, making parse an accidental hub, and now neither imports the other. stats.py takes the pure number and time helpers. parse.py 521 -> 458, detectors.py 559 -> 467. The structural lint findings were refactored, not configured away. _counter_candidates took eight arguments; the five naming which counter field to mine and at what thresholds became a frozen _CounterSpec, so both call sites are one line. _extract_skill carried in_span and a line budget as loop variables across thirteen branches; a _SpanBuffer owns that state now, leaving the walk at four. record() took twelve flat kwargs and now takes one override per record block, with session_meta() so a partial meta override keeps the standard timestamp — a bare SessionMeta(slug=...) would have nulled started_at and quietly changed the per-week arithmetic. All 36 call sites rewritten by an AST pass. tests/factories.py moved into conftest.py, where every other package here keeps shared builders and where name-tests-test does not object. 965 tests pass at 100% statement and branch coverage, every hook is green, and no suppression was added anywhere. `candidates` and `traces skill-finish` both run against the real corpus. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
393 lines
12 KiB
Python
393 lines
12 KiB
Python
"""Export compact transcript excerpts for a candidate.
|
|
|
|
This is what keeps the LLM compile step cheap: instead of re-reading multi-MB
|
|
transcripts, /compile-candidate reads a few hundred normalized lines.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
from python_pkg.session_autopsy.signatures import command_signature, normalize_signature
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
|
|
from python_pkg.session_autopsy.detectors import Candidate
|
|
from python_pkg.session_autopsy.records import SessionRecord
|
|
|
|
MAX_LINES_PER_SPAN = 200
|
|
MAX_TEXT_CHARS = 200
|
|
MAX_RESULT_LINES = 3
|
|
MAX_LINE_CHARS = 160
|
|
MAX_ERR_HITS_PER_SESSION = 5
|
|
|
|
|
|
def render_traces(
|
|
candidate: Candidate,
|
|
records_by_id: dict[str, SessionRecord],
|
|
max_invocations: int,
|
|
) -> str:
|
|
"""Render trace excerpts for a candidate across its evidence sessions.
|
|
|
|
Args:
|
|
candidate: The candidate to trace.
|
|
records_by_id: Stored records keyed by session id (for paths).
|
|
max_invocations: Stop after this many captured spans/hits.
|
|
|
|
Returns:
|
|
The full trace document.
|
|
"""
|
|
lines = [f"# traces {candidate.id} — {candidate.title}", ""]
|
|
captured = 0
|
|
for session_id in candidate.session_ids:
|
|
record = records_by_id.get(session_id)
|
|
if record is None:
|
|
continue
|
|
if captured >= max_invocations:
|
|
break
|
|
session_lines = _trace_session(candidate, record)
|
|
if session_lines:
|
|
captured += 1
|
|
lines.append(
|
|
f"## session {session_id} ({record.meta.started_at or 'undated'})"
|
|
)
|
|
lines.extend(session_lines)
|
|
lines.append("")
|
|
if captured == 0:
|
|
lines.append("no matching spans found (transcripts may have been deleted)")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def _trace_session(candidate: Candidate, record: SessionRecord) -> list[str]:
|
|
"""Extract this candidate's excerpt lines from one session.
|
|
|
|
Args:
|
|
candidate: The candidate to trace.
|
|
record: The session's stored record (holds the transcript path).
|
|
|
|
Returns:
|
|
Excerpt lines, or an empty list when nothing matches.
|
|
"""
|
|
path = _existing_path(record)
|
|
if path is None:
|
|
return []
|
|
extractors = {
|
|
"skill": _extract_skill,
|
|
"err": _extract_error,
|
|
"ngram": _extract_ngram,
|
|
"prompt": _extract_prompt,
|
|
}
|
|
return extractors[candidate.kind](path, candidate.sig)
|
|
|
|
|
|
def _existing_path(record: SessionRecord) -> Path | None:
|
|
"""Resolve the record's transcript path if it still exists.
|
|
|
|
Args:
|
|
record: The stored session record.
|
|
|
|
Returns:
|
|
The path, or None when the transcript is gone.
|
|
"""
|
|
path = Path(record.transcript_path)
|
|
if path.is_file():
|
|
return path
|
|
return None
|
|
|
|
|
|
def _iter_lines(path: Path) -> Iterator[dict[str, object]]:
|
|
"""Yield parsed dict lines of a transcript, skipping malformed ones.
|
|
|
|
Args:
|
|
path: Transcript path.
|
|
|
|
Yields:
|
|
Each line's parsed dict.
|
|
"""
|
|
with path.open(encoding="utf-8", errors="replace") as handle:
|
|
for raw_line in handle:
|
|
stripped = raw_line.strip()
|
|
if not stripped:
|
|
continue
|
|
try:
|
|
obj = json.loads(stripped)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
if isinstance(obj, dict):
|
|
yield obj
|
|
|
|
|
|
def _clip(text: str, limit: int = MAX_LINE_CHARS) -> str:
|
|
"""Single-line clip for trace output.
|
|
|
|
Args:
|
|
text: Raw text.
|
|
limit: Character cap.
|
|
|
|
Returns:
|
|
The first line, capped.
|
|
"""
|
|
first = text.strip().splitlines()[0] if text.strip() else ""
|
|
return first[:limit]
|
|
|
|
|
|
def _content_blocks(obj: dict[str, object]) -> list[dict[str, object]]:
|
|
"""Return a line's message content blocks.
|
|
|
|
Args:
|
|
obj: A parsed transcript line.
|
|
|
|
Returns:
|
|
The content block dicts, or an empty list.
|
|
"""
|
|
message = obj.get("message")
|
|
if not isinstance(message, dict):
|
|
return []
|
|
content = message.get("content")
|
|
if not isinstance(content, list):
|
|
return []
|
|
return [block for block in content if isinstance(block, dict)]
|
|
|
|
|
|
def _is_external_prompt(obj: dict[str, object]) -> bool:
|
|
"""Whether a line is a real typed user prompt.
|
|
|
|
Args:
|
|
obj: A parsed transcript line.
|
|
|
|
Returns:
|
|
True for external, non-sidechain, string-content user lines.
|
|
"""
|
|
if (
|
|
obj.get("type") != "user"
|
|
or obj.get("userType") != "external"
|
|
or obj.get("isSidechain") is True
|
|
):
|
|
return False
|
|
message = obj.get("message")
|
|
return isinstance(message, dict) and isinstance(message.get("content"), str)
|
|
|
|
|
|
def _result_texts(obj: dict[str, object]) -> list[str]:
|
|
"""Collect the textual parts of a line's toolUseResult.
|
|
|
|
Args:
|
|
obj: A parsed transcript line.
|
|
|
|
Returns:
|
|
Zero or more raw result texts.
|
|
"""
|
|
result = obj.get("toolUseResult")
|
|
texts = []
|
|
if isinstance(result, str):
|
|
texts.append(result)
|
|
elif isinstance(result, dict):
|
|
for key in ("stderr", "stdout"):
|
|
value = result.get(key)
|
|
if isinstance(value, str) and value:
|
|
texts.append(value)
|
|
return texts
|
|
|
|
|
|
class _SpanBuffer:
|
|
"""Excerpt lines for the skill spans found in one transcript.
|
|
|
|
Owning ``in_span`` and the per-span line budget here is what keeps
|
|
:func:`_extract_skill` down to a readable walk: every "am I inside a span and
|
|
is there room left" question is answered by :meth:`add`.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
"""Start outside any span with nothing captured."""
|
|
self.lines: list[str] = []
|
|
self.in_span = False
|
|
self._span_lines = 0
|
|
|
|
def start(self, *, matched: bool, header: str) -> None:
|
|
"""Open a span for a Skill invocation, or leave the one in progress.
|
|
|
|
Args:
|
|
matched: Whether the invoked skill is the one being excerpted.
|
|
header: The span's first line, kept only when ``matched``.
|
|
"""
|
|
self.in_span = matched
|
|
self._span_lines = 0
|
|
if matched:
|
|
self.lines.append(header)
|
|
|
|
def close(self) -> None:
|
|
"""End the current span, if any."""
|
|
self.in_span = False
|
|
|
|
def add(self, line: str) -> None:
|
|
"""Capture one line, inside a span and within its budget.
|
|
|
|
Args:
|
|
line: The formatted excerpt line.
|
|
"""
|
|
if self.in_span and self._span_lines < MAX_LINES_PER_SPAN:
|
|
self._span_lines += 1
|
|
self.lines.append(line)
|
|
|
|
|
|
def _capture_results(span: _SpanBuffer, obj: dict[str, object]) -> None:
|
|
"""Capture the head of one tool result.
|
|
|
|
Args:
|
|
span: Buffer for the span in progress.
|
|
obj: A ``user`` transcript line carrying a tool result.
|
|
"""
|
|
for text in _result_texts(obj)[:1]:
|
|
for result_line in text.splitlines()[:MAX_RESULT_LINES]:
|
|
span.add(f"RESULT | {_clip(result_line)}")
|
|
|
|
|
|
def _capture_block(
|
|
span: _SpanBuffer, block: dict[str, object], skill_name: str
|
|
) -> None:
|
|
"""Capture one assistant content block, opening a span when Skill runs.
|
|
|
|
Args:
|
|
span: Buffer for the span in progress.
|
|
block: One content block of an ``assistant`` line.
|
|
skill_name: The skill being excerpted.
|
|
"""
|
|
block_type = block.get("type")
|
|
if block_type == "tool_use":
|
|
name = str(block.get("name") or "?")
|
|
if name == "Skill":
|
|
tool_input = block.get("input")
|
|
invoked = tool_input.get("skill") if isinstance(tool_input, dict) else None
|
|
span.start(
|
|
matched=invoked == skill_name, header=f"TOOL Skill | {skill_name}"
|
|
)
|
|
return
|
|
payload = _clip(json.dumps(block.get("input", {}))[:MAX_LINE_CHARS])
|
|
span.add(f"TOOL {name} | {payload}")
|
|
elif block_type == "text":
|
|
text = block.get("text")
|
|
if isinstance(text, str) and text.strip():
|
|
span.add(f"TEXT | {_clip(text, MAX_TEXT_CHARS)}")
|
|
|
|
|
|
def _extract_skill(path: Path, skill_name: str) -> list[str]:
|
|
"""Excerpt every span of one skill in a transcript.
|
|
|
|
Args:
|
|
path: Transcript path.
|
|
skill_name: The skill to capture.
|
|
|
|
Returns:
|
|
TOOL/TEXT/RESULT lines for each span, capped per span.
|
|
"""
|
|
span = _SpanBuffer()
|
|
for obj in _iter_lines(path):
|
|
if _is_external_prompt(obj):
|
|
span.close()
|
|
elif obj.get("type") == "user":
|
|
if span.in_span:
|
|
_capture_results(span, obj)
|
|
elif obj.get("type") == "assistant":
|
|
for block in _content_blocks(obj):
|
|
_capture_block(span, block, skill_name)
|
|
return span.lines
|
|
|
|
|
|
def _extract_error(path: Path, sig: str) -> list[str]:
|
|
"""Excerpt occurrences of one error signature with the causing command.
|
|
|
|
Args:
|
|
path: Transcript path.
|
|
sig: The normalized error signature to match.
|
|
|
|
Returns:
|
|
CMD/ERR line pairs, capped per session.
|
|
"""
|
|
lines: list[str] = []
|
|
last_bash = ""
|
|
hits = 0
|
|
for obj in _iter_lines(path):
|
|
for block in _content_blocks(obj) if obj.get("type") == "assistant" else []:
|
|
if block.get("type") == "tool_use" and block.get("name") == "Bash":
|
|
tool_input = block.get("input")
|
|
command = (
|
|
tool_input.get("command") if isinstance(tool_input, dict) else None
|
|
)
|
|
if isinstance(command, str):
|
|
last_bash = command
|
|
if obj.get("type") != "user" or hits >= MAX_ERR_HITS_PER_SESSION:
|
|
continue
|
|
for text in _result_texts(obj):
|
|
for result_line in text.splitlines():
|
|
if (
|
|
normalize_signature(result_line) == sig
|
|
and hits < MAX_ERR_HITS_PER_SESSION
|
|
):
|
|
hits += 1
|
|
lines.append(f"CMD | {_clip(last_bash)}")
|
|
lines.append(f"ERR | {_clip(result_line)}")
|
|
return lines
|
|
|
|
|
|
def _extract_ngram(path: Path, sig: str) -> list[str]:
|
|
"""Excerpt the first occurrence of a command sequence.
|
|
|
|
Args:
|
|
path: Transcript path.
|
|
sig: The " → "-joined command-signature gram.
|
|
|
|
Returns:
|
|
The raw commands of the first matching window, or empty.
|
|
"""
|
|
gram = sig.split(" → ")
|
|
window: list[tuple[str, str]] = []
|
|
for obj in _iter_lines(path):
|
|
for block in _content_blocks(obj) if obj.get("type") == "assistant" else []:
|
|
if block.get("type") != "tool_use" or block.get("name") != "Bash":
|
|
continue
|
|
tool_input = block.get("input")
|
|
command = (
|
|
tool_input.get("command") if isinstance(tool_input, dict) else None
|
|
)
|
|
if not isinstance(command, str):
|
|
continue
|
|
window.append((command_signature(command), command))
|
|
window = window[-len(gram) :]
|
|
if [entry[0] for entry in window] == gram:
|
|
return [f"CMD | {_clip(raw)}" for _, raw in window]
|
|
return []
|
|
|
|
|
|
def _extract_prompt(path: Path, sig: str) -> list[str]:
|
|
"""Excerpt occurrences of a repeated typed prompt and what followed.
|
|
|
|
Args:
|
|
path: Transcript path.
|
|
sig: The normalized prompt signature.
|
|
|
|
Returns:
|
|
PROMPT lines followed by the next few tool names.
|
|
"""
|
|
lines: list[str] = []
|
|
following = 0
|
|
for obj in _iter_lines(path):
|
|
if _is_external_prompt(obj):
|
|
message = obj.get("message")
|
|
content = str(message.get("content")) if isinstance(message, dict) else ""
|
|
if normalize_signature(content) == sig:
|
|
lines.append(f"PROMPT | {_clip(content, MAX_TEXT_CHARS)}")
|
|
following = MAX_RESULT_LINES
|
|
else:
|
|
following = 0
|
|
continue
|
|
if following <= 0:
|
|
continue
|
|
for block in _content_blocks(obj) if obj.get("type") == "assistant" else []:
|
|
if block.get("type") == "tool_use" and following > 0:
|
|
following -= 1
|
|
lines.append(f"THEN | {block.get('name')}")
|
|
return lines
|