Files
management/analysis/scripts/inv_shas.py
T
Thore Cimbal 959bf882e9 analysis: resolve SHA references and measure timestamp anonymisation
Two checks the human asked for after Phase 1: whether the mapping list
from the 2026-08-07 history rewrite still resolves older references, and
how far the anonymisation rule actually reaches.

sha_refs.tsv verifies all 251 mapping rows against the repos and
resolves every SHA cited in a management doc.
timestamp_anonymisation.tsv separates the project's own commits from
upstream fork history before counting non-compliant timestamps.
2026-08-10 12:00:00 +00:00

145 lines
5.5 KiB
Python

"""Resolve commit-SHA references -> analysis/data/sha_refs.tsv.
Added at the human's prompt during Phase 2: the 2026-08-07 history
rewrite gave 251 commits new SHAs, and shared/commit-zuordnung-2026-08-07.md
is the mapping list meant to keep older references resolvable. This
script tests whether that actually holds.
Two row kinds share one file:
doc-reference a SHA cited in a management-repo doc
mapping-entry an (old -> new) pair from the mapping table itself
Scope note: only management-repo docs are scanned for references. The
component repos are forks whose upstream docs cite thousands of upstream
SHAs that were never part of this project's history; including them
would bury the signal.
"""
import re
from common import (
DOC_SUFFIXES, MGMT_REPO, cell, git, repos, tracked_files, write_tsv,
)
MAPPING_DOC = "shared/commit-zuordnung-2026-08-07.md"
# Section heading -> repo name used elsewhere in the analysis.
SECTION_REPO = {
"gitops": "axion1337.chat-gitops",
"management": "management",
"ThreadNet-Web": "ThreadNet-Web",
"threadnet-call": "threadnet-call",
}
MAPPING_ROW = re.compile(r"^\|\s*`([0-9a-f]{7,40})`\s*\|\s*`([0-9a-f]{7,40})`\s*\|")
SECTION = re.compile(r"^##\s+(.+?)\s+—\s+(\d+)\s+Commits\s*$")
# A SHA-shaped token: 7-40 hex chars, mixing letters and digits so that
# plain numbers and hex-free words do not qualify.
SHA_TOKEN = re.compile(r"(?<![0-9a-zA-Z_/-])([0-9a-f]{7,40})(?![0-9a-zA-Z_-])")
def all_commits(repo):
"""Every commit SHA reachable from any ref, including remotes and tags."""
out = git(repo, "rev-list", "--all")
return {line.strip() for line in out.splitlines() if line.strip()}
def resolve(sha, index):
"""Repos in which sha is a commit (prefix match), sorted."""
hits = []
for name, commits in index.items():
if sha in commits or any(c.startswith(sha) for c in commits):
hits.append(name)
return sorted(hits)
def parse_mapping(text):
"""[(repo, old, new, declared_section_count)] from the mapping table."""
rows, section, declared = [], None, 0
for line in text.splitlines():
m = SECTION.match(line)
if m:
section = SECTION_REPO.get(m.group(1), m.group(1))
declared = int(m.group(2))
continue
m = MAPPING_ROW.match(line)
if m and section:
rows.append((section, m.group(1), m.group(2), declared))
return rows
def main():
index = {name: all_commits(path) for name, path in repos()}
known_repos = set(index)
mapping_text = (MGMT_REPO / MAPPING_DOC).read_text(encoding="utf-8")
mapping = parse_mapping(mapping_text)
old_to_new = {old: (repo, new) for repo, old, new, _ in mapping}
rows = []
# 1. The mapping table checked against the repos it describes.
for repo, old, new, _declared in mapping:
if repo not in known_repos:
status, note = "unverifiable", f"repo {repo} not in analysis scope"
else:
new_ok = bool(resolve(new, {repo: index[repo]}))
old_ok = bool(resolve(old, {repo: index[repo]}))
if new_ok and not old_ok:
status, note = "ok", "new resolves, old gone as expected"
elif new_ok and old_ok:
status, note = "ok-both-present", "old SHA still reachable from some ref"
elif not new_ok and old_ok:
status, note = "inverted", "new SHA missing but old one resolves"
else:
status, note = "unresolvable", "neither old nor new SHA exists in the repo"
rows.append(["mapping-entry", repo, MAPPING_DOC, "", f"{old}->{new}",
status, repo if status.startswith("ok") else "", note])
# 2. Every SHA cited in a management doc.
docs = [f for f in tracked_files("management", MGMT_REPO)
if any(f.endswith(s) for s in DOC_SUFFIXES)]
for src in sorted(docs):
text = (MGMT_REPO / src).read_text(encoding="utf-8")
for lineno, line in enumerate(text.splitlines(), start=1):
if src == MAPPING_DOC and MAPPING_ROW.match(line):
continue # already covered as mapping-entry rows
for sha in SHA_TOKEN.findall(line):
hits = resolve(sha, index)
if hits:
status = "resolves"
note = f"found in {','.join(hits)}"
elif sha in old_to_new:
repo, new = old_to_new[sha]
if repo in known_repos and resolve(new, {repo: index[repo]}):
status = "resolves-via-mapping"
note = f"pre-rewrite SHA; mapping -> {new} in {repo}"
hits = [repo]
else:
status = "mapping-stale"
note = f"mapping points to {new} in {repo}, which does not exist"
else:
status = "orphan"
note = "resolves in no analysed repo and is not in the mapping table"
rows.append(["doc-reference", "management", src, lineno, sha,
status, ",".join(hits), cell(line.strip())[:160]])
rows.sort(key=lambda r: (r[0], r[2], r[3] if r[3] != "" else 0, r[4]))
write_tsv(
"sha_refs.tsv",
["kind", "repo", "path", "line", "sha", "status", "resolved_in", "note"],
rows,
)
counts = {}
for r in rows:
counts[(r[0], r[5])] = counts.get((r[0], r[5]), 0) + 1
for key in sorted(counts):
print(f" {key[0]:<14} {key[1]:<22} {counts[key]}")
if __name__ == "__main__":
main()