#!/usr/bin/env python3 """Build a source usage and ownership map for Owen Graphite CSS.""" from __future__ import annotations import argparse import importlib.util import json import re import sys from collections import Counter, defaultdict from pathlib import Path ROOT = Path(__file__).resolve().parents[2] MAP = ROOT / "dev" / "WIKI" / "MAP" WIKI = ROOT / "dev" / "WIKI" OWNER_REGISTRY = MAP / "owner-registry.json" EFFECTIVE_SOURCE_MAP = MAP / "effective-source-map.json" OUT_JSON = WIKI / "MAP" / "source-usage-map.json" OUT_MD = WIKI / "MAP" / "source-usage-map.md" OUT_RUNTIME_MD = WIKI / "runtime-debug-protocol.md" SNIPPET_DIR = WIKI / "runtime-debug-snippets" TABLE_CELL_SNIPPET = SNIPPET_DIR / "table-cell-dump.js" MATCHED_RULES_SNIPPET = SNIPPET_DIR / "matched-rules-dump.js" TABLE_TERMS = ("table", "td", "th", "tr", "tbody", "thead", "caption", "cm-table-widget", "table.cm-table", "HyperMD-table-row") CM6_TERMS = ("markdown-source-view.mod-cm6", "cm-line", "HyperMD-", "cm-content", "cm-editor", "cm-scroller") PRINT_TERMS = ("@media print", "@page", "ogd-pdf", "print") CHANGE_TYPE_COMMANDS = { "Table": [ "Open `src/surfaces/20-reading-tables-code.css` first for rendered tables.", "Open `src/base/13-live-preview.css` or `src/surfaces/24-html-table-live-preview-glass.css` for Live Preview HTML tables.", "Run `audit_direct_owner_guard.py`, `audit_lp_pdf_selector_ownership.py`, and `audit_v3_hit_routing.py`.", ], "Live Preview": [ "Open `src/base/13-live-preview.css` first.", "Read `dev/WIKI/MAP/cm6-hit-routing-contract.md` before changing geometry.", "Run `audit_v3_hit_routing.py` and `audit_core_principles.py`.", ], "PDF": [ "Open `src/features/43-print-base.css`, `src/features/41-feature-presets.css`, or `src/features/42-report-print-polish.css` according to the surface.", "Read `dev/WIKI/MAP/pdf-header-footer-contract.md` for marginalia.", "Run `audit_pdf_header_footer.py` and `release_check.py --skip-bundle`.", ], "Chrome/UI": [ "Open `src/chrome/*` owner modules according to `Quick Routing`.", "Read `dev/WIKI/MAP/top-chrome-icon-background-contract.md` for top chrome icons.", "Run `audit_core_principles.py` and screenshot/runtime checks for interactive states.", ], "Docs/README": [ "Update docs and sample assets in the documented locations.", "Run `audit_docs_assets.py` and `audit_readme_svg_layout.py`.", ], "Release": [ "Run `bundle_v3.py`, `release_check.py --skip-bundle`, and `build_release.py`.", "Run `audit_release_zip.py` before publishing.", ], } def load_build_src_map_module(): script = ROOT / "dev" / "scripts" / "build_src_map.py" spec = importlib.util.spec_from_file_location("build_src_map", script) if spec is None or spec.loader is None: raise RuntimeError("unable to load build_src_map.py") module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module def split_selector_list(selector: str) -> list[str]: parts: list[str] = [] buffer: list[str] = [] depth = 0 for char in selector: if char == "(": depth += 1 elif char == ")" and depth: depth -= 1 if char == "," and depth == 0: part = "".join(buffer).strip() if part: parts.append(part) buffer = [] else: buffer.append(char) part = "".join(buffer).strip() if part: parts.append(part) return parts def strip_not_pseudos(selector: str) -> str: output: list[str] = [] index = 0 while index < len(selector): if selector[index : index + 5].lower() == ":not(": depth = 0 while index < len(selector): if selector[index] == "(": depth += 1 elif selector[index] == ")": depth -= 1 if depth == 0: index += 1 break index += 1 else: output.append(selector[index]) index += 1 return "".join(output) def strip_comments_keep_lines(text: str) -> str: import re return re.sub(r"/\*.*?\*/", lambda match: "\n" * match.group(0).count("\n"), text, flags=re.S) def owner_maps() -> tuple[dict[str, list[str]], dict[str, dict[str, object]], dict[str, dict[str, object]]]: registry = json.loads(OWNER_REGISTRY.read_text(encoding="utf-8")) by_module: dict[str, list[str]] = defaultdict(list) by_surface: dict[str, dict[str, object]] = {} for surface in registry["surfaces"]: by_surface[surface["id"]] = surface for module in surface["ownerModules"]: if module.startswith("src/"): by_module[module].append(surface["id"]) for module in surface.get("allowedLateModules", []): if module.startswith("src/"): by_module[module].append(f"{surface['id']} (allowed-late)") support_by_module = {str(item["module"]): item for item in registry.get("supportModules", [])} return dict(by_module), by_surface, support_by_module def bundle_ranges() -> dict[str, dict[str, int]]: data = json.loads(EFFECTIVE_SOURCE_MAP.read_text(encoding="utf-8")) return { item["module"]: { "bundleStartLine": item["bundleStartLine"], "bundleEndLine": item["bundleEndLine"], "sourceStartLine": item["sourceStartLine"], "sourceEndLine": item["sourceEndLine"], } for item in data["ranges"] } def classify(selector: str, body: str, at_context: str) -> set[str]: selector_parts = split_selector_list(selector) selector_text = " ".join(selector_parts) text = f"{selector_text} {body} {at_context}" low = text.lower() labels: set[str] = set() selector_low = selector_text.lower() if any(term.lower() in selector_low for term in TABLE_TERMS): labels.add("table") if ".markdown-source-view.mod-cm6" in low and "table" in low and ":not(.cm-table-widget)" in low and ":not(.cm-table)" in low: labels.add("lp-html-table") if "cm-table-widget" in low or "table.cm-table" in low: labels.add("lp-markdown-table-widget-reference") if "markdown-rendered" in low or "markdown-preview-view" in low or "markdown-reading-view" in low: labels.add("reading-rendered") if any(term.lower() in low for term in CM6_TERMS): labels.add("cm6") if any(term.lower() in low for term in PRINT_TERMS): labels.add("print-pdf") if "pre" in low or "code" in low or "language-" in low: labels.add("code") if "callout" in low or "blockquote" in low or "task-list" in low: labels.add("callout-list") if any(term in low for term in ("workspace-", "nav-", "ribbon", "status-bar", "side-dock")): labels.add("workspace-chrome") if any(term in low for term in ("menu", "suggestion", "popover", "tooltip", "modal", "prompt", "search")): labels.add("overlay-search") return labels def direct_core_table_violation(selector_part: str) -> bool: no_not = strip_not_pseudos(selector_part).lower() return ".cm-table-widget" in no_not or "table.cm-table" in no_not def classify_support_module(module: dict[str, object], support_by_module: dict[str, dict[str, object]]) -> str: path = str(module["module"]) support = support_by_module.get(path) if support: return f"registered support: {support.get('role', 'support module')}" labels = set(module["labels"]) if path.startswith("src/tokens/") or path.startswith("src/themes/"): return "intentional support: theme/tokens layer" if path.startswith("src/plugins/"): return "external/plugin specific support" if path in {"src/base/10-base-workspace.css", "src/surfaces/22-reading-embeds-workspace.css"}: return "intentional support: base/embed workspace primitives" if path == "src/chrome/33-settings-controls.css": return "possible owner registry gap: settings controls" if "workspace-chrome" in labels or "overlay-search" in labels: return "possible owner registry gap: chrome/overlay support" return "needs review" def table_selector_index() -> list[dict[str, str]]: return [ { "pattern": ".markdown-rendered table / .markdown-preview-view table", "owner": "src/surfaces/20-reading-tables-code.css", "purpose": "Reading/rendered table primitives and ordinary table surfaces", "status": "allowed", }, { "pattern": ".markdown-source-view.mod-cm6 ... table:not(.cm-table):not(.cm-table-widget)", "owner": "src/base/13-live-preview.css and src/surfaces/24-html-table-live-preview-glass.css", "purpose": "Live Preview HTML table embeds only", "status": "allowed with both guards", }, { "pattern": ".cm-table-widget / table.cm-table", "owner": "Obsidian core", "purpose": "Live Preview markdown table widget geometry", "status": "forbidden for theme geometry", }, { "pattern": "body.ogd-report-mode ... table / @media print table", "owner": "src/features/42-report-print-polish.css", "purpose": "Report/PDF table output and print-safe adjustments", "status": "allowed in report/print scope", }, { "pattern": "table caption / .table-caption / .table-source", "owner": "src/surfaces/23-liquid-glass-core.css and src/features/42-report-print-polish.css", "purpose": "Rendered captions and report notes", "status": "allowed for rendered/report surfaces", }, ] def render_table_cell_snippet() -> str: return """(() => { const cell = [...document.querySelectorAll('td, th')] .find(el => el.getBoundingClientRect().height > 90) || document.activeElement?.closest?.('td, th'); if (!cell) { copy('NO_CELL_FOUND'); return 'NO_CELL_FOUND'; } const nodes = []; for (let el = cell; el && nodes.length < 12; el = el.parentElement) nodes.push(el); const out = nodes.map(el => { const cs = getComputedStyle(el); const r = el.getBoundingClientRect(); return { tag: el.tagName, cls: String(el.className || ''), text: el.textContent?.trim().slice(0, 80), style: el.getAttribute('style'), rect: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) }, computed: { display: cs.display, position: cs.position, width: cs.width, height: cs.height, minHeight: cs.minHeight, maxHeight: cs.maxHeight, padding: cs.padding, margin: cs.margin, lineHeight: cs.lineHeight, verticalAlign: cs.verticalAlign, overflow: cs.overflow, transform: cs.transform } }; }); copy(JSON.stringify(out, null, 2)); return out; })(); """ def render_matched_rules_snippet() -> str: return """(() => { const target = [...document.querySelectorAll('td, th')] .find(el => el.getBoundingClientRect().height > 90) || document.activeElement?.closest?.('td, th') || document.activeElement; if (!target) { copy('NO_TARGET_FOUND'); return 'NO_TARGET_FOUND'; } const nodes = [target, ...target.querySelectorAll?.('*') || []].slice(0, 20); const out = nodes.map(el => { const matched = []; for (const sheet of [...document.styleSheets]) { let rules; try { rules = sheet.cssRules; } catch { continue; } for (const rule of [...rules]) { if (!rule.selectorText) continue; try { if (el.matches(rule.selectorText) && /height|min-height|max-height|vertical-align|padding|line-height|display|position|transform|width|max-width/.test(rule.style.cssText)) { matched.push({ selector: rule.selectorText, css: rule.style.cssText }); } } catch {} } } const cs = getComputedStyle(el); return { tag: el.tagName, cls: String(el.className || ''), style: el.getAttribute('style'), computed: { height: cs.height, minHeight: cs.minHeight, maxHeight: cs.maxHeight, padding: cs.padding, lineHeight: cs.lineHeight, display: cs.display, position: cs.position, verticalAlign: cs.verticalAlign, width: cs.width, maxWidth: cs.maxWidth }, matched: matched.slice(-50) }; }); copy(JSON.stringify(out, null, 2)); return out; })(); """ def render_runtime_debug_protocol() -> str: return """# Runtime Debug Protocol Use this when static audits pass but a selected, hovered, focused, or active runtime state still fails. ## Required Steps 1. Reproduce the issue in Obsidian with the exact runtime state active. 2. Run `runtime-debug-snippets/table-cell-dump.js` when the issue is table/cell geometry. 3. Run `runtime-debug-snippets/matched-rules-dump.js` to capture theme/core matched rules. 4. Inspect inline `style` first. Inline geometry means the issue may not be solvable by ordinary owner CSS. 5. If a theme rule is responsible, map the bundle line through `effective-source-map.json` and edit the source owner. 6. If an Obsidian core rule is responsible, do not override it unless an owner contract explicitly permits it. 7. Re-run the same runtime state after editing; static audits alone are insufficient. ## Outputs To Preserve - DOM chain: tag, class, inline style, text preview. - Rect chain: x/y/width/height for the target and parents. - Computed geometry: display, position, width, height, min/max height, padding, line-height, vertical-align, overflow, transform. - Matched rules: selector and CSS text for geometry-affecting declarations. ## Snippets - `dev/WIKI/runtime-debug-snippets/table-cell-dump.js` - `dev/WIKI/runtime-debug-snippets/matched-rules-dump.js` """ def build() -> tuple[dict[str, object], str]: build_src_map = load_build_src_map_module() imports = build_src_map.import_order() owners_by_module, surfaces, support_by_module = owner_maps() ranges = bundle_ranges() modules: list[dict[str, object]] = [] table_rules: list[dict[str, object]] = [] hard_violations: list[dict[str, object]] = [] totals = Counter() for index, item in enumerate(imports): module = item["module"] path = ROOT / module css = path.read_text(encoding="utf-8") rules = build_src_map.tokenize_blocks(strip_comments_keep_lines(css)) label_counts = Counter() selector_parts = 0 for rule in rules: labels = classify(rule.selector, rule.body, rule.at_context) for label in labels: label_counts[label] += 1 parts = split_selector_list(rule.selector) selector_parts += len(parts) for part in parts: if direct_core_table_violation(part): hard_violations.append({"module": module, "line": rule.line, "selector": part}) if "table" in labels: table_rules.append( { "module": module, "line": rule.line, "context": rule.at_context, "labels": sorted(labels), "selector": rule.selector, } ) range_info = ranges.get(module, {}) source_lines = css.count("\n") + 1 record = { "index": index + 1, "module": module, "legacyId": item.get("legacy_id", ""), "sourceLines": source_lines, "bundleStartLine": range_info.get("bundleStartLine"), "bundleEndLine": range_info.get("bundleEndLine"), "rules": len(rules), "selectorParts": selector_parts, "labels": dict(sorted(label_counts.items())), "ownerSurfaces": owners_by_module.get(module, []), "supportRole": str(support_by_module.get(module, {}).get("role", "")), "previousModule": imports[index - 1]["module"] if index > 0 else None, "nextModule": imports[index + 1]["module"] if index + 1 < len(imports) else None, } modules.append(record) totals["sourceLines"] += source_lines totals["rules"] += len(rules) totals["selectorParts"] += selector_parts for label, count in label_counts.items(): totals[f"label:{label}"] += count data = { "schema": "owen-graphite/source-usage-map/1", "moduleCount": len(modules), "sourceLines": totals["sourceLines"], "rules": totals["rules"], "selectorParts": totals["selectorParts"], "labelTotals": {key.removeprefix("label:"): value for key, value in sorted(totals.items()) if key.startswith("label:")}, "hardViolations": hard_violations, "modules": modules, "tableRules": table_rules, "tableSelectorIndex": table_selector_index(), "ownerRegistryGaps": [ { "module": module["module"], "classification": classify_support_module(module, support_by_module), "labels": module["labels"], } for module in modules if not module["ownerSurfaces"] and module["module"] not in support_by_module ], "supportModules": [ { "module": module["module"], "role": module["supportRole"], "labels": module["labels"], } for module in modules if module["module"] in support_by_module ], "riskContractGaps": [ { "surface": surface_id, "description": surface.get("description", ""), "ownerModules": surface.get("ownerModules", []), } for surface_id, surface in surfaces.items() if not surface.get("riskContracts") ], "surfaces": surfaces, } return data, render_markdown(data) def render_markdown(data: dict[str, object]) -> str: modules = data["modules"] label_totals = data["labelTotals"] unregistered = [module for module in modules if not module["ownerSurfaces"] and not module.get("supportRole")] lines: list[str] = [] lines.append("# Owen Graphite Source Usage Map") lines.append("") lines.append("Generated from `src/entry.css`, `dev/WIKI/MAP/owner-registry.json`, and `dev/WIKI/MAP/effective-source-map.json`.") lines.append("") lines.append("Canonical WIKI location: `dev/WIKI/MAP/source-usage-map.md`. Machine provenance remains in `dev/WIKI/MAP`.") lines.append("") lines.append("## Summary") lines.append("") lines.append(f"- Source modules: {data['moduleCount']}") lines.append(f"- Source CSS lines: {data['sourceLines']}") lines.append(f"- Parsed CSS rules: {data['rules']}") lines.append(f"- Selector parts: {data['selectorParts']}") lines.append(f"- Hard core-owner violations: {len(data['hardViolations'])}") lines.append("") lines.append("## Surface Totals") lines.append("") for label, count in sorted(label_totals.items()): lines.append(f"- `{label}`: {count} rules") lines.append("") lines.append("## Quick Routing") lines.append("") lines.append("Use this table before editing. Start at the owner module, then inspect allowed-late modules only when the owner registry explicitly allows them.") lines.append("") lines.append("| Work Area | Start Here | Allowed Follow-Up | Must Check |") lines.append("| --- | --- | --- | --- |") lines.append("| Reading typography, links, headings | `src/base/12-reading-content.css` | theme overrides in `src/themes/50-dark.css` when dark-only | `reading-typography` owner surface |") lines.append("| Reading/rendered tables and code | `src/surfaces/20-reading-tables-code.css` | `src/features/42-report-print-polish.css` for print/report only | `Table Selector Rules` below |") lines.append("| Live Preview CM6 line geometry | `src/base/13-live-preview.css` | none | `dev/WIKI/MAP/cm6-hit-routing-contract.md` |") lines.append("| Live Preview markdown table widget | Obsidian core, no theme geometry owner | none | Do not style `.cm-table-widget` or `table.cm-table` |") lines.append("| Live Preview HTML table embed | `src/base/13-live-preview.css`, `src/surfaces/24-html-table-live-preview-glass.css` | `src/features/42-report-print-polish.css` utilities only | Must include `:not(.cm-table-widget)` and `:not(.cm-table)` |") lines.append("| PDF header/footer marginalia | `src/features/41-feature-presets.css`, `src/tokens/00-light-tokens.css`, `src/tokens/01-dark-tokens.css` | none | `dev/WIKI/MAP/pdf-header-footer-contract.md` |") lines.append("| Workspace chrome | `src/chrome/30-workspace.css`, `src/chrome/31-navigation-tasks-search.css`, `src/chrome/34-nav-ribbon-glass.css`, `src/chrome/37-tabs-file-explorer-search.css` | none | `dev/WIKI/MAP/top-chrome-icon-background-contract.md` |") lines.append("| Menus, popovers, search, modals | `src/chrome/32-overlay-popover-dataview.css`, `src/chrome/35-editing-menu-tooltip-glass.css`, `src/chrome/36-floating-ui-glass-system.css` | none | overlay/search selectors in this map |") lines.append("") lines.append("## Core Principle Workflow") lines.append("") lines.append("Before editing CSS:") lines.append("") lines.append("1. Identify the target surface in `Quick Routing`.") lines.append("2. Open the owner module first; do not start from a later visual module.") lines.append("3. Read linked risk contracts when the target is CM6, table, PDF, or top chrome.") lines.append("4. Remove or merge conflicting follow-up rules instead of adding a new override.") lines.append("5. Run `build_source_usage_map.py --check` and `audit_core_principles.py` before commit.") lines.append("") lines.append("Forbidden workflow:") lines.append("") lines.append("- Adding a new late fix because the owner was hard to locate.") lines.append("- Styling Obsidian-owned markdown table widget geometry.") lines.append("- Reintroducing `src/polish/*` or `!important`.") lines.append("- Treating allowed-late modules as broad ownership permission.") lines.append("") lines.append("## Known Failure Modes") lines.append("") lines.append("These are real failure patterns that should stop work until the owner and runtime evidence are clear.") lines.append("") lines.append("| Failure Mode | Symptom | Root Risk | Correct Response |") lines.append("| --- | --- | --- | --- |") lines.append("| Broad CM6 editor selector | Nested editors inside widgets inherit page-level geometry | `.cm-content`, `.cm-line`, `.cm-editor`, or `.cm-scroller` selectors can hit embedded editors | Narrow the owner selector or inspect runtime DOM before editing |") lines.append("| Markdown table widget styled as theme table | Selecting a markdown table cell changes row geometry or hit routing | `.cm-table-widget` / `table.cm-table` belongs to Obsidian core | Do not style widget geometry; target rendered tables or HTML embeds only |") lines.append("| Rendered table and LP widget grouped together | A table fix works in Reading View but breaks Live Preview editing | Selector group mixes `.markdown-rendered table` with source-mode widgets | Split by surface and owner before changing properties |") lines.append("| Late visual module used as repair layer | A fix only works because it wins late in cascade | Owner module remains wrong and future edits become unpredictable | Move the rule to the owner and remove the late correction |") lines.append("| Sync/debug confusion | CSS appears unchanged after a fix | Vault path, theme cache, or runtime DOM not verified | Compare repo/vault CSS and capture computed styles before more edits |") lines.append("| Inline/runtime height | CSS changes do not affect selected row height | Obsidian runtime may set inline style or non-theme DOM state | Use Runtime Debug Protocol; do not add stronger CSS blindly |") lines.append("") lines.append("## Runtime Debug Protocol") lines.append("") lines.append("See `dev/WIKI/runtime-debug-protocol.md` and the snippets in `dev/WIKI/runtime-debug-snippets/`.") lines.append("") lines.append("## If You Touch X") lines.append("") lines.append("| Selector Or Feature | Owner First | Also Inspect | Never Do |") lines.append("| --- | --- | --- | --- |") lines.append("| `.markdown-rendered table`, `.markdown-preview-view table` | `src/surfaces/20-reading-tables-code.css` | `src/features/42-report-print-polish.css`, `src/surfaces/23-liquid-glass-core.css` | Mix with `.cm-table-widget` |") lines.append("| `.cm-table-widget`, `table.cm-table` | Obsidian core | runtime DOM, `cm6-hit-routing-contract.md` | Add theme geometry or visual table styling |") lines.append("| LP HTML `` embed | `src/base/13-live-preview.css`, `src/surfaces/24-html-table-live-preview-glass.css` | `src/features/42-report-print-polish.css` utilities | Omit `:not(.cm-table-widget):not(.cm-table)` |") lines.append("| `.cm-line`, `.HyperMD-*` | `src/base/13-live-preview.css` | `cm6-hit-routing-contract.md` | Add vertical margin/padding to hit-routed lines |") lines.append("| Callouts and blockquotes | `src/surfaces/21-reading-callouts-lists.css` | `src/base/13-live-preview.css`, `src/surfaces/23-liquid-glass-core.css` | Add left rails or late visual repair without owner edit |") lines.append("| Code blocks | `src/surfaces/20-reading-tables-code.css` | `src/base/13-live-preview.css`, `src/features/42-report-print-polish.css` | Split LP/Reading/PDF parity without checking map |") lines.append("| `ogd-pdf-header-*`, `ogd-pdf-footer-*` | `src/features/41-feature-presets.css` | token files, `pdf-header-footer-contract.md` | Put header/footer owner rules in `42-report-print-polish` |") lines.append("| Top tabs/ribbon/sidebar icons | `src/chrome/34-nav-ribbon-glass.css`, `src/chrome/37-tabs-file-explorer-search.css` | `top-chrome-icon-background-contract.md` | Add broad top-chrome selectors in overlay modules |") lines.append("| Menus, suggestions, modals, popovers | `src/chrome/32-overlay-popover-dataview.css`, `src/chrome/35-editing-menu-tooltip-glass.css`, `src/chrome/36-floating-ui-glass-system.css` | accessibility/motion module | Treat overlay state as workspace chrome owner |") lines.append("") lines.append("## Audit Coverage Matrix") lines.append("") lines.append("| Audit | Catches | Does Not Catch |") lines.append("| --- | --- | --- |") lines.append("| `build_source_usage_map.py --check` | Stale source map, missing generated overview | Runtime visual regressions |") lines.append("| `audit_core_principles.py` | Missing map, `src/polish`, `!important`, owner registry drift, core guard, selector ownership, hit routing | Inline Obsidian runtime styles |") lines.append("| `audit_direct_owner_guard.py` | `.cm-table-widget` / `table.cm-table` direct styling, generic LP table selectors, PDF header/footer owner drift | Semantic owner disputes outside encoded rules |") lines.append("| `audit_lp_pdf_selector_ownership.py` | LP/PDF selector ownership distribution | Whether a visual change looks correct |") lines.append("| `audit_v3_hit_routing.py` | Known CM6 hit-routing hazards | Rendered Reading View table aesthetics |") lines.append("| `audit_pdf_header_footer.py` | PDF marginalia owner contract | Non-marginalia PDF typography |") lines.append("| `v3_audit_duplicate_selectors.py` | In-file duplicate and cross-file selector groups | Whether duplicate intent is valid |") lines.append("| Runtime DevTools protocol | Actual selected/hover/focus DOM and computed style | Static owner drift unless matched rule is mapped back |") lines.append("") lines.append("## Audit Blind Spot Follow-Ups") lines.append("") lines.append("| Blind Spot | Required Follow-Up |") lines.append("| --- | --- |") lines.append("| Runtime inline style | Run `matched-rules-dump.js`; inspect `style` before adding CSS |") lines.append("| Obsidian internal DOM change | Capture DOM chain and compare with core contracts |") lines.append("| Vault plugin/snippet influence | Inspect `.obsidian/appearance.json`, enabled snippets, and plugin CSS/JS |") lines.append("| Visual-only layout shift | Create or update a fixture/screenshot audit before changing CSS |") lines.append("| OS/version-specific behavior | Record OS, Obsidian version, theme path, and runtime state |") lines.append("") lines.append("## Change Type Checklists") lines.append("") for change_type, steps in CHANGE_TYPE_COMMANDS.items(): lines.append(f"### {change_type}") lines.append("") for step in steps: lines.append(f"- {step}") lines.append("") lines.append("") lines.append("## Cascade And Ownership Map") lines.append("") lines.append("| # | Module | Bundle Lines | Source Lines | Primary Owners | Major Labels | Cascade Relation |") lines.append("| ---: | --- | --- | ---: | --- | --- | --- |") for module in modules: bundle = f"{module['bundleStartLine']}-{module['bundleEndLine']}" if module.get("bundleStartLine") else "-" owners = ", ".join(module["ownerSurfaces"]) if not owners: owners = f"support: {module['supportRole']}" if module.get("supportRole") else "unregistered/support" labels = ", ".join(f"{label}:{count}" for label, count in module["labels"].items()) or "metadata/tokens" relation = f"after `{module['previousModule']}`; before `{module['nextModule']}`" lines.append(f"| {module['index']} | `{module['module']}` | {bundle} | {module['sourceLines']} | {owners} | {labels} | {relation} |") lines.append("") lines.append("## Table Code Map") lines.append("") lines.append("Table-related rules are intentionally split by surface:") lines.append("") lines.append("- Reading/rendered tables: `src/surfaces/20-reading-tables-code.css`; report/print extensions in `src/features/42-report-print-polish.css`.") lines.append("- Live Preview markdown table widgets: Obsidian core-owned; theme CSS must not style `.cm-table-widget` / `table.cm-table` geometry.") lines.append("- Live Preview HTML table embeds: `src/base/13-live-preview.css`, `src/surfaces/24-html-table-live-preview-glass.css`, and utility hooks in `src/features/42-report-print-polish.css`.") lines.append("- Late visual table surface: `src/surfaces/23-liquid-glass-core.css` for rendered/non-core surfaces only.") lines.append("") lines.append("| Module | Table Rules | LP HTML Table Rules | Reading/Rendered Rules | Print/PDF Rules |") lines.append("| --- | ---: | ---: | ---: | ---: |") for module in modules: labels = module["labels"] table_count = labels.get("table", 0) if not table_count: continue lines.append( f"| `{module['module']}` | {table_count} | {labels.get('lp-html-table', 0)} | {labels.get('reading-rendered', 0)} | {labels.get('print-pdf', 0)} |" ) lines.append("") lines.append("## Table Selector Rules") lines.append("") lines.append("Allowed:") lines.append("") lines.append("- Rendered tables in `src/surfaces/20-reading-tables-code.css`: `:is(.markdown-rendered, .markdown-preview-view, .markdown-reading-view) table ...`") lines.append("- Print/report table extensions in `src/features/42-report-print-polish.css` when the rule is print/report scoped.") lines.append("- Live Preview HTML table embeds with both guards: `.markdown-source-view.mod-cm6 ... table:not(.cm-table):not(.cm-table-widget)`.") lines.append("") lines.append("Forbidden:") lines.append("") lines.append("- `.markdown-source-view.mod-cm6 .cm-table-widget ...`") lines.append("- `.markdown-source-view.mod-cm6 table.cm-table ...`") lines.append("- `.HyperMD-table-row ... td/th/tr/table/.table-cell-wrapper` geometry rules.") lines.append("- Generic `.markdown-source-view.mod-cm6 ... table` without both `:not(.cm-table-widget)` and `:not(.cm-table)`.") lines.append("") lines.append("## Table Selector Reverse Index") lines.append("") lines.append("| Pattern | Owner | Purpose | Status |") lines.append("| --- | --- | --- | --- |") for item in data["tableSelectorIndex"]: lines.append(f"| `{item['pattern']}` | {item['owner']} | {item['purpose']} | {item['status']} |") lines.append("") lines.append("## Risk Contracts") lines.append("") lines.append("| Contract | Applies To | When To Read |") lines.append("| --- | --- | --- |") lines.append("| `dev/WIKI/MAP/cm6-hit-routing-contract.md` | CM6 line geometry, Live Preview widgets | Before editing `src/base/13-live-preview.css` or LP widgets |") lines.append("| `dev/WIKI/MAP/live-preview-pdf-css-map/parity-guidelines.md` | LP/PDF parity, tables, code, callouts | Before changing table/code/callout behavior across LP/Reading/PDF |") lines.append("| `dev/WIKI/MAP/pdf-header-footer-contract.md` | PDF marginalia | Before touching `ogd-pdf-header-*` or `ogd-pdf-footer-*` |") lines.append("| `dev/WIKI/MAP/top-chrome-icon-background-contract.md` | Top chrome/ribbon icon surfaces | Before touching titlebar, tabs, ribbon icons |") lines.append("") lines.append("## Risk Contract Coverage Gaps") lines.append("") if data["riskContractGaps"]: lines.append("Surfaces without explicit `riskContracts` in `owner-registry.json`:") lines.append("") for item in data["riskContractGaps"]: lines.append(f"- `{item['surface']}`: {item['description']} Owners: {', '.join(item['ownerModules'])}") else: lines.append("All owner surfaces have at least one risk contract.") lines.append("") lines.append("## Allowed-Late Does Not Mean New Owner") lines.append("") lines.append("`allowed-late` modules exist to preserve validated cascade order or print/report closure. They do not authorize new broad fixes. If a behavior has a clear owner, edit the owner first and use allowed-late modules only for their registered surface.") lines.append("") if data.get("supportModules"): lines.append("## Registered Support Modules") lines.append("") lines.append("These modules have explicit support roles in `owner-registry.json`. They are not primary owners and must not be used as repair layers.") lines.append("") for item in data["supportModules"]: lines.append(f"- `{item['module']}`: {item['role']}; labels {item['labels'] or {}}") lines.append("") if unregistered: lines.append("## Unregistered/Support Modules") lines.append("") lines.append("These modules are not primary owners in `owner-registry.json`. They may be valid support modules, but new ownership should not be inferred from their presence without updating the registry.") lines.append("") for item in data["ownerRegistryGaps"]: lines.append(f"- `{item['module']}`: {item['classification']}; labels {item['labels'] or {}}") lines.append("") lines.append("## Related Maps And Artifacts") lines.append("") lines.append("- `dev/WIKI/MAP/theme-css-risk-map.html`: visual HTML risk map for selector density and risk review.") lines.append("- `dev/WIKI/MAP/theme-css-risk-map.json`: machine-readable version of the risk map.") lines.append("- `dev/WIKI/MAP/selector-provenance.json`: source selector provenance data.") lines.append("- `dev/WIKI/MAP/unused-css-candidates.md`: unused/reserved selector analysis.") lines.append("- `dev/WIKI/MAP/effective-source-map.json`: bundle line to source module mapping.") lines.append("") lines.append("## Recent Incident Notes") lines.append("") lines.append("| Incident | Wrong Approach | Correct Process | Gate |") lines.append("| --- | --- | --- | --- |") lines.append("| Table row expands when a cell is selected | Repeated selector guesses and late geometry resets | Capture runtime DOM/computed style, map matched rule to owner, edit owner only | `audit_core_principles.py`, runtime debug protocol |") lines.append("| Table code hard to locate | Searching ad hoc selector fragments | Use `Table Selector Reverse Index`, `How To Find Table Code Quickly`, and source usage JSON | `build_source_usage_map.py --check` |") lines.append("") lines.append("## How To Find Table Code Quickly") lines.append("") lines.append("```powershell") lines.append(".\\.venv\\Scripts\\python.exe dev\\scripts\\build_source_usage_map.py --check") lines.append("Select-String -Path src\\**\\*.css -Pattern 'table|td|th|tr|caption|cm-table-widget|table\\.cm-table|HyperMD-table-row'") lines.append(".\\.venv\\Scripts\\python.exe dev\\scripts\\audit_direct_owner_guard.py") lines.append(".\\.venv\\Scripts\\python.exe dev\\scripts\\audit_lp_pdf_selector_ownership.py") lines.append("```") lines.append("") lines.append("## Core Principle Status") lines.append("") if data["hardViolations"]: lines.append("Hard violations detected:") for item in data["hardViolations"]: lines.append(f"- `{item['module']}` line {item['line']}: `{item['selector']}`") else: lines.append("No hard core-owner selector violations were detected by this map builder.") lines.append("") lines.append("This map is descriptive. It does not replace `audit_direct_owner_guard.py`, `audit_v3_hit_routing.py`, `audit_lp_pdf_selector_ownership.py`, or `release_check.py`.") lines.append("") return "\n".join(lines) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--check", action="store_true", help="Fail if generated map artifacts are stale.") args = parser.parse_args() data, markdown = build() json_text = json.dumps(data, ensure_ascii=False, indent=2) + "\n" markdown_text = markdown + "\n" runtime_text = render_runtime_debug_protocol() + "\n" table_snippet_text = render_table_cell_snippet() matched_snippet_text = render_matched_rules_snippet() if args.check: stale: list[str] = [] if not OUT_JSON.is_file() or OUT_JSON.read_text(encoding="utf-8") != json_text: stale.append(OUT_JSON.relative_to(ROOT).as_posix()) if not OUT_MD.is_file() or OUT_MD.read_text(encoding="utf-8") != markdown_text: stale.append(OUT_MD.relative_to(ROOT).as_posix()) if not OUT_RUNTIME_MD.is_file() or OUT_RUNTIME_MD.read_text(encoding="utf-8") != runtime_text: stale.append(OUT_RUNTIME_MD.relative_to(ROOT).as_posix()) if not TABLE_CELL_SNIPPET.is_file() or TABLE_CELL_SNIPPET.read_text(encoding="utf-8") != table_snippet_text: stale.append(TABLE_CELL_SNIPPET.relative_to(ROOT).as_posix()) if not MATCHED_RULES_SNIPPET.is_file() or MATCHED_RULES_SNIPPET.read_text(encoding="utf-8") != matched_snippet_text: stale.append(MATCHED_RULES_SNIPPET.relative_to(ROOT).as_posix()) if stale: print("FAIL: source usage map stale:") for item in stale: print(f" - {item}") print("Re-run: python dev/scripts/build_source_usage_map.py") return 1 print("OK: source usage map is fresh") return 0 OUT_JSON.write_text(json_text, encoding="utf-8") OUT_MD.write_text(markdown_text, encoding="utf-8") SNIPPET_DIR.mkdir(parents=True, exist_ok=True) OUT_RUNTIME_MD.write_text(runtime_text, encoding="utf-8") TABLE_CELL_SNIPPET.write_text(table_snippet_text, encoding="utf-8") MATCHED_RULES_SNIPPET.write_text(matched_snippet_text, encoding="utf-8") print(f"OK: wrote {OUT_JSON.relative_to(ROOT)}") print(f"OK: wrote {OUT_MD.relative_to(ROOT)}") print(f"OK: wrote {OUT_RUNTIME_MD.relative_to(ROOT)}") print(f"OK: wrote {TABLE_CELL_SNIPPET.relative_to(ROOT)}") print(f"OK: wrote {MATCHED_RULES_SNIPPET.relative_to(ROOT)}") return 0 if __name__ == "__main__": raise SystemExit(main())