84 lines
3.2 KiB
Python
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()
|