Skip to content

Land the ResolvedStyleSnapshot: schema v1, interned, with a generated TS mirror - #454

Closed
Alek99 wants to merge 3 commits into
alek/compat-2-modesfrom
alek/compat-3-schema
Closed

Land the ResolvedStyleSnapshot: schema v1, interned, with a generated TS mirror#454
Alek99 wants to merge 3 commits into
alek/compat-2-modesfrom
alek/compat-3-schema

Conversation

@Alek99

@Alek99 Alek99 commented Aug 4, 2026

Copy link
Copy Markdown
Member

Stacked on #453 (compatibility modes), which stacks on #452 (preflight) — third change of the styling-compatibility program, and the schema every later phase consumes (slot migration, browser capture, native cascade).

What this adds

python/xy/styling/resolved.py — the renderer-neutral styling IR — plus its generated TypeScript mirror js/src/14_style_snapshot.ts (scripts/gen_style_snapshot_types.py, with --check wired into the suite so the two languages cannot drift; the gen_capability_matrix.py contract applied to types).

Three contract properties, enforced at construction on both ends of the eventual wire:

  • Concrete values only. No var()/calc()/env()/inherit, no relative units (em, %, vw, …). A value that still depends on a cascade — or on metrics the consumer would re-derive — is rejected with the reason (§28). One smuggled unresolved value would re-create inside the IR exactly the per-renderer divergence the IR exists to end.
  • Interned declarations. Each distinct declaration is stored once; instances reference it by index carrying only identity qualifiers (["y","major","3"]), resolved geometry, and content. Interning is canonicalized (insertion-order independent). The dense-axis fixture — 460 instances, 2 declarations — serializes to ~38.6 KB against the spec's 50 KB budget, asserted with the headroom stated so eating it is a decision, not drift.
  • Closed vocabulary per version. Schema v1's property groups (paint / typography / layout / effects) are generated constants in both languages; growing the vocabulary is a STYLE_SNAPSHOT_VERSION bump. snapshot_from_payload refuses unknown versions rather than guessing — a snapshot is cacheable and supplyable across builds, so version humility is load-bearing.

Protocol posture

Nothing rides the wire yet: PROTOCOL_VERSION stays at 12. spec/design/wire-protocol.md gains §8 documenting the payload shape and reserving style_snapshot_request / style_snapshot for the capture change, which bumps the protocol and carries this schema as its reply. The client build typechecks the mirror; nothing imports it yet.

Evidence

25 new schema tests (interning, order-independence, concreteness rejections with reasons, wire round-trip, malformed-payload refusals, mirror freshness). Full suite: 4012 passed, 109 skipped. node js/build.mjs typechecks + builds clean; ruff/format/pre-commit clean.

Next in the stack: the existing ten native-capable slots routed through this IR behind the _compile_cached style cache, gated byte/pixel-equivalent.

Review in cubic

… TS mirror

python/xy/styling/resolved.py defines the renderer-neutral styling IR the
compatibility program converges on: authored styling arrives from five
mechanisms and (soon) two resolvers, and renderers should consume exactly
one shape regardless of where it came from. Three contract properties, each
enforced at construction on both ends of the eventual wire:

Concrete values only. No var()/calc()/env()/inherit and no relative units:
a value that still depends on a cascade, or on metrics the consumer would
have to re-derive, is rejected loudly with the reason (§28). One unresolved
value smuggled into the IR would re-create exactly the per-renderer
divergence the IR exists to end.

Interned declarations. A snapshot stores each distinct declaration once;
instances reference it by index and carry only identity qualifiers
(e.g. ["y","major","3"]), resolved geometry, and content. Interning is
canonicalized, so a builder fed the same styling in any order emits the
same snapshot. The dense-axis fixture (400 tick labels + 60 legend rows =
460 instances, 2 declarations) serializes to ~38.6 KB against the spec's
50 KB uncompressed budget, and the test states the headroom so eating it
is a visible decision.

Closed vocabulary per version. Schema v1's property list (paint,
typography, layout, effects) is a generated constant in both languages;
growing it is a STYLE_SNAPSHOT_VERSION bump, so a snapshot's vocabulary is
always recoverable from its version field. snapshot_from_payload refuses
versions it does not know rather than guessing.

js/src/14_style_snapshot.ts is the TypeScript mirror, rendered by
scripts/gen_style_snapshot_types.py from the Python module — one schema,
two languages, and the suite runs --check so the committed mirror cannot
drift (the gen_capability_matrix.py contract, applied to types). The
client build typechecks it; nothing imports it yet.

Nothing rides the wire in this change, so PROTOCOL_VERSION stays at 12.
wire-protocol.md gains §8 documenting the payload shape and reserving the
style_snapshot_request / style_snapshot message names for the capture
change, which bumps the protocol and carries this schema as its reply.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a47f88d5-3e93-4cae-855d-f0abb9cc03f7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown

Greptile Summary

The PR introduces schema v1 of an interned, renderer-neutral resolved-style snapshot and generates its TypeScript mirror.

  • Defines the Python snapshot model, canonical declaration interning, concrete-value checks, and payload serialization/deserialization.
  • Adds generated TypeScript schema declarations and a freshness check.
  • Documents the reserved wire shape and adds contract, round-trip, canonicalization, and size-budget tests.

Confidence Score: 4/5

The PR does not yet appear safe to merge because malformed payload state metadata remains accepted and silently changed instead of being rejected.

The token-concreteness issue is fixed, and declaration, environment, and token payload checks were added, but snapshot_from_payload still converts arbitrary states and unrepresentable elements with str(), leaving the previously reported payload-validation contract incomplete.

Files Needing Attention: python/xy/styling/resolved.py, tests/test_resolved_style_snapshot.py

Important Files Changed

Filename Overview
python/xy/styling/resolved.py Adds the snapshot schema, canonical builder, validation, and decoder; token validation is repaired, but payload state metadata is still silently coerced instead of rejected.
tests/test_resolved_style_snapshot.py Thoroughly covers interning, concrete declarations and tokens, malformed core payload fields, round trips, and generator freshness, but omits non-string state metadata.
js/src/14_style_snapshot.ts Adds the generated TypeScript mirror with string-array metadata and the versioned property vocabulary.
scripts/gen_style_snapshot_types.py Generates and checks the committed TypeScript mirror from the Python schema.
spec/design/wire-protocol.md Documents schema v1 and reserves future transport message names without changing the active protocol.

Reviews (2): Last reviewed commit: "Harden the snapshot's concreteness contr..." | Re-trigger Greptile

Comment thread python/xy/styling/resolved.py Outdated
Comment thread python/xy/styling/resolved.py Outdated
@codspeed-hq

codspeed-hq Bot commented Aug 4, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 109 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing alek/compat-3-schema (3aa6728) with alek/compat-2-modes (10b5030)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread python/xy/styling/resolved.py Outdated
Comment thread python/xy/styling/resolved.py Outdated
Comment thread python/xy/styling/resolved.py Outdated
Comment thread python/xy/styling/resolved.py Outdated
Comment thread python/xy/styling/resolved.py
Comment thread python/xy/styling/resolved.py Outdated
Alek99 added 2 commits August 4, 2026 13:52
Review follow-ups on the schema, all four in the same direction: the
contract the module claims is now the contract it enforces.

Relative units are rejected anywhere in a value, not only as a whole-string
suffix. translate(50%, 20%), a "2em 1em" shorthand, and a gradient stop at
50% all carry the same document dependency a bare "1.5em" does; the
end-anchored check let exactly those through. Cascade keywords went the
other way: inherit/unset (plus initial/revert/revert-layer) reject only as
the entire normalized value, so a face named "Inheritance Sans" is no
longer refused for containing the letters.

Tokens share the full string contract with declarations through one helper
— a token "1.5em" or an empty string smuggles what a declaration would —
and snapshot_from_payload now enforces the identical rules build() does:
environment vocabulary and finiteness, and every token through the shared
validator. The payload path is the untrusted end of the wire; a snapshot
that could only exist by bypassing the builder must not become
renderer-facing IR by arriving serialized.

build() now emits canonical bytes: declaration slots assigned by content
(instance indices remapped), instances sorted by identity. The docstring
claimed order-independence while the payload depended on insertion order,
and the old test compared declaration sets, which cannot see index drift —
it now asserts byte-equal payloads from builders fed the same styling in
different orders. Instance order carries no meaning; identity lives in
(slot, qualifiers), which is what makes a snapshot cacheable across
producers.
@Alek99

Alek99 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Consolidated into #455 (single-PR review requested) — same commits including the review follow-ups; the finding dispositions are listed in #455's description.

@Alek99 Alek99 closed this Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant