Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a99c037
Add a report-only style-compatibility preflight and the applicable-sl…
Alek99 Aug 4, 2026
cc0fab7
Stage the styling contract: compatibility= modes and the engine colli…
Alek99 Aug 4, 2026
0b97ae5
Land the ResolvedStyleSnapshot: schema v1, interned, with a generated…
Alek99 Aug 4, 2026
f661794
Close the preflight report's two mirror gaps and derive the summary c…
Alek99 Aug 4, 2026
35e2b2f
Merge branch 'alek/compat-1-preflight' into alek/compat-2-modes
Alek99 Aug 4, 2026
10b5030
Review follow-ups: precedence, attribution, remediation, and batch vo…
Alek99 Aug 4, 2026
4915dae
Merge branch 'alek/compat-2-modes' into alek/compat-3-schema
Alek99 Aug 4, 2026
3aa6728
Harden the snapshot's concreteness contract and canonicalize its bytes
Alek99 Aug 4, 2026
4324206
Second review round: legend property-level routing and payload hardening
Alek99 Aug 4, 2026
24fa109
Route the static slots through the ResolvedStyleSnapshot, byte-equiva…
Alek99 Aug 4, 2026
d7c964f
Capture the live cascade and export it natively (wire protocol v13)
Alek99 Aug 4, 2026
4748e24
Update the four version-locked tests for protocol v13
Alek99 Aug 4, 2026
844803d
Resolve classes and author CSS natively: the xy-cascade extension
Alek99 Aug 4, 2026
88c1f03
Static-chrome parity P0: PDF honors the vector text subset, raster ro…
Alek99 Aug 4, 2026
0a5ca92
Static-chrome parity P0.3: the shared box model and its two emitters
Alek99 Aug 4, 2026
2be0b18
Ship the Tailwind-core manifest: utilities export natively, palette v…
Alek99 Aug 4, 2026
a326726
Static-chrome parity P3.0: the raster box emitter reads w/h as extent…
Alek99 Aug 4, 2026
c7c47ff
Ship the cascade crate in the sdist
Alek99 Aug 4, 2026
6c07c88
Static-chrome parity P3.1: annotation_label reaches both writers thro…
Alek99 Aug 4, 2026
6deeebc
Static-chrome parity P3.2: annotation_layer group opacity and plot-cl…
Alek99 Aug 4, 2026
eb28021
Static-chrome parity P1: root, title, chrome and canvas boxes in both…
Alek99 Aug 4, 2026
43d98c7
Static-chrome parity P3.3: the labels container becomes the writers' …
Alek99 Aug 4, 2026
a7865c8
Merge branch 'p1-parity' into alek/style-compat
Alek99 Aug 4, 2026
b140f80
Return the annotation lists as a fixed triple, not a tuple concat
Alek99 Aug 4, 2026
3f43411
Merge remote-tracking branch 'origin/main' into alek/style-compat
Alek99 Aug 4, 2026
31e5f44
Merge main and narrow the two type holes it exposed
Alek99 Aug 4, 2026
800a80f
Static-chrome parity P2: axis chrome boxes, per-property axis text, t…
Alek99 Aug 4, 2026
f6b4798
Merge branch 'p3-parity' into alek/style-compat
Alek99 Aug 4, 2026
00c0854
Merge branch 'p2-parity' into alek/style-compat
Alek99 Aug 5, 2026
34a05dc
Assert transparent-export corners without Pillow
Alek99 Aug 5, 2026
b01de25
Static-chrome parity P4: the legend family, and the em residue retires
Alek99 Aug 5, 2026
012d837
Merge branch 'p4-legend' into alek/style-compat
Alek99 Aug 5, 2026
d4b9ea7
Record the colorbar shared-layout design and the refutations that bou…
Alek99 Aug 5, 2026
a228a65
Merge upstream main into alek/style-compat
FarhanAliRaza Oct 27, 2026
77a2d9d
Remove local validation artifacts from the source tree
FarhanAliRaza Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,009 changes: 1,009 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# Workspace: the core stays the root package; `cascade` is the optional
# style-cascade extension (its own cdylib, loaded lazily and only when a
# native-cascade export is requested — the §33 install budget and the
# import-weight contract are measured with both, but the core never links it).
[workspace]
members = [".", "cascade"]

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.

P2: Adding this workspace root breaks the standalone examples/osm/osmium-rs crate. It is not a member and is not excluded, so cargo build --release run inside it fails with a workspace-membership error. Add it to exclude so the osm ingest build keeps working.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Cargo.toml, line 6:

<comment>Adding this workspace root breaks the standalone `examples/osm/osmium-rs` crate. It is not a member and is not excluded, so `cargo build --release` run inside it fails with a workspace-membership error. Add it to `exclude` so the osm ingest build keeps working.</comment>

<file context>
@@ -1,3 +1,11 @@
+# native-cascade export is requested — the §33 install budget and the
+# import-weight contract are measured with both, but the core never links it).
+[workspace]
+members = [".", "cascade"]
+default-members = [".", "cascade"]
+
</file context>
Suggested change
members = [".", "cascade"]
members = [".", "cascade"]
exclude = ["examples/osm/osmium-rs"]

default-members = [".", "cascade"]

@cubic-dev-ai cubic-dev-ai Bot Aug 4, 2026 •

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.

P2: Building this workspace now compiles the optional cascade crate by default (default-members = [".", "cascade"]), and cascade depends on lightningcss/parcel_selectors, which are neither vendored nor mirrored here. Per the repo's documented sandbox constraint (crates.io unreachable, deps must be vendored or the core can't build/test), a plain cargo build / cargo test will now fail offline even for the core that never links the extension. Consider scoping default-members to just the root package (.) and building xy-cascade explicitly (or vendoring its deps), with the lockfile staying resolvable for the offline build.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Cargo.toml, line 7:

<comment>Building this workspace now compiles the optional `cascade` crate by default (`default-members = [".", "cascade"]`), and `cascade` depends on `lightningcss`/`parcel_selectors`, which are neither vendored nor mirrored here. Per the repo's documented sandbox constraint (crates.io unreachable, deps must be vendored or the core can't build/test), a plain `cargo build` / `cargo test` will now fail offline even for the core that never links the extension. Consider scoping default-members to just the root package (`.`) and building `xy-cascade` explicitly (or vendoring its deps), with the lockfile staying resolvable for the offline build.</comment>

<file context>
@@ -1,3 +1,11 @@
+# import-weight contract are measured with both, but the core never links it).
+[workspace]
+members = [".", "cascade"]
+default-members = [".", "cascade"]
+
 [package]
</file context>
Suggested change
default-members = [".", "cascade"]
default-members = ["."]
Fix with cubic

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.

With cascade in default-members, every plain cargo test / cargo build --release (the CLAUDE.md core commands) now compiles lightningcss + parcel_selectors (~1000 new Cargo.lock lines; a clean release build here took about 2 minutes). CLAUDE.md asks that dependencies be minimized, feature-gated where possible, and vendored, because crates.io is unreachable from the dev sandbox. It also documents the Rust layout as src/ only.

Required change: either remove cascade from default-members and build it explicitly in hatch_build.py/CI, or keep it and update CLAUDE.md's Layout/Commands sections to describe the cascade/ crate, its dependency tree, and how to build without it offline.


Generated by Claude Code


[package]
name = "xy-core"
version = "0.1.0"
Expand Down
20 changes: 20 additions & 0 deletions cascade/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "xy-cascade"
version = "0.1.0"
edition = "2021"
description = "Optional mount-free CSS cascade for xy: Lightning CSS parsing plus a profile-scoped resolver over the synthetic chart DOM"
license = "Apache-2.0"

[lib]
name = "xy_cascade"
crate-type = ["cdylib"]

# Lightning CSS does the real parsing/normalization; the matcher/cascade over
# the synthetic slot DOM is profile-scoped and lives here. Everything outside
# the published profile is reported, never guessed (§28). Pinned exactly:
# the resolver's output is part of a deterministic export path.
[dependencies]
lightningcss = "=1.0.0-alpha.72"
parcel_selectors = "=0.28.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
78 changes: 78 additions & 0 deletions cascade/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! Mount-free CSS cascade for xy (wire-protocol §8, migration doc "native
//! cascade"): Lightning CSS parses and normalizes author stylesheets; this
//! crate matches them against the synthetic chart DOM and cascades to
//! concrete per-node declarations — the values the ResolvedStyleSnapshot
//! carries and the native writers consume.
//!
//! Profile-scoped on purpose. The published profile is: style rules;
//! class / `[data-xy-slot=…]` attribute / universal / `:root` simple
//! selectors with descendant and child combinators; `@media
//! (prefers-color-scheme: …)`; custom properties with `var()` fallbacks;
//! `em`/`rem` lengths (resolved against the font-size cascade); the
//! inherited text properties. Everything else — other at-rules, pseudo
//! classes/elements, sibling combinators, percentage lengths — lands in the
//! reply's `unsupported` list with the reason, never in a guessed value
//! (§28). The Python side surfaces that list through the preflight.
//!
//! C ABI: JSON in, JSON out, one free function. The boundary stays this
//! narrow so the extension is loadable lazily and replaceable wholesale
//! (the plan's Stylo endgame swaps the resolver, not the contract).

mod resolve;

use std::panic::{catch_unwind, AssertUnwindSafe};

pub const CASCADE_ABI_VERSION: u32 = 1;

#[no_mangle]
pub extern "C" fn xy_cascade_abi_version() -> u32 {
CASCADE_ABI_VERSION
}

/// Resolve `css` against the synthetic document `doc` (JSON; see
/// `resolve::Document`). Writes a malloc'd JSON reply to `out`/`out_len`;
/// returns 0 on success, 1 when the reply is `{"error": …}`, 2 on a caught
/// panic (reply is a static error JSON). The caller frees the reply with
/// `xy_cascade_free` in every case.
///
/// # Safety
/// `css_ptr`/`doc_ptr` must reference `css_len`/`doc_len` readable bytes;
/// `out`/`out_len` must be writable.
#[no_mangle]
pub unsafe extern "C" fn xy_cascade_resolve(
css_ptr: *const u8,
css_len: usize,
doc_ptr: *const u8,
doc_len: usize,
out: *mut *mut u8,
out_len: *mut usize,
) -> i32 {
let css = std::slice::from_raw_parts(css_ptr, css_len);

@cubic-dev-ai cubic-dev-ai Bot Aug 4, 2026 •

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.

P2: Invalid FFI arguments can crash the process instead of returning the documented JSON error reply. xy_cascade_resolve dereferences raw pointers without guards, so adding explicit pointer validation (null allowed only when len==0, plus non-null out/out_len) would make the ABI boundary robust like the rest of the native entrypoints.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cascade/src/lib.rs, line 50:

<comment>Invalid FFI arguments can crash the process instead of returning the documented JSON error reply. `xy_cascade_resolve` dereferences raw pointers without guards, so adding explicit pointer validation (null allowed only when len==0, plus non-null `out`/`out_len`) would make the ABI boundary robust like the rest of the native entrypoints.</comment>

<file context>
@@ -0,0 +1,78 @@
+    out: *mut *mut u8,
+    out_len: *mut usize,
+) -> i32 {
+    let css = std::slice::from_raw_parts(css_ptr, css_len);
+    let doc = std::slice::from_raw_parts(doc_ptr, doc_len);
+    let result = catch_unwind(AssertUnwindSafe(|| resolve::resolve_json(css, doc)));
</file context>
Fix with cubic

let doc = std::slice::from_raw_parts(doc_ptr, doc_len);
let result = catch_unwind(AssertUnwindSafe(|| resolve::resolve_json(css, doc)));
let (code, payload) = match result {
Ok(Ok(json)) => (0, json),
Ok(Err(message)) => (
1,
serde_json::to_string(&serde_json::json!({ "error": message }))
.unwrap_or_else(|_| "{\"error\":\"unserializable error\"}".to_string()),
),
Err(_) => (2, "{\"error\":\"panic in xy_cascade_resolve\"}".to_string()),
};
let boxed = payload.into_bytes().into_boxed_slice();
let len = boxed.len();
*out = Box::into_raw(boxed) as *mut u8;
*out_len = len;
code
}

/// Free a reply produced by `xy_cascade_resolve`.
///
/// # Safety
/// `ptr`/`len` must be exactly a pair returned through `out`/`out_len`.
#[no_mangle]
pub unsafe extern "C" fn xy_cascade_free(ptr: *mut u8, len: usize) {
if !ptr.is_null() {
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)));
}
}
Loading
Loading