mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 22:27:01 +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>
68 lines
2.1 KiB
Python
68 lines
2.1 KiB
Python
"""Stable text signatures for messages, commands and errors.
|
|
|
|
Two modules need these: :mod:`python_pkg.session_autopsy.parse` while streaming
|
|
a transcript, and :mod:`python_pkg.session_autopsy.traces` when re-reading one to
|
|
pull evidence. They live here rather than in either of those so neither has to
|
|
import the other.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
MAX_SIG_LEN = 120
|
|
|
|
_PATH_RE = re.compile(r"(?:~|/)[\w.@+/-]{4,}")
|
|
_HEX_RE = re.compile(r"[0-9a-fA-F]{8,}")
|
|
_NUM_RE = re.compile(r"\b\d+\b")
|
|
_WS_RE = re.compile(r"\s+")
|
|
# Wrapper commands whose real command is the following word.
|
|
_WRAPPERS = frozenset({"cd", "sudo", "env", "nice", "command", "timeout"})
|
|
|
|
|
|
def normalize_signature(text: str) -> str:
|
|
"""Collapse a message/command/error into a stable cross-session signature.
|
|
|
|
Takes the first line only and masks paths, long hex runs, and numbers so
|
|
that reoccurrences of the same event count as one signature.
|
|
|
|
Args:
|
|
text: Raw text (may be multi-line).
|
|
|
|
Returns:
|
|
The normalized first line, truncated to :data:`MAX_SIG_LEN` chars.
|
|
"""
|
|
stripped = text.strip()
|
|
if not stripped:
|
|
return ""
|
|
first = stripped.splitlines()[0]
|
|
first = _PATH_RE.sub("<PATH>", first)
|
|
first = _HEX_RE.sub("<HEX>", first)
|
|
first = _NUM_RE.sub("<N>", first)
|
|
return _WS_RE.sub(" ", first).strip()[:MAX_SIG_LEN]
|
|
|
|
|
|
def command_signature(command: str) -> str:
|
|
"""Reduce a Bash command to its leading command word(s).
|
|
|
|
Wrapper commands (``cd``, ``sudo``, ``timeout`` …) keep their first
|
|
non-flag, non-numeric argument so sequences stay meaningful.
|
|
|
|
Args:
|
|
command: The raw (possibly multi-line) Bash tool input.
|
|
|
|
Returns:
|
|
A short signature such as ``"git"`` or ``"cd <PATH>"``, or ``""``.
|
|
"""
|
|
normalized = normalize_signature(command)
|
|
words = normalized.split()
|
|
if not words:
|
|
return ""
|
|
head = words[0]
|
|
if head not in _WRAPPERS:
|
|
return head
|
|
for word in words[1:]:
|
|
if not word.startswith(("-", "<N>")):
|
|
return f"{head} {word}"
|
|
return head
|