Skip to content

Commit eb4d9e4

Browse files
ci: add claim safety contract check
1 parent 1692aca commit eb4d9e4

1 file changed

Lines changed: 91 additions & 0 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python3
2+
"""Documentation claim-safety check for LiouScope.
3+
4+
The goal is not to ban strong claims forever; the goal is to prevent release,
5+
production, citation, and certification claims from appearing without explicit
6+
qualifiers or linked evidence.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import re
12+
import sys
13+
from pathlib import Path
14+
15+
ROOT = Path(__file__).resolve().parents[2]
16+
DOC_PATHS = [ROOT / "README.md", ROOT / "CHANGELOG.md", ROOT / "docs"]
17+
18+
RISK_PATTERNS = [
19+
re.compile(r"\bproduction[- ]ready\b", re.IGNORECASE),
20+
re.compile(r"\bexternally certified\b", re.IGNORECASE),
21+
re.compile(r"\bclinical(?:ly)? validated\b", re.IGNORECASE),
22+
re.compile(r"\boperational(?:ly)? validated\b", re.IGNORECASE),
23+
re.compile(r"\bPyPI[- ]published\b", re.IGNORECASE),
24+
re.compile(r"\bDOI\b.*\b(complete|published|archived|released)\b", re.IGNORECASE),
25+
re.compile(r"\bZenodo\b.*\b(complete|published|archived|released)\b", re.IGNORECASE),
26+
re.compile(r"\brelease[- ]complete\b", re.IGNORECASE),
27+
]
28+
29+
ALLOW_MARKERS = [
30+
"not ",
31+
"no ",
32+
"does not ",
33+
"must not ",
34+
"unless ",
35+
"until ",
36+
"unverified",
37+
"open gap",
38+
"missing evidence",
39+
"not for diagnostic or operational use",
40+
"does not certify",
41+
]
42+
43+
44+
def _iter_docs() -> list[Path]:
45+
paths: list[Path] = []
46+
for root in DOC_PATHS:
47+
if root.is_file():
48+
paths.append(root)
49+
elif root.is_dir():
50+
paths.extend(sorted(root.rglob("*.md")))
51+
return paths
52+
53+
54+
def _line_is_allowed(line: str) -> bool:
55+
lowered = line.lower()
56+
return any(marker in lowered for marker in ALLOW_MARKERS)
57+
58+
59+
def main() -> int:
60+
errors: list[str] = []
61+
docs = _iter_docs()
62+
63+
for path in docs:
64+
text = path.read_text(encoding="utf-8")
65+
for line_number, line in enumerate(text.splitlines(), start=1):
66+
if _line_is_allowed(line):
67+
continue
68+
for pattern in RISK_PATTERNS:
69+
if pattern.search(line):
70+
rel = path.relative_to(ROOT)
71+
errors.append(
72+
f"{rel}:{line_number}: risky unsupported claim wording: {line.strip()}"
73+
)
74+
break
75+
76+
if errors:
77+
print("Claim-safety check failed:", file=sys.stderr)
78+
for error in errors:
79+
print(f"- {error}", file=sys.stderr)
80+
print(
81+
"\nFix by adding evidence, linking a release audit, or marking the status as not/UNVERIFIED.",
82+
file=sys.stderr,
83+
)
84+
return 1
85+
86+
print(f"Claim-safety check passed for {len(docs)} markdown files.")
87+
return 0
88+
89+
90+
if __name__ == "__main__":
91+
raise SystemExit(main())

0 commit comments

Comments
 (0)