2026-05-24 11:52:32 +00:00
#!/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 )
2026-05-25 00:05:59 +00:00
def owner_maps ( ) - > tuple [ dict [ str , list [ str ] ] , dict [ str , dict [ str , object ] ] , dict [ str , dict [ str , object ] ] ] :
2026-05-24 11:52:32 +00:00
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) " )
2026-05-25 00:05:59 +00:00
support_by_module = { str ( item [ " module " ] ) : item for item in registry . get ( " supportModules " , [ ] ) }
return dict ( by_module ) , by_surface , support_by_module
2026-05-24 11:52:32 +00:00
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
2026-05-25 00:05:59 +00:00
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 "
2026-05-24 11:52:32 +00:00
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 .
2026-06-02 04:42:15 +00:00
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 .
2026-05-24 11:52:32 +00:00
## 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 ( )
2026-05-25 00:05:59 +00:00
owners_by_module , surfaces , support_by_module = owner_maps ( )
2026-05-24 11:52:32 +00:00
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 , [ ] ) ,
2026-05-25 00:05:59 +00:00
" supportRole " : str ( support_by_module . get ( module , { } ) . get ( " role " , " " ) ) ,
2026-05-24 11:52:32 +00:00
" 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 " ] ,
2026-05-25 00:05:59 +00:00
" classification " : classify_support_module ( module , support_by_module ) ,
2026-05-24 11:52:32 +00:00
" labels " : module [ " labels " ] ,
}
for module in modules
2026-05-25 00:05:59 +00:00
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
2026-05-24 11:52:32 +00:00
] ,
" 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 " ]
2026-05-25 00:05:59 +00:00
unregistered = [ module for module in modules if not module [ " ownerSurfaces " ] and not module . get ( " supportRole " ) ]
2026-05-24 11:52:32 +00:00
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 " - "
2026-05-25 00:05:59 +00:00
owners = " , " . join ( module [ " ownerSurfaces " ] )
if not owners :
owners = f " support: { module [ ' supportRole ' ] } " if module . get ( " supportRole " ) else " unregistered/support "
2026-05-24 11:52:32 +00:00
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 ( " " )
2026-05-25 00:05:59 +00:00
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 ( " " )
2026-05-24 11:52:32 +00:00
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 ( ) )