#!/usr/bin/env python3 """Audita o portal estático e as fronteiras da identidade de família.""" from __future__ import annotations import hashlib import json import re import shutil import subprocess import sys from collections import Counter from dataclasses import dataclass, field from html.parser import HTMLParser from pathlib import Path from urllib.parse import unquote, urlsplit ROOT = Path(__file__).resolve().parents[1] ROOT_PAGES = sorted(ROOT.glob("*.html")) CSS_FILES = sorted(path for path in ROOT.rglob("*.css") if not {"fixtures", "references", "rollback"}.intersection(path.parts)) JS_FILES = sorted(path for path in ROOT.rglob("*.js") if "rollback" not in path.parts) TOKEN_FILES = { ROOT / "assets/css/balu.css", ROOT / "assets/css/patterns.css", } COLOR_PATTERN = re.compile(r"(? None: super().__init__(convert_charrefs=True) self.comments: list[tuple[int, str]] = [] self.declarations: list[str] = [] self.elements: list[Element] = [] def handle_decl(self, decl: str) -> None: self.declarations.append(decl) def handle_comment(self, data: str) -> None: self.comments.append((self.getpos()[0], data)) def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: normalized = {name: value or "" for name, value in attrs} self.elements.append(Element(tag=tag.lower(), attrs=normalized, line=self.getpos()[0])) def handle_startendtag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: self.handle_starttag(tag, attrs) @dataclass class Audit: errors: list[str] = field(default_factory=list) warnings: list[str] = field(default_factory=list) checks: Counter[str] = field(default_factory=Counter) def error(self, message: str) -> None: self.errors.append(message) def warning(self, message: str) -> None: self.warnings.append(message) def pass_check(self, name: str, amount: int = 1) -> None: self.checks[name] += amount def relative(path: Path) -> str: return path.relative_to(ROOT).as_posix() def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as source: for chunk in iter(lambda: source.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def parse_html(path: Path) -> PortalHTMLParser: parser = PortalHTMLParser() parser.feed(path.read_text(encoding="utf-8")) return parser def local_target(current: Path, raw_url: str) -> tuple[Path | None, str]: parsed = urlsplit(raw_url) if parsed.scheme or parsed.netloc or raw_url.startswith(("mailto:", "tel:", "javascript:")): return None, "" target_path = unquote(parsed.path) fragment = unquote(parsed.fragment) if not target_path: return current, fragment return (current.parent / target_path).resolve(), fragment def ids_for_file(path: Path, cache: dict[Path, set[str]]) -> set[str]: if path in cache: return cache[path] if path.suffix.lower() != ".html" or not path.exists(): cache[path] = set() return cache[path] parser = parse_html(path) cache[path] = {element.attrs["id"] for element in parser.elements if element.attrs.get("id")} return cache[path] def audit_html(audit: Audit) -> None: ids_cache: dict[Path, set[str]] = {} required_styles = { "assets/icons/phosphor/phosphor.css", "assets/css/balu.css", "assets/css/components.css", "assets/css/portal.css", } if len(ROOT_PAGES) != 8: audit.error(f"Quantidade de páginas HTML inesperada: {len(ROOT_PAGES)}") for path in ROOT_PAGES: parser = parse_html(path) label = relative(path) source = path.read_text(encoding="utf-8") source_lower = source.lower() elements = parser.elements ids = [element.attrs["id"] for element in elements if element.attrs.get("id")] duplicate_ids = [item for item, count in Counter(ids).items() if count > 1] if not any(decl.lower() == "doctype html" for decl in parser.declarations): audit.error(f"{label}: doctype HTML ausente") if duplicate_ids: audit.error(f"{label}: IDs duplicados: {', '.join(duplicate_ids)}") if parser.comments: audit.error(f"{label}: comentários HTML não são permitidos no código final") html_elements = [element for element in elements if element.tag == "html"] if not html_elements or html_elements[0].attrs.get("lang") != "pt-BR": audit.error(f"{label}: lang=pt-BR ausente") expected_page = path.stem if not html_elements or html_elements[0].attrs.get("data-page") != expected_page: audit.error(f"{label}: data-page deve ser {expected_page}") if not html_elements or html_elements[0].attrs.get("data-version") != "1.5.0": audit.error(f"{label}: data-version deve ser 1.5.0") if sum(element.tag == "main" for element in elements) != 1: audit.error(f"{label}: deve existir exatamente um main") if sum(element.tag == "h1" for element in elements) != 1: audit.error(f"{label}: deve existir exatamente um h1") if not any(element.tag == "a" and "skip-link" in element.attrs.get("class", "").split() for element in elements): audit.error(f"{label}: skip link ausente") if not any(element.attrs.get("id") == "main-content" for element in elements): audit.error(f"{label}: #main-content ausente") if not any(element.tag == "header" and "site-header" in element.attrs.get("class", "").split() for element in elements): audit.error(f"{label}: header compartilhado ausente") if not any(element.tag == "aside" and element.attrs.get("id") == "sidebar" for element in elements): audit.error(f"{label}: sidebar compartilhada ausente") if not any(element.tag == "footer" and "site-footer" in element.attrs.get("class", "").split() for element in elements): audit.error(f"{label}: footer compartilhado ausente") if "identidade de família" not in source_lower: audit.error(f"{label}: contrato de identidade de família não está visível") for term, reason in FORBIDDEN_PUBLIC_TERMS.items(): if term == "clickagents" and label == "adoption.html": continue pattern = rf"\b{re.escape(term)}\b" if " " not in term else re.escape(term) if re.search(pattern, source_lower): audit.error(f"{label}: {reason}: {term}") for element in elements: attrs = element.attrs if "style" in attrs: audit.error(f"{label}:{element.line}: style inline estático") for name in attrs: if INLINE_EVENT_PATTERN.match(name): audit.error(f"{label}:{element.line}: evento inline {name}") if element.tag == "button" and not attrs.get("type"): audit.error(f"{label}:{element.line}: button sem type") if element.tag == "img" and "alt" not in attrs: audit.error(f"{label}:{element.line}: img sem alt") if element.tag == "script" and not attrs.get("src"): audit.error(f"{label}:{element.line}: script inline") styles = { element.attrs.get("href", "") for element in elements if element.tag == "link" and "stylesheet" in element.attrs.get("rel", "").split() } if not required_styles.issubset(styles): audit.error(f"{label}: stylesheets compartilhados ausentes: {', '.join(sorted(required_styles - styles))}") scripts = {element.attrs.get("src", "") for element in elements if element.tag == "script"} if "assets/js/theme-init.js" not in scripts or "assets/js/components.js" not in scripts or "assets/js/portal.js" not in scripts: audit.error(f"{label}: scripts compartilhados ausentes") for element in elements: for attr_name in ("href", "src"): raw_url = element.attrs.get(attr_name) if not raw_url or raw_url.startswith("#") and attr_name == "src": continue target, fragment = local_target(path, raw_url) if target is None: continue if not target.exists(): audit.error(f"{label}:{element.line}: destino local inexistente: {raw_url}") continue if fragment and target.suffix.lower() == ".html" and fragment not in ids_for_file(target, ids_cache): audit.error(f"{label}:{element.line}: fragment inexistente: {raw_url}") audit.pass_check("páginas HTML") def audit_css(audit: Audit) -> None: definitions: set[str] = set() usages: set[str] = set() for path in CSS_FILES: text = path.read_text(encoding="utf-8") label = relative(path) definitions.update(VARIABLE_DEFINITION_PATTERN.findall(text)) usages.update(VARIABLE_USAGE_PATTERN.findall(text)) comments = re.findall(r"/\*.*?\*/", text, flags=re.DOTALL) if any("\n" in comment for comment in comments): audit.error(f"{label}: comentário CSS com múltiplas linhas") for line_number, line in enumerate(text.splitlines(), start=1): if not COLOR_PATTERN.search(line): continue if path not in TOKEN_FILES: audit.error(f"{label}:{line_number}: cor direta fora de arquivo de tokens") elif not CUSTOM_PROPERTY_PATTERN.search(line): audit.error(f"{label}:{line_number}: cor direta fora de declaração de token") if re.search(r"\[\s*data-product(?:\s|=|\])", text, flags=re.IGNORECASE): audit.error(f"{label}: seletor visual por produto é proibido") for token in FORBIDDEN_FAMILY_TOKENS: if token in text: audit.error(f"{label}: token de identidade local proibido: {token}") brace_balance = 0 for char in re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL): if char == "{": brace_balance += 1 elif char == "}": brace_balance -= 1 if brace_balance < 0: break if brace_balance != 0: audit.error(f"{label}: chaves CSS desbalanceadas") try: import tinycss2 # type: ignore except ImportError: tinycss2 = None if tinycss2 is not None: parsed = tinycss2.parse_stylesheet(text, skip_comments=False, skip_whitespace=False) for item in parsed: if item.type == "error": audit.error(f"{label}:{item.source_line}: erro CSS: {item.message}") audit.pass_check("arquivos CSS") undefined = sorted(usages - definitions) if undefined: audit.error("Variáveis CSS usadas sem definição: " + ", ".join(undefined)) core_text = "\n".join((ROOT / "assets/css" / name).read_text(encoding="utf-8") for name in ("balu.css", "components.css")) leaked = sorted(token for token in FORBIDDEN_CORE_TOKENS if token in core_text) if leaked: audit.error("Tokens específicos de aplicação vazaram para o core: " + ", ".join(leaked)) def audit_javascript(audit: Audit) -> None: node = shutil.which("node") for path in JS_FILES: text = path.read_text(encoding="utf-8") label = relative(path) without_header = re.sub(r"\A/\*\*.*?\*/\s*", "", text, count=1, flags=re.DOTALL) block_comments = re.findall(r"/\*.*?\*/", without_header, flags=re.DOTALL) if block_comments: audit.error(f"{label}: comentários de bloco além do cabeçalho JSDoc não são permitidos") if node: result = subprocess.run([node, "--check", str(path)], capture_output=True, text=True) if result.returncode != 0: audit.error(f"{label}: sintaxe JavaScript inválida: {result.stderr.strip()}") else: audit.warning("Node não encontrado; sintaxe JavaScript não foi verificada") audit.pass_check("arquivos JavaScript") def audit_documentation(audit: Audit) -> None: required = [ ROOT / "RULES.md", ROOT / "README.md", ROOT / "MIGRATION-v1.5.md", ROOT / "AUDITORIA-CONFORMIDADE-v1.5.md", ROOT / "RODADA-2-INSPIRACIONAL.md", ROOT / "inspiracoes/README.md", ROOT / "inspiracoes/catalogo.md", ROOT / "inspiracoes/template-referencia.md", ROOT / "products/README.md", ROOT / "products/_template/README.md", ROOT / "products/_template/adoption-checklist.md", ROOT / "products/_template/legacy-map.md", ROOT / "products/_template/exceptions.md", ROOT / "products/clickagents/README.md", ROOT / "products/clickagents/adoption-checklist.md", ROOT / "products/clickagents/legacy-map.md", ROOT / "products/clickagents/exceptions.md", ROOT / "governance/FAMILY-CONTRACT.md", ROOT / "governance/PRODUCT-ADOPTION.md", ROOT / "governance/TYPOGRAPHY.md", ROOT / "governance/CONTENT-DESIGN-AND-DENSITY.md", ROOT / "reports/content-density-d0-decisions-v1.5.md", ROOT / "reports/content-density-pilot-prism-red-team-v1.5.md", ROOT / "reports/openai-typography-reference-v1.5.md", ROOT / "reports/clickagents-typography-reference-v1.5.md", ROOT / "reports/typography-proportional-weight-prism-red-team-v1.5.md", ROOT / "reports/sidebar-title-gap-collapsed-icons-prism-red-team-v1.5.md", ROOT / "governance/ICONOGRAPHY.md", ROOT / "governance/ICON-BUTTON.md", ROOT / "governance/SHAPE.md", ROOT / "governance/ELEVATION.md", ROOT / "governance/SPACING-AND-PROXIMITY.md", ROOT / "governance/REPRESENTATION-AND-LIVE.md", ROOT / "governance/RULES-APPLICABILITY.md", ROOT / "governance/references/guia-alinhamento-vertical.pdf", ] for path in required: if not path.exists(): audit.error(f"Documento obrigatório ausente: {relative(path)}") product_css = list((ROOT / "products").rglob("*.css")) if product_css: audit.error("Perfis de adoção não podem conter CSS: " + ", ".join(relative(path) for path in product_css)) adoption_contract = (ROOT / "governance/PRODUCT-ADOPTION.md").read_text(encoding="utf-8") adoption_page = (ROOT / "adoption.html").read_text(encoding="utf-8") adoption_checklist = (ROOT / "products/_template/adoption-checklist.md").read_text(encoding="utf-8") legacy_map = (ROOT / "products/_template/legacy-map.md").read_text(encoding="utf-8") clickagents_profile = (ROOT / "products/clickagents/legacy-map.md").read_text(encoding="utf-8") adoption_fragments = ( (adoption_contract, "Adotar"), (adoption_contract, "Refinar"), (adoption_contract, "Promover extensão"), (adoption_contract, "Rejeitar"), (adoption_contract, "Foundations"), (adoption_contract, "Shell e navegação"), (adoption_contract, "Componentes e estados"), (adoption_contract, "Padrões e domínio"), (adoption_contract, "Qualidade e retirada"), (adoption_contract, "displays 28–64px estabilizam em peso 500"), (adoption_contract, "24/500 legado migra obrigatoriamente"), (adoption_page, 'id="ondas"'), (adoption_page, "segundo produto"), (adoption_checklist, "Ledger"), (adoption_checklist, "Displays 28–64px usam peso 500"), (legacy_map, "| Trilha | Origem legada |"), (clickagents_profile, "título–item 10px"), (clickagents_profile, "ícones primários persistem"), (clickagents_profile, "--nav-section-title"), (clickagents_profile, "Picker 378/42/320px"), ) for source, fragment in adoption_fragments: if fragment not in source: audit.error(f"Contrato de adoção incompleto: {fragment}") audit.pass_check("documentação obrigatória", len(required)) def audit_shape_contract(audit: Audit) -> None: balu_css = (ROOT / "assets/css/balu.css").read_text(encoding="utf-8") components_css = (ROOT / "assets/css/components.css").read_text(encoding="utf-8") patterns_css = (ROOT / "assets/css/patterns.css").read_text(encoding="utf-8") portal_css = (ROOT / "assets/css/portal.css").read_text(encoding="utf-8") required_tokens = ( "--b-corner-rounded", "--b-corner-squircle", "--b-shape-control-radius", "--b-shape-control-compact-radius", "--b-shape-control-large-radius", "--b-shape-navigation-radius", "--b-shape-menu-item-radius", "--b-shape-interactive-radius", "--b-shape-composer-radius", ) for token in required_tokens: if not re.search(rf"{re.escape(token)}\s*:", balu_css): audit.error(f"Token semântico de squircle ausente: {token}") if not re.search(r"--b-shape-navigation-radius\s*:\s*var\(--b-radius-2xl\)", balu_css): audit.error("Curvatura de navegação deve reutilizar --b-radius-2xl (16px)") if not re.search(r"--b-shape-menu-item-radius\s*:\s*var\(--b-radius-3xl\)", balu_css): audit.error("Curvatura do item de Menu deve reutilizar --b-radius-3xl (24px)") primitive_scale = { "--b-radius-xs": "4px", "--b-radius-md": "8px", "--b-radius-xl": "12px", "--b-radius-2xl": "16px", "--b-radius-3xl": "24px", "--b-radius-pill": "999px", "--b-radius-circle": "50%", } for token, expected in primitive_scale.items(): if not re.search(rf"{re.escape(token)}\s*:\s*{re.escape(expected)}", balu_css): audit.error(f"Escala curta de shape diverge: {token}") for forbidden in ("--b-radius-sm", "--b-radius-lg"): if re.search(rf"{re.escape(forbidden)}\s*:", balu_css): audit.error(f"Token removido permanece na escala de shape: {forbidden}") contracts = ( (components_css, ".b-btn"), (components_css, ".b-icon-btn"), (components_css, ".b-nav__item"), (components_css, ".b-nav-section__trigger"), (components_css, ".b-menu__item"), (components_css, ".b-card--interactive"), (components_css, ".b-account"), (components_css, ".b-entity-item"), (components_css, ".b-radio-row"), (components_css, ".b-picker__cell"), (components_css, ".b-picker__upload"), (components_css, ".b-pagination button"), (components_css, ".b-toast__action"), (patterns_css, ".b-composer"), (patterns_css, ".b-file-tile"), (patterns_css, ".b-file-card"), (patterns_css, ".b-link-item"), (portal_css, ".sidebar-link"), (portal_css, ".button"), (portal_css, ".theme-switch__button"), (portal_css, ".skip-link"), (portal_css, ".finder__clear"), (portal_css, ".destination-card"), (portal_css, ".copy-button"), (portal_css, ".p-code__copy"), (portal_css, ".p-swatch"), (portal_css, ".file-swatch"), (portal_css, ".component-overview-card"), ) for stylesheet, selector in contracts: match = re.search(rf"{re.escape(selector)}\s*\{{([^}}]+)\}}", stylesheet) if not match: audit.error(f"Contrato squircle ausente: {selector}") continue block = match.group(1) if "corner-shape: var(--b-corner-squircle)" not in block: audit.error(f"Curva squircle ausente: {selector}") if not re.search(r"border-radius:\s*var\(--b-shape-[a-z-]+-radius\)", block): audit.error(f"Fallback semântico de radius ausente: {selector}") public_html = "\n".join(path.read_text(encoding="utf-8") for path in ROOT_PAGES) if "b-btn--pill" in public_html: audit.error("Button pill permanece em specimen; pills são reservadas a chips, tags e filtros") if "Quatro categorias oficiais" not in (ROOT / "foundations.html").read_text(encoding="utf-8"): audit.error("Foundations não documenta as quatro categorias de shape") browser_report_path = ROOT / "reports/squircle-audit-v1.5.json" if not browser_report_path.exists(): audit.error("Auditoria computada de squircle ausente") else: browser_report = json.loads(browser_report_path.read_text(encoding="utf-8")) if not browser_report.get("cssSupportsSquircle"): audit.error("Navegador de referência não confirmou suporte a squircle") if browser_report.get("contractFailures"): audit.error("Auditoria computada encontrou contratos squircle sem efeito") if browser_report.get("protectedFailures"): audit.error("Exclusão canônica recebeu squircle indevido") if browser_report.get("categoryFailures"): audit.error("Invariante de pill, circle ou rounded falhou") if browser_report.get("interactiveUnclassified"): audit.error("Há superfícies interativas sem classificação de shape") if browser_report.get("selectorContractCount") != len(contracts): audit.error("Contagem de contratos diverge entre auditoria estática e navegador") if browser_report.get("fallbackSimulation", {}).get("failures"): audit.error("Fallback simulado encontrou radius computado inválido") if browser_report.get("forcedColors", {}).get("failures"): audit.error("Auditoria de forced colors encontrou falhas de shape") visual_report_path = ROOT / "reports/squircle-visual-evidence-v1.5.json" if not visual_report_path.exists(): audit.error("Evidência visual before/after de squircle ausente") else: visual_report = json.loads(visual_report_path.read_text(encoding="utf-8")) captures = visual_report.get("captures", []) if len(captures) != 12: audit.error("Evidência visual deve conter 12 capturas") if visual_report.get("validationFailures"): audit.error("Validação automática detectou captura visual inválida") for capture in captures: if not (ROOT / capture.get("path", "")).exists(): audit.error(f"Captura visual ausente: {capture.get('path', 'sem caminho')}") audit.pass_check("contratos squircle", len(contracts)) def audit_edge_alignment(audit: Audit) -> None: """Block regressions in perimeter inset parity.""" components_css = (ROOT / "assets/css/components.css").read_text(encoding="utf-8") patterns_css = (ROOT / "assets/css/patterns.css").read_text(encoding="utf-8") portal_css = (ROOT / "assets/css/portal.css").read_text(encoding="utf-8") contracts = ( (patterns_css, r"\.b-composer\s*\{[^}]*padding:\s*var\(--b-space-7\)"), (patterns_css, r"\.b-workspace-toolbar\s*\{[^}]*padding-inline:\s*calc\(var\(--b-space-4\) - 1px\)"), (patterns_css, r"\.b-file-card__tile\s*\{[^}]*inline-size:\s*36px;[^}]*block-size:\s*36px"), (portal_css, r"\.p-doc-u-008\s*\{[^}]*padding-block:\s*var\(--b-nav-frame-padding-start\)"), (components_css, r"\.b-dialog__header\s*\{[^}]*padding:\s*var\(--b-space-9\) var\(--b-space-9\) 0"), (components_css, r"\.b-dialog__body\s*\{[^}]*padding:\s*var\(--b-space-6\) var\(--b-space-9\)"), (components_css, r"\.b-dialog__footer\s*\{[^}]*padding:\s*0 var\(--b-space-9\) var\(--b-space-9\)"), (components_css, r"\.b-menu__item\s*\{[^}]*line-height:\s*var\(--b-icon-default\)"), (components_css, r"\.b-picker__upload\s*\{[^}]*inline-size:\s*100%"), (components_css, r"\.b-media-card__body\s*\{[^}]*display:\s*block"), ) for stylesheet, pattern in contracts: if not re.search(pattern, stylesheet, flags=re.DOTALL): audit.error(f"Contrato de alinhamento ao perímetro ausente: {pattern}") report_path = ROOT / "reports/edge-alignment-audit-v1.5.json" if not report_path.exists(): audit.error("Auditoria computada de alinhamento ao perímetro ausente") else: report = json.loads(report_path.read_text(encoding="utf-8")) if report.get("contracts") != 10 or report.get("total") != 100 or report.get("failed"): audit.error("Auditoria de alinhamento ao perímetro não aprovou 10 contratos em 100 combinações") if report.get("stateMeasurements") != 580: audit.error("Auditoria de alinhamento deve exercer 580 medições de estado") if report.get("toleranceCssPx") != 0.1: audit.error("Tolerância técnica de alinhamento ao perímetro deve permanecer em 0.1px CSS") visual_path = ROOT / "reports/edge-alignment-visual-v1.5.json" fixture_path = ROOT / "fixtures/edge-alignment-before-v1.5.css" if not visual_path.exists() or not fixture_path.exists(): audit.error("Evidência visual ou fixture before de alinhamento ausente") else: visual = json.loads(visual_path.read_text(encoding="utf-8")) if len(visual.get("captures", [])) != 16 or visual.get("validationFailures"): audit.error("Evidência visual de alinhamento deve conter 16 capturas válidas") for capture in visual.get("captures", []): capture_path = ROOT / capture.get("path", "") if not capture_path.exists() or capture.get("sha256") != sha256(capture_path): audit.error(f"Captura before/after ausente ou alterada: {capture.get('path', '')}") if abs(visual.get("comparisons", {}).get("dialog", {}).get("heightDeltaCssPx", 1)) > .1: audit.error("Correção alterou indevidamente a altura do dialog") if visual.get("beforeFixtureSha256") != sha256(fixture_path): audit.error("Fixture before diverge do hash usado pela evidência visual") manifest_path = ROOT / "reports/edge-alignment-reference-v1.5.json" if not manifest_path.exists(): audit.error("Manifesto das dez referências de alinhamento ausente") else: manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if manifest.get("pairCount") != 5 or manifest.get("imageCount") != 10: audit.error("Manifesto de alinhamento deve mapear cinco pares e dez imagens") for pair in manifest.get("pairs", []): for image in pair.get("images", []): image_path = ROOT / image.get("path", "") if not image_path.exists() or image.get("sha256") != sha256(image_path): audit.error(f"Referência de alinhamento ausente ou alterada: {image.get('path', '')}") cross_browser_path = ROOT / "reports/circle-menu-cross-browser-v1.5.json" if not cross_browser_path.exists(): audit.error("Matriz cross-browser de Circle/menu ausente") else: cross_browser = json.loads(cross_browser_path.read_text(encoding="utf-8")) required_cross_sources = { "components.html", "patterns.html", "assets/css/balu.css", "assets/css/components.css", "assets/css/patterns.css", "assets/css/portal.css", "assets/icons/phosphor/phosphor.css", "assets/icons/phosphor/Phosphor.woff2", "assets/icons/phosphor/Phosphor-Bold.woff2", "assets/icons/phosphor/Phosphor-Fill.woff2", "governance/ICON-BUTTON.md", "scripts/audit_circle_menu_cross_browser.py", } cross_sources = cross_browser.get("sourceHashes", {}) if cross_browser.get("engines") != ["chromium", "firefox", "webkit"] or cross_browser.get("profiles") != 4 or cross_browser.get("failures") or not isinstance(cross_sources, dict) or set(cross_sources) != required_cross_sources: audit.error("Circle/menu não foi aprovado com proveniência completa nos três motores e page scale 125%") if isinstance(cross_sources, dict): for relative_path, expected_hash in cross_sources.items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Matriz cross-browser obsoleta: {relative_path}") for result in cross_browser.get("results", []): for screenshot in result.get("screenshots", {}).values(): screenshot_path = ROOT / screenshot.get("path", "") if not screenshot_path.exists() or screenshot.get("sha256") != sha256(screenshot_path): audit.error(f"Evidência cross-browser ausente ou alterada: {screenshot.get('path', '')}") if "Alinhamento ao perímetro" not in (ROOT / "foundations.html").read_text(encoding="utf-8"): audit.error("Foundations não documenta alinhamento ao perímetro") audit.pass_check("contratos de alinhamento ao perímetro", 10) def audit_icon_button_contract(audit: Audit) -> None: """Enforce the governed Icon Button taxonomy and component boundary.""" components_css = (ROOT / "assets/css/components.css").read_text(encoding="utf-8") public_html = "\n".join(path.read_text(encoding="utf-8") for path in ROOT_PAGES) required_css = ( ".b-icon-btn--compact", ".b-icon-btn--touch", ".b-icon-btn--circle", ".b-icon-btn-context--radial", ".b-icon-btn--subtle", ".b-icon-btn--outline", ".b-icon-btn--filled", ".b-icon-btn--danger", ) for selector in required_css: if selector not in components_css: audit.error(f"Contrato de Icon Button ausente: {selector}") class_tokens = {token for value in re.findall(r'class="([^"]*)"', public_html) for token in value.split()} for legacy in ("icon-button", "b-icon-btn--sm"): if legacy in class_tokens: audit.error(f"Classe legada de Icon Button permanece no HTML: {legacy}") if 'data-shape-role="radial"' in public_html: audit.error("Exceção radial não possui implementação pública na v1.5") if ".icon-button" in (ROOT / "assets/css/portal.css").read_text(encoding="utf-8"): audit.error("Família paralela .icon-button permanece no CSS do portal") report_path = ROOT / "reports/icon-button-audit-v1.5.json" if not report_path.exists(): audit.error("Auditoria computada de Icon Button ausente") else: report = json.loads(report_path.read_text(encoding="utf-8")) if report.get("contractFailures") or report.get("stateFailures"): audit.error("Auditoria computada encontrou falha de Icon Button") if report.get("legacyOccurrences") or report.get("contextualMissingBase"): audit.error("Auditoria encontrou classe legada ou hook contextual sem base") if report.get("unclassifiedIconOnly"): audit.error("Há comando icon-only fora do core ou de compound control reconhecido") if report.get("negativeFixtureFailures") or report.get("negativeFixtureCoverage") != {"iconOnly": 4, "circleRejected": 8, "circleAccepted": 2}: audit.error("Fixtures negativas não detectam escapes icon-only ou Circle fora de contexto") if report.get("circleCount", 0) <= 0: audit.error("Variante Circle de Icon Button não possui cobertura computada") if report.get("radialCount") != 0: audit.error("Atributo radial apareceu como autoautorização") required_sources = {relative(path) for path in ROOT_PAGES} | { "assets/css/balu.css", "assets/css/components.css", "assets/css/patterns.css", "assets/css/portal.css", "assets/js/portal.js", "assets/icons/phosphor/phosphor.css", "assets/icons/phosphor/Phosphor.woff2", "assets/icons/phosphor/Phosphor-Bold.woff2", "assets/icons/phosphor/Phosphor-Fill.woff2", "governance/ICON-BUTTON.md", "scripts/audit_icon_button_contract.py", } if report.get("pages") != len(ROOT_PAGES) or len(report.get("profiles", {})) != 6 or report.get("uniqueElements", 0) <= 0 or not required_sources.issubset(report.get("sourceHashes", {})): audit.error("Cobertura de Icon Button não identifica páginas, elementos únicos e proveniência completa") for relative_path, expected_hash in report.get("sourceHashes", {}).items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Relatório de Icon Button obsoleto: {relative_path}") visual_path = ROOT / "reports/icon-button-visual-v1.5.json" if not visual_path.exists(): audit.error("Evidência visual de Icon Button ausente") else: visual = json.loads(visual_path.read_text(encoding="utf-8")) required_visual_sources = { "components.html", "patterns.html", "assets/css/balu.css", "assets/css/components.css", "assets/css/patterns.css", "assets/css/portal.css", "assets/icons/phosphor/phosphor.css", "assets/icons/phosphor/Phosphor.woff2", "assets/icons/phosphor/Phosphor-Bold.woff2", "assets/icons/phosphor/Phosphor-Fill.woff2", "governance/ICON-BUTTON.md", "scripts/capture_icon_button_evidence.py", } visual_sources = visual.get("sourceHashes", {}) if not isinstance(visual_sources, dict): audit.error("sourceHashes da evidência visual deve ser um objeto") visual_sources = {} if len(visual.get("captures", [])) != 11 or visual.get("validationFailures") or set(visual_sources) != required_visual_sources: audit.error("Evidência visual de Icon Button deve conter onze capturas e os doze caminhos de origem exatos") for relative_path, expected_hash in visual_sources.items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Evidência visual de Icon Button obsoleta: {relative_path}") for capture in visual.get("captures", []): path = ROOT / capture.get("path", "") if not path.exists() or capture.get("sha256") != sha256(path): audit.error(f"Captura de Icon Button ausente ou alterada: {capture.get('path', '')}") audit.pass_check("eixos de Icon Button", 4) def audit_representation_contract(audit: Audit) -> None: """Enforce Scrollbar, Emoji, Picker, Tabs, Media Card, and Live contracts.""" balu_css = (ROOT / "assets/css/balu.css").read_text(encoding="utf-8") components_css = (ROOT / "assets/css/components.css").read_text(encoding="utf-8") components_js = (ROOT / "assets/js/components.js").read_text(encoding="utf-8") required_fragments = ( (balu_css, "--scrollbar-thumb: #dfdfdf"), (balu_css, "--scrollbar-thumb-hover: #363b42"), (balu_css, ".b-emoji"), (components_css, ".b-picker__panel"), (components_css, ".b-picker__indicator"), (components_css, ".b-tab__inner"), (components_css, ".b-tabs__indicator"), (components_css, ".b-media-card__body"), (components_css, "@keyframes b-live-ring"), (components_js, ".b-picker__cats"), (components_js, "setupRadioGroups"), ) for source, fragment in required_fragments: if fragment not in source: audit.error(f"Contrato de representação ausente: {fragment}") report_path = ROOT / "reports/representation-contract-audit-v1.5.json" if not report_path.exists(): audit.error("Auditoria de representação ausente") else: report = json.loads(report_path.read_text(encoding="utf-8")) required_sources = { "components.html", "foundations.html", "assets/css/balu.css", "assets/css/components.css", "assets/css/portal.css", "assets/js/components.js", "governance/REPRESENTATION-AND-LIVE.md", "governance/SCROLLBAR.md", "fixtures/consumer-representation-v1.5.html", "scripts/audit_representation_contract.py", } source_hashes = report.get("sourceHashes", {}) scrollbar_source = report.get("scrollbarSourceContract", {}) if report.get("profiles") != 9 or set(report.get("engines", {})) != {"chromium", "firefox", "webkit"} or report.get("failures") or not scrollbar_source or not all(scrollbar_source.values()) or report.get("unwrappedEmojiFailures") or report.get("negativeEmojiFixtureFailures") or report.get("negativeEmojiFixtureCoverage") != {"rawFlag": 1, "rawKeycap": 1, "governedFlag": 1} or not isinstance(source_hashes, dict) or set(source_hashes) != required_sources: audit.error("Scrollbar, Emoji, Picker, Tabs, Media Card ou Live não passou pela matriz normativa") if isinstance(source_hashes, dict): for relative_path, expected_hash in source_hashes.items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Auditoria de representação obsoleta: {relative_path}") visual_path = ROOT / "reports/representation-visual-v1.5.json" fixture_path = ROOT / "fixtures/representation-before-v1.5.css" if not visual_path.exists() or not fixture_path.exists(): audit.error("Evidência visual ou fixture de representação ausente") else: visual = json.loads(visual_path.read_text(encoding="utf-8")) expected_visual_sources = { "components.html", "foundations.html", "patterns.html", "adoption.html", "assets/css/balu.css", "assets/css/components.css", "assets/css/patterns.css", "assets/css/portal.css", "assets/js/components.js", "assets/js/portal.js", "governance/REPRESENTATION-AND-LIVE.md", "governance/ELEVATION.md", "governance/ICONOGRAPHY.md", "governance/SPACING-AND-PROXIMITY.md", "scripts/capture_representation_evidence.py", } if len(visual.get("captures", [])) != 30 or visual.get("validationFailures") or set(visual.get("sourceHashes", {})) != expected_visual_sources or visual.get("beforeFixtureSha256") != sha256(fixture_path): audit.error("Evidência visual de representação está incompleta ou obsoleta") for relative_path, expected_hash in visual.get("sourceHashes", {}).items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Evidência visual de representação obsoleta: {relative_path}") for capture in visual.get("captures", []): capture_path = ROOT / capture.get("path", "") if not capture_path.exists() or capture.get("sha256") != sha256(capture_path): audit.error(f"Captura de representação ausente ou alterada: {capture.get('path', '')}") elevation_path = ROOT / "reports/elevation-file-audit-v1.5.json" if not elevation_path.exists(): audit.error("Auditoria de elevação, arquivos, marca e swatches ausente") else: elevation = json.loads(elevation_path.read_text(encoding="utf-8")) expected_elevation_sources = { "assets/css/balu.css", "assets/css/components.css", "assets/css/patterns.css", "assets/css/portal.css", "assets/icons/phosphor/phosphor.css", "components.html", "foundations.html", "patterns.html", "governance/ELEVATION.md", "governance/ICONOGRAPHY.md", "MIGRATION-v1.5.md", "scripts/audit_elevation_file_contract.py", } if not elevation.get("passed") or len(elevation.get("profiles", [])) != 4 or set(elevation.get("sourceSha256", {})) != expected_elevation_sources: audit.error("Elevação, arquivos, marca ou swatches não passou pela matriz normativa") for relative_path, expected_hash in elevation.get("sourceSha256", {}).items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Auditoria de elevação/arquivo obsoleta: {relative_path}") reference_path = ROOT / "reports/clickagents-representation-reference-v1.5.json" if not reference_path.exists(): audit.error("Manifesto da referência de representação ausente") else: reference = json.loads(reference_path.read_text(encoding="utf-8")) if not reference.get("contracts", {}).get("picker", {}).get("consensus") or reference.get("contracts", {}).get("picker", {}).get("sourceCount") != 3: audit.error("Referência do Picker não possui consenso reproduzível") if not (ROOT / "governance/REPRESENTATION-AND-LIVE.md").exists(): audit.error("Contrato normativo de representação e Live ausente") audit.pass_check("representação, Publicado e Live", 7) def audit_spacing_control_contract(audit: Audit) -> None: """Validate the compact shape, proximity, controls, and file-output matrix.""" report_path = ROOT / "reports/spacing-control-audit-v1.5.json" expected_sources = { "assets/css/balu.css", "assets/css/components.css", "assets/css/portal.css", "assets/js/components.js", "assets/js/portal.js", "assets/icons/file-formats.json", "components.html", "foundations.html", "patterns.html", "adoption.html", "fixtures/consumer-representation-v1.5.html", "governance/SHAPE.md", "governance/SPACING-AND-PROXIMITY.md", "governance/REPRESENTATION-AND-LIVE.md", "reports/clickagents-file-formats-v1.5.json", "scripts/audit_spacing_control_contract.py", } if not report_path.exists(): audit.error("Auditoria de spacing, controles e saídas ausente") return report = json.loads(report_path.read_text(encoding="utf-8")) if not report.get("passed") or not report.get("documentationPassed") or not report.get("publicRadioRuntimePassed") or len(report.get("profiles", [])) != 6 or report.get("fileFormats", {}).get("count") != 56 or set(report.get("sourceSha256", {})) != expected_sources: audit.error("Spacing, controles, shape ou saídas não passou pela matriz normativa") for relative_path, expected_hash in report.get("sourceSha256", {}).items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Auditoria de spacing/controles obsoleta: {relative_path}") manifest_path = ROOT / "assets/icons/file-formats.json" reference_path = ROOT / "reports/clickagents-file-formats-v1.5.json" if not manifest_path.exists() or not reference_path.exists(): audit.error("Manifesto ou referência de saídas de arquivo ausente") else: manifest = json.loads(manifest_path.read_text(encoding="utf-8")) reference = json.loads(reference_path.read_text(encoding="utf-8")) if len(manifest.get("formats", {})) != 56 or reference.get("extensionCount") != 56 or reference.get("manifestSha256") != sha256(manifest_path): audit.error("Cobertura das saídas ClickAgents diverge do manifesto") audit.pass_check("spacing, controles e saídas", 56) def audit_proportion_containment_contract(audit: Audit) -> None: """Validate intrinsic sizing, icon slots, containment, migration, and page location.""" report_path = ROOT / "reports/proportion-containment-audit-v1.5.json" expected_sources = { "assets/css/balu.css", "assets/css/components.css", "assets/css/patterns.css", "assets/css/portal.css", "assets/js/portal.js", "assets/tokens/migration-v1.5.json", "index.html", "foundations.html", "components.html", "fixtures/consumer-representation-v1.5.html", "governance/PROPORTION-AND-CONTAINMENT.md", "governance/SHAPE.md", "MIGRATION-v1.5.md", "reports/clickagents-fade-reference-v1.5.json", "scripts/inventory_clickagents_fade.py", "scripts/audit_proportion_containment_contract.py", } if not report_path.exists(): audit.error("Auditoria de proporção e contenção ausente") return report = json.loads(report_path.read_text(encoding="utf-8")) if not report.get("passed") or len(report.get("profiles", [])) != 7 or not report.get("static", {}).get("passed") or set(report.get("sourceSha256", {})) != expected_sources: audit.error("Proporção, slots, contenção, migração ou localização diverge do contrato") for relative_path, expected_hash in report.get("sourceSha256", {}).items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Auditoria de proporção/contenção obsoleta: {relative_path}") visual_path = ROOT / "reports/proportion-containment-visual-v1.5.json" if not visual_path.exists(): audit.error("Evidência visual de proporção e contenção ausente") else: visual = json.loads(visual_path.read_text(encoding="utf-8")) captures = visual.get("captures", []) if not visual.get("passed") or not visual.get("activeLocationPassed") or len(captures) != 13: audit.error("Evidência visual de proporção e contenção diverge") for capture in captures: image_path = ROOT / capture.get("file", "") if not image_path.exists() or sha256(image_path) != capture.get("sha256"): audit.error(f"Captura de proporção/contenção obsoleta: {capture.get('file', '')}") audit.pass_check("proporção, slots e contenção", 6) def audit_workspace_geometry_contract(audit: Audit) -> None: """Validate Canvas widths, Composer/Menu perimeter, and two-axis anatomy.""" report_path = ROOT / "reports/workspace-geometry-audit-v1.5.json" expected_sources = { "assets/css/balu.css", "assets/css/components.css", "assets/css/patterns.css", "assets/css/portal.css", "components.html", "patterns.html", "fixtures/workspace-geometry-v1.5.html", "governance/ALIGNMENT-CHECKLIST.md", "governance/CANVAS-WIDTH.md", "assets/tokens/migration-v1.5.json", "reports/clickagents-geometry-reference-v1.5.json", "scripts/inventory_clickagents_geometry.py", "scripts/audit_workspace_geometry_contract.py", } if not report_path.exists(): audit.error("Auditoria de Canvas, Composer e Menu ausente") return report = json.loads(report_path.read_text(encoding="utf-8")) if not report.get("passed") or not report.get("referencePassed") or not report.get("radiusMigrationPassed") or not report.get("menuNormalizationPassed") or len(report.get("profiles", [])) != 9 or set(report.get("sourceSha256", {})) != expected_sources: audit.error("Canvas, Composer, Menu ou anatomia vertical diverge do contrato") for relative_path, expected_hash in report.get("sourceSha256", {}).items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Auditoria de workspace obsoleta: {relative_path}") reference_path = ROOT / "reports/clickagents-geometry-reference-v1.5.json" if reference_path.exists(): reference = json.loads(reference_path.read_text(encoding="utf-8")) if not reference.get("passed") or len(reference.get("screenshots", [])) != 8 or len(reference.get("files", {})) != 5: audit.error("Referência geométrica ClickAgents incompleta") for relative_path, expected_hash in reference.get("files", {}).items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Subset ClickAgents obsoleto: {relative_path}") for capture in reference.get("screenshots", []): image_path = ROOT / capture.get("file", "") if not image_path.exists() or sha256(image_path) != capture.get("sha256"): audit.error(f"Captura geométrica obsoleta: {capture.get('file', '')}") visual_path = ROOT / "reports/workspace-geometry-visual-v1.5.json" if not visual_path.exists(): audit.error("Evidência visual de workspace ausente") else: visual = json.loads(visual_path.read_text(encoding="utf-8")) captures = visual.get("captures", []) if not visual.get("passed") or len(captures) != 8: audit.error("Evidência visual de workspace diverge") for capture in captures: image_path = ROOT / capture.get("file", "") if not image_path.exists() or sha256(image_path) != capture.get("sha256"): audit.error(f"Captura de workspace obsoleta: {capture.get('file', '')}") audit.pass_check("Canvas, Composer, Menu e eixo vertical", 6) def audit_sidebar_geometry_contract(audit: Audit) -> None: """Validate the ClickAgents-derived sidebar frame and portal dogfooding.""" report_path = ROOT / "reports/sidebar-geometry-audit-v1.5.json" expected_sources = { "assets/css/balu.css", "assets/css/components.css", "assets/css/portal.css", "assets/js/components.js", "assets/js/portal.js", "components.html", "foundations.html", "index.html", "fixtures/sidebar-geometry-v1.5.html", "fixtures/clickagents-sidebar-spacing-reference-v1.5.html", "governance/SIDE-NAVIGATION.md", "governance/PRODUCT-SHELL.md", "governance/ICON-BUTTON.md", "reports/clickagents-sidebar-reference-v1.5.json", "scripts/inventory_clickagents_sidebar.py", "scripts/audit_sidebar_geometry_contract.py", } if not report_path.exists(): audit.error("Auditoria do menu lateral ausente") return report = json.loads(report_path.read_text(encoding="utf-8")) closed_group_profiles = [row for row in report.get("profiles", []) if row.get("closedGroupRail") is not None] if not report.get("passed") or not report.get("referencePassed") or not report.get("allPortalPagesDogfood") or not report.get("foundationIdentityPassed") or len(report.get("profiles", [])) != 23 or len(closed_group_profiles) != 13 or not all(row.get("closedGroupRailPassed") for row in closed_group_profiles) or len(report.get("referenceTitleSpacing", [])) != 3 or not all(row.get("passed") for row in report.get("referenceTitleSpacing", [])) or len(report.get("reducedMotion", [])) != 3 or not all(row.get("passed") for row in report.get("reducedMotion", [])) or len(report.get("closedGroupReducedMotion", [])) != 3 or not all(row.get("passed") for row in report.get("closedGroupReducedMotion", [])) or len(report.get("headerBreakpoint", [])) != 3 or not all(row.get("passed") for row in report.get("headerBreakpoint", [])) or len(report.get("noJavaScriptShell", [])) != 3 or not all(row.get("passed") for row in report.get("noJavaScriptShell", [])) or set(report.get("sourceSha256", {})) != expected_sources: audit.error("Menu lateral diverge do contrato ClickAgents preservado") for relative_path, expected_hash in report.get("sourceSha256", {}).items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Auditoria do menu lateral obsoleta: {relative_path}") reference_path = ROOT / "reports/clickagents-sidebar-reference-v1.5.json" if reference_path.exists(): reference = json.loads(reference_path.read_text(encoding="utf-8")) if not reference.get("passed") or len(reference.get("contracts", {})) != 60 or len(reference.get("files", {})) != 6: audit.error("Referência do menu lateral ClickAgents incompleta") for relative_path, expected_hash in reference.get("files", {}).items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Subset do menu lateral ClickAgents obsoleto: {relative_path}") visual_path = ROOT / "reports/sidebar-geometry-visual-v1.5.json" if not visual_path.exists(): audit.error("Evidência visual do menu lateral ausente") else: visual = json.loads(visual_path.read_text(encoding="utf-8")) captures = visual.get("captures", []) if not visual.get("passed") or len(captures) != 28: audit.error("Evidência visual do menu lateral diverge") for capture in captures: image_path = ROOT / capture.get("file", "") if not image_path.exists() or sha256(image_path) != capture.get("sha256"): audit.error(f"Captura do menu lateral obsoleta: {capture.get('file', '')}") audit.pass_check("menu lateral e frame", 9) def audit_nav_section_contract(audit: Audit) -> None: """Validate collapsible navigation sections and stable typography.""" report_path = ROOT / "reports/nav-section-audit-v1.5.json" expected_sources = { "assets/css/balu.css", "assets/css/components.css", "assets/css/portal.css", "assets/js/components.js", "assets/js/portal.js", "index.html", "foundations.html", "components.html", "patterns.html", "guidelines.html", "adoption.html", "inspiracoes.html", "conformidade.html", "fixtures/nav-section-v1.5.html", "governance/FAMILY-CONTRACT.md", "governance/NAV-SECTION.md", "governance/SIDE-NAVIGATION.md", "reports/clickagents-sidebar-reference-v1.5.json", "scripts/inventory_clickagents_sidebar.py", "scripts/audit_nav_section_contract.py", } if not report_path.exists(): audit.error("Auditoria dos grupos colapsáveis de navegação ausente") return report = json.loads(report_path.read_text(encoding="utf-8")) group_counts = report.get("portalGroupCounts", {}) heading_counts = report.get("portalHeadingCounts", {}) if not report.get("passed") or not report.get("referencePassed") or not report.get("staticPassed") or not report.get("typographyDeclarationsPassed") or len(report.get("profiles", [])) != 9 or len(report.get("noJavaScript", [])) != 3 or sum(group_counts.values()) != 40 or sum(heading_counts.values()) != 40 or set(report.get("sourceSha256", {})) != expected_sources: audit.error("Grupo colapsável, tipografia ou runtime público diverge do contrato") for relative_path, expected_hash in report.get("sourceSha256", {}).items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Auditoria do grupo colapsável obsoleta: {relative_path}") visual_path = ROOT / "reports/nav-section-visual-v1.5.json" if not visual_path.exists(): audit.error("Evidência visual dos grupos colapsáveis ausente") else: visual = json.loads(visual_path.read_text(encoding="utf-8")) captures = visual.get("captures", []) if not visual.get("passed") or len(captures) != 7: audit.error("Evidência visual dos grupos colapsáveis diverge") for capture in captures: image_path = ROOT / capture.get("file", "") if not image_path.exists() or sha256(image_path) != capture.get("sha256"): audit.error(f"Captura de grupo colapsável obsoleta: {capture.get('file', '')}") audit.pass_check("grupos colapsáveis de navegação", 9) def audit_typography_contract(audit: Audit) -> None: """Validate proportional type, references, reflow, scripts, and portal adoption.""" report_path = ROOT / "reports/typography-contract-audit-v1.5.json" visual_path = ROOT / "reports/typography-visual-v1.5.json" expected_sources = { "assets/css/balu.css", "assets/css/components.css", "assets/css/portal.css", "assets/fonts/noto-sans/noto-sans.css", "foundations.html", "fixtures/typography-scale-v1.5.html", "governance/TYPOGRAPHY.md", "governance/PRODUCT-ADOPTION.md", "reports/openai-typography-reference-v1.5.json", "reports/clickagents-typography-reference-v1.5.json", "scripts/audit_typography_contract.py", } if not report_path.exists(): audit.error("Auditoria tipográfica ausente") return report = json.loads(report_path.read_text(encoding="utf-8")) if not report.get("passed") or report.get("failures") or len(report.get("fixtureResults", [])) != 12 or len(report.get("portalResults", [])) != 32 or not all(report.get("staticChecks", {}).values()) or set(report.get("sourceSha256", {})) != expected_sources: audit.error("Escala, peso, fonte, reflow, scripts ou adoção tipográfica divergem do contrato") for relative_path, expected_hash in report.get("sourceSha256", {}).items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Auditoria tipográfica obsoleta: {relative_path}") if not visual_path.exists(): audit.error("Evidência visual tipográfica ausente") else: visual = json.loads(visual_path.read_text(encoding="utf-8")) captures = visual.get("captures", []) if not visual.get("passed") or visual.get("failures") or len(captures) != 8: audit.error("Evidência visual tipográfica diverge") for capture in captures: image_path = ROOT / capture.get("file", "") if not image_path.exists() or sha256(image_path) != capture.get("sha256"): audit.error(f"Captura tipográfica obsoleta: {capture.get('file', '')}") audit.pass_check("tipografia proporcional", 44) def audit_content_density_contract(audit: Audit) -> None: """Validate the lossless D1 editorial-density pilot and its evidence.""" report_path = ROOT / "reports/content-density-contract-audit-v1.5.json" visual_path = ROOT / "reports/content-density-visual-v1.5.json" expected_sources = { "components.html", "index.html", "assets/css/portal.css", "assets/js/portal.js", "fixtures/content-density-pilot-v1.5.json", "reports/content-density-d0-decisions-v1.5.md", "governance/CONTENT-DESIGN-AND-DENSITY.md", "scripts/audit_content_density_contract.py", } if not report_path.exists(): audit.error("Auditoria de densidade editorial ausente") return report = json.loads(report_path.read_text(encoding="utf-8")) search_results = report.get("searchResults", []) fragment_results = report.get("fragmentResults", []) accessibility = report.get("accessibility", {}) if not report.get("passed") or report.get("failures") or report.get("pilotCount") != 2 or report.get("unitCount") != 11 or report.get("sourceCharacters") != 2817 or len(report.get("profiles", [])) != 9 or len(report.get("noJavascript", [])) != 6 or len(search_results) != 3 or sum(len(result.get("cases", [])) for result in search_results) != 18 or not all(result.get("passed") for result in search_results) or len(fragment_results) != 3 or sum(len(result.get("cases", [])) for result in fragment_results) != 33 or not all(result.get("passed") for result in fragment_results) or not accessibility.get("passed") or set(report.get("sourceHashes", {})) != expected_sources: audit.error("Piloto de densidade editorial diverge do contrato D1") for relative_path, expected_hash in report.get("sourceHashes", {}).items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Auditoria de densidade editorial obsoleta: {relative_path}") if not visual_path.exists(): audit.error("Evidência visual de densidade editorial ausente") else: visual = json.loads(visual_path.read_text(encoding="utf-8")) captures = visual.get("captures", []) expected_visual_sources = { "components.html", "assets/css/portal.css", "assets/js/theme-init.js", "reports/content-density-contract-audit-v1.5.json", "scripts/capture_content_density_evidence.py", } if not visual.get("passed") or visual.get("failures") or len(captures) != 8 or set(visual.get("sourceHashes", {})) != expected_visual_sources: audit.error("Evidência visual de densidade editorial diverge") for relative_path, expected_hash in visual.get("sourceHashes", {}).items(): source_path = ROOT / relative_path if not source_path.exists() or sha256(source_path) != expected_hash: audit.error(f"Evidência de densidade editorial obsoleta: {relative_path}") for capture in captures: image_path = ROOT / capture.get("file", "") if not image_path.exists() or sha256(image_path) != capture.get("sha256"): audit.error(f"Captura de densidade editorial obsoleta: {capture.get('file', '')}") audit.pass_check("content design e densidade editorial", 11) def audit_text_font_assets(audit: Audit) -> None: """Ensure the final Noto Sans text font is local and traceable.""" font_dir = ROOT / "assets/fonts/noto-sans" report_path = ROOT / "reports/noto-sans-source-v1.5.json" required = ( font_dir / "NotoSans-normal-latin.woff2", font_dir / "NotoSans-normal-latin-ext.woff2", font_dir / "NotoSans-italic-latin.woff2", font_dir / "NotoSans-italic-latin-ext.woff2", font_dir / "noto-sans.css", font_dir / "OFL.txt", report_path, ) for path in required: if not path.exists() or path.stat().st_size == 0: audit.error(f"Asset Noto Sans local ausente: {relative(path)}") for page in ROOT_PAGES: source = page.read_text(encoding="utf-8") if "assets/fonts/noto-sans/noto-sans.css" not in source: audit.error(f"Página não carrega Noto Sans local: {relative(page)}") if "fonts.googleapis.com" in source or "fonts.gstatic.com" in source: audit.error(f"Página ainda depende de fonte de texto remota: {relative(page)}") if report_path.exists(): report = json.loads(report_path.read_text(encoding="utf-8")) for filename, metadata in report.get("installed", {}).items(): path = font_dir / filename if not path.exists() or metadata.get("sha256") != sha256(path): audit.error(f"Hash Noto Sans diverge: {filename}") audit.pass_check("fontes Noto Sans locais", 4) def audit_iconography(audit: Audit) -> None: icon_dir = ROOT / "assets/icons/phosphor" required_assets = ( icon_dir / "Phosphor.woff2", icon_dir / "Phosphor-Bold.woff2", icon_dir / "Phosphor-Fill.woff2", icon_dir / "phosphor.css", icon_dir / "LICENSE", ROOT / "reports/icon-migration-phosphor-v1.5.md", ROOT / "reports/phosphor-source-v1.5.json", ROOT / "assets/icons/file-formats.json", ROOT / "reports/clickagents-file-formats-v1.5.json", ROOT / "reports/clickagents-file-formats-v1.5.md", ROOT / "phosphor-icons.zip", ) for path in required_assets: if not path.exists() or path.stat().st_size == 0: audit.error(f"Asset Phosphor ausente ou vazio: {relative(path)}") source_report_path = ROOT / "reports/phosphor-source-v1.5.json" if source_report_path.exists(): source_report = json.loads(source_report_path.read_text(encoding="utf-8")) if source_report.get("source_archive_sha256") != sha256(ROOT / "phosphor-icons.zip"): audit.error("SHA-256 do arquivo Phosphor diverge da origem registrada") asset_hashes = source_report.get("installed_asset_sha256", {}) for filename in ("Phosphor.woff2", "Phosphor-Bold.woff2", "Phosphor-Fill.woff2"): if asset_hashes.get(filename) != sha256(icon_dir / filename): audit.error(f"SHA-256 do asset Phosphor diverge: {filename}") report_path = ROOT / "reports/icon-migration-phosphor-v1.5.md" if not report_path.exists(): return report = report_path.read_text(encoding="utf-8") mapping = dict(re.findall(r"\| `([a-z0-9_]+)` \| `ph-([a-z0-9-]+)` \|", report)) if len(mapping) != 105: audit.error(f"De–para Phosphor deveria ter 105 decisões, encontrou {len(mapping)}") phosphor_css = (icon_dir / "phosphor.css").read_text(encoding="utf-8") if (icon_dir / "phosphor.css").exists() else "" for target in sorted(set(mapping.values())): for weight_class in ("ph", "ph-bold", "ph-fill"): if f".{weight_class}.ph-{target}::before" not in phosphor_css: audit.error(f"Glyph ausente no subset: {weight_class}.ph-{target}") icon_count = 0 for path in ROOT_PAGES: source = path.read_text(encoding="utf-8") if "Material+Symbols" in source or "material-symbols-fallback" in source: audit.error(f"{path.name}: dependência iconográfica anterior permanece") if re.search(r']*class="[^"]*\bb-icon\b[^"]*"[^>]*>\s*[^<\s]', source): audit.error(f"{path.name}: b-icon ainda contém ligatura textual") parser = parse_html(path) for element in parser.elements: classes = element.attrs.get("class", "").split() if "b-icon" not in classes: continue icon_count += 1 if element.attrs.get("aria-hidden") != "true": audit.error(f"{path.name}:{element.line}: glyph deve ser oculto da árvore acessível") legacy_name = element.attrs.get("data-icon", "") if legacy_name not in mapping: audit.error(f"{path.name}:{element.line}: ícone sem decisão no de–para") continue weights = [name for name in ("ph", "ph-bold", "ph-fill") if name in classes] glyphs = [name[3:] for name in classes if name.startswith("ph-") and name not in {"ph-bold", "ph-fill"}] if len(weights) != 1: audit.error(f"{path.name}:{element.line}: ícone deve declarar exatamente um peso Phosphor") if glyphs != [mapping[legacy_name]]: audit.error(f"{path.name}:{element.line}: {legacy_name} deveria usar ph-{mapping[legacy_name]}") expected_weight = "ph-fill" if {"b-icon--fill", "b-icon--selected"} & set(classes) else "ph-bold" if "b-icon--strong" in classes else "ph" if weights and weights[0] != expected_weight: audit.error(f"{path.name}:{element.line}: peso {weights[0]} diverge de {expected_weight}") source_bundle = "\n".join(path.read_text(encoding="utf-8") for path in [*CSS_FILES, *JS_FILES]) for forbidden in ("material-symbols-fallback", "--b-icon-opsz", "font-variation-settings"): if forbidden in source_bundle: audit.error(f"Contrato iconográfico anterior permanece: {forbidden}") runtime_source = "\n".join((ROOT / "assets/js" / name).read_text(encoding="utf-8") for name in ("components.js", "portal.js")) for required_runtime_class in ("ph-copy", "ph-radio-button", "ph-circle"): if required_runtime_class not in runtime_source: audit.error(f"Ícone runtime não migrado: {required_runtime_class}") audit.pass_check("ícones Phosphor", icon_count) audit.pass_check("decisões do de–para", len(mapping)) def audit_component_catalog(audit: Audit) -> None: coverage_path = ROOT / "reports/clickagents-component-coverage-v1.5.md" public_coverage_path = ROOT / "reports/component-coverage-v1.5.md" inventory_path = ROOT / "reports/clickagents-inventory-v1.5.json" components_path = ROOT / "components.html" if not coverage_path.exists() or not inventory_path.exists(): audit.error("Inventário ou cobertura de componentes ausente") return coverage = coverage_path.read_text(encoding="utf-8") if not public_coverage_path.exists() or public_coverage_path.read_text(encoding="utf-8") != coverage: audit.error("Cópia pública da matriz de cobertura ausente ou desatualizada") rows = [] for line in coverage.splitlines(): if not line.startswith("|") or "---" in line: continue parts = [part.strip() for part in line.strip("|").split("|")] if len(parts) == 4 and parts[1] in {"core", "extension", "pattern", "domain"}: rows.append(parts) page = components_path.read_text(encoding="utf-8") evidence_path = ROOT / "reports/component-catalog-fragment-v1.5.html" evidence = evidence_path.read_text(encoding="utf-8") if evidence_path.exists() else "" cards = re.findall(r'class="component-card component-card--(?:core|extension|pattern|domain)"', evidence) delivery = re.findall(r'class="delivery-chip delivery-chip--(?:implemented|documented)"', evidence) if len(cards) != len(rows): audit.error(f"Evidência exaustiva ({len(cards)}) diverge da cobertura ({len(rows)})") if len(delivery) != len(rows): audit.error(f"Estados de entrega da evidência ({len(delivery)}) divergem da cobertura ({len(rows)})") visual_groups = re.findall(r'class="component-overview-card"', page) if len(visual_groups) != 6: audit.error(f"Visão geral deveria apresentar 6 grupos visuais, encontrou {len(visual_groups)}") if "component-catalog__block" in page or "component-card component-card--" in page: audit.error("Matriz exaustiva não deve interromper o fluxo visual de components.html") example_ids = ( "button", "icon-button", "chip", "text-field", "selection", "picker", "tabs", "nav", "menu", "account", "entity-item", "card", "list", "table", "badge", "media-card", "dialog", "alert", "tooltip", "progress", "empty", "anatomy", ) missing_examples = [component_id for component_id in example_ids if f'id="{component_id}"' not in page] if missing_examples: audit.error("Exemplos detalhados ausentes: " + ", ".join(missing_examples)) patterns = (ROOT / "assets/css/patterns.css").read_text(encoding="utf-8") for selector in (".b-toolbar", ".b-segmented"): if re.search(rf"{re.escape(selector)}(?:\s|\{{|:)", patterns): audit.error(f"Padrão redefine seletor do core: {selector}") if ".b-pattern-segmented" in patterns: audit.error("Contrato segmented legado permanece em patterns.css") css_bundle = "\n".join(path.read_text(encoding="utf-8") for path in CSS_FILES if path.name != "balu.css") if re.search(r"\.b-icon[^\{]*\{[^}]*font-size\s*:", css_bundle, flags=re.DOTALL): audit.error("Ícone altera font-size sem avançar a caixa óptica") if 'role="radiogroup"' not in page or 'role="radio"' not in page: audit.error("Specimen de radio sem contrato radiogroup") if 'role="tablist"' not in page or 'aria-controls="tab-panel-' not in page: audit.error("Specimen de tabs sem associação tabpanel") inventory = json.loads(inventory_path.read_text(encoding="utf-8")) snapshot = inventory.get("source_snapshot", {}) for key in ("branch", "commit", "corpus_sha256"): if not snapshot.get(key) or snapshot.get(key) == "unavailable": audit.error(f"Snapshot da referência sem {key}") audit.pass_check("decisões de componentes", len(rows)) def main() -> int: audit = Audit() audit_html(audit) audit_css(audit) audit_javascript(audit) audit_documentation(audit) audit_shape_contract(audit) audit_edge_alignment(audit) audit_icon_button_contract(audit) audit_representation_contract(audit) audit_spacing_control_contract(audit) audit_proportion_containment_contract(audit) audit_workspace_geometry_contract(audit) audit_sidebar_geometry_contract(audit) audit_nav_section_contract(audit) audit_typography_contract(audit) audit_content_density_contract(audit) audit_text_font_assets(audit) audit_iconography(audit) audit_component_catalog(audit) payload = { "status": "approved" if not audit.errors else "failed", "version": "1.5.0", "errors": audit.errors, "warnings": audit.warnings, "checks": dict(audit.checks), "summary": { "html_pages": len(ROOT_PAGES), "css_files": len(CSS_FILES), "js_files": len(JS_FILES), "errors": len(audit.errors), "warnings": len(audit.warnings), }, } (ROOT / "reports/audit-static-v1.5.json").write_text( json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) if "--json" in sys.argv: print(json.dumps(payload, ensure_ascii=False, indent=2)) else: print("Baluarte v1.5 — auditoria do portal") print(f"HTML: {len(ROOT_PAGES)} · CSS: {len(CSS_FILES)} · JS: {len(JS_FILES)}") if audit.errors: print(f"\nFALHAS ({len(audit.errors)})") for error in audit.errors: print(f"- {error}") else: print("\nAPROVADO — nenhuma falha automatizável encontrada.") if audit.warnings: print(f"\nAVISOS ({len(audit.warnings)})") for warning in audit.warnings: print(f"- {warning}") return 1 if audit.errors else 0 if __name__ == "__main__": raise SystemExit(main())