Owen Graphite Source MAP Risk Map
Canonical MAP root: dev/WIKI/MAP. Source: {html.escape(str(payload['source']))}. Version: {html.escape(str(payload['version']))}.
| Severity | Score | Module | Selectors | Reasons |
|---|
#!/usr/bin/env python3 """Build source-adjacent MAP artifacts for the Owen Graphite src/ tree. The MAP lives under dev/WIKI/MAP so investigation artifacts stay separate from the shipping src/ CSS modules while still mapping directly back to src/entry.css. """ from __future__ import annotations import html import json import re from collections import Counter, defaultdict from dataclasses import dataclass from pathlib import Path ROOT = Path(__file__).resolve().parents[2] SRC_DIR = ROOT / "src" ENTRY = SRC_DIR / "entry.css" MAP_DIR = ROOT / "dev" / "WIKI" / "MAP" MANIFEST = ROOT / "manifest.json" IMPORT_RE = re.compile(r"@import\s+url\(\s*['\"]?([^'\")]+)['\"]?\s*\)\s*;", re.I) LEGACY_RE = re.compile(r"/\*\s*([^*]+?)\s*->\s*([^*]+?)\s*\*/") RULE_RE = re.compile(r"([^{}]+?)\{([^{}]*)\}", re.S) @dataclass(frozen=True) class CssRule: selector: str body: str at_context: str line: int def normalize_ws(text: str) -> str: return re.sub(r"\s+", " ", text).strip() def manifest_version() -> str: return json.loads(MANIFEST.read_text(encoding="utf-8"))["version"] def import_order() -> list[dict[str, object]]: modules: list[dict[str, object]] = [] pending_legacy = "" for line_number, raw_line in enumerate(ENTRY.read_text(encoding="utf-8").splitlines(), start=1): legacy_match = LEGACY_RE.search(raw_line) if legacy_match: pending_legacy = normalize_ws(legacy_match.group(1)) continue import_match = IMPORT_RE.search(raw_line) if not import_match: continue relative_import = import_match.group(1) module_path = (ENTRY.parent / relative_import).resolve() modules.append( { "import_index": len(modules) + 1, "entry_line": line_number, "legacy_id": pending_legacy, "module": module_path.relative_to(ROOT).as_posix(), } ) pending_legacy = "" return modules def tokenize_blocks(css: str) -> list[CssRule]: rules: list[CssRule] = [] at_stack: list[str] = [] index = 0 line_number = 1 css_length = len(css) while index < css_length: if css.startswith("/*", index): comment_end = css.find("*/", index + 2) if comment_end == -1: return rules line_number += css.count("\n", index, comment_end + 2) index = comment_end + 2 continue current_char = css[index] if current_char.isspace(): if current_char == "\n": line_number += 1 index += 1 continue if current_char == "@": header_start = index while index < css_length and css[index] not in "{;": if css[index] == "\n": line_number += 1 index += 1 if index >= css_length: return rules if css[index] == ";": index += 1 continue header = normalize_ws(css[header_start:index]) at_stack.append(header) block_start = index + 1 index = block_start depth = 1 while index < css_length and depth > 0: if css.startswith("/*", index): comment_end = css.find("*/", index + 2) if comment_end == -1: return rules line_number += css.count("\n", index, comment_end + 2) index = comment_end + 2 continue if css[index] == "{": depth += 1 elif css[index] == "}": depth -= 1 if depth == 0: break if css[index] == "\n": line_number += 1 index += 1 inner_css = css[block_start:index] inner_start_line = css.count("\n", 0, block_start) + 1 for inner_rule in tokenize_blocks(inner_css): context = " >> ".join(at_stack + ([inner_rule.at_context] if inner_rule.at_context else [])) rules.append( CssRule( selector=inner_rule.selector, body=inner_rule.body, at_context=context, line=inner_start_line + inner_rule.line - 1, ) ) at_stack.pop() index += 1 continue selector_start = index while index < css_length: if css.startswith("/*", index): comment_end = css.find("*/", index + 2) if comment_end == -1: return rules line_number += css.count("\n", index, comment_end + 2) index = comment_end + 2 continue if css[index] in "{}": break index += 1 if index >= css_length or css[index] != "{": index += 1 continue selector = normalize_ws(css[selector_start:index]) body_start = index + 1 index = body_start depth = 1 while index < css_length and depth > 0: if css.startswith("/*", index): comment_end = css.find("*/", index + 2) if comment_end == -1: return rules line_number += css.count("\n", index, comment_end + 2) index = comment_end + 2 continue if css[index] == "{": depth += 1 elif css[index] == "}": depth -= 1 if depth == 0: break if css[index] == "\n": line_number += 1 index += 1 body = normalize_ws(css[body_start:index]) if selector and body: rules.append(CssRule(selector=selector, body=body, at_context=" >> ".join(at_stack), line=line_number)) index += 1 return rules def scan_structural_issues(css: str) -> list[dict[str, object]]: issues: list[dict[str, object]] = [] depth = 0 line_number = 1 index = 0 css_length = len(css) while index < css_length: if css.startswith("/*", index): comment_end = css.find("*/", index + 2) if comment_end == -1: issues.append({"severity": "critical", "line": line_number, "message": "unterminated comment"}) return issues line_number += css.count("\n", index, comment_end + 2) index = comment_end + 2 continue current_char = css[index] if current_char == "\n": line_number += 1 elif current_char == "{": depth += 1 elif current_char == "}": if depth == 0: issues.append({"severity": "critical", "line": line_number, "message": "orphan closing brace"}) else: depth -= 1 index += 1 if depth: issues.append({"severity": "critical", "line": line_number, "message": f"unclosed block depth={depth}"}) return issues def declarations(body: str) -> list[tuple[str, str]]: parsed: list[tuple[str, str]] = [] for raw_declaration in body.split(";"): if ":" not in raw_declaration: continue property_name, _, value = raw_declaration.partition(":") parsed.append((property_name.strip().lower(), value.strip())) return parsed def specificity(selector: str) -> tuple[int, int, int]: selector_without_strings = re.sub(r"(['\"]).*?\1", "", selector) ids = len(re.findall(r"#[\w-]+", selector_without_strings)) classes = len(re.findall(r"\.[\w-]+|\[[^\]]+\]|:(?!:)[\w-]+(?:\([^)]*\))?", selector_without_strings)) elements = len(re.findall(r"(? str: if critical_count: return "critical" if score >= 30: return "high" if score >= 12: return "medium" if score > 0: return "low" return "info" def severity_rank(severity: object) -> int: order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4} return order.get(str(severity), 5) def analyze_module(module_info: dict[str, object]) -> tuple[dict[str, object], list[dict[str, object]], dict[str, dict[str, object]]]: module_path = ROOT / str(module_info["module"]) css = module_path.read_text(encoding="utf-8") rules = tokenize_blocks(css) structural_issues = scan_structural_issues(css) findings: list[dict[str, object]] = [] selector_entries: dict[str, dict[str, object]] = {} important_count = 0 has_count = 0 high_specificity_count = 0 live_preview_sensitive_count = 0 pdf_header_footer_sensitive_count = 0 max_specificity = (0, 0, 0) property_counter: Counter[str] = Counter() selector_counter: Counter[str] = Counter() for issue in structural_issues: findings.append( { "severity": issue["severity"], "category": "css-structure", "module": module_info["module"], "line": issue["line"], "message": issue["message"], } ) for rule in rules: selector_counter[rule.selector] += 1 selector_specificity = specificity(rule.selector) max_specificity = max(max_specificity, selector_specificity) if selector_specificity >= (0, 8, 0): high_specificity_count += 1 findings.append( { "severity": "medium", "category": "high-specificity", "module": module_info["module"], "line": rule.line, "selector": rule.selector, "message": f"specificity={selector_specificity}", } ) if ":has(" in rule.selector: has_count += rule.selector.count(":has(") findings.append( { "severity": "low", "category": "has-selector", "module": module_info["module"], "line": rule.line, "selector": rule.selector, "message": ":has() selector; keep narrowly scoped", } ) if "markdown-source-view.mod-cm6" in rule.selector: sensitive_props = {"margin", "margin-top", "margin-bottom", "padding", "padding-top", "padding-bottom", "outline", "transform"} declared_props = {property_name for property_name, _ in declarations(rule.body)} if declared_props & sensitive_props: live_preview_sensitive_count += 1 findings.append( { "severity": "medium", "category": "cm6-hit-routing-sensitive", "module": module_info["module"], "line": rule.line, "selector": rule.selector, "message": "CM6 rule declares vertical box or overlay-sensitive properties", } ) pdf_terms = ( "ogd-pdf-header-enabled", "ogd-pdf-footer-enabled", "--ogd-pdf-header-text", "--ogd-pdf-footer-text", "--ogd-pdf-marginalia", "@page", ) if any(term in rule.selector or term in rule.body or term in rule.at_context for term in pdf_terms): pdf_header_footer_sensitive_count += 1 findings.append( { "severity": "low", "category": "pdf-header-footer-sensitive", "module": module_info["module"], "line": rule.line, "selector": rule.selector, "message": "PDF marginalia or @page-sensitive rule; review dev/WIKI/MAP/pdf-header-footer-contract.md", } ) for property_name, value in declarations(rule.body): property_counter[property_name] += 1 if "!important" in value: important_count += 1 findings.append( { "severity": "high", "category": "important", "module": module_info["module"], "line": rule.line, "selector": rule.selector, "message": f"{property_name}: {value}", } ) selector_entries[f"{module_info['module']}::{rule.selector}::{rule.line}"] = { "module": module_info["module"], "selector": rule.selector, "line": rule.line, "at_context": rule.at_context, "specificity": selector_specificity, "declarations": len(declarations(rule.body)), } repeated_in_file = sum(count - 1 for count in selector_counter.values() if count > 1) score = ( len(structural_issues) * 100 + important_count * 15 + high_specificity_count * 3 + live_preview_sensitive_count * 3 + pdf_header_footer_sensitive_count * 2 + has_count * 2 + repeated_in_file ) reasons: list[str] = [] if structural_issues: reasons.append("css-structure") if important_count: reasons.append("important") if high_specificity_count: reasons.append("high-specificity") if live_preview_sensitive_count: reasons.append("cm6-hit-routing-sensitive") if pdf_header_footer_sensitive_count: reasons.append("pdf-header-footer-sensitive") if has_count: reasons.append("has-selector") if repeated_in_file: reasons.append("repeated-selector-in-file") module_record = { **module_info, "line_count": css.count("\n") + 1, "selector_count": len(rules), "declaration_count": sum(property_counter.values()), "important_count": important_count, "has_selector_count": has_count, "high_specificity_selector_count": high_specificity_count, "live_preview_sensitive_rule_count": live_preview_sensitive_count, "pdf_header_footer_sensitive_rule_count": pdf_header_footer_sensitive_count, "repeated_selector_extra_count": repeated_in_file, "max_specificity": max_specificity, "top_properties": property_counter.most_common(12), "risk_score": score, "severity": severity_for_score(score, len(structural_issues)), "risk_reasons": reasons, } return module_record, findings, selector_entries def cross_file_selector_groups(selector_entries: dict[str, dict[str, object]]) -> list[dict[str, object]]: grouped: dict[str, list[dict[str, object]]] = defaultdict(list) for entry in selector_entries.values(): grouped[str(entry["selector"])].append(entry) groups: list[dict[str, object]] = [] for selector, entries in grouped.items(): modules = sorted({str(entry["module"]) for entry in entries}) if len(modules) < 2: continue groups.append( { "selector": selector, "occurrences": len(entries), "modules": modules, "lines": sorted( ({"module": entry["module"], "line": entry["line"]} for entry in entries), key=lambda item: (str(item["module"]), int(item["line"])), ), } ) return sorted(groups, key=lambda group: (-int(group["occurrences"]), str(group["selector"]))) def write_json(path: Path, payload: object) -> None: path.write_text(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") def markdown_report(payload: dict[str, object]) -> str: summary = payload["summary"] finding_counts = summary["finding_severity_counts"] module_counts = summary["module_severity_counts"] module_rows = [] for module in payload["source_modules"]: module_rows.append( "| {severity} | {score} | `{module}` | {selectors} | {important} | {has_count} | {reasons} |".format( severity=module["severity"], score=module["risk_score"], module=module["module"], selectors=module["selector_count"], important=module["important_count"], has_count=module["has_selector_count"], reasons=", ".join(module["risk_reasons"]) or "-", ) ) finding_rows = [] for finding in payload["findings"][:80]: finding_rows.append( "| {severity} | {category} | `{module}:{line}` | {message} |".format( severity=finding["severity"], category=finding["category"], module=finding["module"], line=finding["line"], message=str(finding["message"]).replace("|", "\\|"), ) ) return "\n".join( [ "# Source MAP Risk Classification", "", "Canonical MAP location: `dev/WIKI/MAP`.", "", "## Summary", "", f"- Version: `{payload['version']}`", f"- Source: `{payload['source']}`", f"- Modules: {summary['module_count']}", f"- Selectors: {summary['selector_count']}", f"- Findings: {summary['finding_count']}", f"- Finding severity counts: critical={finding_counts.get('critical', 0)}, high={finding_counts.get('high', 0)}, medium={finding_counts.get('medium', 0)}, low={finding_counts.get('low', 0)}, info={finding_counts.get('info', 0)}", f"- Module severity counts: critical={module_counts.get('critical', 0)}, high={module_counts.get('high', 0)}, medium={module_counts.get('medium', 0)}, low={module_counts.get('low', 0)}, info={module_counts.get('info', 0)}", "", "## Module Risk Table", "", "| Severity | Score | Module | Selectors | !important | :has | Reasons |", "|---|---:|---|---:|---:|---:|---|", *module_rows, "", "## Findings", "", "| Severity | Category | Location | Message |", "|---|---|---|---|", *(finding_rows or ["| info | none | - | No structural findings. |"]), "", ] ) def checklist(payload: dict[str, object]) -> str: finding_counts = payload["summary"]["finding_severity_counts"] module_counts = payload["summary"]["module_severity_counts"] return "\n".join( [ "# Source MAP Stabilization Checklist", "", "## Gates", "", "- Finding `critical = 0`: no orphan braces, unclosed blocks, or unterminated comments.", "- Finding `high` must not increase unless a release note explains the Obsidian core conflict.", "- CM6 hit-routing sensitive findings must be reviewed against `dev/WIKI/MAP/cm6-hit-routing-contract.md`.", "- Top chrome/icon changes must be reviewed against `dev/WIKI/MAP/top-chrome-icon-background-contract.md`.", "- PDF header/footer sensitive findings must be reviewed against `dev/WIKI/MAP/pdf-header-footer-contract.md`.", "", "## Current Finding Baseline", "", f"- critical={finding_counts.get('critical', 0)}", f"- high={finding_counts.get('high', 0)}", f"- medium={finding_counts.get('medium', 0)}", f"- low={finding_counts.get('low', 0)}", f"- info={finding_counts.get('info', 0)}", "", "## Current Module Baseline", "", f"- critical={module_counts.get('critical', 0)}", f"- high={module_counts.get('high', 0)}", f"- medium={module_counts.get('medium', 0)}", f"- low={module_counts.get('low', 0)}", f"- info={module_counts.get('info', 0)}", "", "## Regenerate", "", "```powershell", r".\.venv\Scripts\python.exe dev\scripts\build_src_map.py", "```", "", ] ) def html_report(payload: dict[str, object]) -> str: rows = [] for module in payload["source_modules"]: rows.append( "
{module}Canonical MAP root: dev/WIKI/MAP. Source: {html.escape(str(payload['source']))}. Version: {html.escape(str(payload['version']))}.
| Severity | Score | Module | Selectors | Reasons |
|---|