Files
management/analysis/scripts/inv_claims.py
T
Thore Cimbal b8b853163d analysis: add deterministic inventory scripts and raw data
run_all.sh reproduces every file under analysis/data/ from zero: it
clones the in-scope components if missing, exports group issue metadata
from git.lab, and regenerates the inventories. Reruns are diff-clean --
no wall-clock time enters an output; 'days since' is measured against
the management repo's HEAD date.

The management repo is inventoried at main, not at the analysis branch,
so this analysis does not observe its own commits. inv_repo.py aborts
the run if anything outside analysis/ was modified.
2026-08-09 12:00:00 +00:00

84 lines
3.2 KiB
Python

"""Extract checkable claims from the management repo's docs -> claims.tsv.
Phase 1 only *extracts*; nothing here judges whether a claim is true.
Verification happens in Phase 2.
A script cannot recognise a claim semantically, so extraction is trigger
based: a doc line is emitted when it matches at least one pattern that
marks it as asserting something checkable about the world (a component,
a count, a status, a version, a path, a date, the mirror topology, an
issue). Recall is favoured over precision -- a false positive costs a
Phase-2 glance, a false negative loses evidence.
Only management-repo docs are scanned: the mandate scopes claims to the
management repo's statements about its components.
"""
import re
from common import DOC_SUFFIXES, MGMT_REPO, cell, days_since, last_commit, \
tracked_files, write_tsv
COMPONENT_WORDS = (
r"ThreadNet[- ](?:Web|Call|Git|Operating|Server Suite)|threadnet-call|"
r"thread-net-git|threadnet-operating|threadnet-web|axion1337\.chat-gitops|"
r"game-operating|gameserver|management"
)
TRIGGERS = [
("component-ref", re.compile(COMPONENT_WORDS, re.I)),
("count", re.compile(
r"\b(ein|zwei|drei|vier|fünf|sechs|sieben|acht|neun|zehn|\d+)\s+"
r"(Repos?|Produkt-Repos?|Projekte?|Issues?|Commits?|Pipelines?|Themes?|"
r"Hosts?|Mirrors?)\b", re.I)),
("status", re.compile(
r"\b(erledigt|offen|live|aktiv|geschlossen|leer|umgezogen|entfallen|"
r"abgelöst|veraltet|überholt|scharf|grün|rot|tabu|kanonisch|"
r"zurückgestellt|verifiziert|bereinigt)\b", re.I)),
("version", re.compile(r"\bv?\d+\.\d+\.\d+\b|\b\d+\.\d+\.\d+-[\w.]+\b")),
("path-claim", re.compile(r"`[^`]*(?:/[^`]*|\.(?:md|ya?ml|json|py|ts|toml|crt))`")),
("date-claim", re.compile(r"\b(seit|Stand|bis|am|ab)\s+\d{4}-\d{2}-\d{2}\b", re.I)),
("mirror-topology", re.compile(
r"\b(Mirror|Spiegel|gespiegelt|Gitea|rohana|Push-Mirror|kanonisch|Flux-Source)\b",
re.I)),
("issue-ref", re.compile(r"(?:#\d+\b|/-/issues/\d+|\b[A-Z]{3,8}-\d{2}\b)")),
]
FENCE_RE = re.compile(r"^\s*(```|~~~)")
def main():
rows = []
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):
iso, _, _ = last_commit("management", MGMT_REPO, src)
text = (MGMT_REPO / src).read_text(encoding="utf-8")
in_fence = False
for lineno, line in enumerate(text.splitlines(), start=1):
if FENCE_RE.match(line):
in_fence = not in_fence
continue
stripped = line.strip()
if len(stripped) < 12:
continue
hits = [name for name, rx in TRIGGERS if rx.search(stripped)]
if not hits:
continue
rows.append([
src, lineno, ";".join(hits), "yes" if in_fence else "no",
iso, days_since(iso), cell(stripped)[:400],
])
rows.sort(key=lambda r: (r[0], r[1]))
write_tsv(
"claims.tsv",
["path", "line", "triggers", "in_code_block", "doc_last_commit_date",
"doc_days_since_change", "claim_text"],
rows,
)
if __name__ == "__main__":
main()