Styling compatibility phase 0: preflight contract, staged modes, and the resolved-style IR
Consolidates the three stacked changes (#452, #453, #454) into one PR at the request of review flow — same commits, review follow-ups included. Phase 0 of the styling-compatibility program plus the schema every later phase consumes.
1. Report-only preflight and the applicable-slot matrix
chart.style_compatibility_report(target=..., engine=..., custom_css=...)routes every declared slot style into exactly one ofsurvives,native-subset(naming kept/lost properties per format family),browser-only, orstate-gated— and mirrors the export path's refusals by calling its own resolver and validators, so it cannot disagree with what running the export would do. Constant-time early-out when the chart carries no class/per-slot declarations.- Applicable-slot matrix: each of the 48 chrome slots now carries an applicability — clean-static (24) or gated by
hover/modebar/crosshair/selection/view(24). A styled tooltip is no longer "dropped" by a PNG that never contains a tooltip; it's recorded with its gating state (§28). Registry tests pin the partition and reject native-support claims on state-gated slots.
2. Staged compatibility= modes and the engine contract
Every chart-, figure-, and module-level image export accepts compatibility= (facet grids deliberately excluded until their per-panel preflight): "legacy" (default, byte-identical, one string comparison of cost), "warn" (one StyleCompatibilityWarning naming each loss), "strict" (StyleCompatibilityError before emission, report attached). "lossless" is reserved-and-rejected until preflight routing exists. No mode ever re-routes an explicit engine; resolution errors precede and outrank mode logic in every mode; strict batches fail whole before any file is written. spec/process/style-compatibility-migration.md names the default flips: warn 0.0.7 → strict 0.1.0 → legacy removed 0.2.0.
3. The ResolvedStyleSnapshot (schema v1)
The renderer-neutral IR: interned declarations + identity-carrying instances, concrete values only (no var()/calc(), no relative units — enforced at both construction ends of the eventual wire), closed per-version vocabulary, canonical byte ordering so identical styling serializes identically. Generated TypeScript mirror (js/src/14_style_snapshot.ts) with a --check drift gate. Nothing rides the wire yet: PROTOCOL_VERSION stays 12; wire-protocol §8 documents the shape and reserves the message names.
Review follow-ups already included
From the bot passes on the stacked PRs — fixed: browser-path custom_css validation mirrored (_custom_css_block); malformed figure styling raises like the export instead of being skipped; summary counts derived from the registry; explicit writer-family classification; typed report returns; to_png resolution-error precedence for styled charts (test now feeds the lossy case); measured warning stacklevel so warnings land on the caller's line; SVG-aware strict remediation (no recommending an engine SVG rejects); batch vocabulary validated up front with HTML entries exempt; changelog facet wording; relative units rejected anywhere in a value (translate(50%, 20%), 2em 1em, gradient stops); cascade keywords exact-match only ("Inheritance Sans" is concrete); tokens share the full string contract; snapshot_from_payload enforces build()'s validation; canonical declaration/instance ordering with a byte-equality test. Skipped with reason: freezing the report's sources dict (transient value object, never hashed — reviewer tagged it low-value); deriving the family-prefix test from _STATE_GATED_SLOTS (would make the check assert the mapping against itself; independent restatement is the point).
Evidence
Suites at each stage: 3973 → 3987 → 4012 passed plus 244 across the program's own suites after the review fixes; engine × mode matrix runs against live Chromium; node js/build.mjs typechecks the generated mirror; ruff/format/pre-commit clean; xy.export import weight verified unchanged (lazy __getattr__ — the eager alias would have pulled the native dylib into an import chain that never had it). Dense-axis snapshot fixture: 460 instances, 2 declarations, ~38.6 KB against the 50 KB budget.
Next per the plan: the ten native-capable slots routed through the IR behind _compile_cached, gated byte/pixel-equivalent and within-noise.
Summary by CodeRabbit
-
New Features
- Added styling compatibility reports and
legacy,warn, andstrictmodes for exports. - Added live style snapshot capture and reuse for consistent native exports.
- Added native CSS cascade resolution, stylesheet support, and Tailwind core styling.
- Improved static export support for styled chrome, including bold, italic, borders, shadows, and rounded corners.
- Expanded capability reporting for static and interaction-dependent styling.
- Added styling compatibility reports and
-
Documentation
- Updated export guidance, capability matrices, migration notes, and styling protocol documentation.