Private
Public Access
0
0

feat(chronology): regenerate chronology.md with v2 git-history classifier (closes 5-day desync gap)

This commit is contained in:
2026-07-01 23:50:19 -04:00
parent fedfa6efc8
commit f5a08634b3
3 changed files with 320 additions and 235 deletions
+14 -3
View File
@@ -26,6 +26,7 @@ _METADATA_FIELD_PREFIXES = (
"**Parent umbrella:**",
"**Status:**",
"**Confidence:**",
"**Ancestors:**",
)
@@ -86,16 +87,26 @@ def run_quality_gate(rows: list[dict]) -> QualityGateResult:
def _load_rows_from_chronology(chronology_path: Path) -> list[dict]:
from scripts.audit.generate_chronology import walk_track_folders, _repo_root
root: Path = _repo_root(chronology_path.parent)
return walk_track_folders(root)
# chronology_path is conductor/chronology.md; walk_track_folders expects the conductor dir
conductor_dir: Path = chronology_path.parent.resolve()
if not (conductor_dir / "tracks").is_dir():
# Fallback: resolve from repo root
repo_root: Path = _repo_root(conductor_dir)
conductor_dir = repo_root / "conductor"
return walk_track_folders(conductor_dir)
def main() -> None:
parser = argparse.ArgumentParser(description="Quality gate for conductor/chronology.md")
parser.add_argument("--chronology", type=Path, default=Path("conductor/chronology.md"))
parser.add_argument("--rows-json", type=Path, default=None, help="Load rows from JSON file instead of re-walking")
parser.add_argument("--strict", action="store_true", help="Exit 1 on any violation")
args = parser.parse_args()
rows = _load_rows_from_chronology(args.chronology)
if args.rows_json and args.rows_json.is_file():
import json
rows = json.loads(args.rows_json.read_text(encoding="utf-8"))
else:
rows = _load_rows_from_chronology(args.chronology)
result = run_quality_gate(rows)
print(f"Quality gate: {'PASS' if result.passed else 'FAIL'}")
print(f" Total rows: {result.total_rows}")
+61 -14
View File
@@ -38,12 +38,22 @@ _METADATA_FIELD_PREFIXES = (
"**Parent umbrella:**",
"**Status:**",
"**Confidence:**",
"**Ancestors:**",
)
_WORK_COMMIT_PREFIXES = ("feat:", "fix:", "refactor:", "perf:", "test:", "docs(report):")
_WORK_COMMIT_PREFIXES = ("feat", "fix", "refactor", "perf", "test", "docs(report)")
_METADATA_COMMIT_PREFIXES = ("conductor(plan):", "conductor(state):", "conductor(track):", "docs(spec):", "docs(plan):")
def _is_work_commit(msg: str) -> bool:
"""Check if a commit message is a work commit (feat/fix/refactor/perf/test with optional scope)."""
for prefix in _WORK_COMMIT_PREFIXES:
if msg.startswith(prefix + ":") or msg.startswith(prefix + "("):
if not any(msg.startswith(m) for m in _METADATA_COMMIT_PREFIXES):
return True
return False
def extract_slug_date(folder_name: str) -> Optional[str]:
m = _SLUG_DATE_RE.search(folder_name)
if not m:
@@ -123,6 +133,23 @@ def _git_log(folder_relpath: str, *args: str) -> str:
return ""
def _git_log_multi(*folder_relpaths: str) -> str:
"""Get git log for multiple paths in a single subprocess call."""
try:
result = subprocess.run(
["git", "log", "--oneline", "--", *folder_relpaths],
capture_output=True,
text=True,
timeout=_GIT_TIMEOUT,
check=False,
)
if result.returncode != 0:
return ""
return result.stdout
except (subprocess.SubprocessError, OSError):
return ""
def _git_first_line(folder_relpath: str, *args: str) -> str:
out = _git_log(folder_relpath, *args)
stripped = out.strip()
@@ -188,16 +215,20 @@ def _last_commit_date(folder_relpath: str) -> str:
return "never"
def _count_work_commits(folder_relpath: str) -> int:
log: str = _git_log(folder_relpath, "--oneline")
def _count_work_commits_from_log(log: str) -> int:
count: int = 0
for line in log.splitlines():
msg: str = line.split(" ", 1)[1] if " " in line else ""
if msg.startswith(_WORK_COMMIT_PREFIXES) and not msg.startswith(_METADATA_COMMIT_PREFIXES):
if _is_work_commit(msg):
count += 1
return count
def _count_work_commits(folder_relpath: str) -> int:
log: str = _git_log(folder_relpath, "--oneline")
return _count_work_commits_from_log(log)
def _has_report_matching(reports_dir: Path, track_id: str, prefix: str) -> bool:
if not reports_dir.is_dir():
return False
@@ -215,6 +246,7 @@ def classify_status(
reports_dir: Path,
has_abort_report: bool = False,
state_status: str = "",
work_commits: int = -1,
) -> tuple[str, str, str]:
"""Git-history evidence classifier returning (status, confidence, reason).
@@ -238,7 +270,8 @@ def classify_status(
# 2. Git commit evidence
is_archive = folder_link.startswith("conductor/archive/")
is_tracks = folder_link.startswith("conductor/tracks/")
work_commits: int = _count_work_commits(folder_link)
if work_commits < 0:
work_commits = _count_work_commits(folder_link)
if work_commits >= 3:
return ("Completed", "medium", f"{work_commits} work commits")
if 1 <= work_commits <= 2 and is_tracks:
@@ -273,11 +306,31 @@ def walk_track_folders(root: Path) -> list[dict]:
folder_relpath = _to_posix(str(folder))
track_id: str = folder.name
slug_date = extract_slug_date(track_id)
# Get the full oneline log once. For archive folders, include both the
# archive path and the original tracks/ path (git mv preserves history
# but git log -- <archive_path> alone may miss pre-move commits)
if folder_relpath.startswith("conductor/archive/"):
original_path: str = folder_relpath.replace("conductor/archive/", "conductor/tracks/", 1)
oneline_log: str = _git_log_multi(folder_relpath, original_path)
else:
oneline_log = _git_log(folder_relpath, "--oneline")
log_lines: list[str] = [l for l in oneline_log.splitlines() if l.strip()]
commit_count: int = len(log_lines)
if slug_date:
date = slug_date
else:
elif log_lines:
# First commit date via reverse log
first_commit = _git_first_line(folder_relpath, "--reverse", "--format=%aI")
date = first_commit[:10] if first_commit else ""
else:
date = ""
# Derive init_sha and end_sha from the oneline log
if log_lines:
init_sha: str = log_lines[-1].split(" ", 1)[0] # oldest (last in reverse-chronological git log)
end_sha: str = log_lines[0].split(" ", 1)[0] # newest (first in git log)
else:
init_sha = ""
end_sha = ""
metadata_path = folder / "metadata.json"
meta_status: str = ""
if metadata_path.is_file():
@@ -287,6 +340,7 @@ def walk_track_folders(root: Path) -> list[dict]:
except (json.JSONDecodeError, OSError):
pass
state_status: str = _parse_state_status(folder / "state.toml")
work_commits: int = _count_work_commits_from_log(oneline_log)
status, confidence, reason = classify_status(
folder_link=folder_relpath,
current=meta_status or default_status,
@@ -294,16 +348,9 @@ def walk_track_folders(root: Path) -> list[dict]:
repo_root=repo_root,
reports_dir=reports_dir,
state_status=state_status,
work_commits=work_commits,
)
summary: str = extract_summary(folder)
init_sha: str = _git_first_line(folder_relpath, "--reverse", "--format=%h")
end_sha: str = _git_first_line(folder_relpath, "-1", "--format=%h")
if init_sha and end_sha:
range_log = _git_log(folder_relpath, "--oneline", f"{init_sha}..{end_sha}")
commit_count: int = range_log.count("\n") + (1 if init_sha != end_sha else 0)
else:
fallback_log = _git_log(folder_relpath, "--oneline")
commit_count = fallback_log.count("\n")
try:
folder_link = _to_posix(str(folder.relative_to(repo_root)))
except ValueError: