← Files Frontend Design PremiumARCHIVED FILE
skills/frontend-design-premium/scripts/audit_project.py
36.8 KB · Oct 3, 2026 · 06:31 UTC
#!/usr/bin/env python3
"""Deterministic, non-executing audit for frontend-design-premium projects."""
from __future__ import annotations
import argparse
import json
import re
import sys
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Iterable
SCHEMA_VERSION = 1
SOURCE_SUFFIXES = {".vue", ".tsx", ".jsx", ".ts", ".js", ".svelte", ".html", ".css", ".scss"}
IGNORED_PARTS = {"node_modules", ".git", "dist", "build", "coverage", ".next", ".nuxt"}
MAP_COLUMNS = ("Capability", "Canonical owner", "Source of truth", "Allowed variants", "Verification")
KNOWN_CAPABILITIES = {"Table Selection", "Select/Listbox", "Date", "Form", "Scrollbar", "Toast", "CRUD"}
@dataclass(frozen=True)
class Finding:
file: str
line: int | None
rule_id: str
severity: str
category: str
message: str
remediation: str
def to_dict(self) -> dict[str, object]:
payload = asdict(self)
payload["ruleId"] = payload.pop("rule_id")
return payload
@dataclass(frozen=True)
class AuditResult:
mode: str
project_root: Path
findings: tuple[Finding, ...]
def to_dict(self) -> dict[str, object]:
ordered = sorted(
self.findings,
key=lambda item: (item.category, item.rule_id, item.file, item.line or 0, item.message),
)
return {
"schemaVersion": SCHEMA_VERSION,
"mode": self.mode,
"projectRoot": str(self.project_root),
"findings": [item.to_dict() for item in ordered],
"summary": {
"total": len(ordered),
"errors": sum(item.severity == "error" for item in ordered),
"warnings": sum(item.severity == "warning" for item in ordered),
"violations": sum(item.category == "violation" for item in ordered),
"unresolved": sum(item.category == "unresolved" for item in ordered),
},
}
@dataclass(frozen=True)
class HtmlTag:
name: str
attributes: str
start: int
end: int
closing: bool
self_closing: bool
@dataclass(frozen=True)
class CssRule:
selector: str
body: str
start: int
def finding(
rule_id: str,
message: str,
remediation: str,
*,
file: str = "premium-ui.json",
line: int | None = None,
severity: str = "error",
category: str = "violation",
) -> Finding:
return Finding(file, line, rule_id, severity, category, message, remediation)
def relative(root: Path, path: Path) -> str:
try:
return path.resolve().relative_to(root).as_posix()
except ValueError:
return str(path.resolve())
def line_number(text: str, offset: int) -> int:
return text.count("\n", 0, offset) + 1
def mask_comments(text: str) -> str:
"""Mask HTML/CSS block comments while preserving offsets and line numbers."""
def replace(match: re.Match[str]) -> str:
return "".join("\n" if char == "\n" else " " for char in match.group(0))
return re.sub(r"<!--.*?-->|/\*.*?\*/", replace, text, flags=re.DOTALL)
def find_tag_end(text: str, start: int) -> int | None:
html_quote: str | None = None
expression_quote: str | None = None
expression_depth = 0
escaped = False
for offset in range(start, len(text)):
char = text[offset]
if html_quote is not None:
if char == html_quote:
html_quote = None
continue
if expression_quote is not None:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == expression_quote:
expression_quote = None
continue
if expression_depth:
if char in {'"', "'", "`"}:
expression_quote = char
elif char == "{":
expression_depth += 1
elif char == "}":
expression_depth -= 1
continue
if char in {'"', "'"}:
html_quote = char
elif char == "{":
expression_depth = 1
elif char == ">":
return offset + 1
return None
def find_expression_end(text: str, start: int) -> int:
"""Return the offset after a balanced JSX/template expression."""
depth = 0
quote: str | None = None
escaped = False
offset = start
while offset < len(text):
char = text[offset]
next_char = text[offset + 1] if offset + 1 < len(text) else ""
if quote is not None:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == quote:
quote = None
elif char in {'"', "'", "`"}:
quote = char
elif char == "/" and next_char == "/":
newline = text.find("\n", offset + 2)
offset = len(text) if newline < 0 else newline
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return offset + 1
offset += 1
return len(text)
def is_regex_literal_start(text: str, offset: int, boundary: int = 0) -> bool:
prefix = text[boundary:offset].rstrip()
if not prefix:
return True
if prefix[-1] in "=([{,:;!?&|+-*%^~<>":
return True
return bool(re.search(
r"\b(?:return|case|throw|yield|await|typeof|instanceof|in|of|delete|void|new)\s*$",
prefix,
))
def find_regex_literal_end(text: str, start: int) -> int | None:
escaped = False
character_class = False
offset = start + 1
while offset < len(text):
char = text[offset]
if char == "\n" and not escaped:
return None
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == "[":
character_class = True
elif char == "]" and character_class:
character_class = False
elif char == "/" and not character_class:
offset += 1
while offset < len(text) and text[offset].isalpha():
offset += 1
return offset
offset += 1
return None
def next_regex_literal(text: str, start: int, limit: int) -> int:
offset = text.find("/", start, limit)
while offset >= 0:
if not text.startswith("//", offset) and is_regex_literal_start(text, offset, start):
return offset
offset = text.find("/", offset + 1, limit)
return -1
def mask_expression_code(text: str) -> str:
"""Mask code literals/comments in an expression while retaining nested JSX tags."""
output = list(text)
tag_pattern = re.compile(r"<\s*/?\s*[A-Za-z][\w:-]*\b")
offset = 0
while offset < len(text):
tag = tag_pattern.match(text, offset)
if tag is not None:
end = find_tag_end(text, tag.end())
if end is not None:
offset = end
continue
if text.startswith("//", offset):
newline = text.find("\n", offset + 2)
end = len(text) if newline < 0 else newline
for index in range(offset, end):
output[index] = " "
offset = end
continue
if text[offset] == "/" and is_regex_literal_start(text, offset):
end = find_regex_literal_end(text, offset)
if end is not None:
for index in range(offset, end):
if output[index] != "\n":
output[index] = " "
offset = end
continue
if text[offset] in {'"', "'", "`"}:
quote = text[offset]
end = offset + 1
escaped = False
while end < len(text):
char = text[end]
end += 1
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == quote:
break
for index in range(offset, end):
if output[index] != "\n":
output[index] = " "
offset = end
continue
offset += 1
return "".join(output)
def iter_html_tags(text: str, names: set[str] | None = None) -> Iterable[HtmlTag]:
searchable = mask_comments(text)
pattern = re.compile(r"<\s*(?P<closing>/)?\s*(?P<name>[A-Za-z][\w:-]*)\b")
void_tags = {"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"}
raw_content_tags = {"script", "style"}
stack: list[str] = []
offset = 0
while offset < len(searchable):
if stack and stack[-1] in raw_content_tags:
closing = re.search(rf"<\s*/\s*{re.escape(stack[-1])}\b", searchable[offset:], re.IGNORECASE)
if closing is None:
return
candidate = offset + closing.start()
elif not stack and searchable[offset] in {'"', "'", "`"}:
quote = searchable[offset]
offset += 1
escaped = False
while offset < len(searchable):
char = searchable[offset]
offset += 1
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == quote:
break
continue
elif not stack and searchable.startswith("//", offset):
newline = searchable.find("\n", offset + 2)
offset = len(searchable) if newline < 0 else newline + 1
continue
elif not stack and searchable[offset] == "/" and is_regex_literal_start(searchable, offset):
regex_end = find_regex_literal_end(searchable, offset)
offset = offset + 1 if regex_end is None else regex_end
continue
else:
candidate = searchable.find("<", offset)
if candidate < 0:
return
if stack:
expression = searchable.find("{", offset)
if expression >= 0 and expression < candidate:
expression_end = find_expression_end(searchable, expression)
searchable = (
searchable[:expression]
+ mask_expression_code(searchable[expression:expression_end])
+ searchable[expression_end:]
)
offset = expression + 1
continue
if not stack:
next_quote = min(
(position for quote in ('"', "'", "`") if (position := searchable.find(quote, offset)) >= 0),
default=-1,
)
next_comment = searchable.find("//", offset)
next_regex = next_regex_literal(searchable, offset, candidate)
boundaries = [position for position in (next_quote, next_comment, next_regex) if position >= 0]
if boundaries and min(boundaries) < candidate:
offset = min(boundaries)
continue
match = pattern.match(searchable, candidate)
if match is None:
offset = candidate + 1
continue
end = find_tag_end(searchable, match.end())
if end is None:
return
name = match.group("name").casefold()
attributes = searchable[match.end():end - 1]
closing = bool(match.group("closing"))
self_closing = attributes.rstrip().endswith("/")
if names is None or name in names:
yield HtmlTag(
name=name,
attributes=attributes,
start=match.start(),
end=end,
closing=closing,
self_closing=self_closing,
)
if closing:
match_index = next((index for index in range(len(stack) - 1, -1, -1) if stack[index] == name), None)
if match_index is not None:
del stack[match_index:]
elif not self_closing and name not in void_tags:
stack.append(name)
offset = end
def has_attribute(attributes: str, name: str) -> bool:
return bool(re.search(rf"(?<![\w:-]){re.escape(name)}(?=\s|=|/|$)", attributes, re.IGNORECASE))
def attribute_value(attributes: str, name: str) -> str | None:
match = re.search(
rf"(?<![\w:-]){re.escape(name)}\s*=\s*(?:\"([^\"]*)\"|'([^']*)'|([^\s/>]+))",
attributes,
re.IGNORECASE,
)
if match is None:
return None
return next((value for value in match.groups() if value is not None), "")
def html_regions(tags: Iterable[HtmlTag], text_length: int) -> list[tuple[HtmlTag, int]]:
void_tags = {"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"}
stack: list[HtmlTag] = []
regions: list[tuple[HtmlTag, int]] = []
for tag in tags:
if tag.closing:
match_index = next((
index for index in range(len(stack) - 1, -1, -1)
if stack[index].name == tag.name
), None)
if match_index is not None:
opening = stack[match_index]
del stack[match_index:]
regions.append((opening, tag.start))
elif not tag.self_closing and tag.name not in void_tags:
stack.append(tag)
regions.extend((opening, text_length) for opening in stack)
return regions
def iter_css_rules(text: str) -> Iterable[CssRule]:
searchable = mask_comments(text)
stack: list[tuple[str, int, int]] = []
statement_start = 0
quote: str | None = None
escaped = False
for offset, char in enumerate(searchable):
if quote is not None:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == quote:
quote = None
continue
if char in {'"', "'"}:
quote = char
elif char == ";":
statement_start = offset + 1
elif char == "{":
raw_selector = searchable[statement_start:offset]
selector = raw_selector.strip()
selector_start = statement_start + len(raw_selector) - len(raw_selector.lstrip())
stack.append((selector, selector_start, offset + 1))
statement_start = offset + 1
elif char == "}":
if stack:
selector, selector_start, body_start = stack.pop()
yield CssRule(selector, searchable[body_start:offset], selector_start)
statement_start = offset + 1
def split_selectors(selector: str) -> list[str]:
"""Split a selector list without treating commas inside functions as separators."""
selectors: list[str] = []
start = 0
depth = 0
quote: str | None = None
escaped = False
for offset, char in enumerate(selector):
if quote is not None:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == quote:
quote = None
elif char in {'"', "'"}:
quote = char
elif char in "([":
depth += 1
elif char in ")]":
depth = max(0, depth - 1)
elif char == "," and depth == 0:
selectors.append(selector[start:offset].strip())
start = offset + 1
selectors.append(selector[start:].strip())
return [item for item in selectors if item]
def scrollbar_surfaces(selector: str) -> list[str]:
surfaces: list[str] = []
for item in split_selectors(selector):
match = re.search(r"::\s*-webkit-scrollbar(?:-[a-z-]+)?", item, flags=re.IGNORECASE)
if match is not None:
surfaces.append(item[:match.start()].strip())
return surfaces
def owning_surface(selector: str) -> str:
"""Remove state qualifiers while preserving the selector's element identity."""
without_attributes = re.sub(r"\[[^\]]*\]", "", selector)
return re.sub(r"(?<![:\\]):(?!:)[\w-]+(?:\([^()]*\))?", "", without_attributes).strip()
def selector_covers_surface(selector: str, surface: str) -> bool:
owner = owning_surface(surface)
for candidate in split_selectors(selector):
if candidate in {"*", ":root", "html", "body", "html *", "body *", ":where(*)"}:
return True
if candidate == surface or candidate == owner:
return True
return False
def direct_css_body(body: str) -> str:
"""Mask nested rule bodies so only declarations owned by this rule remain."""
output = list(body)
depth = 0
quote: str | None = None
escaped = False
for offset, char in enumerate(body):
if quote is not None:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == quote:
quote = None
elif char in {'"', "'"}:
quote = char
elif char == "{":
depth += 1
output[offset] = " "
elif char == "}":
output[offset] = " "
depth = max(0, depth - 1)
elif depth and char != "\n":
output[offset] = " "
return "".join(output)
def standards_cover_surface(surface: str, rules: Iterable[CssRule]) -> bool:
properties: set[str] = set()
for rule in rules:
if rule.selector.startswith("@") or not selector_covers_surface(rule.selector, surface):
continue
body = direct_css_body(rule.body)
if re.search(r"(?<![-\w])scrollbar-color\s*:", body, flags=re.IGNORECASE):
properties.add("color")
if re.search(r"(?<![-\w])scrollbar-width\s*:", body, flags=re.IGNORECASE):
properties.add("width")
return properties == {"color", "width"}
def load_manifest(path: Path) -> tuple[dict[str, Any], list[Finding]]:
if not path.exists():
return {}, []
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as error:
return {}, [finding(
"config.invalid-json",
f"Cannot parse project manifest: {error}",
"Fix the JSON syntax or pass --config with a valid premium-ui.json file.",
file=str(path),
category="unresolved",
)]
if not isinstance(payload, dict):
return {}, [finding(
"config.invalid-json",
"Project manifest must contain a JSON object.",
"Replace the top-level JSON value with an object.",
file=str(path),
category="unresolved",
)]
return payload, []
def parse_canonical_map(text: str) -> tuple[set[str], bool]:
lines = text.splitlines()
expected = [column.casefold() for column in MAP_COLUMNS]
for index, raw_line in enumerate(lines):
cells = [cell.strip() for cell in raw_line.strip().strip("|").split("|")]
if [cell.casefold() for cell in cells] != expected:
continue
rows: set[str] = set()
for candidate in lines[index + 2 :]:
if not candidate.strip().startswith("|"):
break
row = [cell.strip() for cell in candidate.strip().strip("|").split("|")]
if len(row) == len(MAP_COLUMNS) and row[0] in KNOWN_CAPABILITIES and all(row[1:]):
rows.add(row[0])
return rows, True
return set(), False
def source_roots(project_root: Path, manifest: dict[str, Any]) -> list[Path]:
configured = manifest.get("sourceRoots")
if isinstance(configured, list) and all(isinstance(value, str) for value in configured):
roots = [project_root / value for value in configured]
else:
roots = [project_root / value for value in ("src", "app", "pages")]
existing = [root for root in roots if root.exists() and root.is_dir()]
return existing or [project_root]
def iter_source_files(project_root: Path, manifest: dict[str, Any]) -> Iterable[Path]:
seen: set[Path] = set()
for root in source_roots(project_root, manifest):
for path in root.rglob("*"):
if not path.is_file() or path.suffix.lower() not in SOURCE_SUFFIXES:
continue
if any(part in IGNORED_PARTS for part in path.parts):
continue
resolved = path.resolve()
if resolved not in seen:
seen.add(resolved)
yield path
def inspect_contracts(project_root: Path, manifest: dict[str, Any]) -> list[Finding]:
findings: list[Finding] = []
if manifest.get("profile") != "product-admin":
return findings
if not (project_root / "DESIGN.md").exists():
findings.append(finding(
"contract.design-missing",
"A product/admin project has no maintained DESIGN.md.",
"Create DESIGN.md or document the maintained equivalent in project policy.",
file="DESIGN.md",
))
map_value = manifest.get("canonicalMap", "UX-CONTRACT.md")
map_path = project_root / map_value if isinstance(map_value, str) else project_root / "UX-CONTRACT.md"
if not map_path.exists():
findings.append(finding(
"contract.ux-missing",
"A product/admin project has no maintained UX contract.",
"Create UX-CONTRACT.md with a Canonical UI Map.",
file=relative(project_root, map_path),
category="unresolved",
))
findings.append(finding(
"canonical.map-missing",
"Canonical UI ownership cannot be resolved because its map is missing.",
"Add the exact five-column Canonical UI Map to the configured UX contract.",
file=relative(project_root, map_path),
category="unresolved",
))
return findings
try:
text = map_path.read_text(encoding="utf-8")
except (OSError, UnicodeError) as error:
findings.append(finding(
"canonical.map-unreadable",
f"The configured Canonical UI Map cannot be read as UTF-8 text: {error}",
"Point canonicalMap to a readable UTF-8 contract file.",
file=relative(project_root, map_path),
category="unresolved",
))
return findings
rows, found_header = parse_canonical_map(text)
if not found_header:
findings.append(finding(
"canonical.map-missing",
"The configured UX contract has no Canonical UI Map with the required columns.",
"Add the exact Capability, Canonical owner, Source of truth, Allowed variants, and Verification columns.",
file=relative(project_root, map_path),
category="unresolved",
))
return findings
required = manifest.get("requiredCapabilities", [])
if not isinstance(required, list):
required = []
for capability in sorted(value for value in required if isinstance(value, str)):
if capability not in rows:
findings.append(finding(
"canonical.owner-unresolved",
f"Canonical owner is unresolved for {capability}.",
f"Add a complete {capability} row to the Canonical UI Map before implementation.",
file=relative(project_root, map_path),
category="unresolved",
))
if rows and required and rows != set(required):
missing = sorted(set(required) - rows)
if missing:
findings.append(finding(
"canonical.map-incomplete",
f"Canonical UI Map is incomplete: {', '.join(missing)}.",
"Complete every capability declared in requiredCapabilities.",
file=relative(project_root, map_path),
category="unresolved",
))
return findings
def inspect_source(project_root: Path, manifest: dict[str, Any]) -> list[Finding]:
findings: list[Finding] = []
ownership = manifest.get("ownership", {})
ownership = ownership if isinstance(ownership, dict) else {}
for path in iter_source_files(project_root, manifest):
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeError) as error:
findings.append(finding(
"source.unreadable",
f"Configured source file cannot be read as UTF-8 text: {error}",
"Convert the source to UTF-8 or remove it from the configured source roots.",
file=relative(project_root, path),
category="unresolved",
))
continue
name = relative(project_root, path)
for match in re.finditer(r'href\s*=\s*["\']#["\']', text, flags=re.IGNORECASE):
findings.append(finding(
"affordance.empty-href",
"Empty hash links look actionable but have no destination.",
"Use a real route/action or render non-interactive text.",
file=name,
line=line_number(text, match.start()),
))
all_tags = list(iter_html_tags(text))
tags = [
tag for tag in all_tags
if tag.name in {"form", "button", "textarea", "select", "input"}
]
form_depth = 0
select_tag: HtmlTag | None = None
native_date_tag: HtmlTag | None = None
for tag in tags:
if tag.name == "form":
if tag.closing:
form_depth = max(0, form_depth - 1)
continue
if not has_attribute(tag.attributes, "novalidate"):
findings.append(finding(
"form.novalidate-missing",
"Application-owned form does not declare its validation owner.",
"Add noValidate/novalidate and implement the canonical validation contract.",
file=name,
line=line_number(text, tag.start),
))
if not tag.self_closing:
form_depth += 1
continue
if tag.closing:
continue
if tag.name == "button":
button_type = (attribute_value(tag.attributes, "type") or "").casefold()
has_action = bool(re.search(
r"(?:@click(?:\.[\w-]+)*|v-on:click(?:\.[\w-]+)*|onclick)(?=\s|=|$)",
tag.attributes,
flags=re.IGNORECASE,
))
is_form_submit = button_type not in {"button", "reset"} and (
bool(form_depth) or has_attribute(tag.attributes, "form")
)
if (
has_attribute(tag.attributes, "disabled")
or button_type == "submit"
or is_form_submit
):
continue
if not has_action:
findings.append(finding(
"affordance.actionless-button",
"Enabled literal button has no detectable action or submit behavior.",
"Connect the button to a real action, make it a submit button, or disable/remove it.",
file=name,
line=line_number(text, tag.start),
))
elif tag.name == "textarea":
has_resize_none = bool(
re.search(r"\bresize-none\b", tag.attributes)
or re.search(r"resize\s*:\s*none", tag.attributes, flags=re.IGNORECASE)
)
if not has_resize_none:
findings.append(finding(
"form.textarea-resize-missing",
"Literal product textarea does not show evidence of the canonical resize-none rule.",
"Use the shared Textarea owner or apply resize-none/resize: none with adequate height or auto-grow behavior.",
file=name,
line=line_number(text, tag.start),
))
elif tag.name == "select" and select_tag is None:
select_tag = tag
elif tag.name == "input" and native_date_tag is None:
input_type = (attribute_value(tag.attributes, "type") or "").casefold()
if input_type in {"date", "time", "month", "week", "datetime-local"}:
native_date_tag = tag
select_owner = ownership.get("Select/Listbox")
if select_tag is not None and select_owner != "native":
undecided = not isinstance(select_owner, str) or not select_owner.strip()
findings.append(finding(
"ownership.native-select-undecided" if undecided else "ownership.native-select-conflict",
(
"Native select is used without an explicit ownership decision."
if undecided
else f"Native select conflicts with recorded Select/Listbox ownership: {select_owner}."
),
"Record Select/Listbox as native or reuse the authored canonical owner.",
file=name,
line=line_number(text, select_tag.start),
category="unresolved" if undecided else "violation",
))
date_owner = ownership.get("Date")
if native_date_tag is not None and date_owner != "native":
undecided = not isinstance(date_owner, str) or not date_owner.strip()
findings.append(finding(
"ownership.native-date-undecided" if undecided else "ownership.native-date-conflict",
(
"Native date/time input is used without an explicit ownership decision."
if undecided
else f"Native date/time input conflicts with recorded Date ownership: {date_owner}."
),
"Record Date as native or reuse the typed/authored canonical owner.",
file=name,
line=line_number(text, native_date_tag.start),
category="unresolved" if undecided else "violation",
))
searchable = mask_comments(text)
for container, region_end in html_regions(all_tags, len(searchable)):
raw_tag = searchable[container.start:container.end]
viewport_match = re.search(
r"(?:h-screen|h-dvh|h-svh|h-lvh|h-full|min-h-screen|100vh|100dvh|100svh|100lvh|height\s*:\s*100%)",
raw_tag,
flags=re.IGNORECASE,
)
overflow_hidden = bool(re.search(
r"overflow-hidden|overflow\s*:\s*hidden",
raw_tag,
flags=re.IGNORECASE,
))
if viewport_match is None or not overflow_hidden:
continue
region = searchable[container.end:region_end]
has_table = bool(re.search(r"<table\b|DataTable|data-table", region, flags=re.IGNORECASE))
has_form = bool(re.search(r"<form\b|AppForm", region, flags=re.IGNORECASE))
if has_table and has_form:
findings.append(finding(
"layout.shared-shell-overflow",
"Table viewport sizing leaks into a shared page/form shell.",
"Give the table body its own bounded scroll surface and let the page/form shell size naturally.",
file=name,
line=line_number(text, container.start + viewport_match.start()),
))
break
if path.suffix.lower() in {".css", ".scss"}:
css_rules = list(iter_css_rules(text))
webkit_rules = [
(rule, surface)
for rule in css_rules
if not rule.selector.startswith("@")
for surface in scrollbar_surfaces(rule.selector)
]
uncovered = next((
rule for rule, surface in webkit_rules
if not standards_cover_surface(surface, css_rules)
), None)
if uncovered is not None:
findings.append(finding(
"scrollbar.webkit-only",
"Scrollbar theme does not provide both standards-based scrollbar properties.",
"Add global scrollbar-color and scrollbar-width standards properties plus fallbacks.",
file=name,
line=line_number(text, uncovered.start),
))
opt_in = next((
rule for rule, _surface in webkit_rules
if re.search(r"\.(?:custom-scrollbar|scrollbar|ui-scroll)\b", rule.selector)
), None)
if opt_in is not None:
findings.append(finding(
"scrollbar.opt-in-base",
"Base scrollbar theming is activated by an opt-in class.",
"Apply base scrollbar tokens globally; reserve classes for geometry or semantic exceptions.",
file=name,
line=line_number(text, opt_in.start),
))
return findings
def inspect_evidence(project_root: Path, manifest: dict[str, Any]) -> list[Finding]:
findings: list[Finding] = []
commands = manifest.get("commands", {})
commands = commands if isinstance(commands, dict) else {}
required = manifest.get("requiredCommands", [])
required = required if isinstance(required, list) else []
for command in sorted(value for value in required if isinstance(value, str)):
if not isinstance(commands.get(command), str) or not commands[command].strip():
findings.append(finding(
"evidence.command-missing",
f"Required runtime verification command is missing: {command}.",
f"Configure commands.{command} and run it separately; the static auditor will not execute it.",
))
evidence = manifest.get("evidence", {})
evidence = evidence if isinstance(evidence, dict) else {}
for key, rule_id, message in (
("crudFullFlow", "evidence.crud-flow-missing", "Declared CRUD full-flow evidence is missing."),
("failurePaths", "evidence.failure-path-missing", "Declared failure-path evidence is missing."),
):
value = evidence.get(key)
if value is None:
continue
if not isinstance(value, str) or not (project_root / value).is_file():
findings.append(finding(
rule_id,
message,
f"Point evidence.{key} to an existing project-owned test or report and run its command separately.",
))
return findings
def audit_project(project_root: Path, mode: str, config_path: Path | None = None) -> AuditResult:
root = project_root.resolve()
manifest_path = config_path.resolve() if config_path else root / "premium-ui.json"
manifest, config_findings = load_manifest(manifest_path)
if config_findings:
return AuditResult(mode, root, tuple(config_findings))
findings = [
*inspect_contracts(root, manifest),
*inspect_source(root, manifest),
*inspect_evidence(root, manifest),
]
return AuditResult(mode, root, tuple(findings))
def exit_code(result: AuditResult) -> int:
operational_failures = {"config.invalid-json", "output.write-failed"}
if any(item.rule_id in operational_failures for item in result.findings):
return 2
if result.mode == "report":
return 0
if any(item.category == "unresolved" for item in result.findings):
return 2
if any(item.category == "violation" and item.severity == "error" for item in result.findings):
return 1
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("project_root", type=Path)
parser.add_argument("--mode", choices=("report", "strict"), required=True)
parser.add_argument("--config", type=Path)
parser.add_argument("--output", type=Path)
parser.add_argument("--no-write", action="store_true")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
result = audit_project(args.project_root, args.mode, args.config)
rendered = json.dumps(result.to_dict(), ensure_ascii=False, indent=2, sort_keys=True) + "\n"
if not args.no_write:
output = args.output or args.project_root / "premium-audit.json"
try:
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(rendered, encoding="utf-8")
except OSError as error:
result = AuditResult(result.mode, result.project_root, (*result.findings, finding(
"output.write-failed",
f"Audit report artifact cannot be written: {error}",
"Choose a writable --output file or pass --no-write for stdout-only inspection.",
file=str(output),
category="unresolved",
)))
rendered = json.dumps(result.to_dict(), ensure_ascii=False, indent=2, sort_keys=True) + "\n"
sys.stdout.write(rendered)
return exit_code(result)
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 67fd35597c85a2f37dd3c566f0bd79768cbe059114edcf28074fc5afa3e0ce68