Skip to content

Check_heuristic_citation

FieldValue
TypeSkill Resource
Source~/.copilot/skills/ux/scripts/check_heuristic_citation.py
DescriptionNot specified

Source Content

#!/usr/bin/env python3
"""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 citation is one of:
- 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.
"""
import re
import sys
# Claim-shaped judgment language: a finding asserting a usability problem.
CLAIM_PATTERNS = [
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",
"error prevention",
"error messages",
"recognition rather than recall",
"flexibility and efficiency",
"aesthetic and minimalist design",
"error recovery",
"help and documentation",
]
CITATION_PATTERNS = [
r"nielsen\s*#?\s*\d+",
r"heuristic\s*#?\s*\d+",
r"\bhick'?s\s+law\b",
r"\bfitts'?s?\s+law\b",
r"\bmiller'?s\s+(?:law|7\s*[±+\-]\s*2)\b",
r"\bjakob'?s\s+law\b",
r"\bcognitive\s+load\b",
r"\bmental\s+models?\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]:
errors = []
lines = open(path, encoding="utf-8").read().splitlines()
for i, line in enumerate(lines):
if not CLAIM_RE.search(line):
continue
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 = line.strip()
if len(snippet) > 100:
snippet = snippet[:97] + "..."
errors.append(
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)"
)
return errors
def main() -> int:
if len(sys.argv) < 2:
print("Usage: check_heuristic_citation.py <findings.md> [more.md ...]",
file=sys.stderr)
return 2
all_errors = []
for path in sys.argv[1:]:
try:
all_errors.extend(check_file(path))
except FileNotFoundError:
print(f"ERROR: file not found: {path}", file=sys.stderr)
return 2
if all_errors:
for err in all_errors:
print(f"❌ {err}")
print(f"\n{len(all_errors)} uncited claim(s) found.")
return 1
print("✅ every usability claim cites a named heuristic, law, or WCAG criterion")
return 0
if __name__ == "__main__":
raise SystemExit(main())