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>
121 lines
3.4 KiB
Python
121 lines
3.4 KiB
Python
"""Small numeric and time helpers shared by the detectors and the report.
|
|
|
|
These are pure functions over plain numbers, timestamps and token totals — no
|
|
knowledge of what a candidate is. Keeping them here leaves
|
|
:mod:`python_pkg.session_autopsy.detectors` to the heuristics themselves.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta
|
|
from difflib import SequenceMatcher
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from python_pkg.session_autopsy.records import SessionRecord
|
|
|
|
TRAILING_WEEKS = 8
|
|
MAX_SIMILARITY_INVOCATIONS = 5
|
|
MIN_SLOPE_SAMPLES = 2
|
|
|
|
|
|
def parse_timestamp(value: str | None) -> datetime | None:
|
|
"""Parse an ISO-8601 transcript timestamp (with trailing ``Z``).
|
|
|
|
Args:
|
|
value: The timestamp string, or None.
|
|
|
|
Returns:
|
|
A timezone-aware datetime, or None when absent/invalid.
|
|
"""
|
|
if not value:
|
|
return None
|
|
try:
|
|
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def per_turn_avg(records: list[SessionRecord]) -> int:
|
|
"""Average output+cache-write tokens per assistant turn, corpus-wide.
|
|
|
|
Args:
|
|
records: All session records.
|
|
|
|
Returns:
|
|
The average, or 0 for an empty corpus.
|
|
"""
|
|
turns = sum(record.counts.assistant_msgs for record in records)
|
|
if turns == 0:
|
|
return 0
|
|
spent = sum(record.tokens.output + record.tokens.cache_write for record in records)
|
|
return spent // turns
|
|
|
|
|
|
def per_week(timestamps: list[datetime | None], now: datetime) -> float:
|
|
"""Occurrences per week over the trailing window.
|
|
|
|
Args:
|
|
timestamps: One entry per occurrence (None when the session had no
|
|
parseable start time; those never count as recent).
|
|
now: Current UTC time.
|
|
|
|
Returns:
|
|
Recent occurrences divided by :data:`TRAILING_WEEKS`.
|
|
"""
|
|
cutoff = now - timedelta(weeks=TRAILING_WEEKS)
|
|
recent = sum(1 for stamp in timestamps if stamp is not None and stamp >= cutoff)
|
|
return recent / TRAILING_WEEKS
|
|
|
|
|
|
def mean(values: list[float]) -> float:
|
|
"""Arithmetic mean, 0.0 for an empty list.
|
|
|
|
Args:
|
|
values: Sample values.
|
|
|
|
Returns:
|
|
The mean.
|
|
"""
|
|
if not values:
|
|
return 0.0
|
|
return sum(values) / len(values)
|
|
|
|
|
|
def mean_pairwise_similarity(sequences: list[list[str]]) -> float:
|
|
"""Mean SequenceMatcher ratio over the first few invocation pairs.
|
|
|
|
Args:
|
|
sequences: One Bash-signature sequence per invocation.
|
|
|
|
Returns:
|
|
Mean similarity in [0, 1]; 0.0 when fewer than two non-empty
|
|
sequences exist (nothing to compare means no evidence of stability).
|
|
"""
|
|
sample = [seq for seq in sequences if seq][:MAX_SIMILARITY_INVOCATIONS]
|
|
ratios = [
|
|
SequenceMatcher(None, sample[i], sample[j]).ratio()
|
|
for i in range(len(sample))
|
|
for j in range(i + 1, len(sample))
|
|
]
|
|
return mean(ratios)
|
|
|
|
|
|
def slope(values: list[float]) -> float:
|
|
"""Least-squares slope of evenly spaced samples.
|
|
|
|
Args:
|
|
values: One sample per week, in order.
|
|
|
|
Returns:
|
|
Tokens-per-turn change per week; 0.0 with fewer than two samples.
|
|
"""
|
|
count = len(values)
|
|
if count < MIN_SLOPE_SAMPLES:
|
|
return 0.0
|
|
mean_x = (count - 1) / 2
|
|
mean_y = sum(values) / count
|
|
numerator = sum((i - mean_x) * (val - mean_y) for i, val in enumerate(values))
|
|
denominator = sum((i - mean_x) ** 2 for i in range(count))
|
|
return numerator / denominator
|