towishy_Owen-Graphite/dev/scripts/build_source_usage_map.py
2026-06-02 13:42:15 +09:00

748 lines
No EOL
41 KiB
Python

#!/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. If the CDP endpoint is closed while Obsidian is already running, close the existing Obsidian processes and reopen Obsidian with `--remote-debugging-port=9222` before capturing evidence.
3. Run `runtime-debug-snippets/table-cell-dump.js` when the issue is table/cell geometry.
4. Run `runtime-debug-snippets/matched-rules-dump.js` to capture theme/core matched rules.
5. Inspect inline `style` first. Inline geometry means the issue may not be solvable by ordinary owner CSS.
6. If a theme rule is responsible, map the bundle line through `effective-source-map.json` and edit the source owner.
7. If an Obsidian core rule is responsible, do not override it unless an owner contract explicitly permits it.
8. 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 `<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 | 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())