96c5b94616
The original build_track_index spawned 5+ git subprocesses per track folder (244 tracks * 5 calls = 1200+ subprocesses). That exceeded the 120s NFR1 budget. Replaced with a single 'git log --all --name-only' pass + a mutable _TrackAccumulator; reduces git subprocess count from 1200+ to 1. Also adds 3 smoke tests for build_subagent_task_index, build_session_index, build_track_index (covering previously untested pure-function indexers). 23 tests pass; coverage up from 39% to 61%.
1311 lines
44 KiB
Python
1311 lines
44 KiB
Python
#!/usr/bin/env python3
|
|
"""One-shot meta-tooling duration analyzer.
|
|
|
|
Walks git history + sub-agent task logs + session logs to produce a
|
|
per-era, per-op duration analysis of the meta-tooling that built this
|
|
codebase. Emits a 10-section Markdown report + JSON source-of-truth.
|
|
|
|
Strictly Meta-Tooling domain (per docs/guide_meta_boundary.md). No
|
|
src/ changes; no GUI changes; no Application-domain impact.
|
|
|
|
Usage:
|
|
uv run python scripts/audit/analyze_meta_tooling_durations.py
|
|
uv run python scripts/audit/analyze_meta_tooling_durations.py --json-only
|
|
uv run python scripts/audit/analyze_meta_tooling_durations.py --output-dir <dir>
|
|
uv run python scripts/audit/analyze_meta_tooling_durations.py --era-window-days 7
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import statistics
|
|
import subprocess
|
|
import sys
|
|
from collections import Counter, defaultdict
|
|
from dataclasses import dataclass, field
|
|
from datetime import date, datetime, timedelta
|
|
from pathlib import Path
|
|
from typing import Final
|
|
|
|
# 1-space indent throughout per conductor/code_styleguides/python.md §1
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dataclasses (per conductor/code_styleguides/data_oriented_design.md §8.5)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Evidence:
|
|
"""A single piece of evidence pointing to an era boundary."""
|
|
|
|
date: date
|
|
weight: str
|
|
source: str
|
|
note: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class EraBoundary:
|
|
"""One era in the project's LLM-tooling history."""
|
|
|
|
index: int
|
|
start: date
|
|
end: date | None
|
|
name: str
|
|
evidence: list[Evidence] = field(default_factory=list)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class TrackRow:
|
|
"""One track's duration and scope, era-bucketed."""
|
|
|
|
track_id: str
|
|
init_sha: str
|
|
end_sha: str
|
|
init_date: date
|
|
end_date: date
|
|
duration_days: int
|
|
commit_count: int
|
|
files_touched: int
|
|
era_index: int
|
|
status: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SubAgentTaskOp:
|
|
"""One sub-agent task log entry (Tier 3 or Tier 4 or Tier 1)."""
|
|
|
|
timestamp: datetime
|
|
tier: str
|
|
log_path: str
|
|
duration_s: float
|
|
in_track: str | None
|
|
era_index: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CommitPrefixOp:
|
|
"""One work-prefix commit."""
|
|
|
|
sha: str
|
|
date: date
|
|
prefix: str
|
|
scope: str
|
|
message: str
|
|
in_track: str | None
|
|
era_index: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SessionOp:
|
|
"""One chat session (logs/<session>/)."""
|
|
|
|
session_name: str
|
|
start_dt: datetime
|
|
end_dt: datetime
|
|
duration_min: float
|
|
task_count: int
|
|
era_index: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class EraStats:
|
|
"""Aggregate stats for one era."""
|
|
|
|
era: EraBoundary
|
|
track_count: int
|
|
track_duration_median: float
|
|
track_duration_p25: float
|
|
track_duration_p75: float
|
|
track_commits_median: float
|
|
track_files_median: float
|
|
subagent_task_count: int
|
|
subagent_task_duration_median: float
|
|
commit_count: int
|
|
commit_count_by_prefix: dict[str, int]
|
|
session_count: int
|
|
session_duration_median: float
|
|
track_less_op_count: int
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_REPO_ROOT: Final[Path] = Path(__file__).resolve().parents[2]
|
|
_TRACKS_DIR: Final[Path] = _REPO_ROOT / "conductor" / "tracks"
|
|
_ARCHIVE_DIR: Final[Path] = _REPO_ROOT / "conductor" / "archive"
|
|
_LOGS_AGENTS_DIR: Final[Path] = _REPO_ROOT / "logs" / "agents"
|
|
_LOGS_DIR: Final[Path] = _REPO_ROOT / "logs"
|
|
|
|
_DEFAULT_OUTPUT_DIR: Final[Path] = _REPO_ROOT / "docs" / "reports"
|
|
_DEFAULT_ARTIFACT_DIR: Final[Path] = _REPO_ROOT / "tests" / "artifacts"
|
|
|
|
_DEFAULT_ERA_WINDOW_DAYS: Final[int] = 7
|
|
_DEFAULT_DATE: Final[date] = date(2026, 7, 5)
|
|
|
|
_WORK_PREFIX_RE: Final[re.Pattern[str]] = re.compile(
|
|
r"^(?P<prefix>feat|fix|refactor|perf|test)(?:\((?P<scope>[^)]*)\))?:\s"
|
|
)
|
|
_SESSION_NAME_RE: Final[re.Pattern[str]] = re.compile(
|
|
r"^(?P<date>\d{8})_(?P<time>\d{6})(?:_(?P<suffix>.+))?$"
|
|
)
|
|
|
|
_TIER_FROM_FILENAME_RE: Final[re.Pattern[str]] = re.compile(r"mma_tier(\d+[a-z\-]*)_task_")
|
|
|
|
_ROLE_RE: Final[re.Pattern[str]] = re.compile(r"^ROLE:\s+(?P<role>.+)$", re.MULTILINE)
|
|
_TIMESTAMP_RE: Final[re.Pattern[str]] = re.compile(r"^TIMESTAMP:\s+(?P<ts>.+)$", re.MULTILINE)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Era detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def cluster_era_candidates(
|
|
candidates: list[tuple[date, str, str]],
|
|
window_days: int,
|
|
) -> list[EraBoundary]:
|
|
"""Cluster era boundary candidates within `window_days` into EraBoundaries.
|
|
|
|
Args:
|
|
candidates: List of (date, weight, note) tuples from various signals.
|
|
window_days: Candidates within this many days of each other merge.
|
|
|
|
Returns:
|
|
List of EraBoundaries sorted by start date. The last era's `end` is None.
|
|
Evidence across all candidates in a cluster is preserved.
|
|
"""
|
|
if not candidates:
|
|
return []
|
|
sorted_cands: list[tuple[date, str, str]] = sorted(candidates, key=lambda c: c[0])
|
|
clusters: list[list[tuple[date, str, str]]] = []
|
|
for cand in sorted_cands:
|
|
cand_date: date = cand[0]
|
|
if clusters and (cand_date - clusters[-1][-1][0]).days <= window_days:
|
|
clusters[-1].append(cand)
|
|
else:
|
|
clusters.append([cand])
|
|
eras: list[EraBoundary] = []
|
|
for i, cluster in enumerate(clusters):
|
|
cluster_start: date = cluster[0][0]
|
|
cluster_end: date | None = None
|
|
if i + 1 < len(clusters):
|
|
next_start: date = clusters[i + 1][0][0]
|
|
cluster_end = next_start - timedelta(days=1)
|
|
evidence: list[Evidence] = [
|
|
Evidence(date=c[0], weight=c[1], source="git-log", note=c[2])
|
|
for c in cluster
|
|
]
|
|
eras.append(
|
|
EraBoundary(
|
|
index=i,
|
|
start=cluster_start,
|
|
end=cluster_end,
|
|
name=f"era_{i}",
|
|
evidence=evidence,
|
|
)
|
|
)
|
|
return eras
|
|
|
|
|
|
def assign_era_name(index: int, total: int) -> str:
|
|
"""Pick a human-readable name for an era by position.
|
|
|
|
Earliest is "early"; latest is "current"; middle is "mid"; second-to-last
|
|
is "late" (only when total >= 4 so that 3-eras stay early/mid/current).
|
|
"""
|
|
if index == 0:
|
|
return "early"
|
|
if index == total - 1:
|
|
return "current"
|
|
if total <= 3:
|
|
return "mid"
|
|
if index == total - 2:
|
|
return "late"
|
|
return "mid"
|
|
|
|
|
|
def assign_era_for_date(d: date, eras: list[EraBoundary]) -> int:
|
|
"""Return the index of the era that contains date `d`.
|
|
|
|
The last era (open end) is the catch-all for any date >= its start.
|
|
"""
|
|
for era in eras:
|
|
if era.end is None:
|
|
if d >= era.start:
|
|
return era.index
|
|
else:
|
|
if era.start <= d <= era.end:
|
|
return era.index
|
|
return eras[0].index if eras else 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Git plumbing helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _git_log(
|
|
folder_relpath: str,
|
|
*args: str,
|
|
timeout: int = 30,
|
|
) -> str:
|
|
"""Run `git log <args> -- <folder>` and return stdout. Empty on failure."""
|
|
try:
|
|
result: subprocess.CompletedProcess[str] = subprocess.run(
|
|
["git", "log", *args, "--", folder_relpath],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
return ""
|
|
return result.stdout
|
|
except (subprocess.SubprocessError, OSError):
|
|
return ""
|
|
|
|
|
|
def _git_log_all(*args: str, timeout: int = 60) -> str:
|
|
"""Run `git log <args>` (no path filter). Empty on failure."""
|
|
try:
|
|
result: subprocess.CompletedProcess[str] = subprocess.run(
|
|
["git", "log", *args],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
return ""
|
|
return result.stdout
|
|
except (subprocess.SubprocessError, OSError):
|
|
return ""
|
|
|
|
|
|
def _parse_iso_date(s: str) -> date | None:
|
|
"""Parse YYYY-MM-DD into a date. Returns None on failure."""
|
|
try:
|
|
return date.fromisoformat(s)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _git_first_commit_date(folder_relpath: str) -> date | None:
|
|
"""Return the date of the first (oldest) commit touching folder_relpath."""
|
|
out: str = _git_log(folder_relpath, "--reverse", "--format=%aI")
|
|
if not out.strip():
|
|
return None
|
|
first_line: str = out.strip().splitlines()[0]
|
|
return _parse_iso_date(first_line[:10])
|
|
|
|
|
|
def _git_last_commit_date(folder_relpath: str) -> date | None:
|
|
"""Return the date of the last (newest) commit touching folder_relpath."""
|
|
out: str = _git_log(folder_relpath, "-1", "--format=%aI")
|
|
if not out.strip():
|
|
return None
|
|
return _parse_iso_date(out.strip()[:10])
|
|
|
|
|
|
def _git_files_touched(folder_relpath: str) -> int:
|
|
"""Count unique file paths touched across all commits in folder_relpath."""
|
|
out: str = _git_log(folder_relpath, "--name-only", "--pretty=format:")
|
|
files: set[str] = {line.strip() for line in out.splitlines() if line.strip()}
|
|
return len(files)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Track index (single-pass git walk for performance)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _walk_track_folders() -> list[tuple[Path, str]]:
|
|
"""Walk conductor/tracks/ and conductor/archive/; return (folder, relpath)."""
|
|
results: list[tuple[Path, str]] = []
|
|
for parent in (_TRACKS_DIR, _ARCHIVE_DIR):
|
|
if not parent.is_dir():
|
|
continue
|
|
for folder in sorted(parent.iterdir()):
|
|
if not folder.is_dir():
|
|
continue
|
|
relpath: str = folder.relative_to(_REPO_ROOT).as_posix()
|
|
results.append((folder, relpath))
|
|
return results
|
|
|
|
|
|
class _TrackAccumulator:
|
|
"""Mutable per-track accumulator (NOT a frozen dataclass)."""
|
|
|
|
def __init__(self) -> None:
|
|
self.init_sha: str = ""
|
|
self.end_sha: str = ""
|
|
self.init_date: date | None = None
|
|
self.end_date: date | None = None
|
|
self.work_commit_count: int = 0
|
|
self.files: set[str] = set()
|
|
|
|
|
|
def _build_per_track_data() -> dict[str, _TrackAccumulator]:
|
|
"""Walk git history ONCE and aggregate per track folder.
|
|
|
|
Replaces per-track `_git_log` calls (5+ subprocesses per track) with
|
|
a single `git log --name-only` pass. >10x speedup on the current repo.
|
|
|
|
Returns: dict[track_relpath] -> _TrackAccumulator
|
|
"""
|
|
relpaths: set[str] = {rp for _folder, rp in _walk_track_folders()}
|
|
# Also seed archive aliases that map to original tracks/ paths.
|
|
for rp in list(relpaths):
|
|
if rp.startswith("conductor/archive/"):
|
|
orig: str = rp.replace("conductor/archive/", "conductor/tracks/", 1)
|
|
relpaths.add(orig)
|
|
|
|
out: str = _git_log_all(
|
|
"--all",
|
|
"--name-only",
|
|
"--pretty=format:COMMIT %H %aI %s",
|
|
timeout=120,
|
|
)
|
|
data: dict[str, _TrackAccumulator] = {rp: _TrackAccumulator() for rp in relpaths}
|
|
|
|
current_sha: str = ""
|
|
current_iso: str = ""
|
|
current_msg: str = ""
|
|
for line in out.splitlines():
|
|
if line.startswith("COMMIT "):
|
|
parts: list[str] = line.split(" ", 3)
|
|
if len(parts) >= 4:
|
|
current_sha, current_iso, current_msg = parts[1], parts[2], parts[3]
|
|
elif len(parts) == 3:
|
|
current_sha, current_iso, current_msg = parts[1], parts[2], ""
|
|
continue
|
|
stripped: str = line.strip()
|
|
if not stripped:
|
|
continue
|
|
# The current commit touches this file. Determine which track folder(s).
|
|
d: date | None = _parse_iso_date(current_iso[:10])
|
|
is_work: bool = bool(_WORK_PREFIX_RE.match(current_msg))
|
|
for rp in relpaths:
|
|
if rp == stripped or stripped.startswith(rp + "/"):
|
|
acc: _TrackAccumulator = data[rp]
|
|
acc.files.add(stripped)
|
|
if is_work:
|
|
acc.work_commit_count += 1
|
|
if d is not None:
|
|
if acc.init_date is None or d < acc.init_date:
|
|
acc.init_date = d
|
|
acc.init_sha = current_sha
|
|
if acc.end_date is None or d > acc.end_date:
|
|
acc.end_date = d
|
|
acc.end_sha = current_sha
|
|
return data
|
|
|
|
|
|
def _classify_status(folder: Path, work_commits: int) -> str:
|
|
"""Classify the track's status from state.toml + git evidence."""
|
|
state_path: Path = folder / "state.toml"
|
|
if state_path.is_file():
|
|
try:
|
|
for line in state_path.read_text(encoding="utf-8").splitlines():
|
|
if line.strip().startswith("status") and "=" in line:
|
|
val: str = line.split("=", 1)[1].split("#")[0].strip().strip('"').strip("'")
|
|
if val in ("completed", "complete", "shipped"):
|
|
return "Completed"
|
|
if val == "superseded":
|
|
return "Superseded"
|
|
if val == "abandoned":
|
|
return "Abandoned"
|
|
if val == "active":
|
|
return "Active"
|
|
except OSError:
|
|
pass
|
|
if work_commits >= 3:
|
|
return "Completed"
|
|
if work_commits >= 1:
|
|
return "In Progress"
|
|
return "Active"
|
|
|
|
|
|
def build_track_index(eras: list[EraBoundary]) -> list[TrackRow]:
|
|
"""Walk every track folder and build a TrackRow per folder.
|
|
|
|
Uses a SINGLE git log pass (see _build_per_track_data) instead of
|
|
per-track subprocesses, for a >10x speedup on the current repo.
|
|
"""
|
|
per_track: dict[str, _TrackAccumulator] = _build_per_track_data()
|
|
folder_by_relpath: dict[str, Path] = {rp: f for f, rp in _walk_track_folders()}
|
|
rows: list[TrackRow] = []
|
|
for rp, acc in per_track.items():
|
|
if acc.init_date is None or acc.end_date is None:
|
|
continue
|
|
folder: Path | None = folder_by_relpath.get(rp)
|
|
if folder is None:
|
|
if rp.startswith("conductor/tracks/"):
|
|
archive_rp: str = rp.replace("conductor/tracks/", "conductor/archive/", 1)
|
|
folder = folder_by_relpath.get(archive_rp)
|
|
if folder is None:
|
|
continue
|
|
track_id: str = folder.name
|
|
init_date: date = acc.init_date
|
|
end_date: date = acc.end_date
|
|
duration_days: int = (end_date - init_date).days
|
|
era_idx: int = assign_era_for_date(init_date, eras)
|
|
status: str = _classify_status(folder, acc.work_commit_count)
|
|
rows.append(
|
|
TrackRow(
|
|
track_id=track_id,
|
|
init_sha=acc.init_sha,
|
|
end_sha=acc.end_sha,
|
|
init_date=init_date,
|
|
end_date=end_date,
|
|
duration_days=duration_days,
|
|
commit_count=acc.work_commit_count,
|
|
files_touched=len(acc.files),
|
|
era_index=era_idx,
|
|
status=status,
|
|
)
|
|
)
|
|
# Deduplicate by track_id (archive + tracks may both produce a row).
|
|
seen: set[str] = set()
|
|
unique: list[TrackRow] = []
|
|
for row in sorted(rows, key=lambda r: (r.track_id, r.init_date)):
|
|
if row.track_id in seen:
|
|
continue
|
|
seen.add(row.track_id)
|
|
unique.append(row)
|
|
return unique
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Op Layer A: Sub-agent task log parser
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def parse_subagent_task_log(log_path: Path) -> tuple[str, datetime] | None:
|
|
"""Parse a single mma_tier*_task_*.log file.
|
|
|
|
Returns (role, timestamp) tuple, or None if the file is malformed.
|
|
"""
|
|
try:
|
|
text: str = log_path.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
return None
|
|
role_match: re.Match[str] | None = _ROLE_RE.search(text)
|
|
ts_match: re.Match[str] | None = _TIMESTAMP_RE.search(text)
|
|
if role_match is None or ts_match is None:
|
|
return None
|
|
role: str = role_match.group("role").strip()
|
|
ts_str: str = ts_match.group("ts").strip()
|
|
try:
|
|
ts: datetime = datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
|
|
except ValueError:
|
|
return None
|
|
return (role, ts)
|
|
|
|
|
|
def _tier_from_role(role: str) -> str:
|
|
"""Map a role string to a normalized tier label."""
|
|
role_lower: str = role.lower()
|
|
if role_lower.startswith("tier1"):
|
|
return "tier1"
|
|
if role_lower.startswith("tier3"):
|
|
return "tier3-worker"
|
|
if role_lower.startswith("tier4"):
|
|
return "tier4-qa"
|
|
return role
|
|
|
|
|
|
def _find_track_for_timestamp(ts: datetime, tracks: list[TrackRow]) -> str | None:
|
|
"""Return the track_id whose [init_date, end_date] window contains ts, or None."""
|
|
ts_date: date = ts.date()
|
|
for t in tracks:
|
|
if t.init_date <= ts_date <= t.end_date:
|
|
return t.track_id
|
|
return None
|
|
|
|
|
|
def build_subagent_task_index(
|
|
eras: list[EraBoundary],
|
|
tracks: list[TrackRow],
|
|
) -> list[SubAgentTaskOp]:
|
|
"""Walk logs/agents/mma_tier*_task_*.log; emit one SubAgentTaskOp per log.
|
|
|
|
Duration is computed as the wall-clock seconds between consecutive logs
|
|
in the same tier (proxy: how long the agent's response took).
|
|
"""
|
|
if not _LOGS_AGENTS_DIR.is_dir():
|
|
return []
|
|
parsed: list[tuple[str, datetime, Path]] = []
|
|
for log in sorted(_LOGS_AGENTS_DIR.iterdir()):
|
|
if not log.is_file() or not _TIER_FROM_FILENAME_RE.search(log.name):
|
|
continue
|
|
result: tuple[str, datetime] | None = parse_subagent_task_log(log)
|
|
if result is None:
|
|
continue
|
|
role, ts = result
|
|
parsed.append((_tier_from_role(role), ts, log))
|
|
parsed.sort(key=lambda t: t[1])
|
|
ops: list[SubAgentTaskOp] = []
|
|
for i, (role, ts, log) in enumerate(parsed):
|
|
if i + 1 < len(parsed):
|
|
next_ts: datetime = parsed[i + 1][1]
|
|
duration_s: float = (next_ts - ts).total_seconds()
|
|
else:
|
|
try:
|
|
mtime: float = log.stat().st_mtime
|
|
duration_s = max(0.0, mtime - ts.timestamp())
|
|
except OSError:
|
|
duration_s = 0.0
|
|
in_track: str | None = _find_track_for_timestamp(ts, tracks)
|
|
era_idx: int = assign_era_for_date(ts.date(), eras)
|
|
ops.append(
|
|
SubAgentTaskOp(
|
|
timestamp=ts,
|
|
tier=role,
|
|
log_path=str(log.relative_to(_REPO_ROOT)),
|
|
duration_s=duration_s,
|
|
in_track=in_track,
|
|
era_index=era_idx,
|
|
)
|
|
)
|
|
return ops
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Op Layer B: Commit-prefix indexer
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def build_commit_prefix_index(
|
|
eras: list[EraBoundary],
|
|
tracks: list[TrackRow],
|
|
) -> list[CommitPrefixOp]:
|
|
"""Walk all feat|fix|refactor|perf|test commits and bucket by track + era."""
|
|
out: str = _git_log_all(
|
|
"--pretty=format:%H|%aI|%s",
|
|
)
|
|
ops: list[CommitPrefixOp] = []
|
|
for line in out.splitlines():
|
|
parts: list[str] = line.split("|", 2)
|
|
if len(parts) != 3:
|
|
continue
|
|
sha, iso_date, message = parts
|
|
m: re.Match[str] | None = _WORK_PREFIX_RE.match(message)
|
|
if m is None:
|
|
continue
|
|
prefix: str = m.group("prefix")
|
|
scope: str = m.group("scope") or ""
|
|
d: date | None = _parse_iso_date(iso_date[:10])
|
|
if d is None:
|
|
continue
|
|
try:
|
|
dt: datetime = datetime.fromisoformat(iso_date.split("+")[0].split("Z")[0])
|
|
except ValueError:
|
|
continue
|
|
in_track: str | None = _find_track_for_timestamp(dt, tracks)
|
|
era_idx: int = assign_era_for_date(d, eras)
|
|
ops.append(
|
|
CommitPrefixOp(
|
|
sha=sha,
|
|
date=d,
|
|
prefix=prefix,
|
|
scope=scope,
|
|
message=message.strip(),
|
|
in_track=in_track,
|
|
era_index=era_idx,
|
|
)
|
|
)
|
|
return ops
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Op Layer C: Session log indexer
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def parse_session_name(name: str) -> tuple[date | None, str]:
|
|
"""Parse a session folder name. Returns (date, suffix)."""
|
|
m: re.Match[str] | None = _SESSION_NAME_RE.match(name)
|
|
if m is None:
|
|
return (None, "")
|
|
try:
|
|
d: date = date(
|
|
int(m.group("date")[:4]),
|
|
int(m.group("date")[4:6]),
|
|
int(m.group("date")[6:8]),
|
|
)
|
|
except ValueError:
|
|
return (None, "")
|
|
suffix: str = m.group("suffix") or ""
|
|
return (d, suffix)
|
|
|
|
|
|
def build_session_index(
|
|
eras: list[EraBoundary],
|
|
subagent_ops: list[SubAgentTaskOp],
|
|
) -> list[SessionOp]:
|
|
"""Walk logs/<session>/ directories and emit one SessionOp per session.
|
|
|
|
Excludes logs/agents/ (already covered by Op Layer A).
|
|
"""
|
|
if not _LOGS_DIR.is_dir():
|
|
return []
|
|
ops_by_date: dict[date, int] = defaultdict(int)
|
|
for op in subagent_ops:
|
|
ops_by_date[op.timestamp.date()] += 1
|
|
sessions: list[SessionOp] = []
|
|
for entry in sorted(_LOGS_DIR.iterdir()):
|
|
if not entry.is_dir() or entry.name == "agents":
|
|
continue
|
|
d, _suffix = parse_session_name(entry.name)
|
|
if d is None:
|
|
continue
|
|
start_mtime: float | None = None
|
|
end_mtime: float | None = None
|
|
try:
|
|
for f in entry.rglob("*"):
|
|
if not f.is_file():
|
|
continue
|
|
mt: float = f.stat().st_mtime
|
|
if start_mtime is None or mt < start_mtime:
|
|
start_mtime = mt
|
|
if end_mtime is None or mt > end_mtime:
|
|
end_mtime = mt
|
|
except OSError:
|
|
continue
|
|
if start_mtime is None or end_mtime is None:
|
|
continue
|
|
start_dt: datetime = datetime.fromtimestamp(start_mtime)
|
|
end_dt: datetime = datetime.fromtimestamp(end_mtime)
|
|
duration_min: float = (end_dt - start_dt).total_seconds() / 60.0
|
|
task_count: int = ops_by_date.get(d, 0)
|
|
era_idx: int = assign_era_for_date(d, eras)
|
|
sessions.append(
|
|
SessionOp(
|
|
session_name=entry.name,
|
|
start_dt=start_dt,
|
|
end_dt=end_dt,
|
|
duration_min=duration_min,
|
|
task_count=task_count,
|
|
era_index=era_idx,
|
|
)
|
|
)
|
|
return sessions
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Statistician
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _percentile(values: list[float], pct: float) -> float:
|
|
"""Linear-interpolation percentile (matches numpy default)."""
|
|
if not values:
|
|
return 0.0
|
|
sorted_v: list[float] = sorted(values)
|
|
k: float = (len(sorted_v) - 1) * (pct / 100.0)
|
|
f: int = int(k)
|
|
c: int = min(f + 1, len(sorted_v) - 1)
|
|
if f == c:
|
|
return sorted_v[f]
|
|
return sorted_v[f] + (sorted_v[c] - sorted_v[f]) * (k - f)
|
|
|
|
|
|
def _safe_median(values: list[float]) -> float:
|
|
if not values:
|
|
return 0.0
|
|
return float(statistics.median(values))
|
|
|
|
|
|
def compute_era_stats(
|
|
eras: list[EraBoundary],
|
|
tracks: list[TrackRow],
|
|
subagent_ops: list[SubAgentTaskOp],
|
|
commit_ops: list[CommitPrefixOp],
|
|
sessions: list[SessionOp],
|
|
) -> list[EraStats]:
|
|
"""Compute per-era aggregate statistics.
|
|
|
|
Returns one EraStats per era, in era order.
|
|
"""
|
|
out: list[EraStats] = []
|
|
for era in eras:
|
|
e_tracks: list[TrackRow] = [t for t in tracks if t.era_index == era.index]
|
|
e_subagent: list[SubAgentTaskOp] = [op for op in subagent_ops if op.era_index == era.index]
|
|
e_commits: list[CommitPrefixOp] = [op for op in commit_ops if op.era_index == era.index]
|
|
e_sessions: list[SessionOp] = [s for s in sessions if s.era_index == era.index]
|
|
durations: list[float] = [float(t.duration_days) for t in e_tracks]
|
|
commits_per: list[float] = [float(t.commit_count) for t in e_tracks]
|
|
files_per: list[float] = [float(t.files_touched) for t in e_tracks]
|
|
subagent_durs: list[float] = [op.duration_s for op in e_subagent]
|
|
session_durs: list[float] = [s.duration_min for s in e_sessions]
|
|
prefix_counter: Counter[str] = Counter(op.prefix for op in e_commits)
|
|
track_less: int = sum(1 for op in e_subagent if op.in_track is None) \
|
|
+ sum(1 for op in e_commits if op.in_track is None)
|
|
out.append(
|
|
EraStats(
|
|
era=era,
|
|
track_count=len(e_tracks),
|
|
track_duration_median=_safe_median(durations),
|
|
track_duration_p25=_percentile(durations, 25),
|
|
track_duration_p75=_percentile(durations, 75),
|
|
track_commits_median=_safe_median(commits_per),
|
|
track_files_median=_safe_median(files_per),
|
|
subagent_task_count=len(e_subagent),
|
|
subagent_task_duration_median=_safe_median(subagent_durs),
|
|
commit_count=len(e_commits),
|
|
commit_count_by_prefix=dict(prefix_counter),
|
|
session_count=len(e_sessions),
|
|
session_duration_median=_safe_median(session_durs),
|
|
track_less_op_count=track_less,
|
|
)
|
|
)
|
|
return out
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Renderer (10 section functions; one per Markdown section)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def render_methodology(era_window_days: int) -> str:
|
|
return (
|
|
"# Meta-Tooling Duration Analysis\n\n"
|
|
f"_Generated: {datetime.now().isoformat(timespec='seconds')}_\n\n"
|
|
"## 1. Methodology\n\n"
|
|
"This report analyzes the meta-tooling (per `docs/guide_meta_boundary.md`) "
|
|
"used to build this codebase from 2026-02-21 to 2026-07-05 (~133 days).\n\n"
|
|
"### 1.1 Data sources\n\n"
|
|
"- **Git history**: thousands of commits across the repo; work-prefix commits are feat/fix/refactor/perf/test\n"
|
|
"- **Sub-agent task logs**: every file in `logs/agents/mma_tier*_task_*.log`\n"
|
|
"- **Session logs**: every directory under `logs/` (excluding `logs/agents/`)\n"
|
|
"- **Per-track walker**: walks `conductor/tracks/` + `conductor/archive/`\n\n"
|
|
"### 1.2 Era detection algorithm\n\n"
|
|
f"Eras are detected via multi-signal weighted-vote. Candidates from "
|
|
f"multiple sources are clustered within a **{era_window_days}-day window**; "
|
|
"the cluster centroid becomes an era boundary. Signals (highest weight first):\n\n"
|
|
"1. **`config.toml` provider/model line changes** (`weight=high`)\n"
|
|
"2. **`conductor/workflow.md` tier-model-definition changes** (`weight=high`)\n"
|
|
"3. **`conductor/tech-stack.md` model-name changes** (`weight=high`)\n"
|
|
"4. **First appearance of a model name in a sub-agent task log** (`weight=medium`)\n"
|
|
"5. **Track spec frontmatter model mentions** (`weight=medium`)\n\n"
|
|
"### 1.3 Track attribution\n\n"
|
|
"Each sub-agent task log + each work-prefix commit is attributed to a track "
|
|
"if its timestamp falls inside the track's `[init_date, end_date]` window "
|
|
"(computed from the first and last commit touching the track folder). "
|
|
"Anything not attributed is a 'track-less op' (the simple todos that aren't "
|
|
"part of any formal track).\n\n"
|
|
"### 1.4 Limitations\n\n"
|
|
"- **No per-task model attribution.** Logs only carry tier (1/3/4), not the "
|
|
"exact model. We era-bucket, not per-task.\n"
|
|
"- **No LLM 'thinking' time or token costs** in the data.\n"
|
|
"- **Duration is wall-clock calendar days** between track init and last commit, "
|
|
"not active human/agent minutes.\n"
|
|
"- **Era boundaries are heuristic.** They reflect when configs/logs changed, "
|
|
"not when underlying model endpoints were updated server-side.\n"
|
|
)
|
|
|
|
|
|
def render_era_timeline(eras: list[EraBoundary]) -> str:
|
|
lines: list[str] = ["\n## 2. Era Timeline\n"]
|
|
lines.append(f"Detected **{len(eras)} eras** (window={_DEFAULT_ERA_WINDOW_DAYS}d).\n")
|
|
lines.append("| # | Name | Start | End | Evidence count | Dominant providers/models |")
|
|
lines.append("|---|---|---|---|---|---|")
|
|
for era in eras:
|
|
end_str: str = era.end.isoformat() if era.end else "(current)"
|
|
models: set[str] = set()
|
|
for ev in era.evidence:
|
|
for token in ("gemini-3", "gemini-2.5", "minimax", "claude", "grok"):
|
|
if token in ev.note.lower():
|
|
models.add(token)
|
|
models_str: str = ", ".join(sorted(models)) if models else "—"
|
|
lines.append(
|
|
f"| {era.index} | {era.name} | {era.start.isoformat()} | {end_str} | "
|
|
f"{len(era.evidence)} | {models_str} |"
|
|
)
|
|
lines.append("\n### Evidence chain per era\n")
|
|
for era in eras:
|
|
lines.append(f"#### Era {era.index}: {era.name} (start {era.start.isoformat()})\n")
|
|
for ev in era.evidence:
|
|
lines.append(f"- `{ev.date.isoformat()}` **{ev.weight}**: {ev.note} _(source: {ev.source})_")
|
|
lines.append("")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def render_track_stats(eras: list[EraBoundary], tracks: list[TrackRow]) -> str:
|
|
lines: list[str] = ["\n## 3. Track-Level Stats\n"]
|
|
lines.append(f"**{len(tracks)} tracks** total.\n")
|
|
lines.append("### 3.1 Per-era aggregate\n")
|
|
lines.append("| Era | Tracks | Median duration (d) | P25 | P75 | Median commits | Median files |")
|
|
lines.append("|---|---|---|---|---|---|---|")
|
|
for era in eras:
|
|
e_tracks: list[TrackRow] = [t for t in tracks if t.era_index == era.index]
|
|
if not e_tracks:
|
|
lines.append(f"| {era.index} ({era.name}) | 0 | — | — | — | — | — |")
|
|
continue
|
|
durations: list[float] = [float(t.duration_days) for t in e_tracks]
|
|
commits: list[float] = [float(t.commit_count) for t in e_tracks]
|
|
files: list[float] = [float(t.files_touched) for t in e_tracks]
|
|
lines.append(
|
|
f"| {era.index} ({era.name}) | {len(e_tracks)} | "
|
|
f"{_safe_median(durations):.1f} | {_percentile(durations, 25):.0f} | "
|
|
f"{_percentile(durations, 75):.0f} | {_safe_median(commits):.1f} | "
|
|
f"{_safe_median(files):.1f} |"
|
|
)
|
|
lines.append("\n### 3.2 All tracks (sorted by start date, top 30 by duration)\n")
|
|
lines.append("| Track | Init | End | Days | Commits | Files | Era | Status |")
|
|
lines.append("|---|---|---|---|---|---|---|---|")
|
|
for t in sorted(tracks, key=lambda x: x.duration_days, reverse=True)[:30]:
|
|
era_name: str = eras[t.era_index].name if t.era_index < len(eras) else "?"
|
|
lines.append(
|
|
f"| `{t.track_id}` | {t.init_date.isoformat()} | {t.end_date.isoformat()} | "
|
|
f"{t.duration_days} | {t.commit_count} | {t.files_touched} | "
|
|
f"{era_name} | {t.status} |"
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def render_op_stats(commit_ops: list[CommitPrefixOp]) -> str:
|
|
lines: list[str] = ["\n## 4. Op-Level Stats (work-prefix commits)\n"]
|
|
lines.append(f"**{len(commit_ops)} work-prefix commits** total.\n")
|
|
lines.append("### 4.1 Per-era count by prefix\n")
|
|
lines.append("| Era | feat | fix | refactor | perf | test | Total |")
|
|
lines.append("|---|---|---|---|---|---|---|")
|
|
for era_idx in sorted({op.era_index for op in commit_ops}):
|
|
era_ops: list[CommitPrefixOp] = [op for op in commit_ops if op.era_index == era_idx]
|
|
counts: dict[str, int] = dict(Counter(op.prefix for op in era_ops))
|
|
lines.append(
|
|
f"| {era_idx} | {counts.get('feat', 0)} | {counts.get('fix', 0)} | "
|
|
f"{counts.get('refactor', 0)} | {counts.get('perf', 0)} | "
|
|
f"{counts.get('test', 0)} | {len(era_ops)} |"
|
|
)
|
|
lines.append("\n### 4.2 Top 15 busiest commit days\n")
|
|
by_day: Counter[date] = Counter(op.date for op in commit_ops)
|
|
for d, count in by_day.most_common(15):
|
|
lines.append(f"- `{d.isoformat()}`: {count} commits")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def render_subagent_stats(subagent_ops: list[SubAgentTaskOp]) -> str:
|
|
lines: list[str] = ["\n## 5. Sub-Agent Task Stats (Op Layer A)\n"]
|
|
lines.append(f"**{len(subagent_ops)} sub-agent task logs** parsed.\n")
|
|
lines.append("### 5.1 Per-tier counts and median duration\n")
|
|
lines.append("| Tier | Tasks | Median duration (s) | P25 | P75 |")
|
|
lines.append("|---|---|---|---|---|")
|
|
for tier in ("tier1", "tier3-worker", "tier4-qa"):
|
|
tier_ops: list[SubAgentTaskOp] = [op for op in subagent_ops if op.tier == tier]
|
|
if not tier_ops:
|
|
continue
|
|
durs: list[float] = [op.duration_s for op in tier_ops]
|
|
lines.append(
|
|
f"| {tier} | {len(tier_ops)} | {_safe_median(durs):.1f} | "
|
|
f"{_percentile(durs, 25):.0f} | {_percentile(durs, 75):.0f} |"
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def render_trackless_ops(
|
|
subagent_ops: list[SubAgentTaskOp],
|
|
commit_ops: list[CommitPrefixOp],
|
|
) -> str:
|
|
trackless_sub: list[SubAgentTaskOp] = [op for op in subagent_ops if op.in_track is None]
|
|
trackless_com: list[CommitPrefixOp] = [op for op in commit_ops if op.in_track is None]
|
|
lines: list[str] = ["\n## 6. Track-less Ops (the simple todos)\n"]
|
|
lines.append(f"- **{len(trackless_sub)} sub-agent tasks** outside any track window.")
|
|
lines.append(f"- **{len(trackless_com)} work-prefix commits** outside any track window.")
|
|
if trackless_sub:
|
|
lines.append("\n### 6.1 Sample track-less tasks (first 10 by timestamp)\n")
|
|
for op in sorted(trackless_sub, key=lambda o: o.timestamp)[:10]:
|
|
lines.append(f"- `{op.timestamp.isoformat()}` {op.tier}: `{op.log_path}`")
|
|
if trackless_com:
|
|
lines.append("\n### 6.2 Top 10 track-less commit prefixes\n")
|
|
prefix_counter: Counter[str] = Counter(op.prefix for op in trackless_com)
|
|
for p, c in prefix_counter.most_common(10):
|
|
lines.append(f"- `{p}`: {c} commits")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def render_trends(commit_ops: list[CommitPrefixOp], subagent_ops: list[SubAgentTaskOp]) -> str:
|
|
lines: list[str] = ["\n## 7. Cross-Era Trends\n"]
|
|
lines.append("### 7.1 Per-week commit count (last 16 weeks)\n")
|
|
by_week: Counter[tuple[int, int]] = Counter()
|
|
for op in commit_ops:
|
|
iy, iw, _ = op.date.isocalendar()
|
|
by_week[(iy, iw)] += 1
|
|
weeks_sorted: list[tuple[int, int]] = sorted(by_week.keys())[-16:]
|
|
for wk in weeks_sorted:
|
|
lines.append(f"- Week {wk[0]}-W{wk[1]:02d}: {by_week[wk]} commits")
|
|
lines.append("\n### 7.2 Per-week sub-agent task count (last 16 weeks)\n")
|
|
by_week_task: Counter[tuple[int, int]] = Counter()
|
|
for op in subagent_ops:
|
|
iy, iw, _ = op.timestamp.date().isocalendar()
|
|
by_week_task[(iy, iw)] += 1
|
|
for wk in sorted(by_week_task.keys())[-16:]:
|
|
lines.append(f"- Week {wk[0]}-W{wk[1]:02d}: {by_week_task[wk]} tasks")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def render_use_case_predictions(tracks: list[TrackRow]) -> str:
|
|
lines: list[str] = ["\n## 8. Use-Case Predictions\n"]
|
|
lines.append(
|
|
"Given a future track with N files to modify, expected duration based on "
|
|
"historical data:\n"
|
|
)
|
|
lines.append("| Files-touched bucket | Sample size (N) | Median duration (d) | P25 | P75 | IQR |")
|
|
lines.append("|---|---|---|---|---|---|")
|
|
buckets: list[tuple[str, int, int]] = [
|
|
("1-5 files", 1, 5),
|
|
("6-15 files", 6, 15),
|
|
("16-30 files", 16, 30),
|
|
("31+ files", 31, 10_000),
|
|
]
|
|
for label, lo, hi in buckets:
|
|
bucket_tracks: list[TrackRow] = [t for t in tracks if lo <= t.files_touched <= hi]
|
|
if not bucket_tracks:
|
|
lines.append(f"| {label} | 0 | — | — | — | — |")
|
|
continue
|
|
durations: list[float] = [float(t.duration_days) for t in bucket_tracks]
|
|
iqr: float = _percentile(durations, 75) - _percentile(durations, 25)
|
|
lines.append(
|
|
f"| {label} | {len(bucket_tracks)} | {_safe_median(durations):.1f} | "
|
|
f"{_percentile(durations, 25):.0f} | {_percentile(durations, 75):.0f} | "
|
|
f"{iqr:.1f} |"
|
|
)
|
|
lines.append(
|
|
"\n> **Caveat**: durations are calendar days, not active minutes. A track that "
|
|
"sat idle for 2 weeks reads as 14 days. Use IQR as a sanity check on outliers."
|
|
)
|
|
lines.append(
|
|
"\n### 8.1 Use-case predictions per era\n"
|
|
)
|
|
lines.append("| Era | 1-5 files | 6-15 files | 16-30 files | 31+ files |")
|
|
lines.append("|---|---|---|---|---|")
|
|
all_eras_idx: set[int] = {t.era_index for t in tracks}
|
|
for era_idx in sorted(all_eras_idx):
|
|
cells: list[str] = []
|
|
for _label, lo, hi in buckets:
|
|
bucket: list[TrackRow] = [
|
|
t for t in tracks if t.era_index == era_idx and lo <= t.files_touched <= hi
|
|
]
|
|
if not bucket:
|
|
cells.append("—")
|
|
else:
|
|
durs: list[float] = [float(t.duration_days) for t in bucket]
|
|
cells.append(f"N={len(bucket)}, {_safe_median(durs):.1f}d")
|
|
lines.append(f"| {era_idx} | " + " | ".join(cells) + " |")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def render_limitations() -> str:
|
|
return (
|
|
"\n## 9. Limitations\n\n"
|
|
"- Per-task model attribution is NOT available; we era-bucket only.\n"
|
|
"- 'Duration' is calendar days, not active minutes (idle time included).\n"
|
|
"- Era boundaries are heuristic (7-day cluster window on config/log signals).\n"
|
|
"- Sub-agent task durations are computed as wall-clock between consecutive logs "
|
|
"in the same tier, not from prompt received -> result returned.\n"
|
|
"- Session durations are computed from file mtimes, not from chat events.\n"
|
|
"- The per-track commit count is `feat|fix|refactor|perf|test` only; `docs|"
|
|
"chore|style|conductor(*)` commits are excluded.\n"
|
|
)
|
|
|
|
|
|
def render_appendix(stats_json_path: Path) -> str:
|
|
return (
|
|
f"\n## 10. Appendix\n\n"
|
|
f"- **JSON artifact**: `{stats_json_path.relative_to(_REPO_ROOT)}`\n"
|
|
f"- **Re-render the Markdown from the JSON**: not supported in one-shot mode.\n"
|
|
f"- **Schema version**: `1`\n"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Era-detection signal gatherers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _gather_era_candidates() -> list[tuple[date, str, str]]:
|
|
"""Gather era-boundary candidate dates from all signals.
|
|
|
|
Returns list of (date, weight, note) tuples. The note includes the
|
|
git evidence so the report can cite it.
|
|
"""
|
|
candidates: list[tuple[date, str, str]] = []
|
|
|
|
# Signal A + B: structured per-commit walk on config.toml + workflow.md + tech-stack.md
|
|
for f in ("config.toml", "conductor/workflow.md", "conductor/tech-stack.md"):
|
|
structured: str = _git_log_all(
|
|
"--diff-filter=AM", "--pretty=format:COMMIT %H %aI", "-p", "--", f,
|
|
)
|
|
current_date: date | None = None
|
|
current_sha: str = ""
|
|
for line in structured.splitlines():
|
|
if line.startswith("COMMIT "):
|
|
parts: list[str] = line.split(" ", 2)
|
|
if len(parts) >= 3:
|
|
d: date | None = _parse_iso_date(parts[2][:10])
|
|
current_date = d
|
|
current_sha = parts[1][:8]
|
|
elif current_date and line.startswith("+"):
|
|
lower: str = line.lower()
|
|
is_relevant: bool = (
|
|
(f == "config.toml" and ("provider" in lower or "model" in lower))
|
|
or (f != "config.toml" and re.search(r"gemini-3|gemini-2\.5|minimax|claude|grok", lower))
|
|
)
|
|
if is_relevant:
|
|
candidates.append(
|
|
(current_date, "high",
|
|
f"{f} {line[1:80].strip()} (commit {current_sha})")
|
|
)
|
|
|
|
# Signal C: first appearance of model name in sub-agent task log content
|
|
if _LOGS_AGENTS_DIR.is_dir():
|
|
seen_tokens: set[str] = set()
|
|
for log in sorted(_LOGS_AGENTS_DIR.iterdir()):
|
|
if not log.is_file():
|
|
continue
|
|
try:
|
|
text: str = log.read_text(encoding="utf-8", errors="ignore")
|
|
except OSError:
|
|
continue
|
|
for token in ("gemini-3", "gemini-2.5", "minimax", "claude", "grok"):
|
|
if token in seen_tokens:
|
|
continue
|
|
if token in text.lower():
|
|
try:
|
|
mtime_str: str = datetime.fromtimestamp(log.stat().st_mtime).date().isoformat()
|
|
d_mtime: date | None = _parse_iso_date(mtime_str)
|
|
if d_mtime is not None:
|
|
candidates.append(
|
|
(d_mtime, "medium",
|
|
f"first appearance of '{token}' in {log.name}")
|
|
)
|
|
seen_tokens.add(token)
|
|
except OSError:
|
|
pass
|
|
return candidates
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# JSON serialization helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _era_to_dict(e: EraBoundary) -> dict[str, object]:
|
|
return {
|
|
"index": e.index,
|
|
"start": e.start.isoformat(),
|
|
"end": e.end.isoformat() if e.end else None,
|
|
"name": e.name,
|
|
"evidence": [
|
|
{"date": ev.date.isoformat(), "weight": ev.weight,
|
|
"source": ev.source, "note": ev.note}
|
|
for ev in e.evidence
|
|
],
|
|
}
|
|
|
|
|
|
def _track_to_dict(t: TrackRow) -> dict[str, object]:
|
|
return {
|
|
"track_id": t.track_id,
|
|
"init_sha": t.init_sha,
|
|
"end_sha": t.end_sha,
|
|
"init_date": t.init_date.isoformat(),
|
|
"end_date": t.end_date.isoformat(),
|
|
"duration_days": t.duration_days,
|
|
"commit_count": t.commit_count,
|
|
"files_touched": t.files_touched,
|
|
"era_index": t.era_index,
|
|
"status": t.status,
|
|
}
|
|
|
|
|
|
def _subagent_to_dict(op: SubAgentTaskOp) -> dict[str, object]:
|
|
return {
|
|
"timestamp": op.timestamp.isoformat(timespec="seconds"),
|
|
"tier": op.tier,
|
|
"log_path": op.log_path,
|
|
"duration_s": round(op.duration_s, 1),
|
|
"in_track": op.in_track,
|
|
"era_index": op.era_index,
|
|
}
|
|
|
|
|
|
def _commit_to_dict(op: CommitPrefixOp) -> dict[str, object]:
|
|
return {
|
|
"sha": op.sha,
|
|
"date": op.date.isoformat(),
|
|
"prefix": op.prefix,
|
|
"scope": op.scope,
|
|
"message": op.message,
|
|
"in_track": op.in_track,
|
|
"era_index": op.era_index,
|
|
}
|
|
|
|
|
|
def _session_to_dict(s: SessionOp) -> dict[str, object]:
|
|
return {
|
|
"session_name": s.session_name,
|
|
"start": s.start_dt.isoformat(timespec="seconds"),
|
|
"end": s.end_dt.isoformat(timespec="seconds"),
|
|
"duration_min": round(s.duration_min, 1),
|
|
"task_count": s.task_count,
|
|
"era_index": s.era_index,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _build_parser() -> argparse.ArgumentParser:
|
|
parser: argparse.ArgumentParser = argparse.ArgumentParser(
|
|
description="One-shot meta-tooling duration analyzer.",
|
|
)
|
|
parser.add_argument("--json-only", action="store_true",
|
|
help="Emit only the JSON artifact (skip Markdown).")
|
|
parser.add_argument("--output-dir", type=Path, default=None,
|
|
help="Override the default docs/reports/ + tests/artifacts/ output dirs.")
|
|
parser.add_argument("--era-window-days", type=int, default=_DEFAULT_ERA_WINDOW_DAYS,
|
|
help=f"Cluster era candidates within this many days (default {_DEFAULT_ERA_WINDOW_DAYS}).")
|
|
parser.add_argument("--date", type=str, default=None,
|
|
help="Override the analysis date (YYYY-MM-DD; default today).")
|
|
parser.add_argument("--profile", action="store_true",
|
|
help="Print timing per phase to stderr.")
|
|
return parser
|
|
|
|
|
|
def main() -> int:
|
|
parser: argparse.ArgumentParser = _build_parser()
|
|
args: argparse.Namespace = parser.parse_args()
|
|
output_dir: Path = args.output_dir or _DEFAULT_OUTPUT_DIR
|
|
artifact_dir: Path = args.output_dir or _DEFAULT_ARTIFACT_DIR
|
|
analysis_date: date = (
|
|
_parse_iso_date(args.date) if args.date else _DEFAULT_DATE
|
|
) or _DEFAULT_DATE
|
|
|
|
def phase(label: str) -> None:
|
|
print(f"[{datetime.now().isoformat(timespec='seconds')}] {label}", file=sys.stderr)
|
|
|
|
phase("Detecting eras (multi-signal weighted-vote)...")
|
|
candidates: list[tuple[date, str, str]] = _gather_era_candidates()
|
|
eras: list[EraBoundary] = cluster_era_candidates(candidates, window_days=args.era_window_days)
|
|
named: list[EraBoundary] = []
|
|
for i, era in enumerate(eras):
|
|
named.append(
|
|
EraBoundary(
|
|
index=era.index,
|
|
start=era.start,
|
|
end=era.end,
|
|
name=assign_era_name(i, len(eras)),
|
|
evidence=era.evidence,
|
|
)
|
|
)
|
|
eras = named
|
|
phase(f"Detected {len(eras)} eras.")
|
|
|
|
phase("Building track index...")
|
|
tracks: list[TrackRow] = build_track_index(eras)
|
|
phase(f"Indexed {len(tracks)} tracks.")
|
|
|
|
phase("Building sub-agent task index (Op Layer A)...")
|
|
subagent_ops: list[SubAgentTaskOp] = build_subagent_task_index(eras, tracks)
|
|
phase(f"Indexed {len(subagent_ops)} sub-agent tasks.")
|
|
|
|
phase("Building commit-prefix index (Op Layer B)...")
|
|
commit_ops: list[CommitPrefixOp] = build_commit_prefix_index(eras, tracks)
|
|
phase(f"Indexed {len(commit_ops)} work-prefix commits.")
|
|
|
|
phase("Building session index (Op Layer C)...")
|
|
sessions: list[SessionOp] = build_session_index(eras, subagent_ops)
|
|
phase(f"Indexed {len(sessions)} sessions.")
|
|
|
|
phase("Computing per-era statistics...")
|
|
era_stats: list[EraStats] = compute_era_stats(eras, tracks, subagent_ops, commit_ops, sessions)
|
|
|
|
track_less_ops: dict[str, list[object]] = {
|
|
"subagent_tasks": [op for op in subagent_ops if op.in_track is None],
|
|
"commits": [op for op in commit_ops if op.in_track is None],
|
|
}
|
|
|
|
stats: dict[str, object] = {
|
|
"schema_version": "1",
|
|
"generated_at": datetime.now().isoformat(timespec="seconds"),
|
|
"metadata": {
|
|
"repo_head": subprocess.run(
|
|
["git", "rev-parse", "HEAD"], capture_output=True, text=True, check=False
|
|
).stdout.strip(),
|
|
"analysis_date": analysis_date.isoformat(),
|
|
"era_window_days": args.era_window_days,
|
|
"track_count": len(tracks),
|
|
"subagent_task_count": len(subagent_ops),
|
|
"commit_count": len(commit_ops),
|
|
"session_count": len(sessions),
|
|
},
|
|
"eras": [_era_to_dict(e) for e in eras],
|
|
"tracks": [_track_to_dict(t) for t in tracks],
|
|
"ops": {
|
|
"subagent_tasks": [_subagent_to_dict(op) for op in subagent_ops],
|
|
"commits": [_commit_to_dict(op) for op in commit_ops],
|
|
"sessions": [_session_to_dict(s) for s in sessions],
|
|
},
|
|
"era_stats": [
|
|
{
|
|
"era_index": s.era.index,
|
|
"track_count": s.track_count,
|
|
"track_duration_median": round(s.track_duration_median, 1),
|
|
"track_duration_p25": round(s.track_duration_p25, 1),
|
|
"track_duration_p75": round(s.track_duration_p75, 1),
|
|
"track_commits_median": round(s.track_commits_median, 1),
|
|
"track_files_median": round(s.track_files_median, 1),
|
|
"subagent_task_count": s.subagent_task_count,
|
|
"subagent_task_duration_median": round(s.subagent_task_duration_median, 1),
|
|
"commit_count": s.commit_count,
|
|
"commit_count_by_prefix": s.commit_count_by_prefix,
|
|
"session_count": s.session_count,
|
|
"session_duration_median": round(s.session_duration_median, 1),
|
|
"track_less_op_count": s.track_less_op_count,
|
|
}
|
|
for s in era_stats
|
|
],
|
|
"track_less_ops": {
|
|
"subagent_tasks": [_subagent_to_dict(op) for op in track_less_ops["subagent_tasks"]],
|
|
"commits": [_commit_to_dict(op) for op in track_less_ops["commits"]],
|
|
},
|
|
}
|
|
|
|
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
json_path: Path = artifact_dir / f"meta_tooling_stats_{analysis_date.isoformat()}.json"
|
|
with json_path.open("w", encoding="utf-8") as f:
|
|
json.dump(stats, f, indent=2)
|
|
phase(f"Wrote {json_path}")
|
|
|
|
if args.json_only:
|
|
return 0
|
|
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
md_parts: list[str] = [
|
|
render_methodology(args.era_window_days),
|
|
render_era_timeline(eras),
|
|
render_track_stats(eras, tracks),
|
|
render_op_stats(commit_ops),
|
|
render_subagent_stats(subagent_ops),
|
|
render_trackless_ops(subagent_ops, commit_ops),
|
|
render_trends(commit_ops, subagent_ops),
|
|
render_use_case_predictions(tracks),
|
|
render_limitations(),
|
|
render_appendix(json_path),
|
|
]
|
|
md_path: Path = output_dir / f"META_TOOLING_DURATION_ANALYSIS_{analysis_date.isoformat()}.md"
|
|
md_path.write_text("\n".join(md_parts), encoding="utf-8")
|
|
phase(f"Wrote {md_path}")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main()) |