"""Check a usability-findings report for uncited judgment claims.
This skill's house rule (SKILL.md § House rules #2): usability findings
must cite Nielsen's 10 Heuristics or a named principle — never bare
preference or gut feel. This script is the enforcement.
Usage: check_heuristic_citation.py <findings.md> [more.md ...]
Scans for claim-shaped sentences ("users found X confusing", "users
struggled to...", "this is unclear/frustrating/difficult", etc.) and flags
any with no citation within the same sentence or the surrounding two lines.
- A named Nielsen heuristic: either "Nielsen #N" / "heuristic #N" or the
heuristic's own name (see references/ux-heuristics.md), e.g.
"Error prevention", "Match between system and real world".
- A named law: Hick's Law, Fitts's Law, Miller's Law / Miller's 7±2,
Jakob's Law, cognitive load, mental model(s).
- A WCAG success criterion: "WCAG 2.2 SC 1.4.3", "WCAG AA", "WCAG AAA".
Exit 0 if every claim is cited. Exit 1 with line-level messages otherwise.
# Claim-shaped judgment language: a finding asserting a usability problem.
r"\busers?\s+(?:found|felt|reported)\b",
r"\busers?\s+struggled?\s+to\b",
r"\busers?\s+(?:couldn'?t|could not|were unable to)\b",
r"\bthis\s+is\s+(?:unclear|frustrating|difficult|confusing|hard to)\b",
r"\b(?:is|was|seems?|appears?)\s+(?:unclear|frustrating|difficult|confusing)\b",
r"\bparticipants?\s+(?:struggled?|failed|couldn'?t|were confused)\b",
r"\busers?\s+(?:were|got|seemed)\s+confused\b",
r"\bhard\s+to\s+(?:find|use|understand|navigate)\b",
r"\bdifficult\s+to\s+(?:find|use|understand|navigate|complete)\b",
NIELSEN_HEURISTIC_NAMES = [
"system status visibility",
"visibility of system status",
"match between system and real world",
"user control and freedom",
"consistency and standards",
"recognition rather than recall",
"flexibility and efficiency",
"aesthetic and minimalist design",
"help and documentation",
r"\bmiller'?s\s+(?:law|7\s*[±+\-]\s*2)\b",
r"\bwcag\s*2?\.?2?\s*(?:sc\s*[\d.]+|aa\+?|aaa|a\b)",
] + [re.escape(name) for name in NIELSEN_HEURISTIC_NAMES]
CLAIM_RE = re.compile("|".join(CLAIM_PATTERNS), re.IGNORECASE)
CITATION_RE = re.compile("|".join(CITATION_PATTERNS), re.IGNORECASE)
CONTEXT_WINDOW = 2 # lines above/below to search for a nearby citation
def check_file(path: str) -> list[str]:
lines = open(path, encoding="utf-8").read().splitlines()
for i, line in enumerate(lines):
if not CLAIM_RE.search(line):
window_start = max(0, i - CONTEXT_WINDOW)
window_end = min(len(lines), i + CONTEXT_WINDOW + 1)
window = "\n".join(lines[window_start:window_end])
if not CITATION_RE.search(window):
snippet = snippet[:97] + "..."
f"{path}:{i + 1}: uncited usability claim — {snippet!r} "
f"(cite a Nielsen heuristic, Hick's/Fitts's/Miller's/Jakob's Law, "
f"or a WCAG success criterion within 2 lines)"
print("Usage: check_heuristic_citation.py <findings.md> [more.md ...]",
for path in sys.argv[1:]:
all_errors.extend(check_file(path))
except FileNotFoundError:
print(f"ERROR: file not found: {path}", file=sys.stderr)
print(f"\n{len(all_errors)} uncited claim(s) found.")
print("✅ every usability claim cites a named heuristic, law, or WCAG criterion")
if __name__ == "__main__":