diff --git a/.github/workflows/browser-output-certification.yml b/.github/workflows/browser-output-certification.yml new file mode 100644 index 0000000..28b3a1e --- /dev/null +++ b/.github/workflows/browser-output-certification.yml @@ -0,0 +1,111 @@ +name: Browser output certification + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + host: + name: ${{ matrix.host }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + include: + - host: windows-x64-chromium + runner: windows-latest + browser: chromium + - host: windows-x64-firefox + runner: windows-latest + browser: firefox + - host: windows-x64-webkit + runner: windows-latest + browser: webkit + - host: linux-x64-chromium + runner: ubuntu-24.04 + browser: chromium + - host: linux-x64-firefox + runner: ubuntu-24.04 + browser: firefox + - host: linux-x64-webkit + runner: ubuntu-24.04 + browser: webkit + - host: macos-arm64-chromium + runner: macos-15 + browser: chromium + - host: macos-arm64-firefox + runner: macos-15 + browser: firefox + - host: macos-arm64-webkit + runner: macos-15 + browser: webkit + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22.13.0 + - name: Install pnpm + run: npm install --global pnpm@11.7.0 + - name: Install locked dependencies + run: pnpm install --frozen-lockfile + - name: Validate certification authority + run: pnpm check:browser-output-authority + - name: Install Rust toolchain + run: rustup toolchain install 1.96.0 --profile minimal + - name: Install browser and Linux dependencies + if: runner.os == 'Linux' + run: pnpm exec playwright install --with-deps ${{ matrix.browser }} + - name: Install browser + if: runner.os != 'Linux' + run: pnpm exec playwright install ${{ matrix.browser }} + - name: Execute browser/output host + run: pnpm check:browser-output -- --browser=${{ matrix.browser }} --host=${{ matrix.host }} --output=target/browser-output-certification/evidence/${{ matrix.host }}.json --require-clean + - name: Upload host evidence + if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: browser-output-${{ matrix.host }}-${{ github.sha }} + path: | + target/browser-output-certification/evidence/${{ matrix.host }}.json + target/browser-output-certification/evidence/${{ matrix.host }}.screenshots + if-no-files-found: error + retention-days: 30 + + matrix: + name: Aggregate browser/output matrix + needs: host + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22.13.0 + - name: Install pnpm + run: npm install --global pnpm@11.7.0 + - name: Install locked dependencies + run: pnpm install --frozen-lockfile + - name: Validate certification authority + run: pnpm check:browser-output-authority + - name: Download all host evidence + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: browser-output-*-${{ github.sha }} + path: target/browser-output-certification/incoming + merge-multiple: true + - name: Validate complete matrix + run: pnpm check:browser-output-matrix -- --evidence-root=target/browser-output-certification/incoming --output=target/browser-output-certification/browser-output-matrix.json + - name: Upload complete matrix + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: browser-output-matrix-${{ github.sha }} + path: target/browser-output-certification/browser-output-matrix.json + if-no-files-found: error + retention-days: 30 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be77296..c565fb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,6 +78,7 @@ jobs: env: PLIEGO_CSSC: target/debug/pliego-cssc${{ runner.os == 'Windows' && '.exe' || '' }} PLIEGO_LOCK_HOLDER: target/debug/pliego-publication-lock-holder${{ runner.os == 'Windows' && '.exe' || '' }} + PLIEGOCSS_RUST_TOOLCHAIN: ${{ matrix.toolchain }} run: node scripts/check-parallel-determinism.mjs - name: Check cross-platform bundle contract run: node scripts/check-portability.mjs @@ -120,6 +121,10 @@ jobs: node-version: 22.13.0 - name: Install pnpm for documentation gates run: npm install --global pnpm@11.7.0 + - name: Install Rust toolchains + run: | + rustup toolchain install 1.85.0 --profile minimal + rustup toolchain install 1.96.0 --profile minimal --component rustfmt - name: Verify release profile registry run: node scripts/check-release.test.mjs - name: Check documentation graph @@ -128,8 +133,6 @@ jobs: run: node scripts/check-brand.mjs - name: Check compatibility policy run: node scripts/check-compatibility-policy.mjs - - name: Install rustfmt - run: rustup toolchain install 1.96.0 --profile minimal --component rustfmt - name: Check formatting run: | cargo +1.96.0 fmt --all -- --check @@ -247,6 +250,12 @@ jobs: rustup target add wasm32-unknown-unknown --toolchain 1.96.0 - name: Install benchmark dependencies run: pnpm install --frozen-lockfile + - name: Check Benchmark Authority v2 + run: pnpm check:benchmark-authority + - name: Check browser/output certification authority + run: pnpm check:browser-output-authority + - name: Smoke Benchmark Authority v2 runner + run: pnpm check:benchmark-smoke - name: Build and validate the PliegoRS-authored website run: pnpm check:site - name: Validate the Cloudflare public deployment profile diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d41377..a831efe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,39 @@ pre-1.0 compatibility policy. ## [Unreleased] +### Changed + +- Separate stable breakpoint identity from narrow-to-wide cascade rank. ThemeId + and theme-binary formats move to 3, catalog to schema 4, and inspection to + schema 3; older generated theme/identity artifacts must be regenerated. +- Namespace official Cargo targets by complete Rust toolchain/platform identity, + including the Rust 1.85 property/LSP gates and Rust 1.86 PliegoRS gate. +- Re-pin the fail-closed PliegoRS integration contract to current `main` after + the G1 native runtime entered the workspace, and replay the complete SSG, + route/island bundle, manifest, and browser-payload gate against that revision. +- Make release-readiness schema 3 the single current promotion authority, + fail closed on unconfigured hosted browser lanes, archive superseded status + reports, and generate the public documentation catalog from hash-bound + Markdown instead of duplicated Rust page data. +- Introduce the pure `CompileRequest`/`CompileResult` engine boundary with a + theme-bound `AnalysisHost`, compiler-owned `PhysicalRulePlanner`, and shared + `pcx` frontend. CLI, watch, macros, source scanning, and LSP now reuse that + engine; the LSP no longer launches compiler children or writes synthetic + source files. +- Replace the handwritten Gate B comparison with Benchmark Authority schema 2: + live `tailwind-latest` 4.3.3, upstream `v3-lts` 3.4.19, frozen 4.3.2 regression, + alternating paired samples, direct-process peak memory, raw/gzip/Brotli output, + micro/medium/large corpora, a seven-day oracle, and JSON-generated benchmark docs. + +### Fixed + +- Emit custom media and container breakpoints by numeric cascade rank instead + of name-assigned `BreakpointId`; the 52rem/72rem regression is frozen in + property tests and Chrome/Edge computed-style gates. +- Reject empty reversible migration projects with a typed public error instead + of indexing an empty inventory, and audit every current Lightning CSS rule + variant without a silent wildcard. + ## [0.1.0-rc.2] - 2026-07-21 This candidate supersedes `0.1.0-rc.1` without moving its immutable tag. It is @@ -16,6 +49,15 @@ compatibility unit. ### Added +- Browser/output certification v1 against the fresh Tailwind authority lane: 52 computed + properties, 1/64-pixel box/text-line geometry bounded to 0.1 CSS px, bounded perceptual screenshots, + responsive/hover/focus scenarios, and explicit + no-reset/shared-reset contracts. +- A clean, seven-day, commit/tree-bound 3×3 Playwright matrix for Chromium, Firefox, and WebKit on + Windows x64, Linux x64, and macOS ARM64, plus fail-closed aggregation and hashed PNG/JSON evidence. + Playwright 1.61.1, pixelmatch 7.2.0, and pngjs 7.0.0 are pinned and attributed. Dirty local + Windows x64 and Linux x64 passes are diagnostic; hosted 9/9 acceptance remains pending. + - A complete bilingual public-preview website authored with PliegoRS and styled by PliegoCSS, including interactive compiler examples, categorized documentation, command search, legal and accessibility pages, GSAP/Lenis diff --git a/Cargo.lock b/Cargo.lock index ec79a7a..f3efbc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -514,9 +514,13 @@ version = "0.1.0-rc.2" name = "pliego-css-lsp" version = "0.1.0-rc.2" dependencies = [ + "pliego-css-compiler", + "pliego-css-config", "pliego-css-io", + "pliego-css-ir", "pliego-css-parser", "pliego-css-source", + "pliego-css-theme", "serde", "serde_json", "sha2", diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 2c6f03a..a0e5dbc 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -64,12 +64,41 @@ bundled software dependencies. ## Benchmark-only software -### Tailwind CSS and CLI 4.3.2 +### Tailwind CSS and CLI benchmark lanes -- Relationship: frozen performance and ergonomics baseline only; not a PliegoCSS runtime/backend - dependency. +- Relationship: performance and ergonomics baselines only; not PliegoCSS runtime/backend + dependencies. Benchmark Authority v2 uses current `4.3.3`, upstream `v3-lts` `3.4.19`, and + frozen regression control `4.3.2`. - License: MIT. - Source: https://github.com/tailwindlabs/tailwindcss -- Locked evidence: `tailwindcss@4.3.2`, `@tailwindcss/cli@4.3.2`, and the complete transitive - `pnpm-lock.yaml` graph at SHA-256 - `c4f9b91705418524bd91178fd417e3b02585dfce613c2a56e6cef25a6c8c1049`. +- Locked evidence: `tailwindcss@4.3.3`, `@tailwindcss/cli@4.3.3`, + `tailwindcss@3.4.19`, `tailwindcss@4.3.2`, `@tailwindcss/cli@4.3.2`, their registry SRI values, + and the complete transitive `pnpm-lock.yaml` graph at SHA-256 + `39f1ccd3f3b3262805d773e92bf8b2d0efc9ac755ba77460a0cb801d5ccea394`. + +## Browser-certification software + +These packages are test-only tooling for Browser/output certification v1. They do not ship in the +PliegoCSS compiler or generated CSS. + +### Playwright 1.61.1 + +- Relationship: launches the pinned Chromium, Firefox, and WebKit certification engines. +- License: Apache-2.0. +- Source: https://github.com/microsoft/playwright +- Locked evidence: `playwright@1.61.1`, `playwright-core@1.61.1`, their npm SRI values, and the + complete `pnpm-lock.yaml` SHA-256 recorded above. + +### pixelmatch 7.2.0 + +- Relationship: bounded perceptual comparison of captured PNG pixels. +- License: ISC. +- Source: https://github.com/mapbox/pixelmatch +- Locked evidence: `pixelmatch@7.2.0`, its npm SRI, and the complete pnpm lock SHA-256. + +### pngjs 7.0.0 + +- Relationship: deterministic PNG decoding and diff encoding for certification evidence. +- License: MIT. +- Source: https://github.com/pngjs/pngjs +- Locked evidence: `pngjs@7.0.0`, its npm SRI, and the complete pnpm lock SHA-256. diff --git a/benchmarks/benchmark-authority-v2/micro.html b/benchmarks/benchmark-authority-v2/micro.html new file mode 100644 index 0000000..df4c57c --- /dev/null +++ b/benchmarks/benchmark-authority-v2/micro.html @@ -0,0 +1,15 @@ + + +
+ + +");
+ output.push_str("| ");
+ output.push_str(&breakpoint.cascade_rank.to_string());
+ output.push_str(" | ");
output.push_str(&markdown_cell(&breakpoint.name));
output.push_str(" | ");
output.push_str(&markdown_cell(&breakpoint.min_width));
diff --git a/crates/pliego-css-build/tests/catalog.rs b/crates/pliego-css-build/tests/catalog.rs
index 21b579c..01de442 100644
--- a/crates/pliego-css-build/tests/catalog.rs
+++ b/crates/pliego-css-build/tests/catalog.rs
@@ -13,11 +13,11 @@ fn seed_catalog_bytes_match_the_existing_cli_contract() {
assert_eq!(
sha256_hex(json.as_bytes()),
- "b5a2092fdfd079e2b586ac9bd850ead20599e4d9176cc358cc8bd1f10ea6273c"
+ "6ac31ef2bb0c1051829e2eaa6bfda389937a0e55f1489f4ad3f00d9d46e7fe93"
);
assert_eq!(
sha256_hex(markdown.as_bytes()),
- "2efd4bf3cc25162de2996ae364876f61e4c8c98626006782bd0648ee149d335e"
+ "e0ebf303c80937697d09723d890487aaa2ed8db6059a766f61b3be71765ba1da"
);
assert!(json.ends_with('\n'));
assert!(markdown.ends_with('\n'));
@@ -43,7 +43,7 @@ fn json_catalog_is_closed_over_the_frozen_top_level_shape() {
"utilities",
]
);
- assert_eq!(document["schemaVersion"], 3);
+ assert_eq!(document["schemaVersion"], 4);
assert!(
document["utilities"]
.as_array()
diff --git a/crates/pliego-css-build/tests/css_audit.rs b/crates/pliego-css-build/tests/css_audit.rs
index c55e02c..357d4f6 100644
--- a/crates/pliego-css-build/tests/css_audit.rs
+++ b/crates/pliego-css-build/tests/css_audit.rs
@@ -280,6 +280,47 @@ fn unknown_container_conditions_fail_as_unclassified_syntax() {
assert_eq!(finding["source"]["byteEnd"], 10);
}
+#[test]
+fn every_supported_leaf_at_rule_is_audited_instead_of_silently_skipped() {
+ let cases = [
+ ("@import", "@import url('theme.css');"),
+ ("@keyframes", "@keyframes pulse { to { opacity: 0; } }"),
+ (
+ "@font-face",
+ "@font-face { font-family: Demo; src: local(Demo); }",
+ ),
+ ("@page", "@page { margin: 1cm; }"),
+ (
+ "@counter-style",
+ "@counter-style thumbs { system: cyclic; symbols: 'x'; }",
+ ),
+ (
+ "@namespace",
+ "@namespace svg url(http://www.w3.org/2000/svg);",
+ ),
+ ("@viewport", "@viewport { width: device-width; }"),
+ ("@custom-media", "@custom-media --narrow (width < 40rem);"),
+ ("@view-transition", "@view-transition { navigation: auto; }"),
+ ];
+ for (syntax, css) in cases {
+ let outcome = audit_standard_css(
+ "src/leaf-at-rule.css",
+ css,
+ CompatibilityProfile::BaselineWidely,
+ )
+ .unwrap_or_else(|error| panic!("{syntax}: {error}"));
+ let value: serde_json::Value =
+ serde_json::from_str(&outcome.document().to_json_pretty().expect("finding JSON"))
+ .expect("document");
+ assert!(
+ value["findings"].as_array().unwrap().iter().any(|finding| {
+ finding["code"] == "PCSS-COMPAT-199" && finding["context"]["syntax"] == syntax
+ }),
+ "missing unclassified finding for {syntax}: {value}"
+ );
+ }
+}
+
#[test]
fn declaration_shape_inventory_distinguishes_typed_unparsed_and_custom_values() {
let css = ".card { display: grid; color: var(--ink); --gap: 1rem; mystery-prop: value; }";
diff --git a/crates/pliego-css-build/tests/internal/asset_plan.rs b/crates/pliego-css-build/tests/internal/asset_plan.rs
index 5e8b38c..0a46f01 100644
--- a/crates/pliego-css-build/tests/internal/asset_plan.rs
+++ b/crates/pliego-css-build/tests/internal/asset_plan.rs
@@ -74,7 +74,7 @@ fn schema_one_plan_is_integrity_bound_and_canonical() {
assert_eq!(plan["applicationCoverage"], "adapter-attested-complete");
assert_eq!(plan["styleIdFormatVersion"], 2);
assert_eq!(plan["classNameFormatVersion"], 1);
- assert_eq!(plan["themeIdFormatVersion"], 2);
+ assert_eq!(plan["themeIdFormatVersion"], 3);
assert_eq!(plan["themeId"], "0123456789abcdef0123456789abcdef");
assert_eq!(plan["targets"], "modern");
assert_eq!(plan["format"], "minified");
@@ -562,7 +562,7 @@ fn manifest(schema: u8, css: &[u8], active_components: &[&str], emits_theme: boo
"schemaVersion": schema,
"styleIdFormatVersion": 2,
"classNameFormatVersion": 1,
- "themeIdFormatVersion": 2,
+ "themeIdFormatVersion": 3,
"themeId": "0123456789abcdef0123456789abcdef",
"targets": "modern",
"format": "minified",
diff --git a/crates/pliego-css-compiler/src/emitter.rs b/crates/pliego-css-compiler/src/emitter.rs
index fbfd53c..451067b 100644
--- a/crates/pliego-css-compiler/src/emitter.rs
+++ b/crates/pliego-css-compiler/src/emitter.rs
@@ -226,7 +226,7 @@ fn emit_css_with_theme_inner(
.into_iter()
.map(|(id, assignments)| (&style.conditions[id.index()], assignments))
.collect::>();
- groups.sort_by(|left, right| compare_resolved_conditions(style, left.0, right.0));
+ groups.sort_by(|left, right| compare_resolved_conditions(theme, style, left.0, right.0));
let mut css = String::new();
let mut rules = Vec::new();
@@ -431,14 +431,21 @@ fn selector(
}
fn compare_resolved_conditions(
+ theme: &ThemeRegistry,
style: &SemanticStyle,
left: &Condition,
right: &Condition,
) -> Ordering {
left.layer
.cmp(&right.layer)
- .then_with(|| left.breakpoint.cmp(&right.breakpoint))
- .then_with(|| left.container_breakpoint.cmp(&right.container_breakpoint))
+ .then_with(|| {
+ breakpoint_order_key(theme, left.breakpoint)
+ .cmp(&breakpoint_order_key(theme, right.breakpoint))
+ })
+ .then_with(|| {
+ breakpoint_order_key(theme, left.container_breakpoint)
+ .cmp(&breakpoint_order_key(theme, right.container_breakpoint))
+ })
.then_with(|| left.theme.cmp(&right.theme))
.then_with(|| left.motion.cmp(&right.motion))
.then_with(|| left.contrast.cmp(&right.contrast))
@@ -456,6 +463,20 @@ fn compare_resolved_conditions(
})
}
+fn breakpoint_order_key(
+ theme: &ThemeRegistry,
+ breakpoint: Option,
+) -> (u8, u16, u16) {
+ match breakpoint {
+ None => (0, 0, 0),
+ Some(id) => theme
+ .breakpoint_by_id(id)
+ .map_or((2, u16::MAX, id.get()), |definition| {
+ (1, definition.cascade_rank, id.get())
+ }),
+ }
+}
+
fn wrap_condition(
theme: &ThemeRegistry,
mut rule: String,
diff --git a/crates/pliego-css-compiler/src/engine.rs b/crates/pliego-css-compiler/src/engine.rs
new file mode 100644
index 0000000..52987c4
--- /dev/null
+++ b/crates/pliego-css-compiler/src/engine.rs
@@ -0,0 +1,749 @@
+//! Pure, incremental analysis and physical-rule planning shared by every adapter.
+
+use core::fmt;
+use std::collections::BTreeMap;
+
+use pliego_css_ir::{Diagnostic, SemanticStyle, StyleId};
+use pliego_css_parser::parse_style_list;
+use pliego_css_theme::{ThemeId, ThemeRegistry};
+
+use super::{
+ CrossClauseConflict, CssFragmentCache, EmitError, IdentityError, StyleLineage,
+ analyze_cross_clause_conflicts, compose_style_override_with_theme, emit_css_with_theme_traced,
+ emit_theme, lower_style_with_theme, try_encode_style_identity_with_theme,
+};
+
+/// Maximum number of statically materialized `pcx!` branch combinations.
+pub const MAX_PCX_COMBINATIONS: usize = 64;
+const MAX_SEMANTIC_CACHE_ITEMS: usize = 4_096;
+
+/// One source-independent input accepted by the shared compiler engine.
+#[non_exhaustive]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum CompileInput {
+ /// Parse and lower one visible utility-list literal.
+ Literal(String),
+ /// Compose independently lowered branches over one base literal.
+ Composition {
+ /// Base utility-list literal.
+ base: String,
+ /// Ordered branch literals whose semantic slots override the base.
+ branches: Vec,
+ },
+ /// A validated semantic style handed off by an advanced adapter.
+ Semantic(SemanticStyle),
+}
+
+impl CompileInput {
+ /// Constructs one literal input.
+ #[must_use]
+ pub fn literal(source: impl Into) -> Self {
+ Self::Literal(source.into())
+ }
+
+ /// Constructs one composition input.
+ #[must_use]
+ pub fn composition(
+ base: impl Into,
+ branches: impl IntoIterator- >,
+ ) -> Self {
+ Self::Composition {
+ base: base.into(),
+ branches: branches.into_iter().map(Into::into).collect(),
+ }
+ }
+}
+
+/// Complete, I/O-free request to the shared compiler engine.
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct CompileRequest {
+ /// Inputs to lower or validate.
+ pub inputs: Vec
,
+ /// Emit the active theme before physical style rules.
+ pub include_theme: bool,
+ /// Record declaration lineage for physical-rule evidence.
+ pub physical_trace: bool,
+}
+
+impl CompileRequest {
+ /// Constructs a request from ordered inputs.
+ #[must_use]
+ pub fn new(inputs: impl IntoIterator- ) -> Self {
+ Self {
+ inputs: inputs.into_iter().collect(),
+ include_theme: false,
+ physical_trace: false,
+ }
+ }
+}
+
+/// Cache activity caused by one engine request.
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+pub struct AnalysisCacheStats {
+ /// Literal analyses served from the semantic cache.
+ pub semantic_hits: usize,
+ /// Literal analyses computed during this request.
+ pub semantic_misses: usize,
+ /// Physical CSS fragments served from the emission cache.
+ pub physical_hits: usize,
+ /// Physical CSS fragments emitted during this request.
+ pub physical_misses: usize,
+ /// Physical fragments removed after the complete snapshot changed.
+ pub physical_pruned: usize,
+}
+
+impl AnalysisCacheStats {
+ fn delta(self, before: Self) -> Self {
+ Self {
+ semantic_hits: self.semantic_hits - before.semantic_hits,
+ semantic_misses: self.semantic_misses - before.semantic_misses,
+ physical_hits: self.physical_hits - before.physical_hits,
+ physical_misses: self.physical_misses - before.physical_misses,
+ physical_pruned: self.physical_pruned - before.physical_pruned,
+ }
+ }
+}
+
+/// One collision-checked physical style in canonical emission order.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct CompiledStyle {
+ /// Stable semantic IR and `StyleId`.
+ pub semantic: SemanticStyle,
+ /// Canonical identity bytes used for ordering, deduplication, and collision checks.
+ pub identity_stream: Vec
,
+ /// Unoptimized deterministic CSS fragment.
+ pub css: String,
+ /// Optional physical-declaration lineage.
+ pub lineage: Option,
+}
+
+/// Complete result returned by [`AnalysisHost::compile`].
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct CompileResult {
+ /// Theme identity used for every semantic and physical decision.
+ pub theme_id: ThemeId,
+ /// Unique styles in canonical identity-stream order.
+ pub styles: Vec,
+ /// Unoptimized deterministic theme and style CSS.
+ pub css: String,
+ /// Cache activity caused by this request.
+ pub cache: AnalysisCacheStats,
+}
+
+/// Failure returned by the pure compiler engine.
+#[non_exhaustive]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum CompileError {
+ /// Parser or semantic lowering rejected one input segment.
+ Diagnostic {
+ /// Zero-based request input index.
+ input: usize,
+ /// `base`, `literal`, or `branch-N`.
+ segment: String,
+ /// Stable compiler diagnostic.
+ diagnostic: Diagnostic,
+ },
+ /// An adapter supplied malformed or theme-incompatible semantic IR.
+ InvalidSemantic {
+ /// Zero-based request input index.
+ input: usize,
+ /// Structural or identity failure.
+ message: String,
+ },
+ /// Two canonical streams resolved to one `StyleId`.
+ StyleIdCollision {
+ /// Colliding style identity.
+ style_id: StyleId,
+ },
+ /// One canonical stream resolved to two `StyleId` values.
+ CanonicalStreamCollision,
+ /// CSS emission failed after semantic validation.
+ Emit(EmitError),
+}
+
+impl fmt::Display for CompileError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Diagnostic {
+ input,
+ segment,
+ diagnostic,
+ } => write!(formatter, "input {} {segment}: {diagnostic}", input + 1),
+ Self::InvalidSemantic { input, message } => {
+ write!(
+ formatter,
+ "input {} semantic IR is invalid: {message}",
+ input + 1
+ )
+ }
+ Self::StyleIdCollision { style_id } => write!(
+ formatter,
+ "StyleId collision under the active format for {:032x}",
+ style_id.get()
+ ),
+ Self::CanonicalStreamCollision => {
+ formatter.write_str("one canonical style stream mapped to multiple StyleId values")
+ }
+ Self::Emit(error) => error.fmt(formatter),
+ }
+ }
+}
+
+impl std::error::Error for CompileError {}
+
+impl From for CompileError {
+ fn from(error: EmitError) -> Self {
+ Self::Emit(error)
+ }
+}
+
+/// One physical-rule plan before target-specific CSS optimization.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct PhysicalRulePlan {
+ /// Unique styles in canonical identity-stream order.
+ pub styles: Vec,
+ /// Deterministic theme and style CSS.
+ pub css: String,
+}
+
+/// Stateful planner that keeps physical emission separate from semantic identity.
+#[derive(Default)]
+pub struct PhysicalRulePlanner {
+ fragments: CssFragmentCache,
+}
+
+impl PhysicalRulePlanner {
+ /// Returns the number of cached physical fragments.
+ #[must_use]
+ pub fn len(&self) -> usize {
+ self.fragments.len()
+ }
+
+ /// Returns whether the physical fragment cache is empty.
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ self.fragments.is_empty()
+ }
+
+ /// Plans, collision-checks, deduplicates, and emits one complete semantic snapshot.
+ ///
+ /// # Errors
+ ///
+ /// Returns structural, identity, collision, or emitter failures without performing I/O.
+ pub fn plan(
+ &mut self,
+ theme: &ThemeRegistry,
+ styles: impl IntoIterator- ,
+ include_theme: bool,
+ physical_trace: bool,
+ stats: &mut AnalysisCacheStats,
+ ) -> Result
{
+ let mut unique = BTreeMap::, SemanticStyle>::new();
+ let mut streams_by_id = BTreeMap::>::new();
+ for (input, style) in styles.into_iter().enumerate() {
+ style
+ .validate()
+ .map_err(|error| CompileError::InvalidSemantic {
+ input,
+ message: error.to_string(),
+ })?;
+ let stream = try_encode_style_identity_with_theme(theme, &style).map_err(
+ |error: IdentityError| CompileError::InvalidSemantic {
+ input,
+ message: error.to_string(),
+ },
+ )?;
+ match streams_by_id.entry(style.id) {
+ std::collections::btree_map::Entry::Vacant(entry) => {
+ entry.insert(stream.clone());
+ }
+ std::collections::btree_map::Entry::Occupied(entry) if entry.get() != &stream => {
+ return Err(CompileError::StyleIdCollision { style_id: style.id });
+ }
+ std::collections::btree_map::Entry::Occupied(_) => {}
+ }
+ match unique.entry(stream) {
+ std::collections::btree_map::Entry::Vacant(entry) => {
+ entry.insert(style);
+ }
+ std::collections::btree_map::Entry::Occupied(entry)
+ if entry.get().id != style.id =>
+ {
+ return Err(CompileError::CanonicalStreamCollision);
+ }
+ std::collections::btree_map::Entry::Occupied(_) => {}
+ }
+ }
+
+ let mut css = String::new();
+ if include_theme {
+ css.push_str(&emit_theme(theme));
+ css.push('\n');
+ }
+ let mut planned = Vec::with_capacity(unique.len());
+ for (stream, semantic) in &unique {
+ let (fragment, lineage) = if physical_trace {
+ let (fragment, lineage) = emit_css_with_theme_traced(theme, semantic)?;
+ stats.physical_misses += 1;
+ (fragment, Some(lineage))
+ } else {
+ let (fragment, hit) = self.fragments.emit(stream, theme, semantic)?;
+ if hit {
+ stats.physical_hits += 1;
+ } else {
+ stats.physical_misses += 1;
+ }
+ (fragment.to_owned(), None)
+ };
+ css.push_str(&fragment);
+ css.push('\n');
+ planned.push(CompiledStyle {
+ semantic: semantic.clone(),
+ identity_stream: stream.clone(),
+ css: fragment,
+ lineage,
+ });
+ }
+ stats.physical_pruned += self.fragments.retain(unique.keys());
+ Ok(PhysicalRulePlan {
+ styles: planned,
+ css,
+ })
+ }
+
+ fn clear(&mut self) {
+ self.fragments = CssFragmentCache::default();
+ }
+}
+
+/// Source-independent `pcx` request shared by macros, scanners, and editors.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct PcxRequest {
+ /// Base utility-list literal.
+ pub base: String,
+ /// Ordered clauses, each containing all statically visible branch literals.
+ pub clauses: Vec>,
+}
+
+impl PcxRequest {
+ /// Constructs a shared conditional-analysis request.
+ #[must_use]
+ pub fn new(
+ base: impl Into,
+ clauses: impl IntoIterator- >>,
+ ) -> Self {
+ Self {
+ base: base.into(),
+ clauses: clauses
+ .into_iter()
+ .map(|clause| clause.into_iter().map(Into::into).collect())
+ .collect(),
+ }
+ }
+}
+
+/// One fully composed static `pcx` branch combination.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct PcxCombination {
+ /// Zero-based selected branch index for every clause.
+ pub selections: Vec
,
+ /// Semantic result after applying the selections over the base.
+ pub semantic: SemanticStyle,
+}
+
+/// Complete shared `pcx` frontend result.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct PcxAnalysis {
+ /// Lowered base style.
+ pub base: SemanticStyle,
+ /// Lowered styles for every visible clause branch.
+ pub clauses: Vec>,
+ /// All bounded branch combinations in stable cartesian order.
+ pub combinations: Vec,
+}
+
+/// Failure returned by the shared `pcx` frontend.
+#[non_exhaustive]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum PcxError {
+ /// The base literal failed parser or semantic validation.
+ Base(Diagnostic),
+ /// One visible branch failed parser or semantic validation.
+ Branch {
+ /// Zero-based clause index.
+ clause: usize,
+ /// Zero-based branch index.
+ branch: usize,
+ /// Stable compiler diagnostic.
+ diagnostic: Diagnostic,
+ },
+ /// One clause did not contain a visible branch.
+ EmptyClause {
+ /// Zero-based clause index.
+ clause: usize,
+ },
+ /// Independently selectable clauses overlap semantically.
+ Conflict(CrossClauseConflict),
+ /// Static materialization would exceed [`MAX_PCX_COMBINATIONS`].
+ ExpansionLimit {
+ /// Maximum accepted combination count.
+ maximum: usize,
+ },
+}
+
+impl fmt::Display for PcxError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Base(diagnostic) => write!(formatter, "pcx base: {diagnostic}"),
+ Self::Branch {
+ clause,
+ branch,
+ diagnostic,
+ } => write!(
+ formatter,
+ "pcx clause {} branch {}: {diagnostic}",
+ clause + 1,
+ branch + 1
+ ),
+ Self::EmptyClause { clause } => {
+ write!(
+ formatter,
+ "pcx clause {} has no visible branches",
+ clause + 1
+ )
+ }
+ Self::Conflict(conflict) => write!(
+ formatter,
+ "PCX003: independent clauses {} and {} overlap",
+ conflict.left_clause + 1,
+ conflict.right_clause + 1
+ ),
+ Self::ExpansionLimit { maximum } => write!(
+ formatter,
+ "PCX004: pcx expands to more than {maximum} style combinations"
+ ),
+ }
+ }
+}
+
+impl std::error::Error for PcxError {}
+
+/// Long-lived semantic and physical analysis state shared by CLI, watch, and LSP adapters.
+pub struct AnalysisHost {
+ theme: ThemeRegistry,
+ semantic: BTreeMap>,
+ planner: PhysicalRulePlanner,
+ stats: AnalysisCacheStats,
+}
+
+impl Default for AnalysisHost {
+ fn default() -> Self {
+ Self::new(ThemeRegistry::seed())
+ }
+}
+
+impl AnalysisHost {
+ /// Starts one analysis host for an exact theme registry.
+ #[must_use]
+ pub fn new(theme: ThemeRegistry) -> Self {
+ Self {
+ theme,
+ semantic: BTreeMap::new(),
+ planner: PhysicalRulePlanner::default(),
+ stats: AnalysisCacheStats::default(),
+ }
+ }
+
+ /// Returns the exact active theme.
+ #[must_use]
+ pub const fn theme(&self) -> &ThemeRegistry {
+ &self.theme
+ }
+
+ /// Replaces the active theme and invalidates every theme-bound cache when its ID changes.
+ pub fn set_theme(&mut self, theme: ThemeRegistry) {
+ if self.theme.id() != theme.id() {
+ self.semantic.clear();
+ self.planner.clear();
+ }
+ self.theme = theme;
+ }
+
+ /// Returns cumulative cache activity for observability and watch-mode reporting.
+ #[must_use]
+ pub const fn stats(&self) -> AnalysisCacheStats {
+ self.stats
+ }
+
+ /// Returns the number of retained physical CSS fragments.
+ #[must_use]
+ pub fn physical_cache_len(&self) -> usize {
+ self.planner.len()
+ }
+
+ /// Parses and lowers one literal through the shared semantic cache.
+ ///
+ /// # Errors
+ ///
+ /// Returns the stable parser or semantic diagnostic.
+ pub fn analyze_literal(&mut self, source: &str) -> Result {
+ if let Some(cached) = self.semantic.get(source) {
+ self.stats.semantic_hits += 1;
+ return cached.clone();
+ }
+ if self.semantic.len() >= MAX_SEMANTIC_CACHE_ITEMS {
+ self.semantic.clear();
+ }
+ self.stats.semantic_misses += 1;
+ let result = parse_style_list(source)
+ .and_then(|syntax| lower_style_with_theme(&self.theme, &syntax));
+ self.semantic.insert(source.to_owned(), result.clone());
+ result
+ }
+
+ /// Runs the shared bounded conditional frontend.
+ ///
+ /// # Errors
+ ///
+ /// Returns literal diagnostics, empty clauses, cross-clause conflicts, or expansion overflow.
+ pub fn analyze_pcx(&mut self, request: &PcxRequest) -> Result {
+ let base = self
+ .analyze_literal(&request.base)
+ .map_err(PcxError::Base)?;
+ let mut clauses = Vec::with_capacity(request.clauses.len());
+ for (clause_index, clause) in request.clauses.iter().enumerate() {
+ if clause.is_empty() {
+ return Err(PcxError::EmptyClause {
+ clause: clause_index,
+ });
+ }
+ let mut branches = Vec::with_capacity(clause.len());
+ for (branch_index, source) in clause.iter().enumerate() {
+ branches.push(self.analyze_literal(source).map_err(|diagnostic| {
+ PcxError::Branch {
+ clause: clause_index,
+ branch: branch_index,
+ diagnostic,
+ }
+ })?);
+ }
+ clauses.push(branches);
+ }
+ analyze_cross_clause_conflicts(&clauses).map_err(PcxError::Conflict)?;
+ let branch_counts = clauses.iter().map(Vec::len).collect::>();
+ let valid_count = branch_counts.iter().try_fold(1_usize, |count, branches| {
+ count
+ .checked_mul(*branches)
+ .filter(|next| *next <= MAX_PCX_COMBINATIONS)
+ });
+ if valid_count.is_none() {
+ return Err(PcxError::ExpansionLimit {
+ maximum: MAX_PCX_COMBINATIONS,
+ });
+ }
+ let mut combinations = Vec::new();
+ for selections in cartesian_indices(&branch_counts) {
+ let mut semantic = base.clone();
+ for (clause, branch) in clauses.iter().zip(&selections) {
+ semantic = compose_style_override_with_theme(
+ &self.theme,
+ semantic,
+ clause[*branch].clone(),
+ );
+ }
+ combinations.push(PcxCombination {
+ selections,
+ semantic,
+ });
+ }
+ Ok(PcxAnalysis {
+ base,
+ clauses,
+ combinations,
+ })
+ }
+
+ /// Executes one complete pure compiler request.
+ ///
+ /// # Errors
+ ///
+ /// Returns stable input diagnostics or physical planning failures.
+ pub fn compile(&mut self, request: &CompileRequest) -> Result {
+ let before = self.stats;
+ let mut semantics = Vec::with_capacity(request.inputs.len());
+ for (input_index, input) in request.inputs.iter().enumerate() {
+ let semantic = match input {
+ CompileInput::Literal(source) => {
+ self.analyze_literal(source)
+ .map_err(|diagnostic| CompileError::Diagnostic {
+ input: input_index,
+ segment: "literal".into(),
+ diagnostic,
+ })?
+ }
+ CompileInput::Composition { base, branches } => {
+ let mut semantic = self.analyze_literal(base).map_err(|diagnostic| {
+ CompileError::Diagnostic {
+ input: input_index,
+ segment: "base".into(),
+ diagnostic,
+ }
+ })?;
+ for (branch_index, branch) in branches.iter().enumerate() {
+ let branch = self.analyze_literal(branch).map_err(|diagnostic| {
+ CompileError::Diagnostic {
+ input: input_index,
+ segment: format!("branch-{}", branch_index + 1),
+ diagnostic,
+ }
+ })?;
+ semantic = compose_style_override_with_theme(&self.theme, semantic, branch);
+ }
+ semantic
+ }
+ CompileInput::Semantic(semantic) => semantic.clone(),
+ };
+ semantics.push(semantic);
+ }
+ let plan = self.planner.plan(
+ &self.theme,
+ semantics,
+ request.include_theme,
+ request.physical_trace,
+ &mut self.stats,
+ )?;
+ Ok(CompileResult {
+ theme_id: self.theme.id(),
+ styles: plan.styles,
+ css: plan.css,
+ cache: self.stats.delta(before),
+ })
+ }
+}
+
+fn cartesian_indices(branch_counts: &[usize]) -> Vec> {
+ let mut combinations = vec![Vec::new()];
+ for branch_count in branch_counts {
+ let mut next = Vec::with_capacity(combinations.len() * branch_count);
+ for prefix in combinations {
+ for branch in 0..*branch_count {
+ let mut combination = prefix.clone();
+ combination.push(branch);
+ next.push(combination);
+ }
+ }
+ combinations = next;
+ }
+ combinations
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn compile_request_is_deterministic_and_incremental() {
+ let mut host = AnalysisHost::default();
+ let request = CompileRequest {
+ inputs: vec![
+ CompileInput::literal("flex p-4"),
+ CompileInput::literal("grid gap-2"),
+ CompileInput::literal("flex p-4"),
+ ],
+ include_theme: false,
+ physical_trace: false,
+ };
+ let first = host.compile(&request).unwrap();
+ let second = host.compile(&request).unwrap();
+ assert_eq!(first.css, second.css);
+ assert_eq!(first.styles.len(), 2);
+ assert_eq!(first.cache.semantic_misses, 2);
+ assert_eq!(first.cache.semantic_hits, 1);
+ assert_eq!(second.cache.semantic_hits, 3);
+ assert_eq!(second.cache.semantic_misses, 0);
+ assert_eq!(second.cache.physical_hits, 2);
+ }
+
+ #[test]
+ fn pcx_frontend_materializes_shared_combinations() {
+ let mut host = AnalysisHost::default();
+ let analysis = host
+ .analyze_pcx(&PcxRequest::new(
+ "flex",
+ [["p-2", "p-4"], ["text-ink", "text-muted"]],
+ ))
+ .unwrap();
+ assert_eq!(analysis.combinations.len(), 4);
+ assert_eq!(analysis.combinations[0].selections, [0, 0]);
+ assert_eq!(analysis.combinations[3].selections, [1, 1]);
+ }
+
+ #[test]
+ fn pcx_frontend_rejects_cross_clause_overlap() {
+ let mut host = AnalysisHost::default();
+ let error = host
+ .analyze_pcx(&PcxRequest::new("flex", [["p-2", "p-4"], ["px-1", "px-3"]]))
+ .unwrap_err();
+ assert!(matches!(error, PcxError::Conflict(_)));
+ }
+
+ #[test]
+ fn pcx_frontend_enforces_the_shared_expansion_limit() {
+ let mut host = AnalysisHost::default();
+ let error = host
+ .analyze_pcx(&PcxRequest::new(
+ "p-1",
+ [
+ ["[g2-a:0]", "[g2-a:1]"],
+ ["[g2-b:0]", "[g2-b:1]"],
+ ["[g2-c:0]", "[g2-c:1]"],
+ ["[g2-d:0]", "[g2-d:1]"],
+ ["[g2-e:0]", "[g2-e:1]"],
+ ["[g2-f:0]", "[g2-f:1]"],
+ ["[g2-g:0]", "[g2-g:1]"],
+ ],
+ ))
+ .unwrap_err();
+ assert_eq!(
+ error,
+ PcxError::ExpansionLimit {
+ maximum: MAX_PCX_COMBINATIONS
+ }
+ );
+ }
+
+ #[test]
+ fn physical_planner_prunes_styles_absent_from_the_complete_snapshot() {
+ let mut host = AnalysisHost::default();
+ host.compile(&CompileRequest::new([
+ CompileInput::literal("flex"),
+ CompileInput::literal("grid"),
+ ]))
+ .unwrap();
+ assert_eq!(host.physical_cache_len(), 2);
+
+ let result = host
+ .compile(&CompileRequest::new([CompileInput::literal("grid")]))
+ .unwrap();
+ assert_eq!(result.cache.physical_hits, 1);
+ assert_eq!(result.cache.physical_pruned, 1);
+ assert_eq!(host.physical_cache_len(), 1);
+ }
+
+ #[test]
+ fn theme_change_invalidates_semantic_cache() {
+ let mut host = AnalysisHost::default();
+ host.analyze_literal("flex").unwrap();
+ host.analyze_literal("flex").unwrap();
+ let before = host.stats();
+ let seed = ThemeRegistry::seed();
+ let mut tokens = seed.tokens().to_vec();
+ tokens[0].value = "1px".into();
+ let theme = ThemeRegistry::from_definitions(tokens, seed.breakpoints().to_vec()).unwrap();
+ assert_ne!(theme.id(), seed.id());
+ host.set_theme(theme);
+ host.analyze_literal("flex").unwrap();
+ assert_eq!(host.stats().semantic_hits, before.semantic_hits);
+ assert_eq!(host.stats().semantic_misses, before.semantic_misses + 1);
+ }
+}
diff --git a/crates/pliego-css-compiler/src/identity.rs b/crates/pliego-css-compiler/src/identity.rs
index 2cf0c0c..d1642f7 100644
--- a/crates/pliego-css-compiler/src/identity.rs
+++ b/crates/pliego-css-compiler/src/identity.rs
@@ -1278,17 +1278,17 @@ mod tests {
assert_eq!(
hex(&stream),
- "706c6965676f2d7374796c652d6964000002010002b3d5ad77175995c2b8f51ef7c0d419910200000003030000001f110e120112130900000007312e323572656d140100150001010100000000000300000024112012012b13070313e3e9b7011388140001150003030201010000000100000003263e700300000030113c00000007636f6e74656e7412015313090000000d27636166c3a97c3af09f98802714000015000101010000000000"
+ "706c6965676f2d7374796c652d6964000002010003b98b78da29201938d135b8bb947177880200000003030000001f110e120112130900000007312e323572656d140100150001010100000000000300000024112012012b13070313e3e9b7011388140001150003030201010000000100000003263e700300000030113c00000007636f6e74656e7412015313090000000d27636166c3a97c3af09f98802714000015000101010000000000"
);
assert_eq!(
format!("{:x}", Sha256::digest(&stream)),
- "f22fe79baa3e45505636d5a9e245d3d0934f8a45afbe662db9f841683277cc04"
+ "a3ce828a00a553726c6b9322bd6b2262bcdf66215326f8754329c6452bfcf266"
);
assert_eq!(
format!("{:032x}", style.id.get()),
- "f22fe79baa3e45505636d5a9e245d3d0"
+ "a3ce828a00a553726c6b9322bd6b2262"
);
- assert_eq!(style.id.to_class_name(), "pc_ec64pw6t451sjonk2yafopa6o");
+ assert_eq!(style.id.to_class_name(), "pc_9p4ab788swo2xs7828cmvjdk2");
}
#[test]
diff --git a/crates/pliego-css-compiler/src/ir_binary.rs b/crates/pliego-css-compiler/src/ir_binary.rs
index a1bc69f..7fedde2 100644
--- a/crates/pliego-css-compiler/src/ir_binary.rs
+++ b/crates/pliego-css-compiler/src/ir_binary.rs
@@ -1869,12 +1869,12 @@ mod tests {
assert_eq!(bytes.len(), 309);
assert_eq!(
hex(&digest),
- "337865f8e7b5b32fe58f67442537ad1d2926d94164d1692863a2616dddc85023"
+ "1d19d4bf7735acaf6b574d95fdb930389713c55983fff87c4c26a6cc2880532b"
);
assert_eq!(
hex(&bytes),
concat!(
- "504c474349520000000200020002b3d5ad77175995c2b8f51ef7c0d419919dda0d2bd9a9bc5c77077006110d08717f17",
+ "504c474349520000000200020003b98b78da29201938d135b8bb94717788647ff20792ad80e5851585f065bdc97e7f17",
"43fbde6e0fc37de303b085e83ff58ce3791ebfa0a06c443a936792d0350d000000050000001511011201011301041400",
"001500010101000000000000000000000000050000001c110e1201121309000000043272656d14010115000101010000",
"000000000000290000003400000028112012012b13070313e3e9b70107d0140000150100010301010101000000010000",
diff --git a/crates/pliego-css-compiler/src/lib.rs b/crates/pliego-css-compiler/src/lib.rs
index 2304ec8..a45752f 100644
--- a/crates/pliego-css-compiler/src/lib.rs
+++ b/crates/pliego-css-compiler/src/lib.rs
@@ -6,6 +6,7 @@
#![forbid(unsafe_code)]
mod emitter;
+mod engine;
mod identity;
mod ir_binary;
@@ -14,6 +15,11 @@ pub use emitter::{
emit_css, emit_css_with_theme, emit_css_with_theme_traced, emit_seed_theme, emit_theme,
emit_theme_references, emit_used_theme, referenced_tokens, theme_custom_property_name,
};
+pub use engine::{
+ AnalysisCacheStats, AnalysisHost, CompileError, CompileInput, CompileRequest, CompileResult,
+ CompiledStyle, MAX_PCX_COMBINATIONS, PcxAnalysis, PcxCombination, PcxError, PcxRequest,
+ PhysicalRulePlan, PhysicalRulePlanner,
+};
pub use identity::{
IdentityError, STYLE_ID_FORMAT_VERSION, derive_style_id, derive_style_id_with_theme,
try_derive_style_id, try_derive_style_id_with_theme, try_encode_style_identity,
@@ -39,7 +45,8 @@ use pliego_css_ir::{
classified_attribute_name, classified_selector_for_variant, is_typed_attribute_variant,
};
use pliego_css_parser::{
- NamedCandidateSyntax, OperandKind, OperandSyntax, parse_named_candidate_at,
+ NamedCandidateSyntax, OperandKind, OperandSyntax, parse_named_candidate,
+ parse_named_candidate_at,
};
use pliego_css_theme::ThemeRegistry;
@@ -836,6 +843,37 @@ pub const fn utility_catalog() -> &'static [UtilityDescriptor] {
CATALOG
}
+/// Resolves the compiler-owned catalog descriptor for one parsed style item.
+///
+/// This read-only bridge lets editor and inspection adapters explain the exact utility accepted by
+/// lowering without reimplementing longest-prefix catalog resolution.
+#[must_use]
+pub fn utility_descriptor_for_style_item(item: &StyleItem) -> Option<&'static UtilityDescriptor> {
+ match &item.candidate.kind {
+ CandidateKind::ArbitraryProperty { .. } => utility_catalog()
+ .iter()
+ .find(|descriptor| descriptor.form() == UtilityForm::ArbitraryProperty),
+ CandidateKind::Named(candidate) => {
+ let body = parse_named_candidate(candidate).ok()?.body;
+ utility_catalog()
+ .iter()
+ .find(|descriptor| {
+ descriptor.form() == UtilityForm::Fixed && descriptor.match_name() == body
+ })
+ .or_else(|| {
+ utility_catalog()
+ .iter()
+ .filter(|descriptor| descriptor.form() == UtilityForm::Parameterized)
+ .filter(|descriptor| {
+ body.strip_prefix(descriptor.match_name())
+ .is_some_and(|suffix| suffix.starts_with('-'))
+ })
+ .max_by_key(|descriptor| descriptor.match_name().len())
+ })
+ }
+ }
+}
+
fn seed_theme() -> &'static ThemeRegistry {
static SEED: OnceLock = OnceLock::new();
SEED.get_or_init(ThemeRegistry::seed)
@@ -2444,6 +2482,7 @@ mod tests {
],
[BreakpointDefinition::new(
pliego_css_ir::BreakpointId::new(7),
+ 0,
"tablet",
"52rem",
)],
@@ -2461,13 +2500,13 @@ mod tests {
assert_eq!(pliego_css_ir::CLASS_NAME_FORMAT_VERSION, 1);
assert_eq!(
format!("{:032x}", theme.id().get()),
- "cf5c4c0674fd1c7e5121da0d27222ae0"
+ "eda25b5ed8fa8ce973662d4f18a46bdc"
);
assert_eq!(
format!("{:032x}", style.id.get()),
- "e0b572e3fdfbf091d9a2ddb126278634"
+ "b742ceb589d4f412c6ba77e81f632f53"
);
- assert_eq!(style.id.to_class_name(), "pc_dax2y1pql4op1rjk97yv9e88k");
+ assert_eq!(style.id.to_class_name(), "pc_aukxxmkm8bmauj8duf8zpjdcj");
}
#[test]
@@ -2478,13 +2517,13 @@ mod tests {
assert_eq!(
format!("{:032x}", theme.id().get()),
- "b3d5ad77175995c2b8f51ef7c0d41991"
+ "b98b78da29201938d135b8bb94717788"
);
assert_eq!(
format!("{:032x}", style.id.get()),
- "70cb04ef9bf9621f5826351f1778f68e"
+ "fe3a92576be2bb53e3240249bc45b829"
);
- assert_eq!(style.id.to_class_name(), "pc_6oe73ec16rbb7ublcoa3bpzf2");
+ assert_eq!(style.id.to_class_name(), "pc_f1u1l7d58kemkdqdjie56hdex");
}
#[test]
diff --git a/crates/pliego-css-compiler/tests/container_queries.rs b/crates/pliego-css-compiler/tests/container_queries.rs
index 2f24d14..65a9ced 100644
--- a/crates/pliego-css-compiler/tests/container_queries.rs
+++ b/crates/pliego-css-compiler/tests/container_queries.rs
@@ -20,9 +20,9 @@ fn establishes_and_queries_an_inline_size_container() {
assert_eq!(
format!("{:032x}", style.id.get()),
- "11893074aaaf88f7b356ad6d53a913e2"
+ "78f8ca7e9a1d8b7737ee243d87faf881"
);
- assert_eq!(class, "pc_11dgqogrs5aq1d9ocwyulg6w2");
+ assert_eq!(class, "pc_75tq511l3l9q0mx595ykqafsx");
assert_eq!(
emit_css(&style).expect("container fixture must emit"),
@@ -66,8 +66,8 @@ fn active_theme_owns_viewport_and_container_widths() {
let theme = ThemeRegistry::from_definitions(
seed.tokens().iter().cloned(),
[
- BreakpointDefinition::new(BreakpointId::new(0), "sm", "32rem"),
- BreakpointDefinition::new(BreakpointId::new(1), "md", "44rem"),
+ BreakpointDefinition::new(BreakpointId::new(0), 0, "sm", "32rem"),
+ BreakpointDefinition::new(BreakpointId::new(1), 1, "md", "44rem"),
],
)
.expect("custom theme must be valid");
diff --git a/crates/pliego-css-compiler/tests/property_contract.rs b/crates/pliego-css-compiler/tests/property_contract.rs
index 2c39ebf..0697521 100644
--- a/crates/pliego-css-compiler/tests/property_contract.rs
+++ b/crates/pliego-css-compiler/tests/property_contract.rs
@@ -8,9 +8,10 @@ use pliego_css_compiler::{
emit_css_with_theme, encode_style_ir_with_theme, lower_style_with_theme,
try_encode_style_identity_with_theme, utility_catalog,
};
+use pliego_css_ir::BreakpointId;
use pliego_css_ir::SemanticStyle;
use pliego_css_parser::{format_style_list, parse_style_list};
-use pliego_css_theme::ThemeRegistry;
+use pliego_css_theme::{BreakpointDefinition, ThemeRegistry};
const PROPERTY_CASES: usize = 2_048;
const CONCURRENT_CASES: usize = 192;
@@ -19,6 +20,7 @@ const THREAD_COUNT: usize = 16;
const CORPUS_SEED: u64 = 0x504c_4945_474f_4353;
const PERMUTATION_SEED: u64 = 0x5354_594c_4549_4431;
const CONCURRENCY_SEED: u64 = 0x434f_4e43_5552_5245;
+const BREAKPOINT_ORDER_SEED: u64 = 0x4252_4541_4b50_4f49;
const VARIANT_PREFIXES: &[&str] = &[
"",
@@ -247,6 +249,63 @@ fn non_conflicting_permutations_have_one_identity_and_css() {
}
}
+#[test]
+fn breakpoint_cascade_order_is_independent_of_ids_names_and_input_permutations() {
+ let seed = ThemeRegistry::seed();
+ let mut rng = FixedRng::new(BREAKPOINT_ORDER_SEED);
+ let narrow_names = ["tablet", "zeta-narrow", "screen-a"];
+ let middle_names = ["content-wide", "alpha-middle", "screen-b"];
+ let wide_names = ["desktop", "beta-wide", "screen-c"];
+
+ for case_index in 0..PROPERTY_CASES {
+ let names = [
+ narrow_names[rng.index(narrow_names.len())],
+ middle_names[rng.index(middle_names.len())],
+ wide_names[rng.index(wide_names.len())],
+ ];
+ let mut ids = [7_u16, 40_000, 60_000];
+ rng.shuffle(&mut ids);
+ let mut breakpoints = vec![
+ BreakpointDefinition::new(BreakpointId::new(ids[0]), 0, names[0], "52rem"),
+ BreakpointDefinition::new(BreakpointId::new(ids[1]), 1, names[1], "72rem"),
+ BreakpointDefinition::new(BreakpointId::new(ids[2]), 2, names[2], "80rem"),
+ ];
+ rng.shuffle(&mut breakpoints);
+ let theme = ThemeRegistry::from_definitions(seed.tokens().iter().cloned(), breakpoints)
+ .unwrap_or_else(|error| panic!("case {case_index} theme must build: {error}"));
+
+ let media_source = format!(
+ "{}:grid-cols-2 {}:grid-cols-3 {}:grid-cols-4",
+ names[0], names[1], names[2]
+ );
+ let media_css = full_contract(&theme, &media_source).semantic.css;
+ let media_positions = ["52rem", "72rem", "80rem"].map(|width| {
+ media_css.find(width).unwrap_or_else(|| {
+ panic!("case {case_index} media CSS misses {width}: {media_css}")
+ })
+ });
+ assert!(
+ media_positions.windows(2).all(|pair| pair[0] < pair[1]),
+ "case {case_index} media cascade is not narrow-to-wide: {media_css}"
+ );
+
+ let container_source = format!(
+ "cq-{}:grid-cols-2 cq-{}:grid-cols-3 cq-{}:grid-cols-4",
+ names[0], names[1], names[2]
+ );
+ let container_css = full_contract(&theme, &container_source).semantic.css;
+ let container_positions = ["52rem", "72rem", "80rem"].map(|width| {
+ container_css.find(width).unwrap_or_else(|| {
+ panic!("case {case_index} container CSS misses {width}: {container_css}")
+ })
+ });
+ assert!(
+ container_positions.windows(2).all(|pair| pair[0] < pair[1]),
+ "case {case_index} container cascade is not narrow-to-wide: {container_css}"
+ );
+ }
+}
+
#[test]
fn shared_theme_is_deterministic_across_sixteen_threads() {
let theme = Arc::new(ThemeRegistry::seed());
diff --git a/crates/pliego-css-config/src/dtcg.rs b/crates/pliego-css-config/src/dtcg.rs
index 88dbb74..da2b078 100644
--- a/crates/pliego-css-config/src/dtcg.rs
+++ b/crates/pliego-css-config/src/dtcg.rs
@@ -490,6 +490,8 @@ fn build_theme(document: Value) -> Result {
&mut input_names,
&mut report,
)?;
+ super::rank_breakpoints(&mut breakpoints)
+ .map_err(|error| invalid(&error.name, error.reason))?;
let registry =
ThemeRegistry::from_definitions(tokens, breakpoints).map_err(DtcgError::Registry)?;
verify_profile_theme_id(root, profile, ®istry)?;
@@ -1459,7 +1461,7 @@ fn merge_breakpoint(
.iter_mut()
.find(|definition| definition.name == name)
{
- *existing = BreakpointDefinition::new(existing.id, name, value);
+ *existing = BreakpointDefinition::new(existing.id, existing.cascade_rank, name, value);
return Ok(());
}
let next = breakpoints
@@ -1471,6 +1473,7 @@ fn merge_breakpoint(
.ok_or_else(|| invalid("breakpoints", "breakpoint ID space is exhausted"))?;
breakpoints.push(BreakpointDefinition::new(
BreakpointId::new(next),
+ 0,
name,
value,
));
diff --git a/crates/pliego-css-config/src/lib.rs b/crates/pliego-css-config/src/lib.rs
index 8200774..256ff62 100644
--- a/crates/pliego-css-config/src/lib.rs
+++ b/crates/pliego-css-config/src/lib.rs
@@ -341,10 +341,12 @@ fn apply_breakpoints(
.iter_mut()
.find(|definition| definition.name == name)
{
- *existing = BreakpointDefinition::new(existing.id, name, min_width);
+ *existing =
+ BreakpointDefinition::new(existing.id, existing.cascade_rank, name, min_width);
} else {
definitions.push(BreakpointDefinition::new(
BreakpointId::new(next_id),
+ 0,
name,
min_width,
));
@@ -355,9 +357,69 @@ fn apply_breakpoints(
})?;
}
}
+ rank_breakpoints(definitions).map_err(|error| ConfigError::InvalidValue {
+ table,
+ name: error.name,
+ reason: error.reason,
+ })
+}
+
+#[derive(Debug)]
+struct BreakpointOrderError {
+ name: String,
+ reason: &'static str,
+}
+
+fn rank_breakpoints(definitions: &mut [BreakpointDefinition]) -> Result<(), BreakpointOrderError> {
+ let mut ranked = Vec::with_capacity(definitions.len());
+ let mut common_unit = None;
+ for (index, definition) in definitions.iter().enumerate() {
+ let Some((number, unit)) = breakpoint_number_and_unit(&definition.min_width) else {
+ return Err(BreakpointOrderError {
+ name: definition.name.clone(),
+ reason: "breakpoint must be a positive CSS length in px, rem, em, ch, or vw",
+ });
+ };
+ if let Some(expected) = common_unit {
+ if expected != unit {
+ return Err(BreakpointOrderError {
+ name: definition.name.clone(),
+ reason: "all breakpoints must use one common unit so cascade order is unambiguous",
+ });
+ }
+ } else {
+ common_unit = Some(unit);
+ }
+ ranked.push((index, number, definition.name.clone(), definition.id));
+ }
+ ranked.sort_by(|left, right| {
+ left.1
+ .total_cmp(&right.1)
+ .then_with(|| left.2.cmp(&right.2))
+ .then_with(|| left.3.cmp(&right.3))
+ });
+ for (rank, (index, _, _, _)) in ranked.into_iter().enumerate() {
+ definitions[index].cascade_rank =
+ u16::try_from(rank).map_err(|_| BreakpointOrderError {
+ name: "".to_owned(),
+ reason: "breakpoint cascade rank space is exhausted",
+ })?;
+ }
Ok(())
}
+fn breakpoint_number_and_unit(value: &str) -> Option<(f64, &str)> {
+ let split = value
+ .find(|character: char| !character.is_ascii_digit() && character != '.')
+ .unwrap_or(value.len());
+ let (number, unit) = value.split_at(split);
+ let number = number
+ .parse::()
+ .ok()
+ .filter(|number| number.is_finite() && *number > 0.0)?;
+ matches!(unit, "px" | "rem" | "em" | "ch" | "vw").then_some((number, unit))
+}
+
fn normalize_name(table: &'static str, name: &str) -> Result {
let name = name.trim();
if name.is_empty() {
@@ -528,6 +590,50 @@ Content_Wide = "72rem"
assert!(registry.breakpoint_by_name("content-wide").is_some());
}
+ #[test]
+ fn breakpoint_ids_do_not_determine_narrow_to_wide_cascade_ranks() {
+ let registry = parse_str(
+ r#"
+schema = 1
+extends = "seed"
+
+[breakpoints]
+content-wide = "72rem"
+tablet = "52rem"
+"#,
+ )
+ .expect("ordered breakpoints");
+ let tablet = registry.breakpoint_by_name("tablet").expect("tablet");
+ let content_wide = registry
+ .breakpoint_by_name("content-wide")
+ .expect("content-wide");
+
+ assert!(
+ content_wide.id < tablet.id,
+ "IDs remain stable by canonical name"
+ );
+ assert!(
+ tablet.cascade_rank < content_wide.cascade_rank,
+ "cascade rank follows 52rem before 72rem"
+ );
+ }
+
+ #[test]
+ fn mixed_breakpoint_units_fail_closed_instead_of_guessing_cascade_order() {
+ let error = parse_str(
+ r#"
+schema = 1
+extends = "seed"
+
+[breakpoints]
+tablet = "900px"
+"#,
+ )
+ .expect_err("mixed units must be ambiguous");
+ assert!(matches!(error, ConfigError::InvalidValue { .. }));
+ assert!(error.to_string().contains("one common unit"));
+ }
+
#[test]
fn seed_tokens_can_be_overridden() {
let registry = parse_str(
diff --git a/crates/pliego-css-lsp/Cargo.toml b/crates/pliego-css-lsp/Cargo.toml
index da4a0a8..65dd9a3 100644
--- a/crates/pliego-css-lsp/Cargo.toml
+++ b/crates/pliego-css-lsp/Cargo.toml
@@ -19,9 +19,13 @@ name = "pliego-css-lsp"
path = "src/main.rs"
[dependencies]
+pliego-css-compiler.workspace = true
+pliego-css-config.workspace = true
+pliego-css-ir.workspace = true
pliego-css-io.workspace = true
pliego-css-parser.workspace = true
pliego-css-source.workspace = true
+pliego-css-theme.workspace = true
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
sha2 = "0.10"
diff --git a/crates/pliego-css-lsp/README.md b/crates/pliego-css-lsp/README.md
index 4561332..10b7d84 100644
--- a/crates/pliego-css-lsp/README.md
+++ b/crates/pliego-css-lsp/README.md
@@ -1,12 +1,9 @@
# pliego-css-lsp
`pliego-css-lsp` is the public-preview standard-LSP transport for PliegoCSS Rust literals. It keeps
-open documents in memory, negotiates UTF-16 positions, publishes scanner and `FMT001` diagnostics,
-returns collision-safe whole-literal formatting edits, and delegates completion/hover metadata to
-the versioned `pliego-cssc` catalog/explain JSON contracts. With an explicit generated Project
-Index, Go to Definition follows verified source sites and manifests to final CSS declarations.
-Syntactically valid literals also reuse the compiler's diagnostic schema for bounded theme-aware
-semantic checks instead of maintaining an editor-only utility validator.
+open documents in memory, negotiates UTF-16 positions, publishes scanner, formatter, semantic, and
+`pcx` diagnostics, and serves completion and hover from the same in-process compiler engine used by
+the CLI and watch mode. It does not launch `pliego-cssc` or create temporary source files.
Print the installed package version without entering stdio mode:
@@ -17,7 +14,7 @@ pliego-css-lsp --version
Run it over stdio from the project root:
```console
-pliego-css-lsp --pliego-cssc /exact/path/to/pliego-cssc --seed
+pliego-css-lsp --seed
```
Enable source-to-CSS navigation without repository inference:
@@ -26,6 +23,9 @@ Enable source-to-CSS navigation without repository inference:
pliego-css-lsp --project-index target/site/assets/pliego.index.json --seed
```
-Without `--seed`, the delegated compiler uses normal conventional theme discovery. `--config`
-selects one exact theme file. The server currently requests full document synchronization and only
-offers semantic features inside statically supported `pc!`/`pcx!` Rust string literals.
+Without `--seed`, the server discovers `pliego.theme.toml` at the initialized workspace root.
+`--config` selects one exact theme file. `--pliego-cssc` and `PLIEGO_CSSC` remain accepted during
+the 0.1 compatibility window, but are ignored: semantic analysis is always in-process.
+
+The server requests full document synchronization and offers semantic features only inside
+statically supported `pc!`/`pcx!` Rust string literals.
diff --git a/crates/pliego-css-lsp/src/lib.rs b/crates/pliego-css-lsp/src/lib.rs
index cda16cc..6750cc6 100644
--- a/crates/pliego-css-lsp/src/lib.rs
+++ b/crates/pliego-css-lsp/src/lib.rs
@@ -4,19 +4,18 @@
use std::collections::BTreeMap;
use std::ffi::OsString;
-use std::fmt::Write as _;
-use std::fs::{OpenOptions, remove_file};
-use std::io::{self, BufRead, BufReader, Read, Write};
-#[cfg(unix)]
-use std::os::unix::fs::OpenOptionsExt;
+use std::io::{self, BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
-use std::process::{Command, Output, Stdio};
-use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
+use pliego_css_compiler::{
+ AnalysisHost, PcxError, PcxRequest, emit_css_with_theme, utility_catalog,
+ utility_descriptor_for_style_item,
+};
+use pliego_css_ir::Diagnostic;
use pliego_css_parser::{format_style_list, parse_style_list};
use pliego_css_source::{InvocationKind, ScanReport, SourceRange, StyleLiteral, scan_source_named};
use serde_json::{Value, json};
@@ -25,20 +24,15 @@ use sha2::{Digest, Sha256};
mod project_index;
const VERSION: &str = env!("CARGO_PKG_VERSION");
-const MAX_SEMANTIC_CACHE_ITEMS: usize = 4_096;
const MAX_SEMANTIC_CHECKS_PER_DOCUMENT: usize = 256;
const MAX_CONFIG_FINGERPRINT_BYTES: u64 = 1024 * 1024;
const MAX_HEADER_BYTES: usize = 64 * 1024;
-const MAX_TOOL_OUTPUT_BYTES: usize = 4 * 1024 * 1024;
const SEMANTIC_DEBOUNCE: Duration = Duration::from_millis(150);
-const SEMANTIC_PROCESS_POLL: Duration = Duration::from_millis(10);
-const TOOL_PROCESS_TIMEOUT: Duration = Duration::from_secs(30);
-static TEMP_SOURCE_COUNTER: AtomicU64 = AtomicU64::new(0);
-/// Exact process and theme selection used by delegated tooling requests.
+/// Exact theme and navigation selection used by in-process analysis.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ServerConfig {
- /// Exact `pliego-cssc` executable name or path.
+ /// Legacy 0.1 compatibility option. The in-process engine never executes this path.
pub compiler: OsString,
/// Use the deterministic seed theme instead of conventional discovery.
pub seed: bool,
@@ -119,7 +113,8 @@ struct Server {
config: ServerConfig,
root: PathBuf,
documents: BTreeMap,
- catalog: Option<(String, Vec)>,
+ catalog: Option>,
+ analysis: Option<(String, AnalysisHost)>,
shutdown: bool,
}
@@ -130,165 +125,94 @@ impl Server {
root: PathBuf::from("."),
documents: BTreeMap::new(),
catalog: None,
+ analysis: None,
shutdown: false,
}
}
- fn tool(&self, command: &str, extra: &[&str]) -> Result {
- let mut process = Command::new(&self.config.compiler);
- process.arg(command).args(extra).current_dir(&self.root);
- if self.config.seed {
- process.arg("--seed");
- } else if let Some(config) = &self.config.config {
- process.arg("--config").arg(config);
- }
- process.arg("--format").arg("json");
- let output = bounded_process_output(
- &mut process,
- "pliego-cssc delegated tool",
- TOOL_PROCESS_TIMEOUT,
- )?;
- if !output.status.success() {
- return Err(String::from_utf8_lossy(&output.stderr).trim().to_owned());
+ fn catalog(&mut self) -> &[Value] {
+ if self.catalog.is_none() {
+ self.catalog = Some(
+ utility_catalog()
+ .iter()
+ .map(|descriptor| {
+ json!({
+ "pattern":descriptor.pattern(),
+ "matchName":descriptor.match_name(),
+ "example":descriptor.example(),
+ "summary":descriptor.summary(),
+ })
+ })
+ .collect(),
+ );
}
- serde_json::from_slice(&output.stdout)
- .map_err(|error| format!("invalid pliego-cssc JSON: {error}"))
+ self.catalog.as_deref().unwrap_or_default()
}
- fn catalog(&mut self) -> Result<&[Value], String> {
+ fn analysis(&mut self) -> Result<&mut AnalysisHost, String> {
let identity = cache_identity(&self.root, &self.config)?;
- if self.catalog.as_ref().map(|(key, _)| key) != Some(&identity) {
- let document = self.tool("catalog", &[])?;
- if document.get("schemaVersion") != Some(&json!(3)) {
- return Err("unsupported catalog schema".into());
- }
- self.catalog = Some((
+ if self.analysis.as_ref().map(|(key, _)| key) != Some(&identity) {
+ self.analysis = Some((
identity,
- document
- .get("utilities")
- .and_then(Value::as_array)
- .ok_or("catalog has no utilities")?
- .clone(),
+ AnalysisHost::new(load_theme(&self.root, &self.config)?),
));
}
- Ok(self.catalog.as_ref().map_or(&[], |(_, values)| values))
+ Ok(&mut self.analysis.as_mut().expect("analysis inserted").1)
}
}
struct SemanticEngine {
config: ServerConfig,
- cache: BTreeMap, String>>,
+ identity: Option,
+ host: AnalysisHost,
}
impl SemanticEngine {
fn new(config: ServerConfig) -> Self {
Self {
config,
- cache: BTreeMap::new(),
+ identity: None,
+ host: AnalysisHost::default(),
}
}
- fn findings(
- &mut self,
- root: &Path,
- style: &str,
- cancellation: &Cancellation,
- job: &SemanticJob,
- ) -> Result | 64rem |
-| md | 48rem |
-| sm | 40rem |
+| Cascade rank | Name | Minimum width |
+|---:|---|---|
+| 0 | sm | 40rem |
+| 1 | md | 48rem |
+| 2 | lg | 64rem |
diff --git a/docs/reference/lsp-diagnostic-corpus.md b/docs/reference/lsp-diagnostic-corpus.md
index c15adee..e1496ad 100644
--- a/docs/reference/lsp-diagnostic-corpus.md
+++ b/docs/reference/lsp-diagnostic-corpus.md
@@ -38,7 +38,7 @@ message, and style byte range. Source cases use `check --source`; `FMT001` uses
`fmt --source --check`. The LSP session then applies each input as a new full-document version,
waits for that version's publication, rejects additional diagnostic codes, and requires exact CLI
code, message, severity, category, suggestion, typed replacement, and UTF-16 range projection.
-The same session independently compares compiler-backed cross-clause `PCX003` over all those fields.
+The same session independently compares shared-engine cross-clause `PCX003` over all those fields.
The corpus exposed three prior mismatches: parser failures used the complete Rust literal range in the
LSP even though the parser and CLI retained an exact `Diagnostic.span`; zero-width `PCS008` ranges
@@ -50,9 +50,9 @@ still fail closed to the complete token.
## Boundary
This closes the current typed utility-authoring families: PCS, PSC, PCR001, FMT001, and PCX003.
-`PCL001`/`PCL002` remain operational fallback paths for unavailable or invalid tools and bounded
-editor limits, so the same session covers them through invalid-JSON and 256-literal fault injection
-rather than authoring corpus inputs. Future codes must add corpus or fault-injection coverage before
-this coverage claim can remain true. Adding a case is append-only inside schema 2 when its fields and
-interpretation are unchanged. Changing the root shape, range meaning, or equality dimensions
-requires a new schema version and migration note.
+`PCL001`/`PCL002` remain operational paths for invalid theme/configuration state and bounded editor
+limits rather than authoring corpus inputs. The session exercises the 256-literal `PCL002` boundary
+and passes a nonexistent legacy compiler path to prove it is not executed. Future authoring codes
+must add corpus coverage before this claim can remain true. Adding a case is append-only inside
+schema 2 when its fields and interpretation are unchanged. Changing the root shape, range meaning,
+or equality dimensions requires a new schema version and migration note.
diff --git a/docs/reference/lsp.md b/docs/reference/lsp.md
index c4ebc36..d377173 100644
--- a/docs/reference/lsp.md
+++ b/docs/reference/lsp.md
@@ -1,195 +1,112 @@
# Language Server Protocol transport
-`pliego-css-lsp` is the initial standard-LSP transport for PliegoCSS Rust literals. It implements a
-bounded subset of the official [LSP 3.18 specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/)
-over framed JSON-RPC on stdin/stdout. The transport owns protocol and document-coordinate concerns;
-it does not fork the compiler's utility catalog or explanation semantics.
+`pliego-css-lsp` implements a bounded subset of the official
+[LSP 3.18 specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/)
+over framed JSON-RPC on stdin/stdout. Protocol transport and document coordinates remain adapter
+concerns; parsing, semantic lowering, conditional analysis, catalog resolution, and physical CSS
+emission come from the shared in-process `AnalysisHost`.
## Start the server
-Install or build `pliego-css-lsp` and `pliego-cssc` from the exact same PliegoCSS version, then start
-the server from the project root:
+Install `pliego-css-lsp` from the same PliegoCSS compatibility unit used by the project:
```console
-pliego-css-lsp --pliego-cssc /exact/path/to/pliego-cssc --seed
+cargo install pliego-css-lsp --version '=0.1.0-rc.2' --locked
+pliego-css-lsp --seed
```
-`PLIEGO_CSSC` is the default compiler selector when `--pliego-cssc` is omitted. Choose exactly one
-theme mode:
+Choose exactly one theme mode:
```console
-# Deterministic built-in seed theme
+# Deterministic built-in registry
pliego-css-lsp --seed
-# Exact project theme
+# Exact project theme; relative paths resolve from initialize.rootUri
pliego-css-lsp --config pliego.theme.toml
-# No theme flag: conventional discovery from the initialize root
+# No theme flag: discover initialize.rootUri/pliego.theme.toml, else seed
pliego-css-lsp
# Optional verified source-to-final-CSS navigation
pliego-css-lsp --project-index target/site/assets/pliego.index.json
```
-The server runs delegated compiler commands from the `initialize.rootUri` file directory. Compiler
-startup failures and unsupported catalog/explain schema versions fail the affected request instead
-of returning guessed metadata.
+The 0.1 options `--pliego-cssc PATH` and `PLIEGO_CSSC` remain syntactically accepted so existing
+clients keep starting, but the path is ignored and excluded from cache identity. The LSP never
+executes a compiler child.
## Implemented protocol surface
The initialize result selects `positionEncoding: "utf-16"`, requests full-document synchronization,
-and advertises completion, hover, document formatting, and optionally definition when a Project
-Index is configured. The current closed method set is:
+and advertises completion, hover, document formatting, and optionally definition.
| Direction | Method | Contract |
|---|---|---|
| client to server | `initialize`, `initialized`, `shutdown`, `exit` | Process lifecycle and one file-root workspace. |
| client to server | `textDocument/didOpen`, `didChange`, `didClose` | In-memory, monotonically versioned, full-document buffers. |
-| server to client | `textDocument/publishDiagnostics` | Rust parse/scanner diagnostics, PCS syntax diagnostics, compiler-backed per-literal semantic diagnostics, and `FMT001` drift. |
-| request | `textDocument/completion` | Catalog schema 3 examples and summaries, with a whole-utility `textEdit`. |
-| request | `textDocument/hover` | Explain schema 2 pattern, summary, and emitted CSS. |
-| request | `textDocument/definition` | Exact Project Index source site to integrity-bound manifest and final CSS declaration links. Advertised only with `--project-index`. |
-| request | `textDocument/formatting` | Complete Rust literal-token edits produced from canonical decoded values. |
-
-Input frames are capped at 16 MiB. LSP lines and characters are zero-based UTF-16 positions; the
-server converts them from exact UTF-8 source bytes and rejects a position that splits a surrogate
-pair. This is intentionally different from CLI diagnostic columns, which count Unicode scalar
-values.
-
-Completion and hover operate only inside statically supported `pc!` and `pcx!` string literals. Raw
-strings and ordinary strings without escapes have an exact source-to-decoded mapping. A cooked
-literal containing escapes fails closed for cursor features because a decoded offset cannot safely
-be treated as a Rust source offset. Document formatting remains safe in that case because it
-replaces the complete literal token with a newly escaped ordinary Rust string.
-
-## Consistency boundary
-
-- Completion invokes `pliego-cssc catalog --format json` once and requires catalog schema 3.
-- Hover invokes `pliego-cssc explain --style ... --format json` and requires explain schema 2.
-- Open-buffer syntax and formatting use the same `pliego-css-source` scanner and
- `pliego-css-parser` canonical formatter as the CLI.
-- Each syntactically valid visible literal is checked through same-version
- `pliego-cssc --diagnostic-format json check --style ...`. Schema-1 code, severity, message,
- suggestion, replacement, and decoded style range are validated before projection into LSP
- UTF-16 coordinates. Cooked strings with escapes use the safe whole-literal range.
-- Semantic results are cached by exact decoded literal, capped at 4,096 entries, and each document
- is capped at 256 semantic literal checks. Exceeding that boundary emits `PCL002` rather than
- starting an unbounded number of compiler processes.
-- Full-buffer changes replace the pending semantic job for that URI and restart a 150 ms debounce.
- Due jobs run on a dedicated worker instead of the JSON-RPC thread. A per-URI version registry
- terminates and reaps the direct compiler child when its snapshot becomes stale; cancelled work is
- neither cached nor published. Stdout and stderr are drained concurrently so a full process pipe
- cannot prevent cancellation.
-- Multi-clause `pcx!` invocations are projected into one bounded synthetic Rust source containing
- only visible utility literals. The worker delegates that document to same-version
- `pliego-cssc check --source`, accepts only compiler `PCX003` cross-clause findings, and maps the
- synthetic branch range back to the original complete Rust literal token. The temporary source is
- created exclusively and removed after the compiler exits; it is never written into the project.
-- Formatting returns edits only; it never writes a source file. The editor remains responsible for
- applying the version-bound edit set.
-- Definition consumes Project Index schema 1 or 2 instead of scanning repository structure. It
- verifies the Asset Plan, source snapshot, bundle ownership, manifest/CSS hashes, schema-5 physical
- contract, and exact declaration ranges on every request. Any stale or mixed artifact fails the
- request closed.
-
-The current diagnostic pass covers Rust parsing, macro extraction, PliegoCSS syntax, canonical
-formatting, and theme-aware compiler validation of every bounded individual literal. Local findings
-publish immediately; uncached compiler checks are debounced and run serially in the background.
-Cancellation is scoped to the direct `pliego-cssc` child for one URI/version; PliegoCSS does not
-claim arbitrary descendant process-tree termination for a replacement compiler executable.
-Compiler-backed `PCX003`
-parity now covers semantic overlaps and exact duplicates across independently selectable clauses.
-The versioned twenty-case negative corpus freezes exact code, message, range, severity, suggestion,
-and typed replacement equality for PCS001–PCS012, PSC001–PSC006, PCR001, and FMT001. The same
-session compares PCX003 and fault-injects PCL001/PCL002, closing the current CLI/editor diagnostic
-families. New diagnostic codes must add coverage before that claim can remain true.
+| server to client | `textDocument/publishDiagnostics` | Scanner, PCS semantic, shared `pcx`, and `FMT001` findings. |
+| request | `textDocument/completion` | Compiler catalog examples and summaries with a whole-utility `textEdit`. |
+| request | `textDocument/hover` | Shared catalog pattern, summary, and compiler-emitted CSS. |
+| request | `textDocument/definition` | Verified Project Index source site to final CSS declaration links. |
+| request | `textDocument/formatting` | Complete Rust literal-token edits from canonical decoded values. |
+
+Input frames are capped at 16 MiB. Lines and characters are zero-based UTF-16 positions. The server
+converts them from exact UTF-8 source bytes and rejects a position that splits a surrogate pair.
+Cursor features fail closed for cooked literals containing escapes because decoded offsets cannot
+be treated as Rust source offsets. Formatting remains safe because it replaces the complete token.
+
+## Shared-engine consistency boundary
+
+- Completion reads `utility_catalog()` directly; it does not deserialize catalog JSON.
+- Hover resolves the accepted `StyleItem`, lowers it through `AnalysisHost`, and emits CSS with the
+ host's exact theme.
+- Every syntactically valid literal uses the same parser, semantic cache, and stable `Diagnostic`
+ type as CLI and watch.
+- Macro expansion, source scanning, and LSP diagnostics all send the same `PcxRequest` to the
+ bounded conditional frontend. `PCX003` targets the original conflicting branch token; no
+ synthetic source file exists.
+- The host cache is theme-bound and capped. A theme ID change invalidates semantic and physical
+ entries. Each document remains capped at 256 semantic checks and emits `PCL002` at the boundary.
+- Full-buffer changes replace the pending job for that URI and restart a 150 ms debounce. Due work
+ runs on a dedicated in-process worker. Version checks before and after analysis suppress stale
+ publication without child-process cancellation.
+- Definition consumes Project Index schema 1 or 2 and verifies adjacent source, Asset Plan,
+ manifest, CSS hashes, and exact declaration ranges on each request.
+
+The versioned twenty-case corpus freezes exact code, message, range, severity, suggestion, and
+replacement equality for PCS001–PCS012, PSC001–PSC006, PCR001, and FMT001. The same session compares
+CLI and LSP `PCX003`, exercises the 256-literal guard, passes a deliberately nonexistent legacy
+compiler path, and reports `compilerProcesses: 0`.
## Current non-goals
-This candidate does not yet provide incremental document changes, workspace folders, code actions,
-semantic tokens, or a hosted multi-editor matrix. Those remain
-release gates; the existence
-of the stdio server and initial VS Code package does not close the full F6 editor-tooling task.
+This candidate does not yet provide incremental text changes, multi-root workspaces, code actions,
+semantic tokens, signed editor packages, or a hosted multi-editor matrix. Full-document sync and a
+single local root remain explicit prerelease limits.
-Run the reproducible local process gate with:
+Run the reproducible gate with:
```console
pnpm integration:lsp
```
-`pliego-css-lsp --version` prints the exact package version and exits. It is an exclusive process
-option; combining it with server, compiler, theme, or Project Index arguments fails instead of
-starting an ambiguously configured transport.
-
-The gate builds both binaries with Rust 1.85, performs one framed stdio session, and asserts UTF-16
-initialization, `FMT001` publication, whole-literal formatting, exact completion replacement,
-compiler-backed hover CSS, clean shutdown, and zero stderr.
-The same session configures a synthetic integrity-bound Project Index and requires Go to Definition
-to select the exact physical declaration in its verified stylesheet.
-It then sends a burst of full-buffer versions ending in an unknown utility, requires the protocol
-to answer a formatting request before semantic publication, and accepts exactly one version-10
-`PCS001` result with the compiler's message and exact UTF-16 range. No stale semantic result may
-survive the version check. The gate then starts a 30-second blocking compiler proxy, replaces that
-document version, proves the stale PID exits, and requires a fresh compiler result within five
-seconds. A subsequent version 13 contains an exact semantic duplicate across two
-independent `pcx!` clauses; the gate requires one compiler `PCX003` mapped to the complete literal
-token in the second clause. Finally, diagnostic corpus schema 2 applies twenty additional buffer
-versions and compares every typed field against the CLI contract, then injects invalid compiler JSON
-and the 256-literal bound to verify PCL001/PCL002. See
-[the corpus contract](./lsp-diagnostic-corpus.md).
-
-## VS Code client candidate
-
-The unreleased client under `editors/vscode` follows the official VS Code language-client pattern
-and selects only file-backed Rust documents. Configure `pliegocss.server.path` and
-`pliegocss.compiler.path` to same-version external binaries; the extension does not download or
-update either executable. Theme mode is explicitly `discover`, `seed`, or `config`. Optional
-`pliegocss.projectIndex.path` forwards one exact workspace-relative or absolute index path and is
-empty by default.
-
-The client runs in the workspace extension host, uses the first local workspace folder as process
-working directory, restarts on configuration changes or through **PliegoCSS: Restart Language
-Server**, and declares virtual and untrusted workspaces unsupported. Build and inspect its VSIX with:
+The gate builds Rust 1.85 binaries, compares the LSP diagnostic corpus against CLI output, verifies
+UTF-16 ranges, completion, hover, formatting, Project Index navigation, bounded diagnostics, current
+version publication, clean shutdown, zero stderr, and zero LSP compiler processes.
-```console
-pnpm integration:vscode
-```
+## Editor clients
-That gate type-checks the client, tests fail-closed argument construction, bundles its production
-entry, verifies the exact four-file extension payload, packages a VSIX below 256 KiB, and confirms
-that no native server is embedded. The VSIX hash is evidence for one run, not a reproducibility
-claim because the packaging tool may encode timestamps.
+The VS Code manifest retains its historical compiler-path property as deprecated configuration, and
+the Neovim table tolerates a legacy `compiler` member. Both clients ignore those values and launch
+only the language server. `pliego-cssc` remains a separate build and CI tool.
-Run the real extension-host gate with:
+Run their repository gates with:
```console
+pnpm integration:vscode
pnpm integration:vscode-host
-```
-
-The first run downloads the exact VS Code 1.105.1 test runtime through the official Microsoft test
-harness; later runs reuse the ignored local cache. The gate builds external Rust 1.85 server and
-compiler binaries, loads the development extension in a real workspace host, follows Go to
-Definition to the exact physical CSS range, edits the Rust buffer, and requires the compiler-backed
-`PCS001` diagnostic followed by the exact cross-clause `PCX003` diagnostic. The extension itself
-still downloads no server or compiler.
-
-## Neovim client candidate
-
-The unreleased module under `editors/neovim` uses Neovim's built-in LSP client. It requires explicit
-external server/compiler paths and accepts the same discover/seed/config theme modes plus an optional
-Project Index. Its `FileType` autocmd starts only for Rust buffers; invalid paths or modes fail before
-spawning a server. The 3,466-byte client payload is exactly `README.md` plus
-`lua/pliegocss/init.lua` and contains no download surface.
-
-Run the real second-editor gate with:
-
-```console
pnpm integration:neovim
```
-The gate pins Neovim 0.12.4 and verifies the official archive SHA-256 before extraction into an
-ignored test cache. Windows x64 uses `nvim-win64.zip`; Debian x86-64 uses
-`nvim-linux-x86_64.tar.gz`. In both hosts Neovim opens a file-backed Rust buffer, starts the external
-PliegoCSS binaries, follows Project Index definition to the physical CSS range, then observes exact
-`PCS001` and cross-clause `PCX003` diagnostics. Runtime download belongs only to the test harness;
-the Lua client never downloads Neovim or PliegoCSS binaries.
+See [Editor setup](../getting-started/editor-setup.md) and
+[the diagnostic corpus](./lsp-diagnostic-corpus.md).
diff --git a/docs/reference/public-api.md b/docs/reference/public-api.md
index 96b6027..c097fc3 100644
--- a/docs/reference/public-api.md
+++ b/docs/reference/public-api.md
@@ -345,6 +345,10 @@ They expose `pub` items because sibling packages need them. Their mutable IR, pa
catalog model, binary encoder/decoder, scanner DTOs/functions, and composition helpers are not
covered by the minimal `0.1.x` application promise unless a later RC document explicitly promotes
an item.
+The G2 `CompileRequest`, `CompileResult`, `AnalysisHost`, `PhysicalRulePlanner`, and `PcxRequest`
+surfaces are likewise exact-version advanced APIs shared by the packaged CLI, watch, macro, scanner,
+and LSP adapters. Their architectural role is documented and tested; they are not yet a stable
+third-party library promise. See [Shared compiler engine](./engine-api.md).
Persisted bytes and schemas named above remain versioned regardless of this Rust-API classification.
## Change policy
diff --git a/docs/reference/standards-provenance.md b/docs/reference/standards-provenance.md
index c4ebbaf..5e6fe12 100644
--- a/docs/reference/standards-provenance.md
+++ b/docs/reference/standards-provenance.md
@@ -27,10 +27,10 @@ The root has exactly `schemaVersion` (`1`) and `sources`. Every source has exact
| `kind` | `software`, `dataset`, or `specification`. |
| `version` | Exact numeric or SemVer-like version; no ranges or moving tags. |
| `status` | Exact publication/package status. |
-| `license` | Reviewed license identifier. |
+| `license` | Reviewed license identifier, including Apache-2.0, ISC, MIT, MPL-2.0, and the named W3C licenses. |
| `url` | Canonical, credential-free HTTPS source; unique. |
-| `relationship` | Why PliegoCSS records the source. |
-| `usage` | `build-time`, `frozen-data`, `implemented-contract`, `audit-reference`, or `benchmark-only`. |
+| `relationship` | Why PliegoCSS records the source, including the isolated browser-certification toolchain. |
+| `usage` | `build-time`, `frozen-data`, `implemented-contract`, `audit-reference`, `benchmark-only`, or `test-only`. |
| `evidence[]` | Existing repository-relative files that prove the relationship. |
| `integrity[]` | Unique subject plus reviewed npm SRI, SHA-256 metadata, or Cargo checksum; empty only for versioned specifications. |
@@ -49,7 +49,10 @@ benchmark artifact.
| WCAG | `2.2` Recommendation | Authoritative accessibility reference; a static PliegoCSS pass is not a WCAG conformance claim. |
| CSS Color Adjustment | Level 1 Candidate Recommendation Snapshot | Forced-colors reference. |
| Media Queries | Level 5 Working Draft | User-preference media-query reference; draft status is explicit. |
-| Tailwind CSS and CLI | `4.3.2`, MIT | Performance/ergonomics baseline only; package SRI plus the full transitive pnpm lock SHA-256 are frozen. It is not a PliegoCSS runtime or backend dependency. |
+| Tailwind CSS and CLI | current `4.3.3`, upstream `v3-lts` `3.4.19`, frozen `4.3.2`; MIT | Benchmark Authority v2 lanes only; package SRI plus the full transitive pnpm lock SHA-256 are frozen. They are not PliegoCSS runtime or backend dependencies. |
+| Playwright | `1.61.1`, Apache-2.0 | Test-only Chromium, Firefox, and WebKit orchestration for Browser/output certification v1; `playwright-core` is pinned too. |
+| pixelmatch | `7.2.0`, ISC | Test-only bounded perceptual PNG comparison; the original artifact hashes are retained. |
+| pngjs | `7.0.0`, MIT | Test-only PNG decoding and diff encoding for certification evidence. |
The DTCG status comes from the official [Format](https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/),
[Color](https://www.w3.org/community/reports/design-tokens/CG-FINAL-color-20251028/), and
diff --git a/docs/reference/style-id-format-v2.md b/docs/reference/style-id-format-v2.md
index fd216c3..f2e5149 100644
--- a/docs/reference/style-id-format-v2.md
+++ b/docs/reference/style-id-format-v2.md
@@ -45,7 +45,7 @@ their raw bytes, and removes exact duplicate records. The resulting order and co
Authoring order, assignment-table order, and duplicate provenance therefore do not affect identity.
The explicit theme-format field prevents the same 128 theme bits from being interpreted under an
-unreported ThemeId contract. The current stream embeds `THEME_ID_FORMAT_VERSION = 2` and the
+unreported ThemeId contract. The current stream embeds `THEME_ID_FORMAT_VERSION = 3` and the
theme binary format.
## Assignment record
@@ -208,9 +208,9 @@ Current frozen examples are:
| Theme/style | StyleId | Class |
|---|---|---|
-| Seed theme, `flex gap-4` | `70cb04ef9bf9621f5826351f1778f68e` | `pc_6oe73ec16rbb7ublcoa3bpzf2` |
-| Minimal compatibility theme, `flex gap-gutter tablet:grid` | `e0b572e3fdfbf091d9a2ddb126278634` | `pc_dax2y1pql4op1rjk97yv9e88k` |
-| Seed theme, `container-inline cq-sm:grid` | `11893074aaaf88f7b356ad6d53a913e2` | `pc_11dgqogrs5aq1d9ocwyulg6w2` |
+| Seed theme, `flex gap-4` | `fe3a92576be2bb53e3240249bc45b829` | `pc_f1u1l7d58kemkdqdjie56hdex` |
+| Minimal compatibility theme, `flex gap-gutter tablet:grid` | `b742ceb589d4f412c6ba77e81f632f53` | `pc_aukxxmkm8bmauj8duf8zpjdcj` |
+| Seed theme, `container-inline cq-sm:grid` | `78f8ca7e9a1d8b7737ee243d87faf881` | `pc_75tq511l3l9q0mx595ykqafsx` |
## Document versions
@@ -220,14 +220,14 @@ Artifacts that expose identity now report all three related versions at the top
{
"styleIdFormatVersion": 2,
"classNameFormatVersion": 1,
- "themeIdFormatVersion": 2
+ "themeIdFormatVersion": 3
}
```
-These fields are required in CSS manifest schemas 3, 4, and 5, inspection schema 2, catalog schema 3,
+These fields are required in CSS manifest schemas 3, 4, and 5, inspection schema 3, catalog schema 4,
and explain schema 2. They let a consumer reject mixed identity contracts before interpreting a
`styleId` or class. Structured diagnostics remain schema 1, the theme configuration remains schema
-1, the theme identity and binary remain format 1, declarative bundle plans use schemas 1/2,
+1, the theme identity and binary use format 3, declarative bundle plans use schemas 1/2,
the reachability sidecar is schema 1, nested manifest graphs are schemas 1/2, and the PliegoRS
source-surface contract remains schema 2.
diff --git a/docs/reference/theme-schema.md b/docs/reference/theme-schema.md
index a8aee48..80ab148 100644
--- a/docs/reference/theme-schema.md
+++ b/docs/reference/theme-schema.md
@@ -106,12 +106,17 @@ tablet = "52rem" # adds a new responsive variant
Accepted units are `px`, `rem`, `em`, `ch`, and `vw`. Units are lowercase and a value must be a
finite number greater than zero. Percentages, unitless values, media-query expressions, and values
-such as `0rem` are rejected.
+such as `0rem` are rejected. Every active breakpoint must use the same unit. Mixed units are rejected
+because their runtime order depends on font, container, or viewport state and cannot produce one
+deterministic narrow-to-wide cascade.
New breakpoint IDs are assigned deterministically after the seed IDs. Duplicate names or IDs are
fatal registry errors. A custom breakpoint cannot use a built-in condition name such as `hover`,
`focus`, `dark`, `motion-safe`, or `placeholder`; configuration rejects the collision instead of
-changing the meaning of that variant. Seed breakpoint names such as `sm` may be overridden.
+changing the meaning of that variant. Seed breakpoint names such as `sm` may be overridden. Stable
+IDs are lookup identity only. A separate, unique `cascade_rank` is derived from the numeric minimum
+width and controls media/container emission from narrowest to widest; equal widths use canonical
+name/ID tie-breaking. Catalog schema 4 and inspection schema 3 expose this rank explicitly.
## Build artifact and identity
@@ -122,12 +127,13 @@ lowercase `ThemeId`, so a format migration cannot alias an older artifact. The b
magic header, format version, size limits, UTF-8 validation, registry validation, and a stored-ID
check when decoded.
-ThemeId format 2 hashes an explicitly domain-separated and length-framed canonical stream with
+ThemeId format 3 hashes an explicitly domain-separated and length-framed canonical stream with
SHA-256. The first 16 digest bytes, interpreted big-endian, form the `ThemeId` (with all-zero remapped
to one). Token and breakpoint records include explicit section/record tags, counts, fixed-width IDs,
-and UTF-8 byte lengths. Input definitions are sorted before encoding, so order does not affect the
-stream; any canonical value change does. Because the stored identity semantics changed, the current
-theme binary is format 2. Format-1 binaries are rejected explicitly and must be regenerated.
+breakpoint cascade ranks, and UTF-8 byte lengths. Input definitions are sorted before encoding, so
+order does not affect the stream; any canonical value change does. Because breakpoint order is now
+part of stored identity, the current theme binary is format 3. Older binaries are rejected
+explicitly and must be regenerated.
The decoder rejects artifacts larger than 16 MiB, more than 65,536 entries in either the token or
breakpoint list, and names or values larger than 1 MiB each. It also rejects truncation, unknown
diff --git a/docs/releases/0.1.0-rc.1.md b/docs/releases/0.1.0-rc.1.md
index d23abf5..68a9ffc 100644
--- a/docs/releases/0.1.0-rc.1.md
+++ b/docs/releases/0.1.0-rc.1.md
@@ -1,5 +1,8 @@
# PliegoCSS 0.1.0-rc.1
+> Historical release record. It has no current promotion authority. See the
+> schema-3 [machine readiness record](../product/release-readiness-0.1.0.json).
+
The first PliegoCSS release candidate established the standards-first CSS
compiler, verifier, and evidence pipeline written in Rust. It supported
ordinary CSS adoption alongside an optional compile-time typed Rust frontend.
diff --git a/docs/releases/0.1.0-rc.2.md b/docs/releases/0.1.0-rc.2.md
index 8725043..18c7a91 100644
--- a/docs/releases/0.1.0-rc.2.md
+++ b/docs/releases/0.1.0-rc.2.md
@@ -1,5 +1,9 @@
# PliegoCSS 0.1.0-rc.2
+> Historical release record. It describes the published RC.2 tag but has no
+> current promotion authority. See the schema-3
+> [machine readiness record](../product/release-readiness-0.1.0.json).
+
The second PliegoCSS release candidate is a standards-first CSS compiler,
verifier, and evidence pipeline written in Rust. It supersedes the historical
`rc.1` tag with corrected release provenance, a coordinated crates.io unit,
diff --git a/docs/site/audit-and-transform.md b/docs/site/audit-and-transform.md
new file mode 100644
index 0000000..86b53bf
--- /dev/null
+++ b/docs/site/audit-and-transform.md
@@ -0,0 +1,24 @@
+
+
+# Standards-first, not framework-first.
+
+Analyze standard CSS, enforce policy, and transform output without claiming ownership of authored source.
+
+## Bounded classification {#classifier}
+
+The classifier recognizes deliberate standard-CSS surfaces and reports unsupported constructs instead of guessing. A versioned corpus controls the claim.
+
+## Deterministic transformation {#transform}
+
+Target browsers, minify, and check output drift. Add --check in CI to remain read-only.
+
+```console
+pliego-cssc transform-css --input app.css \
+ --output dist/app.css --targets modern --format minified
+```
+
+## Compatibility, accessibility, and budgets {#policy}
+
+Policy documents bind findings to explicit targets and limits. The same canonical findings feed terminals, JSON automation, and SARIF.
diff --git a/docs/site/cascade.md b/docs/site/cascade.md
new file mode 100644
index 0000000..947f208
--- /dev/null
+++ b/docs/site/cascade.md
@@ -0,0 +1,24 @@
+
+
+# Explain the winning declaration.
+
+Inspect authored CSS by element and longhand to see matching selectors, precedence, and the final winner.
+
+## Ask one precise question {#query}
+
+Provide a CSS file, a bounded element description, and one longhand. The command reports candidates instead of simulating an entire browser.
+
+```console
+pliego-cssc explain-cascade --input app.css \
+ --element 'button#save.action' --property background-color
+```
+
+## See why it won {#winner}
+
+Origin, importance, layer, specificity, and source position are surfaced in comparison order with a stable machine-readable form.
+
+## Keep the model honest {#boundary}
+
+This is a static bounded inspector. Runtime DOM state, animation interpolation, adopted sheets, and script mutation remain browser responsibilities.
diff --git a/docs/site/composition.md b/docs/site/composition.md
new file mode 100644
index 0000000..1763821
--- /dev/null
+++ b/docs/site/composition.md
@@ -0,0 +1,32 @@
+
+
+# Compose by meaning, not source order.
+
+Compatible slots commute. Conflicting slots require an explicit branch-over-base operation.
+
+## Equivalent input, equivalent identity {#commute}
+
+Utilities that occupy independent semantic slots canonicalize to the same identity even when authored in a different order.
+
+```text
+flex gap-4 p-6
+p-6 flex gap-4
+```
+
+## Ambiguous lists are rejected {#conflict}
+
+p-4 p-6 does not mean last one wins. Both declarations claim the same slot under the same condition, so compilation stops.
+
+```console
+pliego-cssc check --style "p-4 p-6" --seed
+```
+
+## Overrides are explicit operations {#override}
+
+When an application intentionally replaces one semantic assignment with another, use composition so the override relationship remains visible and inspectable.
+
+```console
+pliego-cssc compile --compose "p-4" "p-6" --seed
+```
diff --git a/docs/site/control-artifacts.md b/docs/site/control-artifacts.md
new file mode 100644
index 0000000..2aa69ea
--- /dev/null
+++ b/docs/site/control-artifacts.md
@@ -0,0 +1,29 @@
+
+
+# Publish the receipt last.
+
+Controlled builds stage related outputs, lock the destination, verify hashes, compensate failures, and expose one completed receipt.
+
+## One rollback-capable group {#group}
+
+CSS, source map, style manifest, Token Graph, findings, control manifest, and receipt are generated from one exact input snapshot.
+
+```console
+pliego-cssc compile --source src --output dist/app.css \
+ --manifest dist/app.manifest.json --control-dir dist
+```
+
+## Check without writing {#check}
+
+The same command with --check recomputes every artifact and fails on byte drift without taking publication locks or replacing files.
+
+```console
+pliego-cssc compile --source src --output dist/app.css \
+ --manifest dist/app.manifest.json --control-dir dist --check
+```
+
+## Completion is explicit {#receipt}
+
+Consumers treat the fixed receipt as the publication boundary. Its absence means the group is incomplete, even if individual files are present.
diff --git a/docs/site/critical-css.md b/docs/site/critical-css.md
new file mode 100644
index 0000000..6382a90
--- /dev/null
+++ b/docs/site/critical-css.md
@@ -0,0 +1,19 @@
+
+
+# Prove what rendered above the fold.
+
+Join observed browser behavior to project topology and retained style identity without letting an observation silently authorize removal.
+
+## Observe a real route {#observe}
+
+Capture viewport, route, interaction state, loaded assets, matched styles, and source identities from a browser run against the built application.
+
+## Join exact universes {#join}
+
+Evidence is accepted only when its project index, asset plan, reachability, and CSS identities match the build under review.
+
+## Observation does not equal deletion {#authority}
+
+A style unseen in one scope remains unknown outside that scope. Explicit retention and reachability policies control removal authority.
diff --git a/docs/site/editor-setup.md b/docs/site/editor-setup.md
new file mode 100644
index 0000000..4bcc9d2
--- /dev/null
+++ b/docs/site/editor-setup.md
@@ -0,0 +1,27 @@
+
+
+# Put the compiler engine in the feedback loop.
+
+Use `pliego-css-lsp` for diagnostics, completion, hover evidence, and catalog-aware authoring without launching the build CLI.
+
+## VS Code extension {#vscode}
+
+The maintained extension launches `pliego-css-lsp`, discovers the project theme, and renders diagnostics in Rust source.
+
+```console
+pnpm --filter pliegocss-vscode package
+```
+
+## Any LSP client {#generic}
+
+Launch the server over stdio from the same workspace used by CI. The protocol stays editor-neutral.
+
+```console
+pliego-css-lsp --seed
+```
+
+## Bound in-process work {#safety}
+
+Document versions debounce for 150 ms, stale results are suppressed, semantic caches are bounded, and each document stops at 256 checks with `PCL002`.
diff --git a/docs/site/evidence.md b/docs/site/evidence.md
new file mode 100644
index 0000000..84a6e9f
--- /dev/null
+++ b/docs/site/evidence.md
@@ -0,0 +1,24 @@
+
+
+# A green build should explain itself.
+
+Artifacts carry hashes, source lineage, graphs, findings, and receipts designed for replay.
+
+## Canonical manifests {#manifest}
+
+Style identity, class identity, theme identity, origins, conditions, physical declarations, and final CSS hashes live in a deterministic document.
+
+```console
+pliego-cssc compile --source src --seed --theme \
+ --output dist/app.css --manifest dist/app.manifest.json
+```
+
+## Receipt-last publication {#receipts}
+
+Controlled operations stage output, lock the destination, verify inputs, publish artifacts, and write the receipt last.
+
+## Measured, inherited, pending, uncertain {#benchmarks}
+
+Release readiness preserves the evidence class. Old hosted evidence never silently passes changed source.
diff --git a/docs/site/first-audit.md b/docs/site/first-audit.md
new file mode 100644
index 0000000..7738c4a
--- /dev/null
+++ b/docs/site/first-audit.md
@@ -0,0 +1,28 @@
+
+
+# Learn before changing.
+
+Run a read-only CSS audit, inspect canonical findings, then decide whether transformation or policy is justified.
+
+## Read the first report {#human}
+
+Human output is optimized for local diagnosis. Every finding carries a stable code and source location rather than an opaque score.
+
+```console
+pliego-cssc audit --input app.css \
+ --targets baseline-widely --format human
+```
+
+## Feed automation safely {#automation}
+
+Canonical JSON and SARIF preserve the same finding model for CI, code scanning, and archival evidence.
+
+```console
+pliego-cssc audit --input app.css --targets modern --format sarif > findings.sarif
+```
+
+## Unknown means unknown {#unknown}
+
+The standard-CSS classifier is bounded. Unsupported selectors, properties, or values fail closed under compatibility policy rather than being treated as safe.
diff --git a/docs/site/getting-started.md b/docs/site/getting-started.md
new file mode 100644
index 0000000..98a6544
--- /dev/null
+++ b/docs/site/getting-started.md
@@ -0,0 +1,36 @@
+
+
+# Start with the CSS you already have.
+
+Audit first, compile when it adds value, and keep the browser contract ordinary.
+
+## Install the release candidate {#install}
+
+The complete 0.1.0-rc.2 compatibility unit is published on crates.io and replayed from the registry with Rust 1.85.
+
+```console
+cargo install pliego-cssc --version '=0.1.0-rc.2' --locked
+```
+
+## Audit ordinary CSS {#audit}
+
+Start without changing authoring. The analyzer emits human, canonical JSON, or SARIF findings and fails closed on unknown surfaces.
+
+```console
+pliego-cssc audit --input app.css \
+ --targets baseline-widely --format human
+```
+
+## Add typed identities selectively {#compile}
+
+Rust literals are validated at compile time and compile to static standards-compliant CSS. There is no styling runtime.
+
+```rust
+use pliego_css::{pc, Style};
+
+const BUTTON: Style = pc!(
+ "inline-flex gap-2 rounded-lg bg-accent px-4 py-2"
+);
+```
diff --git a/docs/site/installation.md b/docs/site/installation.md
new file mode 100644
index 0000000..310dbe3
--- /dev/null
+++ b/docs/site/installation.md
@@ -0,0 +1,29 @@
+
+
+# Choose an adoption surface.
+
+Install the CLI for any project, add the Rust crates only when typed authoring belongs in the application.
+
+## CLI-only adoption {#cli}
+
+The command-line compiler can audit, transform, explain, bundle, and migrate standard CSS without adding a runtime dependency to the application.
+
+```console
+cargo install pliego-cssc --version '=0.1.0-rc.2' --locked
+pliego-cssc --version
+```
+
+## Typed Rust authoring {#rust}
+
+Add pliego-css and its macro crate to applications that want compile-time style identities. The emitted result remains ordinary CSS.
+
+```console
+[dependencies]
+pliego-css = "=0.1.0-rc.2"
+```
+
+## Pin the exact release identity {#pin}
+
+Before the public release, use the signed project tag plus Cargo.lock. Do not track a moving branch in a production build.
diff --git a/docs/site/integrations/build-tools.md b/docs/site/integrations/build-tools.md
new file mode 100644
index 0000000..99b7c6e
--- /dev/null
+++ b/docs/site/integrations/build-tools.md
@@ -0,0 +1,26 @@
+
+
+# Integrate through files, not hidden hooks.
+
+Vite, webpack, esbuild, CI systems, and custom pipelines consume explicit CSS, manifests, findings, maps, and asset plans.
+
+## Run PliegoCSS as a producer {#producer}
+
+Invoke compile or transform before the application bundler and pass its ordinary CSS output through the normal asset graph.
+
+```text
+"scripts": {
+ "css": "pliego-cssc compile --source src --output dist/pliego.css",
+ "build": "npm run css && vite build"
+}
+```
+
+## Consume stable sidecars {#consumer}
+
+Use the numbered JSON schemas for manifests, Token Graphs, findings, reachability, project indexes, and Asset Plans instead of scraping terminal text.
+
+## Keep topology in its owner {#ownership}
+
+A framework adapter may export route and component reachability. PliegoCSS validates and projects that sidecar but does not crawl application internals.
diff --git a/docs/site/integrations/plain-html.md b/docs/site/integrations/plain-html.md
new file mode 100644
index 0000000..72d78f5
--- /dev/null
+++ b/docs/site/integrations/plain-html.md
@@ -0,0 +1,28 @@
+
+
+# Audit and transform without a framework.
+
+Use PliegoCSS as a standards control layer around an ordinary stylesheet and static HTML build.
+
+## Start with emitted CSS {#audit}
+
+No adapter is required. Audit the exact stylesheet the browser loads and archive canonical findings beside the build.
+
+```console
+pliego-cssc audit --input public/app.css --targets baseline-widely
+```
+
+## Transform explicitly {#transform}
+
+Compatibility transformation and minification are separate from analysis so a read-only audit never rewrites authored output.
+
+```console
+pliego-cssc transform-css --input src/app.css \
+ --output public/app.css --targets modern --format minified
+```
+
+## Typed authoring is optional {#typed}
+
+Rust applications can compile typed styles into the same static site, but plain HTML consumers only need the final CSS asset.
diff --git a/docs/site/integrations/pliegors.md b/docs/site/integrations/pliegors.md
new file mode 100644
index 0000000..3157d10
--- /dev/null
+++ b/docs/site/integrations/pliegors.md
@@ -0,0 +1,23 @@
+
+
+# Native together. Independent by contract.
+
+PliegoRS is the first native consumer, not the only target. It supplies topology; PliegoCSS supplies style identity and artifacts.
+
+## Explicit ownership {#boundary}
+
+PliegoRS owns routes, pages, assets, and browser lifecycle. PliegoCSS owns parsing, semantic conflicts, CSS, manifests, and policy findings.
+
+## Route and island bundles {#bundles}
+
+A reachability sidecar projects application-owned topology into selected CSS bundles without PliegoCSS pretending to understand the framework.
+
+## Replayed from published crates {#proof}
+
+The maintained fixture locks PliegoRS 0.0.2 registry packages and verifies the remote-reachable framework revision separately.
+
+```console
+PLIEGORS_ROOT=/path/to/pliegors pnpm integration:pliegors
+```
diff --git a/docs/site/manifests.md b/docs/site/manifests.md
new file mode 100644
index 0000000..e1df74e
--- /dev/null
+++ b/docs/site/manifests.md
@@ -0,0 +1,24 @@
+
+
+# Trace source intent into emitted bytes.
+
+Manifest schema 5 connects source origins, semantic assignments, final selectors, physical declarations, and CSS ranges.
+
+## Identity chain {#identity}
+
+StyleId identifies canonical semantics. Class identity, theme identity, configuration, and final CSS hash bind that style into one build.
+
+```console
+pliego-cssc compile --source src --manifest dist/app.manifest.json \
+ --manifest-version 5 --output dist/app.css
+```
+
+## Physical lineage {#physical}
+
+Generated custom properties and synthesized declarations retain contributors, importance, conditions, selector, property, value, and output byte range.
+
+## Request the schema you consume {#versions}
+
+Schema 3 supports style manifests, schema 4 adds reachability ownership, and schema 5 adds closed physical tracing. Consumers must pin their contract.
diff --git a/docs/site/migration.md b/docs/site/migration.md
new file mode 100644
index 0000000..7507e3f
--- /dev/null
+++ b/docs/site/migration.md
@@ -0,0 +1,23 @@
+
+
+# Adopt without surrendering the exit.
+
+Inventory first, generate bounded plans, preserve originals, and detect drift immediately before publication.
+
+## Read-only discovery {#inventory}
+
+Tailwind, Sass, CSS Modules, and standard CSS can be inventoried before any replacement plan exists.
+
+```console
+pliego-cssc migration-project-plan ./migration-declaration.json
+```
+
+## Reversible replacements {#reversible}
+
+Plans bind source and destination hashes. Apply and rollback use adjacent locks, pre-publication revalidation, backups, and group compensation.
+
+## No speculative codemods {#limits}
+
+Unsupported or context-dependent constructs stay in the inventory. The migration layer does not infer framework semantics it cannot prove.
diff --git a/docs/site/migration/css-modules.md b/docs/site/migration/css-modules.md
new file mode 100644
index 0000000..a6d19a4
--- /dev/null
+++ b/docs/site/migration/css-modules.md
@@ -0,0 +1,23 @@
+
+
+# Preserve component ownership.
+
+Discover module exports and usage while keeping application-owned component topology separate from CSS semantics.
+
+## Inventory exported classes {#inventory}
+
+The scanner identifies bounded module class definitions and their source locations without assuming a particular bundler naming strategy.
+
+```console
+pliego-cssc migration-inventory css-modules src/Card.module.css
+```
+
+## Let the application own reachability {#ownership}
+
+Route and component reachability comes from the framework or product index, not from filename heuristics inside the CSS compiler.
+
+## Mixed adoption is first-class {#mixed}
+
+A project can retain modules for component styles, audit their emitted CSS, and introduce typed identities only for new or shared surfaces.
diff --git a/docs/site/migration/sass.md b/docs/site/migration/sass.md
new file mode 100644
index 0000000..d6a8c73
--- /dev/null
+++ b/docs/site/migration/sass.md
@@ -0,0 +1,23 @@
+
+
+# Separate syntax conversion from semantic adoption.
+
+Inventory variables, nesting, mixins, imports, and generated surfaces without pretending every Sass abstraction is a utility.
+
+## Map the source surface {#inventory}
+
+The inventory records statically visible Sass roles and unresolved dynamic behavior so the team can choose what stays authored CSS.
+
+```console
+pliego-cssc migration-inventory sass styles/main.scss
+```
+
+## Ordinary CSS is a valid destination {#css-first}
+
+Use the standard-CSS transformer for syntax and compatibility work. Adopt typed PliegoCSS styles only where identity and provenance pay for themselves.
+
+## Keep originals and rollback evidence {#rollback}
+
+Applied replacements preserve source backups and hash-bound rollback records. A changed destination aborts rather than overwriting concurrent edits.
diff --git a/docs/site/migration/tailwind.md b/docs/site/migration/tailwind.md
new file mode 100644
index 0000000..762ebf5
--- /dev/null
+++ b/docs/site/migration/tailwind.md
@@ -0,0 +1,27 @@
+
+
+# Inventory Tailwind before translating it.
+
+Recover the real utility surface, classify supported roles, and keep unsupported framework behavior visible.
+
+## Scan without replacement {#inventory}
+
+The inventory recognizes bounded class-bearing surfaces and reports candidates with source ranges. It does not rewrite template semantics during discovery.
+
+```console
+pliego-cssc migration-inventory tailwind src/App.tsx
+```
+
+## Generate a hash-bound project plan {#plan}
+
+A declaration selects roots and destinations. The plan records exact source and output hashes before any replacement can be applied.
+
+```console
+pliego-cssc migration-project-plan migration.json
+```
+
+## Preserve unsupported behavior {#gaps}
+
+Plugins, generated variants, arbitrary selectors, and runtime class construction remain explicit migration findings unless a project-owned adapter proves them.
diff --git a/docs/site/reference/configuration.md b/docs/site/reference/configuration.md
new file mode 100644
index 0000000..3b6e8a0
--- /dev/null
+++ b/docs/site/reference/configuration.md
@@ -0,0 +1,19 @@
+
+
+# Configuration is part of identity.
+
+Theme, target, format, manifests, reachability, pruning, and token selections are explicit, validated, and hash-bound.
+
+## Conventional TOML discovery only {#discovery}
+
+Without --config, --tokens, or --seed, the CLI searches for pliego.theme.toml from the workspace and input package roots. Ambiguous candidates fail.
+
+## Selectors are mutually exclusive {#exclusive}
+
+--config, --tokens, and --seed cannot be combined. JSON token documents are never discovered implicitly.
+
+## Bind the complete choice {#hash}
+
+Targets, format, emission settings, manifest version, pruning, theme bytes, resolver selections, and reachability bytes participate in the control configuration hash.
diff --git a/docs/site/reference/diagnostics.md b/docs/site/reference/diagnostics.md
new file mode 100644
index 0000000..b4e96b8
--- /dev/null
+++ b/docs/site/reference/diagnostics.md
@@ -0,0 +1,23 @@
+
+
+# Stable codes, precise ranges.
+
+Parser, source scanner, composition, policy, invocation, migration, and formatter failures share a canonical transport.
+
+## Human diagnostics {#human}
+
+Terminal output favors the next corrective action while retaining the stable code, severity, file, line, column, and byte range.
+
+## Canonical JSON {#json}
+
+JSON mode writes one schema-1 document to stderr on failure and leaves stdout empty. Consumers should branch on codes, not localized prose.
+
+```console
+pliego-cssc --diagnostic-format json check --style "p-4 p-6" --seed
+```
+
+## SARIF for code scanning {#sarif}
+
+Audit findings can be serialized as SARIF while preserving canonical rule identity and source regions for hosted review systems.
diff --git a/docs/site/release-readiness.md b/docs/site/release-readiness.md
new file mode 100644
index 0000000..43a0697
--- /dev/null
+++ b/docs/site/release-readiness.md
@@ -0,0 +1,19 @@
+
+
+# Release truth bound to one exact candidate.
+
+The current 1.0 promotion state is blocked; no historical report can override these source-bound gates.
+
+## Candidate identity {#candidate}
+
+Target 0.1.0; candidate 0.1.0-rc.2; tag v0.1.0-rc.2; commit 064dcbce96a3a5cc97a940d07566c008bb5e2d3e; Git tree 010a64f5d0bbd142a1d015fda4ccf573cc6e4133; observed 2026-07-22T14:10:00Z; expires 2026-07-29T14:10:00Z.
+
+## Readiness dimensions {#dimensions}
+
+Technical: blocked; Operational: blocked; Adoption: blocked; Authorized: blocked; Promotion: blocked.
+
+## Current blockers {#blockers}
+
+g0-corrections-on-candidate (pending): G0 corrections are not part of the exact RC.2 candidate. They require a newly named candidate plus hosted replay before they can become release evidence. browser-release-matrix (blocked): Every required hosted browser lane is not configured for the exact candidate source; local or historical browser runs have no promotion authority. registry-replay-refresh (pending): RC.2 was previously replayed from crates.io, but schema 3 requires a fresh, unexpired, artifact-hashed replay for the exact promotion candidate. production-deployment-refresh (pending): The production site has historical deployment evidence, but a current exact-source edge and browser replay with hashed artifacts is missing. signed-binary-distribution (blocked): Signed native binaries, checksums, SBOMs, attestations, and the npm launcher required by the LTS distribution boundary do not exist yet. external-adoption-evidence (pending): The required external interviews, real incidents, and pilots have not been recorded as reviewed evidence. final-promotion-authorization (not-authorized): Mario has not explicitly authorized publishing final 0.1.0 for this exact source commit and Git tree.
diff --git a/docs/site/standard-css/policies.md b/docs/site/standard-css/policies.md
new file mode 100644
index 0000000..a1b4c46
--- /dev/null
+++ b/docs/site/standard-css/policies.md
@@ -0,0 +1,28 @@
+
+
+# Make browser support and budgets executable.
+
+Compatibility, accessibility, payload, package, route, and ownership policies turn expectations into reviewable inputs.
+
+## Version the target {#compatibility}
+
+baseline-widely and modern are frozen policy profiles with explicit browser and source-data identities. They do not drift silently with a dependency update.
+
+```console
+pliego-cssc compatibility --targets baseline-widely
+```
+
+## Bind budgets to owners {#budgets}
+
+Direct files can name budget subjects manually. Asset Plans use an ownership sidecar to aggregate package and composed-route costs without guessing responsibility.
+
+```console
+pliego-cssc audit --asset-plan dist/pliego.assets.json \
+ --ownership pliego.ownership.json --budget-policy budgets.json
+```
+
+## Audit static accessibility contracts {#accessibility}
+
+Contrast and interaction guards use explicit policy plus token evidence. Dynamic visual behavior still requires browser and assistive-technology testing.
diff --git a/docs/site/syntax.md b/docs/site/syntax.md
new file mode 100644
index 0000000..e235405
--- /dev/null
+++ b/docs/site/syntax.md
@@ -0,0 +1,31 @@
+
+
+# A finite language with visible edges.
+
+Utilities lower into semantic slots, conditions, and typed values before any CSS serialization occurs.
+
+## Utility plus typed value {#shape}
+
+Spacing, color, typography, layout, effects, and interactivity draw from one validated theme registry. Arbitrary escape hatches remain policy-visible.
+
+```text
+flex items-center gap-4 rounded-lg bg-surface px-4 py-2 text-sm
+```
+
+## Conditions are structure {#conditions}
+
+Responsive, state, attribute, container, layer, and media variants become normalized condition paths. Their order is not incidental string decoration.
+
+```text
+md:grid-cols-2 hover:bg-accent-strong aria-[expanded=true]:ring-2
+```
+
+## Importance follows the physical effect {#important}
+
+A trailing exclamation mark marks the semantic contribution important. Synthesized effects such as the composed box-shadow preserve that authority in final CSS and lineage.
+
+```text
+ring-2! shadow-md!
+```
diff --git a/docs/site/themes.md b/docs/site/themes.md
new file mode 100644
index 0000000..287c602
--- /dev/null
+++ b/docs/site/themes.md
@@ -0,0 +1,32 @@
+
+
+# Resolve tokens before styles.
+
+TOML themes and DTCG Resolver documents feed the same typed registry, identity, graph, and emitted variables.
+
+## Extend the seed {#toml}
+
+A project theme can override or add typed colors, spacing, radii, typography, and breakpoints without changing utility grammar.
+
+```text
+schema = 1
+extends = "seed"
+
+[tokens.color]
+brand = "oklch(62% 0.18 250)"
+```
+
+## Select DTCG contexts explicitly {#dtcg}
+
+Resolver inputs are never auto-discovered. Modifier selections participate in configuration identity even when two contexts resolve to equal values.
+
+```console
+pliego-cssc compile --tokens product.resolver.json \
+ --token-input appearance=dark --style "bg-brand"
+```
+
+## Publish the complete Token Graph {#graph}
+
+Controlled output binds selected CSS to canonical token provenance, aliases, permutations, and reachability-aware usage evidence.
diff --git a/docs/site/tooling/cli.md b/docs/site/tooling/cli.md
new file mode 100644
index 0000000..78df506
--- /dev/null
+++ b/docs/site/tooling/cli.md
@@ -0,0 +1,28 @@
+
+
+# One CLI, explicit modes.
+
+Compile, inspect, audit, transform, explain, bundle, watch, migrate, format, plan, and verify with strict argument contracts.
+
+## Discover the real command surface {#discover}
+
+Root and command help are generated from the maintained command synopsis. Unknown options, duplicate single-value options, and missing values fail.
+
+```console
+pliego-cssc --help
+pliego-cssc compile --help
+```
+
+## Select diagnostic transport globally {#diagnostics}
+
+Human and JSON diagnostics can be requested before or after the command without stealing option values that happen to resemble flags.
+
+```console
+pliego-cssc --diagnostic-format json check --style "p-4 p-6" --seed
+```
+
+## Use --check in CI {#determinism}
+
+Commands that publish controlled or generated artifacts expose a read-only comparison path. CI should verify the checked-in or staged output, not regenerate it silently.
diff --git a/docs/site/tooling/lsp.md b/docs/site/tooling/lsp.md
new file mode 100644
index 0000000..0f48670
--- /dev/null
+++ b/docs/site/tooling/lsp.md
@@ -0,0 +1,19 @@
+
+
+# Compiler feedback without editor subprocesses.
+
+The LSP uses the same in-process `AnalysisHost` as CLI and watch without launching `pliego-cssc` or writing synthetic source files.
+
+## Diagnostics, completion, and hover {#features}
+
+Visible `pc!` and `pcx!` literals receive shared diagnostics, catalog-backed completion, and compiler-emitted hover CSS under the exact active theme.
+
+## Suppress stale document work {#cancellation}
+
+Document versions are debounced for 150 ms, and version checks around bounded in-process analysis prevent obsolete results from being published.
+
+## Bound analysis {#limits}
+
+Each document is capped at 256 semantic checks, the shared cache is bounded and theme-invalidated, and excessive input emits `PCL002`.
diff --git a/docs/site/tooling/repair.md b/docs/site/tooling/repair.md
new file mode 100644
index 0000000..7fddece
--- /dev/null
+++ b/docs/site/tooling/repair.md
@@ -0,0 +1,29 @@
+
+
+# Plan first. Authorize exact bytes.
+
+Generate bounded repair proposals, bind authorization to source hashes, verify in staging, then publish receipts.
+
+## Join findings to a proposal {#plan}
+
+Repair plans name exact finding codes, files, byte ranges, replacements, expected hashes, and verification commands.
+
+```console
+pliego-cssc plan --findings findings.json \
+ --proposal proposal.json --source-root .
+```
+
+## Verify before mutation {#dry-run}
+
+Dry-run applies the authorized patch inside a staging copy and executes bounded Rust or browser checks without replacing the source tree.
+
+```console
+pliego-cssc fix --plan plan.json --findings findings.json \
+ --source-root . --dry-run
+```
+
+## Time and output are part of safety {#resource}
+
+Test, browser, toolchain, and version subprocesses use explicit deadlines and output limits while the repair lock is held.
diff --git a/docs/site/tooling/watch.md b/docs/site/tooling/watch.md
new file mode 100644
index 0000000..dca6366
--- /dev/null
+++ b/docs/site/tooling/watch.md
@@ -0,0 +1,24 @@
+
+
+# Keep the last complete truth.
+
+Poll coherent snapshots, republish only valid groups, and retain the last valid output across transient edits.
+
+## Watch inputs and output {#run}
+
+The watcher accepts the same source, theme, target, format, manifest, reachability, and controlled-output options as compile.
+
+```console
+pliego-cssc watch --source src --theme \
+ --output dist/app.css --manifest dist/app.manifest.json
+```
+
+## Confirm one coherent snapshot {#snapshot}
+
+Inputs are observed twice before compilation so an editor save sequence does not publish a mixture of old and new files.
+
+## Never replace validity with a partial edit {#last-valid}
+
+A parser error, invalid token document, or failed group publication leaves the previous complete artifact set in place.
diff --git a/docs/site/typed-styles.md b/docs/site/typed-styles.md
new file mode 100644
index 0000000..37f5172
--- /dev/null
+++ b/docs/site/typed-styles.md
@@ -0,0 +1,32 @@
+
+
+# Types at author time. CSS at run time.
+
+Stable 128-bit identities connect Rust source, emitted classes, manifests, and physical declarations.
+
+## One literal, one stable identity {#pc}
+
+pc! accepts one visible string literal. The macro parses semantics, rejects conflicting slots, and returns a sealed Style.
+
+```rust
+const CARD: Style = pc!(
+ "grid gap-4 rounded-xl border border-line p-6 shadow-md"
+);
+```
+
+## Visible state spaces {#pcx}
+
+pcx! compiles every visible branch combination and selects one identity at runtime. Independent clauses that can collide are rejected.
+
+```rust
+let style = pcx!(
+ "block",
+ if active { "opacity-100" } else { "opacity-50" }
+);
+```
+
+## Follow the physical effect {#lineage}
+
+Manifest schema 5 joins semantic declarations to final rules, conditions, byte ranges, and synthesized effects such as box-shadow.
diff --git a/docs/site/variants.md b/docs/site/variants.md
new file mode 100644
index 0000000..a00d322
--- /dev/null
+++ b/docs/site/variants.md
@@ -0,0 +1,31 @@
+
+
+# Conditions without string-order folklore.
+
+Every variant joins a typed condition path with deterministic normalization and specificity behavior.
+
+## Responsive paths {#responsive}
+
+Theme breakpoints are validated once and emitted as ordered media conditions. Assignments at different breakpoints do not conflict.
+
+```text
+grid-cols-1 md:grid-cols-2 lg:grid-cols-3
+```
+
+## State and attributes {#state}
+
+Pseudo-class and bounded attribute variants stay in semantic IR so policy and physical lineage can explain their selectors.
+
+```text
+hover:bg-accent focus-visible:ring-2 data-[state=open]:opacity-100
+```
+
+## Cascade layers {#layers}
+
+Fixed layer variants model author-controlled precedence without allowing arbitrary source-order dependence to leak into identity.
+
+```text
+layer-components:bg-surface layer-utilities:p-4
+```
diff --git a/docs/status/f0-verification.md b/docs/status/f0-verification.md
index 911e0fc..4cf60b0 100644
--- a/docs/status/f0-verification.md
+++ b/docs/status/f0-verification.md
@@ -8,7 +8,7 @@ Date: 2026-07-12
- Cargo 1.96.0.
- Node 24.16.0.
- pnpm 11.7.0.
-- Tailwind CSS and CLI 4.3.2.
+- Tailwind CSS and CLI 4.3.2 (historical F0 environment; not the current oracle).
- Windows 10.0.26200, x64.
- Intel Core Ultra 9 285H, 16 logical CPUs.
diff --git a/docs/status/f0.md b/docs/status/f0.md
index 25ac6d3..ca56db9 100644
--- a/docs/status/f0.md
+++ b/docs/status/f0.md
@@ -13,7 +13,7 @@ Updated: 2026-07-12
- [x] Built-in condition normalization separated from ordered selector transforms.
- [x] Initial 40-family utility catalog selected.
- [x] Five Tailwind v4 fixtures created.
-- [x] Tailwind v4.3.2 and CLI pinned with pnpm.
+- [x] Tailwind v4.3.2 and CLI pinned with pnpm for the historical F0 snapshot.
- [x] Fresh-process benchmark harness created.
- [x] Full, no-preflight, complete, and Gate-A core profiles defined.
- [x] Workspace and five initial Rust crates scaffolded.
diff --git a/docs/status/f6.md b/docs/status/f6.md
index 5d91ceb..7901a1e 100644
--- a/docs/status/f6.md
+++ b/docs/status/f6.md
@@ -136,38 +136,35 @@ evidence and RC activation remain open**
cross-repository fixture.
- [x] Add opt-in, collision-safe application of Rust source-format fixes while preserving the
existing read-only diagnostic mode.
-- [x] Add a standard stdio LSP transport over catalog schema 3, explain schema 2, in-memory scanner
- diagnostics, UTF-16 conversion, and collision-safe whole-literal formatting edits.
+- [x] Add a standard stdio LSP transport over the shared in-process compiler catalog/engine,
+ in-memory scanner diagnostics, UTF-16 conversion, and collision-safe whole-literal formatting edits.
- [x] Add an unreleased VS Code client candidate for file-backed Rust documents with explicit
- external binary/theme configuration and a typecheck/test/bundle/VSIX package gate.
+ external server/theme configuration and a typecheck/test/bundle/VSIX package gate.
- [x] Add opt-in Project Index schema 1/2 navigation from an exact Rust utility site to every
bundle-qualified physical CSS declaration. The LSP verifies source, Asset Plan, manifest, and CSS
integrity plus schema-5 ranges and fails closed on stale or mixed artifacts.
-- [x] Reuse `pliego-cssc check` diagnostic schema 1 for bounded, cached, theme-aware validation of
- individual visible literals, preserving compiler codes/messages and exact UTF-16 editor ranges.
-- [x] Run a real VS Code 1.105.1 extension host against the development extension, external Rust
- 1.85 server/compiler binaries, and a local workspace. The gate follows the Project Index to the
- exact final CSS range, mutates the open Rust buffer, and observes exact compiler-backed `PCS001`
+- [x] Reuse `AnalysisHost` for bounded, cached, theme-aware validation of individual visible
+ literals, preserving compiler codes/messages and exact UTF-16 editor ranges without CLI children.
+- [x] Run a real VS Code 1.105.1 extension host against the development extension, an external Rust
+ 1.85 server, and a local workspace. The gate follows the Project Index to the exact final CSS
+ range, mutates the open Rust buffer, and observes exact shared-engine `PCS001`
plus cross-clause `PCX003`.
- [x] Debounce theme-aware semantic checks for 150 ms, execute them outside the protocol thread,
and discard results whose full-document version is stale. The Windows and Debian process gate
sends versions 2–10 as a burst, proves request responsiveness, and accepts only version 10.
-- [x] Cancel and reap the direct compiler child when a newer version for the same URI makes its
- snapshot stale. The worker drains stdout and stderr concurrently, does not cache or publish a
- cancelled result, and resumes with the latest debounced job. Windows and Debian start a real
- 30-second blocking proxy, prove its PID exits, and receive the replacement `PCS001` within the
- five-second gate timeout.
-- [x] Reuse `pliego-cssc check --source` for semantic `PCX003` equality across independently
- selectable clauses. One bounded literal-only synthetic document per buffer preserves compiler
- ownership of overlap semantics and maps the conflict back to the exact original branch token;
- both the process gate and VS Code 1.105.1 extension host exercise an exact duplicate.
-- [x] Add a bounded Neovim built-in-LSP client with explicit external binaries and a 3,466-byte
+- [x] Suppress stale in-process semantic work when a newer URI/version supersedes its snapshot.
+ Version checks before and after bounded analysis prevent obsolete publication; the integration
+ gate accepts only the latest `PCS001` and reports zero compiler processes.
+- [x] Share `PcxRequest` semantic `PCX003` analysis across procedural macros, source scanning, and
+ LSP. The editor maps the shared conflict directly to the original branch token without synthetic
+ Rust source or a compiler child.
+- [x] Add a bounded Neovim built-in-LSP client with an explicit external server and a closed
closed payload. Official Neovim 0.12.4 hosts pass Project Index definition, `PCS001`, and `PCX003`
on Windows x64 and Debian x86-64 with pinned archive hashes.
- [x] Freeze diagnostic corpus schema 2 with twenty cases covering PCS001–PCS012, PSC001–PSC006,
PCR001, and FMT001. Windows and Debian compare exact code, message, range, severity, suggestion,
- and typed replacement against CLI schema 1; the same session covers PCX003 and fault-injected
- PCL001/PCL002. Parser spans, including zero-width spans, now project exactly; Rust parse failures
+ and typed replacement against CLI schema 1; the same session covers PCX003 and bounded PCL002.
+ Parser spans, including zero-width spans, now project exactly; Rust parse failures
are typed in both transports and scanner codes survive context prepended by `syn`.
- [x] Publish local unreleased VS Code/Neovim onboarding and LSP troubleshooting, add exclusive
`pliego-css-lsp --version`, and verify all 127 Markdown files plus 442 local links through
diff --git a/docs/status/portability.md b/docs/status/portability.md
index 0380219..48dc418 100644
--- a/docs/status/portability.md
+++ b/docs/status/portability.md
@@ -1,220 +1,104 @@
# Portability contract
-Updated: 2026-07-15
+Updated: 2026-07-22
-Status: **executable contract implemented; hosted matrix evidence and Cloudflare remain open**
+Status: **executable contract and hosted CI matrix implemented; release-candidate binding remains
+open**
-## Configured matrix
+## Authority
-The Rust CI matrix targets:
+`.github/workflows/ci.yml` runs the same portability contract on Ubuntu, Windows, and macOS with
+Rust 1.85.0 and 1.96.0. Node.js 22.13.0 executes the byte-level vector on every Rust/OS lane. A run
+is evidence only for its exact `head_sha`; a green branch run does not certify an older tag and does
+not promote `0.1.0-rc.2`.
-- Ubuntu, Windows, and macOS;
-- Rust 1.85.0 (MSRV) and 1.96.0 (current pinned toolchain);
-- workspace tests and Clippy with warnings denied;
-- Node.js 22.13.0 executing the same portability contract on every host. This matches the minimum
- engine required by the pinned pnpm 11.7.0 toolchain.
+The separate browser-output workflow owns the G4 Windows/Linux/macOS × Chromium/Firefox/WebKit
+matrix. Portability proves compiler and artifact identity; browser output proves rendered behavior.
+Neither substitutes for a named release-candidate replay.
-The WASM job also checks `pliego-css` with Rust 1.85.0 for
-`wasm32-unknown-unknown` before running the existing Rust 1.96 zero-overhead micro-fixture. This is
-not a claim of WASI, `no_std`, every WASM target, or a deployed Cloudflare Workers build.
-
-The repository currently has no configured Git remote, so this document distinguishes the checked-in
-matrix from hosted green-run evidence. The current canonical-token-graph vector has passed locally on
-Windows x64 with the checkout's default Rust toolchain and Node.js 24.16.0, and on Debian WSL2 Linux
-x64 with the same graph-bearing hashes under Node.js 20.19.2. The Linux Node version is extra local
-evidence and does not lower the supported Node.js 22.13.0 floor. The earlier Rust 1.85,
-Node.js 22.13.0, and MSRV WASM observations predate `pliego.tokens.json`; they remain superseded for
-this vector until rerun.
+The WASM job additionally checks the public crate with Rust 1.85.0 for
+`wasm32-unknown-unknown`. This is not a WASI, `no_std`, every-WASM-target, or deployed Cloudflare
+Workers claim.
## Frozen executable vector
-`node scripts/check-portability.mjs` creates an isolated bundle-plan project and verifies:
-
-- identical CSS and schema-3 manifest bytes when invoked with the same absolute plan/output arguments
- from two unrelated process directories;
-- plan, source, output, and process-directory paths nested below a directory containing a space;
-- a UTF-8 path with an accented character and astral content before the scanned macro;
-- CRLF Rust source input whose manifest range selects the exact macro bytes, with `/` manifest paths;
-- manifest byte count and SHA-256 binding to the adjacent CSS;
-- a successful `--check` leaves existing artifact bytes, size, modification time, inode identity, and
- the output tree unchanged, while a missing-output check fails with an empty final directory and no
- persistent locks;
-- a drift failure is read-only;
-- source and theme paths cannot escape through an intermediate Unix symlink or Windows junction;
-- a linked output directory fails before publication;
-- explicit `.` path segments fail closed;
-- ASCII case-insensitive config/output aliases fail before any mutation on every host, so Linux
- validation models the collision boundary of a later Windows replay.
-- direct-CSS audit control artifacts have frozen byte hashes, and `--check` verifies the complete
- findings/manifest/receipt group without changing it.
-- a manifest-5 `bundle --control` vector binds exact reachability, CSS/source-map/style-manifest,
- Asset Plan, Project Index, canonical TokenGraph, findings, control manifest, and receipt bytes; its
- `--check` is also read-only and rejects token-graph drift without repairing it.
-- a separate `audit --asset-plan` invocation reopens the on-disk plan and adjacent CSS/manifest pair,
- proves canonical regeneration, freezes its own control group, and verifies read-only `--check`.
-- exact-snapshot compile and watch each freeze CSS, Source Map v3, style manifest, canonical
- TokenGraph, findings, control manifest, and receipt: seven artifacts per vector. Compile `--check`
- proves the finite group is read-only and rejects token-graph drift without repairing it. Each vector
- verifies compact canonical fields, non-empty mappings, logical Unicode source identity, and exact
- manifest byte/hash cross-references while proving CSS has no injected discovery comment.
-- every generated TokenGraph parses as compact canonical JSON, declares
- `pliegocss-token-graph/1`, matches `tokens.graphHash`, and is reciprocally related to the generated
- CSS and style manifest in the control manifest.
-
-The separate `pnpm check:manifest` fixture uses only plan-relative logical paths and freezes
-schema-4 graph bytes across the same host-independent source/ThemeId boundary. The established
-portability vector remains default schema 3, so opt-in graph metadata cannot silently change its
-bundle bytes.
-
-`pnpm check:trace` adds a separate host-independent schema-5 golden with exact UTF-8 CSS ranges and
-proves that schema 5 projects to the frozen schema-4 graph without changing the CSS artifact.
-
-The vector freezes these bytes for both supported Rust toolchains:
-
-| Artifact | Contract |
+`node scripts/check-portability.mjs` builds an isolated project and fails closed unless all of these
+properties hold:
+
+- identical CSS and manifest bytes from unrelated process directories;
+- a path containing a space, UTF-8 `src/café.rs`, an astral character, and CRLF source bytes;
+- exact logical provenance and macro byte ranges with forward-slash paths;
+- CSS byte count and SHA-256 bound by the adjacent style manifest;
+- read-only `--check`, including missing-output and drift failures;
+- rejection of symlink/junction escape, linked output/theme paths, dot segments, and
+ ASCII-case aliases before publication;
+- exact direct-audit, bundle-control, Asset Plan, compile, watch, and DTCG control groups;
+- compact canonical `pliegocss-token-graph/1` bytes, reciprocal artifact relationships, and receipt
+ checks;
+- complete Resolver 2025.10 projection: 12 sources, four canonical selections, 53 tokens per theme,
+ one alias, one deprecation, and `{ "color": 3 }` DTCG inventory;
+- selection identity in `configHash` even when the resulting ThemeId and artifact bytes converge.
+
+The current shared-engine vector freezes:
+
+| Artifact | Frozen value |
|---|---:|
| CSS bytes | 424 |
-| CSS SHA-256 | `d208d2960bd28fb29354b1a89c74c106686c8a8a2d71a9cc7cc9971eb2e97070` |
-| Manifest SHA-256 | `3a89e04856d5ff8244c29e91325271d6a57e33eec99242b121308c477facd169` |
+| CSS SHA-256 | `65dbbcff74b6886fdd749b1c7c0587eaff06b4f4720405e10716c2b0dfee9bbc` |
+| Style manifest SHA-256 | `453f708c9c4edbd388eda1ee9c883212cd5d31421077d123cb1d32bc0b28d4bb` |
+| Seed TokenGraph SHA-256 | `3298873e66ade310459593530c86419c05a973d2404b71882c2d0ae430030a28` |
| Provenance path | `src/café.rs` |
-The direct-CSS audit vector additionally freezes:
+The generation/control groups freeze these identities in addition to their complete per-file maps
+inside `scripts/check-portability.mjs`:
-| Control artifact | SHA-256 |
-|---|---|
-| `pliego.css.findings.json` | `75015787cd20bd52e4c1d504943b675785f55d7376a583a103cad4ab5c7de464` |
-| `pliego.css.manifest.json` | `bac4099153b23cfd990fc2e70506153d08b725ca27661ad102934f45df721ac1` |
-| `pliego.css.receipt.json` | `cb680d32a41321b089cf2fd98b7f60501fa8ba306f408d5f448a11551598b9b1` |
+| Group | CSS or primary artifact | Control manifest | Receipt |
+|---|---|---|---|
+| Direct CSS audit | `a353b52f386f0b896db3c354f45bc0b304dffab1477904a5ee37cd8768be468a` findings | `b8513ecd80e9fe566778aa0bfb0b142ecfb06a014bb34a1caa02c1ad28567fd3` | `81777f9533bb6cd203f68e0f052e5974d09a2d773d7adf8f062f9361100a1c28` |
+| Bundle control | `65dbbcff74b6886fdd749b1c7c0587eaff06b4f4720405e10716c2b0dfee9bbc` CSS | `e75b481bd2755a70d5c2ac1030729633cb96d28d8107e562b55ae0951913ba12` | `6fe898d54ad8c745d63d1e6cdd8d31ae6c157130ad4d047542ba6f3cb1f4b5cf` |
+| Reopened Asset Plan | `bd9fa74558e57f08cc9bdff0bf6f6fa6502c483487451652795353a14dab579d` findings | `f254eebbfc716351308f56f34d6494e4317866965cbea6d4bf6b27922f0b09ea` | `d79e5cca007ee6af62cbafaa36532503860e6243f7d534398d27a1bde4e40dce` |
+| Compile control | `65dbbcff74b6886fdd749b1c7c0587eaff06b4f4720405e10716c2b0dfee9bbc` CSS | `2ab00fa0f7b8a06e7bff3780fdc06d61b26b7136e163f8e6437907ddb013343e` | `3dfb3de854d95b39c7f69325f9eae71b37e62607f9c4e367e8188a63f848da70` |
+| Watch control | `65dbbcff74b6886fdd749b1c7c0587eaff06b4f4720405e10716c2b0dfee9bbc` CSS | `537de8064740008b879872d77abfd767352c3a75474b72f79a7d95aab3982c85` | `602c062c391c030112356f996eb5bec251757cdf5ffb59b0f299b575dc869a49` |
+| Direct DTCG | `9a349f5059ffb3ace6c985dfe1fadcd9cb9ac45668b5cd0c00e26625e5a6c6c3` CSS | `96a9fb6769b6ba1260e4581528c834d248a7dcb52e9a9be616ed1edfdb686c84` | `62968ba59c63c76ef01c73a8091978c30855ba8d39cf10f6717332786086546d` |
-The manifest-5 bundle-build vector freezes the complete nine-artifact group:
+The direct DTCG vector freezes `examples/product.resolver.json` at
+`5965f868707ad92b0164788b5a7b8b8a2234ce03d7e6c71e1df66d612801b876`, the complete graph at
+`8659f9c443cb5c77c399fd741a27d476ec18887742c5459002fa6b82c22e864a`, and these selection hashes:
-| Bundle artifact | SHA-256 |
-|---|---|
-| `app.css` | `d208d2960bd28fb29354b1a89c74c106686c8a8a2d71a9cc7cc9971eb2e97070` |
-| `app.css.map` | `39a03b016583b0827ec8bc92a2a1677dcb4d151324dcdd9c77d303af29b1dddf` |
-| `app.manifest.json` | `0b04b7cf34339e598aeb256c5667a808461dfd02e90ce18aaec0a60de501eb9a` |
-| `pliego.assets.json` | `b004fd1b8c28a13b1af0f80c105fc71a7615bab45ea3a1619f7f65084d1b343e` |
-| `pliego.css.findings.json` | `e03d275890f0db3ccceaddfb6dc5b43acd0f0f731b99c77bd0b9bd9b6bf61d40` |
-| `pliego.css.manifest.json` | `19bbb01d407c958d0938ae536c4c08889d1084fd86f6ee246bae0d0326a98db3` |
-| `pliego.css.receipt.json` | `2bac654c6060a50f4a1fa21ea22baa1ef5d87c7f741aa37c41a5068dba1bf52d` |
-| `pliego.index.json` | `6d60bcce6f91e4dc71ca98d921e9ce8bb267cb4d027d51d664b4e87477313dec` |
-| `pliego.tokens.json` | `bb525a903714688d1ada713bff7a0c831631ee1c08b10434d1d03c79d742d204` |
-
-The independently reopened Asset Plan audit vector freezes:
-
-| Asset Plan audit artifact | SHA-256 |
+| Selection | `configHash` |
|---|---|
-| `pliego.css.findings.json` | `67e1d24a75adc042312286ade5572379ec706df1ad177031b9160862d27c5e59` |
-| `pliego.css.manifest.json` | `004d39320c6aab32a6293896db98bbef21a040ec457afc2114e82fc67e7ab795` |
-| `pliego.css.receipt.json` | `d392124440e0295760fb5e8629fb5e4d07271c1f5fcffc03b628bdfbdbab65ce` |
+| `appearance=dark,channel=light` | `sha256:e6ce8e2578b2bbb68c0b533495dab2a781e5c7a32665d4c4a4845146f014ba3e` |
+| `appearance=dark,channel=dark` | `sha256:5bab41c2b1a8d06d05fbaae1517d425a35b0bb8143fcdaeaee3c5faf48d9469e` |
-The exact-snapshot direct compile vector freezes:
+## StyleId format-2 refresh audit
-| Compile artifact | SHA-256 |
-|---|---|
-| `app.css` | `d208d2960bd28fb29354b1a89c74c106686c8a8a2d71a9cc7cc9971eb2e97070` |
-| `app.css.map` | `39a03b016583b0827ec8bc92a2a1677dcb4d151324dcdd9c77d303af29b1dddf` |
-| `app.manifest.json` | `3a89e04856d5ff8244c29e91325271d6a57e33eec99242b121308c477facd169` |
-| `pliego.css.findings.json` | `b59823b27e3f045625bcc9a8ecc3842de039ae1d70d2c98348bd0444550abe04` |
-| `pliego.css.manifest.json` | `51bd81698f58ea3c4ab66c6f5c111135523b62e0177f970a6ae40229ac96d247` |
-| `pliego.css.receipt.json` | `f94939da3e2c04c8d373b2fe881e9234b5abcee8bbb81b6b0d7e1a9c3fdb4fce` |
-| `pliego.tokens.json` | `bb525a903714688d1ada713bff7a0c831631ee1c08b10434d1d03c79d742d204` |
+The shared pure compilation engine intentionally moved the portable output from the RC.2 identity
+stream to StyleId format 2. Before updating the current goldens, a detached `v0.1.0-rc.2` worktree at
+`064dcbce96a3a5cc97a940d07566c008bb5e2d3e` was built with Rust 1.85.0 and replayed with Node.js
+22.13.0. It reproduced the legacy CSS hash
+`acdb0bfee613ddb3fdae0feb9a8e5118dc5f74d385ed53eb610ff4eefd05d454`, manifest hash
+`8c1dc690c2cbb8102b6935a970e60938bbda7e50a0c0bf766c5c800305cce598`, and every legacy control
+hash byte for byte.
-The exact-snapshot watch vector freezes:
+The current checkout then reproduced the new complete vector with Node.js 22.13.0 and 24.16.0.
+CSS remains 424 bytes; source bytes, Resolver hash, graph cardinalities, canonical selections,
+read-only checks, and rejection cases are unchanged. Only identities and artifacts derived from the
+new identity stream were refreshed. The temporary audit worktree and Docker volume were removed
+after verification.
-| Watch artifact | SHA-256 |
-|---|---|
-| `app.css` | `d208d2960bd28fb29354b1a89c74c106686c8a8a2d71a9cc7cc9971eb2e97070` |
-| `app.css.map` | `39a03b016583b0827ec8bc92a2a1677dcb4d151324dcdd9c77d303af29b1dddf` |
-| `app.manifest.json` | `3a89e04856d5ff8244c29e91325271d6a57e33eec99242b121308c477facd169` |
-| `pliego.css.findings.json` | `3df2586a1041d39a0e3e66db4e6674df39479157b3ac413c0fea8b7f85ad23bd` |
-| `pliego.css.manifest.json` | `b0713b0a3957f267e960ec026088441f4a4e3d476e52becd30c1cfe08af0aa64` |
-| `pliego.css.receipt.json` | `fe5633bfa90a7edb953e390e5185597fd909d5f9b696059f36dd1e2c0ed6d2e9` |
-| `pliego.tokens.json` | `bb525a903714688d1ada713bff7a0c831631ee1c08b10434d1d03c79d742d204` |
-
-The direct DTCG Resolver vector selects `appearance=dark,channel=light`, freezes the exact
-`examples/product.resolver.json` input at
-`5965f868707ad92b0164788b5a7b8b8a2234ce03d7e6c71e1df66d612801b876`, and freezes:
-
-| Direct DTCG artifact | SHA-256 |
-|---|---|
-| `app.css` | `cec35891bc4d167bb359b57cf5fcda10a06e2b0665398424ff63b8e01c8b4850` |
-| `app.css.map` | `ce30a183a990c8dca861721b4c5ea953884fd4fb4898220023192e20abc62c3c` |
-| `app.manifest.json` | `88597038d0aab327fc11d11a4371b03e1aa53b44199cc137eb84729f3eddd00a` |
-| `pliego.css.findings.json` | `2135fdb346d08d7e25516006a9d7829780d155b8e1e2374f19ddd7a08a7916c9` |
-| `pliego.css.manifest.json` | `9fb936c7c415af5c78a3edd298e31e6a4d750a84374e830961b2472117ac3e97` |
-| `pliego.css.receipt.json` | `b24d23b286df8c35711684c585962a2b25d84bf1c6de84b7c618c08101738183` |
-| `pliego.tokens.json` | `7a0862dcd2cd8152cface3ea82fbccf1877e75c6f505efa8a3032f8652f354f0` |
-
-The vector also freezes `configHash` as
-`sha256:e06966ae5f001ba6b1b29bfa4d52a3c3d26f9c4ba588d8ff558bd820c23767a9`.
-Changing only `channel=dark` retains the same `ThemeId`, CSS, style manifest, and complete graph but
-changes `configHash` to
-`sha256:d0ab9d7cdc46d564adc14142be10d15b14dee36066966a29f4857e22a0f3616c`.
-This proves that canonical selections remain identity-bound independently of the compiled registry.
-
-The generated bundle/compile/watch manifest hashes include each exact Source Map v3 reference, the
-canonical `pliegocss-token-graph/1` artifact and identity, reciprocal graph relationships, and
-distinct referenced-token coverage. Direct CSS and reopened Asset Plan audit remain unchanged because
-they do not own generation and their token state is explicitly unavailable.
-
-On 2026-07-14 the complete updated script passed all six exact control groups on local Windows x64
-using Node.js 24.16.0 and again on Debian WSL2 Linux x64 using Node.js 20.19.2 with its runtime on the
-native Linux filesystem. The Linux replay used the graph-bearing bundle/compile/watch and direct
-DTCG vectors shown above, including canonical graph parsing, exact Resolver-ledger identity,
-case/order convergence, reciprocal relationships, exact receipt evidence, drift rejection, and
-read-only checks. All frozen direct DTCG hashes matched Windows byte for byte. No
-hosted-runner, macOS, Rust 1.85 graph-bearing, supported-floor Node.js 22.13, or Cloudflare
-portability claim is made.
-
-Bundle-plan schema 2 also passed its three DTCG E2E cases on local Windows x64 and on Debian WSL2
-Linux x64 with a native Linux target directory. Those cases validate schema-1 compatibility,
-default/dark contexts, exact `token-resolver` evidence, the complete graph, same-ThemeId selection
-identity, read-only `--check`, and preservation of the last valid group. This is cross-OS execution
-evidence, not a new frozen byte vector: the temporary plan path and exact plan bytes intentionally
-participate in bundle `configHash`.
-
-On 2026-07-15 the Cargo build-macro DTCG bridge passed the complete Debian WSL2 Linux x64 workspace
-with a native Linux target directory, Clippy with warnings denied, the locked Rust 1.85 workspace
-check, and the Rust 1.85 downstream public-API identity smoke. That smoke froze
-`StyleId=1b052ee4ca1192db2e1ef91594166f70`,
-`ThemeId=aeba2dade1fb8ef54e6db50494167fbd`, 722 CSS bytes, and CSS SHA-256
-`25a0aa84e0ba5256c21ecd5740d9e6094632aeb2fc5600992f9d4f16013efd2d`; it also proved TOML registry
-convergence and direct DTCG macro/CLI identity. Linux executed an actual file-symlink rejection.
-
-The post-change Windows x64 dirty package gate compiled every archive and ran separate extracted
-Rust 1.85 DTCG and legacy-TOML build-script consumers with byte-identical selected-registry output.
-The Windows file-symlink test is conditional: this checkout could not create the link because the OS
-returned error 1314, so this milestone does not claim an observed Windows reparse-point rejection.
-The code still fails closed on reparse metadata and opens with `FILE_FLAG_OPEN_REPARSE_POINT`, but
-that is an implemented contract rather than executed evidence here. Native macOS, native ARM64,
-and hosted runners remain open. Commit `9714b09` passed the complete clean Debian WSL2 package gate:
-all eleven archives compiled, and the extracted Rust 1.85 DTCG and legacy-TOML consumers produced
-byte-identical output.
-
-Changing one of these values requires an intentional compatibility review, not a silent fixture
-update.
-
-## Line-ending contract
-
-`.gitattributes` fixes LF for source, configuration, documentation, workflow, fixture, CSS, HTML, and
-diagnostic snapshot files. Binary media and WASM remain binary. The explicit CRLF source inside the
-portability vector proves that scanner offsets still describe the exact supplied bytes; it does not
-make checked-in line endings host-dependent.
+Changing any frozen value still requires an intentional compatibility review. Update mode prints
+witness values but does not bypass semantic assertions.
## Boundaries still open
+- A new named release candidate must bind its tag, source commit, tree, portability run, browser
+ matrix, registry replay, production replay, signed distribution, and final authorization. RC.2
+ cannot inherit this branch's evidence.
- Normal compile/watch without `--control-dir` preserves author-supplied provenance and is not
- promised byte-identical across hosts. Controlled compile/watch and bundle-plan provenance use
- portable logical paths and are the cross-OS contracts.
-- The hosted matrix must run green after a remote repository exists.
-- The canonical-token-graph vector still needs macOS, Rust 1.85.0, and supported-floor Node.js
- 22.13.0 evidence; local Linux x64 is now covered but is not hosted evidence.
-- Hosted runner labels and action major tags are intentionally not release-pinned yet; supply-chain
- SHA pinning and recording the runner image belong to the release gate.
-- PliegoRS integration still depends on a dirty sibling checkout and is not a clean-clone CI job.
-- Filesystem locks are advisory and are not certified for NFS/SMB or non-PliegoCSS writers.
-- Grouped publication is rollback-capable for handled failures, not gap-free, crash-atomic, or durable
- against power loss. A stale compiler process can still publish after a newer compilation.
-- Browser behavior still lacks a Chromium/Firefox/WebKit matrix.
-- Cloudflare Workers deployment and an application-level WASM/SSR gate remain separate work.
+ promised byte-identical across hosts.
+- Filesystem locks are not certified for NFS/SMB or non-PliegoCSS writers; grouped publication is
+ rollback-capable for handled failures, not durable crash atomicity.
+- The WASM check is a library/target gate. Cloudflare Workers execution and application-level SSR
+ remain separate release evidence.
+- Native ARM64 binaries and signed distribution artifacts remain publication work even though the
+ hosted macOS runner contributes compiler and browser evidence.
diff --git a/docs/troubleshooting/lsp.md b/docs/troubleshooting/lsp.md
index 04dcd45..d116a5e 100644
--- a/docs/troubleshooting/lsp.md
+++ b/docs/troubleshooting/lsp.md
@@ -5,15 +5,16 @@ The current clients deliberately fail closed; they do not search the network or
## The server does not start
-1. Run both configured executables directly with `--version`.
-2. Confirm they come from the same checkout revision and lockfile.
-3. Use absolute paths. Neovim requires readable executable files; VS Code resolves its process from
+1. Run the configured `pliego-css-lsp` executable directly with `--version`.
+2. Confirm it comes from the same compatibility unit as the project's PliegoCSS packages.
+3. Use an absolute path. Neovim requires a readable executable; VS Code resolves its process from
the first local workspace folder.
4. Confirm the buffer is a saved Rust file. Untitled/virtual buffers are unsupported.
5. Restart the client after changing executable paths.
-An unavailable compiler produces `PCL001`; it is not converted into a guessed style diagnostic.
-Capture the complete message before replacing paths or theme settings.
+The legacy compiler-path setting is ignored. A missing or invalid theme can produce `PCL001`; it is
+not converted into a guessed style diagnostic. Capture the complete message before changing theme
+settings.
## Diagnostics disagree with the CLI
@@ -25,8 +26,8 @@ pliego-cssc --diagnostic-format json check --source src --seed
Replace `--seed` with the exact configured TOML or DTCG selection when appropriate. Current corpus
schema 2 requires CLI/LSP equality for code, message, range, severity, category, suggestion, and
-typed replacement across PCS, PSC, PCR001, FMT001, and PCX003. `PCL001` and `PCL002` are operational
-fallbacks tested through fault injection.
+typed replacement across PCS, PSC, PCR001, FMT001, and PCX003. `PCL001` and `PCL002` remain
+operational configuration and bounded-analysis findings rather than authoring corpus cases.
If only an escaped cooked Rust literal has a wider range, that is intentional: decoded offsets
cannot always be mapped safely through escapes, so the server selects the complete literal token.
@@ -55,16 +56,16 @@ mixed snapshot and must continue to fail.
## Diagnostics arrive twice or appear stale
-Local parser/scanner/format diagnostics publish immediately; compiler-backed diagnostics arrive
-after the 150 ms debounce. A newer full-document version cancels the stale direct compiler child and
-its result is neither cached nor published. If an editor plugin sends incremental changes instead of
+Local parser/scanner/format diagnostics publish immediately; theme-aware semantic diagnostics arrive
+after the 150 ms debounce. A newer full-document version supersedes pending work, and version checks
+around in-process analysis prevent stale publication. If an editor plugin sends incremental changes instead of
the negotiated full-document synchronization, the server rejects that request.
## A large file reports PCL002
One document is capped at 256 semantic utility-literal checks. `PCL002` with
-`semantic diagnostic literal limit exceeded` means the server stopped before launching an unbounded
-number of compiler processes. Split generated or unusually large Rust source units; do not suppress
+`semantic diagnostic literal limit exceeded` means the server stopped before performing unbounded
+semantic work. Split generated or unusually large Rust source units; do not suppress
the finding and assume the remaining literals were validated.
## Collect reproducible evidence
@@ -77,6 +78,6 @@ pnpm integration:vscode-host
pnpm integration:neovim
```
-Report the editor version, OS, exact PliegoCSS commit, both binary paths/versions, theme mode, and
+Report the editor version, OS, exact PliegoCSS commit, server path/version, theme mode, and
whether Project Index navigation was enabled. Do not attach proprietary source or theme documents
without redaction and authorization.
diff --git a/editors/neovim/README.md b/editors/neovim/README.md
index feafb59..c3b4c22 100644
--- a/editors/neovim/README.md
+++ b/editors/neovim/README.md
@@ -1,23 +1,22 @@
# PliegoCSS for Neovim
-This unreleased client uses Neovim's built-in LSP client and external, same-version
-`pliego-css-lsp` and `pliego-cssc` binaries. It does not download or embed PliegoCSS executables.
+This unreleased client uses Neovim's built-in LSP client and one external `pliego-css-lsp`. It does
+not download or embed PliegoCSS executables, and editor semantics do not require `pliego-cssc`.
-Add this directory to `runtimepath`, then configure explicit paths:
+Add this directory to `runtimepath`, then configure:
```lua
require("pliegocss").setup({
server = "/exact/path/to/pliego-css-lsp",
- compiler = "/exact/path/to/pliego-cssc",
root_dir = vim.fn.getcwd(),
theme = { mode = "seed" }, -- or discover; config requires path
project_index = "target/site/assets/pliego.index.json", -- optional
})
```
-Only file-backed Rust buffers are started by the `FileType` autocmd. Invalid paths, theme modes,
-and missing optional artifacts fail before a client starts. The project root determines compiler
-discovery and relative configuration behavior.
+Only file-backed Rust buffers are started by the `FileType` autocmd. Invalid server/config paths,
+theme modes, and missing optional artifacts fail before a client starts. A legacy `compiler` key is
+accepted as an unused table member for 0.1 configuration compatibility.
Run the pinned real-host gate from the repository root:
@@ -25,7 +24,5 @@ Run the pinned real-host gate from the repository root:
pnpm integration:neovim
```
-The first run downloads the official Neovim 0.12.4 archive into an ignored cache and verifies its
-frozen SHA-256 before extraction. The headless host opens a real Rust buffer, follows Project Index
-definition, and observes compiler-backed `PCS001` and cross-clause `PCX003` diagnostics. The test
-runtime download is gate infrastructure; the client module never downloads an editor or server.
+The headless host opens a real Rust buffer, follows Project Index definition, and observes
+shared-engine `PCS001` and cross-clause `PCX003` diagnostics.
diff --git a/editors/neovim/lua/pliegocss/init.lua b/editors/neovim/lua/pliegocss/init.lua
index aebc9be..f6475cb 100644
--- a/editors/neovim/lua/pliegocss/init.lua
+++ b/editors/neovim/lua/pliegocss/init.lua
@@ -15,8 +15,7 @@ end
local function command(options)
local server = absolute_executable(options.server, "pliego-css-lsp")
- local compiler = absolute_executable(options.compiler, "pliego-cssc")
- local args = { server, "--pliego-cssc", compiler }
+ local args = { server }
local theme = options.theme or { mode = "discover" }
if theme.mode == "seed" then
table.insert(args, "--seed")
diff --git a/editors/neovim/tests/host/init.lua b/editors/neovim/tests/host/init.lua
index 5ac401d..36e93d5 100644
--- a/editors/neovim/tests/host/init.lua
+++ b/editors/neovim/tests/host/init.lua
@@ -18,7 +18,6 @@ vim.opt.runtimepath:prepend(editor)
require("pliegocss").setup({
server = required("PLIEGOCSS_NEOVIM_LSP"),
- compiler = required("PLIEGOCSS_NEOVIM_COMPILER"),
root_dir = workspace,
theme = { mode = "seed" },
project_index = workspace .. "/out/pliego.index.json",
diff --git a/editors/vscode/README.md b/editors/vscode/README.md
index 08bbc8b..0827489 100644
--- a/editors/vscode/README.md
+++ b/editors/vscode/README.md
@@ -1,21 +1,18 @@
# PliegoCSS for VS Code
-This unreleased extension connects file-backed Rust documents to an externally installed
-`pliego-css-lsp` process. It provides the completion, hover, diagnostics, formatting, and optional
-source-to-CSS navigation capabilities advertised by that server. The extension never downloads or
-updates native executables.
+This unreleased extension launches an external `pliego-css-lsp` for file-backed Rust documents. It
+does not download or embed native executables, and editor semantics do not require `pliego-cssc`.
-Configure `pliegocss.server.path` and `pliegocss.compiler.path` to exact same-version binaries. Set
-`pliegocss.theme.mode` to `discover`, `seed`, or `config`; config mode also requires
-`pliegocss.theme.config`. Use **PliegoCSS: Restart Language Server** after changing external files.
-Set `pliegocss.projectIndex.path` to a generated `pliego.index.json` to enable verified Go to
-Definition from a Rust utility literal to its final physical CSS declarations. An empty value keeps
-navigation disabled.
+Configure `pliegocss.server.path`, select `discover`, `seed`, or `config` theme mode, and optionally
+set `pliegocss.projectIndex.path` for verified source-to-CSS navigation. The historical
+`pliegocss.compiler.path` setting is deprecated and ignored.
-Only the first local workspace folder is currently used as the native process working directory.
-Virtual and untrusted workspaces are explicitly unsupported.
+Build and test from the repository root:
-`pnpm integration:vscode-host` launches the development extension in pinned VS Code 1.105.1 and
-exercises real `PCS001`/cross-clause `PCX003` diagnostics plus Project Index definition. Its first
-run downloads that editor test
-runtime; this is test infrastructure and does not change the extension's no-server-download policy.
+```console
+pnpm integration:vscode
+pnpm integration:vscode-host
+```
+
+The real-host gate uses VS Code 1.105.1, opens a Rust buffer, verifies Project Index definition, and
+observes shared-engine `PCS001` and `PCX003` diagnostics.
diff --git a/editors/vscode/package.json b/editors/vscode/package.json
index 165405a..b373b9b 100644
--- a/editors/vscode/package.json
+++ b/editors/vscode/package.json
@@ -57,9 +57,10 @@
},
"pliegocss.compiler.path": {
"type": "string",
- "default": "pliego-cssc",
+ "default": "",
"scope": "machine-overridable",
- "description": "Exact path or executable name for the same-version pliego-cssc used by completion and hover."
+ "description": "Deprecated compatibility setting. Editor analysis is in-process inside pliego-css-lsp.",
+ "deprecationMessage": "No longer used. Remove this setting; pliego-css-lsp does not launch pliego-cssc."
},
"pliegocss.projectIndex.path": {
"type": "string",
diff --git a/editors/vscode/scripts/check-host.mjs b/editors/vscode/scripts/check-host.mjs
index 33f37c3..36ec0df 100644
--- a/editors/vscode/scripts/check-host.mjs
+++ b/editors/vscode/scripts/check-host.mjs
@@ -11,7 +11,6 @@ const target = process.env.CARGO_TARGET_DIR
const workspace = resolve(target, "lsp-integration-workspace");
const executable = process.platform === "win32" ? ".exe" : "";
const lsp = resolve(target, "debug", `pliego-css-lsp${executable}`);
-const compiler = resolve(target, "debug", `pliego-cssc${executable}`);
/** @param {string} message */
function fail(message) {
@@ -34,7 +33,6 @@ writeFileSync(
`${JSON.stringify(
{
"pliegocss.server.path": lsp,
- "pliegocss.compiler.path": compiler,
"pliegocss.theme.mode": "seed",
"pliegocss.projectIndex.path": "out/pliego.index.json",
},
@@ -68,7 +66,7 @@ process.stdout.write(
schemaVersion: 1,
vscode: "1.105.1",
extension: "celiums.pliegocss-vscode",
- diagnostics: "compiler-backed-pcs-and-pcx",
+ diagnostics: "shared-engine-pcs-and-pcx",
definition: "project-index-to-physical-css",
serverDownload: false,
}, null, 2)}\n`,
diff --git a/editors/vscode/src/extension.js b/editors/vscode/src/extension.js
index 6ceafda..db77a0f 100644
--- a/editors/vscode/src/extension.js
+++ b/editors/vscode/src/extension.js
@@ -12,7 +12,6 @@ function readSettings() {
const configuration = vscode.workspace.getConfiguration("pliegocss");
return {
serverPath: configuration.get("server.path", "pliego-css-lsp"),
- compilerPath: configuration.get("compiler.path", "pliego-cssc"),
projectIndexPath: configuration.get("projectIndex.path", ""),
themeMode: configuration.get("theme.mode", "discover"),
themeConfig: configuration.get("theme.config", "pliego.theme.toml"),
diff --git a/editors/vscode/src/settings.js b/editors/vscode/src/settings.js
index 2c09140..d948a84 100644
--- a/editors/vscode/src/settings.js
+++ b/editors/vscode/src/settings.js
@@ -3,7 +3,7 @@
const THEME_MODES = new Set(["discover", "seed", "config"]);
/**
- * @typedef {{serverPath:string, compilerPath:string, projectIndexPath:string, themeMode:string, themeConfig:string}} Settings
+ * @typedef {{serverPath:string, projectIndexPath:string, themeMode:string, themeConfig:string}} Settings
*/
/**
@@ -13,13 +13,11 @@ const THEME_MODES = new Set(["discover", "seed", "config"]);
*/
function serverInvocation(settings) {
const serverPath = settings.serverPath.trim();
- const compilerPath = settings.compilerPath.trim();
if (!serverPath) throw new Error("pliegocss.server.path cannot be empty");
- if (!compilerPath) throw new Error("pliegocss.compiler.path cannot be empty");
if (!THEME_MODES.has(settings.themeMode)) {
throw new Error(`unsupported PliegoCSS theme mode: ${settings.themeMode}`);
}
- const args = ["--pliego-cssc", compilerPath];
+ const args = [];
const projectIndexPath = settings.projectIndexPath.trim();
if (projectIndexPath) args.push("--project-index", projectIndexPath);
if (settings.themeMode === "seed") args.push("--seed");
diff --git a/editors/vscode/tests/settings.test.js b/editors/vscode/tests/settings.test.js
index 95305cc..fae20b1 100644
--- a/editors/vscode/tests/settings.test.js
+++ b/editors/vscode/tests/settings.test.js
@@ -6,28 +6,25 @@ const { serverInvocation } = require("../src/settings");
const defaults = {
serverPath: "pliego-css-lsp",
- compilerPath: "pliego-cssc",
projectIndexPath: "",
themeMode: "discover",
themeConfig: "pliego.theme.toml",
};
-test("discovery keeps an explicit compiler and no hidden theme flag", () => {
+test("discovery starts only the in-process language server", () => {
assert.deepEqual(serverInvocation(defaults), {
command: "pliego-css-lsp",
- args: ["--pliego-cssc", "pliego-cssc"],
+ args: [],
});
});
test("seed and config are exact mutually exclusive invocations", () => {
assert.deepEqual(serverInvocation({ ...defaults, themeMode: "seed" }).args, [
- "--pliego-cssc",
- "pliego-cssc",
"--seed",
]);
assert.deepEqual(
serverInvocation({ ...defaults, themeMode: "config", themeConfig: "themes/app.toml" }).args,
- ["--pliego-cssc", "pliego-cssc", "--config", "themes/app.toml"],
+ ["--config", "themes/app.toml"],
);
});
@@ -35,17 +32,14 @@ test("Project Index navigation is explicit and path preserving", () => {
assert.deepEqual(
serverInvocation({ ...defaults, projectIndexPath: "target/site/assets/pliego.index.json" }).args,
[
- "--pliego-cssc",
- "pliego-cssc",
"--project-index",
"target/site/assets/pliego.index.json",
],
);
});
-test("empty executables, invalid modes, and empty config fail closed", () => {
+test("empty server, invalid modes, and empty config fail closed", () => {
assert.throws(() => serverInvocation({ ...defaults, serverPath: " " }), /server\.path/);
- assert.throws(() => serverInvocation({ ...defaults, compilerPath: " " }), /compiler\.path/);
assert.throws(() => serverInvocation({ ...defaults, themeMode: "automatic" }), /theme mode/);
assert.throws(
() => serverInvocation({ ...defaults, themeMode: "config", themeConfig: " " }),
diff --git a/integration-tests/manifest-graph/expected.css b/integration-tests/manifest-graph/expected.css
index 17ad57e..35c51c5 100644
--- a/integration-tests/manifest-graph/expected.css
+++ b/integration-tests/manifest-graph/expected.css
@@ -1 +1 @@
-.pc_4sm4s6x04ha0fvxci6gffftk1{padding:1rem;background-color:var(--color-accent)}
+.pc_dc4tv3z8js64dcf7c1y2k5tzw{padding:1rem;background-color:var(--color-accent)}
diff --git a/integration-tests/manifest-graph/expected.manifest.json b/integration-tests/manifest-graph/expected.manifest.json
index a524684..230f3a5 100644
--- a/integration-tests/manifest-graph/expected.manifest.json
+++ b/integration-tests/manifest-graph/expected.manifest.json
@@ -2,16 +2,16 @@
"schemaVersion": 4,
"styleIdFormatVersion": 2,
"classNameFormatVersion": 1,
- "themeIdFormatVersion": 2,
- "themeId": "b3d5ad77175995c2b8f51ef7c0d41991",
+ "themeIdFormatVersion": 3,
+ "themeId": "b98b78da29201938d135b8bb94717788",
"targets": "modern",
"format": "minified",
- "cssSha256": "0bde100d7584b36809ee78cf8304f32ff44ede166600813b327c52e366cad5c2",
+ "cssSha256": "3657abf7c63ba9b0ccaf2428f59a33691fa6fe8d5f155f16a804da9ba46cf770",
"cssBytes": 81,
"styles": [
{
- "styleId": "50fda9c01f9e2dd03aff0d05d8d3ce61",
- "className": "pc_4sm4s6x04ha0fvxci6gffftk1",
+ "styleId": "e1476a2192509ea7a3aae909fe70631c",
+ "className": "pc_dc4tv3z8js64dcf7c1y2k5tzw",
"origins": [
{
"source": "p-4 bg-accent",
@@ -31,13 +31,13 @@
"applicationCoverage": "adapter-attested-complete",
"declarations": [
{
- "id": "decl:50fda9c01f9e2dd03aff0d05d8d3ce61:00000000",
- "styleId": "50fda9c01f9e2dd03aff0d05d8d3ce61",
+ "id": "decl:e1476a2192509ea7a3aae909fe70631c:00000000",
+ "styleId": "e1476a2192509ea7a3aae909fe70631c",
"ordinal": 0
},
{
- "id": "decl:50fda9c01f9e2dd03aff0d05d8d3ce61:00000001",
- "styleId": "50fda9c01f9e2dd03aff0d05d8d3ce61",
+ "id": "decl:e1476a2192509ea7a3aae909fe70631c:00000001",
+ "styleId": "e1476a2192509ea7a3aae909fe70631c",
"ordinal": 1
}
],
@@ -87,21 +87,21 @@
{
"kind": "componentUsesDeclaration",
"from": "component:card",
- "to": "decl:50fda9c01f9e2dd03aff0d05d8d3ce61:00000000"
+ "to": "decl:e1476a2192509ea7a3aae909fe70631c:00000000"
},
{
"kind": "componentUsesDeclaration",
"from": "component:card",
- "to": "decl:50fda9c01f9e2dd03aff0d05d8d3ce61:00000001"
+ "to": "decl:e1476a2192509ea7a3aae909fe70631c:00000001"
},
{
"kind": "declarationUsesToken",
- "from": "decl:50fda9c01f9e2dd03aff0d05d8d3ce61:00000000",
+ "from": "decl:e1476a2192509ea7a3aae909fe70631c:00000000",
"to": "token:spacing:310ca263"
},
{
"kind": "declarationUsesToken",
- "from": "decl:50fda9c01f9e2dd03aff0d05d8d3ce61:00000001",
+ "from": "decl:e1476a2192509ea7a3aae909fe70631c:00000001",
"to": "token:color:13e3e9b7"
},
{
@@ -136,13 +136,13 @@
},
{
"kind": "styleHasDeclaration",
- "from": "style:50fda9c01f9e2dd03aff0d05d8d3ce61",
- "to": "decl:50fda9c01f9e2dd03aff0d05d8d3ce61:00000000"
+ "from": "style:e1476a2192509ea7a3aae909fe70631c",
+ "to": "decl:e1476a2192509ea7a3aae909fe70631c:00000000"
},
{
"kind": "styleHasDeclaration",
- "from": "style:50fda9c01f9e2dd03aff0d05d8d3ce61",
- "to": "decl:50fda9c01f9e2dd03aff0d05d8d3ce61:00000001"
+ "from": "style:e1476a2192509ea7a3aae909fe70631c",
+ "to": "decl:e1476a2192509ea7a3aae909fe70631c:00000001"
}
]
}
diff --git a/integration-tests/physical-trace/expected.css b/integration-tests/physical-trace/expected.css
index 94134d0..53ba1af 100644
--- a/integration-tests/physical-trace/expected.css
+++ b/integration-tests/physical-trace/expected.css
@@ -1 +1 @@
-:root{--color-accent:oklch(58% .19 32);--color-accent-strong:oklch(50% .18 30);--color-canvas:oklch(98.5% .004 80);--color-ink:oklch(19% .012 70);--color-line:oklch(88% .008 75);--color-muted:oklch(50% .014 70);--color-surface:#fff;--color-surface-raised:oklch(96% .006 80);--font-mono:ui-monospace,SFMono-Regular,monospace;--font-sans:ui-sans-serif,system-ui,sans-serif}.pc_7trr4palscrsvtq46omh80m6e{--pc-shadow:0 0 #0000;--pc-ring-width:0;--pc-ring-color:currentColor;font-size:.875rem;line-height:1.25rem;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;outline-width:2px;outline-style:solid;--pc-shadow:0 4px 6px -1px #0000001a;--pc-ring-width:2px;--pc-ring-color:color-mix(in oklab,var(--color-accent) 20%,transparent);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);--label:"😀;";color:var(--lightningcss-light,red)var(--lightningcss-dark,#00f);box-shadow:0 0 0 var(--pc-ring-width,0) var(--pc-ring-color,currentColor),var(--pc-shadow,0 0 #0000);opacity:.5!important}@media (width>=48rem){.pc_7trr4palscrsvtq46omh80m6e:hover{background-color:var(--color-accent)}}
+:root{--color-accent:oklch(58% .19 32);--color-accent-strong:oklch(50% .18 30);--color-canvas:oklch(98.5% .004 80);--color-ink:oklch(19% .012 70);--color-line:oklch(88% .008 75);--color-muted:oklch(50% .014 70);--color-surface:#fff;--color-surface-raised:oklch(96% .006 80);--font-mono:ui-monospace,SFMono-Regular,monospace;--font-sans:ui-sans-serif,system-ui,sans-serif}.pc_5qli19dn9o0cpcpna4kf91gg8{--pc-shadow:0 0 #0000;--pc-ring-width:0;--pc-ring-color:currentColor;font-size:.875rem;line-height:1.25rem;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;outline-width:2px;outline-style:solid;--pc-shadow:0 4px 6px -1px #0000001a;--pc-ring-width:2px;--pc-ring-color:color-mix(in oklab,var(--color-accent) 20%,transparent);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);--label:"😀;";color:var(--lightningcss-light,red)var(--lightningcss-dark,#00f);box-shadow:0 0 0 var(--pc-ring-width,0) var(--pc-ring-color,currentColor),var(--pc-shadow,0 0 #0000);opacity:.5!important}@media (width>=48rem){.pc_5qli19dn9o0cpcpna4kf91gg8:hover{background-color:var(--color-accent)}}
diff --git a/integration-tests/physical-trace/expected.manifest.json b/integration-tests/physical-trace/expected.manifest.json
index 2e37676..2fce786 100644
--- a/integration-tests/physical-trace/expected.manifest.json
+++ b/integration-tests/physical-trace/expected.manifest.json
@@ -2,16 +2,16 @@
"schemaVersion": 5,
"styleIdFormatVersion": 2,
"classNameFormatVersion": 1,
- "themeIdFormatVersion": 2,
- "themeId": "b3d5ad77175995c2b8f51ef7c0d41991",
+ "themeIdFormatVersion": 3,
+ "themeId": "b98b78da29201938d135b8bb94717788",
"targets": "modern",
"format": "minified",
- "cssSha256": "6525573b544ae20913a015b8b12b9447532c2110d0f081a7ab36ea08d62c4137",
+ "cssSha256": "a7cb505f693d93fb6a47aaa3d45c8f028f396f963a63e4c5229d4a62205b62d6",
"cssBytes": 1210,
"styles": [
{
- "styleId": "8434f98c54887df6c28fd869c949bc46",
- "className": "pc_7trr4palscrsvtq46omh80m6e",
+ "styleId": "60ef78cd6fb3591f3a1201fbfab38e88",
+ "className": "pc_5qli19dn9o0cpcpna4kf91gg8",
"origins": [
{
"source": "text-sm antialiased transition-colors outline-2 ring-2 ring-accent/20 shadow-md opacity-50! [color:light-dark(red,blue)] [--label:'😀;'] md:hover:bg-accent",
@@ -34,58 +34,58 @@
"physicalCoverage": "compiler-verified-complete",
"declarations": [
{
- "id": "decl:8434f98c54887df6c28fd869c949bc46:00000000",
- "styleId": "8434f98c54887df6c28fd869c949bc46",
+ "id": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000000",
+ "styleId": "60ef78cd6fb3591f3a1201fbfab38e88",
"ordinal": 0
},
{
- "id": "decl:8434f98c54887df6c28fd869c949bc46:00000001",
- "styleId": "8434f98c54887df6c28fd869c949bc46",
+ "id": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000001",
+ "styleId": "60ef78cd6fb3591f3a1201fbfab38e88",
"ordinal": 1
},
{
- "id": "decl:8434f98c54887df6c28fd869c949bc46:00000002",
- "styleId": "8434f98c54887df6c28fd869c949bc46",
+ "id": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000002",
+ "styleId": "60ef78cd6fb3591f3a1201fbfab38e88",
"ordinal": 2
},
{
- "id": "decl:8434f98c54887df6c28fd869c949bc46:00000003",
- "styleId": "8434f98c54887df6c28fd869c949bc46",
+ "id": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000003",
+ "styleId": "60ef78cd6fb3591f3a1201fbfab38e88",
"ordinal": 3
},
{
- "id": "decl:8434f98c54887df6c28fd869c949bc46:00000004",
- "styleId": "8434f98c54887df6c28fd869c949bc46",
+ "id": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000004",
+ "styleId": "60ef78cd6fb3591f3a1201fbfab38e88",
"ordinal": 4
},
{
- "id": "decl:8434f98c54887df6c28fd869c949bc46:00000005",
- "styleId": "8434f98c54887df6c28fd869c949bc46",
+ "id": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000005",
+ "styleId": "60ef78cd6fb3591f3a1201fbfab38e88",
"ordinal": 5
},
{
- "id": "decl:8434f98c54887df6c28fd869c949bc46:00000006",
- "styleId": "8434f98c54887df6c28fd869c949bc46",
+ "id": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000006",
+ "styleId": "60ef78cd6fb3591f3a1201fbfab38e88",
"ordinal": 6
},
{
- "id": "decl:8434f98c54887df6c28fd869c949bc46:00000007",
- "styleId": "8434f98c54887df6c28fd869c949bc46",
+ "id": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000007",
+ "styleId": "60ef78cd6fb3591f3a1201fbfab38e88",
"ordinal": 7
},
{
- "id": "decl:8434f98c54887df6c28fd869c949bc46:00000008",
- "styleId": "8434f98c54887df6c28fd869c949bc46",
+ "id": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000008",
+ "styleId": "60ef78cd6fb3591f3a1201fbfab38e88",
"ordinal": 8
},
{
- "id": "decl:8434f98c54887df6c28fd869c949bc46:00000009",
- "styleId": "8434f98c54887df6c28fd869c949bc46",
+ "id": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000009",
+ "styleId": "60ef78cd6fb3591f3a1201fbfab38e88",
"ordinal": 9
},
{
- "id": "decl:8434f98c54887df6c28fd869c949bc46:0000000a",
- "styleId": "8434f98c54887df6c28fd869c949bc46",
+ "id": "decl:60ef78cd6fb3591f3a1201fbfab38e88:0000000a",
+ "styleId": "60ef78cd6fb3591f3a1201fbfab38e88",
"ordinal": 10
}
],
@@ -566,216 +566,216 @@
{
"kind": "componentUsesDeclaration",
"from": "component:app::card",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000000"
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000000"
},
{
"kind": "componentUsesDeclaration",
"from": "component:app::card",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000001"
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000001"
},
{
"kind": "componentUsesDeclaration",
"from": "component:app::card",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000002"
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000002"
},
{
"kind": "componentUsesDeclaration",
"from": "component:app::card",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000003"
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000003"
},
{
"kind": "componentUsesDeclaration",
"from": "component:app::card",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000004"
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000004"
},
{
"kind": "componentUsesDeclaration",
"from": "component:app::card",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000005"
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000005"
},
{
"kind": "componentUsesDeclaration",
"from": "component:app::card",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000006"
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000006"
},
{
"kind": "componentUsesDeclaration",
"from": "component:app::card",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000007"
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000007"
},
{
"kind": "componentUsesDeclaration",
"from": "component:app::card",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000008"
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000008"
},
{
"kind": "componentUsesDeclaration",
"from": "component:app::card",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000009"
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000009"
},
{
"kind": "componentUsesDeclaration",
"from": "component:app::card",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:0000000a"
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:0000000a"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000000",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000000",
"to": "css-decl:00000001:00000003"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000000",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000000",
"to": "css-decl:00000001:00000004"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000001",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000001",
"to": "css-decl:00000001:00000005"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000001",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000001",
"to": "css-decl:00000001:00000006"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000002",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000002",
"to": "css-decl:00000001:00000007"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000002",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000002",
"to": "css-decl:00000001:00000008"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000003",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000003",
"to": "css-decl:00000001:00000012"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000004",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000004",
"to": "css-decl:00000001:00000000"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000004",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000004",
"to": "css-decl:00000001:00000001"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000004",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000004",
"to": "css-decl:00000001:00000002"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000004",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000004",
"to": "css-decl:00000001:00000009"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000004",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000004",
"to": "css-decl:00000001:00000011"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000005",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000005",
"to": "css-decl:00000001:00000000"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000005",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000005",
"to": "css-decl:00000001:00000001"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000005",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000005",
"to": "css-decl:00000001:00000002"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000005",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000005",
"to": "css-decl:00000001:0000000a"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000005",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000005",
"to": "css-decl:00000001:00000011"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000006",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000006",
"to": "css-decl:00000001:00000000"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000006",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000006",
"to": "css-decl:00000001:00000001"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000006",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000006",
"to": "css-decl:00000001:00000002"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000006",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000006",
"to": "css-decl:00000001:0000000b"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000006",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000006",
"to": "css-decl:00000001:00000011"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000007",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000007",
"to": "css-decl:00000001:0000000c"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000007",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000007",
"to": "css-decl:00000001:0000000d"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000007",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000007",
"to": "css-decl:00000001:0000000e"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000008",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000008",
"to": "css-decl:00000001:0000000f"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000009",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000009",
"to": "css-decl:00000001:00000010"
},
{
"kind": "declarationContributesToPhysicalDeclaration",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:0000000a",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:0000000a",
"to": "css-decl:00000003:00000000"
},
{
"kind": "declarationUsesToken",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000000",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000000",
"to": "token:font-size:4452173d"
},
{
"kind": "declarationUsesToken",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000004",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000004",
"to": "token:shadow:672e2914"
},
{
"kind": "declarationUsesToken",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:00000006",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000006",
"to": "token:color:13e3e9b7"
},
{
"kind": "declarationUsesToken",
- "from": "decl:8434f98c54887df6c28fd869c949bc46:0000000a",
+ "from": "decl:60ef78cd6fb3591f3a1201fbfab38e88:0000000a",
"to": "token:color:13e3e9b7"
},
{
@@ -945,58 +945,58 @@
},
{
"kind": "styleHasDeclaration",
- "from": "style:8434f98c54887df6c28fd869c949bc46",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000000"
+ "from": "style:60ef78cd6fb3591f3a1201fbfab38e88",
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000000"
},
{
"kind": "styleHasDeclaration",
- "from": "style:8434f98c54887df6c28fd869c949bc46",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000001"
+ "from": "style:60ef78cd6fb3591f3a1201fbfab38e88",
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000001"
},
{
"kind": "styleHasDeclaration",
- "from": "style:8434f98c54887df6c28fd869c949bc46",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000002"
+ "from": "style:60ef78cd6fb3591f3a1201fbfab38e88",
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000002"
},
{
"kind": "styleHasDeclaration",
- "from": "style:8434f98c54887df6c28fd869c949bc46",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000003"
+ "from": "style:60ef78cd6fb3591f3a1201fbfab38e88",
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000003"
},
{
"kind": "styleHasDeclaration",
- "from": "style:8434f98c54887df6c28fd869c949bc46",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000004"
+ "from": "style:60ef78cd6fb3591f3a1201fbfab38e88",
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000004"
},
{
"kind": "styleHasDeclaration",
- "from": "style:8434f98c54887df6c28fd869c949bc46",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000005"
+ "from": "style:60ef78cd6fb3591f3a1201fbfab38e88",
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000005"
},
{
"kind": "styleHasDeclaration",
- "from": "style:8434f98c54887df6c28fd869c949bc46",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000006"
+ "from": "style:60ef78cd6fb3591f3a1201fbfab38e88",
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000006"
},
{
"kind": "styleHasDeclaration",
- "from": "style:8434f98c54887df6c28fd869c949bc46",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000007"
+ "from": "style:60ef78cd6fb3591f3a1201fbfab38e88",
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000007"
},
{
"kind": "styleHasDeclaration",
- "from": "style:8434f98c54887df6c28fd869c949bc46",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000008"
+ "from": "style:60ef78cd6fb3591f3a1201fbfab38e88",
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000008"
},
{
"kind": "styleHasDeclaration",
- "from": "style:8434f98c54887df6c28fd869c949bc46",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:00000009"
+ "from": "style:60ef78cd6fb3591f3a1201fbfab38e88",
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:00000009"
},
{
"kind": "styleHasDeclaration",
- "from": "style:8434f98c54887df6c28fd869c949bc46",
- "to": "decl:8434f98c54887df6c28fd869c949bc46:0000000a"
+ "from": "style:60ef78cd6fb3591f3a1201fbfab38e88",
+ "to": "decl:60ef78cd6fb3591f3a1201fbfab38e88:0000000a"
},
{
"kind": "syntheticProducerProducesPhysicalDeclaration",
diff --git a/integration-tests/pliegors-smoke/pliegors-contract.json b/integration-tests/pliegors-smoke/pliegors-contract.json
index cecd44e..993b4cb 100644
--- a/integration-tests/pliegors-smoke/pliegors-contract.json
+++ b/integration-tests/pliegors-smoke/pliegors-contract.json
@@ -1,7 +1,7 @@
{
"schemaVersion": 2,
- "revision": "f3f4eb9be7ba2abfb7ed952675bba062d375e1fe",
- "sourceSha256": "a11ca98bf64a93486da1d0a3478cf73920b7105608c55d215d4e3e5d0c817373",
+ "revision": "b00259c12fb5b069b2a50461bd244c9f098e834b",
+ "sourceSha256": "7b1588be7a5c6a8036a50c47fafbf3322ab7eec9f335156aa1ef869567f8a9a4",
"surfaces": [
"Cargo.toml",
"Cargo.lock",
diff --git a/integration-tests/public-api-smoke/src/main.rs b/integration-tests/public-api-smoke/src/main.rs
index 8f80f08..575f191 100644
--- a/integration-tests/public-api-smoke/src/main.rs
+++ b/integration-tests/public-api-smoke/src/main.rs
@@ -59,7 +59,7 @@ const OWNERSHIP_ASSET_PLAN: &[u8] = br#"{
"applicationCoverage": "adapter-attested-complete",
"styleIdFormatVersion": 2,
"classNameFormatVersion": 1,
- "themeIdFormatVersion": 2,
+ "themeIdFormatVersion": 3,
"themeId": "00000000000000000000000000000000",
"targets": "none",
"format": "minified",
diff --git a/package.json b/package.json
index db0d1b9..5a07496 100644
--- a/package.json
+++ b/package.json
@@ -8,15 +8,18 @@
"scripts": {
"baseline:build": "tailwindcss -i ./benchmarks/tailwind-v4/input.css -o ./benchmarks/tailwind-v4/dist/full-complete.min.css --minify",
"baseline:watch": "tailwindcss -i ./benchmarks/tailwind-v4/input.css -o ./benchmarks/tailwind-v4/dist/full-complete.min.css --watch",
- "baseline:measure": "node ./scripts/measure-tailwind-baseline.mjs",
+ "baseline:measure": "node ./scripts/measure-benchmark-authority-v2.mjs",
+ "baseline:measure-legacy-v4": "node ./scripts/measure-tailwind-baseline.mjs",
"baseline:measure-watch": "node ./scripts/measure-tailwind-watch.mjs",
"baseline:measure-rust": "node ./scripts/measure-rust-check.mjs",
"baseline:measure-pliego": "node ./scripts/measure-pliego-gate-a.mjs",
- "baseline:measure-pliego-b": "node ./scripts/measure-pliego-gate-b.mjs",
+ "baseline:measure-pliego-b": "node ./scripts/measure-benchmark-authority-v2.mjs",
+ "baseline:measure-authority-v2": "node ./scripts/measure-benchmark-authority-v2.mjs",
"baseline:measure-media": "node ./scripts/measure-media-merge.mjs",
"baseline:measure-wasm": "node ./scripts/measure-wasm-overhead.mjs",
"baseline:check-diagnostics": "node ./scripts/check-diagnostics.mjs",
"check:api": "node ./scripts/check-public-api.mjs && pnpm check:manifest && pnpm check:trace",
+ "check:engine-boundary": "node ./scripts/check-engine-boundary.mjs",
"check:determinism": "node ./scripts/check-parallel-determinism.mjs",
"check:properties": "cargo +1.85.0 test --locked -p pliego-css-compiler --test property_contract",
"check:fuzz": "node ./scripts/check-fuzz.mjs",
@@ -26,20 +29,31 @@
"check:media": "node ./scripts/measure-media-merge.mjs --check",
"check:pruning": "node ./scripts/measure-reachability-pruning.mjs --check",
"check:evidence": "node ./scripts/check-benchmark-evidence.mjs",
+ "check:benchmark-authority": "node ./scripts/check-benchmark-authority-v2.mjs && node ./scripts/render-benchmark-authority-v2.mjs --check",
+ "check:benchmark-oracle": "node ./scripts/check-benchmark-authority-v2.mjs --network",
+ "check:benchmark-result": "node ./scripts/check-benchmark-result-v2.mjs",
+ "check:benchmark-smoke": "node ./scripts/measure-benchmark-authority-v2.mjs --smoke && node ./scripts/check-benchmark-result-v2.mjs",
"check:portability": "cargo build --locked -p pliego-cssc && node ./scripts/check-portability.mjs",
"check:compatibility": "node ./scripts/check-compatibility-policy.mjs",
"check:attribution": "node ./scripts/check-standards-provenance.mjs",
"check:docs": "node ./scripts/check-docs.mjs",
+ "check:site-docs": "node ./site/scripts/generate-docs.test.mjs && node ./site/scripts/generate-docs.mjs --check",
"check:brand": "node ./scripts/check-brand.mjs",
"check:maturity": "node ./scripts/check-maturity-map.mjs",
"check:tailwind-matrix": "node ./scripts/check-tailwind-matrix.mjs",
"check:representative-gaps": "node ./scripts/check-representative-gap-map.mjs",
"check:css-classifier": "node ./scripts/check-standard-css-classifier-corpus.mjs",
- "check:hosted-browser-matrix": "node ./scripts/check-hosted-browser-matrix.mjs",
+ "check:hosted-browser-matrix": "node ./scripts/check-hosted-browser-matrix.mjs --validate",
+ "check:browser-output-authority": "node ./scripts/check-browser-output-authority.mjs && node ./scripts/check-browser-output-matrix.test.mjs && node ./scripts/render-browser-output-certification.mjs --check",
+ "check:browser-output": "node ./scripts/browser-output-certification.mjs",
+ "check:browser-output-matrix": "node ./scripts/check-browser-output-matrix.mjs",
+ "check:release-authority": "node ./scripts/release-authority.test.mjs && node ./scripts/check-release-readiness.mjs --validate && node ./scripts/check-hosted-browser-matrix.mjs --validate && node ./scripts/check-document-authority.mjs && pnpm check:site-docs",
"integration:browser:chrome": "node ./scripts/check-local-cdp-browser.mjs \"C:/Program Files/Google/Chrome/Application/chrome.exe\"",
"integration:browser:edge": "node ./scripts/check-local-cdp-browser.mjs \"C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe\"",
"integration:browser:theme-root:chrome": "node ./scripts/check-theme-root-browser.mjs \"C:/Program Files/Google/Chrome/Application/chrome.exe\"",
"integration:browser:theme-root:edge": "node ./scripts/check-theme-root-browser.mjs \"C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe\"",
+ "integration:browser:breakpoints:chrome": "node ./scripts/check-breakpoint-order-browser.mjs \"C:/Program Files/Google/Chrome/Application/chrome.exe\"",
+ "integration:browser:breakpoints:edge": "node ./scripts/check-breakpoint-order-browser.mjs \"C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe\"",
"integration:browser:utility:chrome": "node ./scripts/check-utility-projection-browser.mjs \"C:/Program Files/Google/Chrome/Application/chrome.exe\"",
"integration:browser:utility:edge": "node ./scripts/check-utility-projection-browser.mjs \"C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe\"",
"integration:browser:template-group:chrome": "node ./scripts/check-template-group-browser.mjs \"C:/Program Files/Google/Chrome/Application/chrome.exe\"",
@@ -82,11 +96,17 @@
"bench:migration-adoption": "node ./scripts/measure-migration-adoption.mjs"
},
"devDependencies": {
- "@tailwindcss/cli": "4.3.2",
+ "@tailwindcss/cli": "4.3.3",
+ "@tailwindcss/cli-frozen": "npm:@tailwindcss/cli@4.3.2",
"esbuild": "0.28.1",
"gsap": "3.15.0",
"lenis": "1.3.25",
- "three": "0.185.1",
- "tailwindcss": "4.3.2"
+ "pixelmatch": "7.2.0",
+ "playwright": "1.61.1",
+ "pngjs": "7.0.0",
+ "tailwindcss": "4.3.3",
+ "tailwindcss-frozen": "npm:tailwindcss@4.3.2",
+ "tailwindcss-v3-lts": "npm:tailwindcss@3.4.19",
+ "three": "0.185.1"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8786f2b..85ab5e8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,8 +9,11 @@ importers:
.:
devDependencies:
'@tailwindcss/cli':
- specifier: 4.3.2
- version: 4.3.2
+ specifier: 4.3.3
+ version: 4.3.3
+ '@tailwindcss/cli-frozen':
+ specifier: npm:@tailwindcss/cli@4.3.2
+ version: '@tailwindcss/cli@4.3.2'
esbuild:
specifier: 0.28.1
version: 0.28.1
@@ -20,9 +23,24 @@ importers:
lenis:
specifier: 1.3.25
version: 1.3.25
+ pixelmatch:
+ specifier: 7.2.0
+ version: 7.2.0
+ playwright:
+ specifier: 1.61.1
+ version: 1.61.1
+ pngjs:
+ specifier: 7.0.0
+ version: 7.0.0
tailwindcss:
- specifier: 4.3.2
- version: 4.3.2
+ specifier: 4.3.3
+ version: 4.3.3
+ tailwindcss-frozen:
+ specifier: npm:tailwindcss@4.3.2
+ version: tailwindcss@4.3.2
+ tailwindcss-v3-lts:
+ specifier: npm:tailwindcss@3.4.19
+ version: tailwindcss@3.4.19
three:
specifier: 0.185.1
version: 0.185.1
@@ -73,6 +91,10 @@ importers:
packages:
+ '@alloc/quick-lru@5.2.0':
+ resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
+ engines: {node: '>=10'}
+
'@azu/format-text@1.0.2':
resolution: {integrity: sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==}
@@ -678,39 +700,76 @@ packages:
resolution: {integrity: sha512-Fzt+HrIZHDlkRYKdLMBeufaroaPvwCBG70sMLdmurdeadNMO/LxbmT8Sbb+P83ep0iAlAImettb7Y+rO+37rXw==}
hasBin: true
+ '@tailwindcss/cli@4.3.3':
+ resolution: {integrity: sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw==}
+ hasBin: true
+
'@tailwindcss/node@4.3.2':
resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==}
+ '@tailwindcss/node@4.3.3':
+ resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==}
+
'@tailwindcss/oxide-android-arm64@4.3.2':
resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [android]
+ '@tailwindcss/oxide-android-arm64@4.3.3':
+ resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [android]
+
'@tailwindcss/oxide-darwin-arm64@4.3.2':
resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [darwin]
+ '@tailwindcss/oxide-darwin-arm64@4.3.3':
+ resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [darwin]
+
'@tailwindcss/oxide-darwin-x64@4.3.2':
resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [darwin]
+ '@tailwindcss/oxide-darwin-x64@4.3.3':
+ resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [darwin]
+
'@tailwindcss/oxide-freebsd-x64@4.3.2':
resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [freebsd]
+ '@tailwindcss/oxide-freebsd-x64@4.3.3':
+ resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [freebsd]
+
'@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2':
resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==}
engines: {node: '>= 20'}
cpu: [arm]
os: [linux]
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3':
+ resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm]
+ os: [linux]
+
'@tailwindcss/oxide-linux-arm64-gnu@4.3.2':
resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==}
engines: {node: '>= 20'}
@@ -718,6 +777,13 @@ packages:
os: [linux]
libc: [glibc]
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.3':
+ resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
'@tailwindcss/oxide-linux-arm64-musl@4.3.2':
resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==}
engines: {node: '>= 20'}
@@ -725,6 +791,13 @@ packages:
os: [linux]
libc: [musl]
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.3':
+ resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
'@tailwindcss/oxide-linux-x64-gnu@4.3.2':
resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==}
engines: {node: '>= 20'}
@@ -732,6 +805,13 @@ packages:
os: [linux]
libc: [glibc]
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.3':
+ resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
'@tailwindcss/oxide-linux-x64-musl@4.3.2':
resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==}
engines: {node: '>= 20'}
@@ -739,6 +819,13 @@ packages:
os: [linux]
libc: [musl]
+ '@tailwindcss/oxide-linux-x64-musl@4.3.3':
+ resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
'@tailwindcss/oxide-wasm32-wasi@4.3.2':
resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==}
engines: {node: '>=14.0.0'}
@@ -751,22 +838,50 @@ packages:
- '@emnapi/wasi-threads'
- tslib
+ '@tailwindcss/oxide-wasm32-wasi@4.3.3':
+ resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
'@tailwindcss/oxide-win32-arm64-msvc@4.3.2':
resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [win32]
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.3':
+ resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [win32]
+
'@tailwindcss/oxide-win32-x64-msvc@4.3.2':
resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [win32]
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.3':
+ resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [win32]
+
'@tailwindcss/oxide@4.3.2':
resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==}
engines: {node: '>= 20'}
+ '@tailwindcss/oxide@4.3.3':
+ resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==}
+ engines: {node: '>= 20'}
+
'@textlint/ast-node-types@15.7.1':
resolution: {integrity: sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==}
@@ -878,6 +993,16 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
+ any-promise@1.3.0:
+ resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
+
+ anymatch@3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+
+ arg@5.0.2:
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -898,6 +1023,10 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ binary-extensions@2.3.0:
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+ engines: {node: '>=8'}
+
binaryextensions@6.11.0:
resolution: {integrity: sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==}
engines: {node: '>=4'}
@@ -943,6 +1072,10 @@ packages:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
+ camelcase-css@2.0.1:
+ resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
+ engines: {node: '>= 6'}
+
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@@ -958,6 +1091,10 @@ packages:
resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==}
engines: {node: '>=20.18.1'}
+ chokidar@3.6.0:
+ resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+ engines: {node: '>= 8.10.0'}
+
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
@@ -988,6 +1125,10 @@ packages:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
+ commander@4.1.1:
+ resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
+ engines: {node: '>= 6'}
+
cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
@@ -1002,6 +1143,11 @@ packages:
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
engines: {node: '>= 6'}
+ cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -1044,6 +1190,12 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
+ didyoumean@1.2.2:
+ resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
+
+ dlv@1.1.3:
+ resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
+
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
@@ -1084,6 +1236,10 @@ packages:
resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==}
engines: {node: '>=10.13.0'}
+ enhanced-resolve@5.24.3:
+ resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==}
+ engines: {node: '>=10.13.0'}
+
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@@ -1141,6 +1297,15 @@ packages:
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -1156,6 +1321,11 @@ packages:
resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==}
engines: {node: '>=14.14'}
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1183,6 +1353,10 @@ packages:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
glob@13.0.6:
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
engines: {node: 18 || 20 || >=22}
@@ -1260,6 +1434,14 @@ packages:
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+ is-binary-path@2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+
+ is-core-module@2.16.2:
+ resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==}
+ engines: {node: '>= 0.4'}
+
is-docker@3.0.0:
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -1309,6 +1491,10 @@ packages:
resolution: {integrity: sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==}
engines: {node: '>=4'}
+ jiti@1.21.7:
+ resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
+ hasBin: true
+
jiti@2.7.0:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
@@ -1449,6 +1635,13 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
+ lilconfig@3.1.3:
+ resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
+ engines: {node: '>=14'}
+
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
linkify-it@5.0.2:
resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==}
@@ -1566,6 +1759,14 @@ packages:
mute-stream@0.0.8:
resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
+ mz@2.7.0:
+ resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
+
+ nanoid@3.3.16:
+ resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
@@ -1587,9 +1788,21 @@ packages:
resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==}
engines: {node: ^16.14.0 || >=18.0.0}
+ normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
+ object-hash@3.0.0:
+ resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
+ engines: {node: '>= 6'}
+
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
@@ -1632,6 +1845,9 @@ packages:
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+ path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
path-scurry@2.0.2:
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
engines: {node: 18 || 20 || >=22}
@@ -1656,6 +1872,32 @@ packages:
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
engines: {node: '>=8.6'}
+ picomatch@4.0.5:
+ resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
+ engines: {node: '>=12'}
+
+ pify@2.3.0:
+ resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
+ engines: {node: '>=0.10.0'}
+
+ pirates@4.0.7:
+ resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
+ engines: {node: '>= 6'}
+
+ pixelmatch@7.2.0:
+ resolution: {integrity: sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==}
+ hasBin: true
+
+ playwright-core@1.61.1:
+ resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.61.1:
+ resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
pluralize@2.0.0:
resolution: {integrity: sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==}
@@ -1663,6 +1905,57 @@ packages:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
+ pngjs@7.0.0:
+ resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
+ engines: {node: '>=14.19.0'}
+
+ postcss-import@15.1.0:
+ resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ postcss: ^8.0.0
+
+ postcss-js@4.1.0:
+ resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==}
+ engines: {node: ^12 || ^14 || >= 16}
+ peerDependencies:
+ postcss: ^8.4.21
+
+ postcss-load-config@6.0.1:
+ resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ jiti: '>=1.21.0'
+ postcss: '>=8.0.9'
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+ postcss:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ postcss-nested@6.2.0:
+ resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.2.14
+
+ postcss-selector-parser@6.1.4:
+ resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==}
+ engines: {node: '>=4'}
+
+ postcss-value-parser@4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+
+ postcss@8.5.21:
+ resolution: {integrity: sha512-v4sDNP3fdNiWMfabO7OwOQdOX8TiQSztKyT1Wj0w+j7LDallJThJRBBBmzVGyYj0crMh7jlV4zepPkiNu9UwDQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
@@ -1693,6 +1986,9 @@ packages:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
+ read-cache@1.0.0:
+ resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
+
read-pkg@9.0.1:
resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==}
engines: {node: '>=18'}
@@ -1708,10 +2004,19 @@ packages:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
+ readdirp@3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
+ resolve@1.22.12:
+ resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
restore-cursor@5.1.0:
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
engines: {node: '>=18'}
@@ -1844,6 +2149,11 @@ packages:
structured-source@4.0.0:
resolution: {integrity: sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==}
+ sucrase@3.35.1:
+ resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+
supports-color@10.2.2:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'}
@@ -1856,13 +2166,25 @@ packages:
resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==}
engines: {node: '>=14.18'}
+ supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+
table@6.9.0:
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
engines: {node: '>=10.0.0'}
+ tailwindcss@3.4.19:
+ resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
tailwindcss@4.3.2:
resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==}
+ tailwindcss@4.3.3:
+ resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==}
+
tapable@2.3.3:
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
engines: {node: '>=6'}
@@ -1885,9 +2207,20 @@ packages:
resolution: {integrity: sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==}
engines: {node: '>=4'}
+ thenify-all@1.6.0:
+ resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
+ engines: {node: '>=0.8'}
+
+ thenify@3.3.1:
+ resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+
three@0.185.1:
resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==}
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
tmp@0.2.7:
resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==}
engines: {node: '>=14.14'}
@@ -1896,6 +2229,9 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
+ ts-interface-checker@0.1.13:
+ resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
+
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -2045,6 +2381,8 @@ packages:
snapshots:
+ '@alloc/quick-lru@5.2.0': {}
+
'@azu/format-text@1.0.2': {}
'@azu/style-format@1.0.1':
@@ -2545,6 +2883,16 @@ snapshots:
picocolors: 1.1.1
tailwindcss: 4.3.2
+ '@tailwindcss/cli@4.3.3':
+ dependencies:
+ '@parcel/watcher': 2.5.1
+ '@tailwindcss/node': 4.3.3
+ '@tailwindcss/oxide': 4.3.3
+ enhanced-resolve: 5.24.3
+ mri: 1.2.0
+ picocolors: 1.1.1
+ tailwindcss: 4.3.3
+
'@tailwindcss/node@4.3.2':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -2555,42 +2903,88 @@ snapshots:
source-map-js: 1.2.1
tailwindcss: 4.3.2
+ '@tailwindcss/node@4.3.3':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.24.3
+ jiti: 2.7.0
+ lightningcss: 1.32.0
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.3.3
+
'@tailwindcss/oxide-android-arm64@4.3.2':
optional: true
+ '@tailwindcss/oxide-android-arm64@4.3.3':
+ optional: true
+
'@tailwindcss/oxide-darwin-arm64@4.3.2':
optional: true
+ '@tailwindcss/oxide-darwin-arm64@4.3.3':
+ optional: true
+
'@tailwindcss/oxide-darwin-x64@4.3.2':
optional: true
+ '@tailwindcss/oxide-darwin-x64@4.3.3':
+ optional: true
+
'@tailwindcss/oxide-freebsd-x64@4.3.2':
optional: true
+ '@tailwindcss/oxide-freebsd-x64@4.3.3':
+ optional: true
+
'@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2':
optional: true
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3':
+ optional: true
+
'@tailwindcss/oxide-linux-arm64-gnu@4.3.2':
optional: true
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.3':
+ optional: true
+
'@tailwindcss/oxide-linux-arm64-musl@4.3.2':
optional: true
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.3':
+ optional: true
+
'@tailwindcss/oxide-linux-x64-gnu@4.3.2':
optional: true
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.3':
+ optional: true
+
'@tailwindcss/oxide-linux-x64-musl@4.3.2':
optional: true
+ '@tailwindcss/oxide-linux-x64-musl@4.3.3':
+ optional: true
+
'@tailwindcss/oxide-wasm32-wasi@4.3.2':
optional: true
+ '@tailwindcss/oxide-wasm32-wasi@4.3.3':
+ optional: true
+
'@tailwindcss/oxide-win32-arm64-msvc@4.3.2':
optional: true
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.3':
+ optional: true
+
'@tailwindcss/oxide-win32-x64-msvc@4.3.2':
optional: true
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.3':
+ optional: true
+
'@tailwindcss/oxide@4.3.2':
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.3.2
@@ -2606,6 +3000,21 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.2
'@tailwindcss/oxide-win32-x64-msvc': 4.3.2
+ '@tailwindcss/oxide@4.3.3':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.3.3
+ '@tailwindcss/oxide-darwin-arm64': 4.3.3
+ '@tailwindcss/oxide-darwin-x64': 4.3.3
+ '@tailwindcss/oxide-freebsd-x64': 4.3.3
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3
+ '@tailwindcss/oxide-linux-arm64-musl': 4.3.3
+ '@tailwindcss/oxide-linux-x64-gnu': 4.3.3
+ '@tailwindcss/oxide-linux-x64-musl': 4.3.3
+ '@tailwindcss/oxide-wasm32-wasi': 4.3.3
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3
+ '@tailwindcss/oxide-win32-x64-msvc': 4.3.3
+
'@textlint/ast-node-types@15.7.1': {}
'@textlint/linter-formatter@15.7.1':
@@ -2759,6 +3168,15 @@ snapshots:
dependencies:
color-convert: 2.0.1
+ any-promise@1.3.0: {}
+
+ anymatch@3.1.3:
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.2
+
+ arg@5.0.2: {}
+
argparse@2.0.1: {}
astral-regex@2.0.0: {}
@@ -2775,6 +3193,8 @@ snapshots:
base64-js@1.5.1:
optional: true
+ binary-extensions@2.3.0: {}
+
binaryextensions@6.11.0:
dependencies:
editions: 6.22.0
@@ -2824,6 +3244,8 @@ snapshots:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
+ camelcase-css@2.0.1: {}
+
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
@@ -2854,6 +3276,18 @@ snapshots:
undici: 7.28.0
whatwg-mimetype: 4.0.0
+ chokidar@3.6.0:
+ dependencies:
+ anymatch: 3.1.3
+ braces: 3.0.3
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
chownr@1.1.4:
optional: true
@@ -2877,6 +3311,8 @@ snapshots:
commander@12.1.0: {}
+ commander@4.1.1: {}
+
cookie@1.1.1: {}
core-util-is@1.0.3: {}
@@ -2891,6 +3327,8 @@ snapshots:
css-what@6.2.2: {}
+ cssesc@3.0.0: {}
+
debug@4.4.3:
dependencies:
ms: 2.1.3
@@ -2918,6 +3356,10 @@ snapshots:
detect-libc@2.1.2: {}
+ didyoumean@1.2.2: {}
+
+ dlv@1.1.3: {}
+
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
@@ -2969,6 +3411,11 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.3
+ enhanced-resolve@5.24.3:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.3
+
entities@4.5.0: {}
entities@6.0.1: {}
@@ -3042,6 +3489,10 @@ snapshots:
dependencies:
reusify: 1.1.0
+ fdir@6.5.0(picomatch@4.0.5):
+ optionalDependencies:
+ picomatch: 4.0.5
+
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -3063,6 +3514,9 @@ snapshots:
jsonfile: 6.2.1
universalify: 2.0.1
+ fsevents@2.3.2:
+ optional: true
+
fsevents@2.3.3:
optional: true
@@ -3095,6 +3549,10 @@ snapshots:
dependencies:
is-glob: 4.0.3
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
glob@13.0.6:
dependencies:
minimatch: 10.2.5
@@ -3175,6 +3633,14 @@ snapshots:
ini@1.3.8:
optional: true
+ is-binary-path@2.1.0:
+ dependencies:
+ binary-extensions: 2.3.0
+
+ is-core-module@2.16.2:
+ dependencies:
+ hasown: 2.0.4
+
is-docker@3.0.0: {}
is-extglob@2.1.1: {}
@@ -3209,6 +3675,8 @@ snapshots:
editions: 6.22.0
textextensions: 6.11.0
+ jiti@1.21.7: {}
+
jiti@2.7.0: {}
js-tokens@4.0.0: {}
@@ -3325,6 +3793,10 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
+ lilconfig@3.1.3: {}
+
+ lines-and-columns@1.2.4: {}
+
linkify-it@5.0.2:
dependencies:
uc.micro: 2.1.0
@@ -3427,6 +3899,14 @@ snapshots:
mute-stream@0.0.8: {}
+ mz@2.7.0:
+ dependencies:
+ any-promise: 1.3.0
+ object-assign: 4.1.1
+ thenify-all: 1.6.0
+
+ nanoid@3.3.16: {}
+
napi-build-utils@2.0.0:
optional: true
@@ -3451,10 +3931,16 @@ snapshots:
semver: 7.8.5
validate-npm-package-license: 3.0.4
+ normalize-path@3.0.0: {}
+
nth-check@2.1.1:
dependencies:
boolbase: 1.0.0
+ object-assign@4.1.1: {}
+
+ object-hash@3.0.0: {}
+
object-inspect@1.13.4: {}
once@1.4.0:
@@ -3512,6 +3998,8 @@ snapshots:
dependencies:
entities: 6.0.1
+ path-parse@1.0.7: {}
+
path-scurry@2.0.2:
dependencies:
lru-cache: 11.5.2
@@ -3529,10 +4017,67 @@ snapshots:
picomatch@2.3.2: {}
+ picomatch@4.0.5: {}
+
+ pify@2.3.0: {}
+
+ pirates@4.0.7: {}
+
+ pixelmatch@7.2.0:
+ dependencies:
+ pngjs: 7.0.0
+
+ playwright-core@1.61.1: {}
+
+ playwright@1.61.1:
+ dependencies:
+ playwright-core: 1.61.1
+ optionalDependencies:
+ fsevents: 2.3.2
+
pluralize@2.0.0: {}
pluralize@8.0.0: {}
+ pngjs@7.0.0: {}
+
+ postcss-import@15.1.0(postcss@8.5.21):
+ dependencies:
+ postcss: 8.5.21
+ postcss-value-parser: 4.2.0
+ read-cache: 1.0.0
+ resolve: 1.22.12
+
+ postcss-js@4.1.0(postcss@8.5.21):
+ dependencies:
+ camelcase-css: 2.0.1
+ postcss: 8.5.21
+
+ postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.21):
+ dependencies:
+ lilconfig: 3.1.3
+ optionalDependencies:
+ jiti: 1.21.7
+ postcss: 8.5.21
+
+ postcss-nested@6.2.0(postcss@8.5.21):
+ dependencies:
+ postcss: 8.5.21
+ postcss-selector-parser: 6.1.4
+
+ postcss-selector-parser@6.1.4:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss-value-parser@4.2.0: {}
+
+ postcss@8.5.21:
+ dependencies:
+ nanoid: 3.3.16
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.1.2
@@ -3583,6 +4128,10 @@ snapshots:
strip-json-comments: 2.0.1
optional: true
+ read-cache@1.0.0:
+ dependencies:
+ pify: 2.3.0
+
read-pkg@9.0.1:
dependencies:
'@types/normalize-package-data': 2.4.4
@@ -3612,8 +4161,19 @@ snapshots:
util-deprecate: 1.0.2
optional: true
+ readdirp@3.6.0:
+ dependencies:
+ picomatch: 2.3.2
+
require-from-string@2.0.2: {}
+ resolve@1.22.12:
+ dependencies:
+ es-errors: 1.3.0
+ is-core-module: 2.16.2
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
restore-cursor@5.1.0:
dependencies:
onetime: 7.0.0
@@ -3786,6 +4346,16 @@ snapshots:
dependencies:
boundary: 2.0.0
+ sucrase@3.35.1:
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ commander: 4.1.1
+ lines-and-columns: 1.2.4
+ mz: 2.7.0
+ pirates: 4.0.7
+ tinyglobby: 0.2.17
+ ts-interface-checker: 0.1.13
+
supports-color@10.2.2: {}
supports-color@7.2.0:
@@ -3797,6 +4367,8 @@ snapshots:
has-flag: 4.0.0
supports-color: 7.2.0
+ supports-preserve-symlinks-flag@1.0.0: {}
+
table@6.9.0:
dependencies:
ajv: 8.20.0
@@ -3805,8 +4377,38 @@ snapshots:
string-width: 4.2.3
strip-ansi: 6.0.1
+ tailwindcss@3.4.19:
+ dependencies:
+ '@alloc/quick-lru': 5.2.0
+ arg: 5.0.2
+ chokidar: 3.6.0
+ didyoumean: 1.2.2
+ dlv: 1.1.3
+ fast-glob: 3.3.3
+ glob-parent: 6.0.2
+ is-glob: 4.0.3
+ jiti: 1.21.7
+ lilconfig: 3.1.3
+ micromatch: 4.0.8
+ normalize-path: 3.0.0
+ object-hash: 3.0.0
+ picocolors: 1.1.1
+ postcss: 8.5.21
+ postcss-import: 15.1.0(postcss@8.5.21)
+ postcss-js: 4.1.0(postcss@8.5.21)
+ postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.21)
+ postcss-nested: 6.2.0(postcss@8.5.21)
+ postcss-selector-parser: 6.1.4
+ resolve: 1.22.12
+ sucrase: 3.35.1
+ transitivePeerDependencies:
+ - tsx
+ - yaml
+
tailwindcss@4.3.2: {}
+ tailwindcss@4.3.3: {}
+
tapable@2.3.3: {}
tar-fs@2.1.5:
@@ -3837,14 +4439,29 @@ snapshots:
dependencies:
editions: 6.22.0
+ thenify-all@1.6.0:
+ dependencies:
+ thenify: 3.3.1
+
+ thenify@3.3.1:
+ dependencies:
+ any-promise: 1.3.0
+
three@0.185.1: {}
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.5)
+ picomatch: 4.0.5
+
tmp@0.2.7: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
+ ts-interface-checker@0.1.13: {}
+
tslib@2.8.1: {}
tunnel-agent@0.6.0:
diff --git a/scripts/benchmark-authority-v2.mjs b/scripts/benchmark-authority-v2.mjs
new file mode 100644
index 0000000..7308ddb
--- /dev/null
+++ b/scripts/benchmark-authority-v2.mjs
@@ -0,0 +1,127 @@
+import { createHash } from "node:crypto";
+import { readFileSync } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+export const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+export const authorityPath = join(
+ repositoryRoot,
+ "benchmarks",
+ "benchmark-authority-v2",
+ "oracle.json",
+);
+
+export function sha256(bytes) {
+ return createHash("sha256").update(bytes).digest("hex");
+}
+
+export function loadAuthority() {
+ return JSON.parse(readFileSync(authorityPath, "utf8"));
+}
+
+export function extractClassValues(html) {
+ return [...html.matchAll(/class="([^"]*)"/gu)].map((match) => match[1]);
+}
+
+export function materializeCorpus(corpus) {
+ const source = readFileSync(resolve(repositoryRoot, corpus.source), "utf8").replaceAll(
+ "\r\n",
+ "\n",
+ );
+ if (corpus.materialization === "tracked") {
+ return source;
+ }
+ if (corpus.materialization !== "repeat-body") {
+ throw new Error(`unknown corpus materialization ${JSON.stringify(corpus.materialization)}`);
+ }
+ const body = /]*>([\s\S]*?)<\/body>/u.exec(source);
+ if (!body) {
+ throw new Error(`${corpus.source} has no materializable body`);
+ }
+ const bodyClass = body[1]?.trim() ?? "";
+ const contents = body[2].trim();
+ const copies = Array.from({ length: corpus.repeat }, (_, index) => {
+ const classAttribute = bodyClass ? ` class="${bodyClass}"` : "";
+ return ` \n${contents}\n `;
+ }).join("\n");
+ return [
+ "",
+ '',
+ " ",
+ ' ',
+ ' ',
+ ` PliegoCSS benchmark authority ${corpus.id} corpus `,
+ " ",
+ " ",
+ copies,
+ " ",
+ "",
+ "",
+ ].join("\n");
+}
+
+export function corpusMetrics(html) {
+ const classValues = extractClassValues(html);
+ const utilityTokens = classValues.flatMap((value) => value.trim().split(/\s+/u).filter(Boolean));
+ return {
+ sha256: sha256(Buffer.from(html, "utf8")),
+ htmlBytes: Buffer.byteLength(html),
+ classAttributeCount: classValues.length,
+ utilityOccurrences: utilityTokens.length,
+ uniqueUtilityTokens: new Set(utilityTokens).size,
+ uniqueStyleInputs: new Set(classValues).size,
+ };
+}
+
+export function round(value) {
+ return Number(value.toFixed(3));
+}
+
+function median(sorted) {
+ const middle = Math.floor(sorted.length / 2);
+ return sorted.length % 2 === 0
+ ? (sorted[middle - 1] + sorted[middle]) / 2
+ : sorted[middle];
+}
+
+export function summarize(values, unit) {
+ if (!Array.isArray(values) || values.length === 0 || values.some((value) => !Number.isFinite(value))) {
+ throw new Error("cannot summarize an empty or non-finite sample series");
+ }
+ const sorted = [...values].sort((left, right) => left - right);
+ const center = median(sorted);
+ const deviations = sorted.map((value) => Math.abs(value - center)).sort((left, right) => left - right);
+ return {
+ unit,
+ samples: values.map(round),
+ count: values.length,
+ min: round(sorted[0]),
+ median: round(center),
+ p95: round(sorted[Math.ceil(sorted.length * 0.95) - 1]),
+ max: round(sorted.at(-1)),
+ medianAbsoluteDeviation: round(median(deviations)),
+ };
+}
+
+export function packageJsonPath(alias) {
+ return join(repositoryRoot, "node_modules", ...alias.split("/"), "package.json");
+}
+
+export function readInstalledPackage(alias) {
+ const path = packageJsonPath(alias);
+ return { path, value: JSON.parse(readFileSync(path, "utf8")) };
+}
+
+export function resolveLaneCli(lane) {
+ const installed = readInstalledPackage(lane.cliPackageAlias);
+ const bin = installed.value.bin;
+ const relative = typeof bin === "string" ? bin : (bin?.tailwindcss ?? bin?.tailwind);
+ if (!relative) {
+ throw new Error(`${lane.cliPackageAlias} exposes no Tailwind CLI binary`);
+ }
+ return resolve(dirname(installed.path), relative);
+}
+
+export function canonicalLaneMap(authority) {
+ return new Map(authority.lanes.map((lane) => [lane.id, lane]));
+}
diff --git a/scripts/benchmark-process-metrics.rs b/scripts/benchmark-process-metrics.rs
new file mode 100644
index 0000000..1a613c2
--- /dev/null
+++ b/scripts/benchmark-process-metrics.rs
@@ -0,0 +1,171 @@
+use std::env;
+use std::process::{Command, ExitCode, Stdio};
+use std::thread;
+use std::time::{Duration, Instant};
+
+#[cfg(windows)]
+use std::ffi::c_void;
+#[cfg(target_os = "macos")]
+use std::ffi::c_void;
+#[cfg(target_os = "linux")]
+use std::fs;
+#[cfg(windows)]
+use std::mem::{size_of, zeroed};
+#[cfg(windows)]
+use std::os::windows::io::AsRawHandle;
+
+#[cfg(windows)]
+#[allow(non_snake_case)]
+#[repr(C)]
+struct ProcessMemoryCounters {
+ cb: u32,
+ PageFaultCount: u32,
+ PeakWorkingSetSize: usize,
+ WorkingSetSize: usize,
+ QuotaPeakPagedPoolUsage: usize,
+ QuotaPagedPoolUsage: usize,
+ QuotaPeakNonPagedPoolUsage: usize,
+ QuotaNonPagedPoolUsage: usize,
+ PagefileUsage: usize,
+ PeakPagefileUsage: usize,
+}
+
+#[cfg(windows)]
+#[link(name = "psapi")]
+unsafe extern "system" {
+ fn GetProcessMemoryInfo(
+ process: *mut c_void,
+ counters: *mut ProcessMemoryCounters,
+ size: u32,
+ ) -> i32;
+}
+
+#[cfg(windows)]
+fn peak_working_set(child: &std::process::Child) -> Option {
+ let mut counters: ProcessMemoryCounters = unsafe { zeroed() };
+ counters.cb = size_of::() as u32;
+ let succeeded = unsafe {
+ GetProcessMemoryInfo(
+ child.as_raw_handle(),
+ &mut counters,
+ size_of::() as u32,
+ )
+ };
+ (succeeded != 0).then_some(counters.PeakWorkingSetSize as u64)
+}
+
+#[cfg(target_os = "linux")]
+fn peak_working_set(child: &std::process::Child) -> Option {
+ let status = fs::read_to_string(format!("/proc/{}/status", child.id())).ok()?;
+ let kilobytes = status.lines().find_map(|line| {
+ line.strip_prefix("VmHWM:")?
+ .split_whitespace()
+ .next()?
+ .parse::()
+ .ok()
+ })?;
+ Some(kilobytes * 1024)
+}
+
+#[cfg(target_os = "macos")]
+#[repr(C)]
+struct RusageInfoV2 {
+ uuid: [u8; 16],
+ user_time: u64,
+ system_time: u64,
+ pkg_idle_wkups: u64,
+ interrupt_wkups: u64,
+ pageins: u64,
+ wired_size: u64,
+ resident_size: u64,
+ phys_footprint: u64,
+ proc_start_abstime: u64,
+ proc_exit_abstime: u64,
+}
+
+#[cfg(target_os = "macos")]
+unsafe extern "C" {
+ fn proc_pid_rusage(pid: i32, flavor: i32, buffer: *mut c_void) -> i32;
+}
+
+#[cfg(target_os = "macos")]
+fn peak_working_set(child: &std::process::Child) -> Option {
+ const RUSAGE_INFO_V2: i32 = 2;
+ let mut usage = RusageInfoV2 {
+ uuid: [0; 16],
+ user_time: 0,
+ system_time: 0,
+ pkg_idle_wkups: 0,
+ interrupt_wkups: 0,
+ pageins: 0,
+ wired_size: 0,
+ resident_size: 0,
+ phys_footprint: 0,
+ proc_start_abstime: 0,
+ proc_exit_abstime: 0,
+ };
+ let succeeded = unsafe {
+ proc_pid_rusage(
+ child.id() as i32,
+ RUSAGE_INFO_V2,
+ (&mut usage as *mut RusageInfoV2).cast::(),
+ )
+ };
+ (succeeded == 0).then_some(usage.resident_size)
+}
+
+#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
+fn peak_working_set(_child: &std::process::Child) -> Option {
+ None
+}
+
+fn main() -> ExitCode {
+ let mut arguments = env::args_os();
+ let _program = arguments.next();
+ let Some(command) = arguments.next() else {
+ eprintln!("usage: benchmark-process-metrics [arguments ...]");
+ return ExitCode::from(2);
+ };
+
+ let started = Instant::now();
+ let mut child = match Command::new(command)
+ .args(arguments)
+ .stdin(Stdio::null())
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .spawn()
+ {
+ Ok(child) => child,
+ Err(error) => {
+ eprintln!("cannot start measured process: {error}");
+ return ExitCode::from(2);
+ }
+ };
+
+ let mut peak = 0_u64;
+ loop {
+ if let Some(observed) = peak_working_set(&child) {
+ peak = peak.max(observed);
+ }
+ match child.try_wait() {
+ Ok(Some(status)) => {
+ let elapsed = started.elapsed().as_nanos();
+ let code = status.code().unwrap_or(-1);
+ println!(
+ "{{\"schemaVersion\":1,\"elapsedNs\":{elapsed},\"peakWorkingSetBytes\":{peak},\"exitCode\":{code}}}"
+ );
+ return if status.success() {
+ ExitCode::SUCCESS
+ } else {
+ ExitCode::from(1)
+ };
+ }
+ Ok(None) => thread::sleep(Duration::from_millis(1)),
+ Err(error) => {
+ eprintln!("cannot observe measured process: {error}");
+ let _ = child.kill();
+ return ExitCode::from(2);
+ }
+ }
+ }
+}
diff --git a/scripts/browser-output-certification.mjs b/scripts/browser-output-certification.mjs
new file mode 100644
index 0000000..2309a75
--- /dev/null
+++ b/scripts/browser-output-certification.mjs
@@ -0,0 +1,721 @@
+import { spawnSync } from "node:child_process";
+import {
+ mkdirSync,
+ readFileSync,
+ rmSync,
+ writeFileSync,
+} from "node:fs";
+import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
+import pixelmatch from "pixelmatch";
+import { PNG } from "pngjs";
+import { chromium, firefox, webkit } from "playwright";
+import {
+ authorityPath as competitorAuthorityPath,
+ extractClassValues,
+ loadAuthority as loadCompetitorAuthority,
+ readInstalledPackage,
+ repositoryRoot,
+ resolveLaneCli,
+ sha256,
+} from "./benchmark-authority-v2.mjs";
+import { targetDirectoryForRustcIdentity } from "./rust-target.mjs";
+
+const authorityPath = resolve(
+ repositoryRoot,
+ "benchmarks",
+ "browser-output-certification-v1",
+ "authority.json",
+);
+const authority = JSON.parse(readFileSync(authorityPath, "utf8"));
+const browserTypes = { chromium, firefox, webkit };
+
+function fail(message) {
+ throw new Error(`Browser/output certification: ${message}`);
+}
+
+function parseOptions(arguments_) {
+ const options = {
+ browser: null,
+ host: null,
+ output: null,
+ requireClean: false,
+ };
+ for (const argument of arguments_) {
+ if (argument === "--") continue;
+ if (argument === "--require-clean") options.requireClean = true;
+ else if (argument.startsWith("--browser=")) options.browser = argument.slice(10);
+ else if (argument.startsWith("--host=")) options.host = argument.slice(7);
+ else if (argument.startsWith("--output=")) {
+ options.output = resolve(repositoryRoot, argument.slice(9));
+ } else {
+ fail(
+ "usage: node scripts/browser-output-certification.mjs --browser= [--host=] [--output=] [--require-clean]",
+ );
+ }
+ }
+ if (!authority.browsers.includes(options.browser)) fail("a supported --browser is required");
+ const candidates = authority.hosts.filter(
+ (host) =>
+ host.browser === options.browser &&
+ host.os === process.platform &&
+ host.arch === process.arch,
+ );
+ const host = options.host
+ ? authority.hosts.find((entry) => entry.id === options.host)
+ : candidates[0];
+ if (!host) fail("the current OS/architecture/browser has no authority host");
+ if (
+ host.browser !== options.browser ||
+ host.os !== process.platform ||
+ host.arch !== process.arch
+ ) {
+ fail(
+ `host ${host.id} requires ${host.os}/${host.arch}/${host.browser}, found ${process.platform}/${process.arch}/${options.browser}`,
+ );
+ }
+ options.host = host;
+ options.output ??= resolve(
+ repositoryRoot,
+ "target",
+ "browser-output-certification",
+ `${host.id}.local.json`,
+ );
+ const child = relative(repositoryRoot, options.output);
+ if (
+ !child ||
+ child === ".." ||
+ child.startsWith(`..${sep}`) ||
+ isAbsolute(child) ||
+ !options.output.endsWith(".json")
+ ) {
+ fail("--output must be a .json path inside the repository");
+ }
+ return options;
+}
+
+function run(command, arguments_, options = {}) {
+ const result = spawnSync(command, arguments_, {
+ cwd: repositoryRoot,
+ encoding: "utf8",
+ windowsHide: true,
+ timeout: 600_000,
+ maxBuffer: 32 * 1024 * 1024,
+ ...options,
+ });
+ if (result.error) throw result.error;
+ if (result.status !== 0) {
+ fail(
+ `${command} ${arguments_.join(" ")} exited ${result.status}\n${`${result.stdout ?? ""}${result.stderr ?? ""}`.slice(-8_192)}`,
+ );
+ }
+ return result;
+}
+
+function git(arguments_) {
+ return run("git", arguments_).stdout;
+}
+
+function gitState() {
+ const status = git(["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
+ return {
+ commit: git(["rev-parse", "HEAD"]).trim(),
+ gitTree: git(["rev-parse", "HEAD^{tree}"]).trim(),
+ dirty: status.length > 0,
+ statusEntryCount: status.split("\0").filter(Boolean).length,
+ statusSha256: sha256(Buffer.from(status, "utf8")),
+ };
+}
+
+function instant(date = new Date()) {
+ return date.toISOString().replace(/\.\d{3}Z$/u, "Z");
+}
+
+function prepareCompiler() {
+ const rustcVerbose = run("rustc", ["+1.96.0", "-vV"]).stdout.trim();
+ const host = /^host: (\S+)$/mu.exec(rustcVerbose)?.[1];
+ if (!host) fail("cannot resolve Rust host");
+ const cargoTarget = targetDirectoryForRustcIdentity(
+ repositoryRoot,
+ process.env,
+ rustcVerbose,
+ );
+ const environment = {
+ ...process.env,
+ CARGO_INCREMENTAL: "0",
+ CARGO_TARGET_DIR: cargoTarget,
+ CARGO_TERM_COLOR: "never",
+ };
+ run(
+ "cargo",
+ ["+1.96.0", "build", "--release", "--locked", "--target", host, "-p", "pliego-cssc"],
+ { env: environment },
+ );
+ const executable = join(
+ cargoTarget,
+ host,
+ "release",
+ process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc",
+ );
+ return {
+ executable,
+ sha256: sha256(readFileSync(executable)),
+ rustc: /^rustc .+$/mu.exec(rustcVerbose)?.[0] ?? "unknown",
+ host,
+ };
+}
+
+function tailwindInput(packageAlias) {
+ return `@layer theme, utilities;\n@import "${packageAlias}/theme.css" layer(theme);\n@import "${packageAlias}/utilities.css" layer(utilities) source(none);\n@source "./fixture.html";\n@theme {\n --color-canvas: oklch(98.5% .004 80);\n --color-surface: #fff;\n --color-surface-raised: oklch(96% .006 80);\n --color-ink: oklch(19% .012 70);\n --color-muted: oklch(50% .014 70);\n --color-accent: oklch(58% .19 32);\n --color-accent-strong: oklch(50% .18 30);\n --color-line: oklch(88% .008 75);\n --font-sans: ui-sans-serif, system-ui, sans-serif;\n --shadow-sm: 0 1px 2px #0000000d;\n}\n`;
+}
+
+function decorateFixture(html) {
+ let index = 0;
+ return html.replace(/class="([^"]*)"/gu, (_, classes) => {
+ const id = `node-${String(index).padStart(2, "0")}`;
+ index += 1;
+ return `data-cert-node="${id}" class="${classes}"`;
+ });
+}
+
+function rewritePliegoHtml(html, manifest) {
+ const bySource = new Map();
+ for (const style of manifest.styles ?? []) {
+ for (const origin of style.origins ?? []) {
+ const existing = bySource.get(origin.source);
+ if (existing && existing !== style.className) {
+ fail(`manifest maps ${JSON.stringify(origin.source)} to multiple classes`);
+ }
+ bySource.set(origin.source, style.className);
+ }
+ }
+ let replacements = 0;
+ const rewritten = html.replace(/class="([^"]*)"/gu, (_, source) => {
+ const replacement = bySource.get(source);
+ if (!replacement) fail(`manifest has no generated class for ${JSON.stringify(source)}`);
+ replacements += 1;
+ return `class="${replacement}"`;
+ });
+ if (replacements !== extractClassValues(html).length) fail("not every class attribute was rewritten");
+ return rewritten;
+}
+
+function withoutStylesheetLink(html) {
+ return html.replace(/\s*]*>\s*/u, "\n");
+}
+
+function browserDocument(html, css, resetCss) {
+ const deterministicCss = `\n*,:before,:after{animation-duration:0s!important;animation-delay:0s!important;transition-duration:0s!important;caret-color:transparent!important}\n`;
+ const style = ``;
+ return withoutStylesheetLink(html).replace("", `${style}`);
+}
+
+function prepareOutputs(compiler) {
+ const competitor = loadCompetitorAuthority();
+ const lane = competitor.lanes.find((entry) => entry.id === authority.competitorLane);
+ if (!lane) fail(`competitor lane ${authority.competitorLane} is missing`);
+ if (Date.now() > Date.parse(competitor.expiresAtUtc)) fail("competitor oracle is expired");
+ const fixturePath = resolve(repositoryRoot, authority.fixture.path);
+ const fixtureBytes = readFileSync(fixturePath);
+ if (sha256(fixtureBytes) !== authority.fixture.sha256) fail("fixture hash drifted");
+ const resetPath = resolve(repositoryRoot, authority.reset.path);
+ const resetBytes = readFileSync(resetPath);
+ if (sha256(resetBytes) !== authority.reset.sha256) fail("shared reset hash drifted");
+
+ const workRoot = resolve(
+ repositoryRoot,
+ "target",
+ "browser-output-certification",
+ `work-${process.pid}-${Date.now()}`,
+ );
+ mkdirSync(workRoot, { recursive: true });
+ const fixture = decorateFixture(fixtureBytes.toString("utf8"));
+ const values = [...new Set(extractClassValues(fixture))];
+ const fixtureOutput = join(workRoot, "fixture.html");
+ const stylesOutput = join(workRoot, "styles.txt");
+ const inputOutput = join(workRoot, "input.css");
+ const tailwindOutput = join(workRoot, "tailwind.css");
+ const pliegoOutput = join(workRoot, "pliego.css");
+ const manifestOutput = join(workRoot, "pliego.manifest.json");
+ writeFileSync(fixtureOutput, fixture);
+ writeFileSync(stylesOutput, `${values.join("\n")}\n`);
+ writeFileSync(inputOutput, tailwindInput(lane.packageAlias));
+ run(
+ process.execPath,
+ [resolveLaneCli(lane), "-i", inputOutput, "-o", tailwindOutput, "--minify"],
+ { cwd: workRoot },
+ );
+ run(
+ compiler.executable,
+ [
+ "compile",
+ "--input",
+ stylesOutput,
+ "--seed",
+ "--theme",
+ "--output",
+ pliegoOutput,
+ "--manifest",
+ manifestOutput,
+ ],
+ { cwd: workRoot },
+ );
+ const manifest = JSON.parse(readFileSync(manifestOutput, "utf8"));
+ const pliegoHtml = rewritePliegoHtml(fixture, manifest);
+ const tailwindCss = readFileSync(tailwindOutput, "utf8");
+ const pliegoCss = readFileSync(pliegoOutput, "utf8");
+ return {
+ workRoot,
+ lane,
+ resetCss: resetBytes.toString("utf8"),
+ html: { pliego: pliegoHtml, tailwind: fixture },
+ css: { pliego: pliegoCss, tailwind: tailwindCss },
+ provenance: {
+ competitorAuthoritySha256: sha256(readFileSync(competitorAuthorityPath)),
+ competitorObservedAtUtc: competitor.observedAtUtc,
+ competitorExpiresAtUtc: competitor.expiresAtUtc,
+ tailwindVersion: lane.version,
+ tailwindCliVersion: lane.cliVersion,
+ tailwindPackageManifestSha256: sha256(
+ readFileSync(readInstalledPackage(lane.packageAlias).path),
+ ),
+ tailwindCliManifestSha256: sha256(
+ readFileSync(readInstalledPackage(lane.cliPackageAlias).path),
+ ),
+ fixtureSha256: authority.fixture.sha256,
+ resetSha256: authority.reset.sha256,
+ pliegoCssSha256: sha256(pliegoCss),
+ tailwindCssSha256: sha256(tailwindCss),
+ },
+ };
+}
+
+async function capture(page, properties) {
+ return page.evaluate(({ propertyNames, lengthQuantum, geometryResolution }) => {
+ const colorProperties = new Set([
+ "color",
+ "background-color",
+ "border-top-color",
+ "outline-color",
+ ]);
+ const canvas = document.createElement("canvas");
+ canvas.width = 1;
+ canvas.height = 1;
+ const context = canvas.getContext("2d", { willReadFrequently: true });
+ const normalizeColor = (value) => {
+ context.clearRect(0, 0, 1, 1);
+ context.fillStyle = "rgba(0,0,0,0)";
+ context.fillStyle = value;
+ context.fillRect(0, 0, 1, 1);
+ return [...context.getImageData(0, 0, 1, 1).data].join(",");
+ };
+ const normalizeLengths = (value) =>
+ value.replace(/-?(?:\d+\.?\d*|\.\d+)px/gu, (token) => {
+ const number = Number.parseFloat(token);
+ const rounded = Math.round(number / lengthQuantum) * lengthQuantum;
+ return `${Object.is(rounded, -0) ? 0 : Number(rounded.toFixed(4))}px`;
+ });
+ const quantize = (value) => {
+ const rounded = Math.round(value / geometryResolution) * geometryResolution;
+ return Object.is(rounded, -0) ? 0 : Number(rounded.toFixed(6));
+ };
+ const rectangle = (value) => ({
+ x: quantize(value.x),
+ y: quantize(value.y),
+ width: quantize(value.width),
+ height: quantize(value.height),
+ });
+ const directTextRectangles = (element) => {
+ const rectangles = [];
+ for (const node of element.childNodes) {
+ if (node.nodeType !== Node.TEXT_NODE || !node.textContent?.trim()) continue;
+ const range = document.createRange();
+ range.selectNodeContents(node);
+ rectangles.push(...[...range.getClientRects()].map(rectangle));
+ range.detach();
+ }
+ return rectangles;
+ };
+ const nodes = {};
+ for (const element of document.querySelectorAll("[data-cert-node]")) {
+ const id = element.getAttribute("data-cert-node");
+ const style = getComputedStyle(element);
+ const computed = {};
+ for (const property of propertyNames) {
+ let value = style.getPropertyValue(property).trim();
+ if (colorProperties.has(property)) value = normalizeColor(value);
+ if (property === "justify-content" && value === "end") value = "flex-end";
+ value = normalizeLengths(value);
+ computed[property] = value;
+ }
+ const record = {
+ tag: element.tagName.toLowerCase(),
+ computed,
+ geometry: {
+ borderBox: rectangle(element.getBoundingClientRect()),
+ clientWidth: element.clientWidth,
+ clientHeight: element.clientHeight,
+ scrollWidth: element.scrollWidth,
+ scrollHeight: element.scrollHeight,
+ directTextRects: directTextRectangles(element),
+ },
+ };
+ if (element.matches("input[placeholder], textarea[placeholder]")) {
+ const placeholder = getComputedStyle(element, "::placeholder");
+ record.placeholder = {
+ color: normalizeColor(placeholder.getPropertyValue("color").trim()),
+ opacity: placeholder.getPropertyValue("opacity").trim(),
+ };
+ }
+ nodes[id] = record;
+ }
+ return {
+ activeNode: document.activeElement?.getAttribute("data-cert-node") ?? null,
+ documentElement: {
+ backgroundColor: getComputedStyle(document.documentElement).backgroundColor,
+ colorScheme: getComputedStyle(document.documentElement).colorScheme,
+ },
+ nodes,
+ };
+ }, {
+ propertyNames: properties,
+ lengthQuantum: authority.comparison.computedLengthQuantumCssPx,
+ geometryResolution: authority.comparison.layoutGeometryResolutionCssPx,
+ });
+}
+
+function compareScreenshots(pliegoBytes, tailwindBytes, diffPath) {
+ const pliego = PNG.sync.read(pliegoBytes);
+ const tailwind = PNG.sync.read(tailwindBytes);
+ if (pliego.width !== tailwind.width || pliego.height !== tailwind.height) {
+ return {
+ passed: false,
+ byteEqual: false,
+ width: pliego.width,
+ height: pliego.height,
+ tailwindWidth: tailwind.width,
+ tailwindHeight: tailwind.height,
+ mismatchPixels: null,
+ mismatchRatio: null,
+ diff: null,
+ };
+ }
+ const diff = new PNG({ width: pliego.width, height: pliego.height });
+ const mismatchPixels = pixelmatch(
+ pliego.data,
+ tailwind.data,
+ diff.data,
+ pliego.width,
+ pliego.height,
+ {
+ threshold: authority.comparison.screenshotPixelThreshold,
+ includeAA: false,
+ diffMask: true,
+ },
+ );
+ const mismatchRatio = mismatchPixels / (pliego.width * pliego.height);
+ const diffBytes = PNG.sync.write(diff);
+ writeFileSync(diffPath, diffBytes);
+ return {
+ passed: mismatchRatio <= authority.comparison.maximumScreenshotMismatchRatio,
+ byteEqual: pliegoBytes.equals(tailwindBytes),
+ width: pliego.width,
+ height: pliego.height,
+ tailwindWidth: tailwind.width,
+ tailwindHeight: tailwind.height,
+ mismatchPixels,
+ mismatchRatio: Number(mismatchRatio.toFixed(9)),
+ diff: {
+ path: relative(dirname(options.output), diffPath).replaceAll("\\", "/"),
+ sha256: sha256(diffBytes),
+ bytes: diffBytes.byteLength,
+ },
+ };
+}
+
+function computedDiff(pliego, tailwind) {
+ const differences = [];
+ const geometry = { maximumDeltaCssPx: 0 };
+ const ids = [...new Set([...Object.keys(pliego.nodes), ...Object.keys(tailwind.nodes)])].sort();
+ if (pliego.activeNode !== tailwind.activeNode) {
+ differences.push({ path: "activeNode", pliego: pliego.activeNode, tailwind: tailwind.activeNode });
+ }
+ for (const id of ids) {
+ const left = pliego.nodes[id];
+ const right = tailwind.nodes[id];
+ if (!left || !right) {
+ differences.push({ path: id, pliego: left ?? null, tailwind: right ?? null });
+ continue;
+ }
+ for (const property of authority.computedProperties) {
+ if (left.computed[property] !== right.computed[property]) {
+ differences.push({
+ path: `${id}.${property}`,
+ pliego: left.computed[property],
+ tailwind: right.computed[property],
+ });
+ }
+ }
+ geometryDiff(left.geometry, right.geometry, `${id}.geometry`, differences, geometry);
+ if (JSON.stringify(left.placeholder ?? null) !== JSON.stringify(right.placeholder ?? null)) {
+ differences.push({
+ path: `${id}.placeholder`,
+ pliego: left.placeholder ?? null,
+ tailwind: right.placeholder ?? null,
+ });
+ }
+ }
+ return {
+ differences,
+ maximumLayoutGeometryDeltaCssPx: Number(geometry.maximumDeltaCssPx.toFixed(6)),
+ };
+}
+
+function geometryDiff(pliego, tailwind, path, differences, geometry) {
+ if (typeof pliego === "number" && typeof tailwind === "number") {
+ const delta = Math.abs(pliego - tailwind);
+ geometry.maximumDeltaCssPx = Math.max(geometry.maximumDeltaCssPx, delta);
+ if (delta > authority.comparison.maximumLayoutGeometryDeltaCssPx) {
+ differences.push({ path, pliego, tailwind, delta: Number(delta.toFixed(6)) });
+ }
+ return;
+ }
+ if (Array.isArray(pliego) && Array.isArray(tailwind)) {
+ if (pliego.length !== tailwind.length) {
+ differences.push({ path: `${path}.length`, pliego: pliego.length, tailwind: tailwind.length });
+ return;
+ }
+ for (let index = 0; index < pliego.length; index += 1) {
+ geometryDiff(pliego[index], tailwind[index], `${path}[${index}]`, differences, geometry);
+ }
+ return;
+ }
+ if (
+ pliego &&
+ tailwind &&
+ typeof pliego === "object" &&
+ typeof tailwind === "object"
+ ) {
+ const keys = [...new Set([...Object.keys(pliego), ...Object.keys(tailwind)])].sort();
+ for (const key of keys) {
+ geometryDiff(pliego[key], tailwind[key], `${path}.${key}`, differences, geometry);
+ }
+ return;
+ }
+ if (pliego !== tailwind) differences.push({ path, pliego: pliego ?? null, tailwind: tailwind ?? null });
+}
+
+async function renderEngine(browserInstance, engine, prepared, mode, scenario, screenshotPath) {
+ const context = await browserInstance.newContext({
+ viewport: { width: scenario.width, height: scenario.height },
+ deviceScaleFactor: 1,
+ locale: "en-US",
+ colorScheme: "light",
+ reducedMotion: "reduce",
+ forcedColors: "none",
+ });
+ const page = await context.newPage();
+ try {
+ const resetCss = mode.id === "shared-reset" ? prepared.resetCss : "";
+ await page.setContent(
+ browserDocument(prepared.html[engine], prepared.css[engine], resetCss),
+ { waitUntil: "load" },
+ );
+ await page.evaluate(() => document.fonts.ready);
+ if (scenario.action === "hover") await page.locator(scenario.selector).hover();
+ else if (scenario.action === "focus") await page.locator(scenario.selector).focus();
+ else if (scenario.action !== "none") fail(`unknown scenario action ${scenario.action}`);
+ const computed = await capture(page, authority.computedProperties);
+ const screenshot = await page.screenshot({
+ path: screenshotPath,
+ fullPage: true,
+ animations: "disabled",
+ caret: "hide",
+ scale: "css",
+ });
+ return { computed, screenshot };
+ } finally {
+ await context.close();
+ }
+}
+
+const options = parseOptions(process.argv.slice(2));
+const state = gitState();
+if (options.requireClean && state.dirty) fail("--require-clean rejects a dirty Git worktree");
+const generatedAt = new Date();
+const expiresAt = new Date(
+ generatedAt.getTime() + authority.maximumEvidenceAgeHours * 60 * 60 * 1_000,
+);
+const compiler = prepareCompiler();
+const prepared = prepareOutputs(compiler);
+const outputDirectory = dirname(options.output);
+mkdirSync(outputDirectory, { recursive: true });
+const screenshotRoot = resolve(
+ outputDirectory,
+ `${basename(options.output, ".json")}.screenshots`,
+);
+const screenshotChild = relative(outputDirectory, screenshotRoot);
+if (!screenshotChild || screenshotChild.startsWith("..") || isAbsolute(screenshotChild)) {
+ fail("unsafe screenshot output path");
+}
+rmSync(screenshotRoot, { recursive: true, force: true });
+mkdirSync(screenshotRoot, { recursive: true });
+
+const playwrightPackage = JSON.parse(
+ readFileSync(resolve(repositoryRoot, "node_modules", "playwright", "package.json"), "utf8"),
+);
+const browserInstance = await browserTypes[options.browser].launch({ headless: true });
+const observations = [];
+let computedMismatchCount = 0;
+let screenshotMismatchCount = 0;
+let maximumLayoutGeometryDeltaCssPx = 0;
+try {
+ for (const mode of authority.resetModes) {
+ for (const scenario of authority.scenarios) {
+ process.stdout.write(`[browser-output] ${options.host.id}/${mode.id}/${scenario.id}\n`);
+ const scenarioRoot = resolve(screenshotRoot, mode.id, scenario.id);
+ mkdirSync(scenarioRoot, { recursive: true });
+ const paths = {
+ pliego: resolve(scenarioRoot, "pliego.png"),
+ tailwind: resolve(scenarioRoot, "tailwind.png"),
+ diff: resolve(scenarioRoot, "diff.png"),
+ };
+ const pliego = await renderEngine(
+ browserInstance,
+ "pliego",
+ prepared,
+ mode,
+ scenario,
+ paths.pliego,
+ );
+ const tailwind = await renderEngine(
+ browserInstance,
+ "tailwind",
+ prepared,
+ mode,
+ scenario,
+ paths.tailwind,
+ );
+ const computedComparison = computedDiff(pliego.computed, tailwind.computed);
+ const { differences } = computedComparison;
+ const screenshotComparison = compareScreenshots(
+ pliego.screenshot,
+ tailwind.screenshot,
+ paths.diff,
+ );
+ computedMismatchCount += differences.length;
+ maximumLayoutGeometryDeltaCssPx = Math.max(
+ maximumLayoutGeometryDeltaCssPx,
+ computedComparison.maximumLayoutGeometryDeltaCssPx,
+ );
+ if (!screenshotComparison.passed) screenshotMismatchCount += 1;
+ observations.push({
+ resetMode: mode.id,
+ scenario: scenario.id,
+ viewport: { width: scenario.width, height: scenario.height },
+ action: scenario.action,
+ computed: {
+ equal: differences.length === 0,
+ differenceCount: differences.length,
+ differences: differences.slice(0, 100),
+ maximumLayoutGeometryDeltaCssPx:
+ computedComparison.maximumLayoutGeometryDeltaCssPx,
+ pliegoSha256: sha256(Buffer.from(JSON.stringify(pliego.computed))),
+ tailwindSha256: sha256(Buffer.from(JSON.stringify(tailwind.computed))),
+ nodeCount: Object.keys(pliego.computed.nodes).length,
+ },
+ screenshots: {
+ ...screenshotComparison,
+ threshold: authority.comparison.screenshotPixelThreshold,
+ maximumMismatchRatio: authority.comparison.maximumScreenshotMismatchRatio,
+ pliego: {
+ path: relative(outputDirectory, paths.pliego).replaceAll("\\", "/"),
+ sha256: sha256(pliego.screenshot),
+ bytes: pliego.screenshot.byteLength,
+ },
+ tailwind: {
+ path: relative(outputDirectory, paths.tailwind).replaceAll("\\", "/"),
+ sha256: sha256(tailwind.screenshot),
+ bytes: tailwind.screenshot.byteLength,
+ },
+ },
+ });
+ }
+ }
+} finally {
+ await browserInstance.close();
+ const workChild = relative(
+ resolve(repositoryRoot, "target", "browser-output-certification"),
+ prepared.workRoot,
+ );
+ if (
+ !workChild ||
+ workChild === ".." ||
+ workChild.startsWith(`..${sep}`) ||
+ isAbsolute(workChild) ||
+ !basename(prepared.workRoot).startsWith("work-")
+ ) {
+ fail("refusing to clean an unsafe certification work directory");
+ }
+ rmSync(prepared.workRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
+}
+
+const result = computedMismatchCount === 0 && screenshotMismatchCount === 0 ? "passed" : "failed";
+const document = {
+ schemaVersion: 1,
+ kind: "pliegocss-browser-output-host-evidence",
+ result,
+ generatedAtUtc: instant(generatedAt),
+ expiresAtUtc: instant(expiresAt),
+ source: state,
+ host: {
+ id: options.host.id,
+ runner: options.host.runner,
+ os: process.platform,
+ arch: process.arch,
+ node: process.version,
+ playwright: playwrightPackage.version,
+ browser: options.browser,
+ browserVersion: browserInstance.version(),
+ },
+ authority: {
+ path: relative(repositoryRoot, authorityPath).replaceAll("\\", "/"),
+ sha256: sha256(readFileSync(authorityPath)),
+ },
+ compiler: {
+ sha256: compiler.sha256,
+ rustc: compiler.rustc,
+ host: compiler.host,
+ },
+ inputs: prepared.provenance,
+ resetModes: authority.resetModes,
+ computedProperties: authority.computedProperties,
+ observations,
+ coverage: authority.requiredCoverage,
+ summary: {
+ resetModeCount: authority.resetModes.length,
+ scenarioCount: observations.length,
+ computedMismatchCount,
+ screenshotMismatchCount,
+ maximumLayoutGeometryDeltaCssPx: Number(maximumLayoutGeometryDeltaCssPx.toFixed(6)),
+ },
+ claimBoundary: authority.comparison.claimBoundary,
+};
+writeFileSync(options.output, `${JSON.stringify(document, null, 2)}\n`);
+process.stdout.write(
+ `${JSON.stringify(
+ {
+ schemaVersion: 1,
+ result,
+ host: options.host.id,
+ browserVersion: document.host.browserVersion,
+ scenarios: observations.length,
+ computedMismatchCount,
+ screenshotMismatchCount,
+ maximumLayoutGeometryDeltaCssPx: document.summary.maximumLayoutGeometryDeltaCssPx,
+ output: relative(repositoryRoot, options.output).replaceAll("\\", "/"),
+ },
+ null,
+ 2,
+ )}\n`,
+);
+if (result !== "passed") process.exitCode = 1;
diff --git a/scripts/check-benchmark-authority-v2.mjs b/scripts/check-benchmark-authority-v2.mjs
new file mode 100644
index 0000000..cf569fd
--- /dev/null
+++ b/scripts/check-benchmark-authority-v2.mjs
@@ -0,0 +1,167 @@
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import {
+ authorityPath,
+ canonicalLaneMap,
+ corpusMetrics,
+ loadAuthority,
+ materializeCorpus,
+ readInstalledPackage,
+ repositoryRoot,
+ resolveLaneCli,
+ sha256,
+} from "./benchmark-authority-v2.mjs";
+
+const network = process.argv.slice(2).includes("--network");
+if (process.argv.length > (network ? 3 : 2)) {
+ throw new Error("usage: node scripts/check-benchmark-authority-v2.mjs [--network]");
+}
+
+function fail(message) {
+ throw new Error(`Benchmark Authority v2: ${message}`);
+}
+
+function equal(actual, expected, label) {
+ if (actual !== expected) fail(`${label}: expected ${JSON.stringify(expected)}, found ${JSON.stringify(actual)}`);
+}
+
+function nonEmpty(value, label) {
+ if (typeof value !== "string" || value.trim() === "") fail(`${label} must be non-empty`);
+}
+
+const authorityBytes = readFileSync(authorityPath);
+const authority = loadAuthority();
+equal(authority.schemaVersion, 2, "schemaVersion");
+equal(authority.status, "active", "status");
+equal(authority.registry, "https://registry.npmjs.org", "registry");
+equal(authority.primaryLane, "tailwind-latest", "primaryLane");
+equal(authority.resetContract, "no-preflight", "resetContract");
+equal(authority.maximumAgeHours, 168, "maximumAgeHours");
+
+const observedAt = Date.parse(authority.observedAtUtc);
+const expiresAt = Date.parse(authority.expiresAtUtc);
+if (!Number.isFinite(observedAt) || !Number.isFinite(expiresAt)) fail("oracle dates must be ISO timestamps");
+equal(expiresAt - observedAt, authority.maximumAgeHours * 60 * 60 * 1_000, "oracle validity window");
+const now = Date.now();
+if (observedAt > now + 5 * 60 * 1_000) fail("oracle observation is in the future");
+if (now > expiresAt) fail(`oracle expired at ${authority.expiresAtUtc}; refresh it before comparison`);
+
+const expectedLaneIds = ["tailwind-latest", "tailwind-v3-lts", "tailwind-frozen-release"];
+if (!Array.isArray(authority.lanes)) fail("lanes must be an array");
+equal(JSON.stringify(authority.lanes.map((lane) => lane.id)), JSON.stringify(expectedLaneIds), "lane order");
+const laneMap = canonicalLaneMap(authority);
+equal(laneMap.get("tailwind-latest")?.distTag, "latest", "latest dist-tag");
+equal(laneMap.get("tailwind-v3-lts")?.distTag, "v3-lts", "v3 LTS dist-tag");
+equal(laneMap.get("tailwind-frozen-release")?.distTag, null, "frozen lane dist-tag");
+if (laneMap.get("tailwind-frozen-release")?.version === laneMap.get("tailwind-latest")?.version) {
+ fail("frozen release must remain distinct from the current competitor lane");
+}
+
+const packageJson = JSON.parse(readFileSync(resolve(repositoryRoot, "package.json"), "utf8"));
+const lockText = readFileSync(resolve(repositoryRoot, "pnpm-lock.yaml"), "utf8");
+for (const lane of authority.lanes) {
+ for (const field of ["role", "packageAlias", "packageName", "version", "integrity", "cliPackageAlias", "cliPackageName", "cliVersion", "cliIntegrity"]) {
+ nonEmpty(lane[field], `${lane.id}.${field}`);
+ }
+ const packageSpec = packageJson.devDependencies?.[lane.packageAlias];
+ const expectedPackageSpec =
+ lane.packageAlias === lane.packageName ? lane.version : `npm:${lane.packageName}@${lane.version}`;
+ equal(packageSpec, expectedPackageSpec, `${lane.id} package pin`);
+ const cliSpec = packageJson.devDependencies?.[lane.cliPackageAlias];
+ const expectedCliSpec =
+ lane.cliPackageAlias === lane.cliPackageName
+ ? lane.cliVersion
+ : `npm:${lane.cliPackageName}@${lane.cliVersion}`;
+ equal(cliSpec, expectedCliSpec, `${lane.id} CLI pin`);
+
+ const installedPackage = readInstalledPackage(lane.packageAlias).value;
+ equal(installedPackage.name, lane.packageName, `${lane.id} installed package name`);
+ equal(installedPackage.version, lane.version, `${lane.id} installed package version`);
+ const installedCli = readInstalledPackage(lane.cliPackageAlias).value;
+ equal(installedCli.name, lane.cliPackageName, `${lane.id} installed CLI name`);
+ equal(installedCli.version, lane.cliVersion, `${lane.id} installed CLI version`);
+ resolveLaneCli(lane);
+ if (!lockText.includes(lane.integrity) || !lockText.includes(lane.cliIntegrity)) {
+ fail(`${lane.id} registry integrity is not bound by pnpm-lock.yaml`);
+ }
+}
+
+const expectedCorpora = ["micro", "medium", "large"];
+if (!Array.isArray(authority.corpora)) fail("corpora must be an array");
+equal(JSON.stringify(authority.corpora.map((corpus) => corpus.id)), JSON.stringify(expectedCorpora), "corpus order");
+const corpusResults = [];
+for (const corpus of authority.corpora) {
+ if (!Number.isSafeInteger(corpus.repeat) || corpus.repeat < 1) fail(`${corpus.id}.repeat is invalid`);
+ nonEmpty(corpus.source, `${corpus.id}.source`);
+ nonEmpty(corpus.scope, `${corpus.id}.scope`);
+ const html = materializeCorpus(corpus);
+ const metrics = corpusMetrics(html);
+ for (const field of ["sha256", "htmlBytes", "classAttributeCount", "utilityOccurrences", "uniqueUtilityTokens", "uniqueStyleInputs"]) {
+ equal(corpus.expected?.[field], metrics[field], `${corpus.id}.expected.${field}`);
+ }
+ corpusResults.push({ id: corpus.id, ...metrics });
+}
+if (!(corpusResults[0].htmlBytes < corpusResults[1].htmlBytes && corpusResults[1].htmlBytes < corpusResults[2].htmlBytes)) {
+ fail("corpus byte sizes must increase micro < medium < large");
+}
+
+const methodology = authority.methodology;
+equal(methodology.design, "paired-interleaved-alternating-order", "methodology.design");
+equal(methodology.warmupPairs, 5, "methodology.warmupPairs");
+equal(methodology.measuredPairs, 30, "methodology.measuredPairs");
+equal(methodology.freshProcess, true, "methodology.freshProcess");
+equal(methodology.freshOutputVerification, true, "methodology.freshOutputVerification");
+equal(methodology.completeSamples, true, "methodology.completeSamples");
+equal(methodology.compression?.gzipLevel, 9, "methodology gzip");
+equal(methodology.compression?.brotliQuality, 11, "methodology Brotli");
+nonEmpty(methodology.memoryMetric, "methodology.memoryMetric");
+nonEmpty(methodology.scorePolicy, "methodology.scorePolicy");
+
+const requiredFiles = [
+ "scripts/benchmark-process-metrics.rs",
+ "scripts/measure-benchmark-authority-v2.mjs",
+ "scripts/render-benchmark-authority-v2.mjs",
+ authority.artifacts.generatedDocumentation,
+];
+for (const path of requiredFiles) {
+ readFileSync(resolve(repositoryRoot, path));
+}
+const legacyGate = readFileSync(resolve(repositoryRoot, "scripts/measure-pliego-gate-b.mjs"), "utf8");
+if (legacyGate.includes("frozenTailwind") || legacyGate.includes("frozen_tailwind_baseline")) {
+ fail("Gate B still owns a handwritten Tailwind baseline");
+}
+
+const networkResults = [];
+if (network) {
+ const responses = new Map();
+ for (const packageName of new Set(authority.lanes.flatMap((lane) => [lane.packageName, lane.cliPackageName]))) {
+ const response = await fetch(`${authority.registry}/${packageName.replaceAll("/", "%2f")}`);
+ if (!response.ok) fail(`registry request for ${packageName} returned HTTP ${response.status}`);
+ responses.set(packageName, await response.json());
+ }
+ for (const lane of authority.lanes) {
+ const packageMetadata = responses.get(lane.packageName);
+ if (lane.distTag) equal(packageMetadata["dist-tags"]?.[lane.distTag], lane.version, `${lane.id} live dist-tag`);
+ equal(packageMetadata.versions?.[lane.version]?.dist?.integrity, lane.integrity, `${lane.id} live package integrity`);
+ const cliMetadata = responses.get(lane.cliPackageName);
+ equal(cliMetadata.versions?.[lane.cliVersion]?.dist?.integrity, lane.cliIntegrity, `${lane.id} live CLI integrity`);
+ networkResults.push({ id: lane.id, registryVerified: true });
+ }
+}
+
+process.stdout.write(
+ `${JSON.stringify(
+ {
+ schemaVersion: 2,
+ status: "passed",
+ authoritySha256: sha256(authorityBytes),
+ observedAtUtc: authority.observedAtUtc,
+ expiresAtUtc: authority.expiresAtUtc,
+ lanes: authority.lanes.map((lane) => ({ id: lane.id, version: lane.version })),
+ corpora: corpusResults,
+ network: network ? networkResults : "not-requested",
+ },
+ null,
+ 2,
+ )}\n`,
+);
diff --git a/scripts/check-benchmark-result-v2.mjs b/scripts/check-benchmark-result-v2.mjs
new file mode 100644
index 0000000..7bcd62c
--- /dev/null
+++ b/scripts/check-benchmark-result-v2.mjs
@@ -0,0 +1,243 @@
+import { spawnSync } from "node:child_process";
+import { readFileSync } from "node:fs";
+import { isAbsolute, join, relative, resolve, sep } from "node:path";
+import {
+ authorityPath,
+ loadAuthority,
+ readInstalledPackage,
+ repositoryRoot,
+ round,
+ sha256,
+ summarize,
+} from "./benchmark-authority-v2.mjs";
+
+const arguments_ = process.argv.slice(2);
+let canonical = false;
+let resultPath = resolve(
+ repositoryRoot,
+ "benchmarks",
+ "results",
+ "benchmark-authority-v2.local.json",
+);
+for (const argument of arguments_) {
+ if (argument === "--") continue;
+ else if (argument === "--canonical") canonical = true;
+ else if (argument.startsWith("--result=")) resultPath = resolve(repositoryRoot, argument.slice(9));
+ else throw new Error("usage: node scripts/check-benchmark-result-v2.mjs [--canonical] [--result=]");
+}
+
+function fail(message) {
+ throw new Error(`Benchmark result schema 2: ${message}`);
+}
+function equal(actual, expected, label) {
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
+ fail(`${label} drifted`);
+ }
+}
+function positive(value, label, allowNegative = false) {
+ if (!Number.isFinite(value) || (!allowNegative && value <= 0)) fail(`${label} is invalid`);
+}
+function checkSummary(values, document, unit, label) {
+ equal(document, summarize(values, unit), label);
+}
+function checkCurrentFile(document, expectedPath, label) {
+ const repositoryPath = relative(repositoryRoot, expectedPath).replaceAll("\\", "/");
+ if (document?.path !== repositoryPath) fail(`${label} path drifted`);
+ if (document?.sha256 !== sha256(readFileSync(expectedPath))) fail(`${label} hash drifted`);
+}
+function git(arguments_) {
+ const result = spawnSync("git", arguments_, {
+ cwd: repositoryRoot,
+ encoding: "utf8",
+ windowsHide: true,
+ });
+ if (result.error) throw result.error;
+ if (result.status !== 0) fail(`git ${arguments_.join(" ")} failed`);
+ return result.stdout;
+}
+function currentGitState() {
+ const status = git(["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
+ return {
+ commit: git(["rev-parse", "HEAD"]).trim(),
+ dirty: status.length > 0,
+ statusEntryCount: status.split("\0").filter(Boolean).length,
+ statusSha256: sha256(Buffer.from(status, "utf8")),
+ };
+}
+
+const resultChild = relative(repositoryRoot, resultPath);
+if (!resultChild || resultChild === ".." || resultChild.startsWith(`..${sep}`) || isAbsolute(resultChild)) {
+ fail("result path escapes the repository");
+}
+const authority = loadAuthority();
+const document = JSON.parse(readFileSync(resultPath, "utf8"));
+if (document.schemaVersion !== 2) fail("schemaVersion must be 2");
+if (document.benchmark !== "PliegoCSS Benchmark Authority v2 paired competitor oracle") {
+ fail("benchmark identity drifted");
+}
+if (document.authority?.sha256 !== sha256(readFileSync(authorityPath))) fail("authority hash drifted");
+if (document.authority?.path !== "benchmarks/benchmark-authority-v2/oracle.json") {
+ fail("authority path drifted");
+}
+if (document.authority?.observedAtUtc !== authority.observedAtUtc) fail("authority observation drifted");
+if (document.authority?.expiresAtUtc !== authority.expiresAtUtc) fail("authority expiration drifted");
+if (document.authority?.primaryLane !== authority.primaryLane) fail("primary lane drifted");
+if (document.authority?.resetContract !== "no-preflight") fail("reset contract drifted");
+const generatedAt = Date.parse(document.generatedAtUtc);
+if (!Number.isFinite(generatedAt)) fail("generatedAtUtc is invalid");
+if (generatedAt < Date.parse(authority.observedAtUtc) || generatedAt > Date.parse(authority.expiresAtUtc)) {
+ fail("result was not generated inside the oracle validity window");
+}
+if (document.methodology?.design !== authority.methodology.design) fail("pair design drifted");
+if (canonical) {
+ if (document.mode === "smoke") fail("canonical result cannot be smoke evidence");
+ if (document.methodology.warmupPairs !== authority.methodology.warmupPairs) fail("warmup count drifted");
+ if (document.methodology.measuredPairs !== authority.methodology.measuredPairs) fail("pair count drifted");
+}
+if (
+ !document.methodology?.freshProcess ||
+ !document.methodology?.completeSamples ||
+ !document.methodology?.freshOutputVerification
+) {
+ fail("fresh processes, complete samples, and fresh verification are required");
+}
+if (document.methodology.pairOrder !== authority.methodology.pairOrder) fail("pair order drifted");
+if (document.methodology.memoryMetric !== authority.methodology.memoryMetric) fail("memory metric drifted");
+equal(document.methodology.compression, authority.methodology.compression, "compression contract");
+if (document.claimBoundary !== authority.methodology.scorePolicy) fail("claim boundary drifted");
+
+const harnessPath = resolve(repositoryRoot, "scripts", "measure-benchmark-authority-v2.mjs");
+const runnerPath = resolve(repositoryRoot, "scripts", "benchmark-process-metrics.rs");
+checkCurrentFile(document.provenance?.harness, harnessPath, "harness provenance");
+checkCurrentFile(document.provenance?.processMetricsRunner, runnerPath, "process runner provenance");
+if (document.provenance?.packageJsonSha256 !== sha256(readFileSync(resolve(repositoryRoot, "package.json")))) {
+ fail("package.json provenance drifted");
+}
+if (document.provenance?.pnpmLockSha256 !== sha256(readFileSync(resolve(repositoryRoot, "pnpm-lock.yaml")))) {
+ fail("pnpm lock provenance drifted");
+}
+equal(document.provenance?.git, currentGitState(), "Git source state");
+if (document.mode === "immutable-evidence" && document.provenance.git.dirty) {
+ fail("immutable evidence was captured from a dirty source tree");
+}
+const executablePath = join(
+ repositoryRoot,
+ document.environment?.cargoTarget ?? "",
+ document.environment?.toolchain?.host ?? "",
+ "release",
+ process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc",
+);
+if (document.provenance?.executableSha256 !== sha256(readFileSync(executablePath))) {
+ fail("measured PliegoCSS executable provenance drifted");
+}
+if (document.mode === "immutable-evidence") {
+ const securityStatus = document.environment?.securityTooling?.status;
+ if (!new Set(["active", "disabled", "not-installed", "unknown"]).has(securityStatus)) {
+ fail("immutable evidence has no explicit security-tooling state");
+ }
+}
+
+const expectedLanes = canonical
+ ? authority.lanes.map((lane) => lane.id)
+ : Object.keys(document.lanes ?? {});
+if (expectedLanes.length === 0) fail("no measured lanes");
+if (canonical) equal(Object.keys(document.lanes), expectedLanes, "lane set");
+const expectedCorpora = authority.corpora.map((corpus) => corpus.id);
+let comparisons = 0;
+for (const laneId of expectedLanes) {
+ const laneAuthority = authority.lanes.find((lane) => lane.id === laneId);
+ const lane = document.lanes[laneId];
+ if (!laneAuthority || !lane) fail(`unknown or missing lane ${laneId}`);
+ if (lane.version !== laneAuthority.version || lane.cliVersion !== laneAuthority.cliVersion) {
+ fail(`${laneId} version drifted`);
+ }
+ if (
+ lane.packageManifestSha256 !==
+ sha256(readFileSync(readInstalledPackage(laneAuthority.packageAlias).path)) ||
+ lane.cliManifestSha256 !==
+ sha256(readFileSync(readInstalledPackage(laneAuthority.cliPackageAlias).path))
+ ) {
+ fail(`${laneId} installed package provenance drifted`);
+ }
+ if (canonical) equal(Object.keys(lane.corpora), expectedCorpora, `${laneId} corpus set`);
+ for (const [corpusId, comparison] of Object.entries(lane.corpora)) {
+ const corpusAuthority = authority.corpora.find((corpus) => corpus.id === corpusId);
+ if (!corpusAuthority) fail(`${laneId} has unknown corpus ${corpusId}`);
+ equal(comparison.fixture, corpusAuthority.expected, `${laneId}/${corpusId} fixture`);
+ const pairs = comparison.pairs;
+ if (!Array.isArray(pairs) || pairs.length !== document.methodology.measuredPairs) {
+ fail(`${laneId}/${corpusId} pair count drifted`);
+ }
+ const pliegoLatency = [];
+ const tailwindLatency = [];
+ const deltas = [];
+ const ratios = [];
+ const pliegoMemory = [];
+ const tailwindMemory = [];
+ const memoryDeltas = [];
+ for (const [index, pair] of pairs.entries()) {
+ const expectedOrder = index % 2 === 0 ? ["pliego", "tailwind"] : ["tailwind", "pliego"];
+ if (pair.pair !== index + 1) fail(`${laneId}/${corpusId} pair index drifted`);
+ equal(pair.order, expectedOrder, `${laneId}/${corpusId} pair order`);
+ for (const kind of ["pliego", "tailwind"]) {
+ positive(pair[kind]?.elapsedNs, `${laneId}/${corpusId} ${kind} elapsed`);
+ positive(pair[kind]?.peakWorkingSetBytes, `${laneId}/${corpusId} ${kind} memory`);
+ if (pair[kind].exitCode !== 0) fail(`${laneId}/${corpusId} ${kind} failed`);
+ }
+ if (pair.deltaNs !== pair.pliego.elapsedNs - pair.tailwind.elapsedNs) fail("paired latency delta drifted");
+ if (pair.tailwindToPliegoRatio !== round(pair.tailwind.elapsedNs / pair.pliego.elapsedNs)) {
+ fail("paired latency ratio drifted");
+ }
+ if (pair.peakWorkingSetDeltaBytes !== pair.pliego.peakWorkingSetBytes - pair.tailwind.peakWorkingSetBytes) {
+ fail("paired memory delta drifted");
+ }
+ pliegoLatency.push(pair.pliego.elapsedNs / 1_000_000);
+ tailwindLatency.push(pair.tailwind.elapsedNs / 1_000_000);
+ deltas.push(pair.deltaNs / 1_000_000);
+ ratios.push(pair.tailwindToPliegoRatio);
+ pliegoMemory.push(pair.pliego.peakWorkingSetBytes);
+ tailwindMemory.push(pair.tailwind.peakWorkingSetBytes);
+ memoryDeltas.push(pair.peakWorkingSetDeltaBytes);
+ }
+ const summaries = comparison.summaries;
+ checkSummary(pliegoLatency, summaries.pliegoLatency, "ms", "Pliego latency summary");
+ checkSummary(tailwindLatency, summaries.tailwindLatency, "ms", "Tailwind latency summary");
+ checkSummary(deltas, summaries.pairedLatencyDelta, "ms", "paired latency summary");
+ checkSummary(ratios, summaries.tailwindToPliegoRatio, "ratio", "paired ratio summary");
+ checkSummary(pliegoMemory, summaries.pliegoPeakWorkingSet, "bytes", "Pliego memory summary");
+ checkSummary(tailwindMemory, summaries.tailwindPeakWorkingSet, "bytes", "Tailwind memory summary");
+ checkSummary(memoryDeltas, summaries.pairedPeakWorkingSetDelta, "bytes", "paired memory summary");
+ if (!comparison.determinism?.freshOutputVerified || comparison.determinism.measuredPairs !== pairs.length) {
+ fail(`${laneId}/${corpusId} determinism contract drifted`);
+ }
+ if (comparison.coverage?.ratio !== 1 || comparison.coverage.missing?.length !== 0) {
+ fail(`${laneId}/${corpusId} candidate coverage is incomplete`);
+ }
+ for (const kind of ["pliego", "tailwind"]) {
+ const output = comparison.output[kind];
+ for (const part of ["css", "html"]) {
+ for (const field of ["rawBytes", "gzipBytes", "brotliBytes"]) {
+ positive(output[part][field], `${laneId}/${corpusId} ${kind} ${part} ${field}`);
+ }
+ if (!/^[0-9a-f]{64}$/u.test(output[part].sha256)) fail("output SHA-256 is invalid");
+ }
+ if (comparison.determinism[`${kind}Sha256`] !== output.css.sha256) {
+ fail(`${laneId}/${corpusId} ${kind} deterministic output hash drifted`);
+ }
+ if (output.transferGzipBytes !== output.css.gzipBytes + output.html.gzipBytes) {
+ fail("gzip transfer sum drifted");
+ }
+ if (output.transferBrotliBytes !== output.css.brotliBytes + output.html.brotliBytes) {
+ fail("Brotli transfer sum drifted");
+ }
+ }
+ comparisons += 1;
+ }
+}
+if (canonical && comparisons !== authority.lanes.length * authority.corpora.length) {
+ fail("canonical result is incomplete");
+}
+
+process.stdout.write(
+ `${JSON.stringify({ schemaVersion: 2, status: "passed", mode: document.mode, lanes: expectedLanes.length, comparisons, pairsPerComparison: document.methodology.measuredPairs }, null, 2)}\n`,
+);
diff --git a/scripts/check-breakpoint-order-browser.mjs b/scripts/check-breakpoint-order-browser.mjs
new file mode 100644
index 0000000..12ec387
--- /dev/null
+++ b/scripts/check-breakpoint-order-browser.mjs
@@ -0,0 +1,186 @@
+import { spawn, spawnSync } from "node:child_process";
+import { createServer } from "node:http";
+import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join, resolve } from "node:path";
+import { setTimeout as delay } from "node:timers/promises";
+import { cargoTargetRoot, isolatedCargoEnvironment } from "./rust-target.mjs";
+
+const browser = process.argv[2];
+if (!browser) throw new Error("browser executable required");
+const root = resolve(import.meta.dirname, "..");
+const cargoEnvironment = isolatedCargoEnvironment(root);
+const cargoTarget = cargoTargetRoot(root, cargoEnvironment);
+const binary = join(
+ cargoTarget,
+ "debug",
+ process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc",
+);
+const runtime = mkdtempSync(join(tmpdir(), "pliegocss-breakpoint-order-"));
+const profile = mkdtempSync(join(tmpdir(), "pliegocss-breakpoint-cdp-"));
+const config = join(runtime, "pliego.theme.toml");
+const cssPath = join(runtime, "app.css");
+const manifestPath = join(runtime, "app.manifest.json");
+writeFileSync(
+ config,
+ 'schema = 1\nextends = "seed"\n\n[breakpoints]\ncontent-wide = "72rem"\ntablet = "52rem"\n',
+);
+const build = spawnSync("cargo", ["build", "--quiet", "--locked", "-p", "pliego-cssc"], {
+ cwd: root,
+ env: cargoEnvironment,
+ encoding: "utf8",
+ windowsHide: true,
+});
+if (build.error) throw build.error;
+if (build.status !== 0) throw new Error(`compiler build failed:\n${build.stdout}${build.stderr}`);
+const compile = spawnSync(
+ binary,
+ [
+ "compile",
+ "--style",
+ "grid grid-cols-1 tablet:grid-cols-2 content-wide:grid-cols-3",
+ "--config",
+ config,
+ "--output",
+ cssPath,
+ "--manifest",
+ manifestPath,
+ ],
+ { cwd: root, encoding: "utf8", windowsHide: true },
+);
+if (compile.error) throw compile.error;
+if (compile.status !== 0) throw new Error(`compiler invocation failed:\n${compile.stdout}${compile.stderr}`);
+const css = readFileSync(cssPath, "utf8");
+const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
+const className = manifest.styles?.[0]?.className;
+if (!/^pc_[a-z0-9]+$/u.test(className ?? "")) throw new Error("compiled class name missing");
+const narrowIndex = css.indexOf("(width>=52rem)");
+const wideIndex = css.indexOf("(width>=72rem)");
+if (narrowIndex < 0 || wideIndex <= narrowIndex) {
+ throw new Error(`compiled media order is not 52rem before 72rem:\n${css}`);
+}
+
+const html = ``;
+const server = createServer((request, response) => {
+ if (request.url === "/app.css") {
+ response.writeHead(200, { "content-type": "text/css" });
+ response.end(css);
+ return;
+ }
+ response.writeHead(200, { "content-type": "text/html" });
+ response.end(html);
+});
+await new Promise((ready) => server.listen(0, "127.0.0.1", ready));
+const port = server.address().port;
+const debugPort = 10_200 + Math.floor(Math.random() * 300);
+const child = spawn(
+ browser,
+ [
+ "--headless=new",
+ `--remote-debugging-port=${debugPort}`,
+ `--user-data-dir=${profile}`,
+ "--no-first-run",
+ "--disable-default-apps",
+ `http://127.0.0.1:${port}/`,
+ ],
+ { stdio: "ignore" },
+);
+
+try {
+ let target;
+ let version;
+ for (let attempt = 0; attempt < 60; attempt++) {
+ try {
+ version = await (await fetch(`http://127.0.0.1:${debugPort}/json/version`)).json();
+ const targets = await (await fetch(`http://127.0.0.1:${debugPort}/json/list`)).json();
+ target = targets.find((entry) => entry.type === "page" && entry.url.includes(`:${port}`));
+ if (target) break;
+ } catch {}
+ await delay(100);
+ }
+ if (!target) throw new Error("CDP target unavailable");
+ const socket = new WebSocket(target.webSocketDebuggerUrl);
+ await new Promise((open, reject) => {
+ socket.addEventListener("open", open, { once: true });
+ socket.addEventListener("error", reject, { once: true });
+ });
+ let id = 0;
+ const call = (method, params = {}) =>
+ new Promise((resolveCall, reject) => {
+ const current = ++id;
+ const listener = (event) => {
+ const message = JSON.parse(event.data);
+ if (message.id !== current) return;
+ socket.removeEventListener("message", listener);
+ message.error ? reject(new Error(message.error.message)) : resolveCall(message.result);
+ };
+ socket.addEventListener("message", listener);
+ socket.send(JSON.stringify({ id: current, method, params }));
+ });
+ await call("Page.enable");
+ const observations = [];
+ for (const [width, expectedColumns] of [
+ [800, 1],
+ [900, 2],
+ [1200, 3],
+ ]) {
+ await call("Emulation.setDeviceMetricsOverride", {
+ width,
+ height: 800,
+ deviceScaleFactor: 1,
+ mobile: false,
+ });
+ await delay(100);
+ const evaluation = await call("Runtime.evaluate", {
+ returnByValue: true,
+ expression:
+ "(() => getComputedStyle(document.querySelector('#grid')).gridTemplateColumns.split(' ').filter(Boolean).length)()",
+ });
+ const actualColumns = evaluation.result.value;
+ observations.push({ width, columns: actualColumns });
+ if (actualColumns !== expectedColumns) {
+ throw new Error(
+ `breakpoint regression at ${width}px: expected ${expectedColumns}, found ${actualColumns}`,
+ );
+ }
+ }
+ socket.close();
+ process.stdout.write(
+ `${JSON.stringify(
+ {
+ schemaVersion: 1,
+ browser: version.Browser,
+ breakpoints: ["52rem", "72rem"],
+ compiledOrder: "narrow-to-wide",
+ observations,
+ },
+ null,
+ 2,
+ )}\n`,
+ );
+} finally {
+ if (process.platform === "win32") {
+ spawnSync("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
+ stdio: "ignore",
+ windowsHide: true,
+ });
+ } else {
+ child.kill();
+ }
+ server.close();
+ await delay(300);
+ for (const directory of [profile, runtime]) {
+ for (let attempt = 0; attempt < 20; attempt++) {
+ try {
+ rmSync(directory, { recursive: true, force: true, maxRetries: 2, retryDelay: 100 });
+ break;
+ } catch (error) {
+ if (attempt === 19) {
+ process.stderr.write(`warning: deferred browser temp cleanup for ${directory}: ${error.message}\n`);
+ break;
+ }
+ await delay(100);
+ }
+ }
+ }
+}
diff --git a/scripts/check-browser-output-authority.mjs b/scripts/check-browser-output-authority.mjs
new file mode 100644
index 0000000..50da535
--- /dev/null
+++ b/scripts/check-browser-output-authority.mjs
@@ -0,0 +1,188 @@
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { repositoryRoot, sha256 } from "./benchmark-authority-v2.mjs";
+
+const authorityPath = resolve(
+ repositoryRoot,
+ "benchmarks",
+ "browser-output-certification-v1",
+ "authority.json",
+);
+const authority = JSON.parse(readFileSync(authorityPath, "utf8"));
+
+function fail(message) {
+ throw new Error(`Browser/output authority: ${message}`);
+}
+
+function equal(actual, expected, label) {
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) fail(`${label} drifted`);
+}
+
+function nonEmpty(value, label) {
+ if (typeof value !== "string" || value.trim() === "") fail(`${label} must be non-empty`);
+}
+
+if (authority.schemaVersion !== 1 || authority.status !== "active-contract") {
+ fail("header drifted");
+}
+if (authority.kind !== "pliegocss-browser-output-certification") fail("kind drifted");
+if (authority.competitorAuthority !== "benchmarks/benchmark-authority-v2/oracle.json") {
+ fail("competitor authority path drifted");
+}
+if (authority.competitorLane !== "tailwind-latest") fail("competitor lane drifted");
+if (authority.maximumEvidenceAgeHours !== 168) fail("evidence validity drifted");
+
+for (const input of [authority.fixture, authority.reset]) {
+ if (!/^[0-9a-f]{64}$/u.test(input.sha256)) fail(`${input.path} hash is invalid`);
+ const bytes = readFileSync(resolve(repositoryRoot, input.path));
+ if (sha256(bytes) !== input.sha256) fail(`${input.path} hash drifted`);
+}
+if (!readFileSync(resolve(repositoryRoot, authority.reset.path), "utf8").startsWith("@layer reset")) {
+ fail("shared reset must be isolated in the lowest cascade layer");
+}
+
+equal(authority.resetModes.map((mode) => mode.id), ["no-reset", "shared-reset"], "reset modes");
+authority.resetModes.forEach((mode) => nonEmpty(mode.contract, `${mode.id}.contract`));
+equal(authority.browsers, ["chromium", "firefox", "webkit"], "browser engines");
+
+const expectedHosts = [
+ ["windows", "windows-latest", "win32", "x64"],
+ ["linux", "ubuntu-24.04", "linux", "x64"],
+ ["macos", "macos-15", "darwin", "arm64"],
+].flatMap(([prefix, runner, os, arch]) =>
+ authority.browsers.map((browser) => ({
+ id: `${prefix}-${arch === "x64" ? "x64" : "arm64"}-${browser}`,
+ runner,
+ os,
+ arch,
+ browser,
+ })),
+);
+equal(authority.hosts, expectedHosts, "host matrix");
+
+if (
+ !Array.isArray(authority.computedProperties) ||
+ authority.computedProperties.length < 40 ||
+ new Set(authority.computedProperties).size !== authority.computedProperties.length
+) {
+ fail("computed property set is incomplete or contains duplicates");
+}
+for (const required of [
+ "display",
+ "width",
+ "padding-top",
+ "grid-template-columns",
+ "color",
+ "background-color",
+ "border-top-width",
+ "outline-width",
+ "font-size",
+ "opacity",
+]) {
+ if (!authority.computedProperties.includes(required)) fail(`computed property ${required} is missing`);
+}
+
+equal(
+ authority.scenarios.map(({ id, width, height, action }) => ({ id, width, height, action })),
+ [
+ { id: "base-mobile", width: 375, height: 812, action: "none" },
+ { id: "base-tablet", width: 768, height: 1024, action: "none" },
+ { id: "base-desktop", width: 1440, height: 900, action: "none" },
+ { id: "hover-primary", width: 1440, height: 900, action: "hover" },
+ { id: "focus-input", width: 1440, height: 900, action: "focus" },
+ ],
+ "scenario contract",
+);
+for (const scenario of authority.scenarios.filter((entry) => entry.action !== "none")) {
+ nonEmpty(scenario.selector, `${scenario.id}.selector`);
+}
+
+equal(
+ authority.requiredCoverage,
+ [
+ "computed-style",
+ "layout-geometry",
+ "responsive",
+ "interactive-state",
+ "reset-explicit",
+ "screenshot-bounded",
+ "tailwind-latest",
+ ],
+ "coverage contract",
+);
+if (authority.comparison.computedLengthQuantumCssPx !== 0.25) fail("length quantum drifted");
+if (
+ authority.comparison.layoutGeometryResolutionCssPx !== 0.015625 ||
+ authority.comparison.maximumLayoutGeometryDeltaCssPx !== 0.1
+) {
+ fail("layout geometry precision or tolerance drifted");
+}
+if (authority.comparison.screenshotPixelThreshold !== 0.2) fail("pixel threshold drifted");
+if (authority.comparison.maximumScreenshotMismatchRatio !== 0.005) {
+ fail("screenshot mismatch budget drifted");
+}
+for (const field of ["computedStyle", "layoutGeometry", "screenshots", "claimBoundary"]) {
+ nonEmpty(authority.comparison[field], `comparison.${field}`);
+}
+
+const packageJson = JSON.parse(readFileSync(resolve(repositoryRoot, "package.json"), "utf8"));
+const expectedPackages = {
+ playwright: "1.61.1",
+ pixelmatch: "7.2.0",
+ pngjs: "7.0.0",
+};
+for (const [name, version] of Object.entries(expectedPackages)) {
+ if (packageJson.devDependencies?.[name] !== version) fail(`${name} must be pinned to ${version}`);
+ const installed = JSON.parse(
+ readFileSync(resolve(repositoryRoot, "node_modules", name, "package.json"), "utf8"),
+ );
+ if (installed.version !== version) fail(`installed ${name} version drifted`);
+}
+
+const runnerSource = readFileSync(
+ resolve(repositoryRoot, "scripts", "browser-output-certification.mjs"),
+ "utf8",
+);
+for (const marker of [
+ "getComputedStyle",
+ "getBoundingClientRect",
+ "page.screenshot",
+ "pixelmatch",
+ "--require-clean",
+ "authority.competitorLane",
+]) {
+ if (!runnerSource.includes(marker)) fail(`runner is missing ${marker}`);
+}
+const workflow = readFileSync(
+ resolve(repositoryRoot, ".github", "workflows", "browser-output-certification.yml"),
+ "utf8",
+);
+for (const host of authority.hosts) {
+ if (!workflow.includes(`host: ${host.id}`)) fail(`workflow is missing ${host.id}`);
+}
+for (const marker of [
+ "playwright install",
+ "--require-clean",
+ "check:browser-output-matrix",
+ "actions/upload-artifact@",
+ "actions/download-artifact@",
+]) {
+ if (!workflow.includes(marker)) fail(`workflow is missing ${marker}`);
+}
+
+process.stdout.write(
+ `${JSON.stringify(
+ {
+ schemaVersion: 1,
+ status: "passed",
+ authoritySha256: sha256(readFileSync(authorityPath)),
+ hosts: authority.hosts.length,
+ browsers: authority.browsers,
+ resetModes: authority.resetModes.map((mode) => mode.id),
+ scenarios: authority.scenarios.length,
+ computedProperties: authority.computedProperties.length,
+ },
+ null,
+ 2,
+ )}\n`,
+);
diff --git a/scripts/check-browser-output-matrix.mjs b/scripts/check-browser-output-matrix.mjs
new file mode 100644
index 0000000..2dafccc
--- /dev/null
+++ b/scripts/check-browser-output-matrix.mjs
@@ -0,0 +1,341 @@
+import { spawnSync } from "node:child_process";
+import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
+import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
+import { repositoryRoot, sha256 } from "./benchmark-authority-v2.mjs";
+
+const authorityPath = resolve(
+ repositoryRoot,
+ "benchmarks",
+ "browser-output-certification-v1",
+ "authority.json",
+);
+const authority = JSON.parse(readFileSync(authorityPath, "utf8"));
+const competitorPath = resolve(repositoryRoot, authority.competitorAuthority);
+const competitorBytes = readFileSync(competitorPath);
+const competitor = JSON.parse(competitorBytes);
+const lane = competitor.lanes.find((candidate) => candidate.id === authority.competitorLane);
+if (!lane) throw new Error(`Browser/output matrix: missing ${authority.competitorLane}`);
+if (Date.now() > Date.parse(competitor.expiresAtUtc)) {
+ throw new Error("Browser/output matrix: competitor authority expired");
+}
+const tailwindManifestSha256 = sha256(
+ readFileSync(resolve(repositoryRoot, "node_modules", lane.packageAlias, "package.json")),
+);
+const tailwindCliManifestSha256 = sha256(
+ readFileSync(resolve(repositoryRoot, "node_modules", ...lane.cliPackageAlias.split("/"), "package.json")),
+);
+const arguments_ = process.argv.slice(2).filter((argument) => argument !== "--");
+let evidenceRoot = resolve(
+ repositoryRoot,
+ "target",
+ "browser-output-certification",
+ "incoming",
+);
+let outputPath = resolve(
+ repositoryRoot,
+ "target",
+ "browser-output-certification",
+ "browser-output-matrix.json",
+);
+for (const argument of arguments_) {
+ if (argument.startsWith("--evidence-root=")) evidenceRoot = resolve(repositoryRoot, argument.slice(16));
+ else if (argument.startsWith("--output=")) outputPath = resolve(repositoryRoot, argument.slice(9));
+ else throw new Error("usage: node scripts/check-browser-output-matrix.mjs [--evidence-root=] [--output=]");
+}
+
+function fail(message) {
+ throw new Error(`Browser/output matrix: ${message}`);
+}
+
+function repositoryChild(path, label) {
+ const child = relative(repositoryRoot, path);
+ if (!child || child === ".." || child.startsWith(`..${sep}`) || isAbsolute(child)) {
+ fail(`${label} escapes the repository`);
+ }
+ return child.replaceAll("\\", "/");
+}
+
+repositoryChild(evidenceRoot, "evidence root");
+repositoryChild(outputPath, "output");
+
+function jsonFiles(directory) {
+ if (!existsSync(directory)) fail(`evidence root does not exist: ${directory}`);
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
+ const path = resolve(directory, entry.name);
+ if (entry.isDirectory()) return jsonFiles(path);
+ return entry.isFile() && entry.name.endsWith(".json") ? [path] : [];
+ });
+}
+
+function canonicalInstant(value, label) {
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/u.test(value ?? "")) {
+ fail(`${label} is not a canonical UTC instant`);
+ }
+ return Date.parse(value);
+}
+
+function same(actual, expected, label) {
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) fail(`${label} drifted`);
+}
+
+function gitObject(arguments_, label) {
+ const result = spawnSync("git", arguments_, {
+ cwd: repositoryRoot,
+ encoding: "utf8",
+ windowsHide: true,
+ });
+ if (result.error || result.status !== 0) fail(`cannot resolve checkout ${label}`);
+ const value = result.stdout.trim();
+ if (!/^[0-9a-f]{40}$/u.test(value)) fail(`checkout ${label} is invalid`);
+ return value;
+}
+
+function verifyArtifact(evidencePath, artifact, label) {
+ if (!artifact || typeof artifact.path !== "string" || !/^[0-9a-f]{64}$/u.test(artifact.sha256)) {
+ fail(`${label} metadata is invalid`);
+ }
+ const path = resolve(dirname(evidencePath), artifact.path);
+ const child = relative(dirname(evidencePath), path);
+ if (!child || child === ".." || child.startsWith(`..${sep}`) || isAbsolute(child)) {
+ fail(`${label} escapes its host artifact`);
+ }
+ const bytes = readFileSync(path);
+ if (sha256(bytes) !== artifact.sha256 || bytes.byteLength !== artifact.bytes) {
+ fail(`${label} hash or size drifted`);
+ }
+}
+
+const authoritySha256 = sha256(readFileSync(authorityPath));
+const documents = jsonFiles(evidenceRoot)
+ .map((path) => ({ path, value: JSON.parse(readFileSync(path, "utf8")) }))
+ .filter(({ value }) => value.kind === "pliegocss-browser-output-host-evidence");
+if (documents.length !== authority.hosts.length) {
+ fail(`expected ${authority.hosts.length} host documents, found ${documents.length}`);
+}
+
+const source = documents[0]?.value.source;
+const inputs = documents[0]?.value.inputs;
+const checkoutSource = {
+ commit: gitObject(["rev-parse", "HEAD"], "commit"),
+ gitTree: gitObject(["rev-parse", "HEAD^{tree}"], "Git tree"),
+};
+if (source?.commit !== checkoutSource.commit || source?.gitTree !== checkoutSource.gitTree) {
+ fail("host evidence is not bound to the aggregator checkout");
+}
+const hostSummaries = [];
+for (const expectedHost of authority.hosts) {
+ const entry = documents.find(({ value }) => value.host?.id === expectedHost.id);
+ if (!entry) fail(`missing ${expectedHost.id}`);
+ const document = entry.value;
+ if (document.schemaVersion !== 1 || document.result !== "passed") fail(`${expectedHost.id} did not pass`);
+ same(document.source, source, `${expectedHost.id} source`);
+ if (document.source.dirty || document.source.statusEntryCount !== 0) {
+ fail(`${expectedHost.id} is not clean-tree evidence`);
+ }
+ if (document.source.statusSha256 !== sha256(Buffer.alloc(0))) {
+ fail(`${expectedHost.id} clean status hash drifted`);
+ }
+ if (!/^[0-9a-f]{40}$/u.test(document.source.commit) || !/^[0-9a-f]{40}$/u.test(document.source.gitTree)) {
+ fail(`${expectedHost.id} source IDs are invalid`);
+ }
+ const generated = canonicalInstant(document.generatedAtUtc, `${expectedHost.id}.generatedAtUtc`);
+ const expires = canonicalInstant(document.expiresAtUtc, `${expectedHost.id}.expiresAtUtc`);
+ if (expires - generated !== authority.maximumEvidenceAgeHours * 60 * 60 * 1_000) {
+ fail(`${expectedHost.id} validity window drifted`);
+ }
+ if (Date.now() > expires) fail(`${expectedHost.id} evidence expired`);
+ same(
+ {
+ id: document.host.id,
+ runner: document.host.runner,
+ os: document.host.os,
+ arch: document.host.arch,
+ browser: document.host.browser,
+ },
+ expectedHost,
+ `${expectedHost.id} host identity`,
+ );
+ if (!document.host.browserVersion || document.host.playwright !== "1.61.1") {
+ fail(`${expectedHost.id} browser/tooling identity is incomplete`);
+ }
+ if (
+ typeof document.compiler?.rustc !== "string" ||
+ typeof document.compiler?.host !== "string" ||
+ !/^[0-9a-f]{64}$/u.test(document.compiler?.sha256 ?? "") ||
+ Object.hasOwn(document.compiler ?? {}, "executable")
+ ) {
+ fail(`${expectedHost.id} compiler identity is incomplete or non-portable`);
+ }
+ if (
+ document.authority?.path !== "benchmarks/browser-output-certification-v1/authority.json" ||
+ document.authority?.sha256 !== authoritySha256
+ ) {
+ fail(`${expectedHost.id} authority drifted`);
+ }
+ same(document.resetModes, authority.resetModes, `${expectedHost.id} reset modes`);
+ same(document.computedProperties, authority.computedProperties, `${expectedHost.id} computed properties`);
+ same(document.coverage, authority.requiredCoverage, `${expectedHost.id} coverage`);
+ same(document.inputs, inputs, `${expectedHost.id} inputs`);
+ if (
+ JSON.stringify(Object.keys(document.inputs ?? {}).sort()) !== JSON.stringify([
+ "competitorAuthoritySha256",
+ "competitorExpiresAtUtc",
+ "competitorObservedAtUtc",
+ "fixtureSha256",
+ "pliegoCssSha256",
+ "resetSha256",
+ "tailwindCliManifestSha256",
+ "tailwindCliVersion",
+ "tailwindCssSha256",
+ "tailwindPackageManifestSha256",
+ "tailwindVersion",
+ ]) ||
+ document.inputs.competitorAuthoritySha256 !== sha256(competitorBytes) ||
+ document.inputs.competitorObservedAtUtc !== competitor.observedAtUtc ||
+ document.inputs.competitorExpiresAtUtc !== competitor.expiresAtUtc ||
+ document.inputs?.tailwindVersion !== lane.version ||
+ document.inputs?.tailwindCliVersion !== lane.cliVersion ||
+ document.inputs.tailwindPackageManifestSha256 !== tailwindManifestSha256 ||
+ document.inputs.tailwindCliManifestSha256 !== tailwindCliManifestSha256 ||
+ document.inputs.fixtureSha256 !== authority.fixture.sha256 ||
+ document.inputs.resetSha256 !== authority.reset.sha256 ||
+ !/^[0-9a-f]{64}$/u.test(document.inputs.pliegoCssSha256 ?? "") ||
+ !/^[0-9a-f]{64}$/u.test(document.inputs.tailwindCssSha256 ?? "")
+ ) {
+ fail(`${expectedHost.id} competitor provenance is incomplete`);
+ }
+ const expectedObservationIds = authority.resetModes.flatMap((mode) =>
+ authority.scenarios.map((scenario) => `${mode.id}/${scenario.id}`),
+ );
+ const actualObservationIds = document.observations.map(
+ (observation) => `${observation.resetMode}/${observation.scenario}`,
+ );
+ same(actualObservationIds, expectedObservationIds, `${expectedHost.id} observations`);
+ let maximumMismatchRatio = 0;
+ let maximumLayoutGeometryDeltaCssPx = 0;
+ for (const observation of document.observations) {
+ const scenario = authority.scenarios.find((candidate) => candidate.id === observation.scenario);
+ if (!scenario) fail(`${expectedHost.id}/${observation.scenario} scenario is not authoritative`);
+ same(
+ observation.viewport,
+ { width: scenario.width, height: scenario.height },
+ `${expectedHost.id}/${observation.resetMode}/${observation.scenario} viewport`,
+ );
+ if (observation.action !== scenario.action) {
+ fail(`${expectedHost.id}/${observation.resetMode}/${observation.scenario} action drifted`);
+ }
+ if (!observation.computed.equal || observation.computed.differenceCount !== 0) {
+ fail(`${expectedHost.id}/${observation.resetMode}/${observation.scenario} computed styles differ`);
+ }
+ if (
+ !Array.isArray(observation.computed.differences) ||
+ observation.computed.differences.length !== 0 ||
+ !/^[0-9a-f]{64}$/u.test(observation.computed.pliegoSha256 ?? "") ||
+ !/^[0-9a-f]{64}$/u.test(observation.computed.tailwindSha256 ?? "") ||
+ !Number.isSafeInteger(observation.computed.nodeCount) ||
+ observation.computed.nodeCount < 1
+ ) {
+ fail(`${expectedHost.id}/${observation.resetMode}/${observation.scenario} computed evidence is incomplete`);
+ }
+ const geometryDelta = observation.computed.maximumLayoutGeometryDeltaCssPx;
+ if (
+ !Number.isFinite(geometryDelta) ||
+ geometryDelta < 0 ||
+ geometryDelta > authority.comparison.maximumLayoutGeometryDeltaCssPx
+ ) {
+ fail(`${expectedHost.id}/${observation.resetMode}/${observation.scenario} layout geometry failed`);
+ }
+ maximumLayoutGeometryDeltaCssPx = Math.max(
+ maximumLayoutGeometryDeltaCssPx,
+ geometryDelta,
+ );
+ const screenshots = observation.screenshots;
+ if (
+ !screenshots.passed ||
+ screenshots.threshold !== authority.comparison.screenshotPixelThreshold ||
+ screenshots.maximumMismatchRatio !== authority.comparison.maximumScreenshotMismatchRatio ||
+ !Number.isSafeInteger(screenshots.mismatchPixels) ||
+ screenshots.mismatchPixels < 0 ||
+ !Number.isFinite(screenshots.mismatchRatio) ||
+ screenshots.mismatchRatio < 0 ||
+ screenshots.mismatchRatio > authority.comparison.maximumScreenshotMismatchRatio ||
+ !Number.isSafeInteger(screenshots.width) ||
+ !Number.isSafeInteger(screenshots.height) ||
+ screenshots.width !== scenario.width ||
+ screenshots.height < scenario.height ||
+ screenshots.tailwindWidth !== screenshots.width ||
+ screenshots.tailwindHeight !== screenshots.height ||
+ screenshots.mismatchPixels > screenshots.width * screenshots.height ||
+ screenshots.mismatchRatio !==
+ Number((screenshots.mismatchPixels / (screenshots.width * screenshots.height)).toFixed(9))
+ ) {
+ fail(`${expectedHost.id}/${observation.resetMode}/${observation.scenario} screenshot comparison failed`);
+ }
+ maximumMismatchRatio = Math.max(maximumMismatchRatio, screenshots.mismatchRatio ?? 1);
+ verifyArtifact(entry.path, screenshots.pliego, `${expectedHost.id} Pliego screenshot`);
+ verifyArtifact(entry.path, screenshots.tailwind, `${expectedHost.id} Tailwind screenshot`);
+ verifyArtifact(entry.path, screenshots.diff, `${expectedHost.id} screenshot diff`);
+ }
+ if (
+ document.summary.computedMismatchCount !== 0 ||
+ document.summary.screenshotMismatchCount !== 0 ||
+ document.summary.resetModeCount !== authority.resetModes.length ||
+ document.summary.scenarioCount !== expectedObservationIds.length ||
+ document.summary.maximumLayoutGeometryDeltaCssPx !==
+ Number(maximumLayoutGeometryDeltaCssPx.toFixed(6))
+ ) {
+ fail(`${expectedHost.id} summary drifted`);
+ }
+ hostSummaries.push({
+ id: expectedHost.id,
+ runner: expectedHost.runner,
+ os: expectedHost.os,
+ arch: expectedHost.arch,
+ browser: expectedHost.browser,
+ browserVersion: document.host.browserVersion,
+ generatedAtUtc: document.generatedAtUtc,
+ expiresAtUtc: document.expiresAtUtc,
+ scenarios: document.observations.length,
+ maximumLayoutGeometryDeltaCssPx: Number(maximumLayoutGeometryDeltaCssPx.toFixed(6)),
+ maximumScreenshotMismatchRatio: Number(maximumMismatchRatio.toFixed(9)),
+ evidence: {
+ path: repositoryChild(entry.path, `${expectedHost.id} evidence`),
+ sha256: sha256(readFileSync(entry.path)),
+ },
+ });
+}
+
+const matrix = {
+ schemaVersion: 1,
+ kind: "pliegocss-browser-output-certification-matrix",
+ result: "passed",
+ generatedAtUtc: new Date().toISOString().replace(/\.\d{3}Z$/u, "Z"),
+ source: {
+ commit: source.commit,
+ gitTree: source.gitTree,
+ },
+ authority: {
+ path: "benchmarks/browser-output-certification-v1/authority.json",
+ sha256: authoritySha256,
+ },
+ coverage: authority.requiredCoverage,
+ resetModes: authority.resetModes.map((mode) => mode.id),
+ hosts: hostSummaries,
+ claimBoundary: authority.comparison.claimBoundary,
+};
+mkdirSync(dirname(outputPath), { recursive: true });
+writeFileSync(outputPath, `${JSON.stringify(matrix, null, 2)}\n`);
+process.stdout.write(
+ `${JSON.stringify(
+ {
+ schemaVersion: 1,
+ result: "passed",
+ source: matrix.source,
+ hosts: matrix.hosts.length,
+ browsers: authority.browsers,
+ output: repositoryChild(outputPath, "output"),
+ },
+ null,
+ 2,
+ )}\n`,
+);
diff --git a/scripts/check-browser-output-matrix.test.mjs b/scripts/check-browser-output-matrix.test.mjs
new file mode 100644
index 0000000..90ed248
--- /dev/null
+++ b/scripts/check-browser-output-matrix.test.mjs
@@ -0,0 +1,212 @@
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+import { createHash } from "node:crypto";
+import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { dirname, relative, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const authorityPath = resolve(
+ root,
+ "benchmarks",
+ "browser-output-certification-v1",
+ "authority.json",
+);
+const authorityBytes = readFileSync(authorityPath);
+const authority = JSON.parse(authorityBytes);
+const competitor = JSON.parse(readFileSync(resolve(root, authority.competitorAuthority), "utf8"));
+const competitorBytes = readFileSync(resolve(root, authority.competitorAuthority));
+const lane = competitor.lanes.find((candidate) => candidate.id === authority.competitorLane);
+assert(lane);
+
+const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
+const workRoot = resolve(
+ root,
+ "target",
+ "browser-output-certification",
+ `matrix-contract-${process.pid}`,
+);
+const incoming = resolve(workRoot, "incoming");
+const output = resolve(workRoot, "matrix.json");
+const generated = new Date();
+generated.setMilliseconds(0);
+const expires = new Date(generated.getTime() + authority.maximumEvidenceAgeHours * 60 * 60 * 1_000);
+const source = {
+ commit: spawnSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).stdout.trim(),
+ gitTree: spawnSync("git", ["rev-parse", "HEAD^{tree}"], { cwd: root, encoding: "utf8" }).stdout.trim(),
+ dirty: false,
+ statusEntryCount: 0,
+ statusSha256: sha256(Buffer.alloc(0)),
+};
+
+function runMatrix() {
+ return spawnSync(
+ process.execPath,
+ [
+ "scripts/check-browser-output-matrix.mjs",
+ `--evidence-root=${relative(root, incoming).replaceAll("\\", "/")}`,
+ `--output=${relative(root, output).replaceAll("\\", "/")}`,
+ ],
+ { cwd: root, encoding: "utf8", windowsHide: true },
+ );
+}
+
+function evidence(host, directory) {
+ const artifacts = {};
+ for (const [name, bytes] of [
+ ["pliego", Buffer.from("pliego")],
+ ["tailwind", Buffer.from("tailwind")],
+ ["diff", Buffer.from("diff")],
+ ]) {
+ const path = resolve(directory, `${name}.png`);
+ writeFileSync(path, bytes);
+ artifacts[name] = { path: `${name}.png`, sha256: sha256(bytes), bytes: bytes.byteLength };
+ }
+ const observations = authority.resetModes.flatMap((mode) =>
+ authority.scenarios.map((scenario) => ({
+ resetMode: mode.id,
+ scenario: scenario.id,
+ viewport: { width: scenario.width, height: scenario.height },
+ action: scenario.action,
+ computed: {
+ equal: true,
+ differenceCount: 0,
+ differences: [],
+ maximumLayoutGeometryDeltaCssPx: 0,
+ pliegoSha256: "3".repeat(64),
+ tailwindSha256: "3".repeat(64),
+ nodeCount: 44,
+ },
+ screenshots: {
+ passed: true,
+ byteEqual: true,
+ width: scenario.width,
+ height: scenario.height,
+ tailwindWidth: scenario.width,
+ tailwindHeight: scenario.height,
+ mismatchPixels: 0,
+ mismatchRatio: 0,
+ diff: artifacts.diff,
+ threshold: authority.comparison.screenshotPixelThreshold,
+ maximumMismatchRatio: authority.comparison.maximumScreenshotMismatchRatio,
+ pliego: artifacts.pliego,
+ tailwind: artifacts.tailwind,
+ },
+ })),
+ );
+ return {
+ schemaVersion: 1,
+ kind: "pliegocss-browser-output-host-evidence",
+ result: "passed",
+ generatedAtUtc: generated.toISOString().replace(/\.000Z$/u, "Z"),
+ expiresAtUtc: expires.toISOString().replace(/\.000Z$/u, "Z"),
+ source,
+ host: {
+ ...host,
+ os: host.os,
+ arch: host.arch,
+ node: process.version,
+ playwright: "1.61.1",
+ browserVersion: "contract-test",
+ },
+ authority: {
+ path: "benchmarks/browser-output-certification-v1/authority.json",
+ sha256: sha256(authorityBytes),
+ },
+ compiler: {
+ sha256: "4".repeat(64),
+ rustc: "rustc 1.96.0 (contract-test)",
+ host: "contract-test",
+ },
+ inputs: {
+ competitorAuthoritySha256: sha256(competitorBytes),
+ competitorObservedAtUtc: competitor.observedAtUtc,
+ competitorExpiresAtUtc: competitor.expiresAtUtc,
+ tailwindVersion: lane.version,
+ tailwindCliVersion: lane.cliVersion,
+ tailwindPackageManifestSha256: sha256(
+ readFileSync(resolve(root, "node_modules", lane.packageAlias, "package.json")),
+ ),
+ tailwindCliManifestSha256: sha256(
+ readFileSync(resolve(root, "node_modules", ...lane.cliPackageAlias.split("/"), "package.json")),
+ ),
+ fixtureSha256: authority.fixture.sha256,
+ resetSha256: authority.reset.sha256,
+ pliegoCssSha256: "5".repeat(64),
+ tailwindCssSha256: "6".repeat(64),
+ },
+ resetModes: authority.resetModes,
+ computedProperties: authority.computedProperties,
+ observations,
+ coverage: authority.requiredCoverage,
+ summary: {
+ resetModeCount: authority.resetModes.length,
+ scenarioCount: observations.length,
+ computedMismatchCount: 0,
+ screenshotMismatchCount: 0,
+ maximumLayoutGeometryDeltaCssPx: 0,
+ },
+ claimBoundary: authority.comparison.claimBoundary,
+ };
+}
+
+try {
+ mkdirSync(incoming, { recursive: true });
+ const paths = [];
+ for (const host of authority.hosts) {
+ const directory = resolve(incoming, host.id);
+ mkdirSync(directory, { recursive: true });
+ const path = resolve(directory, "evidence.json");
+ writeFileSync(path, `${JSON.stringify(evidence(host, directory), null, 2)}\n`);
+ paths.push(path);
+ }
+
+ const passing = runMatrix();
+ assert.equal(passing.status, 0, passing.stderr);
+ const matrix = JSON.parse(readFileSync(output, "utf8"));
+ assert.equal(matrix.result, "passed");
+ assert.equal(matrix.hosts.length, authority.hosts.length);
+
+ for (let index = 0; index < paths.length; index += 1) {
+ const drifted = evidence(authority.hosts[index], dirname(paths[index]));
+ drifted.source = { ...drifted.source, commit: "f".repeat(40) };
+ writeFileSync(paths[index], `${JSON.stringify(drifted, null, 2)}\n`);
+ }
+ const checkoutDrift = runMatrix();
+ assert.notEqual(checkoutDrift.status, 0);
+ assert.match(checkoutDrift.stderr, /not bound to the aggregator checkout/u);
+
+ for (let index = 0; index < paths.length; index += 1) {
+ writeFileSync(
+ paths[index],
+ `${JSON.stringify(evidence(authority.hosts[index], dirname(paths[index])), null, 2)}\n`,
+ );
+ }
+ const first = JSON.parse(readFileSync(paths[0], "utf8"));
+ first.source = { ...first.source, dirty: true, statusEntryCount: 1 };
+ writeFileSync(paths[0], `${JSON.stringify(first, null, 2)}\n`);
+ const dirty = runMatrix();
+ assert.notEqual(dirty.status, 0);
+ assert.match(dirty.stderr, /source drifted|not clean-tree evidence/u);
+
+ writeFileSync(paths[0], `${JSON.stringify(evidence(authority.hosts[0], dirname(paths[0])), null, 2)}\n`);
+ const geometryFailure = JSON.parse(readFileSync(paths[0], "utf8"));
+ geometryFailure.observations[0].computed.maximumLayoutGeometryDeltaCssPx =
+ authority.comparison.maximumLayoutGeometryDeltaCssPx + 0.01;
+ geometryFailure.summary.maximumLayoutGeometryDeltaCssPx =
+ geometryFailure.observations[0].computed.maximumLayoutGeometryDeltaCssPx;
+ writeFileSync(paths[0], `${JSON.stringify(geometryFailure, null, 2)}\n`);
+ const geometry = runMatrix();
+ assert.notEqual(geometry.status, 0);
+ assert.match(geometry.stderr, /layout geometry failed/u);
+
+ writeFileSync(paths[0], `${JSON.stringify(evidence(authority.hosts[0], dirname(paths[0])), null, 2)}\n`);
+ writeFileSync(resolve(dirname(paths[0]), "diff.png"), "tampered");
+ const tampered = runMatrix();
+ assert.notEqual(tampered.status, 0);
+ assert.match(tampered.stderr, /hash or size drifted/u);
+
+ process.stdout.write("browser/output matrix fail-closed contract: pass\n");
+} finally {
+ rmSync(workRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
+}
diff --git a/scripts/check-compatibility-policy.mjs b/scripts/check-compatibility-policy.mjs
index 9abec45..b7336c5 100644
--- a/scripts/check-compatibility-policy.mjs
+++ b/scripts/check-compatibility-policy.mjs
@@ -2,11 +2,14 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
import { dirname, join, resolve } from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
+import { cargoTargetRoot, isolatedCargoEnvironment } from "./rust-target.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
-const targetDir = process.env.CARGO_TARGET_DIR
- ? resolve(root, process.env.CARGO_TARGET_DIR)
- : join(root, "target");
+const cargoEnvironment = isolatedCargoEnvironment(root, {
+ env: process.env,
+ toolchain: "1.85.0",
+});
+const targetDir = cargoTargetRoot(root, cargoEnvironment);
const runtime = join(targetDir, "compatibility-policy", `run-${process.pid}-${Date.now()}`);
const expected = readFileSync(
join(root, "integration-tests", "compatibility-policy", "expected.baseline-widely.json"),
@@ -24,7 +27,7 @@ function run(program, args, { cwd = runtime, expectedStatus = 0, timeout = 30_00
const result = spawnSync(program, args, {
cwd,
encoding: "utf8",
- env: { ...process.env, CARGO_TARGET_DIR: targetDir },
+ env: cargoEnvironment,
timeout,
windowsHide: true,
});
@@ -45,7 +48,7 @@ function resolveExecutable() {
return configured;
}
- run("cargo", ["+1.85", "build", "--locked", "-p", "pliego-cssc"], {
+ run("cargo", ["+1.85.0", "build", "--locked", "-p", "pliego-cssc"], {
cwd: root,
timeout: 300_000,
});
diff --git a/scripts/check-document-authority.mjs b/scripts/check-document-authority.mjs
new file mode 100644
index 0000000..84e4f60
--- /dev/null
+++ b/scripts/check-document-authority.mjs
@@ -0,0 +1,79 @@
+import { spawnSync } from "node:child_process";
+import { existsSync, readFileSync, readdirSync } from "node:fs";
+import { resolve } from "node:path";
+
+const root = resolve(import.meta.dirname, "..");
+const fail = (message) => { throw new Error(`document authority: ${message}`); };
+const authority = JSON.parse(
+ readFileSync(resolve(root, "docs/product/document-authority.json"), "utf8"),
+);
+const exact = (value, keys, role) => {
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail(`${role} must be an object`);
+ if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...keys].sort())) {
+ fail(`${role} fields drifted`);
+ }
+};
+exact(
+ authority,
+ ["schemaVersion", "kind", "currentState", "siteProjection", "normative", "historical", "rules"],
+ "root",
+);
+if (authority.schemaVersion !== 1 || authority.kind !== "pliegocss-document-authority") {
+ fail("header drifted");
+}
+exact(authority.currentState, ["machine", "human", "generatedBy"], "currentState");
+exact(authority.siteProjection, ["source", "route"], "siteProjection");
+const requiredCurrent = {
+ machine: "docs/product/release-readiness-0.1.0.json",
+ human: "docs/product/release-readiness-0.1.0.md",
+ generatedBy: "scripts/render-release-readiness.mjs",
+};
+if (JSON.stringify(authority.currentState) !== JSON.stringify(requiredCurrent)) {
+ fail("current state is not singular and canonical");
+}
+if (
+ authority.siteProjection.source !== "docs/site/release-readiness.md" ||
+ authority.siteProjection.route !== "/docs/release-readiness/"
+) {
+ fail("site projection drifted");
+}
+for (const path of [
+ ...Object.values(authority.currentState),
+ authority.siteProjection.source,
+ ...authority.normative,
+]) {
+ if (!existsSync(resolve(root, path))) fail(`missing governed document ${path}`);
+}
+for (const entry of authority.historical) {
+ exact(entry, ["path", "supersededBy"], `historical.${entry.path}`);
+ if (!entry.path.startsWith("docs/archive/")) fail(`${entry.path} is not archived`);
+ if (!existsSync(resolve(root, entry.path))) fail(`missing archive ${entry.path}`);
+ if (!existsSync(resolve(root, entry.supersededBy))) fail(`missing successor ${entry.supersededBy}`);
+ const bytes = readFileSync(resolve(root, entry.path), "utf8");
+ if (!bytes.includes("historical") && !bytes.includes("archiveStatus")) {
+ fail(`${entry.path} lacks an archive boundary`);
+ }
+}
+for (const stale of [
+ "docs/product/hardening-report-0.1.0-rc.2.md",
+ "docs/product/next-gap-2026-07-20.md",
+ "docs/product/next-gap-2026-07-20.json",
+]) {
+ if (existsSync(resolve(root, stale))) fail(`stale current-looking path still exists: ${stale}`);
+}
+const readinessFiles = readdirSync(resolve(root, "docs/product"))
+ .filter((name) => /^release-readiness-.+\.json$/u.test(name));
+if (JSON.stringify(readinessFiles) !== JSON.stringify(["release-readiness-0.1.0.json"])) {
+ fail("more than one current readiness machine document exists");
+}
+const render = spawnSync(
+ process.execPath,
+ [resolve(root, authority.currentState.generatedBy), "--check"],
+ { cwd: root, encoding: "utf8", windowsHide: true },
+);
+if (render.error || render.status !== 0) {
+ fail(`generated current page drifted: ${render.error?.message ?? render.stderr.trim()}`);
+}
+process.stdout.write(
+ `${JSON.stringify({schemaVersion: 1, currentState: authority.currentState.machine, historical: authority.historical.length, siteRoute: authority.siteProjection.route}, null, 2)}\n`,
+);
diff --git a/scripts/check-engine-boundary.mjs b/scripts/check-engine-boundary.mjs
new file mode 100644
index 0000000..488ba52
--- /dev/null
+++ b/scripts/check-engine-boundary.mjs
@@ -0,0 +1,78 @@
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+const root = resolve(import.meta.dirname, "..");
+const read = (path) => readFileSync(resolve(root, path), "utf8");
+
+const compiler = read("crates/pliego-css-compiler/src/engine.rs");
+const compilerLib = read("crates/pliego-css-compiler/src/lib.rs");
+const cli = read("crates/pliego-cssc/src/main.rs");
+const scanner = read("crates/pliego-cssc/src/source_candidates.rs");
+const macros = read("crates/pliego-css-macros/src/lib.rs");
+const lsp = read("crates/pliego-css-lsp/src/lib.rs");
+
+function requireText(source, fragment, label) {
+ if (!source.includes(fragment)) throw new Error(`${label} is missing ${fragment}`);
+}
+
+function forbidText(source, fragment, label) {
+ if (source.includes(fragment)) throw new Error(`${label} still contains ${fragment}`);
+}
+
+for (const contract of [
+ "pub struct CompileRequest",
+ "pub struct CompileResult",
+ "pub struct AnalysisHost",
+ "pub struct PhysicalRulePlanner",
+ "pub struct PcxRequest",
+ "pub fn compile(&mut self, request: &CompileRequest)",
+]) {
+ requireText(compiler, contract, "shared compiler engine");
+}
+for (const exported of [
+ "AnalysisHost",
+ "CompileRequest",
+ "CompileResult",
+ "PhysicalRulePlanner",
+ "PcxRequest",
+]) {
+ requireText(compilerLib, exported, "engine export");
+}
+
+for (const [source, label] of [
+ [cli, "CLI/watch adapter"],
+ [scanner, "Rust source scanner"],
+ [macros, "procedural macro"],
+ [lsp, "LSP adapter"],
+]) {
+ requireText(source, "AnalysisHost", label);
+}
+requireText(cli, "CompileRequest", "CLI/watch adapter");
+requireText(scanner, "PcxRequest", "Rust source scanner");
+requireText(macros, "PcxRequest", "procedural macro");
+requireText(lsp, "PcxRequest", "LSP adapter");
+requireText(cli, "engine: AnalysisHost", "watch cache");
+
+forbidText(cli, "CssFragmentCache", "CLI physical planner");
+forbidText(scanner, "analyze_cross_clause_conflicts", "Rust source scanner");
+forbidText(macros, "fn cartesian_indices", "procedural macro");
+forbidText(macros, "analyze_cross_clause_conflicts", "procedural macro");
+forbidText(lsp, "Command::", "LSP adapter");
+forbidText(lsp, "std::process::{", "LSP adapter");
+forbidText(lsp, "TemporarySource", "LSP adapter");
+forbidText(lsp, "run_source_check", "LSP adapter");
+
+process.stdout.write(
+ `${JSON.stringify(
+ {
+ schemaVersion: 1,
+ engineApi: "CompileRequest->CompileResult",
+ sharedPcxFrontend: ["macros", "scanner", "lsp"],
+ sharedAdapters: ["cli", "watch", "lsp"],
+ physicalPlanner: "compiler-owned",
+ lspCompilerProcesses: 0,
+ },
+ null,
+ 2,
+ )}\n`,
+);
diff --git a/scripts/check-getting-started.mjs b/scripts/check-getting-started.mjs
index 0533fd7..5c9c50e 100644
--- a/scripts/check-getting-started.mjs
+++ b/scripts/check-getting-started.mjs
@@ -1,15 +1,16 @@
import { createHash } from "node:crypto";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
-import { isAbsolute, join, resolve } from "node:path";
+import { join, resolve } from "node:path";
import { spawnSync } from "node:child_process";
+import { cargoTargetRoot, isolatedCargoEnvironment } from "./rust-target.mjs";
const ROOT = resolve(import.meta.dirname, "..");
-const TARGET_ROOT = process.env.CARGO_TARGET_DIR
- ? isAbsolute(process.env.CARGO_TARGET_DIR)
- ? process.env.CARGO_TARGET_DIR
- : resolve(ROOT, process.env.CARGO_TARGET_DIR)
- : join(ROOT, "target");
+const cargoEnvironment = isolatedCargoEnvironment(ROOT, {
+ env: process.env,
+ toolchain: "1.85.0",
+});
+const TARGET_ROOT = cargoTargetRoot(ROOT, cargoEnvironment);
const EXECUTABLE_SUFFIX = process.platform === "win32" ? ".exe" : "";
const BASIC_EXECUTABLE = join(TARGET_ROOT, "debug", `pliego-css-basic-example${EXECUTABLE_SUFFIX}`);
const CLI_EXECUTABLE = join(TARGET_ROOT, "debug", `pliego-cssc${EXECUTABLE_SUFFIX}`);
@@ -24,6 +25,7 @@ function fail(message, detail = undefined) {
function run(command, args) {
const result = spawnSync(command, args, {
cwd: ROOT,
+ env: cargoEnvironment,
encoding: "utf8",
windowsHide: true,
});
diff --git a/scripts/check-hosted-browser-matrix.mjs b/scripts/check-hosted-browser-matrix.mjs
index 4127cd1..89b781d 100644
--- a/scripts/check-hosted-browser-matrix.mjs
+++ b/scripts/check-hosted-browser-matrix.mjs
@@ -1,18 +1,37 @@
-import { existsSync, readFileSync } from "node:fs";
+import { readFileSync } from "node:fs";
import { resolve } from "node:path";
+import { AuthorityError, validateBrowserMatrix } from "./release-authority.mjs";
const root = resolve(import.meta.dirname, "..");
-const matrix = JSON.parse(readFileSync(resolve(root, "docs/benchmarks/hosted-browser-matrix.json"), "utf8"));
-const currentOs = process.platform === "win32" ? "windows" : process.platform;
-function fail(message) { throw new Error(`hosted browser matrix: ${message}`); }
-if (matrix.schemaVersion !== 1 || matrix.hosts?.length !== 5) fail("header or host inventory drifted");
-const allowed = new Set(["available", "not-configured", "passed", "failed"]);
-for (const host of matrix.hosts) {
- if (!allowed.has(host.status)) fail(`invalid status for ${host.id}`);
- if (host.status === "passed" && host.automation !== "passed") fail(`${host.id} claims pass without automation`);
- if (host.status === "available" && (!host.executable || !["executable-discovery-only", "local-computed-style-smoke"].includes(host.claim))) fail(`${host.id} availability is not bounded`);
- if (host.status === "not-configured" && host.claim !== "none") fail(`${host.id} claims evidence while not configured`);
- if (host.executable && currentOs === host.os && !existsSync(resolve(host.executable))) fail(`${host.id} executable is absent: ${host.executable}`);
+const args = process.argv.slice(2);
+const enforceRelease =
+ (args.length === 1 && args[0] === "--profile=release") ||
+ (args.length === 2 && args[0] === "--profile" && args[1] === "release");
+if (
+ !enforceRelease &&
+ !(
+ args.length === 0 ||
+ (args.length === 1 && args[0] === "--validate")
+ )
+) {
+ throw new Error(`hosted browser matrix: unknown mode ${args.join(" ")}`);
+}
+
+try {
+ const value = JSON.parse(
+ readFileSync(resolve(root, "docs/benchmarks/hosted-browser-matrix.json"), "utf8"),
+ );
+ const summary = validateBrowserMatrix(value, { root });
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
+ if (enforceRelease && summary.result !== "ready") {
+ process.stderr.write(
+ `hosted browser matrix: release blocked by ${summary.missingRequiredHosts.join(", ")}\n`,
+ );
+ process.exitCode = 1;
+ }
+} catch (error) {
+ if (error instanceof AuthorityError) {
+ throw new Error(`hosted browser matrix: ${error.message}`);
+ }
+ throw error;
}
-if (matrix.historicalEvidence?.status !== "historical-manual" || matrix.historicalEvidence.ciReplay !== "not-configured") fail("historical evidence boundary drifted");
-process.stdout.write(`${JSON.stringify({schemaVersion:1,available:matrix.hosts.filter((host)=>host.status==='available').map((host)=>host.id),passed:matrix.hosts.filter((host)=>host.status==='passed').map((host)=>host.id),notConfigured:matrix.hosts.filter((host)=>host.status==='not-configured').map((host)=>host.id)},null,2)}\n`);
diff --git a/scripts/check-lsp-integration.mjs b/scripts/check-lsp-integration.mjs
index c8f7e92..c6396a3 100644
--- a/scripts/check-lsp-integration.mjs
+++ b/scripts/check-lsp-integration.mjs
@@ -3,15 +3,17 @@ import { createHash } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { resolve, sep } from "node:path";
import { pathToFileURL } from "node:url";
+import { cargoTargetRoot, isolatedCargoEnvironment } from "./rust-target.mjs";
const ROOT = resolve(import.meta.dirname, "..");
const executable = process.platform === "win32" ? ".exe" : "";
-const target = process.env.CARGO_TARGET_DIR
- ? resolve(ROOT, process.env.CARGO_TARGET_DIR)
- : resolve(ROOT, "target");
+const cargoEnvironment = isolatedCargoEnvironment(ROOT, {
+ env: process.env,
+ toolchain: "1.85.0",
+});
+const target = cargoTargetRoot(ROOT, cargoEnvironment);
const lsp = resolve(target, "debug", `pliego-css-lsp${executable}`);
const compiler = resolve(target, "debug", `pliego-cssc${executable}`);
-const cancellationProxy = resolve(target, `lsp-cancellation-proxy${executable}`);
const diagnosticCorpus = JSON.parse(
readFileSync(resolve(ROOT, "integration-tests", "lsp-diagnostics", "corpus.json"), "utf8"),
);
@@ -22,25 +24,12 @@ function fail(message) {
const build = spawnSync(
"cargo",
- ["+1.85", "build", "--locked", "-p", "pliego-css-lsp", "-p", "pliego-cssc"],
- { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
+ ["+1.85.0", "build", "--locked", "-p", "pliego-css-lsp", "-p", "pliego-cssc"],
+ { cwd: ROOT, env: cargoEnvironment, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
);
if (build.error) fail(`cannot build LSP gate: ${build.error.message}`);
if (build.status !== 0) fail(`${build.stdout}${build.stderr}`.trim());
if (!existsSync(lsp) || !existsSync(compiler)) fail("LSP gate binaries are missing");
-const proxyBuild = spawnSync(
- "rustc",
- [
- "+1.85",
- resolve(ROOT, "integration-tests", "lsp-diagnostics", "cancellation-proxy.rs"),
- "-o",
- cancellationProxy,
- ],
- { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
-);
-if (proxyBuild.error) fail(`cannot build cancellation proxy: ${proxyBuild.error.message}`);
-if (proxyBuild.status !== 0) fail(`${proxyBuild.stdout}${proxyBuild.stderr}`.trim());
-if (!existsSync(cancellationProxy)) fail("LSP cancellation proxy is missing");
if (
diagnosticCorpus.kind !== "pliegocss-lsp-diagnostic-corpus" ||
diagnosticCorpus.schemaVersion !== 2 ||
@@ -113,7 +102,7 @@ const index = {
physicalCoverage: "compiler-verified-complete",
styleIdFormatVersion: 2,
classNameFormatVersion: 1,
- themeIdFormatVersion: 2,
+ themeIdFormatVersion: 3,
themeId: "seed",
targets: "modern",
format: "minified",
@@ -198,7 +187,10 @@ for (const item of diagnosticCorpus.cases) {
stdio: ["ignore", "pipe", "pipe"],
});
if (checked.error || checked.status === 0) {
- fail(`CLI diagnostic corpus case ${item.id} did not fail as required`);
+ fail(
+ `CLI diagnostic corpus case ${item.id} did not fail as required ` +
+ `(status=${checked.status}, error=${checked.error?.message ?? "none"}, stdout=${checked.stdout}, stderr=${checked.stderr})`,
+ );
}
const document = JSON.parse(checked.stderr);
if (document.schemaVersion !== 1 || document.diagnostics?.length !== 1) {
@@ -219,15 +211,17 @@ for (const item of diagnosticCorpus.cases) {
const child = spawn(
lsp,
- ["--pliego-cssc", cancellationProxy, "--seed", "--project-index", "out/pliego.index.json"],
+ [
+ "--pliego-cssc",
+ resolve(workspace, `must-not-execute-pliego-cssc${executable}`),
+ "--seed",
+ "--project-index",
+ "out/pliego.index.json",
+ ],
{
cwd: workspace,
stdio: ["pipe", "pipe", "pipe"],
- env: {
- ...process.env,
- PLIEGOCSS_REAL_COMPILER: compiler,
- PLIEGOCSS_CANCELLATION_MARKER: resolve(workspace, "cancellation.pid"),
- },
+ env: process.env,
},
);
const output = [];
@@ -250,25 +244,6 @@ async function waitForOutput(fragment, offset = 0, timeoutMs = 5000) {
fail(`LSP did not emit ${JSON.stringify(fragment)} within ${timeoutMs} ms`);
}
-async function waitForFile(path, timeoutMs = 5000) {
- const deadline = Date.now() + timeoutMs;
- while (Date.now() < deadline) {
- if (existsSync(path)) return;
- await new Promise((resolveDelay) => setTimeout(resolveDelay, 10));
- }
- fail(`LSP cancellation child did not create ${path} within ${timeoutMs} ms`);
-}
-
-function processIsAlive(pid) {
- try {
- process.kill(pid, 0);
- return true;
- } catch (error) {
- if (error?.code === "ESRCH") return false;
- throw error;
- }
-}
-
const uri = pathToFileURL(resolve(workspace, "src", "view.rs")).href;
const rootUri = pathToFileURL(workspace).href;
const cursor = text.indexOf("gap") + 3;
@@ -331,7 +306,6 @@ send({
params: { textDocument: { uri }, options: { tabSize: 4, insertSpaces: true } },
});
await waitForOutput("unknown utility `unknown-thing`");
-const cancellationMarker = resolve(workspace, "cancellation.pid");
const cancellationVersion = 11;
send({
jsonrpc: "2.0",
@@ -341,11 +315,6 @@ send({
contentChanges: [{ text: 'fn view(){let _=pc!("cancel-me");}' }],
},
});
-await waitForFile(cancellationMarker);
-const cancelledPid = Number(readFileSync(cancellationMarker, "utf8").trim());
-if (!Number.isSafeInteger(cancelledPid) || cancelledPid <= 0) {
- fail("cancellation proxy wrote an invalid process id");
-}
const recoveryVersion = 12;
const recoveryText = 'fn view(){let _=pc!("unknown-after-cancel");}';
const recoveryOffset = Buffer.concat(output).length;
@@ -360,11 +329,6 @@ send({
});
await waitForOutput("unknown utility `unknown-after-cancel`", recoveryOffset, 5000);
const cancellationLatencyMs = Date.now() - cancellationStarted;
-const processDeadline = Date.now() + 2000;
-while (Date.now() < processDeadline && processIsAlive(cancelledPid)) {
- await new Promise((resolveDelay) => setTimeout(resolveDelay, 10));
-}
-if (processIsAlive(cancelledPid)) fail("stale semantic compiler child remained alive");
const pcxVersion = 13;
const pcxText =
@@ -408,19 +372,7 @@ for (const item of diagnosticCorpus.cases) {
corpusRuns.push({ item, sourceText, version: corpusVersion });
corpusVersion += 1;
}
-const toolFailureVersion = corpusVersion;
-const toolFailureOffset = Buffer.concat(output).length;
-send({
- jsonrpc: "2.0",
- method: "textDocument/didChange",
- params: {
- textDocument: { uri, version: toolFailureVersion },
- contentChanges: [{ text: 'fn view(){let _=pc!("proxy-invalid-json");}' }],
- },
-});
-await waitForOutput('"code":"PCL001"', toolFailureOffset);
-
-const literalLimitVersion = toolFailureVersion + 1;
+const literalLimitVersion = corpusVersion;
const literalLimitText = `fn view(){${'let _=pc!("flex");'.repeat(257)}}`;
const literalLimitOffset = Buffer.concat(output).length;
send({
@@ -542,19 +494,17 @@ if (
const cancelledVersionMessages = published.filter(
(message) =>
message.params?.version === cancellationVersion &&
- message.params?.diagnostics?.some((diagnostic) => diagnostic.code === "PCL001"),
+ message.params?.diagnostics?.some((diagnostic) => diagnostic.code === "PCS001"),
);
if (cancelledVersionMessages.length !== 0) {
- fail("cancelled semantic process leaked a tooling diagnostic");
+ fail("superseded in-process semantic analysis leaked a stale diagnostic");
}
const recoveryMessages = published.filter(
(message) =>
message.params?.version === recoveryVersion &&
message.params?.diagnostics?.some((diagnostic) => diagnostic.code === "PCS001"),
);
-if (recoveryMessages.length !== 1) {
- fail("semantic worker did not recover after forceful cancellation");
-}
+if (recoveryMessages.length !== 1) fail("semantic worker did not publish the current version");
for (const run of corpusRuns) {
const diagnostics = published
.filter((message) => message.params?.version === run.version)
@@ -591,17 +541,6 @@ for (const run of corpusRuns) {
fail(`LSP diagnostic corpus range ${run.item.id} drifted`);
}
}
-const toolFailureDiagnostics = published
- .filter((message) => message.params?.version === toolFailureVersion)
- .flatMap((message) => message.params?.diagnostics ?? []);
-const toolFailure = toolFailureDiagnostics.find((diagnostic) => diagnostic.code === "PCL001");
-if (
- toolFailure?.severity !== 1 ||
- toolFailure?.data?.category !== "tool" ||
- !toolFailure.message.includes("invalid pliego-cssc diagnostic JSON")
-) {
- fail("PCL001 fault-injection contract drifted");
-}
const literalLimitDiagnostics = published
.filter((message) => message.params?.version === literalLimitVersion)
.flatMap((message) => message.params?.diagnostics ?? []);
@@ -657,9 +596,10 @@ process.stdout.write(
semanticVersion: semanticMessages[0].params.version,
staleSemanticResults: 0,
protocolResponsiveDuringDebounce: true,
- forcefulCancellation: true,
- cancelledCompilerPid: cancelledPid,
- cancellationRecoveryMs: cancellationLatencyMs,
+ inProcessAnalysis: true,
+ compilerProcesses: 0,
+ supersededAnalysisSuppressed: true,
+ currentVersionLatencyMs: cancellationLatencyMs,
pcxDiagnostic: pcxDiagnostic.code,
pcxVersion: pcxMessages[0].params.version,
pcxRange: pcxDiagnostic.range,
@@ -667,7 +607,7 @@ process.stdout.write(
diagnosticCorpusCases: corpusRuns.length,
diagnosticCorpusEquality: "code-message-range-severity-suggestion-replacement",
typedDiagnosticCodes: [...expectedCorpusCodes, "PCX003"],
- operationalFallbacks: [toolFailure.code, literalLimit.code],
+ operationalGuards: [literalLimit.code],
formattingEdits: byId.get(2).result.length,
completion: item.label,
completionRange: item.textEdit.range,
diff --git a/scripts/check-manifest-graph.mjs b/scripts/check-manifest-graph.mjs
index 26ad069..a2b554d 100644
--- a/scripts/check-manifest-graph.mjs
+++ b/scripts/check-manifest-graph.mjs
@@ -13,16 +13,25 @@ import {
import { dirname, join, resolve } from "node:path";
import { spawn, spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
+import { cargoTargetRoot, isolatedCargoEnvironment } from "./rust-target.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const fixture = join(root, "integration-tests", "manifest-graph");
const runtime = join(root, "target", "manifest-graph", `run-${process.pid}-${Date.now()}`);
const project = join(runtime, "project");
-const targetDir = join(root, "target");
+const cargoEnvironment = isolatedCargoEnvironment(root, {
+ env: process.env,
+ toolchain: "1.85.0",
+});
+const targetDir = cargoTargetRoot(root, cargoEnvironment);
const expectedCss = readFileSync(join(fixture, "expected.css"));
const expectedManifest = readFileSync(join(fixture, "expected.manifest.json"));
const reachabilityName = "pliego.reachability.json";
const updateGoldens = process.env.PLIEGOCSS_UPDATE_GOLDENS === "1";
+const cargoBuildTimeout = Number.parseInt(
+ process.env.PLIEGOCSS_CARGO_BUILD_TIMEOUT_MS ?? "600000",
+ 10,
+);
function fail(message) {
throw new Error(message);
@@ -46,7 +55,7 @@ function run(program, args, { cwd = root, expectedStatus = 0, timeout = 30_000 }
const result = spawnSync(program, args, {
cwd,
encoding: "utf8",
- env: { ...process.env, CARGO_TARGET_DIR: targetDir },
+ env: cargoEnvironment,
timeout,
windowsHide: true,
});
@@ -67,8 +76,8 @@ function resolveExecutable() {
return configured;
}
- run("cargo", ["+1.85", "build", "--locked", "-p", "pliego-cssc"], {
- timeout: 120_000,
+ run("cargo", ["+1.85.0", "build", "--locked", "-p", "pliego-cssc"], {
+ timeout: cargoBuildTimeout,
});
const executable = join(
targetDir,
@@ -251,7 +260,7 @@ async function assertWatchContract(executable) {
],
{
cwd: project,
- env: { ...process.env, CARGO_TARGET_DIR: targetDir },
+ env: cargoEnvironment,
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
},
@@ -368,7 +377,7 @@ function assertManifestIntegrity(manifest, css, schemaVersion) {
assert(manifest.schemaVersion === schemaVersion, `expected manifest schema ${schemaVersion}`);
assert(manifest.styleIdFormatVersion === 2, "StyleId format version drifted");
assert(manifest.classNameFormatVersion === 1, "class-name format version drifted");
- assert(manifest.themeIdFormatVersion === 2, "theme ID format version drifted");
+ assert(manifest.themeIdFormatVersion === 3, "theme ID format version drifted");
assert(manifest.targets === "modern", "target contract drifted");
assert(manifest.format === "minified", "format contract drifted");
assert(manifest.cssBytes === css.byteLength, "manifest CSS byte length is stale");
diff --git a/scripts/check-maturity-map.mjs b/scripts/check-maturity-map.mjs
index a74d6ad..38bb63b 100644
--- a/scripts/check-maturity-map.mjs
+++ b/scripts/check-maturity-map.mjs
@@ -49,7 +49,7 @@ for (const [index, capability] of document.capabilities.entries()) {
if (!existsSync(resolve(root, owner))) fail(`${label}.owners references missing ${owner}`);
}
}
-const expected = { stable: 6, beta: 12, experimental: 3 };
+const expected = { stable: 6, beta: 14, experimental: 3 };
for (const maturity of maturities) {
if (counts[maturity] !== expected[maturity]) {
fail(`expected ${expected[maturity]} ${maturity} capabilities, found ${counts[maturity]}`);
diff --git a/scripts/check-migration-corpus.mjs b/scripts/check-migration-corpus.mjs
index c80c1eb..a7ea0e9 100644
--- a/scripts/check-migration-corpus.mjs
+++ b/scripts/check-migration-corpus.mjs
@@ -3,10 +3,13 @@ import { createHash } from "node:crypto";
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
+import { cargoTargetRoot, isolatedCargoEnvironment } from "./rust-target.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const corpusRoot = join(root, "integration-tests", "migration-corpus");
const manifest = JSON.parse(readFileSync(join(corpusRoot, "cases.json"), "utf8"));
+const cargoEnvironment = isolatedCargoEnvironment(root);
+const cargoTarget = cargoTargetRoot(root, cargoEnvironment);
function fail(message) {
throw new Error(message);
@@ -48,10 +51,13 @@ if (!Array.isArray(manifest.cases) || manifest.cases.length < 3) {
}
const cargo = process.env.CARGO ?? "cargo";
-run(cargo, ["build", "--locked", "--quiet", "-p", "pliego-cssc"], { cwd: root });
+run(cargo, ["build", "--locked", "--quiet", "-p", "pliego-cssc"], {
+ cwd: root,
+ env: cargoEnvironment,
+});
const executable =
process.env.PLIEGOCSS_BIN ??
- join(root, "target", "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
+ join(cargoTarget, "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
if (!existsSync(executable)) fail(`missing PliegoCSS CLI at ${executable}`);
const ids = new Set();
diff --git a/scripts/check-migration-real-corpus.mjs b/scripts/check-migration-real-corpus.mjs
index f642355..95bb811 100644
--- a/scripts/check-migration-real-corpus.mjs
+++ b/scripts/check-migration-real-corpus.mjs
@@ -11,8 +11,11 @@ import {
} from "node:fs";
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
+import { cargoTargetRoot, isolatedCargoEnvironment } from "./rust-target.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const cargoEnvironment = isolatedCargoEnvironment(root);
+const cargoTarget = cargoTargetRoot(root, cargoEnvironment);
const manifestPath = join(root, "integration-tests", "migration-real-corpus", "projects.json");
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
@@ -130,11 +133,14 @@ if (process.env.PLIEGOCSS_RUN_NETWORK_CORPUS !== "1") {
const cargo = process.env.CARGO ?? "cargo";
if (!process.env.PLIEGOCSS_BIN) {
- run(cargo, ["build", "--locked", "--quiet", "-p", "pliego-cssc"], { cwd: root });
+ run(cargo, ["build", "--locked", "--quiet", "-p", "pliego-cssc"], {
+ cwd: root,
+ env: cargoEnvironment,
+ });
}
const executable =
process.env.PLIEGOCSS_BIN ??
- join(root, "target", "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
+ join(cargoTarget, "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
if (!existsSync(executable)) fail(`missing PliegoCSS CLI at ${executable}`);
const temporaryBase = join(root, "target");
diff --git a/scripts/check-neovim-client.mjs b/scripts/check-neovim-client.mjs
index e7024ac..e24d163 100644
--- a/scripts/check-neovim-client.mjs
+++ b/scripts/check-neovim-client.mjs
@@ -10,6 +10,7 @@ import {
writeFileSync,
} from "node:fs";
import { basename, relative, resolve, sep } from "node:path";
+import { cargoTargetRoot, isolatedCargoEnvironment } from "./rust-target.mjs";
const ROOT = resolve(import.meta.dirname, "..");
const EDITOR = resolve(ROOT, "editors", "neovim");
@@ -95,14 +96,15 @@ if (!existsSync(executable)) {
}
if (!existsSync(executable)) fail(`Neovim executable is missing after extraction: ${executable}`);
-const target = process.env.CARGO_TARGET_DIR
- ? resolve(ROOT, process.env.CARGO_TARGET_DIR)
- : resolve(ROOT, "target");
+const cargoEnvironment = isolatedCargoEnvironment(ROOT, {
+ env: process.env,
+ toolchain: "1.85.0",
+});
+const target = cargoTargetRoot(ROOT, cargoEnvironment);
const workspace = resolve(target, "lsp-integration-workspace");
const output = resolve(target, "neovim-host-result.json");
const binarySuffix = process.platform === "win32" ? ".exe" : "";
const lsp = resolve(target, "debug", `pliego-css-lsp${binarySuffix}`);
-const compiler = resolve(target, "debug", `pliego-cssc${binarySuffix}`);
for (const path of [workspace, output]) {
if (!path.startsWith(`${target}${sep}`)) fail(`unsafe fixture path: ${path}`);
}
@@ -112,7 +114,7 @@ const fixture = spawnSync(
{
cwd: ROOT,
encoding: "utf8",
- env: { ...process.env, PLIEGOCSS_KEEP_LSP_FIXTURE: "1" },
+ env: { ...cargoEnvironment, PLIEGOCSS_KEEP_LSP_FIXTURE: "1" },
},
);
if (fixture.error || fixture.status !== 0) {
@@ -133,7 +135,6 @@ try {
PLIEGOCSS_NEOVIM_WORKSPACE: workspace,
PLIEGOCSS_NEOVIM_OUTPUT: output,
PLIEGOCSS_NEOVIM_LSP: lsp,
- PLIEGOCSS_NEOVIM_COMPILER: compiler,
},
timeout: 60_000,
},
diff --git a/scripts/check-packages.mjs b/scripts/check-packages.mjs
index a842f56..74d2527 100644
--- a/scripts/check-packages.mjs
+++ b/scripts/check-packages.mjs
@@ -13,6 +13,7 @@ import {
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
+import { isolatedCargoEnvironment } from "./rust-target.mjs";
const ROOT = resolve(import.meta.dirname, "..");
const EXPECTED_ORDER = [
@@ -51,6 +52,7 @@ const PUBLIC_API_FIXTURE_FILES = [
"src/main.rs",
];
const PACKAGE_TOOLCHAIN = "1.96.0";
+const MSRV_TOOLCHAIN = "1.85.0";
const EXPECTED_REPOSITORY = "https://github.com/celiumsai/pliegocss";
const EXPECTED_HOMEPAGE = "https://pliegocss.dev";
const EXPECTED_REGISTRY = "crates-io";
@@ -58,14 +60,23 @@ const EXPECTED_MSRV = "1.85";
const MAX_COMPRESSED_ARCHIVE_BYTES = 72 * 1024;
const EXPECTED_LICENSE_SHA256 =
"8ada45cd9f843acf64e4722ae262c622a2b3b3007c7310ef36ac1061a30f6adb";
+const packageCargoEnvironment = isolatedCargoEnvironment(ROOT, {
+ env: process.env,
+ toolchain: PACKAGE_TOOLCHAIN,
+});
+const msrvCargoEnvironment = isolatedCargoEnvironment(ROOT, {
+ env: process.env,
+ toolchain: MSRV_TOOLCHAIN,
+});
function fail(message) {
throw new Error(message);
}
-function run(command, args, { capture = false, cwd = ROOT } = {}) {
+function run(command, args, { capture = false, cwd = ROOT, env = process.env } = {}) {
const result = spawnSync(command, args, {
cwd,
+ env,
encoding: "utf8",
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
});
@@ -85,7 +96,10 @@ function run(command, args, { capture = false, cwd = ROOT } = {}) {
}
function runPackageCargo(args, options = {}) {
- return run("cargo", [`+${PACKAGE_TOOLCHAIN}`, ...args], options);
+ return run("cargo", [`+${PACKAGE_TOOLCHAIN}`, ...args], {
+ ...options,
+ env: packageCargoEnvironment,
+ });
}
function dependencyContract(dependency) {
@@ -616,13 +630,14 @@ try {
run("cargo", ["+1.85.0", "generate-lockfile"], {
cwd: downstreamRoot,
capture: true,
+ env: msrvCargoEnvironment,
});
const downstreamMetadata = JSON.parse(
run(
"cargo",
["+1.85.0", "metadata", "--format-version", "1", "--all-features", "--locked"],
- { cwd: downstreamRoot, capture: true },
+ { cwd: downstreamRoot, capture: true, env: msrvCargoEnvironment },
),
);
if (canonical(downstreamMetadata.workspace_root) !== canonical(downstreamRoot)) {
@@ -766,12 +781,12 @@ try {
run(
"cargo",
["+1.85.0", "check", "--release", "--all-targets", "--all-features", "--locked"],
- { cwd: downstreamRoot },
+ { cwd: downstreamRoot, env: msrvCargoEnvironment },
);
const dtcgDownstreamRun = run(
"cargo",
["+1.85.0", "run", "--release", "--quiet", "--locked"],
- { cwd: downstreamRoot, capture: true },
+ { cwd: downstreamRoot, capture: true, env: msrvCargoEnvironment },
).trim();
if (!/^[0-9a-f]{32}\tpc_[0-9a-z]+$/u.test(dtcgDownstreamRun)) {
fail(
@@ -783,7 +798,7 @@ try {
const tomlDownstreamRun = run(
"cargo",
["+1.85.0", "run", "--release", "--quiet", "--locked"],
- { cwd: downstreamRoot, capture: true },
+ { cwd: downstreamRoot, capture: true, env: msrvCargoEnvironment },
).trim();
if (!/^[0-9a-f]{32}\tpc_[0-9a-z]+$/u.test(tomlDownstreamRun)) {
fail(
diff --git a/scripts/check-parallel-determinism.mjs b/scripts/check-parallel-determinism.mjs
index 2a8cfcb..1610b7c 100644
--- a/scripts/check-parallel-determinism.mjs
+++ b/scripts/check-parallel-determinism.mjs
@@ -11,8 +11,15 @@ import {
} from "node:fs";
import { basename, dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
+import { cargoTargetRoot, isolatedCargoEnvironment } from "./rust-target.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const rustToolchain = process.env.PLIEGOCSS_RUST_TOOLCHAIN ?? "1.85.0";
+const cargoEnvironment = isolatedCargoEnvironment(root, {
+ env: process.env,
+ toolchain: rustToolchain,
+});
+const cargoTarget = cargoTargetRoot(root, cargoEnvironment);
const runtime = join(root, "target", "parallel-determinism", `run-${process.pid}-${Date.now()}`);
const project = join(runtime, "project");
const workers = 8;
@@ -42,7 +49,7 @@ function run(program, args, options = {}) {
const result = spawnSync(program, args, {
cwd: root,
encoding: "utf8",
- env: { ...process.env },
+ env: cargoEnvironment,
maxBuffer: 16 * 1024 * 1024,
timeout: 180_000,
windowsHide: true,
@@ -62,7 +69,7 @@ function runAsync(program, args, options = {}) {
return new Promise((resolvePromise, reject) => {
const child = spawn(program, args, {
cwd: project,
- env: { ...process.env },
+ env: cargoEnvironment,
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
...options,
@@ -97,10 +104,9 @@ function resolveExecutable() {
assert(existsSync(configured), `PLIEGO_CSSC does not exist: ${configured}`);
return configured;
}
- run("cargo", ["+1.85.0", "build", "--locked", "-p", "pliego-cssc"]);
+ run("cargo", [`+${rustToolchain}`, "build", "--locked", "-p", "pliego-cssc"]);
const executable = join(
- root,
- "target",
+ cargoTarget,
"debug",
process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc",
);
@@ -115,17 +121,14 @@ function resolveLockHolder() {
return configured;
}
run("cargo", [
- "+1.85.0",
+ `+${rustToolchain}`,
"build",
"--locked",
"--manifest-path",
"integration-tests/publication-lock-holder/Cargo.toml",
- "--target-dir",
- "target",
]);
const executable = join(
- root,
- "target",
+ cargoTarget,
"debug",
process.platform === "win32"
? "pliego-publication-lock-holder.exe"
@@ -462,6 +465,7 @@ async function main() {
JSON.stringify(
{
schemaVersion: 1,
+ rustToolchain,
workersPerCohort: workers,
maximumConcurrentCompilerProcesses: workers,
referenceProcesses: profiles.length,
diff --git a/scripts/check-physical-trace.mjs b/scripts/check-physical-trace.mjs
index d7f80d5..89cd875 100644
--- a/scripts/check-physical-trace.mjs
+++ b/scripts/check-physical-trace.mjs
@@ -12,16 +12,25 @@ import {
} from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
+import { cargoTargetRoot, isolatedCargoEnvironment } from "./rust-target.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const fixture = join(root, "integration-tests", "physical-trace");
const runtime = join(root, "target", "physical-trace", `run-${process.pid}-${Date.now()}`);
const project = join(runtime, "project");
-const targetDir = join(root, "target");
+const cargoEnvironment = isolatedCargoEnvironment(root, {
+ env: process.env,
+ toolchain: "1.85.0",
+});
+const targetDir = cargoTargetRoot(root, cargoEnvironment);
const reachabilityName = "pliego.reachability.json";
const expectedCss = readFileSync(join(fixture, "expected.css"));
const expectedManifest = readFileSync(join(fixture, "expected.manifest.json"));
const updateGoldens = process.env.PLIEGOCSS_UPDATE_GOLDENS === "1";
+const cargoBuildTimeout = Number.parseInt(
+ process.env.PLIEGOCSS_CARGO_BUILD_TIMEOUT_MS ?? "600000",
+ 10,
+);
const physicalEdgeKinds = new Set([
"declarationContributesToPhysicalDeclaration",
@@ -80,7 +89,7 @@ function run(program, args, { cwd = root, expectedStatus = 0, timeout = 30_000 }
const result = spawnSync(program, args, {
cwd,
encoding: "utf8",
- env: { ...process.env, CARGO_TARGET_DIR: targetDir },
+ env: cargoEnvironment,
maxBuffer: 16 * 1024 * 1024,
timeout,
windowsHide: true,
@@ -102,8 +111,8 @@ function resolveExecutable() {
return configured;
}
- run("cargo", ["+1.85", "build", "--locked", "-p", "pliego-cssc"], {
- timeout: 180_000,
+ run("cargo", ["+1.85.0", "build", "--locked", "-p", "pliego-cssc"], {
+ timeout: cargoBuildTimeout,
});
const executable = join(
targetDir,
@@ -295,7 +304,7 @@ async function assertWatchContract(executable) {
],
{
cwd: project,
- env: { ...process.env, CARGO_TARGET_DIR: targetDir },
+ env: cargoEnvironment,
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
},
@@ -448,7 +457,7 @@ function assertManifest(manifest, css, schemaVersion, targets, format) {
assert(manifest.schemaVersion === schemaVersion, `expected manifest schema ${schemaVersion}`);
assert(manifest.styleIdFormatVersion === 2, "StyleId format version drifted");
assert(manifest.classNameFormatVersion === 1, "class-name format version drifted");
- assert(manifest.themeIdFormatVersion === 2, "theme ID format version drifted");
+ assert(manifest.themeIdFormatVersion === 3, "theme ID format version drifted");
assert(/^[0-9a-f]{32}$/.test(manifest.themeId), "ThemeId is not fixed-width hex");
assert(manifest.targets === targets, `expected target contract ${targets}`);
assert(manifest.format === format, `expected CSS format ${format}`);
diff --git a/scripts/check-plain-html-integration.mjs b/scripts/check-plain-html-integration.mjs
index 93d3535..f13e326 100644
--- a/scripts/check-plain-html-integration.mjs
+++ b/scripts/check-plain-html-integration.mjs
@@ -12,8 +12,13 @@ import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { basename, extname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
+import { isolatedCargoEnvironment } from "./rust-target.mjs";
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
+const cargoEnvironment = isolatedCargoEnvironment(root, {
+ env: process.env,
+ toolchain: "1.85.0",
+});
const fixtureRoot = join(root, "integration-tests", "plain-html-smoke");
const adapterPath = join(fixtureRoot, "adapter.json");
@@ -140,7 +145,7 @@ function runCompiler(adapter, outputRoot) {
"--manifest-version",
"3",
],
- { cwd: root, encoding: "utf8", windowsHide: true },
+ { cwd: root, env: cargoEnvironment, encoding: "utf8", windowsHide: true },
);
if (result.error) {
throw result.error;
diff --git a/scripts/check-pliegors-integration.mjs b/scripts/check-pliegors-integration.mjs
index 341ec07..88a2ea5 100644
--- a/scripts/check-pliegors-integration.mjs
+++ b/scripts/check-pliegors-integration.mjs
@@ -19,6 +19,7 @@ import {
resolve,
} from "node:path";
import { fileURLToPath } from "node:url";
+import { isolatedCargoEnvironment } from "./rust-target.mjs";
import { gzipSync } from "node:zlib";
import { verifyPliegorsContract } from "./pliegors-contract.mjs";
@@ -124,16 +125,17 @@ mkdirSync(generatedRoot);
mkdirSync(bundlesPath);
mkdirSync(allCompiledBundlesPath);
-const environment = {
- ...process.env,
+const targetBaseEnvironment = process.env.PLIEGOCSS_CARGO_TARGET_DIR
+ ? { ...process.env, CARGO_TARGET_DIR: resolve(process.env.PLIEGOCSS_CARGO_TARGET_DIR) }
+ : process.env;
+const environment = isolatedCargoEnvironment(root, {
+ env: targetBaseEnvironment,
+ toolchain: "1.86.0",
// Keep native executables below the workspace target root. Windows Application
// Control policies commonly trust Cargo's project target but reject binaries
// from disposable benchmark directories. WSL callers can opt into a native
// Linux target directory without weakening the default Windows boundary.
- CARGO_TARGET_DIR: process.env.PLIEGOCSS_CARGO_TARGET_DIR
- ? resolve(process.env.PLIEGOCSS_CARGO_TARGET_DIR)
- : join(root, "target"),
-};
+});
// `pliego build` deliberately rejects environment-level compiler/profile overrides. The harness
// keeps only CARGO_TARGET_DIR as the explicit generated-output location and strips inherited
// overrides before any Cargo child is launched.
@@ -524,7 +526,7 @@ if (
manifest.schemaVersion !== 3 ||
manifest.styleIdFormatVersion !== 2 ||
manifest.classNameFormatVersion !== 1 ||
- manifest.themeIdFormatVersion !== 2 ||
+ manifest.themeIdFormatVersion !== 3 ||
manifest.cssSha256 !== cssSha256 ||
manifest.cssBytes !== Buffer.byteLength(css)
) {
@@ -668,7 +670,7 @@ for (const bundle of bundles) {
bundle.manifestDocument.schemaVersion !== 5 ||
bundle.manifestDocument.styleIdFormatVersion !== 2 ||
bundle.manifestDocument.classNameFormatVersion !== 1 ||
- bundle.manifestDocument.themeIdFormatVersion !== 2 ||
+ bundle.manifestDocument.themeIdFormatVersion !== 3 ||
bundle.manifestDocument.targets !== "modern" ||
bundle.manifestDocument.format !== "minified" ||
bundle.manifestDocument.cssSha256 !== digest ||
diff --git a/scripts/check-portability.mjs b/scripts/check-portability.mjs
index 716bcf1..8300f25 100644
--- a/scripts/check-portability.mjs
+++ b/scripts/check-portability.mjs
@@ -39,9 +39,9 @@ const foreignCwd = join(runtime, "foreign-cwd");
const sourceName = "café.rs";
const logicalSource = `src/${sourceName}`;
const expectedCssSha256 =
- "acdb0bfee613ddb3fdae0feb9a8e5118dc5f74d385ed53eb610ff4eefd05d454";
+ "65dbbcff74b6886fdd749b1c7c0587eaff06b4f4720405e10716c2b0dfee9bbc";
const expectedManifestSha256 =
- "8c1dc690c2cbb8102b6935a970e60938bbda7e50a0c0bf766c5c800305cce598";
+ "453f708c9c4edbd388eda1ee9c883212cd5d31421077d123cb1d32bc0b28d4bb";
const expectedControlHashes = {
"pliego.css.findings.json":
"a353b52f386f0b896db3c354f45bc0b304dffab1477904a5ee37cd8768be468a",
@@ -51,84 +51,84 @@ const expectedControlHashes = {
"81777f9533bb6cd203f68e0f052e5974d09a2d773d7adf8f062f9361100a1c28",
};
const expectedBundleControlHashes = {
- "app.css": "acdb0bfee613ddb3fdae0feb9a8e5118dc5f74d385ed53eb610ff4eefd05d454",
+ "app.css": "65dbbcff74b6886fdd749b1c7c0587eaff06b4f4720405e10716c2b0dfee9bbc",
"app.css.map":
"39a03b016583b0827ec8bc92a2a1677dcb4d151324dcdd9c77d303af29b1dddf",
"app.manifest.json":
- "4328616cd9a2a9e57add1cd6b3aaa735cc8a2d7aa2721c8a2e591ecc0b2bba2c",
+ "0b17997d04739e7698300d9d13278ecd537b2821e7e123db00f83e4a80d3d609",
"pliego.assets.json":
- "f24ee1ccb288a3e9243d686caa4f0cc1e83f6d12021a7715cee9cd5eff0dce66",
+ "9664a30c61b226d6f81fc771c0042206e9a98a64a77429d7e05498b78f194d89",
"pliego.css.findings.json":
- "2fcc03aff9f36f09095ef046019b8fbfdd2d9af658adf259132a2c761c557cc5",
+ "99a2101ad94c358f743b3df1a1214efe8a7716a9006226e43b74141039804e59",
"pliego.css.manifest.json":
- "4ac39f4ec4035e4cdf337f900f23eea44b8a52abf951750b3d02e4bd4bd1d76a",
+ "e75b481bd2755a70d5c2ac1030729633cb96d28d8107e562b55ae0951913ba12",
"pliego.css.receipt.json":
- "5776c810e022e391505b3efd441b9f3b0219dab32af6d4bc6f5dc4222c8a3ac0",
+ "6fe898d54ad8c745d63d1e6cdd8d31ae6c157130ad4d047542ba6f3cb1f4b5cf",
"pliego.index.json":
- "4338ee784241b023b9bbb2b4959187d6f9dddaa75c7c6cdf021ec60346bc1393",
+ "b685bbaaa08bfac33f9024f96a8a6c8338771ed4d019fe7c01db602ea40a1864",
"pliego.tokens.json":
- "0a45f1d6e48280ee7b357e2e4d58d82bfc27be715c9fd3f3385098608edd4e42",
+ "3298873e66ade310459593530c86419c05a973d2404b71882c2d0ae430030a28",
};
const expectedAssetPlanControlHashes = {
"pliego.css.findings.json":
- "3faa43be937eb688dddeb8c72be3a7e3257fc34a0643c4a5b6de51844e03b0ef",
+ "bd9fa74558e57f08cc9bdff0bf6f6fa6502c483487451652795353a14dab579d",
"pliego.css.manifest.json":
- "cf15c3f7fc738c1058eaa2471cfb942e62969b7aed9e5cdad599ab7522394488",
+ "f254eebbfc716351308f56f34d6494e4317866965cbea6d4bf6b27922f0b09ea",
"pliego.css.receipt.json":
- "d76be0703b140e636ebcc2dacec079e8c5cf893139a317e7429bdce4d3b92cea",
+ "d79e5cca007ee6af62cbafaa36532503860e6243f7d534398d27a1bde4e40dce",
};
const expectedCompileControlHashes = {
- "app.css": "acdb0bfee613ddb3fdae0feb9a8e5118dc5f74d385ed53eb610ff4eefd05d454",
+ "app.css": "65dbbcff74b6886fdd749b1c7c0587eaff06b4f4720405e10716c2b0dfee9bbc",
"app.css.map":
"39a03b016583b0827ec8bc92a2a1677dcb4d151324dcdd9c77d303af29b1dddf",
"app.manifest.json":
- "8c1dc690c2cbb8102b6935a970e60938bbda7e50a0c0bf766c5c800305cce598",
+ "453f708c9c4edbd388eda1ee9c883212cd5d31421077d123cb1d32bc0b28d4bb",
"pliego.css.findings.json":
- "6e75872fa833f550501f433a7be89bf1b90cf97bf52dc130169851a0c9ea2c52",
+ "f0bef2cd675d1bf1f4a759c851d9a8be6e4617fdaaa443c00fd3696278691437",
"pliego.css.manifest.json":
- "9a98a8a78cb9f2597bfa2970a646c0ba4af99838a08e9063e3da85f4176a7cd2",
+ "2ab00fa0f7b8a06e7bff3780fdc06d61b26b7136e163f8e6437907ddb013343e",
"pliego.css.receipt.json":
- "a4d9f387b460b650d7c9b904b0f54721f9915c1ccea5753a41b646c503923aa7",
+ "3dfb3de854d95b39c7f69325f9eae71b37e62607f9c4e367e8188a63f848da70",
"pliego.tokens.json":
- "0a45f1d6e48280ee7b357e2e4d58d82bfc27be715c9fd3f3385098608edd4e42",
+ "3298873e66ade310459593530c86419c05a973d2404b71882c2d0ae430030a28",
};
const expectedWatchControlHashes = {
- "app.css": "acdb0bfee613ddb3fdae0feb9a8e5118dc5f74d385ed53eb610ff4eefd05d454",
+ "app.css": "65dbbcff74b6886fdd749b1c7c0587eaff06b4f4720405e10716c2b0dfee9bbc",
"app.css.map":
"39a03b016583b0827ec8bc92a2a1677dcb4d151324dcdd9c77d303af29b1dddf",
"app.manifest.json":
- "8c1dc690c2cbb8102b6935a970e60938bbda7e50a0c0bf766c5c800305cce598",
+ "453f708c9c4edbd388eda1ee9c883212cd5d31421077d123cb1d32bc0b28d4bb",
"pliego.css.findings.json":
- "3affbc8e71ebb3096ce896e8dbfdff84073dfcc8b6eb8509431d768442545dad",
+ "52ccd05b7c6f39520c474a72a5b3e9a6eaa34b7e79fa8f05a759dfacf1ce2852",
"pliego.css.manifest.json":
- "15831a792506ecf7b97e01907b7fdf1d1e4e2ccf7d90a1c2af105668adab3ed1",
+ "537de8064740008b879872d77abfd767352c3a75474b72f79a7d95aab3982c85",
"pliego.css.receipt.json":
- "dc2532037337cfe285ff26162a7b0d404307d2d3c3a159ac95cf4862b9586b29",
+ "602c062c391c030112356f996eb5bec251757cdf5ffb59b0f299b575dc869a49",
"pliego.tokens.json":
- "0a45f1d6e48280ee7b357e2e4d58d82bfc27be715c9fd3f3385098608edd4e42",
+ "3298873e66ade310459593530c86419c05a973d2404b71882c2d0ae430030a28",
};
const expectedDtcgControlHashes = {
- "app.css": "5991b204608619242608e12e9b60ffad378c6ab185b91ca173260d9adf9dfdc3",
+ "app.css": "9a349f5059ffb3ace6c985dfe1fadcd9cb9ac45668b5cd0c00e26625e5a6c6c3",
"app.css.map":
"ce30a183a990c8dca861721b4c5ea953884fd4fb4898220023192e20abc62c3c",
"app.manifest.json":
- "b402c1804da5ac08d31000b8ecec1924b6c4955e6535d538c50fc4bd030ff8f2",
+ "9ed48b83a35dc176305c7761938435e9a0aa5e3c15907e3c3d80de1b248bad76",
"pliego.css.findings.json":
- "914d8463aa4e7abe3e7257879e0ecdb5df9605361cc803fa3a5e113ea2b6d00f",
+ "1915a6ca985813c148b7c8ad4724b3061d26790b2b3bc301a9cf9e029422cb2c",
"pliego.css.manifest.json":
- "991801df2b11ad17e2adc5003e4e49d13d1c8050f829ab32c99b71965b79b924",
+ "96a9fb6769b6ba1260e4581528c834d248a7dcb52e9a9be616ed1edfdb686c84",
"pliego.css.receipt.json":
- "b40d88514e19892b08b6478b94bce582b08dea8fa1d5eed26cfa47f40871af9a",
+ "62968ba59c63c76ef01c73a8091978c30855ba8d39cf10f6717332786086546d",
"pliego.tokens.json":
- "59422650b57f45d5601a0dadce3ab1e9fc9bbf7e3c35d089f203516cf1c9a553",
+ "8659f9c443cb5c77c399fd741a27d476ec18887742c5459002fa6b82c22e864a",
};
const expectedDtcgResolverSha256 =
"5965f868707ad92b0164788b5a7b8b8a2234ce03d7e6c71e1df66d612801b876";
const expectedDtcgConfigHashes = {
darkLight:
- "sha256:1aa3a252d70b115f68861b96d8ecd9b485fdd62ad6408777d00ae46c2893d584",
+ "sha256:e6ce8e2578b2bbb68c0b533495dab2a781e5c7a32665d4c4a4845146f014ba3e",
darkDark:
- "sha256:e0c65ea08fe97f14b426058a23e4b42d31047afec100fb4164c41798f65f532e",
+ "sha256:5bab41c2b1a8d06d05fbaae1517d425a35b0bb8143fcdaeaee3c5faf48d9469e",
};
const updateGoldens = process.env.PLIEGOCSS_UPDATE_GOLDENS === "1";
@@ -619,7 +619,7 @@ if (
manifest.schemaVersion !== 3 ||
manifest.styleIdFormatVersion !== 2 ||
manifest.classNameFormatVersion !== 1 ||
- manifest.themeIdFormatVersion !== 2 ||
+ manifest.themeIdFormatVersion !== 3 ||
manifest.cssBytes !== firstCss.byteLength ||
manifest.cssSha256 !== sha256(firstCss) ||
manifest.styles.length !== 1
@@ -996,7 +996,7 @@ const dtcgCss = readFileSync(join(dtcgControlOutput, "app.css"));
if (
dtcgControlManifest.inputs.configHash !==
expectedDtcgConfigHashes.darkLight ||
- dtcgStyleManifest.themeId !== "a3a372ca8c3407a68302c8a71e89b121" ||
+ dtcgStyleManifest.themeId !== "28acd9cd56861d1aea635ce531e240f5" ||
!dtcgCss.toString("utf8").includes("--color-brand:color(srgb .1 .1 .1)")
) {
if (updateGoldens) {
diff --git a/scripts/check-public-api.mjs b/scripts/check-public-api.mjs
index 7563128..6e5c2b0 100644
--- a/scripts/check-public-api.mjs
+++ b/scripts/check-public-api.mjs
@@ -4,9 +4,18 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
+import { isolatedCargoEnvironment } from "./rust-target.mjs";
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const root = resolve(scriptDirectory, "..");
+const releaseCargoEnvironment = isolatedCargoEnvironment(root, {
+ env: process.env,
+ toolchain: "1.96.0",
+});
+const msrvCargoEnvironment = isolatedCargoEnvironment(root, {
+ env: process.env,
+ toolchain: "1.85.0",
+});
const fixtureRoot = join(root, "integration-tests", "public-api-smoke");
const fixtureManifest = join(fixtureRoot, "Cargo.toml");
const fixtureSource = join(fixtureRoot, "src");
@@ -53,9 +62,9 @@ function assertIdentityVersions(document, schema, role) {
document.schemaVersion !== schema ||
document.styleIdFormatVersion !== 2 ||
document.classNameFormatVersion !== 1 ||
- document.themeIdFormatVersion !== 2
+ document.themeIdFormatVersion !== 3
) {
- fail(`${role} does not expose schema ${schema} with identity formats 2/1/2`);
+ fail(`${role} does not expose schema ${schema} with identity formats 2/1/3`);
}
}
@@ -90,7 +99,7 @@ const cliBuild = run("cargo", [
"--message-format=json-render-diagnostics",
"-p",
"pliego-cssc",
-]);
+], { env: releaseCargoEnvironment });
let executable;
for (const line of cliBuild.stdout.split(/\r?\n/u).filter(Boolean)) {
let message;
@@ -112,7 +121,6 @@ if (!executable) {
fail("Cargo did not report the pliego-cssc executable it built");
}
-const fixtureTarget = join(root, "target", "public-api-smoke");
const fixtureRun = run(
"cargo",
[
@@ -126,7 +134,7 @@ const fixtureRun = run(
"--manifest-path",
fixtureManifest,
],
- { env: { ...process.env, CARGO_TARGET_DIR: fixtureTarget } },
+ { env: msrvCargoEnvironment },
);
const publicLine = fixtureRun.stdout.trim();
const publicMatch = /^([0-9a-f]{32})\t(pc_[0-9a-z]+)$/u.exec(publicLine);
@@ -199,14 +207,14 @@ try {
{ cwd: workspace },
).stdout,
);
- assertIdentityVersions(inspection, 2, "inspection");
+ assertIdentityVersions(inspection, 3, "inspection");
const catalog = JSON.parse(
run(executable, ["catalog", "--config", fixtureTheme, "--format", "json"], {
cwd: workspace,
}).stdout,
);
- assertIdentityVersions(catalog, 3, "catalog");
+ assertIdentityVersions(catalog, 4, "catalog");
const explanation = JSON.parse(
run(
@@ -243,7 +251,7 @@ try {
inspectionSchema: inspection.schemaVersion,
catalogSchema: catalog.schemaVersion,
explainSchema: explanation.schemaVersion,
- identityFormats: [2, 1, 2],
+ identityFormats: [2, 1, 3],
renamedFacadeDependency: true,
dtcgBuildMacro: true,
tomlRegistryConvergence: true,
diff --git a/scripts/check-release-readiness.mjs b/scripts/check-release-readiness.mjs
index ade369d..0522d41 100644
--- a/scripts/check-release-readiness.mjs
+++ b/scripts/check-release-readiness.mjs
@@ -1,90 +1,37 @@
-import { existsSync, readFileSync } from "node:fs";
+import { readFileSync } from "node:fs";
import { resolve } from "node:path";
+import { AuthorityError, validateReleaseReadiness } from "./release-authority.mjs";
const root = resolve(import.meta.dirname, "..");
-const path = resolve(root, "docs/product/release-readiness-0.1.0.json");
-if (!existsSync(path)) throw new Error("release readiness document is missing");
-
-const value = JSON.parse(readFileSync(path, "utf8"));
-const fail = (message) => {
- throw new Error(`release readiness: ${message}`);
-};
-const requiredChecks = new Set([
- "targeted-corrections",
- "brand-system",
- "fast-profile-current-source",
- "pliegors-current-contract",
- "browser-current-source",
- "package-replay-current-source",
- "benchmark-evidence-current-source",
- "supply-chain-policy",
- "website-current-source",
- "hosted-cross-os-current-source",
- "registry-replay",
- "final-promotion-authorization",
-]);
-const allowedStatuses = new Set([
- "passed",
- "pending",
- "blocked",
- "not-authorized",
- "not-applicable",
-]);
-const allowedEvidence = new Set(["measured", "inherited", "pending", "uncertain"]);
-
-if (
- value.schemaVersion !== 2 ||
- value.targetVersion !== "0.1.0" ||
- value.candidateVersion !== "0.1.0-rc.2"
-) {
- fail("header drifted");
-}
-if (value.repository !== "https://github.com/celiumsai/pliegocss") {
- fail("repository drifted");
-}
+const args = process.argv.slice(2);
+const enforceRelease =
+ (args.length === 1 && args[0] === "--profile=release") ||
+ (args.length === 2 && args[0] === "--profile" && args[1] === "release");
if (
- value.repositoryVisibility !== "public" ||
- value.productStage !== "public-preview"
+ !enforceRelease &&
+ !(
+ args.length === 0 ||
+ (args.length === 1 && args[0] === "--validate")
+ )
) {
- fail("repository visibility and public-preview product stage drifted");
+ throw new Error(`release readiness: unknown mode ${args.join(" ")}`);
}
-if (!Array.isArray(value.checks)) fail("checks must be an array");
-const ids = value.checks.map((check) => check.id);
-if (new Set(ids).size !== ids.length) fail("check identifiers must be unique");
-for (const id of requiredChecks) {
- if (!ids.includes(id)) fail(`missing required check ${id}`);
-}
-for (const check of value.checks) {
- if (!allowedStatuses.has(check.status)) fail(`invalid status for ${check.id}`);
- if (!allowedEvidence.has(check.evidenceClass)) {
- fail(`invalid evidence class for ${check.id}`);
- }
- if (check.status === "passed" && check.evidenceClass === "pending") {
- fail(`${check.id} cannot pass on pending evidence`);
+
+try {
+ const value = JSON.parse(
+ readFileSync(resolve(root, "docs/product/release-readiness-0.1.0.json"), "utf8"),
+ );
+ const summary = validateReleaseReadiness(value, { root });
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
+ if (enforceRelease && summary.result !== "ready") {
+ process.stderr.write(
+ `release readiness: promotion blocked by ${summary.blockers.join(", ")}\n`,
+ );
+ process.exitCode = 1;
}
- if (!check.summary || typeof check.summary !== "string") {
- fail(`${check.id} must include a summary`);
+} catch (error) {
+ if (error instanceof AuthorityError) {
+ throw new Error(`release readiness: ${error.message}`);
}
+ throw error;
}
-const blocking = value.checks.filter(
- (check) => check.requiredForPromotion && check.status !== "passed",
-);
-const derivedResult = blocking.length === 0 ? "ready" : "blocked";
-if (value.result !== derivedResult) fail("result does not match required checks");
-if (!value.promotionRule || typeof value.promotionRule !== "string") {
- fail("promotion rule is missing");
-}
-
-process.stdout.write(
- `${JSON.stringify(
- {
- schemaVersion: value.schemaVersion,
- candidate: value.candidateVersion,
- result: value.result,
- passed: value.checks.filter((check) => check.status === "passed").length,
- blocking: blocking.map((check) => check.id),
- },
- null,
- 2,
- )}\n`,
-);
diff --git a/scripts/check-release.mjs b/scripts/check-release.mjs
index bef9983..c326812 100644
--- a/scripts/check-release.mjs
+++ b/scripts/check-release.mjs
@@ -1,6 +1,7 @@
import { spawnSync } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
+import { isolatedCargoEnvironment } from "./rust-target.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -23,6 +24,24 @@ const gates = Object.freeze([
command: "cargo",
args: ["check", "--workspace", "--all-targets", "--locked"],
},
+ {
+ id: "engine-boundary",
+ tier: "fast",
+ command: "pnpm",
+ args: ["check:engine-boundary"],
+ },
+ {
+ id: "benchmark-authority-v2",
+ tier: "fast",
+ command: "pnpm",
+ args: ["check:benchmark-authority"],
+ },
+ {
+ id: "browser-output-authority",
+ tier: "fast",
+ command: "pnpm",
+ args: ["check:browser-output-authority"],
+ },
{
id: "rust-tests",
tier: "fast",
@@ -101,16 +120,34 @@ const gates = Object.freeze([
args: ["test", "-p", "pliego-css-usage", "--test", "generic_css_usage", "--locked"],
},
{
- id: "release-readiness",
+ id: "release-authority-contract-tests",
tier: "fast",
command: "node",
- args: ["scripts/check-release-readiness.mjs"],
+ args: ["scripts/release-authority.test.mjs"],
},
{
- id: "hosted-browser-matrix",
+ id: "release-readiness-contract",
+ tier: "fast",
+ command: "node",
+ args: ["scripts/check-release-readiness.mjs", "--validate"],
+ },
+ {
+ id: "hosted-browser-matrix-contract",
tier: "fast",
command: "node",
- args: ["scripts/check-hosted-browser-matrix.mjs"],
+ args: ["scripts/check-hosted-browser-matrix.mjs", "--validate"],
+ },
+ {
+ id: "document-authority",
+ tier: "fast",
+ command: "node",
+ args: ["scripts/check-document-authority.mjs"],
+ },
+ {
+ id: "site-markdown-docs",
+ tier: "fast",
+ command: "pnpm",
+ args: ["check:site-docs"],
},
{
id: "getting-started",
@@ -118,6 +155,12 @@ const gates = Object.freeze([
command: "node",
args: ["scripts/check-getting-started.mjs"],
},
+ {
+ id: "benchmark-authority-smoke",
+ tier: "integration",
+ command: "pnpm",
+ args: ["check:benchmark-smoke"],
+ },
{
id: "standards-provenance",
tier: "fast",
@@ -233,12 +276,25 @@ const gates = Object.freeze([
requires: ["PLIEGORS_ROOT", "PLIEGOCSS_RUN_BROWSERS"],
},
+ {
+ id: "release-readiness",
+ tier: "release",
+ command: "node",
+ args: ["scripts/check-release-readiness.mjs", "--profile=release"],
+ },
+ {
+ id: "hosted-browser-matrix",
+ tier: "release",
+ command: "node",
+ args: ["scripts/check-hosted-browser-matrix.mjs", "--profile=release"],
+ },
{ id: "public-api", tier: "release", command: "pnpm", args: ["check:api"] },
{
id: "properties",
tier: "release",
command: "pnpm",
args: ["check:properties"],
+ rustToolchain: "1.85.0",
},
{
id: "determinism",
@@ -259,6 +315,12 @@ const gates = Object.freeze([
command: "pnpm",
args: ["check:evidence"],
},
+ {
+ id: "benchmark-oracle-live",
+ tier: "release",
+ command: "pnpm",
+ args: ["check:benchmark-oracle"],
+ },
{
id: "media-query-merge",
tier: "release",
@@ -337,17 +399,18 @@ function listedGate(gate) {
tier: gate.tier,
command: [gate.command, ...gate.args],
requires: gate.requires ?? [],
+ rustToolchain: gate.rustToolchain ?? "active",
};
}
-function spawnGate(gate) {
+function spawnGate(gate, environment) {
if (process.platform === "win32" && gate.command === "pnpm") {
return spawnSync(
process.env.ComSpec ?? "cmd.exe",
["/d", "/s", "/c", gate.command, ...gate.args],
{
cwd: root,
- env: process.env,
+ env: environment,
encoding: "utf8",
stdio: "inherit",
windowsHide: true,
@@ -356,7 +419,7 @@ function spawnGate(gate) {
}
return spawnSync(gate.command, gate.args, {
cwd: root,
- env: process.env,
+ env: environment,
encoding: "utf8",
stdio: "inherit",
windowsHide: true,
@@ -371,6 +434,7 @@ function run(profile) {
}
const results = [];
+ const targetEnvironments = new Map();
for (const gate of selected.gates) {
const missing = (gate.requires ?? []).filter((name) => !process.env[name]);
if (missing.length > 0) {
@@ -380,8 +444,18 @@ function run(profile) {
);
continue;
}
+ const toolchain = gate.rustToolchain ?? "active";
+ let environment = targetEnvironments.get(toolchain);
+ if (!environment) {
+ environment = isolatedCargoEnvironment(root, {
+ env: process.env,
+ toolchain: gate.rustToolchain,
+ });
+ targetEnvironments.set(toolchain, environment);
+ process.stdout.write(`[target] ${toolchain}: ${environment.CARGO_TARGET_DIR}\n`);
+ }
process.stdout.write(`[running] ${gate.id}\n`);
- const result = spawnGate(gate);
+ const result = spawnGate(gate, environment);
if (result.error) {
results.push({
id: gate.id,
diff --git a/scripts/check-release.test.mjs b/scripts/check-release.test.mjs
index 5d72595..d9b66fc 100644
--- a/scripts/check-release.test.mjs
+++ b/scripts/check-release.test.mjs
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
+import { targetDirectoryForRustcIdentity } from "./rust-target.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const script = resolve(root, "scripts", "check-release.mjs");
@@ -33,6 +34,14 @@ assert.deepEqual(fast.includes, ["fast"]);
assert(fast.gates.some((gate) => gate.id === "rust-clippy"));
assert(fast.gates.some((gate) => gate.id === "standards-provenance"));
assert(fast.gates.some((gate) => gate.id === "brand-system"));
+assert(fast.gates.some((gate) => gate.id === "release-authority-contract-tests"));
+assert(fast.gates.some((gate) => gate.id === "release-readiness-contract"));
+assert(fast.gates.some((gate) => gate.id === "hosted-browser-matrix-contract"));
+assert(fast.gates.some((gate) => gate.id === "document-authority"));
+assert(fast.gates.some((gate) => gate.id === "site-markdown-docs"));
+assert(fast.gates.some((gate) => gate.id === "benchmark-authority-v2"));
+assert(fast.gates.some((gate) => gate.id === "browser-output-authority"));
+assert(!fast.gates.some((gate) => gate.id === "release-readiness"));
assert(!fast.gates.some((gate) => gate.id === "getting-started"));
const integration = list("integration");
@@ -40,6 +49,7 @@ assert.deepEqual(integration.includes, ["fast", "integration"]);
assert(integration.gates.some((gate) => gate.id === "plain-html"));
assert(integration.gates.some((gate) => gate.id === "getting-started"));
assert(integration.gates.some((gate) => gate.id === "pliegors-browser"));
+assert(integration.gates.some((gate) => gate.id === "benchmark-authority-smoke"));
const release = list("release");
assert.deepEqual(release.includes, ["fast", "integration", "release"]);
@@ -51,6 +61,15 @@ assert(release.gates.some((gate) => gate.id === "supply-chain"));
assert(release.gates.some((gate) => gate.id === "site"));
assert(release.gates.some((gate) => gate.id === "site-deployment"));
assert(release.gates.some((gate) => gate.id === "migration-real-corpus"));
+assert(release.gates.some((gate) => gate.id === "benchmark-oracle-live"));
+assert.deepEqual(
+ release.gates.find((gate) => gate.id === "release-readiness")?.command,
+ ["node", "scripts/check-release-readiness.mjs", "--profile=release"],
+);
+assert.deepEqual(
+ release.gates.find((gate) => gate.id === "hosted-browser-matrix")?.command,
+ ["node", "scripts/check-hosted-browser-matrix.mjs", "--profile=release"],
+);
assert.equal(
new Set(release.gates.map((gate) => gate.id)).size,
release.gates.length,
@@ -68,6 +87,37 @@ assert.match(
/process\.platform === "win32" && gate\.command === "pnpm"/u,
"Windows must invoke pnpm command shims through cmd.exe",
);
+const targetBase = resolve(root, "target", "isolation-contract");
+const targetEnvironment = { CARGO_TARGET_DIR: targetBase };
+const stableTarget = targetDirectoryForRustcIdentity(
+ root,
+ targetEnvironment,
+ "release: 1.85.0\nhost: x86_64-pc-windows-msvc\ncommit-hash: aaaa",
+);
+assert.equal(
+ stableTarget,
+ targetDirectoryForRustcIdentity(
+ root,
+ targetEnvironment,
+ "release: 1.85.0\nhost: x86_64-pc-windows-msvc\ncommit-hash: aaaa",
+ ),
+ "one exact toolchain identity must map to one deterministic target",
+);
+assert.notEqual(
+ stableTarget,
+ targetDirectoryForRustcIdentity(
+ root,
+ targetEnvironment,
+ "release: 1.96.0\nhost: x86_64-pc-windows-msvc\ncommit-hash: bbbb",
+ ),
+ "different Rust toolchains must never share build artifacts",
+);
+assert.match(stableTarget, /toolchains/u);
+assert.equal(
+ release.gates.find((gate) => gate.id === "properties")?.rustToolchain,
+ "1.85.0",
+ "the MSRV property gate must select its own target namespace",
+);
for (const dependency of ["pliego-dom", "pliego-ssg"]) {
assert.match(
pliegorsDevLoopManifest,
diff --git a/scripts/check-repair-corpus.mjs b/scripts/check-repair-corpus.mjs
index 115bc01..3461334 100644
--- a/scripts/check-repair-corpus.mjs
+++ b/scripts/check-repair-corpus.mjs
@@ -2,12 +2,14 @@ import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { spawnSync } from "node:child_process";
+import { isolatedCargoEnvironment } from "./rust-target.mjs";
const root = resolve(import.meta.dirname, "..");
const corpusPath = resolve(root, "benchmarks", "repair-corpus", "cases.json");
const corpusBytes = readFileSync(corpusPath);
const corpus = JSON.parse(corpusBytes.toString("utf8"));
const reportPrefix = "PLIEGOCSS_REPAIR_CORPUS_REPORT ";
+const cargoEnvironment = isolatedCargoEnvironment(root);
function fail(message) {
throw new Error(message);
@@ -80,6 +82,7 @@ const result = spawnSync(
["test", "--locked", "-p", "pliego-css-agent", "--test", "repair_corpus", "--", "--nocapture"],
{
cwd: root,
+ env: cargoEnvironment,
encoding: "utf8",
windowsHide: true,
maxBuffer: 16 * 1024 * 1024,
diff --git a/scripts/check-representative-css-modules.mjs b/scripts/check-representative-css-modules.mjs
index e8e266c..1f506db 100644
--- a/scripts/check-representative-css-modules.mjs
+++ b/scripts/check-representative-css-modules.mjs
@@ -2,10 +2,11 @@ import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
+import { cargoTargetRoot } from "./rust-target.mjs";
const root = resolve(import.meta.dirname, "..");
const fixture = join(root, "integration-tests", "representative", "css-modules-consumer");
-const executable = join(root, "target", "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
+const executable = join(cargoTargetRoot(root), "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
function fail(message, detail) { throw new Error(detail === undefined ? message : `${message}\n${JSON.stringify(detail, null, 2)}`); }
function run(command, args, cwd = root) {
const result = spawnSync(command, args, { cwd, encoding: "utf8", windowsHide: true });
diff --git a/scripts/check-representative-framework-routes.mjs b/scripts/check-representative-framework-routes.mjs
index d027b27..8da7c35 100644
--- a/scripts/check-representative-framework-routes.mjs
+++ b/scripts/check-representative-framework-routes.mjs
@@ -3,11 +3,12 @@ import { createHash } from "node:crypto";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
+import { cargoTargetRoot } from "./rust-target.mjs";
const root = resolve(import.meta.dirname, "..");
const fixture = join(root, "integration-tests", "representative", "framework-neutral-routes");
const reachabilityPath = join(fixture, ".reachability.generated.json");
-const cli = join(root, "target", "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
+const cli = join(cargoTargetRoot(root), "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
function fail(message, detail) { throw new Error(detail === undefined ? message : `${message}\n${JSON.stringify(detail, null, 2)}`); }
function run(command, args, cwd = root) {
const result = spawnSync(command, args, { cwd, encoding: "utf8", windowsHide: true });
diff --git a/scripts/check-representative-plain-html-audit.mjs b/scripts/check-representative-plain-html-audit.mjs
index dd76d0a..5f408ab 100644
--- a/scripts/check-representative-plain-html-audit.mjs
+++ b/scripts/check-representative-plain-html-audit.mjs
@@ -3,10 +3,11 @@ import { createHash } from "node:crypto";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
+import { cargoTargetRoot } from "./rust-target.mjs";
const root = resolve(import.meta.dirname, "..");
const fixture = join(root, "integration-tests", "representative", "plain-html-audit");
-const cli = join(root, "target", "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
+const cli = join(cargoTargetRoot(root), "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
function fail(message, detail) {
throw new Error(detail === undefined ? message : `${message}\n${JSON.stringify(detail, null, 2)}`);
diff --git a/scripts/check-representative-rust-control.mjs b/scripts/check-representative-rust-control.mjs
index 27dcbe0..9167a0d 100644
--- a/scripts/check-representative-rust-control.mjs
+++ b/scripts/check-representative-rust-control.mjs
@@ -3,10 +3,15 @@ import { createHash } from "node:crypto";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
+import { cargoTargetRoot, isolatedCargoEnvironment } from "./rust-target.mjs";
const root = resolve(import.meta.dirname, "..");
const fixture = join(root, "integration-tests", "representative", "rust-typed-control");
-const cli = join(root, "target", "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
+const cli = join(cargoTargetRoot(root), "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
+const msrvEnvironment = isolatedCargoEnvironment(root, {
+ env: process.env,
+ toolchain: "1.85.0",
+});
function fail(message, detail) { throw new Error(detail === undefined ? message : `${message}\n${JSON.stringify(detail, null, 2)}`); }
function run(command, args, cwd = root) {
const result = spawnSync(command, args, { cwd, encoding: "utf8", windowsHide: true });
@@ -16,7 +21,13 @@ function run(command, args, cwd = root) {
}
function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
for (const name of ["Cargo.toml", "src/main.rs"]) if (!existsSync(join(fixture, name))) fail(`missing ${name}`);
-run("cargo", ["+1.85.0", "check", "--quiet", "--locked", "--manifest-path", join(fixture, "Cargo.toml")]);
+const msrv = spawnSync(
+ "cargo",
+ ["+1.85.0", "check", "--quiet", "--locked", "--manifest-path", join(fixture, "Cargo.toml")],
+ { cwd: root, env: msrvEnvironment, encoding: "utf8", windowsHide: true },
+);
+if (msrv.error) throw msrv.error;
+if (msrv.status !== 0) fail("MSRV fixture check failed", msrv);
run("cargo", ["build", "--quiet", "--locked", "-p", "pliego-cssc"]);
let output;
try {
diff --git a/scripts/check-representative-vite-tailwind-replacement.mjs b/scripts/check-representative-vite-tailwind-replacement.mjs
index 6cfb495..23349b4 100644
--- a/scripts/check-representative-vite-tailwind-replacement.mjs
+++ b/scripts/check-representative-vite-tailwind-replacement.mjs
@@ -2,11 +2,12 @@ import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
+import { cargoTargetRoot } from "./rust-target.mjs";
const root = resolve(import.meta.dirname, "..");
const source = resolve(root, "integration-tests/representative/vite-tailwind-inventory");
const stage = resolve(root, "target/tests/vite-tailwind-plan-replacement");
-const binary = resolve(root, process.platform === "win32" ? "target/debug/pliego-cssc.exe" : "target/debug/pliego-cssc");
+const binary = resolve(cargoTargetRoot(root), "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
const run = (args, cwd = stage) => {
const result = spawnSync(binary, args, { cwd, encoding: "utf8" });
if (result.status !== 0) throw new Error(`${args.join(" ")} failed:\n${result.stderr}`);
diff --git a/scripts/check-representative-vite-tailwind-reversible.mjs b/scripts/check-representative-vite-tailwind-reversible.mjs
index 27315e9..2058181 100644
--- a/scripts/check-representative-vite-tailwind-reversible.mjs
+++ b/scripts/check-representative-vite-tailwind-reversible.mjs
@@ -2,10 +2,11 @@ import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, readFileSync, rmSync } from "node:fs";
import { join, resolve } from "node:path";
+import { cargoTargetRoot } from "./rust-target.mjs";
const root = resolve(import.meta.dirname, "..");
const fixture = resolve(root, "integration-tests/representative/vite-tailwind-inventory");
-const binary = resolve(root, process.platform === "win32" ? "target/debug/pliego-cssc.exe" : "target/debug/pliego-cssc");
+const binary = resolve(cargoTargetRoot(root), "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
const sidecar = join(fixture, "pliego.migration.css");
const receipt = join(fixture, "pliego.migration.receipt.json");
const files = ["package.json", "index.html", "src/app.css", "src/main.ts"];
diff --git a/scripts/check-representative-vite-tailwind-template-group.mjs b/scripts/check-representative-vite-tailwind-template-group.mjs
index 5aa392a..0ce2f8f 100644
--- a/scripts/check-representative-vite-tailwind-template-group.mjs
+++ b/scripts/check-representative-vite-tailwind-template-group.mjs
@@ -2,7 +2,8 @@ import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
-const root=resolve(import.meta.dirname,".."),source=resolve(root,"integration-tests/representative/vite-tailwind-inventory"),stage=resolve(root,"target/tests/vite-tailwind-css-template-group"),binary=resolve(root,process.platform==="win32"?"target/debug/pliego-cssc.exe":"target/debug/pliego-cssc");
+import { cargoTargetRoot } from "./rust-target.mjs";
+const root=resolve(import.meta.dirname,".."),source=resolve(root,"integration-tests/representative/vite-tailwind-inventory"),stage=resolve(root,"target/tests/vite-tailwind-css-template-group"),binary=resolve(cargoTargetRoot(root),"debug",process.platform==="win32"?"pliego-cssc.exe":"pliego-cssc");
const run=(args)=>{const r=spawnSync(binary,args,{cwd:stage,encoding:"utf8"});if(r.status!==0)throw new Error(`${args.join(' ')} failed:\n${r.stderr}`);return r.stdout};const sha=b=>createHash("sha256").update(b).digest("hex");
rmSync(stage,{recursive:true,force:true});mkdirSync(stage,{recursive:true});cpSync(source,stage,{recursive:true});
const cssFile=resolve(stage,"src/app.css"),htmlFile=resolve(stage,"index.html"),cssBefore=readFileSync(cssFile),htmlBefore=readFileSync(htmlFile);
diff --git a/scripts/check-representative-vite-tailwind-theme-root.mjs b/scripts/check-representative-vite-tailwind-theme-root.mjs
index 265b6a0..fd4b761 100644
--- a/scripts/check-representative-vite-tailwind-theme-root.mjs
+++ b/scripts/check-representative-vite-tailwind-theme-root.mjs
@@ -2,11 +2,12 @@ import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
+import { cargoTargetRoot } from "./rust-target.mjs";
const root = resolve(import.meta.dirname, "..");
const source = resolve(root, "integration-tests/representative/vite-tailwind-inventory");
const stage = resolve(root, "target/tests/vite-tailwind-theme-root");
-const binary = resolve(root, process.platform === "win32" ? "target/debug/pliego-cssc.exe" : "target/debug/pliego-cssc");
+const binary = resolve(cargoTargetRoot(root), "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
const run = (args) => { const result = spawnSync(binary, args, {cwd:stage,encoding:"utf8"}); if(result.status!==0) throw new Error(`${args.join(' ')} failed:\n${result.stderr}`); return result.stdout; };
const sha = (bytes) => createHash("sha256").update(bytes).digest("hex");
rmSync(stage,{recursive:true,force:true}); mkdirSync(stage,{recursive:true}); cpSync(source,stage,{recursive:true});
diff --git a/scripts/check-representative-vite-tailwind-utility.mjs b/scripts/check-representative-vite-tailwind-utility.mjs
index 5529d76..b3ca0d3 100644
--- a/scripts/check-representative-vite-tailwind-utility.mjs
+++ b/scripts/check-representative-vite-tailwind-utility.mjs
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
-const root=resolve(import.meta.dirname,".."),source=resolve(root,"integration-tests/representative/vite-tailwind-inventory"),stage=resolve(root,"target/tests/vite-tailwind-utility-projection"),binary=resolve(root,process.platform==="win32"?"target/debug/pliego-cssc.exe":"target/debug/pliego-cssc");
+import { cargoTargetRoot } from "./rust-target.mjs";
+const root=resolve(import.meta.dirname,".."),source=resolve(root,"integration-tests/representative/vite-tailwind-inventory"),stage=resolve(root,"target/tests/vite-tailwind-utility-projection"),binary=resolve(cargoTargetRoot(root),"debug",process.platform==="win32"?"pliego-cssc.exe":"pliego-cssc");
const run=(args)=>{const r=spawnSync(binary,args,{cwd:stage,encoding:"utf8"});if(r.status!==0)throw new Error(`${args.join(' ')} failed:\n${r.stderr}`);return r.stdout};const sha=b=>createHash("sha256").update(b).digest("hex");
rmSync(stage,{recursive:true,force:true});mkdirSync(stage,{recursive:true});cpSync(source,stage,{recursive:true});const plan=JSON.parse(run(["migration-project-plan","."]));const proposal=plan.proposals.find(x=>x.kind==="utility-projection");if(!proposal||proposal.automatic!==false)throw new Error("utility proposal absent");const file=resolve(stage,proposal.file),before=readFileSync(file);const text=before.toString();const m=text.match(/@utility\s+([A-Za-z0-9_-]+)\s*\{([^{}]*)\}/);if(!m)throw new Error("static utility absent");const after=Buffer.concat([before,Buffer.from(`/* PliegoCSS projected Tailwind utility */\n.${m[1]} {${m[2]}}\n`)]);if(sha(after)!==proposal.afterSha256)throw new Error("proposal hash mismatch");writeFileSync(resolve(stage,"before.css"),before);writeFileSync(resolve(stage,"after.css"),after);run(["migration-replace-apply","--file",proposal.file,"--before","before.css","--after","after.css","--receipt","receipt.json"]);const applied=JSON.parse(run(["migration-project-inventory","."])).sources.find(x=>x.file===proposal.file);if(applied.sourceSha256!==proposal.afterSha256)throw new Error("applied inventory mismatch");run(["migration-replace-rollback","--file",proposal.file,"--receipt","receipt.json"]);if(sha(readFileSync(file))!==proposal.beforeSha256)throw new Error("rollback mismatch");rmSync(stage,{recursive:true,force:true});process.stdout.write(`${JSON.stringify({schemaVersion:1,fixture:"vite-tailwind-inventory-copy",proposal,apply:"passed",inventory:"passed",rollback:"passed"},null,2)}\n`);
diff --git a/scripts/check-representative-vite-tailwind.mjs b/scripts/check-representative-vite-tailwind.mjs
index 73c56e9..3f738d0 100644
--- a/scripts/check-representative-vite-tailwind.mjs
+++ b/scripts/check-representative-vite-tailwind.mjs
@@ -2,10 +2,11 @@ import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { join, relative, resolve } from "node:path";
+import { cargoTargetRoot } from "./rust-target.mjs";
const root = resolve(import.meta.dirname, "..");
const fixture = join(root, "integration-tests", "representative", "vite-tailwind-inventory");
-const executable = join(root, "target", "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
+const executable = join(cargoTargetRoot(root), "debug", process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc");
function fail(message, detail) {
throw new Error(detail === undefined ? message : `${message}\n${JSON.stringify(detail, null, 2)}`);
diff --git a/scripts/check-standards-provenance.mjs b/scripts/check-standards-provenance.mjs
index 198acb7..9e10673 100644
--- a/scripts/check-standards-provenance.mjs
+++ b/scripts/check-standards-provenance.mjs
@@ -7,8 +7,8 @@ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const realRoot = realpathSync(root);
const attributionCommand = "node ./scripts/check-standards-provenance.mjs";
const reviewedFileDigests = Object.freeze({
- "THIRD_PARTY_NOTICES.md": "00c94fd9f07291966d5ffa252c6cf01d86da7a6ebecc8d526c7024f2cd36ce60",
- "docs/reference/standards-provenance.md": "2a48b22dd41fe7cdc33e2f7f075a1af261fdf5bfdba1f0ef6daa202151bf74fd",
+ "THIRD_PARTY_NOTICES.md": "8c3afb5b3b6735c2bac0696872df513e372541606ebb117c3349a6cde8a7fc79",
+ "docs/reference/standards-provenance.md": "b3a0b68daaab8af4d832ff71df0e633d14f4f6052cf21961463c90a8645ddbdf",
});
const sourceKeys = [
"id",
@@ -31,7 +31,10 @@ const expectedSources = JSON.parse(`[
{"id":"dtcg-resolver-2025.10","name":"Design Tokens Resolver Module 2025.10","kind":"specification","version":"2025.10","status":"final-community-group-report","license":"W3C-Community-Final-Specification-Agreement","url":"https://www.w3.org/community/reports/design-tokens/CG-FINAL-resolver-20251028/","relationship":"exchange-specification","usage":"implemented-contract","evidence":["crates/pliego-css-config/src/dtcg_resolver.rs","docs/reference/dtcg-bridge.md"],"integrity":[]},
{"id":"lightningcss","name":"Lightning CSS","kind":"software","version":"1.0.0-alpha.71","status":"versioned-crate","license":"MPL-2.0","url":"https://github.com/parcel-bundler/lightningcss","relationship":"css-backend","usage":"build-time","evidence":["Cargo.lock"],"integrity":[{"subject":"lightningcss@1.0.0-alpha.71","value":"cargo-checksum:cb6314c2f0590ac93c86099b98bb7ba8abcf759bfd89604ffca906472bb54937"}]},
{"id":"mediaqueries-5","name":"Media Queries Level 5","kind":"specification","version":"5","status":"w3c-working-draft","license":"W3C-Software-and-Document-License-2023","url":"https://www.w3.org/TR/2026/WD-mediaqueries-5-20260219/","relationship":"css-specification","usage":"audit-reference","evidence":["docs/reference/accessibility-policy.md"],"integrity":[]},
- {"id":"tailwindcss","name":"Tailwind CSS and CLI","kind":"software","version":"4.3.2","status":"versioned-package","license":"MIT","url":"https://github.com/tailwindlabs/tailwindcss","relationship":"benchmark-baseline","usage":"benchmark-only","evidence":["benchmarks/tailwind-v4/input.css","package.json","pnpm-lock.yaml"],"integrity":[{"subject":"@tailwindcss/cli@4.3.2","value":"sha512-Fzt+HrIZHDlkRYKdLMBeufaroaPvwCBG70sMLdmurdeadNMO/LxbmT8Sbb+P83ep0iAlAImettb7Y+rO+37rXw=="},{"subject":"pnpm-lock.yaml","value":"sha256:333f16de16053c85708ea74473a3008f314e1c3ee45490106e1462df06de93f9"},{"subject":"tailwindcss@4.3.2","value":"sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA=="}]},
+ {"id":"pixelmatch","name":"pixelmatch","kind":"software","version":"7.2.0","status":"versioned-package","license":"ISC","url":"https://github.com/mapbox/pixelmatch","relationship":"browser-certification","usage":"test-only","evidence":["benchmarks/browser-output-certification-v1/authority.json","package.json","pnpm-lock.yaml","scripts/browser-output-certification.mjs"],"integrity":[{"subject":"pixelmatch@7.2.0","value":"sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg=="},{"subject":"pnpm-lock.yaml","value":"sha256:39f1ccd3f3b3262805d773e92bf8b2d0efc9ac755ba77460a0cb801d5ccea394"}]},
+ {"id":"playwright","name":"Playwright","kind":"software","version":"1.61.1","status":"versioned-package","license":"Apache-2.0","url":"https://github.com/microsoft/playwright","relationship":"browser-certification","usage":"test-only","evidence":[".github/workflows/browser-output-certification.yml","benchmarks/browser-output-certification-v1/authority.json","package.json","pnpm-lock.yaml","scripts/browser-output-certification.mjs"],"integrity":[{"subject":"playwright-core@1.61.1","value":"sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg=="},{"subject":"playwright@1.61.1","value":"sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="},{"subject":"pnpm-lock.yaml","value":"sha256:39f1ccd3f3b3262805d773e92bf8b2d0efc9ac755ba77460a0cb801d5ccea394"}]},
+ {"id":"pngjs","name":"pngjs","kind":"software","version":"7.0.0","status":"versioned-package","license":"MIT","url":"https://github.com/pngjs/pngjs","relationship":"browser-certification","usage":"test-only","evidence":["benchmarks/browser-output-certification-v1/authority.json","package.json","pnpm-lock.yaml","scripts/browser-output-certification.mjs"],"integrity":[{"subject":"pngjs@7.0.0","value":"sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="},{"subject":"pnpm-lock.yaml","value":"sha256:39f1ccd3f3b3262805d773e92bf8b2d0efc9ac755ba77460a0cb801d5ccea394"}]},
+ {"id":"tailwindcss","name":"Tailwind CSS and CLI","kind":"software","version":"4.3.3","status":"versioned-package","license":"MIT","url":"https://github.com/tailwindlabs/tailwindcss","relationship":"benchmark-baseline","usage":"benchmark-only","evidence":["benchmarks/benchmark-authority-v2/oracle.json","benchmarks/tailwind-v4/input.css","package.json","pnpm-lock.yaml"],"integrity":[{"subject":"@tailwindcss/cli@4.3.2","value":"sha512-Fzt+HrIZHDlkRYKdLMBeufaroaPvwCBG70sMLdmurdeadNMO/LxbmT8Sbb+P83ep0iAlAImettb7Y+rO+37rXw=="},{"subject":"@tailwindcss/cli@4.3.3","value":"sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw=="},{"subject":"pnpm-lock.yaml","value":"sha256:39f1ccd3f3b3262805d773e92bf8b2d0efc9ac755ba77460a0cb801d5ccea394"},{"subject":"tailwindcss@3.4.19","value":"sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="},{"subject":"tailwindcss@4.3.2","value":"sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA=="},{"subject":"tailwindcss@4.3.3","value":"sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="}]},
{"id":"wcag-2.2","name":"Web Content Accessibility Guidelines (WCAG) 2.2","kind":"specification","version":"2.2","status":"w3c-recommendation","license":"W3C-Document-License-2023","url":"https://www.w3.org/TR/2024/REC-WCAG22-20241212/","relationship":"accessibility-reference","usage":"audit-reference","evidence":["docs/reference/accessibility-policy.md"],"integrity":[]},
{"id":"web-features","name":"web-features","kind":"dataset","version":"3.32.0","status":"versioned-package","license":"Apache-2.0","url":"https://github.com/web-platform-dx/web-features","relationship":"compatibility-dataset","usage":"frozen-data","evidence":["integration-tests/compatibility-policy/expected.baseline-widely.json"],"integrity":[{"subject":"web-features@3.32.0","value":"sha512-PQBbTofqV8FtMP65oT9tLPjbN4FSB2dRdNxLM0A9j4bNifVpFhEP/ATXSMMJAqPPWb/pgUOh6B+98yzfNEVbNw=="},{"subject":"web-features@3.32.0/data.json","value":"sha256:58bc2056041c93e313c3a58658a8120f857cf4888d0592e6e4a9b0484748441e"}]}
]`);
@@ -204,7 +207,7 @@ function validateManifest(manifest) {
"w3c-candidate-recommendation-snapshot",
"w3c-working-draft",
].includes(source.status), `${label}.status is invalid`);
- assert(["Apache-2.0", "MIT", "MPL-2.0", "W3C-Document-License-2023", "W3C-Software-and-Document-License-2023", "W3C-Community-Final-Specification-Agreement"].includes(source.license), `${label}.license is invalid`);
+ assert(["Apache-2.0", "ISC", "MIT", "MPL-2.0", "W3C-Document-License-2023", "W3C-Software-and-Document-License-2023", "W3C-Community-Final-Specification-Agreement"].includes(source.license), `${label}.license is invalid`);
assert([
"css-backend",
"compatibility-dataset",
@@ -212,8 +215,9 @@ function validateManifest(manifest) {
"accessibility-reference",
"css-specification",
"benchmark-baseline",
+ "browser-certification",
].includes(source.relationship), `${label}.relationship is invalid`);
- assert(["build-time", "frozen-data", "implemented-contract", "audit-reference", "benchmark-only"].includes(source.usage), `${label}.usage is invalid`);
+ assert(["build-time", "frozen-data", "implemented-contract", "audit-reference", "benchmark-only", "test-only"].includes(source.usage), `${label}.usage is invalid`);
let url;
try {
@@ -324,8 +328,7 @@ function verifyAttributionScript(packageJson) {
);
}
-function verifyTailwindLock(lock, source) {
- const contract = tailwindLockContract(source);
+function verifyTailwindLock(lock, source, authority) {
assert(lock.startsWith("lockfileVersion: '9.0'\n"), "pnpm lockfile version drifted");
const importers = lockSection(lock, "importers");
const packages = sectionEntries(lockSection(lock, "packages"));
@@ -333,28 +336,67 @@ function verifyTailwindLock(lock, source) {
const importerEntries = sectionEntries(importers);
const rootImporter = oneEntry(importerEntries, ".", "importers");
- assert(
- exactCount(rootImporter, contract.cliImporter) === 1 &&
- exactCount(rootImporter, contract.coreImporter) === 1,
- "Tailwind root importer drifted",
- );
- assert((importers.match(/^\s+'?@tailwindcss\/cli'?:$/gm) ?? []).length === 1, "Tailwind CLI must have exactly one importer edge");
- assert((importers.match(/^\s+tailwindcss:$/gm) ?? []).length === 1, "tailwindcss must have exactly one importer edge");
-
- assert(oneEntry(packages, contract.cli, "packages") === contract.cliPackage, "Tailwind CLI package snapshot drifted");
- assert(oneEntry(packages, contract.core, "packages") === contract.corePackage, "tailwindcss package snapshot drifted");
- assert(oneEntry(snapshots, contract.cli, "snapshots") === contract.cliSnapshot, "Tailwind CLI dependency graph drifted");
- assert(oneEntry(snapshots, contract.core, "snapshots") === contract.coreSnapshot, "tailwindcss runtime snapshot drifted");
+ const pins = new Map();
+ for (const lane of authority.lanes) {
+ for (const [alias, packageName, version, integrity] of [
+ [lane.packageAlias, lane.packageName, lane.version, lane.integrity],
+ [lane.cliPackageAlias, lane.cliPackageName, lane.cliVersion, lane.cliIntegrity],
+ ]) {
+ const key = `${packageName}@${version}`;
+ const existing = pins.get(alias);
+ assert(!existing || existing.key === key, `Tailwind alias ${alias} has conflicting pins`);
+ pins.set(alias, { alias, packageName, version, integrity, key });
+ assert(
+ integrityValue(source, key) === integrity,
+ `standards provenance integrity drifted for ${key}`,
+ );
+ }
+ }
+ for (const pin of pins.values()) {
+ const aliasKey = pin.alias.startsWith("@") ? `'${pin.alias}'` : pin.alias;
+ const specifier = pin.alias === pin.packageName
+ ? pin.version
+ : `npm:${pin.packageName}@${pin.version}`;
+ const resolvedVersion = pin.alias === pin.packageName
+ ? pin.version
+ : pin.packageName.startsWith("@")
+ ? `'${pin.packageName}@${pin.version}'`
+ : `${pin.packageName}@${pin.version}`;
+ const importer = ` ${aliasKey}:\n specifier: ${specifier}\n version: ${resolvedVersion}`;
+ assert(exactCount(rootImporter, importer) === 1, `Tailwind importer pin drifted for ${pin.alias}`);
+ }
- for (const [prefix, expected] of [["@tailwindcss/cli@", contract.cli], ["tailwindcss@", contract.core]]) {
- for (const [section, entries] of [["packages", packages], ["snapshots", snapshots]]) {
- const versions = entries.filter((entry) => entry.name.startsWith(prefix)).map((entry) => entry.name);
- assert(versions.length === 1 && versions[0] === expected, `${section} contains orphan or multiple ${prefix} entries`);
+ const uniquePackages = new Map([...pins.values()].map((pin) => [pin.key, pin]));
+ for (const pin of uniquePackages.values()) {
+ const packageEntry = oneEntry(packages, pin.key, "packages");
+ assert(
+ packageEntry.includes(`resolution: {integrity: ${pin.integrity}}`),
+ `${pin.key} registry integrity drifted`,
+ );
+ oneEntry(snapshots, pin.key, "snapshots");
+ }
+ for (const lane of authority.lanes.filter((item) => item.cliPackageName === "@tailwindcss/cli")) {
+ const snapshot = oneEntry(
+ snapshots,
+ `${lane.cliPackageName}@${lane.cliVersion}`,
+ "snapshots",
+ );
+ for (const dependency of [
+ `'@tailwindcss/node': ${lane.cliVersion}`,
+ `'@tailwindcss/oxide': ${lane.cliVersion}`,
+ `tailwindcss: ${lane.version}`,
+ ]) {
+ assert(snapshot.includes(dependency), `${lane.id} CLI dependency graph drifted`);
}
}
- for (const dependency of contract.dependencies) {
- oneEntry(packages, dependency, "packages");
- oneEntry(snapshots, dependency, "snapshots");
+ for (const [prefix, expected] of [
+ ["@tailwindcss/cli@", ["@tailwindcss/cli@4.3.2", "@tailwindcss/cli@4.3.3"]],
+ ["tailwindcss@", ["tailwindcss@3.4.19", "tailwindcss@4.3.2", "tailwindcss@4.3.3"]],
+ ]) {
+ for (const [section, entries] of [["packages", packages], ["snapshots", snapshots]]) {
+ const actual = entries.filter((entry) => entry.name.startsWith(prefix)).map((entry) => entry.name).sort();
+ assert(JSON.stringify(actual) === JSON.stringify(expected), `${section} ${prefix} set drifted`);
+ }
}
}
@@ -362,13 +404,71 @@ function verifyTailwind(manifest) {
const source = sourceById(manifest, "tailwindcss");
assert(source.usage === "benchmark-only", "Tailwind must remain benchmark-only");
const packageJson = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
+ const authority = JSON.parse(
+ readFileSync(resolve(root, "benchmarks", "benchmark-authority-v2", "oracle.json"), "utf8"),
+ );
+ assert(authority.schemaVersion === 2, "Benchmark Authority must use schema 2");
+ const primary = authority.lanes.find((lane) => lane.id === authority.primaryLane);
+ assert(primary && primary.version === source.version, "Tailwind provenance primary lane drifted");
verifyAttributionScript(packageJson);
- assert(packageJson.devDependencies?.tailwindcss === source.version, "tailwindcss package version drifted");
- assert(packageJson.devDependencies?.["@tailwindcss/cli"] === source.version, "Tailwind CLI package version drifted");
+ for (const lane of authority.lanes) {
+ for (const [alias, packageName, version] of [
+ [lane.packageAlias, lane.packageName, lane.version],
+ [lane.cliPackageAlias, lane.cliPackageName, lane.cliVersion],
+ ]) {
+ const expected = alias === packageName ? version : `npm:${packageName}@${version}`;
+ assert(packageJson.devDependencies?.[alias] === expected, `${alias} package pin drifted`);
+ }
+ }
const lockBytes = readFileSync(repositoryFile("pnpm-lock.yaml", "pnpm lockfile"));
verifyPnpmLockIntegrity(lockBytes, source);
const lock = lockBytes.toString("utf8").replaceAll("\r\n", "\n");
- verifyTailwindLock(lock, source);
+ verifyTailwindLock(lock, source, authority);
+}
+
+function verifyBrowserCertificationToolchain(manifest) {
+ const packageJson = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
+ const lockBytes = readFileSync(repositoryFile("pnpm-lock.yaml", "pnpm lockfile"));
+ const lock = lockBytes.toString("utf8").replaceAll("\r\n", "\n");
+ const importer = oneEntry(sectionEntries(lockSection(lock, "importers")), ".", "importers");
+ const packages = sectionEntries(lockSection(lock, "packages"));
+ const snapshots = sectionEntries(lockSection(lock, "snapshots"));
+ for (const id of ["pixelmatch", "playwright", "pngjs"]) {
+ const source = sourceById(manifest, id);
+ assert(packageJson.devDependencies?.[id] === source.version, `${id} package pin drifted`);
+ verifyPnpmLockIntegrity(lockBytes, source);
+ assert(
+ importer.includes(` ${id}:\n specifier: ${source.version}\n version: ${source.version}`),
+ `${id} importer pin drifted`,
+ );
+ const key = `${id}@${source.version}`;
+ const packageEntry = oneEntry(packages, key, "packages");
+ assert(
+ packageEntry.includes(`resolution: {integrity: ${integrityValue(source, key)}}`),
+ `${key} registry integrity drifted`,
+ );
+ oneEntry(snapshots, key, "snapshots");
+ }
+ const playwright = sourceById(manifest, "playwright");
+ const coreKey = `playwright-core@${playwright.version}`;
+ assert(
+ oneEntry(packages, coreKey, "packages").includes(
+ `resolution: {integrity: ${integrityValue(playwright, coreKey)}}`,
+ ),
+ `${coreKey} registry integrity drifted`,
+ );
+ oneEntry(snapshots, coreKey, "snapshots");
+ assert(
+ oneEntry(snapshots, `playwright@${playwright.version}`, "snapshots").includes(
+ `playwright-core: ${playwright.version}`,
+ ),
+ "Playwright dependency graph drifted",
+ );
+ assert(
+ oneEntry(snapshots, `pixelmatch@${sourceById(manifest, "pixelmatch").version}`, "snapshots")
+ .includes(`pngjs: ${sourceById(manifest, "pngjs").version}`),
+ "pixelmatch dependency graph drifted",
+ );
}
function exactCount(value, needle) {
@@ -476,6 +576,9 @@ function verifyNoticeContent(manifest, notices) {
"dtcg-resolver-2025.10": "## Implemented exchange reports",
lightningcss: "## Build-time software",
"mediaqueries-5": "## Accessibility and CSS references",
+ pixelmatch: "## Browser-certification software",
+ playwright: "## Browser-certification software",
+ pngjs: "## Browser-certification software",
tailwindcss: "## Benchmark-only software",
"wcag-2.2": "## Accessibility and CSS references",
"web-features": "## Frozen compatibility data",
@@ -540,22 +643,37 @@ function verifyIntegrationRegressions(manifest) {
expectRejection("no-op attribution script", "command drifted", () => verifyAttributionScript(noOp));
const source = sourceById(manifest, "tailwindcss");
- const contract = tailwindLockContract(source);
+ const authority = JSON.parse(
+ readFileSync(resolve(root, "benchmarks", "benchmark-authority-v2", "oracle.json"), "utf8"),
+ );
const lock = readFileSync(resolve(root, "pnpm-lock.yaml"), "utf8").replaceAll("\r\n", "\n");
- const importerDrift = lock.replace(" specifier: 4.3.2", " specifier: 4.3.1");
- expectRejection("Tailwind importer drift", "root importer drifted", () => verifyTailwindLock(importerDrift, source));
+ const importerDrift = lock.replace(" specifier: 4.3.3", " specifier: 4.3.1");
+ expectRejection("Tailwind importer drift", "importer pin drifted", () => {
+ verifyTailwindLock(importerDrift, source, authority);
+ });
- const duplicatePackage = lock.replace(contract.cliPackage, `${contract.cliPackage}\n\n${contract.cliPackage}`);
- expectRejection("duplicate Tailwind package", "exactly one", () => verifyTailwindLock(duplicatePackage, source));
+ const packageEntries = sectionEntries(lockSection(lock, "packages"));
+ const cliPackage = oneEntry(packageEntries, "@tailwindcss/cli@4.3.3", "packages");
+ const duplicatePackage = lock.replace(cliPackage, `${cliPackage}\n\n${cliPackage}`);
+ expectRejection("duplicate Tailwind package", "exactly one", () => {
+ verifyTailwindLock(duplicatePackage, source, authority);
+ });
- const orphanSnapshot = lock.replace(`\n${contract.coreSnapshot}\n`, "\n");
- expectRejection("orphan Tailwind package", "exactly one", () => verifyTailwindLock(orphanSnapshot, source));
+ const snapshotEntries = sectionEntries(lockSection(lock, "snapshots"));
+ const coreSnapshot = oneEntry(snapshotEntries, "tailwindcss@4.3.3", "snapshots");
+ const orphanSnapshot = lock.replace(`\n${coreSnapshot}\n`, "\n");
+ expectRejection("orphan Tailwind package", "exactly one", () => {
+ verifyTailwindLock(orphanSnapshot, source, authority);
+ });
+ const cliSnapshot = oneEntry(snapshotEntries, "@tailwindcss/cli@4.3.3", "snapshots");
const graphDrift = lock.replace(
- contract.cliSnapshot,
- contract.cliSnapshot.replace(`tailwindcss: ${source.version}`, "tailwindcss: 4.3.1"),
+ cliSnapshot,
+ cliSnapshot.replace("tailwindcss: 4.3.3", "tailwindcss: 4.3.1"),
);
- expectRejection("Tailwind dependency graph drift", "dependency graph drifted", () => verifyTailwindLock(graphDrift, source));
+ expectRejection("Tailwind dependency graph drift", "dependency graph drifted", () => {
+ verifyTailwindLock(graphDrift, source, authority);
+ });
const transitiveDrift = lock.replace(" lightningcss: 1.32.0", " lightningcss: 1.31.0");
expectRejection("Tailwind transitive lock drift", "SHA-256 drifted", () => {
@@ -638,6 +756,7 @@ function main() {
verifyDtcg(manifest);
verifyAccessibilityReferences(manifest);
verifyTailwind(manifest);
+ verifyBrowserCertificationToolchain(manifest);
verifyNotices(manifest);
verifyRepositoryWiring();
console.log("standards provenance gate: pass");
diff --git a/scripts/check-tailwind-matrix.mjs b/scripts/check-tailwind-matrix.mjs
index 07a09b0..14b500f 100644
--- a/scripts/check-tailwind-matrix.mjs
+++ b/scripts/check-tailwind-matrix.mjs
@@ -1,11 +1,17 @@
+import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
-import { dirname, resolve } from "node:path";
-import { fileURLToPath } from "node:url";
+import { resolve } from "node:path";
+import {
+ corpusMetrics,
+ loadAuthority,
+ materializeCorpus,
+ repositoryRoot,
+} from "./benchmark-authority-v2.mjs";
-const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
-const matrixPath = resolve(root, "docs/benchmarks/tailwind-v4-competitive-matrix.json");
+const matrixPath = resolve(repositoryRoot, "docs/benchmarks/tailwind-v4-competitive-matrix.json");
const matrix = JSON.parse(readFileSync(matrixPath, "utf8"));
+const authority = loadAuthority();
function fail(message) {
throw new Error(`Tailwind competitive matrix: ${message}`);
@@ -18,33 +24,37 @@ function array(value, label) {
return value;
}
-if (matrix.schemaVersion !== 1) fail("schemaVersion must be 1");
-if (matrix.baseline?.product !== "Tailwind CSS" || matrix.baseline?.version !== "4.3.2") {
- fail("baseline must pin Tailwind CSS 4.3.2");
+if (matrix.schemaVersion !== 2) fail("schemaVersion must be 2");
+if (matrix.baseline?.product !== "Tailwind CSS" || matrix.baseline?.license !== "MIT") {
+ fail("baseline identity must be Tailwind CSS/MIT");
}
-if (matrix.baseline?.license !== "MIT") fail("baseline license must be MIT");
-const packagePath = resolve(root, matrix.baseline.packageManifest);
-const fixturePath = resolve(root, matrix.baseline.fixture);
-for (const [path, expected, label] of [
- [packagePath, matrix.baseline.packageManifestSha256, "package manifest"],
- [fixturePath, matrix.baseline.fixtureSha256, "fixture"],
-]) {
- if (!existsSync(path)) fail(`${label} is missing`);
- const actual = sha256(path);
- if (actual !== expected) fail(`${label} hash drifted: expected ${expected}, found ${actual}`);
+if (matrix.baseline.authority !== "benchmarks/benchmark-authority-v2/oracle.json") {
+ fail("baseline must delegate competitor identity to Benchmark Authority v2");
+}
+if (matrix.baseline.primaryLane !== authority.primaryLane) fail("primary lane drifted");
+const primaryLane = authority.lanes.find((lane) => lane.id === authority.primaryLane);
+if (!primaryLane || matrix.baseline.version !== primaryLane.version) fail("primary version drifted");
+const packagePath = resolve(repositoryRoot, matrix.baseline.packageManifest);
+const fixturePath = resolve(repositoryRoot, matrix.baseline.fixture);
+if (sha256(packagePath) !== matrix.baseline.packageManifestSha256) fail("package manifest hash drifted");
+const installed = JSON.parse(readFileSync(packagePath, "utf8"));
+if (installed.name !== "tailwindcss" || installed.version !== primaryLane.version) {
+ fail("installed primary Tailwind package drifted");
}
-const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
-if (packageJson.name !== "tailwindcss" || packageJson.version !== matrix.baseline.version) {
- fail("installed Tailwind package identity drifted");
+const fixtureMetrics = corpusMetrics(readFileSync(fixturePath, "utf8").replaceAll("\r\n", "\n"));
+for (const [field, expected] of [
+ ["sha256", matrix.baseline.fixtureSha256],
+ ["classAttributeCount", matrix.baseline.fixtureClassAttributes],
+ ["utilityOccurrences", matrix.baseline.fixtureUtilityOccurrences],
+ ["uniqueUtilityTokens", matrix.baseline.fixtureUniqueUtilities],
+]) {
+ if (fixtureMetrics[field] !== expected) fail(`fixture ${field} drifted`);
}
-const fixture = readFileSync(fixturePath, "utf8");
-const classValues = [...fixture.matchAll(/class="([^"]+)"/g)].map((match) => match[1]);
-const utilities = classValues.flatMap((value) => value.trim().split(/\s+/u));
-if (classValues.length !== matrix.baseline.fixtureClassAttributes) fail("fixture class count drifted");
-if (utilities.length !== matrix.baseline.fixtureUtilityOccurrences) fail("fixture utility count drifted");
-if (new Set(utilities).size !== matrix.baseline.fixtureUniqueUtilities) {
- fail("fixture unique utility count drifted");
+const medium = authority.corpora.find((corpus) => corpus.id === "medium");
+if (JSON.stringify(corpusMetrics(materializeCorpus(medium))) !== JSON.stringify(medium.expected)) {
+ fail("matrix fixture is not the authority medium corpus");
}
+
const statuses = new Set([
"matched",
"pliego-differentiator",
@@ -53,20 +63,18 @@ const statuses = new Set([
"open-gap",
]);
const impacts = new Set(["keep-green", "blocking", "should", "non-blocking", "post-0.1"]);
-if (Object.keys(matrix.statuses ?? {}).sort().join("\n") !== [...statuses].sort().join("\n")) {
+if (JSON.stringify(Object.keys(matrix.statuses).sort()) !== JSON.stringify([...statuses].sort())) {
fail("status definitions drifted");
}
const sourceIds = new Set();
for (const source of array(matrix.sources, "sources")) {
if (sourceIds.has(source.id)) fail(`duplicate source ${source.id}`);
sourceIds.add(source.id);
- if (source.kind === "local-evidence" || source.kind === "local-contract") {
- if (!existsSync(resolve(root, source.path))) fail(`source ${source.id} path is missing`);
+ if (new Set(["local-evidence", "historical-evidence", "local-contract"]).has(source.kind)) {
+ if (!existsSync(resolve(repositoryRoot, source.path))) fail(`source ${source.id} is missing`);
} else if (source.kind === "upstream-doc") {
- if (!/^https:\/\/tailwindcss\.com\/docs\//u.test(source.url ?? "")) {
- fail(`source ${source.id} is not a Tailwind documentation URL`);
- }
- if (source.versionObserved !== "v4.3") fail(`source ${source.id} version is not v4.3`);
+ if (!/^https:\/\/tailwindcss\.com\/docs\//u.test(source.url ?? "")) fail(`${source.id} is not an upstream docs URL`);
+ if (source.versionObserved !== "v4.3") fail(`${source.id} upstream version drifted`);
} else {
fail(`source ${source.id} has unknown kind`);
}
@@ -74,35 +82,39 @@ for (const source of array(matrix.sources, "sources")) {
const dimensionIds = new Set();
const counts = Object.fromEntries([...statuses].map((status) => [status, 0]));
for (const dimension of array(matrix.dimensions, "dimensions")) {
- if (!/^[a-z][a-z0-9-]*$/u.test(dimension.id ?? "")) fail("dimension id is not canonical");
- if (dimensionIds.has(dimension.id)) fail(`duplicate dimension ${dimension.id}`);
+ if (!/^[a-z][a-z0-9-]*$/u.test(dimension.id ?? "") || dimensionIds.has(dimension.id)) {
+ fail(`invalid or duplicate dimension ${dimension.id}`);
+ }
dimensionIds.add(dimension.id);
if (!statuses.has(dimension.status)) fail(`${dimension.id} has unknown status`);
if (!impacts.has(dimension.releaseImpact)) fail(`${dimension.id} has unknown releaseImpact`);
for (const field of ["area", "tailwind", "pliego", "nextAction"]) {
- if (typeof dimension[field] !== "string" || dimension[field].trim() === "") {
- fail(`${dimension.id}.${field} is empty`);
- }
+ if (typeof dimension[field] !== "string" || !dimension[field].trim()) fail(`${dimension.id}.${field} is empty`);
}
for (const evidence of array(dimension.evidence, `${dimension.id}.evidence`)) {
if (!sourceIds.has(evidence)) fail(`${dimension.id} references unknown evidence ${evidence}`);
}
counts[dimension.status] += 1;
}
-const expectedCounts = {
- matched: 5,
- "pliego-differentiator": 6,
- "tailwind-advantage": 5,
- "different-by-design": 3,
- "open-gap": 3,
-};
-if (JSON.stringify(counts) !== JSON.stringify(expectedCounts)) {
- fail(`status counts drifted: ${JSON.stringify(counts)}`);
+if (dimensionIds.size !== 22) fail(`expected 22 competitive dimensions, found ${dimensionIds.size}`);
+for (const [status, count] of Object.entries(counts)) {
+ if (count < 1) fail(`matrix has no ${status} dimension`);
}
-const human = readFileSync(resolve(root, "docs/benchmarks/tailwind-v4-competitive-matrix.md"), "utf8");
-for (const marker of ["Tailwind CSS v4.3.2", "Blocking `0.1.0`", "Explicitly post-0.1"]) {
- if (!human.includes(marker)) fail(`human matrix is missing ${marker}`);
+for (const id of ["build-latency-fixture", "payload-fixture"]) {
+ const dimension = matrix.dimensions.find((item) => item.id === id);
+ if (dimension?.status !== "open-gap" || dimension.releaseImpact !== "blocking") {
+ fail(`${id} must remain blocking until clean schema-2 evidence exists`);
+ }
}
+
+const renderer = spawnSync(process.execPath, ["scripts/render-tailwind-matrix.mjs", "--check"], {
+ cwd: repositoryRoot,
+ encoding: "utf8",
+ windowsHide: true,
+});
+if (renderer.error) throw renderer.error;
+if (renderer.status !== 0) fail(renderer.stderr.trim() || "generated Markdown check failed");
+
process.stdout.write(
- `${JSON.stringify({ schemaVersion: 1, baseline: matrix.baseline.version, dimensions: matrix.dimensions.length, counts }, null, 2)}\n`,
+ `${JSON.stringify({ schemaVersion: 2, baseline: primaryLane.version, dimensions: dimensionIds.size, counts }, null, 2)}\n`,
);
diff --git a/scripts/check-vscode-client.mjs b/scripts/check-vscode-client.mjs
index 9908320..8880ee9 100644
--- a/scripts/check-vscode-client.mjs
+++ b/scripts/check-vscode-client.mjs
@@ -54,9 +54,10 @@ if (manifest.capabilities?.untrustedWorkspaces?.supported !== false) {
fail("untrusted workspaces must fail closed");
}
const bundle = readFileSync(resolve(EXTENSION, "dist", "extension.js"), "utf8");
-for (const contract of ["pliego-css-lsp", "pliego-cssc", "onDidChangeConfiguration"]) {
+for (const contract of ["pliego-css-lsp", "onDidChangeConfiguration"]) {
if (!bundle.includes(contract)) fail(`bundle is missing ${contract}`);
}
+if (bundle.includes("--pliego-cssc")) fail("VS Code client still delegates to a compiler path");
process.stdout.write(
`${JSON.stringify({
diff --git a/scripts/measure-benchmark-authority-v2.mjs b/scripts/measure-benchmark-authority-v2.mjs
new file mode 100644
index 0000000..71cfb9f
--- /dev/null
+++ b/scripts/measure-benchmark-authority-v2.mjs
@@ -0,0 +1,569 @@
+import { spawnSync } from "node:child_process";
+import { createRequire } from "node:module";
+import {
+ existsSync,
+ mkdirSync,
+ readFileSync,
+ rmSync,
+ writeFileSync,
+} from "node:fs";
+import {
+ availableParallelism,
+ cpus,
+ release as osRelease,
+ totalmem,
+ type as osType,
+ version as osVersion,
+} from "node:os";
+import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
+import { fileURLToPath } from "node:url";
+import { brotliCompressSync, constants as zlibConstants, gzipSync } from "node:zlib";
+import {
+ authorityPath,
+ canonicalLaneMap,
+ corpusMetrics,
+ extractClassValues,
+ loadAuthority,
+ materializeCorpus,
+ readInstalledPackage,
+ repositoryRoot,
+ resolveLaneCli,
+ round,
+ sha256,
+ summarize,
+} from "./benchmark-authority-v2.mjs";
+import { targetDirectoryForRustcIdentity } from "./rust-target.mjs";
+
+const require = createRequire(import.meta.url);
+const escapeClassName = require(
+ join(repositoryRoot, "node_modules", "tailwindcss-v3-lts", "lib", "util", "escapeClassName.js"),
+).default;
+const benchmarkRoot = join(repositoryRoot, "target", "benchmarks", "benchmark-authority-v2");
+const localResultPath = join(
+ repositoryRoot,
+ "benchmarks",
+ "results",
+ "benchmark-authority-v2.local.json",
+);
+const evidenceRoot = join(repositoryRoot, "benchmarks", "evidence", "v2");
+const harnessPath = fileURLToPath(import.meta.url);
+const runnerSourcePath = join(repositoryRoot, "scripts", "benchmark-process-metrics.rs");
+const packagePath = join(repositoryRoot, "package.json");
+const lockPath = join(repositoryRoot, "pnpm-lock.yaml");
+
+function fail(message) {
+ throw new Error(`Benchmark Authority v2 measurement: ${message}`);
+}
+
+function parsePositiveInteger(value, label, allowZero = false) {
+ const parsed = Number(value);
+ if (!Number.isSafeInteger(parsed) || parsed < (allowZero ? 0 : 1)) {
+ fail(`${label} must be ${allowZero ? "a non-negative" : "a positive"} integer`);
+ }
+ return parsed;
+}
+
+function parseOptions(arguments_, authority) {
+ const options = {
+ pairs: authority.methodology.measuredPairs,
+ warmups: authority.methodology.warmupPairs,
+ laneIds: authority.lanes.map((lane) => lane.id),
+ corpusIds: authority.corpora.map((corpus) => corpus.id),
+ evidence: null,
+ smoke: false,
+ };
+ for (const argument of arguments_) {
+ if (argument === "--smoke") {
+ options.smoke = true;
+ options.pairs = 1;
+ options.warmups = 0;
+ } else if (argument.startsWith("--pairs=")) {
+ options.pairs = parsePositiveInteger(argument.slice("--pairs=".length), "--pairs");
+ } else if (argument.startsWith("--warmups=")) {
+ options.warmups = parsePositiveInteger(
+ argument.slice("--warmups=".length),
+ "--warmups",
+ true,
+ );
+ } else if (argument.startsWith("--lanes=")) {
+ options.laneIds = argument.slice("--lanes=".length).split(",").filter(Boolean);
+ } else if (argument.startsWith("--corpora=")) {
+ options.corpusIds = argument.slice("--corpora=".length).split(",").filter(Boolean);
+ } else if (argument.startsWith("--evidence=")) {
+ options.evidence = resolve(repositoryRoot, argument.slice("--evidence=".length));
+ } else {
+ fail(`unknown argument ${JSON.stringify(argument)}`);
+ }
+ }
+ if (options.evidence && (options.smoke || options.pairs !== authority.methodology.measuredPairs || options.warmups !== authority.methodology.warmupPairs)) {
+ fail("evidence requires the canonical 5 warmup and 30 measured pairs");
+ }
+ return options;
+}
+
+function run(command, arguments_, options = {}) {
+ const result = spawnSync(command, arguments_, {
+ cwd: repositoryRoot,
+ encoding: "utf8",
+ windowsHide: true,
+ timeout: 180_000,
+ maxBuffer: 16 * 1024 * 1024,
+ ...options,
+ });
+ if (result.error) throw result.error;
+ if (result.status !== 0) {
+ fail(
+ `${command} ${arguments_.join(" ")} exited ${result.status}\n${`${result.stderr ?? ""}`.slice(-4_096)}`,
+ );
+ }
+ return result;
+}
+
+function git(arguments_, fallback = "") {
+ try {
+ return run("git", arguments_).stdout.trim() || fallback;
+ } catch {
+ return fallback;
+ }
+}
+
+function gitState() {
+ const status = run("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"]).stdout;
+ return {
+ commit: git(["rev-parse", "HEAD"], "unborn"),
+ dirty: status.length > 0,
+ statusEntryCount: status.split("\0").filter(Boolean).length,
+ statusSha256: sha256(Buffer.from(status, "utf8")),
+ };
+}
+
+function safeEvidencePath(path) {
+ if (!path) return;
+ const child = relative(evidenceRoot, path);
+ if (
+ !child ||
+ child === ".." ||
+ child.startsWith(`..${sep}`) ||
+ isAbsolute(child) ||
+ !path.endsWith(".json") ||
+ existsSync(path)
+ ) {
+ fail("--evidence must be a new .json file below benchmarks/evidence/v2");
+ }
+}
+
+function capturePowerPlan() {
+ const declared = process.env.PLIEGO_BENCH_POWER_PLAN?.trim();
+ if (declared) return { status: "declared", source: "PLIEGO_BENCH_POWER_PLAN", value: declared };
+ const probes =
+ process.platform === "win32"
+ ? [["powercfg", ["/getactivescheme"]]]
+ : process.platform === "darwin"
+ ? [["pmset", ["-g"]]]
+ : [["powerprofilesctl", ["get"]]];
+ for (const [command, arguments_] of probes) {
+ const result = spawnSync(command, arguments_, { encoding: "utf8", windowsHide: true });
+ if (!result.error && result.status === 0 && result.stdout.trim()) {
+ return { status: "recorded", source: `${command} ${arguments_.join(" ")}`, value: result.stdout.trim() };
+ }
+ }
+ return { status: "unavailable", source: null, value: null };
+}
+
+function captureSecurityTooling(evidence) {
+ const status = process.env.PLIEGO_BENCH_SECURITY_TOOLING_STATUS?.trim().toLowerCase();
+ const tooling = process.env.PLIEGO_BENCH_SECURITY_TOOLING?.trim() || null;
+ const allowed = new Set(["active", "disabled", "not-installed", "unknown"]);
+ if (!status) {
+ if (evidence) fail("evidence requires PLIEGO_BENCH_SECURITY_TOOLING_STATUS");
+ return { status: "not-reported", tooling: null };
+ }
+ if (!allowed.has(status)) fail("invalid PLIEGO_BENCH_SECURITY_TOOLING_STATUS");
+ if ((status === "active" || status === "disabled") && !tooling) {
+ fail(`${status} security tooling requires PLIEGO_BENCH_SECURITY_TOOLING`);
+ }
+ return { status, tooling };
+}
+
+function toolchain() {
+ const rustcVerbose = run("rustc", ["+1.96.0", "-vV"]).stdout.trim();
+ const host = /^host: (\S+)$/mu.exec(rustcVerbose)?.[1];
+ if (!host) fail("cannot resolve the Rust 1.96.0 host target");
+ return {
+ cargo: run("cargo", ["+1.96.0", "--version"]).stdout.trim(),
+ rustc: /^rustc .+$/mu.exec(rustcVerbose)?.[0] ?? "unknown",
+ rustcVerbose,
+ host,
+ };
+}
+
+function buildTools(toolchainValue) {
+ const cargoTarget = targetDirectoryForRustcIdentity(
+ repositoryRoot,
+ process.env,
+ toolchainValue.rustcVerbose,
+ );
+ const runnerRoot = join(cargoTarget, "benchmark-tools");
+ mkdirSync(cargoTarget, { recursive: true });
+ mkdirSync(runnerRoot, { recursive: true });
+ const cargoEnvironment = {
+ ...process.env,
+ CARGO_INCREMENTAL: "0",
+ CARGO_TARGET_DIR: cargoTarget,
+ CARGO_TERM_COLOR: "never",
+ };
+ run(
+ "cargo",
+ ["+1.96.0", "build", "--release", "--locked", "--target", toolchainValue.host, "-p", "pliego-cssc"],
+ { env: cargoEnvironment },
+ );
+ const pliego = join(
+ cargoTarget,
+ toolchainValue.host,
+ "release",
+ process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc",
+ );
+ const runner = join(
+ runnerRoot,
+ process.platform === "win32" ? "benchmark-process-metrics.exe" : "benchmark-process-metrics",
+ );
+ run("rustc", ["+1.96.0", runnerSourcePath, "-O", "-o", runner]);
+ return { pliego, runner, cargoTarget };
+}
+
+function measuredProcess(runner, command, arguments_, cwd) {
+ const result = run(runner, [command, ...arguments_], { cwd, timeout: 60_000 });
+ const metrics = JSON.parse(result.stdout.trim());
+ if (
+ metrics.schemaVersion !== 1 ||
+ !Number.isSafeInteger(metrics.elapsedNs) ||
+ metrics.elapsedNs <= 0 ||
+ !Number.isSafeInteger(metrics.peakWorkingSetBytes) ||
+ metrics.peakWorkingSetBytes <= 0 ||
+ metrics.exitCode !== 0
+ ) {
+ fail(`invalid process metrics: ${result.stdout}`);
+ }
+ return metrics;
+}
+
+function v4Input(packageAlias) {
+ return `@layer theme, utilities;\n@import "${packageAlias}/theme.css" layer(theme);\n@import "${packageAlias}/utilities.css" layer(utilities) source(none);\n@source "./fixture.html";\n@theme {\n --color-canvas: oklch(0.985 0.004 80);\n --color-surface: oklch(1 0 0);\n --color-surface-raised: oklch(0.96 0.006 80);\n --color-ink: oklch(0.19 0.012 70);\n --color-muted: oklch(0.5 0.014 70);\n --color-accent: oklch(0.58 0.19 32);\n --color-accent-strong: oklch(0.5 0.18 30);\n --color-line: oklch(0.88 0.008 75);\n}\n`;
+}
+
+function v3Config() {
+ return `module.exports = {\n content: ["./fixture.html"],\n corePlugins: { preflight: false },\n theme: {\n extend: {\n colors: {\n canvas: "oklch(0.985 0.004 80 / )",\n surface: "oklch(1 0 0 / )",\n "surface-raised": "oklch(0.96 0.006 80 / )",\n ink: "oklch(0.19 0.012 70 / )",\n muted: "oklch(0.5 0.014 70 / )",\n accent: "oklch(0.58 0.19 32 / )",\n "accent-strong": "oklch(0.5 0.18 30 / )",\n line: "oklch(0.88 0.008 75 / )"\n }\n }\n }\n};\n`;
+}
+
+function prepareLaneWorkspace(workspace, lane, corpus, html, pliego) {
+ const laneRoot = join(workspace, lane.id, corpus.id);
+ mkdirSync(laneRoot, { recursive: true });
+ const fixture = join(laneRoot, "fixture.html");
+ const styles = join(laneRoot, "styles.txt");
+ const tailwindInput = join(laneRoot, "input.css");
+ const tailwindOutput = join(laneRoot, "tailwind.css");
+ const pliegoOutput = join(laneRoot, "pliego.css");
+ const manifestPath = join(laneRoot, "manifest.json");
+ const manifestCss = join(laneRoot, "manifest.css");
+ const classValues = extractClassValues(html);
+ writeFileSync(fixture, html);
+ writeFileSync(styles, `${classValues.join("\n")}\n`);
+ if (lane.version.startsWith("3.")) {
+ writeFileSync(tailwindInput, "@tailwind utilities;\n");
+ writeFileSync(join(laneRoot, "tailwind.config.cjs"), v3Config());
+ } else {
+ writeFileSync(tailwindInput, v4Input(lane.packageAlias));
+ }
+ const cli = resolveLaneCli(lane);
+ const tailwindArguments = [cli, "-i", tailwindInput, "-o", tailwindOutput, "--minify"];
+ if (lane.version.startsWith("3.")) {
+ tailwindArguments.push("-c", join(laneRoot, "tailwind.config.cjs"));
+ }
+ const pliegoArguments = [
+ "compile",
+ "--input",
+ styles,
+ "--seed",
+ "--theme",
+ "--output",
+ pliegoOutput,
+ ];
+ run(process.execPath, tailwindArguments, { cwd: laneRoot, timeout: 60_000 });
+ run(pliego, pliegoArguments, { cwd: laneRoot, timeout: 60_000 });
+ run(
+ pliego,
+ [
+ ...pliegoArguments.slice(0, -1),
+ manifestCss,
+ "--manifest",
+ manifestPath,
+ ],
+ { cwd: laneRoot, timeout: 60_000 },
+ );
+ return {
+ laneRoot,
+ fixture,
+ styles,
+ tailwindOutput,
+ pliegoOutput,
+ manifestPath,
+ manifestCss,
+ classValues,
+ commands: {
+ pliego: { command: pliego, arguments: pliegoArguments },
+ tailwind: { command: process.execPath, arguments: tailwindArguments },
+ },
+ };
+}
+
+function outputSize(bytes) {
+ return {
+ rawBytes: bytes.byteLength,
+ gzipBytes: gzipSync(bytes, { level: 9 }).byteLength,
+ brotliBytes: brotliCompressSync(bytes, {
+ params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 11 },
+ }).byteLength,
+ sha256: sha256(bytes),
+ };
+}
+
+function generatedHtml(html, manifest) {
+ const bySource = new Map();
+ for (const style of manifest.styles) {
+ for (const origin of style.origins) {
+ const existing = bySource.get(origin.source);
+ if (existing && existing !== style.className) fail(`manifest maps ${origin.source} twice`);
+ bySource.set(origin.source, style.className);
+ }
+ }
+ let replacements = 0;
+ const rendered = html.replace(/class="([^"]*)"/gu, (_, source) => {
+ const replacement = bySource.get(source);
+ if (!replacement) fail(`manifest has no class for ${JSON.stringify(source)}`);
+ replacements += 1;
+ return `class="${replacement}"`;
+ });
+ return { bytes: Buffer.from(rendered, "utf8"), replacements };
+}
+
+function candidateCoverage(css, html) {
+ const tokens = [...new Set(extractClassValues(html).flatMap((value) => value.trim().split(/\s+/u).filter(Boolean)))];
+ const missing = tokens.filter((token) => !css.includes(`.${escapeClassName(token)}`));
+ return {
+ expected: tokens.length,
+ emitted: tokens.length - missing.length,
+ ratio: round((tokens.length - missing.length) / tokens.length),
+ missing,
+ };
+}
+
+function measureComparison(prepared, tools, html, warmups, pairs) {
+ const invoke = (kind, timed) => {
+ const command = prepared.commands[kind];
+ if (timed) {
+ return measuredProcess(tools.runner, command.command, command.arguments, prepared.laneRoot);
+ }
+ run(command.command, command.arguments, { cwd: prepared.laneRoot, timeout: 60_000 });
+ return null;
+ };
+ for (let index = 0; index < warmups; index += 1) {
+ const order = index % 2 === 0 ? ["pliego", "tailwind"] : ["tailwind", "pliego"];
+ for (const kind of order) invoke(kind, false);
+ }
+
+ const observations = [];
+ const hashes = { pliego: [], tailwind: [] };
+ for (let index = 0; index < pairs; index += 1) {
+ const order = index % 2 === 0 ? ["pliego", "tailwind"] : ["tailwind", "pliego"];
+ const record = { pair: index + 1, order };
+ for (const kind of order) {
+ record[kind] = invoke(kind, true);
+ hashes[kind].push(sha256(readFileSync(prepared[`${kind}Output`])));
+ }
+ record.deltaNs = record.pliego.elapsedNs - record.tailwind.elapsedNs;
+ record.tailwindToPliegoRatio = round(record.tailwind.elapsedNs / record.pliego.elapsedNs);
+ record.peakWorkingSetDeltaBytes =
+ record.pliego.peakWorkingSetBytes - record.tailwind.peakWorkingSetBytes;
+ observations.push(record);
+ }
+ for (const kind of ["pliego", "tailwind"]) {
+ const unique = new Set(hashes[kind]);
+ if (unique.size !== 1) fail(`${kind} emitted ${unique.size} hashes across measured pairs`);
+ rmSync(prepared[`${kind}Output`], { force: true });
+ invoke(kind, false);
+ const freshHash = sha256(readFileSync(prepared[`${kind}Output`]));
+ if (!unique.has(freshHash)) fail(`${kind} fresh output differs from measured output`);
+ }
+
+ const pliegoCss = readFileSync(prepared.pliegoOutput);
+ const tailwindCss = readFileSync(prepared.tailwindOutput);
+ const manifestCss = readFileSync(prepared.manifestCss);
+ if (!pliegoCss.equals(manifestCss)) fail("manifest generation changed PliegoCSS output");
+ const manifest = JSON.parse(readFileSync(prepared.manifestPath, "utf8"));
+ const rewritten = generatedHtml(html, manifest);
+ const pliegoCssSize = outputSize(pliegoCss);
+ const tailwindCssSize = outputSize(tailwindCss);
+ const utilityHtmlSize = outputSize(Buffer.from(html, "utf8"));
+ const generatedHtmlSize = outputSize(rewritten.bytes);
+ const coverage = candidateCoverage(tailwindCss.toString("utf8"), html);
+ if (coverage.ratio !== 1) {
+ fail(`Tailwind candidate coverage is ${coverage.emitted}/${coverage.expected}: ${coverage.missing.join(", ")}`);
+ }
+
+ const pliegoLatency = observations.map((pair) => pair.pliego.elapsedNs / 1_000_000);
+ const tailwindLatency = observations.map((pair) => pair.tailwind.elapsedNs / 1_000_000);
+ return {
+ pairs: observations,
+ summaries: {
+ pliegoLatency: summarize(pliegoLatency, "ms"),
+ tailwindLatency: summarize(tailwindLatency, "ms"),
+ pairedLatencyDelta: summarize(
+ observations.map((pair) => pair.deltaNs / 1_000_000),
+ "ms",
+ ),
+ tailwindToPliegoRatio: summarize(
+ observations.map((pair) => pair.tailwindToPliegoRatio),
+ "ratio",
+ ),
+ pliegoPeakWorkingSet: summarize(
+ observations.map((pair) => pair.pliego.peakWorkingSetBytes),
+ "bytes",
+ ),
+ tailwindPeakWorkingSet: summarize(
+ observations.map((pair) => pair.tailwind.peakWorkingSetBytes),
+ "bytes",
+ ),
+ pairedPeakWorkingSetDelta: summarize(
+ observations.map((pair) => pair.peakWorkingSetDeltaBytes),
+ "bytes",
+ ),
+ },
+ determinism: {
+ measuredPairs: pairs,
+ freshOutputVerified: true,
+ pliegoSha256: pliegoCssSize.sha256,
+ tailwindSha256: tailwindCssSize.sha256,
+ },
+ coverage,
+ output: {
+ pliego: {
+ css: pliegoCssSize,
+ html: generatedHtmlSize,
+ transferGzipBytes: pliegoCssSize.gzipBytes + generatedHtmlSize.gzipBytes,
+ transferBrotliBytes: pliegoCssSize.brotliBytes + generatedHtmlSize.brotliBytes,
+ classAttributesRewritten: rewritten.replacements,
+ },
+ tailwind: {
+ css: tailwindCssSize,
+ html: utilityHtmlSize,
+ transferGzipBytes: tailwindCssSize.gzipBytes + utilityHtmlSize.gzipBytes,
+ transferBrotliBytes: tailwindCssSize.brotliBytes + utilityHtmlSize.brotliBytes,
+ },
+ },
+ };
+}
+
+const authority = loadAuthority();
+const options = parseOptions(process.argv.slice(2), authority);
+const laneMap = canonicalLaneMap(authority);
+const lanes = options.laneIds.map((id) => laneMap.get(id) ?? fail(`unknown lane ${id}`));
+const corpusMap = new Map(authority.corpora.map((corpus) => [corpus.id, corpus]));
+const corpora = options.corpusIds.map((id) => corpusMap.get(id) ?? fail(`unknown corpus ${id}`));
+safeEvidencePath(options.evidence);
+const stateBefore = gitState();
+if (options.evidence && stateBefore.dirty) fail("evidence requires a clean Git worktree");
+const toolchainValue = toolchain();
+const tools = buildTools(toolchainValue);
+const runDirectory = join(benchmarkRoot, `run-${process.pid}-${Date.now()}`);
+mkdirSync(runDirectory, { recursive: true });
+
+try {
+ const results = {};
+ for (const lane of lanes) {
+ results[lane.id] = {
+ role: lane.role,
+ version: lane.version,
+ cliVersion: lane.cliVersion,
+ packageManifestSha256: sha256(readFileSync(readInstalledPackage(lane.packageAlias).path)),
+ cliManifestSha256: sha256(readFileSync(readInstalledPackage(lane.cliPackageAlias).path)),
+ corpora: {},
+ };
+ for (const corpus of corpora) {
+ process.stdout.write(`[measuring] ${lane.id}/${corpus.id}\n`);
+ const html = materializeCorpus(corpus);
+ const metrics = corpusMetrics(html);
+ if (JSON.stringify(metrics) !== JSON.stringify(corpus.expected)) {
+ fail(`${corpus.id} materialization drifted from oracle`);
+ }
+ const prepared = prepareLaneWorkspace(runDirectory, lane, corpus, html, tools.pliego);
+ results[lane.id].corpora[corpus.id] = {
+ fixture: metrics,
+ ...measureComparison(prepared, tools, html, options.warmups, options.pairs),
+ };
+ }
+ }
+ const stateAfter = gitState();
+ if (JSON.stringify(stateBefore) !== JSON.stringify(stateAfter)) fail("source tree changed during measurement");
+ const result = {
+ schemaVersion: 2,
+ benchmark: "PliegoCSS Benchmark Authority v2 paired competitor oracle",
+ generatedAtUtc: new Date().toISOString(),
+ mode: options.evidence ? "immutable-evidence" : options.smoke ? "smoke" : "machine-local",
+ authority: {
+ path: relative(repositoryRoot, authorityPath).replaceAll("\\", "/"),
+ sha256: sha256(readFileSync(authorityPath)),
+ observedAtUtc: authority.observedAtUtc,
+ expiresAtUtc: authority.expiresAtUtc,
+ primaryLane: authority.primaryLane,
+ resetContract: authority.resetContract,
+ },
+ provenance: {
+ git: stateBefore,
+ harness: { path: relative(repositoryRoot, harnessPath).replaceAll("\\", "/"), sha256: sha256(readFileSync(harnessPath)) },
+ processMetricsRunner: { path: relative(repositoryRoot, runnerSourcePath).replaceAll("\\", "/"), sha256: sha256(readFileSync(runnerSourcePath)) },
+ packageJsonSha256: sha256(readFileSync(packagePath)),
+ pnpmLockSha256: sha256(readFileSync(lockPath)),
+ executableSha256: sha256(readFileSync(tools.pliego)),
+ },
+ methodology: {
+ design: authority.methodology.design,
+ warmupPairs: options.warmups,
+ measuredPairs: options.pairs,
+ pairOrder: authority.methodology.pairOrder,
+ freshProcess: true,
+ completeSamples: true,
+ freshOutputVerification: true,
+ memoryMetric: authority.methodology.memoryMetric,
+ compression: authority.methodology.compression,
+ },
+ environment: {
+ os: { platform: process.platform, type: osType(), release: osRelease(), version: osVersion(), architecture: process.arch },
+ cpu: { models: [...new Set(cpus().map((cpu) => cpu.model))], logicalCoreCount: cpus().length, availableParallelism: availableParallelism() },
+ totalMemoryBytes: totalmem(),
+ node: process.version,
+ zlib: process.versions.zlib,
+ toolchain: toolchainValue,
+ cargoTarget: relative(repositoryRoot, tools.cargoTarget).replaceAll("\\", "/"),
+ powerPlan: capturePowerPlan(),
+ securityTooling: captureSecurityTooling(options.evidence),
+ },
+ lanes: results,
+ claimBoundary: authority.methodology.scorePolicy,
+ };
+ const serialized = `${JSON.stringify(result, null, 2)}\n`;
+ mkdirSync(dirname(localResultPath), { recursive: true });
+ writeFileSync(localResultPath, serialized);
+ if (options.evidence) {
+ mkdirSync(dirname(options.evidence), { recursive: true });
+ writeFileSync(options.evidence, serialized, { flag: "wx" });
+ }
+ process.stdout.write(
+ `${JSON.stringify({ schemaVersion: 2, status: "passed", mode: result.mode, lanes: Object.keys(results), corpora: options.corpusIds, pairs: options.pairs, result: relative(repositoryRoot, options.evidence ?? localResultPath).replaceAll("\\", "/") }, null, 2)}\n`,
+ );
+} finally {
+ const child = relative(benchmarkRoot, runDirectory);
+ if (!child || child === ".." || child.startsWith(`..${sep}`) || isAbsolute(child) || !basename(runDirectory).startsWith("run-")) {
+ fail(`refusing to remove unsafe benchmark run directory ${runDirectory}`);
+ }
+ rmSync(runDirectory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
+}
diff --git a/scripts/measure-media-merge.mjs b/scripts/measure-media-merge.mjs
index e3b61e3..169ff8e 100644
--- a/scripts/measure-media-merge.mjs
+++ b/scripts/measure-media-merge.mjs
@@ -25,28 +25,28 @@ const expected = {
profiles: {
utilitiesOnly: {
control: {
- sha256: "acfe0a07ca05336b89504bc4c919d09afaf26f3a07c2cabb53fd7be3af2f7f7c",
- rawBytes: 1_447,
- gzipBytes: 648,
+ sha256: "efee258d711879f4e7c89e134c88c9c797d65e524727878a72e474894f17d2c2",
+ rawBytes: 1_449,
+ gzipBytes: 649,
mediaWrappers: 20,
},
candidate: {
- sha256: "3eb3494eb3b128ab1ce84b1546cc08d17c179b2710175016e25e331d10070287",
- rawBytes: 1_010,
- gzipBytes: 638,
+ sha256: "ac316f5581edfef5562ade30dc758815ab724665cade8b42ea748349d293c5f1",
+ rawBytes: 1_012,
+ gzipBytes: 637,
mediaWrappers: 1,
},
},
themeAndUtilities: {
control: {
- sha256: "4e241be64c52679bfbe5b2966fccf8b00307fdd0bd06c3af8675c95b8a0d0eb3",
- rawBytes: 1_818,
- gzipBytes: 822,
+ sha256: "908dbf7df76c4598f89bf9bcf9c6b3b91918ca964275a6e1536d11419d31d608",
+ rawBytes: 1_820,
+ gzipBytes: 821,
mediaWrappers: 20,
},
candidate: {
- sha256: "5697d5c24f2a2441519754cb6e3bae0ae8e46e34636964a0c3118be97cbd625b",
- rawBytes: 1_381,
+ sha256: "05b254d481de13dd11d235308e5d8bf3dc977829dec3e71f1fc042b2f7793f45",
+ rawBytes: 1_383,
gzipBytes: 809,
mediaWrappers: 1,
},
@@ -54,15 +54,15 @@ const expected = {
},
neutralFixtures: {
gateA: {
- sha256: "12b6d497962ec8ae30d1c9343ca180f65a81f628161d038a653ac7be32fd6688",
+ sha256: "1d879683af938faba7a7e44a0ad330e9f2f6a5f6b363b0f1556cc56711cd6f5b",
rawBytes: 6_548,
- gzipBytes: 1_594,
+ gzipBytes: 1_596,
mediaWrappers: 0,
},
gateB: {
- sha256: "d873a432d1174af4715f7cb33359c6ad2781f611f9712e3a72b5630aa6f1bffc",
- rawBytes: 10_468,
- gzipBytes: 2_047,
+ sha256: "757e259d072723eb06ea5517627ac3422e73c16f5eb36b6ecc57c8aa104c126d",
+ rawBytes: 10_461,
+ gzipBytes: 2_046,
mediaWrappers: 12,
},
},
diff --git a/scripts/measure-pliego-gate-a.mjs b/scripts/measure-pliego-gate-a.mjs
index 6ff344c..4a43723 100644
--- a/scripts/measure-pliego-gate-a.mjs
+++ b/scripts/measure-pliego-gate-a.mjs
@@ -663,7 +663,7 @@ try {
manifest.schemaVersion !== 3 ||
manifest.styleIdFormatVersion !== 2 ||
manifest.classNameFormatVersion !== 1 ||
- manifest.themeIdFormatVersion !== 2
+ manifest.themeIdFormatVersion !== 3
) {
throw new Error(
`expected manifest schema/identity formats 3/2/1/2, received ${manifest.schemaVersion}/${manifest.styleIdFormatVersion}/${manifest.classNameFormatVersion}/${manifest.themeIdFormatVersion}`,
diff --git a/scripts/measure-pliego-gate-b.mjs b/scripts/measure-pliego-gate-b.mjs
index 7aea6c1..0522536 100644
--- a/scripts/measure-pliego-gate-b.mjs
+++ b/scripts/measure-pliego-gate-b.mjs
@@ -1,867 +1,3 @@
-import { createHash, randomUUID } from "node:crypto";
-import { spawnSync } from "node:child_process";
-import { lstat, mkdir, mkdtemp, open, readFile, realpath, rm, writeFile } from "node:fs/promises";
-import {
- availableParallelism,
- cpus,
- release as osRelease,
- tmpdir,
- totalmem,
- type as osType,
- version as osVersion,
-} from "node:os";
-import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
-import { fileURLToPath } from "node:url";
-import { gzipSync } from "node:zlib";
-
-const harnessPath = fileURLToPath(import.meta.url);
-const scriptDirectory = dirname(harnessPath);
-const repositoryRoot = resolve(scriptDirectory, "..");
-const fixturePath = join(repositoryRoot, "benchmarks", "tailwind-v4", "fixtures.html");
-const resultsPath = join(repositoryRoot, "benchmarks", "results", "pliego-gate-b.local.json");
-const evidenceRoot = join(repositoryRoot, "benchmarks", "evidence");
-const cargoTargetDirectory = join(repositoryRoot, "target", "benchmarks", "pliego-gate-b");
-const cargoHomeDirectory = join(repositoryRoot, "target", "benchmarks", "cargo-home-gate-b");
-const runLockPath = join(repositoryRoot, "target", "benchmarks", "locks", "pliego-gate-b.lock");
-const evidencePath = parseEvidencePath(process.argv.slice(2));
-const measuredProcessCount = 30;
-const warmupProcessCount = 5;
-const expectedClassAttributeCount = 44;
-const requiredVariants = [
- "disabled",
- "focus",
- "focus-visible",
- "hover",
- "lg",
- "md",
- "placeholder",
- "sm",
-];
-const emittedVariantMarkers = {
- disabled: ":disabled{",
- focus: ":focus{",
- "focus-visible": ":focus-visible{",
- hover: ":hover{",
- lg: "@media (width>=64rem)",
- md: "@media (width>=48rem)",
- placeholder: "::placeholder{",
- sm: "@media (width>=40rem)",
-};
-const frozenTailwind = {
- version: "4.3.2",
- generated_at: "2026-07-12T22:30:41.861Z",
- profiles: {
- "full-complete": {
- median_ms: 210.202,
- min_ms: 187.341,
- p95_ms: 318.531,
- css_raw_bytes: 12_377,
- css_gzip_bytes: 3_333,
- html_gzip_bytes: 1_506,
- transfer_gzip_bytes: 4_839,
- },
- "no-preflight-complete": {
- median_ms: 204.826,
- min_ms: 185.374,
- p95_ms: 418.003,
- css_raw_bytes: 8_618,
- css_gzip_bytes: 2_327,
- html_gzip_bytes: 1_506,
- transfer_gzip_bytes: 3_833,
- },
- },
-};
-const cargoInfluencePatterns = [
- /^CARGO_BUILD_/iu,
- /^CARGO_ENCODED_RUSTFLAGS$/iu,
- /^CARGO_HOME$/iu,
- /^CARGO_INCREMENTAL$/iu,
- /^CARGO_PROFILE_/iu,
- /^CARGO_TARGET_/iu,
- /^CARGO_TERM_/iu,
- /^RUSTC$/iu,
- /^RUSTC_BOOTSTRAP$/iu,
- /^RUSTC_WORKSPACE_WRAPPER$/iu,
- /^RUSTC_WRAPPER$/iu,
- /^RUSTDOCFLAGS$/iu,
- /^RUSTFLAGS$/iu,
- /^RUSTUP_TOOLCHAIN$/iu,
-];
-
-function sanitizedCargoEnvironmentKeys() {
- return Object.keys(process.env)
- .filter((key) => cargoInfluencePatterns.some((pattern) => pattern.test(key)))
- .sort();
-}
-
-function controlledCargoEnvironment() {
- const environment = { ...process.env };
- for (const key of sanitizedCargoEnvironmentKeys()) {
- delete environment[key];
- }
- return {
- ...environment,
- CARGO_HOME: cargoHomeDirectory,
- CARGO_INCREMENTAL: "0",
- CARGO_TARGET_DIR: cargoTargetDirectory,
- CARGO_TERM_COLOR: "never",
- };
-}
-
-function parseEvidencePath(arguments_) {
- if (arguments_.length === 0) {
- return null;
- }
- if (arguments_.length !== 2 || arguments_[0] !== "--evidence" || !arguments_[1]) {
- throw new Error(
- "usage: node scripts/measure-pliego-gate-b.mjs [--evidence benchmarks/evidence/.json]",
- );
- }
- if (arguments_[1].includes("\0")) {
- throw new Error("evidence path contains a NUL byte");
- }
- const candidate = resolve(repositoryRoot, arguments_[1]);
- const child = relative(evidenceRoot, candidate);
- if (
- !child ||
- child === ".." ||
- child.startsWith(`..${sep}`) ||
- isAbsolute(child) ||
- extname(candidate).toLowerCase() !== ".json"
- ) {
- throw new Error("--evidence must name a .json file below benchmarks/evidence/");
- }
- return candidate;
-}
-
-function run(command, commandArguments, options = {}) {
- const result = spawnSync(command, commandArguments, {
- cwd: repositoryRoot,
- encoding: "utf8",
- windowsHide: true,
- ...options,
- });
- if (result.error) {
- throw result.error;
- }
- if (result.status !== 0) {
- throw new Error(
- `${command} ${commandArguments.join(" ")} failed with exit code ${result.status}\n${result.stderr}`,
- );
- }
- return result;
-}
-
-function probeCommand(command, commandArguments) {
- const result = spawnSync(command, commandArguments, {
- cwd: repositoryRoot,
- encoding: "utf8",
- windowsHide: true,
- });
- const source = `${command} ${commandArguments.join(" ")}`;
- if (result.error) {
- return {
- status: result.error.code === "ENOENT" ? "unavailable" : "error",
- source,
- value: null,
- detail: result.error.message,
- };
- }
- if (result.status !== 0) {
- return {
- status: "error",
- source,
- value: null,
- exit_code: result.status,
- detail: `${result.stderr ?? ""}`.trim().slice(0, 2_048) || null,
- };
- }
- const value = `${result.stdout ?? ""}`.trim().replaceAll("\r\n", "\n");
- return {
- status: value ? "recorded" : "empty",
- source,
- value: value || null,
- };
-}
-
-function sha256(bytes) {
- return createHash("sha256").update(bytes).digest("hex");
-}
-
-function round(value) {
- return Number(value.toFixed(3));
-}
-
-function percentile(sorted, percentage) {
- const index = Math.ceil((percentage / 100) * sorted.length) - 1;
- return sorted[Math.max(0, Math.min(index, sorted.length - 1))];
-}
-
-function conventionalMedian(sorted) {
- const middle = Math.floor(sorted.length / 2);
- return sorted.length % 2 === 0
- ? (sorted[middle - 1] + sorted[middle]) / 2
- : sorted[middle];
-}
-
-function summarize(samples) {
- const sorted = [...samples].sort((left, right) => left - right);
- const mean = sorted.reduce((sum, sample) => sum + sample, 0) / sorted.length;
- const median = conventionalMedian(sorted);
- const absoluteDeviations = sorted
- .map((sample) => Math.abs(sample - median))
- .sort((left, right) => left - right);
- const variance =
- sorted.reduce((sum, sample) => sum + (sample - mean) ** 2, 0) / sorted.length;
- return {
- runs: sorted.length,
- min_ms: round(sorted[0]),
- median_ms: round(median),
- mean_ms: round(mean),
- p95_ms: round(percentile(sorted, 95)),
- max_ms: round(sorted.at(-1)),
- median_absolute_deviation_ms: round(conventionalMedian(absoluteDeviations)),
- standard_deviation_ms: round(Math.sqrt(variance)),
- samples_ms: samples.map(round),
- };
-}
-
-function verifySummaryContract() {
- const even = summarize([4, 1, 3, 2]);
- const odd = summarize([3, 1, 2]);
- if (
- even.median_ms !== 2.5 ||
- even.median_absolute_deviation_ms !== 1 ||
- odd.median_ms !== 2 ||
- odd.median_absolute_deviation_ms !== 1
- ) {
- throw new Error("benchmark summary contract is not conventional median/MAD");
- }
-}
-
-verifySummaryContract();
-
-function toolVersion(command, commandArguments) {
- return run(command, commandArguments).stdout.trim();
-}
-
-function captureToolchain() {
- const cargo = toolVersion("cargo", ["+1.96.0", "--version"]);
- const rustc = toolVersion("rustc", ["+1.96.0", "--version"]);
- const rustcVerbose = toolVersion("rustc", ["+1.96.0", "-vV"]);
- if (!/^cargo 1\.96\.0\b/u.test(cargo) || !/^rustc 1\.96\.0\b/u.test(rustc)) {
- throw new Error(`benchmark requires exact Cargo/rustc 1.96.0; found ${cargo} / ${rustc}`);
- }
- const hostTarget = /^host: (\S+)$/mu.exec(rustcVerbose)?.[1];
- if (!hostTarget) {
- throw new Error(`cannot determine the Rust 1.96.0 host target from:\n${rustcVerbose}`);
- }
- return { cargo, rustc, host_target: hostTarget };
-}
-
-function captureGitState() {
- const commit = toolVersion("git", ["rev-parse", "HEAD"]);
- if (!/^[0-9a-f]{40}$/u.test(commit)) {
- throw new Error(`unexpected Git commit identity: ${commit}`);
- }
- const status = run("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"]).stdout;
- const entries = status.split("\0").filter(Boolean);
- return {
- commit,
- dirty: entries.length > 0,
- status_entry_count: entries.length,
- status_sha256: sha256(Buffer.from(status, "utf8")),
- };
-}
-
-function trackedInputProvenance(commit, absolutePath, bytes) {
- const path = relative(repositoryRoot, absolutePath).replaceAll("\\", "/");
- if (!path || path.startsWith("../") || isAbsolute(path)) {
- throw new Error(`tracked benchmark input escapes the repository: ${absolutePath}`);
- }
- if (evidencePath) {
- const result = spawnSync("git", ["show", "--no-textconv", `${commit}:${path}`], {
- cwd: repositoryRoot,
- encoding: null,
- maxBuffer: 16 * 1024 * 1024,
- windowsHide: true,
- });
- if (result.error) {
- throw new Error(`cannot read tracked benchmark input ${commit}:${path}: ${result.error.message}`);
- }
- if (result.status !== 0) {
- throw new Error(
- `cannot read tracked benchmark input ${commit}:${path}: ${result.stderr.toString("utf8").trim()}`,
- );
- }
- if (!result.stdout.equals(bytes)) {
- throw new Error(
- `benchmark input ${path} differs byte-for-byte from ${commit}; normalize or restore it before recording evidence`,
- );
- }
- }
- return { path, sha256: sha256(bytes) };
-}
-
-function securityToolingMetadata() {
- const reportedStatus = process.env.PLIEGO_BENCH_SECURITY_TOOLING_STATUS?.trim().toLowerCase();
- const tooling = process.env.PLIEGO_BENCH_SECURITY_TOOLING?.trim() || null;
- if (!reportedStatus) {
- if (tooling) {
- throw new Error(
- "PLIEGO_BENCH_SECURITY_TOOLING requires PLIEGO_BENCH_SECURITY_TOOLING_STATUS",
- );
- }
- if (evidencePath) {
- throw new Error("--evidence requires PLIEGO_BENCH_SECURITY_TOOLING_STATUS");
- }
- return {
- status: "not-reported",
- tooling: null,
- source: "PLIEGO_BENCH_SECURITY_TOOLING_STATUS/PLIEGO_BENCH_SECURITY_TOOLING",
- };
- }
- const allowed = new Set(["active", "disabled", "not-installed", "unknown"]);
- if (!allowed.has(reportedStatus)) {
- throw new Error(
- "PLIEGO_BENCH_SECURITY_TOOLING_STATUS must be active, disabled, not-installed, or unknown",
- );
- }
- if ((reportedStatus === "active" || reportedStatus === "disabled") && !tooling) {
- throw new Error(`${reportedStatus} security tooling requires PLIEGO_BENCH_SECURITY_TOOLING`);
- }
- return {
- status: reportedStatus,
- tooling,
- source: "environment",
- };
-}
-
-async function powerPlanMetadata() {
- const declared = process.env.PLIEGO_BENCH_POWER_PLAN?.trim();
- if (declared) {
- return { status: "declared", source: "PLIEGO_BENCH_POWER_PLAN", value: declared };
- }
- if (process.platform === "win32") {
- return probeCommand("powercfg", ["/getactivescheme"]);
- }
- if (process.platform === "darwin") {
- return probeCommand("pmset", ["-g"]);
- }
- if (process.platform === "linux") {
- const profile = probeCommand("powerprofilesctl", ["get"]);
- if (profile.status === "recorded") {
- return profile;
- }
- const governorPath = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor";
- try {
- const governor = (await readFile(governorPath, "utf8")).trim();
- return {
- status: governor ? "recorded" : "empty",
- source: governorPath,
- value: governor || null,
- fallback_from: profile,
- };
- } catch (error) {
- return {
- status: error.code === "ENOENT" ? "unavailable" : "error",
- source: "powerprofilesctl/sysfs",
- value: null,
- detail: error.message,
- powerprofilesctl: profile,
- };
- }
- }
- return {
- status: "unsupported",
- source: null,
- value: null,
- detail: `no power-plan probe for ${process.platform}`,
- };
-}
-
-async function captureEnvironment(toolchain) {
- const cpuRecords = cpus();
- return {
- os: {
- platform: process.platform,
- type: osType(),
- release: osRelease(),
- version: osVersion(),
- architecture: process.arch,
- },
- cpu: {
- models: [...new Set(cpuRecords.map((cpu) => cpu.model))],
- logical_core_count: cpuRecords.length,
- available_parallelism: availableParallelism(),
- },
- memory: { total_bytes: totalmem() },
- node: process.version,
- zlib: process.versions.zlib,
- toolchain,
- build: {
- host_target: toolchain.host_target,
- cargo_target_dir: relative(repositoryRoot, cargoTargetDirectory).replaceAll("\\", "/"),
- cargo_incremental: false,
- cargo_home: relative(repositoryRoot, cargoHomeDirectory).replaceAll("\\", "/"),
- cargo_home_policy: "isolated benchmark directory without user Cargo configuration",
- sanitized_environment_keys: sanitizedCargoEnvironmentKeys(),
- },
- power_plan: await powerPlanMetadata(),
- security_tooling: securityToolingMetadata(),
- };
-}
-
-async function prepareCargoHome() {
- await mkdir(cargoHomeDirectory, { recursive: true });
- const info = await lstat(cargoHomeDirectory);
- const targetRoot = await realpath(join(repositoryRoot, "target"));
- if (
- !info.isDirectory() ||
- info.isSymbolicLink() ||
- !pathIsWithin(targetRoot, await realpath(cargoHomeDirectory))
- ) {
- throw new Error("benchmark CARGO_HOME must be a real directory below target/");
- }
- for (const name of ["config", "config.toml"]) {
- if (await lstatOrNull(join(cargoHomeDirectory, name))) {
- throw new Error(`benchmark CARGO_HOME must not contain user configuration: ${name}`);
- }
- }
-}
-
-async function acquireRunLock() {
- await mkdir(dirname(runLockPath), { recursive: true });
- const token = randomUUID();
- let handle;
- try {
- handle = await open(runLockPath, "wx", 0o600);
- await handle.writeFile(
- `${JSON.stringify({ pid: process.pid, started_at: new Date().toISOString(), token })}\n`,
- "utf8",
- );
- } catch (error) {
- if (handle) {
- await handle.close();
- await rm(runLockPath, { force: true });
- }
- if (error.code === "EEXIST") {
- throw new Error(`another Gate B benchmark may be running: ${runLockPath}`);
- }
- throw error;
- }
- return { handle, token };
-}
-
-async function releaseRunLock(lock) {
- const current = JSON.parse(await readFile(runLockPath, "utf8"));
- await lock.handle.close();
- if (current.token !== lock.token) {
- throw new Error(`Gate B run lock ownership changed: ${runLockPath}`);
- }
- await rm(runLockPath);
-}
-
-function pathIsWithin(root, candidate) {
- const child = relative(root, candidate);
- return child === "" || (child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child));
-}
-
-async function lstatOrNull(path) {
- try {
- return await lstat(path);
- } catch (error) {
- if (error.code === "ENOENT") {
- return null;
- }
- throw error;
- }
-}
-
-async function prepareEvidenceDestination(destination) {
- if (!destination) {
- return;
- }
- await mkdir(evidenceRoot, { recursive: true });
- const rootInfo = await lstat(evidenceRoot);
- if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink()) {
- throw new Error("benchmarks/evidence must be a real directory, not a link");
- }
- const physicalRoot = await realpath(evidenceRoot);
- const parts = relative(evidenceRoot, destination).split(sep);
- let current = evidenceRoot;
- for (const part of parts.slice(0, -1)) {
- current = join(current, part);
- let info = await lstatOrNull(current);
- if (!info) {
- await mkdir(current);
- info = await lstat(current);
- }
- if (!info.isDirectory() || info.isSymbolicLink()) {
- throw new Error(`evidence parent is not a real directory: ${current}`);
- }
- const physical = await realpath(current);
- if (!pathIsWithin(physicalRoot, physical)) {
- throw new Error(`evidence parent escapes benchmarks/evidence: ${current}`);
- }
- }
- if (await lstatOrNull(destination)) {
- throw new Error(`evidence snapshots are immutable and already exist: ${destination}`);
- }
-}
-
-async function safelyRemoveWorkspace(path, prefix) {
- const temporaryRoot = resolve(tmpdir());
- const candidate = resolve(path);
- const child = relative(temporaryRoot, candidate);
- if (
- !child ||
- child === ".." ||
- child.startsWith(`..${sep}`) ||
- isAbsolute(child) ||
- !basename(candidate).startsWith(prefix)
- ) {
- throw new Error(`refusing to remove unsafe benchmark workspace: ${candidate}`);
- }
- await rm(candidate, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
-}
-
-function sizeReport(bytes, cssGzipBytes) {
- const gzipBytes = gzipSync(bytes, { level: 9 }).byteLength;
- return {
- raw_bytes: bytes.byteLength,
- gzip_bytes: gzipBytes,
- transfer_html_css_gzip_bytes: gzipBytes + cssGzipBytes,
- sha256: sha256(bytes),
- };
-}
-
-function percentChange(reference, candidate) {
- return round(((candidate - reference) / reference) * 100);
-}
-
-function replaceClassValues(html, classesBySource) {
- let replacements = 0;
- const rendered = html.replace(/class="([^"]*)"/gu, (_, source) => {
- const className = classesBySource.get(source);
- if (typeof className !== "string") {
- throw new Error(`manifest has no className for class attribute: ${source}`);
- }
- replacements += 1;
- return `class="${className}"`;
- });
- if (replacements !== expectedClassAttributeCount) {
- throw new Error(
- `expected ${expectedClassAttributeCount} class replacements, performed ${replacements}`,
- );
- }
- const withoutClassValues = (value) => value.replace(/class="[^"]*"/gu, 'class=""');
- if (withoutClassValues(html) !== withoutClassValues(rendered)) {
- throw new Error("generated HTML changed bytes outside class attribute values");
- }
- return { bytes: Buffer.from(rendered, "utf8"), replacements };
-}
-
-const runLock = await acquireRunLock();
-try {
-const harnessBytesBefore = await readFile(harnessPath);
-const gitStateBefore = captureGitState();
-if (evidencePath && gitStateBefore.dirty) {
- throw new Error("--evidence requires a clean Git worktree");
-}
-const harnessProvenance = trackedInputProvenance(
- gitStateBefore.commit,
- harnessPath,
- harnessBytesBefore,
-);
-const toolchain = captureToolchain();
-const environment = await captureEnvironment(toolchain);
-const cargoEnvironment = controlledCargoEnvironment();
-const executablePath = join(
- cargoTargetDirectory,
- toolchain.host_target,
- "release",
- process.platform === "win32" ? "pliego-cssc.exe" : "pliego-cssc",
-);
-await prepareEvidenceDestination(evidencePath);
-await prepareCargoHome();
-
-console.log("Building pliego-cssc in release mode (excluded from timings)...");
-run(
- "cargo",
- [
- "+1.96.0",
- "build",
- "--release",
- "--locked",
- "--target",
- toolchain.host_target,
- "-p",
- "pliego-cssc",
- ],
- { env: cargoEnvironment },
-);
-
-const workspace = await mkdtemp(join(tmpdir(), "pliego-gate-b-"));
-try {
- const fixtureBefore = await readFile(fixturePath);
- const fixtureProvenance = trackedInputProvenance(
- gitStateBefore.commit,
- fixturePath,
- fixtureBefore,
- );
- const fixtureHtml = fixtureBefore.toString("utf8");
- const classValues = [...fixtureHtml.matchAll(/class="([^"]*)"/gu)].map(
- (match) => match[1],
- );
- if (classValues.length !== expectedClassAttributeCount) {
- throw new Error(
- `complete fixture must contain ${expectedClassAttributeCount} class attributes, found ${classValues.length}`,
- );
- }
-
- const uniqueStyleInputs = new Set(classValues);
- const utilityTokens = classValues.flatMap((value) => value.split(/\s+/u));
- const variants = [...new Set(
- utilityTokens.flatMap((token) => {
- const segments = token.split(":");
- return segments.length > 1 ? segments.slice(0, -1) : [];
- }),
- )].sort();
- const missingVariants = requiredVariants.filter((variant) => !variants.includes(variant));
- if (missingVariants.length > 0) {
- throw new Error(`complete fixture is missing required variants: ${missingVariants.join(", ")}`);
- }
-
- const inputPath = join(workspace, "complete-styles.txt");
- const inputBytes = Buffer.from(`${classValues.join("\n")}\n`, "utf8");
- await writeFile(inputPath, inputBytes);
-
- const warmupOutputPath = join(workspace, "warmup.css");
- for (let index = 0; index < warmupProcessCount; index += 1) {
- run(executablePath, [
- "compile",
- "--input",
- inputPath,
- "--seed",
- "--theme",
- "--output",
- warmupOutputPath,
- ]);
- }
-
- const elapsedMilliseconds = [];
- const outputs = [];
- const hashes = [];
- for (let index = 0; index < measuredProcessCount; index += 1) {
- const outputPath = join(workspace, `gate-b-${index}.css`);
- const startedAt = process.hrtime.bigint();
- run(executablePath, [
- "compile",
- "--input",
- inputPath,
- "--seed",
- "--theme",
- "--output",
- outputPath,
- ]);
- const stoppedAt = process.hrtime.bigint();
- const css = await readFile(outputPath);
- elapsedMilliseconds.push(Number(stoppedAt - startedAt) / 1_000_000);
- outputs.push(css);
- hashes.push(sha256(css));
- }
-
- const uniqueHashes = [...new Set(hashes)];
- if (uniqueHashes.length !== 1) {
- throw new Error(`non-deterministic output: observed ${uniqueHashes.length} SHA-256 hashes`);
- }
- const fixtureAfter = await readFile(fixturePath);
- if (!fixtureBefore.equals(fixtureAfter)) {
- throw new Error("tracked complete fixture changed during measurement");
- }
-
- const css = outputs[0];
- const cssText = css.toString("utf8");
- const missingEmittedVariants = Object.entries(emittedVariantMarkers)
- .filter(([, marker]) => !cssText.includes(marker))
- .map(([variant]) => variant);
- if (missingEmittedVariants.length > 0) {
- throw new Error(
- `compiled CSS is missing markers for variants: ${missingEmittedVariants.join(", ")}`,
- );
- }
- const compressedCss = gzipSync(css, { level: 9 });
- const manifestPath = join(workspace, "manifest.json");
- const manifestOutputPath = join(workspace, "manifest.css");
- run(executablePath, [
- "compile",
- "--input",
- inputPath,
- "--seed",
- "--theme",
- "--output",
- manifestOutputPath,
- "--manifest",
- manifestPath,
- ]);
- const manifestBytes = await readFile(manifestPath);
- const manifestCss = await readFile(manifestOutputPath);
- if (!css.equals(manifestCss)) {
- throw new Error("manifest generation changed the compiled CSS artifact");
- }
- const manifest = JSON.parse(manifestBytes.toString("utf8"));
- if (
- manifest.schemaVersion !== 3 ||
- manifest.styleIdFormatVersion !== 2 ||
- manifest.classNameFormatVersion !== 1 ||
- manifest.themeIdFormatVersion !== 2
- ) {
- throw new Error(
- `expected manifest schema/identity formats 3/2/1/2, received ${manifest.schemaVersion}/${manifest.styleIdFormatVersion}/${manifest.classNameFormatVersion}/${manifest.themeIdFormatVersion}`,
- );
- }
- const classesBySource = new Map();
- for (const style of manifest.styles) {
- for (const origin of style.origins) {
- const existing = classesBySource.get(origin.source);
- if (existing && existing !== style.className) {
- throw new Error(`manifest maps ${origin.source} to multiple classes`);
- }
- classesBySource.set(origin.source, style.className);
- }
- }
- if (classesBySource.size !== uniqueStyleInputs.size) {
- throw new Error(
- `expected ${uniqueStyleInputs.size} manifest styles, received ${classesBySource.size}`,
- );
- }
- if (new Set(classesBySource.values()).size !== classesBySource.size) {
- throw new Error("manifest contains a generated class collision");
- }
-
- const generatedHtml = replaceClassValues(fixtureHtml, classesBySource);
- const utilityHtmlReport = sizeReport(fixtureBefore, compressedCss.byteLength);
- const generatedClassHtmlReport = sizeReport(generatedHtml.bytes, compressedCss.byteLength);
- const timing = summarize(elapsedMilliseconds);
- const output = {
- css_raw_bytes: css.byteLength,
- css_gzip_bytes: compressedCss.byteLength,
- css_sha256: uniqueHashes[0],
- executable_sha256: sha256(await readFile(executablePath)),
- manifest_sha256: sha256(manifestBytes),
- transfer_gzip_bytes: generatedClassHtmlReport.transfer_html_css_gzip_bytes,
- html_unchanged_outside_class_values: true,
- html: {
- utility_strings: utilityHtmlReport,
- generated_classes: generatedClassHtmlReport,
- },
- };
- const comparisons = Object.fromEntries(
- Object.entries(frozenTailwind.profiles).map(([profile, baseline]) => [
- profile,
- {
- median_time_change_percent: percentChange(baseline.median_ms, timing.median_ms),
- median_speed_ratio: round(baseline.median_ms / timing.median_ms),
- css_raw_change_percent: percentChange(baseline.css_raw_bytes, output.css_raw_bytes),
- css_gzip_change_percent: percentChange(baseline.css_gzip_bytes, output.css_gzip_bytes),
- transfer_gzip_change_percent: percentChange(
- baseline.transfer_gzip_bytes,
- output.transfer_gzip_bytes,
- ),
- },
- ]),
- );
- const harnessBytesAfter = await readFile(harnessPath);
- if (!harnessBytesBefore.equals(harnessBytesAfter)) {
- throw new Error("benchmark harness changed during measurement");
- }
- const gitStateAfter = captureGitState();
- if (JSON.stringify(gitStateBefore) !== JSON.stringify(gitStateAfter)) {
- throw new Error("Git commit or worktree state changed during measurement");
- }
- const result = {
- schema_version: 3,
- benchmark: "PliegoCSS Gate B complete-fixture fresh-process compile",
- profile: "complete",
- generated_at: new Date().toISOString(),
- provenance: {
- git: gitStateBefore,
- harness: harnessProvenance,
- tracked_inputs: [fixtureProvenance],
- },
- fixture: {
- html: "benchmarks/tailwind-v4/fixtures.html",
- groups: ["button", "card", "navbar", "form", "dashboard"],
- class_attribute_count: classValues.length,
- class_attributes_resolved: generatedHtml.replacements,
- all_class_attributes_resolved: generatedHtml.replacements === classValues.length,
- unique_style_inputs: uniqueStyleInputs.size,
- utility_occurrences: utilityTokens.length,
- unique_utility_tokens: new Set(utilityTokens).size,
- variants,
- required_variants: requiredVariants,
- emitted_variant_markers_verified: Object.keys(emittedVariantMarkers).sort(),
- theme_included: true,
- html_sha256: sha256(fixtureBefore),
- derived_input_sha256: sha256(inputBytes),
- },
- environment,
- command:
- "pliego-cssc compile --input --seed --theme --output ",
- statistics: {
- median: "average of the two middle values for even samples",
- p95: "nearest-rank",
- median_absolute_deviation: "conventional median of absolute deviations from the median",
- },
- timing: {
- warmup_processes: warmupProcessCount,
- ...timing,
- },
- determinism: {
- verified_runs: measuredProcessCount,
- deterministic: true,
- sha256: uniqueHashes[0],
- },
- output,
- frozen_tailwind_baseline: frozenTailwind,
- comparisons,
- caveats: [
- "Fresh-process timings retain normal OS caches and exclude the release build.",
- "Manifest generation and HTML rewriting are verified outside the timed region.",
- "PliegoCSS does not emit reset/preflight; no-preflight-complete is the primary payload comparison.",
- "The generated-class HTML and Tailwind utility HTML preserve the same DOM but have different class-value bytes.",
- ],
- };
-
- const serializedResult = `${JSON.stringify(result, null, 2)}\n`;
- await mkdir(dirname(resultsPath), { recursive: true });
- await writeFile(resultsPath, serializedResult, "utf8");
- if (evidencePath) {
- await writeFile(evidencePath, serializedResult, { encoding: "utf8", flag: "wx" });
- }
-
- console.log(
- `Gate B complete: ${generatedHtml.replacements}/${classValues.length} class attributes resolved (${uniqueStyleInputs.size} unique styles), ${warmupProcessCount} warmups + ${measuredProcessCount} measured fresh processes`,
- );
- console.log(`Variants: ${variants.join(", ")}`);
- console.log(
- `Timing: min ${timing.min_ms} ms | median ${timing.median_ms} ms | p95 ${timing.p95_ms} ms | max ${timing.max_ms} ms`,
- );
- console.log(`CSS: ${output.css_raw_bytes} B raw | ${output.css_gzip_bytes} B gzip`);
- console.log(
- `Generated HTML: ${generatedClassHtmlReport.raw_bytes} B raw | ${generatedClassHtmlReport.gzip_bytes} B gzip | ${generatedClassHtmlReport.transfer_html_css_gzip_bytes} B transfer`,
- );
- console.log(
- `Determinism: ${measuredProcessCount}/${measuredProcessCount} identical (${uniqueHashes[0]})`,
- );
- console.log(`Results: ${resultsPath}`);
- if (evidencePath) {
- console.log(`Evidence: ${evidencePath}`);
- }
-} finally {
- await safelyRemoveWorkspace(workspace, "pliego-gate-b-");
-}
-} finally {
- await releaseRunLock(runLock);
-}
+// Compatibility entry point. Gate B's handwritten Tailwind 4.3.2 baseline was superseded by the
+// three-lane, paired Benchmark Authority v2 contract. Arguments are forwarded unchanged.
+await import("./measure-benchmark-authority-v2.mjs");
diff --git a/scripts/measure-reachability-pruning.mjs b/scripts/measure-reachability-pruning.mjs
index 36a929f..890cda8 100644
--- a/scripts/measure-reachability-pruning.mjs
+++ b/scripts/measure-reachability-pruning.mjs
@@ -15,10 +15,10 @@ const expected = {
sourceSha256: "159233dbb9258f93bbc0561e795780aa3b803a2cab8bfa3b19d37284d40359c0",
reachabilitySha256: "d457dd60004d9d989d42928c5665a5ed84561bf4955f61af3a23ecf21b876a76",
baseline: {
- cssSha256: "9bf886ca054ada11b15b3bbd1e705954ae6aa1d4cb06ec65c168bd46b1f510a7",
- rawBytes: 7_286,
- gzipBytes: 1_215,
- manifestSha256: "7fd4798a648df834353862cef0054cdbe1cc1471429db7261c682342f1e641f7",
+ cssSha256: "a0d4a94b0be27ecbd113b764eca1d20ccbf5c328c896b3f2ca1a0f86516859ee",
+ rawBytes: 7_282,
+ gzipBytes: 1_213,
+ manifestSha256: "a9e293c0f64cfff3658f0ca8d041c9ce659427915bb0064401999bf7ff210b5a",
styles: 14,
declarations: 147,
tokens: 30,
@@ -26,10 +26,10 @@ const expected = {
physicalDeclarations: 207,
},
pruned: {
- cssSha256: "ce92fd334f9defec213c7aad7e8b855b573fc883724b5c57a4df8cb96f559317",
+ cssSha256: "b1e26159f1078fd29403b5f70fb85fed7d1e290941dc07c91a476d96b317222a",
rawBytes: 366,
- gzipBytes: 225,
- manifestSha256: "ef8622609fa44d9b6b55640ca2774bd1cb9125132463024d17d0564286aa08c3",
+ gzipBytes: 224,
+ manifestSha256: "8a991deb50fbb5299e3de3008297570bd14c62af2b47f47fe9449e344b7d241b",
styles: 2,
declarations: 13,
tokens: 9,
@@ -37,10 +37,10 @@ const expected = {
physicalDeclarations: 15,
},
themedBaseline: {
- cssSha256: "e1f3d60c48f444e0a78e985f7632a8d2c9cb47c5500833c4df25a9cafc05162e",
- rawBytes: 7_657,
+ cssSha256: "cd2a276e221155f29cfc12ff3dbd8394cd144e314ccc1a753ad097b1178cb70f",
+ rawBytes: 7_653,
gzipBytes: 1_369,
- manifestSha256: "58cb0152d35ee0569b6d0a8936ec3721018089ce436e1004c6b756fe288ca40d",
+ manifestSha256: "db2c50a01903add21dec541155781f7b64ba04509103f23bc73236aca7c5d7e4",
styles: 14,
declarations: 147,
tokens: 30,
@@ -60,10 +60,10 @@ const expected = {
],
},
themedPruned: {
- cssSha256: "ec4f3c40228ae18690b43bb7e1d6e4fcde7241a4ecc9842987659d594f320ee5",
+ cssSha256: "a3b4b91dad2f3e627f56d219cf476bd1dfb22b12c5669c8f26af469d380a7799",
rawBytes: 457,
- gzipBytes: 266,
- manifestSha256: "b6f838baf3e444cddb8bc2daaf439f34cb3cdb5b626e672fdee966102b72966b",
+ gzipBytes: 265,
+ manifestSha256: "615fb6fe955d7950bcb58da700ad8b8fe676baeb93de409fa1e1991e10106eee",
styles: 2,
declarations: 13,
tokens: 9,
@@ -75,16 +75,16 @@ const expected = {
const expectedGzipSha256ByZlib = {
"1.3.0.1-motley-82a5fec": {
- baseline: "67e3b128ee810beb6ce5c2cca5bbed3ab9e0544b373388db41bb56fe354a3707",
- pruned: "c5923c28ff5234957276529060728513e31c1fb19ef3c61164a1c96e105df9d1",
- themedBaseline: "64800d9e18bcf91aeebb029ccc32ca454c9a2d57c99241261270777956b8b87b",
- themedPruned: "9f83be529c8822e54ac82a1794d7d5183a929407b6b3f1048ad6b7735c0ab60d",
+ baseline: "941b57383fd67450ae34234876b2b09d64dd6fb68dd138503858b9c4dfa5b4b1",
+ pruned: "4b62e9053298bcfb12e07330bac4c47c68a7c45815832d4d087f89e754a4501c",
+ themedBaseline: "a13dee3f2d2f5093c7b3564d17b6aad9869a05618fb887bd2c0b359dc4a57814",
+ themedPruned: "8670a47312abec7e2fe58770c2a9c136aa7529eac104923acc9c6efea4f0312c",
},
"1.3.1-e00f703": {
- baseline: "67e3b128ee810beb6ce5c2cca5bbed3ab9e0544b373388db41bb56fe354a3707",
- pruned: "c5923c28ff5234957276529060728513e31c1fb19ef3c61164a1c96e105df9d1",
- themedBaseline: "64800d9e18bcf91aeebb029ccc32ca454c9a2d57c99241261270777956b8b87b",
- themedPruned: "9f83be529c8822e54ac82a1794d7d5183a929407b6b3f1048ad6b7735c0ab60d",
+ baseline: "941b57383fd67450ae34234876b2b09d64dd6fb68dd138503858b9c4dfa5b4b1",
+ pruned: "4b62e9053298bcfb12e07330bac4c47c68a7c45815832d4d087f89e754a4501c",
+ themedBaseline: "a13dee3f2d2f5093c7b3564d17b6aad9869a05618fb887bd2c0b359dc4a57814",
+ themedPruned: "8670a47312abec7e2fe58770c2a9c136aa7529eac104923acc9c6efea4f0312c",
},
};
diff --git a/scripts/measure-wasm-overhead.mjs b/scripts/measure-wasm-overhead.mjs
index 02bcb2e..abcb281 100644
--- a/scripts/measure-wasm-overhead.mjs
+++ b/scripts/measure-wasm-overhead.mjs
@@ -81,7 +81,7 @@ if (
manifest.schemaVersion !== 3 ||
manifest.styleIdFormatVersion !== 2 ||
manifest.classNameFormatVersion !== 1 ||
- manifest.themeIdFormatVersion !== 2
+ manifest.themeIdFormatVersion !== 3
) {
throw new Error(
`expected manifest schema/identity formats 3/2/1/2, received ${manifest.schemaVersion}/${manifest.styleIdFormatVersion}/${manifest.classNameFormatVersion}/${manifest.themeIdFormatVersion}`,
diff --git a/scripts/release-authority.mjs b/scripts/release-authority.mjs
new file mode 100644
index 0000000..9ea4a54
--- /dev/null
+++ b/scripts/release-authority.mjs
@@ -0,0 +1,420 @@
+import { createHash } from "node:crypto";
+import { spawnSync } from "node:child_process";
+import { existsSync, readFileSync } from "node:fs";
+import { isAbsolute, relative, resolve } from "node:path";
+
+const COMMIT = /^[0-9a-f]{40}$/u;
+const DIGEST = /^sha256:[0-9a-f]{64}$/u;
+const INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/u;
+
+export class AuthorityError extends Error {
+ constructor(message) {
+ super(message);
+ this.name = "AuthorityError";
+ }
+}
+
+function fail(message) {
+ throw new AuthorityError(message);
+}
+
+function object(value, role) {
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
+ fail(`${role} must be an object`);
+ }
+ return value;
+}
+
+function exactKeys(value, keys, role) {
+ object(value, role);
+ const actual = Object.keys(value).sort();
+ const expected = [...keys].sort();
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
+ fail(`${role} fields drifted: ${actual.join(", ")}`);
+ }
+}
+
+function strings(value, role) {
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || !entry)) {
+ fail(`${role} must be an array of non-empty strings`);
+ }
+ if (new Set(value).size !== value.length) fail(`${role} contains duplicates`);
+ return value;
+}
+
+function instant(value, role) {
+ if (typeof value !== "string" || !INSTANT.test(value) || Number.isNaN(Date.parse(value))) {
+ fail(`${role} must be a canonical UTC second instant`);
+ }
+ return Date.parse(value);
+}
+
+function source(value, role, { tag = false } = {}) {
+ const keys = tag ? ["tag", "commit", "gitTree"] : ["commit", "gitTree"];
+ exactKeys(value, keys, role);
+ if (!COMMIT.test(value.commit) || !COMMIT.test(value.gitTree)) {
+ fail(`${role} commit and Git tree must be full lowercase object IDs`);
+ }
+ if (tag && !/^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(value.tag)) {
+ fail(`${role}.tag is not a release tag`);
+ }
+ return value;
+}
+
+function sourceEquals(left, right) {
+ return left.commit === right.commit && left.gitTree === right.gitTree;
+}
+
+function localPath(root, value, role) {
+ if (
+ typeof value !== "string" ||
+ !value ||
+ isAbsolute(value) ||
+ value.includes("\\") ||
+ value.split("/").includes("..")
+ ) {
+ fail(`${role} must be a portable repository-relative path`);
+ }
+ const absolute = resolve(root, value);
+ const child = relative(root, absolute);
+ if (!child || child.startsWith("..") || isAbsolute(child)) {
+ fail(`${role} escapes the repository`);
+ }
+ if (!existsSync(absolute)) fail(`${role} does not exist: ${value}`);
+ return absolute;
+}
+
+function artifact(root, value, role) {
+ exactKeys(value, ["path", "url", "sha256"], role);
+ if (typeof value.url !== "string" || !value.url.startsWith("https://")) {
+ fail(`${role}.url must use HTTPS`);
+ }
+ if (!DIGEST.test(value.sha256)) fail(`${role}.sha256 is not canonical`);
+ const path = localPath(root, value.path, `${role}.path`);
+ const actual = `sha256:${createHash("sha256").update(readFileSync(path)).digest("hex")}`;
+ if (actual !== value.sha256) fail(`${role} hash drifted for ${value.path}`);
+}
+
+function reference(value, role) {
+ exactKeys(value, ["kind", "url"], role);
+ if (typeof value.kind !== "string" || !value.kind) fail(`${role}.kind is missing`);
+ if (typeof value.url !== "string" || !value.url.startsWith("https://")) {
+ fail(`${role}.url must use HTTPS`);
+ }
+}
+
+function environment(value, role) {
+ exactKeys(value, ["os", "arch", "rust", "node", "browser"], role);
+ for (const key of Object.keys(value)) {
+ if (value[key] !== null && (typeof value[key] !== "string" || !value[key])) {
+ fail(`${role}.${key} must be a non-empty string or null`);
+ }
+ }
+}
+
+function waiver(root, value, role) {
+ if (value === null) return;
+ const path = localPath(root, value, role);
+ const child = relative(resolve(root, "docs", "adr"), path);
+ if (!child || child.startsWith("..") || isAbsolute(child)) {
+ fail(`${role} must point inside docs/adr`);
+ }
+}
+
+function verifyTag(root, value) {
+ const commands = [
+ [["rev-parse", `${value.tag}^{commit}`], value.commit, "tag commit"],
+ [["show", "-s", "--format=%T", value.commit], value.gitTree, "Git tree"],
+ ];
+ for (const [args, expected, role] of commands) {
+ const result = spawnSync("git", args, { cwd: root, encoding: "utf8", windowsHide: true });
+ if (result.error || result.status !== 0) {
+ fail(`cannot verify ${role}: ${result.error?.message ?? result.stderr.trim()}`);
+ }
+ if (result.stdout.trim() !== expected) fail(`${role} does not match the repository`);
+ }
+}
+
+function validWindow(observedAt, expiresAt, role, now) {
+ const observed = instant(observedAt, `${role}.observedAt`);
+ const expires = instant(expiresAt, `${role}.expiresAt`);
+ if (expires <= observed) fail(`${role} expires before it is observed`);
+ return now <= expires;
+}
+
+function completeCoverage(required, covered) {
+ const actual = new Set(covered);
+ return required.every((item) => actual.has(item));
+}
+
+export function validateReleaseReadiness(value, { root, now = Date.now(), verifyGit = true }) {
+ exactKeys(
+ value,
+ [
+ "schemaVersion",
+ "kind",
+ "state",
+ "targetVersion",
+ "candidateVersion",
+ "repository",
+ "productStage",
+ "source",
+ "observedAt",
+ "expiresAt",
+ "result",
+ "dimensions",
+ "checks",
+ "promotionRule",
+ ],
+ "readiness",
+ );
+ if (
+ value.schemaVersion !== 3 ||
+ value.kind !== "pliegocss-release-readiness" ||
+ value.state !== "current" ||
+ value.targetVersion !== "0.1.0" ||
+ value.candidateVersion !== "0.1.0-rc.2" ||
+ value.repository !== "https://github.com/celiumsai/pliegocss" ||
+ value.productStage !== "public-preview"
+ ) {
+ fail("readiness header drifted");
+ }
+ const rootSource = source(value.source, "readiness.source", { tag: true });
+ if (verifyGit) verifyTag(root, rootSource);
+ validWindow(value.observedAt, value.expiresAt, "readiness", now);
+ exactKeys(
+ value.dimensions,
+ ["technicalReady", "operationallyReady", "adoptionReady", "authorized", "promotionReady"],
+ "readiness.dimensions",
+ );
+ if (!Array.isArray(value.checks)) fail("readiness.checks must be an array");
+ const requiredIds = new Set([
+ "exact-candidate-ci",
+ "exact-candidate-codeql",
+ "g0-corrections-on-candidate",
+ "browser-release-matrix",
+ "registry-replay-refresh",
+ "production-deployment-refresh",
+ "signed-binary-distribution",
+ "external-adoption-evidence",
+ "final-promotion-authorization",
+ ]);
+ const ids = value.checks.map((check) => check.id);
+ if (new Set(ids).size !== ids.length) fail("readiness check IDs are not unique");
+ if (ids.length !== requiredIds.size || ids.some((id) => !requiredIds.has(id))) {
+ fail("readiness check inventory drifted");
+ }
+ const allowedDimensions = new Set(["technical", "operational", "adoption", "authorization"]);
+ const allowedStatuses = new Set(["passed", "pending", "blocked", "not-authorized", "not-applicable"]);
+ const allowedEvidence = new Set(["measured", "inherited", "pending", "uncertain"]);
+ for (const check of value.checks) {
+ const role = `readiness.checks.${check.id}`;
+ exactKeys(
+ check,
+ [
+ "id",
+ "dimension",
+ "status",
+ "evidenceClass",
+ "requiredForPromotion",
+ "summary",
+ "source",
+ "observedAt",
+ "expiresAt",
+ "environment",
+ "requiredCoverage",
+ "coveredCoverage",
+ "waiverAdr",
+ "artifacts",
+ "references",
+ ],
+ role,
+ );
+ if (!allowedDimensions.has(check.dimension)) fail(`${role}.dimension is invalid`);
+ if (!allowedStatuses.has(check.status)) fail(`${role}.status is invalid`);
+ if (!allowedEvidence.has(check.evidenceClass)) fail(`${role}.evidenceClass is invalid`);
+ if (typeof check.requiredForPromotion !== "boolean") fail(`${role}.requiredForPromotion must be boolean`);
+ if (typeof check.summary !== "string" || !check.summary) fail(`${role}.summary is missing`);
+ const checkSource = source(check.source, `${role}.source`);
+ if (!sourceEquals(checkSource, rootSource)) fail(`${role} is not bound to the candidate source`);
+ const unexpired = validWindow(check.observedAt, check.expiresAt, role, now);
+ environment(check.environment, `${role}.environment`);
+ const required = strings(check.requiredCoverage, `${role}.requiredCoverage`);
+ const covered = strings(check.coveredCoverage, `${role}.coveredCoverage`);
+ waiver(root, check.waiverAdr, `${role}.waiverAdr`);
+ if (!Array.isArray(check.artifacts)) fail(`${role}.artifacts must be an array`);
+ check.artifacts.forEach((entry, index) => artifact(root, entry, `${role}.artifacts[${index}]`));
+ if (!Array.isArray(check.references)) fail(`${role}.references must be an array`);
+ check.references.forEach((entry, index) => reference(entry, `${role}.references[${index}]`));
+ if (check.status === "passed") {
+ if (check.evidenceClass !== "measured") fail(`${role} passed without measured evidence`);
+ if (!unexpired) fail(`${role} passed on expired evidence`);
+ if (!completeCoverage(required, covered)) fail(`${role} passed with incomplete coverage`);
+ if (check.waiverAdr !== null) fail(`${role} passed through a waiver`);
+ if (check.artifacts.length === 0) fail(`${role} passed without a hashed artifact`);
+ if (check.references.length === 0) fail(`${role} passed without a source URL`);
+ }
+ }
+ const dimension = (name) =>
+ value.checks
+ .filter((check) => check.requiredForPromotion && check.dimension === name)
+ .every((check) => check.status === "passed");
+ const derived = {
+ technicalReady: dimension("technical"),
+ operationallyReady: dimension("operational"),
+ adoptionReady: dimension("adoption"),
+ authorized: dimension("authorization"),
+ };
+ derived.promotionReady = Object.values(derived).every(Boolean);
+ if (JSON.stringify(value.dimensions) !== JSON.stringify(derived)) {
+ fail("readiness dimensions do not match the required checks");
+ }
+ const result = derived.promotionReady ? "ready" : "blocked";
+ if (value.result !== result) fail("readiness result does not match its dimensions");
+ if (typeof value.promotionRule !== "string" || !value.promotionRule) {
+ fail("readiness promotion rule is missing");
+ }
+ return {
+ schemaVersion: value.schemaVersion,
+ candidate: value.candidateVersion,
+ sourceCommit: rootSource.commit,
+ gitTree: rootSource.gitTree,
+ result,
+ dimensions: derived,
+ blockers: value.checks
+ .filter((check) => check.requiredForPromotion && check.status !== "passed")
+ .map((check) => check.id),
+ };
+}
+
+export function validateBrowserMatrix(value, { root, now = Date.now(), verifyGit = true }) {
+ exactKeys(
+ value,
+ [
+ "schemaVersion",
+ "kind",
+ "state",
+ "source",
+ "observedAt",
+ "expiresAt",
+ "profiles",
+ "hosts",
+ "result",
+ "missingRequiredHosts",
+ "releaseBoundary",
+ ],
+ "browserMatrix",
+ );
+ if (
+ value.schemaVersion !== 2 ||
+ value.kind !== "pliegocss-hosted-browser-matrix" ||
+ value.state !== "current"
+ ) {
+ fail("browser matrix header drifted");
+ }
+ const rootSource = source(value.source, "browserMatrix.source", { tag: true });
+ if (verifyGit) verifyTag(root, rootSource);
+ validWindow(value.observedAt, value.expiresAt, "browserMatrix", now);
+ exactKeys(value.profiles, ["release"], "browserMatrix.profiles");
+ exactKeys(
+ value.profiles.release,
+ ["requiredHosts", "allowNotConfigured", "allowStale", "allowWaivers"],
+ "browserMatrix.profiles.release",
+ );
+ const requiredHosts = strings(
+ value.profiles.release.requiredHosts,
+ "browserMatrix.profiles.release.requiredHosts",
+ );
+ if (
+ value.profiles.release.allowNotConfigured !== false ||
+ value.profiles.release.allowStale !== false ||
+ value.profiles.release.allowWaivers !== false
+ ) {
+ fail("release browser profile must fail closed");
+ }
+ if (!Array.isArray(value.hosts)) fail("browserMatrix.hosts must be an array");
+ const ids = value.hosts.map((host) => host.id);
+ if (new Set(ids).size !== ids.length) fail("browser host IDs are not unique");
+ if (ids.length !== requiredHosts.length || requiredHosts.some((id) => !ids.includes(id))) {
+ fail("browser host inventory does not match release coverage");
+ }
+ const allowedStatuses = new Set(["passed", "failed", "not-configured", "stale"]);
+ for (const host of value.hosts) {
+ const role = `browserMatrix.hosts.${host.id}`;
+ exactKeys(
+ host,
+ [
+ "id",
+ "os",
+ "arch",
+ "browser",
+ "browserVersion",
+ "status",
+ "sourceCommit",
+ "gitTree",
+ "observedAt",
+ "expiresAt",
+ "automation",
+ "requiredCoverage",
+ "coveredCoverage",
+ "waiverAdr",
+ "artifacts",
+ ],
+ role,
+ );
+ for (const key of ["id", "os", "arch", "browser", "automation"]) {
+ if (typeof host[key] !== "string" || !host[key]) fail(`${role}.${key} is missing`);
+ }
+ if (!allowedStatuses.has(host.status)) fail(`${role}.status is invalid`);
+ if (host.sourceCommit !== rootSource.commit || host.gitTree !== rootSource.gitTree) {
+ fail(`${role} is not bound to the candidate source`);
+ }
+ const required = strings(host.requiredCoverage, `${role}.requiredCoverage`);
+ const covered = strings(host.coveredCoverage, `${role}.coveredCoverage`);
+ waiver(root, host.waiverAdr, `${role}.waiverAdr`);
+ if (!Array.isArray(host.artifacts)) fail(`${role}.artifacts must be an array`);
+ host.artifacts.forEach((entry, index) => artifact(root, entry, `${role}.artifacts[${index}]`));
+ if (host.status === "passed") {
+ if (host.automation !== "passed") fail(`${role} passed without automation`);
+ if (typeof host.browserVersion !== "string" || !host.browserVersion) {
+ fail(`${role} passed without a browser version`);
+ }
+ if (host.observedAt === null || host.expiresAt === null) fail(`${role} passed without a validity window`);
+ if (!validWindow(host.observedAt, host.expiresAt, role, now)) fail(`${role} passed on expired evidence`);
+ if (!completeCoverage(required, covered)) fail(`${role} passed with incomplete coverage`);
+ if (host.waiverAdr !== null) fail(`${role} passed through a waiver`);
+ if (host.artifacts.length === 0) fail(`${role} passed without hashed artifacts`);
+ } else if (host.status === "not-configured") {
+ if (
+ host.automation !== "not-configured" ||
+ host.browserVersion !== null ||
+ host.observedAt !== null ||
+ host.expiresAt !== null ||
+ covered.length !== 0 ||
+ host.artifacts.length !== 0 ||
+ host.waiverAdr !== null
+ ) {
+ fail(`${role} claims evidence while not configured`);
+ }
+ }
+ }
+ const missing = requiredHosts.filter(
+ (id) => value.hosts.find((host) => host.id === id)?.status !== "passed",
+ );
+ if (JSON.stringify(value.missingRequiredHosts) !== JSON.stringify(missing)) {
+ fail("browser missing-host projection drifted");
+ }
+ const result = missing.length === 0 ? "ready" : "blocked";
+ if (value.result !== result) fail("browser result does not match required hosts");
+ if (typeof value.releaseBoundary !== "string" || !value.releaseBoundary) {
+ fail("browser release boundary is missing");
+ }
+ return {
+ schemaVersion: value.schemaVersion,
+ sourceCommit: rootSource.commit,
+ gitTree: rootSource.gitTree,
+ result,
+ passed: value.hosts.filter((host) => host.status === "passed").map((host) => host.id),
+ missingRequiredHosts: missing,
+ };
+}
diff --git a/scripts/release-authority.test.mjs b/scripts/release-authority.test.mjs
new file mode 100644
index 0000000..1a6c685
--- /dev/null
+++ b/scripts/release-authority.test.mjs
@@ -0,0 +1,73 @@
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import {
+ AuthorityError,
+ validateBrowserMatrix,
+ validateReleaseReadiness,
+} from "./release-authority.mjs";
+
+const root = resolve(import.meta.dirname, "..");
+const fixedNow = Date.parse("2026-07-22T15:00:00Z");
+const readiness = JSON.parse(
+ readFileSync(resolve(root, "docs/product/release-readiness-0.1.0.json"), "utf8"),
+);
+const browser = JSON.parse(
+ readFileSync(resolve(root, "docs/benchmarks/hosted-browser-matrix.json"), "utf8"),
+);
+assert.equal(
+ validateReleaseReadiness(readiness, { root, now: fixedNow, verifyGit: false }).result,
+ "blocked",
+);
+assert.equal(
+ validateBrowserMatrix(browser, { root, now: fixedNow, verifyGit: false }).result,
+ "blocked",
+);
+
+const unbound = structuredClone(readiness);
+unbound.checks[0].source.commit = "0".repeat(40);
+assert.throws(
+ () => validateReleaseReadiness(unbound, { root, now: fixedNow, verifyGit: false }),
+ (error) => error instanceof AuthorityError && /not bound/u.test(error.message),
+);
+
+const artifactless = structuredClone(readiness);
+artifactless.checks[0].artifacts = [];
+assert.throws(
+ () => validateReleaseReadiness(artifactless, { root, now: fixedNow, verifyGit: false }),
+ (error) => error instanceof AuthorityError && /hashed artifact/u.test(error.message),
+);
+
+const externalWaiver = structuredClone(readiness);
+externalWaiver.checks[2].waiverAdr = "docs/index.md";
+assert.throws(
+ () => validateReleaseReadiness(externalWaiver, { root, now: fixedNow, verifyGit: false }),
+ (error) => error instanceof AuthorityError && /inside docs\/adr/u.test(error.message),
+);
+
+const optimisticBrowser = structuredClone(browser);
+Object.assign(optimisticBrowser.hosts[0], {
+ status: "passed",
+ automation: "passed",
+ browserVersion: "150.0.0",
+ observedAt: "2026-07-22T14:10:00Z",
+ expiresAt: "2026-07-29T14:10:00Z",
+ coveredCoverage: optimisticBrowser.hosts[0].requiredCoverage,
+});
+assert.throws(
+ () => validateBrowserMatrix(optimisticBrowser, { root, now: fixedNow, verifyGit: false }),
+ (error) => error instanceof AuthorityError && /hashed artifacts/u.test(error.message),
+);
+
+for (const script of ["check-release-readiness.mjs", "check-hosted-browser-matrix.mjs"]) {
+ const result = spawnSync(process.execPath, [resolve(root, "scripts", script), "--profile=release"], {
+ cwd: root,
+ encoding: "utf8",
+ windowsHide: true,
+ });
+ assert.equal(result.status, 1, `${script} must block the release profile`);
+ assert.match(result.stderr, /blocked/u);
+}
+
+process.stdout.write("release authority fail-closed contracts: pass\n");
diff --git a/scripts/render-benchmark-authority-v2.mjs b/scripts/render-benchmark-authority-v2.mjs
new file mode 100644
index 0000000..a7a8e1b
--- /dev/null
+++ b/scripts/render-benchmark-authority-v2.mjs
@@ -0,0 +1,114 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { resolve } from "node:path";
+import {
+ corpusMetrics,
+ loadAuthority,
+ materializeCorpus,
+ repositoryRoot,
+} from "./benchmark-authority-v2.mjs";
+
+const arguments_ = process.argv.slice(2);
+if (arguments_.length !== 1 || !new Set(["--check", "--write"]).has(arguments_[0])) {
+ throw new Error("usage: node scripts/render-benchmark-authority-v2.mjs --check|--write");
+}
+
+const authority = loadAuthority();
+const destination = resolve(repositoryRoot, authority.artifacts.generatedDocumentation);
+const tag = (lane) => lane.distTag ?? "pinned version";
+const laneRows = authority.lanes
+ .map(
+ (lane) =>
+ `| \`${lane.id}\` | ${lane.role} | \`${tag(lane)}\` | \`${lane.version}\` | \`${lane.cliVersion}\` |`,
+ )
+ .join("\n");
+const corpusRows = authority.corpora
+ .map((corpus) => {
+ const metrics = corpusMetrics(materializeCorpus(corpus));
+ return `| \`${corpus.id}\` | ${corpus.scope} | ${metrics.htmlBytes.toLocaleString("en-US")} | ${metrics.classAttributeCount.toLocaleString("en-US")} | ${metrics.utilityOccurrences.toLocaleString("en-US")} | ${metrics.uniqueUtilityTokens.toLocaleString("en-US")} |`;
+ })
+ .join("\n");
+
+const markdown = `# Tailwind Benchmark Authority v2
+
+Status: **active machine authority; generated from [\`oracle.json\`](../../benchmarks/benchmark-authority-v2/oracle.json)**
+
+This document is generated. Edit the schema-2 JSON authority and run
+\`node scripts/render-benchmark-authority-v2.mjs --write\`; do not maintain the tables by hand.
+
+The primary competitor is \`${authority.primaryLane}\`. The oracle was observed at
+\`${authority.observedAtUtc}\`, expires at \`${authority.expiresAtUtc}\`, and has a maximum age of
+${authority.maximumAgeHours} hours. An expired or live-registry-mismatched oracle blocks a new
+competitive result. The reset contract is explicitly **${authority.resetContract}** because
+PliegoCSS does not emit Preflight.
+
+## Competitor lanes
+
+| Lane | Role | Registry selector | Tailwind | CLI |
+|---|---|---|---:|---:|
+${laneRows}
+
+\`tailwind-frozen-release\` is a regression control. It is not allowed to stand in for current
+Tailwind. The v3 lane follows the upstream npm \`v3-lts\` dist-tag; “LTS” here names that registry
+lane and does not expand Tailwind's support promises.
+
+## Corpora
+
+| Corpus | Scope | HTML bytes | Class attributes | Utility occurrences | Unique utilities |
+|---|---|---:|---:|---:|---:|
+${corpusRows}
+
+The large corpus repeats the medium body ${authority.corpora.find((corpus) => corpus.id === "large").repeat}
+times. It measures source-volume scaling while deliberately keeping the same utility vocabulary;
+it is not presented as broader catalog coverage.
+
+## Measurement contract
+
+- ${authority.methodology.warmupPairs} warmup pairs and ${authority.methodology.measuredPairs} measured pairs per lane/corpus.
+- Pair order alternates: ${authority.methodology.pairOrder}.
+- Every observation is a fresh process. Complete per-pair latency and peak-working-set samples are retained.
+- A separate fresh invocation must reproduce each measured output hash.
+- CSS and rewritten/source HTML report raw, gzip level ${authority.methodology.compression.gzipLevel}, and Brotli quality ${authority.methodology.compression.brotliQuality} bytes.
+- Transfer compares separate HTML and CSS streams under both compression formats.
+- Candidate coverage must be 100% for the shared corpus before a pair is comparable.
+- PliegoCSS and Tailwind use the same no-Preflight utility intent; browser computed-style equivalence belongs to G4.
+
+Peak working set is measured for the direct child process by
+[\`benchmark-process-metrics.rs\`](../../scripts/benchmark-process-metrics.rs), not inferred from
+host free-memory deltas. Process startup, security tooling and normal OS caches remain part of the
+fresh-process observation; the release build happens outside the timed region.
+
+## Commands
+
+\`\`\`console
+pnpm check:benchmark-authority
+pnpm check:benchmark-oracle
+node scripts/measure-benchmark-authority-v2.mjs --smoke
+node scripts/measure-benchmark-authority-v2.mjs
+\`\`\`
+
+Immutable evidence additionally uses \`--evidence=benchmarks/evidence/v2/.json\`, the
+canonical 5/30 sample counts, a clean source tree, a new destination, and an explicit security-tooling
+declaration. Machine-local results remain ignored at
+\`${authority.artifacts.localResult}\`.
+
+## Claim boundary
+
+${authority.methodology.scorePolicy}
+
+Legacy Gate A/B snapshots remain historical integrity evidence. They cannot supply the current
+competitor lane, memory, Brotli, or micro/medium/large claims and therefore cannot produce the v2
+competitive score.
+`;
+
+if (arguments_[0] === "--write") {
+ writeFileSync(destination, markdown);
+ process.stdout.write(`${destination}\n`);
+} else {
+ const current = readFileSync(destination, "utf8");
+ if (current !== markdown) {
+ throw new Error(`${authority.artifacts.generatedDocumentation} is stale; run the renderer with --write`);
+ }
+ process.stdout.write(
+ `${JSON.stringify({ schemaVersion: 2, status: "passed", document: authority.artifacts.generatedDocumentation, lanes: authority.lanes.length, corpora: authority.corpora.length }, null, 2)}\n`,
+ );
+}
diff --git a/scripts/render-browser-output-certification.mjs b/scripts/render-browser-output-certification.mjs
new file mode 100644
index 0000000..4b3e010
--- /dev/null
+++ b/scripts/render-browser-output-certification.mjs
@@ -0,0 +1,113 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { repositoryRoot } from "./benchmark-authority-v2.mjs";
+
+const authority = JSON.parse(
+ readFileSync(
+ resolve(repositoryRoot, "benchmarks", "browser-output-certification-v1", "authority.json"),
+ "utf8",
+ ),
+);
+const outputPath = resolve(
+ repositoryRoot,
+ "docs",
+ "benchmarks",
+ "browser-output-certification-v1.md",
+);
+
+function tableRows() {
+ return authority.hosts
+ .map(
+ (host) =>
+ `| \`${host.id}\` | \`${host.runner}\` | ${host.os} | ${host.arch} | ${host.browser} |`,
+ )
+ .join("\n");
+}
+
+const document = `# Browser/output certification v1
+
+Status: **implemented hosted 3×3 workflow; release evidence remains source-bound**
+
+This page is generated from
+[\`authority.json\`](../../benchmarks/browser-output-certification-v1/authority.json). Edit the
+JSON contract and run \`node scripts/render-browser-output-certification.mjs --write\`; do not
+maintain the matrix by hand.
+
+## Scope
+
+The certification compiles the frozen medium fixture once with PliegoCSS and once with the fresh
+\`${authority.competitorLane}\` lane from Benchmark Authority v2. Within each host it compares
+${authority.computedProperties.length} computed properties on every styled node and captures both
+original screenshots plus a pixel-diff image for every scenario.
+
+Passing proves only the frozen shared utility intent. It is not general Tailwind compatibility,
+does not compare pixels across different browser engines, and does not broaden PliegoCSS's utility
+catalog.
+
+## Reset contract
+
+| Mode | Contract |
+|---|---|
+${authority.resetModes.map((mode) => `| \`${mode.id}\` | ${mode.contract} |`).join("\n")}
+
+The shared reset is isolated in \`@layer reset\`. Tailwind Preflight is not silently enabled because
+PliegoCSS does not own a Preflight implementation. This keeps the reset/no-reset comparison
+explicit and prevents an unlayered reset from outranking Tailwind's layered utilities.
+
+## Hosted matrix
+
+| Host | GitHub runner | OS | Architecture | Browser engine |
+|---|---|---|---|---|
+${tableRows()}
+
+The matrix is the full browser/OS cross-product for Chromium, Firefox, and WebKit across Windows,
+Linux, and macOS. Windows and Linux provide x64 evidence; the standard \`macos-15\` runner provides
+ARM64 evidence.
+
+## Scenarios
+
+| Scenario | Viewport | State |
+|---|---:|---|
+${authority.scenarios.map((scenario) => `| \`${scenario.id}\` | ${scenario.width}×${scenario.height} | ${scenario.action} |`).join("\n")}
+
+Every scenario runs under both reset modes. Computed colors normalize to 8-bit sRGB, equivalent
+flex-end keywords normalize together, and layout lengths quantize to
+${authority.comparison.computedLengthQuantumCssPx} CSS px to account for browser-internal
+subpixel serialization. The same host must also match every styled border box, client/scroll extent,
+and direct text-node rectangle captured at ${authority.comparison.layoutGeometryResolutionCssPx} CSS
+px resolution with at most ${authority.comparison.maximumLayoutGeometryDeltaCssPx} CSS px delta.
+Each scenario and host summary retain the maximum observed geometry delta; aggregation recomputes
+that maximum and rejects missing, non-finite, negative, over-budget, or summary-drifted values.
+Screenshots use pixelmatch threshold
+${authority.comparison.screenshotPixelThreshold} and may differ on at most
+${authority.comparison.maximumScreenshotMismatchRatio * 100}% of CSS pixels; original PNG hashes
+remain retained so the tolerance cannot hide or rewrite evidence.
+
+## Commands
+
+\`\`\`console
+pnpm check:browser-output-authority
+pnpm exec playwright install chromium firefox webkit
+pnpm check:browser-output -- --browser=chromium
+pnpm check:browser-output -- --browser=firefox
+pnpm check:browser-output -- --browser=webkit
+\`\`\`
+
+CI executes each of the nine hosts with \`--require-clean\`, uploads the JSON and PNG artifacts,
+then aggregates them only when every host is unexpired, clean-tree, bound to one commit/tree, and
+complete. That source identity must equal the aggregator checkout, so nine mutually consistent but
+stale documents cannot pass. Local dirty-tree runs are diagnostic and cannot satisfy the hosted matrix.
+`;
+
+const mode = process.argv[2];
+if (mode === "--write") {
+ writeFileSync(outputPath, document);
+ process.stdout.write(`wrote ${outputPath}\n`);
+} else if (mode === "--check") {
+ if (readFileSync(outputPath, "utf8") !== document) {
+ throw new Error("browser/output certification documentation is stale; run --write");
+ }
+ process.stdout.write("browser/output certification documentation: current\n");
+} else {
+ throw new Error("usage: node scripts/render-browser-output-certification.mjs --write|--check");
+}
diff --git a/scripts/render-release-readiness.mjs b/scripts/render-release-readiness.mjs
new file mode 100644
index 0000000..2cc3f38
--- /dev/null
+++ b/scripts/render-release-readiness.mjs
@@ -0,0 +1,167 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { validateReleaseReadiness } from "./release-authority.mjs";
+
+const root = resolve(import.meta.dirname, "..");
+const sourcePath = resolve(root, "docs/product/release-readiness-0.1.0.json");
+const outputPath = resolve(root, "docs/product/release-readiness-0.1.0.md");
+const siteOutputPath = resolve(root, "docs/site/release-readiness.md");
+
+function cell(value) {
+ return String(value).replaceAll("|", "\\|").replaceAll("\n", " ");
+}
+
+function linkArtifact(artifact) {
+ return `[${artifact.path}](${artifact.url}) — \`${artifact.sha256}\``;
+}
+
+export function renderReleaseReadiness(value) {
+ const summary = validateReleaseReadiness(value, { root });
+ const lines = [
+ "# PliegoCSS 0.1.0 release readiness",
+ "",
+ "",
+ "",
+ `Status: **${summary.result}**`,
+ "",
+ "This page is a deterministic projection of the schema-3",
+ "[`release-readiness-0.1.0.json`](./release-readiness-0.1.0.json) machine authority.",
+ "Historical reports and release notes do not change this state.",
+ "",
+ "## Candidate identity",
+ "",
+ "| Field | Value |",
+ "| --- | --- |",
+ `| Target | \`${cell(value.targetVersion)}\` |`,
+ `| Candidate | \`${cell(value.candidateVersion)}\` |`,
+ `| Tag | \`${cell(value.source.tag)}\` |`,
+ `| Source commit | \`${cell(value.source.commit)}\` |`,
+ `| Git tree | \`${cell(value.source.gitTree)}\` |`,
+ `| Observed | \`${cell(value.observedAt)}\` |`,
+ `| Expires | \`${cell(value.expiresAt)}\` |`,
+ "",
+ "## Readiness dimensions",
+ "",
+ "| Dimension | Ready |",
+ "| --- | --- |",
+ `| Technical | ${value.dimensions.technicalReady ? "yes" : "no"} |`,
+ `| Operational | ${value.dimensions.operationallyReady ? "yes" : "no"} |`,
+ `| Adoption | ${value.dimensions.adoptionReady ? "yes" : "no"} |`,
+ `| Authorized | ${value.dimensions.authorized ? "yes" : "no"} |`,
+ `| Promotion | ${value.dimensions.promotionReady ? "yes" : "no"} |`,
+ "",
+ "## Required checks",
+ "",
+ "| Check | Dimension | Status | Evidence | Coverage | Waiver |",
+ "| --- | --- | --- | --- | ---: | --- |",
+ ];
+ for (const check of value.checks) {
+ lines.push(
+ `| \`${cell(check.id)}\` | ${cell(check.dimension)} | **${cell(check.status)}** | ${cell(check.evidenceClass)} | ${check.coveredCoverage.length}/${check.requiredCoverage.length} | ${check.waiverAdr ? `\`${cell(check.waiverAdr)}\`` : "none"} |`,
+ );
+ }
+ lines.push("", "## Evidence and blockers", "");
+ for (const check of value.checks) {
+ lines.push(
+ `### ${check.id}`,
+ "",
+ check.summary,
+ "",
+ `- Source: \`${check.source.commit}\` / tree \`${check.source.gitTree}\``,
+ `- Validity: \`${check.observedAt}\` → \`${check.expiresAt}\``,
+ `- Required coverage: ${check.requiredCoverage.map((item) => `\`${item}\``).join(", ")}`,
+ `- Covered: ${check.coveredCoverage.length === 0 ? "none" : check.coveredCoverage.map((item) => `\`${item}\``).join(", ")}`,
+ );
+ if (check.artifacts.length > 0) {
+ lines.push("- Hashed artifacts:");
+ for (const artifact of check.artifacts) lines.push(` - ${linkArtifact(artifact)}`);
+ } else {
+ lines.push("- Hashed artifacts: none");
+ }
+ if (check.references.length > 0) {
+ lines.push("- References:");
+ for (const reference of check.references) {
+ lines.push(` - [${reference.kind}](${reference.url})`);
+ }
+ }
+ lines.push("");
+ }
+ lines.push(
+ "## Promotion rule",
+ "",
+ value.promotionRule,
+ "",
+ "Archived snapshots are indexed under [`../archive/`](../archive/README.md).",
+ "",
+ );
+ return lines.join("\n");
+}
+
+export function renderSiteReleaseReadiness(value) {
+ const summary = validateReleaseReadiness(value, { root });
+ const dimensions = [
+ ["Technical", value.dimensions.technicalReady],
+ ["Operational", value.dimensions.operationallyReady],
+ ["Adoption", value.dimensions.adoptionReady],
+ ["Authorized", value.dimensions.authorized],
+ ["Promotion", value.dimensions.promotionReady],
+ ]
+ .map(([label, ready]) => `${label}: ${ready ? "ready" : "blocked"}`)
+ .join("; ");
+ const blockers = value.checks
+ .filter((check) => check.status !== "passed")
+ .map((check) => `${check.id} (${check.status}): ${check.summary}`)
+ .join(" ");
+ return [
+ "",
+ "",
+ "# Release truth bound to one exact candidate.",
+ "",
+ `The current 1.0 promotion state is ${summary.result}; no historical report can override these source-bound gates.`,
+ "",
+ "## Candidate identity {#candidate}",
+ "",
+ `Target ${value.targetVersion}; candidate ${value.candidateVersion}; tag ${value.source.tag}; commit ${value.source.commit}; Git tree ${value.source.gitTree}; observed ${value.observedAt}; expires ${value.expiresAt}.`,
+ "",
+ "## Readiness dimensions {#dimensions}",
+ "",
+ `${dimensions}.`,
+ "",
+ "## Current blockers {#blockers}",
+ "",
+ blockers || "No blockers remain in the machine authority.",
+ "",
+ ].join("\n");
+}
+
+const args = process.argv.slice(2);
+if (args.length > 1 || (args[0] && !["--check", "--write"].includes(args[0]))) {
+ throw new Error(`usage: node scripts/render-release-readiness.mjs [--check|--write]`);
+}
+const mode = args[0] ?? "--check";
+const document = JSON.parse(readFileSync(sourcePath, "utf8"));
+const rendered = renderReleaseReadiness(document);
+const siteRendered = renderSiteReleaseReadiness(document);
+if (mode === "--write") {
+ writeFileSync(outputPath, rendered, "utf8");
+ writeFileSync(siteOutputPath, siteRendered, "utf8");
+ process.stdout.write(`wrote ${outputPath}\nwrote ${siteOutputPath}\n`);
+} else {
+ const current = readFileSync(outputPath, "utf8").replaceAll("\r\n", "\n");
+ if (current !== rendered) {
+ throw new Error("release readiness Markdown drifted; run with --write");
+ }
+ const siteCurrent = readFileSync(siteOutputPath, "utf8").replaceAll("\r\n", "\n");
+ if (siteCurrent !== siteRendered) {
+ throw new Error("site release readiness Markdown drifted; run with --write");
+ }
+ process.stdout.write("release readiness Markdown projections: current\n");
+}
diff --git a/scripts/render-tailwind-matrix.mjs b/scripts/render-tailwind-matrix.mjs
new file mode 100644
index 0000000..0914f7c
--- /dev/null
+++ b/scripts/render-tailwind-matrix.mjs
@@ -0,0 +1,113 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { loadAuthority, repositoryRoot } from "./benchmark-authority-v2.mjs";
+
+const matrixPath = resolve(repositoryRoot, "docs/benchmarks/tailwind-v4-competitive-matrix.json");
+const documentPath = resolve(repositoryRoot, "docs/benchmarks/tailwind-v4-competitive-matrix.md");
+
+function cell(value) {
+ return `${value}`.replaceAll("|", "\\|").replaceAll(/\s+/gu, " ").trim();
+}
+
+export function renderTailwindMatrix() {
+ const matrix = JSON.parse(readFileSync(matrixPath, "utf8"));
+ const authority = loadAuthority();
+ const lanes = authority.lanes
+ .map(
+ (lane) =>
+ `| \`${lane.id}\` | ${cell(lane.role)} | \`${lane.distTag ?? "pinned"}\` | \`${lane.version}\` |`,
+ )
+ .join("\n");
+ const dimensions = matrix.dimensions
+ .map(
+ (dimension) =>
+ `| \`${dimension.id}\` | ${cell(dimension.area)} | **${cell(dimension.status)}** | ${cell(dimension.tailwind)} | ${cell(dimension.pliego)} | ${cell(dimension.releaseImpact)} |`,
+ )
+ .join("\n");
+ const priorities = (impact) =>
+ matrix.dimensions
+ .filter((dimension) => dimension.releaseImpact === impact)
+ .map((dimension) => `- \`${dimension.id}\`: ${dimension.nextAction}`)
+ .join("\n") || "- None.";
+ const counts = Object.fromEntries(Object.keys(matrix.statuses).map((status) => [status, 0]));
+ for (const dimension of matrix.dimensions) counts[dimension.status] += 1;
+ const countText = Object.entries(counts)
+ .map(([status, count]) => `\`${status}\` ${count}`)
+ .join(", ");
+
+ return `# PliegoCSS and Tailwind CSS ${matrix.baseline.version}
+
+Status: **generated competitive matrix for release planning; not a universal performance or parity claim**
+
+This document is generated from [\`tailwind-v4-competitive-matrix.json\`](./tailwind-v4-competitive-matrix.json).
+The competitor and corpus authority is [Benchmark Authority v2](./tailwind-benchmark-authority-v2.md).
+Edit those JSON contracts and regenerate; do not update this table by hand.
+
+PliegoCSS is not positioned as “Tailwind in Rust.” Tailwind is the production utility-framework
+baseline; PliegoCSS competes through typed semantics, policy, provenance, ownership, diagnostics and
+receipts. Catalog breadth and a historical single-fixture timing are not a complete score.
+
+## Active oracle
+
+Primary lane: \`${authority.primaryLane}\`. Oracle observation: \`${authority.observedAtUtc}\`.
+Expiration: \`${authority.expiresAtUtc}\`. Reset contract: **${authority.resetContract}**.
+
+| Lane | Role | Registry selector | Version |
+|---|---|---|---:|
+${lanes}
+
+The \`4.3.2\` lane is historical regression control only. Current performance and payload claims
+remain open until immutable schema-2 evidence is recorded from a clean tree with complete paired
+samples, peak memory, gzip, Brotli and all three corpus sizes.
+
+## Executive matrix
+
+| Dimension | Area | Assessment | Tailwind | PliegoCSS | Release impact |
+|---|---|---|---|---|---|
+${dimensions}
+
+Status totals: ${countText}.
+
+## Blocking
+
+${priorities("blocking")}
+
+## Should
+
+${priorities("should")}
+
+## Explicitly post-0.1
+
+${priorities("post-0.1")}
+
+## Historical evidence boundary
+
+[Gate B](./pliego-gate-b.md) remains immutable historical evidence for Tailwind \`4.3.2\` on its
+recorded host. It cannot represent \`tailwind-latest\`, v3-LTS, current paired latency, memory,
+Brotli, or micro/medium/large behavior. No current competitive score is emitted from that snapshot.
+
+## Verification
+
+\`\`\`console
+pnpm check:benchmark-authority
+pnpm check:benchmark-oracle
+pnpm check:tailwind-matrix
+node scripts/measure-benchmark-authority-v2.mjs --smoke
+\`\`\`
+`;
+}
+
+const arguments_ = process.argv.slice(2);
+if (arguments_.length !== 1 || !new Set(["--check", "--write"]).has(arguments_[0])) {
+ throw new Error("usage: node scripts/render-tailwind-matrix.mjs --check|--write");
+}
+const rendered = renderTailwindMatrix();
+if (arguments_[0] === "--write") {
+ writeFileSync(documentPath, rendered);
+ process.stdout.write(`${documentPath}\n`);
+} else {
+ if (readFileSync(documentPath, "utf8") !== rendered) {
+ throw new Error("Tailwind competitive matrix Markdown is stale; run the renderer with --write");
+ }
+ process.stdout.write(`${JSON.stringify({ schemaVersion: 2, status: "passed", document: "docs/benchmarks/tailwind-v4-competitive-matrix.md" }, null, 2)}\n`);
+}
diff --git a/scripts/rust-target.mjs b/scripts/rust-target.mjs
new file mode 100644
index 0000000..a34c3fd
--- /dev/null
+++ b/scripts/rust-target.mjs
@@ -0,0 +1,56 @@
+import { createHash } from "node:crypto";
+import { spawnSync } from "node:child_process";
+import { isAbsolute, join, resolve } from "node:path";
+
+function absoluteTargetBase(root, env) {
+ const configured = env.PLIEGOCSS_TARGET_BASE ?? env.CARGO_TARGET_DIR;
+ if (!configured) return join(root, "target");
+ return isAbsolute(configured) ? configured : resolve(root, configured);
+}
+
+function safeSegment(value) {
+ return value.toLowerCase().replaceAll(/[^a-z0-9._-]+/gu, "-").replaceAll(/^-+|-+$/gu, "");
+}
+
+export function targetDirectoryForRustcIdentity(root, env, identity) {
+ const digest = createHash("sha256").update(identity).digest("hex");
+ const release = identity.match(/^release:\s*(.+)$/mu)?.[1] ?? "unknown";
+ const host = identity.match(/^host:\s*(.+)$/mu)?.[1] ?? `${process.platform}-${process.arch}`;
+ const segment = `${safeSegment(host)}-rustc-${safeSegment(release)}-${digest.slice(0, 16)}`;
+ return join(absoluteTargetBase(root, env), "toolchains", segment);
+}
+
+function rustcIdentity(env, toolchain) {
+ const command = toolchain ? "rustup" : (env.RUSTC ?? "rustc");
+ const args = toolchain ? ["run", toolchain, "rustc", "-Vv"] : ["-Vv"];
+ const result = spawnSync(command, args, {
+ env,
+ encoding: "utf8",
+ windowsHide: true,
+ });
+ if (result.error) throw result.error;
+ if (result.status !== 0) {
+ throw new Error(`cannot identify Rust toolchain (${command} ${args.join(" ")}): ${result.stderr}`);
+ }
+ return result.stdout.replaceAll("\r\n", "\n").trim();
+}
+
+export function isolatedCargoEnvironment(root, options = {}) {
+ const env = options.env ?? process.env;
+ if (env.PLIEGOCSS_ISOLATED_TARGET === "1" && !options.toolchain) return { ...env };
+ const identity = rustcIdentity(env, options.toolchain);
+ const target = targetDirectoryForRustcIdentity(root, env, identity);
+ return {
+ ...env,
+ CARGO_TARGET_DIR: target,
+ PLIEGOCSS_TARGET_BASE: absoluteTargetBase(root, env),
+ PLIEGOCSS_ISOLATED_TARGET: "1",
+ PLIEGOCSS_RUSTC_IDENTITY_SHA256: createHash("sha256").update(identity).digest("hex"),
+ };
+}
+
+export function cargoTargetRoot(root, env = process.env) {
+ const configured = env.CARGO_TARGET_DIR;
+ if (!configured) return join(root, "target");
+ return isAbsolute(configured) ? configured : resolve(root, configured);
+}
diff --git a/site/Cargo.lock b/site/Cargo.lock
index dfa6fa1..bc5ae5c 100644
--- a/site/Cargo.lock
+++ b/site/Cargo.lock
@@ -356,6 +356,7 @@ dependencies = [
"pliego-css",
"pliego-dom",
"pliego-ssg",
+ "serde",
"serde_json",
]
diff --git a/site/Cargo.toml b/site/Cargo.toml
index 69c8090..d974022 100644
--- a/site/Cargo.toml
+++ b/site/Cargo.toml
@@ -19,4 +19,5 @@ bench = false
pliego-css = { path = "../crates/pliego-css", version = "=0.1.0-rc.2" }
pliego-dom = "=0.0.2"
pliego-ssg = "=0.0.2"
+serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
diff --git a/site/README.md b/site/README.md
index a3737e1..0844f32 100644
--- a/site/README.md
+++ b/site/README.md
@@ -2,18 +2,32 @@
# PliegoCSS website
-The official site is authored as escaped PliegoRS views and published by the
-PliegoRS SSG. PliegoCSS `pc!` literals provide real compile-time style
-identities and the same source is compiled into the shipped utility bundle.
+The official site shell is authored as escaped PliegoRS views and published by
+the PliegoRS SSG. Public documentation prose lives in `docs/site/**/*.md`;
+`site/scripts/generate-docs.mjs` strictly parses those files and writes the
+tracked `site/src/docs.generated.json` projection with the exact source path
+and SHA-256 for every page. Rust consumes that projection and does not carry a
+second hand-maintained document catalog. The release-readiness page is itself
+generated from the schema-3 machine authority.
+
+PliegoCSS `pc!` literals provide real compile-time style identities and the
+same Rust source is compiled into the shipped utility bundle.
GSAP, Lenis, and Three.js run behind an explicit client lifecycle with reduced
motion, Save-Data, visibility, and WebGL fallbacks.
```console
pnpm site:build
pnpm site:preview
+pnpm check:site-docs
pnpm check:site
```
+Run `node site/scripts/generate-docs.mjs --write` after an intentional Markdown
+change. Build and validation use `--check` and fail if the tracked projection
+drifts. Each Markdown file must keep the closed `pliegocss-site` metadata
+header, one H1, a one-line summary, and sections shaped as
+`## Title {#stable-id}` with one body paragraph and at most one code fence.
+
The generated site is written to `site/target/site/` and is not committed.
The public deployment profile in `wrangler.jsonc` serves that exact directory
from Cloudflare Workers Static Assets at `pliegocss.dev`, with preview URLs and
diff --git a/site/scripts/build.mjs b/site/scripts/build.mjs
index 6d48f4b..60202f7 100644
--- a/site/scripts/build.mjs
+++ b/site/scripts/build.mjs
@@ -65,6 +65,33 @@ function runCaptured(command, args, options = {}) {
});
}
+function runPliegoCssCaptured(args) {
+ if (process.platform === "win32" && existsSync(applicationControlMarker)) {
+ const translated = args.map((argument) =>
+ /^[A-Za-z]:[\\/]/u.test(argument) ? windowsToWsl(argument) : argument,
+ );
+ return runCaptured("wsl.exe", [
+ "--cd",
+ windowsToWsl(root),
+ "-e",
+ wslPliegoCssExecutable(),
+ ...translated,
+ ]);
+ }
+ return runCaptured(cli, args);
+}
+
+function runPliegoCss(args) {
+ const result = runPliegoCssCaptured(args);
+ if (result.error) throw result.error;
+ if (result.status !== 0) {
+ throw new Error(`pliego-cssc failed with ${result.status}\n${result.stderr ?? ""}`);
+ }
+ process.stdout.write(result.stdout ?? "");
+ process.stderr.write(result.stderr ?? "");
+ return result.stdout ?? "";
+}
+
function sha256(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
@@ -132,10 +159,9 @@ function prepareAssets() {
}
function buildPliegoCss() {
- run("cargo", ["+1.85.0", "build", "--locked", "-p", "pliego-cssc"]);
const outputCss = join(generated, "pliegocss.css");
const manifest = join(generated, "pliegocss.manifest.json");
- run(cli, [
+ const arguments_ = [
"compile",
"--source",
join(siteRoot, "src"),
@@ -145,7 +171,31 @@ function buildPliegoCss() {
outputCss,
"--manifest",
manifest,
- ]);
+ ];
+ if (process.platform === "win32" && existsSync(applicationControlMarker)) {
+ buildPliegoCssWithWsl(outputCss, manifest);
+ } else {
+ run("cargo", ["+1.85.0", "build", "--locked", "-p", "pliego-cssc"]);
+ const result = runCaptured(cli, arguments_);
+ const diagnostics = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
+ if (result.error || result.status !== 0) {
+ if (
+ process.platform === "win32" &&
+ (result.error || diagnostics.includes("Application Control policy has blocked this file"))
+ ) {
+ mkdirSync(dirname(applicationControlMarker), { recursive: true });
+ writeFileSync(applicationControlMarker, "Windows executable launch blocked; use verified WSL build.\n");
+ buildPliegoCssWithWsl(outputCss, manifest);
+ } else if (result.error) {
+ throw result.error;
+ } else {
+ throw new Error(`pliego-cssc failed with ${result.status}\n${diagnostics}`);
+ }
+ } else {
+ process.stdout.write(result.stdout ?? "");
+ process.stderr.write(result.stderr ?? "");
+ }
+ }
const manifestDocument = JSON.parse(readFileSync(manifest, "utf8"));
if (
manifestDocument.cssSha256 !== sha256(readFileSync(outputCss)) ||
@@ -231,7 +281,7 @@ function generateLaboratory() {
mkdirSync(runtime, { recursive: true });
const cssPath = join(runtime, "laboratory.css");
const manifestPath = join(runtime, "laboratory.manifest.json");
- run(cli, [
+ runPliegoCss([
"compile",
...combinations.flatMap((combination) => ["--style", combination.input]),
"--seed",
@@ -293,7 +343,7 @@ function generateLaboratory() {
label: "Interactive action",
},
].map((entry) => {
- const result = runCaptured(cli, [
+ const result = runPliegoCssCaptured([
"explain",
"--style",
entry.input,
@@ -329,7 +379,7 @@ function generateLaboratory() {
input: "p-4 md:p-6",
},
].map((entry) => {
- const result = runCaptured(cli, [
+ const result = runPliegoCssCaptured([
"--diagnostic-format",
"json",
"check",
@@ -384,7 +434,7 @@ function generateLaboratory() {
function generateCatalog() {
const output = join(generated, "catalog.json");
- run(cli, [
+ runPliegoCss([
"catalog",
"--seed",
"--format",
@@ -515,6 +565,54 @@ function buildSiteWithWsl() {
]);
}
+function buildPliegoCssWithWsl(outputCss, manifest) {
+ const home = wslHome();
+ const cargo = `${home}/.cargo/bin/cargo`;
+ const wslRoot = windowsToWsl(root);
+ const wslTarget = wslPliegoCssTarget();
+ const environment = [
+ `PATH=${home}/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
+ `CARGO_TARGET_DIR=${wslTarget}`,
+ ];
+ run("wsl.exe", [
+ "--cd",
+ wslRoot,
+ "-e",
+ "env",
+ ...environment,
+ cargo,
+ "+1.85.0",
+ "build",
+ "--locked",
+ "-p",
+ "pliego-cssc",
+ ]);
+ run("wsl.exe", [
+ "--cd",
+ wslRoot,
+ "-e",
+ `${wslTarget}/debug/pliego-cssc`,
+ "compile",
+ "--source",
+ windowsToWsl(join(siteRoot, "src")),
+ "--seed",
+ "--theme",
+ "--output",
+ windowsToWsl(outputCss),
+ "--manifest",
+ windowsToWsl(manifest),
+ ]);
+}
+
+function wslPliegoCssTarget() {
+ const checkoutKey = sha256(Buffer.from(root, "utf8")).slice(0, 16);
+ return `${wslHome()}/.cache/pliegocss/site-css-${checkoutKey}`;
+}
+
+function wslPliegoCssExecutable() {
+ return `${wslPliegoCssTarget()}/debug/pliego-cssc`;
+}
+
function buildSite() {
rmSync(output, { recursive: true, force: true });
if (process.platform === "win32" && existsSync(applicationControlMarker)) {
@@ -552,6 +650,7 @@ function buildSite() {
}
}
+run(process.execPath, [join(siteRoot, "scripts", "generate-docs.mjs"), "--check"]);
prepareAssets();
buildPliegoCss();
generateLaboratory();
diff --git a/site/scripts/check-site.mjs b/site/scripts/check-site.mjs
index 0785aac..5b08360 100644
--- a/site/scripts/check-site.mjs
+++ b/site/scripts/check-site.mjs
@@ -81,6 +81,7 @@ function staticContract() {
"docs/integrations/pliegors/index.html",
"docs/tooling/repair/index.html",
"docs/reference/diagnostics/index.html",
+ "docs/release-readiness/index.html",
"docs/utilities/index.html",
"playground/index.html",
"examples/index.html",
@@ -97,6 +98,7 @@ function staticContract() {
"es/index.html",
"es/docs/index.html",
"es/docs/getting-started/index.html",
+ "es/docs/release-readiness/index.html",
"es/docs/utilities/index.html",
"es/playground/index.html",
"es/examples/index.html",
@@ -211,12 +213,35 @@ function staticContract() {
const sitemap = readFileSync(join(output, "sitemap.xml"), "utf8");
const sitemapUrls = sitemap.match(//gu)?.length ?? 0;
if (
- sitemapUrls !== 88 ||
+ sitemapUrls !== 90 ||
!sitemap.includes('xmlns:xhtml="http://www.w3.org/1999/xhtml"') ||
!sitemap.includes("https://pliegocss.dev/es/legal/privacy/")
) {
fail(`bilingual sitemap drifted (${sitemapUrls} URLs)`);
}
+ const readiness = JSON.parse(
+ readFileSync(join(root, "docs", "product", "release-readiness-0.1.0.json"), "utf8"),
+ );
+ const generatedDocs = JSON.parse(
+ readFileSync(join(siteRoot, "src", "docs.generated.json"), "utf8"),
+ );
+ const readinessDocument = generatedDocs.documents.find(
+ (document) => document.route === "/docs/release-readiness/",
+ );
+ const readinessHtml = readFileSync(
+ join(output, "docs", "release-readiness", "index.html"),
+ "utf8",
+ );
+ if (
+ !readinessDocument ||
+ !readinessHtml.includes(readiness.source.commit) ||
+ !readinessHtml.includes(readiness.source.gitTree) ||
+ !readinessHtml.includes(readinessDocument.sourcePath) ||
+ !readinessHtml.includes(readinessDocument.sourceSha256) ||
+ !readinessHtml.includes("Current blockers")
+ ) {
+ fail("release readiness route is not bound to the generated Markdown authority");
+ }
const laboratory = JSON.parse(
readFileSync(join(output, "assets", "laboratory.json")),
);
@@ -309,22 +334,55 @@ async function browserContract() {
await new Promise((resolveListen) => server.listen(0, "127.0.0.1", resolveListen));
const port = server.address().port;
const profile = mkdtempSync(join(tmpdir(), "pliegocss-site-cdp-"));
- const debugPort = 10500 + Math.floor(Math.random() * 500);
+ const debugPortFile = join(profile, "DevToolsActivePort");
+ const chromeStderr = [];
+ let childError;
const child = spawn(
browser,
[
"--headless=new",
- `--remote-debugging-port=${debugPort}`,
+ "--remote-debugging-port=0",
`--user-data-dir=${profile}`,
"--no-first-run",
"--disable-default-apps",
+ "--disable-dev-shm-usage",
"about:blank",
],
- { stdio: "ignore" },
+ { stdio: ["ignore", "ignore", "pipe"], windowsHide: true },
);
+ child.stderr.on("data", (chunk) => {
+ chromeStderr.push(chunk);
+ if (chromeStderr.length > 32) chromeStderr.shift();
+ });
+ child.once("error", (error) => {
+ childError = error;
+ });
try {
+ let debugPort;
+ for (let attempt = 0; attempt < 300; attempt += 1) {
+ if (existsSync(debugPortFile)) {
+ const candidate = Number.parseInt(
+ readFileSync(debugPortFile, "utf8").split(/\r?\n/u)[0],
+ 10,
+ );
+ if (Number.isInteger(candidate) && candidate > 0 && candidate <= 65_535) {
+ debugPort = candidate;
+ break;
+ }
+ }
+ if (childError || child.exitCode !== null) break;
+ await delay(100);
+ }
+ if (!debugPort) {
+ const stderr = Buffer.concat(chromeStderr).toString("utf8").trim().slice(-2_000);
+ fail(
+ `Chrome CDP bootstrap unavailable (browser=${browser}, exit=${
+ child.exitCode ?? "running"
+ }, error=${childError?.message ?? "none"}, stderr=${JSON.stringify(stderr)})`,
+ );
+ }
let version;
- for (let attempt = 0; attempt < 80; attempt += 1) {
+ for (let attempt = 0; attempt < 100; attempt += 1) {
try {
version = await (
await fetch(`http://127.0.0.1:${debugPort}/json/version`)
@@ -334,7 +392,13 @@ async function browserContract() {
await delay(100);
}
}
- if (!version) fail("Chrome CDP endpoint unavailable");
+ if (!version) {
+ fail(
+ `Chrome CDP endpoint unavailable on allocated port ${debugPort} (exit=${
+ child.exitCode ?? "running"
+ })`,
+ );
+ }
let target;
for (let attempt = 0; attempt < 60; attempt += 1) {
const targets = await (
@@ -651,7 +715,7 @@ async function browserContract() {
})()`,
});
if (
- docs.result.value.items < 29 ||
+ docs.result.value.items < 30 ||
docs.result.value.groups !== 8 ||
docs.result.value.visible !== 1 ||
docs.result.value.visibleGroups !== 1
diff --git a/site/scripts/generate-docs.mjs b/site/scripts/generate-docs.mjs
new file mode 100644
index 0000000..cd5a134
--- /dev/null
+++ b/site/scripts/generate-docs.mjs
@@ -0,0 +1,194 @@
+// SPDX-License-Identifier: Apache-2.0
+
+import { createHash } from "node:crypto";
+import {
+ existsSync,
+ readFileSync,
+ readdirSync,
+ statSync,
+ writeFileSync,
+} from "node:fs";
+import { dirname, relative, resolve, sep } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const scriptPath = fileURLToPath(import.meta.url);
+const siteRoot = resolve(dirname(scriptPath), "..");
+const root = resolve(siteRoot, "..");
+const docsRoot = resolve(root, "docs/site");
+const outputPath = resolve(siteRoot, "src/docs.generated.json");
+const metadataFields = [
+ "schemaVersion",
+ "route",
+ "category",
+ "eyebrow",
+ "order",
+];
+
+function fail(sourcePath, message) {
+ throw new Error(`site docs: ${sourcePath}: ${message}`);
+}
+
+function exactFields(value, expected, sourcePath, role) {
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
+ fail(sourcePath, `${role} must be an object`);
+ }
+ const actual = Object.keys(value).sort();
+ if (JSON.stringify(actual) !== JSON.stringify([...expected].sort())) {
+ fail(sourcePath, `${role} fields drifted: ${actual.join(", ")}`);
+ }
+}
+
+function portablePath(path) {
+ return path.split(sep).join("/");
+}
+
+function markdownFiles(directory) {
+ const files = [];
+ for (const entry of readdirSync(directory).sort()) {
+ const path = resolve(directory, entry);
+ if (statSync(path).isDirectory()) files.push(...markdownFiles(path));
+ else if (entry.endsWith(".md")) files.push(path);
+ }
+ return files;
+}
+
+function oneLine(lines, sourcePath, role) {
+ if (lines.length !== 1 || lines[0].trim() !== lines[0] || !lines[0]) {
+ fail(sourcePath, `${role} must be one non-empty paragraph line`);
+ }
+ return lines[0];
+}
+
+export function parseDocument(bytes, sourcePath = "document.md") {
+ const source = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes, "utf8");
+ const text = source.toString("utf8").replaceAll("\r\n", "\n");
+ const lines = text.endsWith("\n") ? text.slice(0, -1).split("\n") : text.split("\n");
+ if (
+ lines[0] !== "" ||
+ lines[3] !== ""
+ ) {
+ fail(sourcePath, "expected the strict three-line pliegocss-site header");
+ }
+ let metadata;
+ try {
+ metadata = JSON.parse(lines[1]);
+ } catch (error) {
+ fail(sourcePath, `invalid metadata JSON: ${error.message}`);
+ }
+ exactFields(metadata, metadataFields, sourcePath, "metadata");
+ if (metadata.schemaVersion !== 1) fail(sourcePath, "unsupported metadata schema");
+ if (!/^\/docs\/[a-z0-9][a-z0-9/-]*\/$/u.test(metadata.route)) {
+ fail(sourcePath, "route must be a canonical /docs/.../ path");
+ }
+ if (
+ typeof metadata.category !== "string" ||
+ !metadata.category ||
+ typeof metadata.eyebrow !== "string" ||
+ !metadata.eyebrow ||
+ !Number.isSafeInteger(metadata.order) ||
+ metadata.order < 1
+ ) {
+ fail(sourcePath, "category, eyebrow, or order is invalid");
+ }
+ const titleMatch = /^# (.+)$/u.exec(lines[4] ?? "");
+ if (!titleMatch || lines[5] !== "") fail(sourcePath, "expected one H1 followed by a blank line");
+ const summary = oneLine([lines[6] ?? ""], sourcePath, "summary");
+ if (lines[7] !== "") fail(sourcePath, "summary must be followed by a blank line");
+
+ const sections = [];
+ const sectionIds = new Set();
+ let index = 8;
+ while (index < lines.length) {
+ const heading = /^## (.+) \{#([a-z0-9][a-z0-9-]*)\}$/u.exec(lines[index] ?? "");
+ if (!heading) fail(sourcePath, `unsupported structure at line ${index + 1}`);
+ if (sectionIds.has(heading[2])) fail(sourcePath, `duplicate section id ${heading[2]}`);
+ sectionIds.add(heading[2]);
+ if (lines[index + 1] !== "") fail(sourcePath, `section ${heading[2]} needs a blank line`);
+ const body = oneLine([lines[index + 2] ?? ""], sourcePath, `section ${heading[2]} body`);
+ index += 3;
+ let code = null;
+ if (index < lines.length) {
+ if (lines[index] !== "") fail(sourcePath, `section ${heading[2]} body must end with a blank line`);
+ index += 1;
+ if ((lines[index] ?? "").startsWith("```")) {
+ if (!/^```[a-z0-9-]*$/u.test(lines[index])) {
+ fail(sourcePath, `section ${heading[2]} has an invalid code fence`);
+ }
+ index += 1;
+ const codeLines = [];
+ while (index < lines.length && lines[index] !== "```") {
+ codeLines.push(lines[index]);
+ index += 1;
+ }
+ if (lines[index] !== "```") fail(sourcePath, `section ${heading[2]} code fence is open`);
+ if (codeLines.length === 0) fail(sourcePath, `section ${heading[2]} code is empty`);
+ code = codeLines.join("\n");
+ index += 1;
+ if (index < lines.length) {
+ if (lines[index] !== "") fail(sourcePath, `section ${heading[2]} code must end with a blank line`);
+ index += 1;
+ }
+ }
+ }
+ sections.push({ id: heading[2], title: heading[1], body, code });
+ }
+ if (sections.length === 0) fail(sourcePath, "at least one section is required");
+ return {
+ sourcePath,
+ sourceSha256: `sha256:${createHash("sha256").update(source).digest("hex")}`,
+ route: metadata.route,
+ title: titleMatch[1],
+ category: metadata.category,
+ eyebrow: metadata.eyebrow,
+ summary,
+ order: metadata.order,
+ sections,
+ };
+}
+
+export function generateManifest() {
+ if (!existsSync(docsRoot)) throw new Error("site docs: docs/site does not exist");
+ const documents = markdownFiles(docsRoot).map((path) => {
+ const sourcePath = portablePath(relative(root, path));
+ if (!sourcePath.startsWith("docs/site/") || sourcePath.includes("../")) {
+ fail(sourcePath, "source escaped docs/site");
+ }
+ return parseDocument(readFileSync(path), sourcePath);
+ });
+ documents.sort((left, right) => left.order - right.order || left.route.localeCompare(right.route));
+ const routes = new Set();
+ for (const [index, document] of documents.entries()) {
+ if (document.order !== index + 1) {
+ fail(document.sourcePath, `order must be contiguous; expected ${index + 1}`);
+ }
+ if (routes.has(document.route)) fail(document.sourcePath, `duplicate route ${document.route}`);
+ routes.add(document.route);
+ }
+ return { schemaVersion: 1, kind: "pliegocss-site-markdown", documents };
+}
+
+export function renderManifest() {
+ return `${JSON.stringify(generateManifest(), null, 2)}\n`;
+}
+
+function main() {
+ const args = process.argv.slice(2);
+ if (args.length > 1 || (args[0] && !["--check", "--write"].includes(args[0]))) {
+ throw new Error("usage: node site/scripts/generate-docs.mjs [--check|--write]");
+ }
+ const mode = args[0] ?? "--check";
+ const rendered = renderManifest();
+ if (mode === "--write") {
+ writeFileSync(outputPath, rendered, "utf8");
+ process.stdout.write(`wrote ${outputPath}\n`);
+ return;
+ }
+ if (!existsSync(outputPath) || readFileSync(outputPath, "utf8").replaceAll("\r\n", "\n") !== rendered) {
+ throw new Error("site docs: generated manifest drifted; run with --write");
+ }
+ const manifest = JSON.parse(rendered);
+ process.stdout.write(`site Markdown manifest: current (${manifest.documents.length} documents)\n`);
+}
+
+if (process.argv[1] && resolve(process.argv[1]) === scriptPath) main();
diff --git a/site/scripts/generate-docs.test.mjs b/site/scripts/generate-docs.test.mjs
new file mode 100644
index 0000000..823460b
--- /dev/null
+++ b/site/scripts/generate-docs.test.mjs
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: Apache-2.0
+
+import assert from "node:assert/strict";
+import { generateManifest, parseDocument } from "./generate-docs.mjs";
+
+const manifest = generateManifest();
+assert.equal(manifest.schemaVersion, 1);
+assert.equal(manifest.kind, "pliegocss-site-markdown");
+assert.equal(manifest.documents.length, 30);
+assert.equal(new Set(manifest.documents.map((document) => document.route)).size, 30);
+assert.equal(manifest.documents.at(-1).route, "/docs/release-readiness/");
+assert.match(manifest.documents.at(-1).sourceSha256, /^sha256:[a-f0-9]{64}$/u);
+
+const sample = `
+
+# Sample title
+
+Sample summary.
+
+## First section {#first}
+
+Sample body.
+`;
+assert.equal(parseDocument(sample).sections[0].body, "Sample body.");
+assert.throws(
+ () => parseDocument(sample.replace("## First section", "### First section")),
+ /unsupported structure/u,
+);
+assert.throws(
+ () => parseDocument(sample.replace('"order":1', '"order":1,"extra":true')),
+ /metadata fields drifted/u,
+);
+process.stdout.write("site Markdown source contracts: pass\n");
diff --git a/site/src/docs.generated.json b/site/src/docs.generated.json
new file mode 100644
index 0000000..f36f7a9
--- /dev/null
+++ b/site/src/docs.generated.json
@@ -0,0 +1,906 @@
+{
+ "schemaVersion": 1,
+ "kind": "pliegocss-site-markdown",
+ "documents": [
+ {
+ "sourcePath": "docs/site/getting-started.md",
+ "sourceSha256": "sha256:759274aa8cd671310039549403f44456199a2ebd624e89f56e375aa875c66ded",
+ "route": "/docs/getting-started/",
+ "title": "Start with the CSS you already have.",
+ "category": "Start",
+ "eyebrow": "Getting started",
+ "summary": "Audit first, compile when it adds value, and keep the browser contract ordinary.",
+ "order": 1,
+ "sections": [
+ {
+ "id": "install",
+ "title": "Install the release candidate",
+ "body": "The complete 0.1.0-rc.2 compatibility unit is published on crates.io and replayed from the registry with Rust 1.85.",
+ "code": "cargo install pliego-cssc --version '=0.1.0-rc.2' --locked"
+ },
+ {
+ "id": "audit",
+ "title": "Audit ordinary CSS",
+ "body": "Start without changing authoring. The analyzer emits human, canonical JSON, or SARIF findings and fails closed on unknown surfaces.",
+ "code": "pliego-cssc audit --input app.css \\\n --targets baseline-widely --format human"
+ },
+ {
+ "id": "compile",
+ "title": "Add typed identities selectively",
+ "body": "Rust literals are validated at compile time and compile to static standards-compliant CSS. There is no styling runtime.",
+ "code": "use pliego_css::{pc, Style};\n\nconst BUTTON: Style = pc!(\n \"inline-flex gap-2 rounded-lg bg-accent px-4 py-2\"\n);"
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/typed-styles.md",
+ "sourceSha256": "sha256:69cc2ec5e262871914e68e7f49a0f5502b748d6872c3401ac7147f350a06223a",
+ "route": "/docs/typed-styles/",
+ "title": "Types at author time. CSS at run time.",
+ "category": "Core",
+ "eyebrow": "Typed styles",
+ "summary": "Stable 128-bit identities connect Rust source, emitted classes, manifests, and physical declarations.",
+ "order": 2,
+ "sections": [
+ {
+ "id": "pc",
+ "title": "One literal, one stable identity",
+ "body": "pc! accepts one visible string literal. The macro parses semantics, rejects conflicting slots, and returns a sealed Style.",
+ "code": "const CARD: Style = pc!(\n \"grid gap-4 rounded-xl border border-line p-6 shadow-md\"\n);"
+ },
+ {
+ "id": "pcx",
+ "title": "Visible state spaces",
+ "body": "pcx! compiles every visible branch combination and selects one identity at runtime. Independent clauses that can collide are rejected.",
+ "code": "let style = pcx!(\n \"block\",\n if active { \"opacity-100\" } else { \"opacity-50\" }\n);"
+ },
+ {
+ "id": "lineage",
+ "title": "Follow the physical effect",
+ "body": "Manifest schema 5 joins semantic declarations to final rules, conditions, byte ranges, and synthesized effects such as box-shadow.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/audit-and-transform.md",
+ "sourceSha256": "sha256:1ae15b85458272dbdf5bf697ec4d5161c5d995961bc39f3c9c08c72c79b29b84",
+ "route": "/docs/audit-and-transform/",
+ "title": "Standards-first, not framework-first.",
+ "category": "Standard CSS",
+ "eyebrow": "Audit and transform",
+ "summary": "Analyze standard CSS, enforce policy, and transform output without claiming ownership of authored source.",
+ "order": 3,
+ "sections": [
+ {
+ "id": "classifier",
+ "title": "Bounded classification",
+ "body": "The classifier recognizes deliberate standard-CSS surfaces and reports unsupported constructs instead of guessing. A versioned corpus controls the claim.",
+ "code": null
+ },
+ {
+ "id": "transform",
+ "title": "Deterministic transformation",
+ "body": "Target browsers, minify, and check output drift. Add --check in CI to remain read-only.",
+ "code": "pliego-cssc transform-css --input app.css \\\n --output dist/app.css --targets modern --format minified"
+ },
+ {
+ "id": "policy",
+ "title": "Compatibility, accessibility, and budgets",
+ "body": "Policy documents bind findings to explicit targets and limits. The same canonical findings feed terminals, JSON automation, and SARIF.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/evidence.md",
+ "sourceSha256": "sha256:3deb73bd0b795902d702ca0cf877a560e6db99c35817c21423466a571f6e48ef",
+ "route": "/docs/evidence/",
+ "title": "A green build should explain itself.",
+ "category": "Evidence",
+ "eyebrow": "Evidence and provenance",
+ "summary": "Artifacts carry hashes, source lineage, graphs, findings, and receipts designed for replay.",
+ "order": 4,
+ "sections": [
+ {
+ "id": "manifest",
+ "title": "Canonical manifests",
+ "body": "Style identity, class identity, theme identity, origins, conditions, physical declarations, and final CSS hashes live in a deterministic document.",
+ "code": "pliego-cssc compile --source src --seed --theme \\\n --output dist/app.css --manifest dist/app.manifest.json"
+ },
+ {
+ "id": "receipts",
+ "title": "Receipt-last publication",
+ "body": "Controlled operations stage output, lock the destination, verify inputs, publish artifacts, and write the receipt last.",
+ "code": null
+ },
+ {
+ "id": "benchmarks",
+ "title": "Measured, inherited, pending, uncertain",
+ "body": "Release readiness preserves the evidence class. Old hosted evidence never silently passes changed source.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/migration.md",
+ "sourceSha256": "sha256:6d3b48362eb1ef99e432a923ce2ede72ae231b9272199571827f56e6801ed6b3",
+ "route": "/docs/migration/",
+ "title": "Adopt without surrendering the exit.",
+ "category": "Migration",
+ "eyebrow": "Controlled migration",
+ "summary": "Inventory first, generate bounded plans, preserve originals, and detect drift immediately before publication.",
+ "order": 5,
+ "sections": [
+ {
+ "id": "inventory",
+ "title": "Read-only discovery",
+ "body": "Tailwind, Sass, CSS Modules, and standard CSS can be inventoried before any replacement plan exists.",
+ "code": "pliego-cssc migration-project-plan ./migration-declaration.json"
+ },
+ {
+ "id": "reversible",
+ "title": "Reversible replacements",
+ "body": "Plans bind source and destination hashes. Apply and rollback use adjacent locks, pre-publication revalidation, backups, and group compensation.",
+ "code": null
+ },
+ {
+ "id": "limits",
+ "title": "No speculative codemods",
+ "body": "Unsupported or context-dependent constructs stay in the inventory. The migration layer does not infer framework semantics it cannot prove.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/integrations/pliegors.md",
+ "sourceSha256": "sha256:dbfe2999c765e1ec3bd1e8868e474fac69bb735cef042596a6f5322b81b7fd29",
+ "route": "/docs/integrations/pliegors/",
+ "title": "Native together. Independent by contract.",
+ "category": "Integrations",
+ "eyebrow": "PliegoRS integration",
+ "summary": "PliegoRS is the first native consumer, not the only target. It supplies topology; PliegoCSS supplies style identity and artifacts.",
+ "order": 6,
+ "sections": [
+ {
+ "id": "boundary",
+ "title": "Explicit ownership",
+ "body": "PliegoRS owns routes, pages, assets, and browser lifecycle. PliegoCSS owns parsing, semantic conflicts, CSS, manifests, and policy findings.",
+ "code": null
+ },
+ {
+ "id": "bundles",
+ "title": "Route and island bundles",
+ "body": "A reachability sidecar projects application-owned topology into selected CSS bundles without PliegoCSS pretending to understand the framework.",
+ "code": null
+ },
+ {
+ "id": "proof",
+ "title": "Replayed from published crates",
+ "body": "The maintained fixture locks PliegoRS 0.0.2 registry packages and verifies the remote-reachable framework revision separately.",
+ "code": "PLIEGORS_ROOT=/path/to/pliegors pnpm integration:pliegors"
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/installation.md",
+ "sourceSha256": "sha256:2459d3c1c9082a6107df25e9dbc5b461623c182f064b623216dc509a17aa23ec",
+ "route": "/docs/installation/",
+ "title": "Choose an adoption surface.",
+ "category": "Start",
+ "eyebrow": "Installation",
+ "summary": "Install the CLI for any project, add the Rust crates only when typed authoring belongs in the application.",
+ "order": 7,
+ "sections": [
+ {
+ "id": "cli",
+ "title": "CLI-only adoption",
+ "body": "The command-line compiler can audit, transform, explain, bundle, and migrate standard CSS without adding a runtime dependency to the application.",
+ "code": "cargo install pliego-cssc --version '=0.1.0-rc.2' --locked\npliego-cssc --version"
+ },
+ {
+ "id": "rust",
+ "title": "Typed Rust authoring",
+ "body": "Add pliego-css and its macro crate to applications that want compile-time style identities. The emitted result remains ordinary CSS.",
+ "code": "[dependencies]\npliego-css = \"=0.1.0-rc.2\""
+ },
+ {
+ "id": "pin",
+ "title": "Pin the exact release identity",
+ "body": "Before the public release, use the signed project tag plus Cargo.lock. Do not track a moving branch in a production build.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/first-audit.md",
+ "sourceSha256": "sha256:d6476ad1b40afd7211cb9adbbc542384baeba1f15265c62a0e056641f9e3cfa7",
+ "route": "/docs/first-audit/",
+ "title": "Learn before changing.",
+ "category": "Start",
+ "eyebrow": "First audit",
+ "summary": "Run a read-only CSS audit, inspect canonical findings, then decide whether transformation or policy is justified.",
+ "order": 8,
+ "sections": [
+ {
+ "id": "human",
+ "title": "Read the first report",
+ "body": "Human output is optimized for local diagnosis. Every finding carries a stable code and source location rather than an opaque score.",
+ "code": "pliego-cssc audit --input app.css \\\n --targets baseline-widely --format human"
+ },
+ {
+ "id": "automation",
+ "title": "Feed automation safely",
+ "body": "Canonical JSON and SARIF preserve the same finding model for CI, code scanning, and archival evidence.",
+ "code": "pliego-cssc audit --input app.css --targets modern --format sarif > findings.sarif"
+ },
+ {
+ "id": "unknown",
+ "title": "Unknown means unknown",
+ "body": "The standard-CSS classifier is bounded. Unsupported selectors, properties, or values fail closed under compatibility policy rather than being treated as safe.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/editor-setup.md",
+ "sourceSha256": "sha256:6c222319c97d9648c1146dbb5c5ea6911531a39283aff12b1bc64444a3daca54",
+ "route": "/docs/editor-setup/",
+ "title": "Put the compiler engine in the feedback loop.",
+ "category": "Start",
+ "eyebrow": "Editor setup",
+ "summary": "Use `pliego-css-lsp` for diagnostics, completion, hover evidence, and catalog-aware authoring without launching the build CLI.",
+ "order": 9,
+ "sections": [
+ {
+ "id": "vscode",
+ "title": "VS Code extension",
+ "body": "The maintained extension launches `pliego-css-lsp`, discovers the project theme, and renders diagnostics in Rust source.",
+ "code": "pnpm --filter pliegocss-vscode package"
+ },
+ {
+ "id": "generic",
+ "title": "Any LSP client",
+ "body": "Launch the server over stdio from the same workspace used by CI. The protocol stays editor-neutral.",
+ "code": "pliego-css-lsp --seed"
+ },
+ {
+ "id": "safety",
+ "title": "Bound in-process work",
+ "body": "Document versions debounce for 150 ms, stale results are suppressed, semantic caches are bounded, and each document stops at 256 checks with `PCL002`.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/syntax.md",
+ "sourceSha256": "sha256:eca84634a88f97e387815fd73ae183b7c0b8c76218f4961056cc5f07e908846f",
+ "route": "/docs/syntax/",
+ "title": "A finite language with visible edges.",
+ "category": "Core",
+ "eyebrow": "Utility syntax",
+ "summary": "Utilities lower into semantic slots, conditions, and typed values before any CSS serialization occurs.",
+ "order": 10,
+ "sections": [
+ {
+ "id": "shape",
+ "title": "Utility plus typed value",
+ "body": "Spacing, color, typography, layout, effects, and interactivity draw from one validated theme registry. Arbitrary escape hatches remain policy-visible.",
+ "code": "flex items-center gap-4 rounded-lg bg-surface px-4 py-2 text-sm"
+ },
+ {
+ "id": "conditions",
+ "title": "Conditions are structure",
+ "body": "Responsive, state, attribute, container, layer, and media variants become normalized condition paths. Their order is not incidental string decoration.",
+ "code": "md:grid-cols-2 hover:bg-accent-strong aria-[expanded=true]:ring-2"
+ },
+ {
+ "id": "important",
+ "title": "Importance follows the physical effect",
+ "body": "A trailing exclamation mark marks the semantic contribution important. Synthesized effects such as the composed box-shadow preserve that authority in final CSS and lineage.",
+ "code": "ring-2! shadow-md!"
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/composition.md",
+ "sourceSha256": "sha256:bfa71645825196c5da9b03a52df6d01ddead2fb93d1afd900d10bbeb17cdf25e",
+ "route": "/docs/composition/",
+ "title": "Compose by meaning, not source order.",
+ "category": "Core",
+ "eyebrow": "Composition",
+ "summary": "Compatible slots commute. Conflicting slots require an explicit branch-over-base operation.",
+ "order": 11,
+ "sections": [
+ {
+ "id": "commute",
+ "title": "Equivalent input, equivalent identity",
+ "body": "Utilities that occupy independent semantic slots canonicalize to the same identity even when authored in a different order.",
+ "code": "flex gap-4 p-6\np-6 flex gap-4"
+ },
+ {
+ "id": "conflict",
+ "title": "Ambiguous lists are rejected",
+ "body": "p-4 p-6 does not mean last one wins. Both declarations claim the same slot under the same condition, so compilation stops.",
+ "code": "pliego-cssc check --style \"p-4 p-6\" --seed"
+ },
+ {
+ "id": "override",
+ "title": "Overrides are explicit operations",
+ "body": "When an application intentionally replaces one semantic assignment with another, use composition so the override relationship remains visible and inspectable.",
+ "code": "pliego-cssc compile --compose \"p-4\" \"p-6\" --seed"
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/variants.md",
+ "sourceSha256": "sha256:65640a6e4ecbb21cdb5a8a7e5d10aeeffeafac33491291365b7f1036f8c55b3c",
+ "route": "/docs/variants/",
+ "title": "Conditions without string-order folklore.",
+ "category": "Core",
+ "eyebrow": "Variants and conditions",
+ "summary": "Every variant joins a typed condition path with deterministic normalization and specificity behavior.",
+ "order": 12,
+ "sections": [
+ {
+ "id": "responsive",
+ "title": "Responsive paths",
+ "body": "Theme breakpoints are validated once and emitted as ordered media conditions. Assignments at different breakpoints do not conflict.",
+ "code": "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
+ },
+ {
+ "id": "state",
+ "title": "State and attributes",
+ "body": "Pseudo-class and bounded attribute variants stay in semantic IR so policy and physical lineage can explain their selectors.",
+ "code": "hover:bg-accent focus-visible:ring-2 data-[state=open]:opacity-100"
+ },
+ {
+ "id": "layers",
+ "title": "Cascade layers",
+ "body": "Fixed layer variants model author-controlled precedence without allowing arbitrary source-order dependence to leak into identity.",
+ "code": "layer-components:bg-surface layer-utilities:p-4"
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/themes.md",
+ "sourceSha256": "sha256:6a0abef1d4f797432dc0e8a0337993c324e212165133186a5cbe6591de30e224",
+ "route": "/docs/themes/",
+ "title": "Resolve tokens before styles.",
+ "category": "Core",
+ "eyebrow": "Themes and tokens",
+ "summary": "TOML themes and DTCG Resolver documents feed the same typed registry, identity, graph, and emitted variables.",
+ "order": 13,
+ "sections": [
+ {
+ "id": "toml",
+ "title": "Extend the seed",
+ "body": "A project theme can override or add typed colors, spacing, radii, typography, and breakpoints without changing utility grammar.",
+ "code": "schema = 1\nextends = \"seed\"\n\n[tokens.color]\nbrand = \"oklch(62% 0.18 250)\""
+ },
+ {
+ "id": "dtcg",
+ "title": "Select DTCG contexts explicitly",
+ "body": "Resolver inputs are never auto-discovered. Modifier selections participate in configuration identity even when two contexts resolve to equal values.",
+ "code": "pliego-cssc compile --tokens product.resolver.json \\\n --token-input appearance=dark --style \"bg-brand\""
+ },
+ {
+ "id": "graph",
+ "title": "Publish the complete Token Graph",
+ "body": "Controlled output binds selected CSS to canonical token provenance, aliases, permutations, and reachability-aware usage evidence.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/standard-css/policies.md",
+ "sourceSha256": "sha256:4e7ad817de6b1f1a9192d4c069fad466ed83087e23ed3cdca16bc373935682bc",
+ "route": "/docs/standard-css/policies/",
+ "title": "Make browser support and budgets executable.",
+ "category": "Standard CSS",
+ "eyebrow": "Policies",
+ "summary": "Compatibility, accessibility, payload, package, route, and ownership policies turn expectations into reviewable inputs.",
+ "order": 14,
+ "sections": [
+ {
+ "id": "compatibility",
+ "title": "Version the target",
+ "body": "baseline-widely and modern are frozen policy profiles with explicit browser and source-data identities. They do not drift silently with a dependency update.",
+ "code": "pliego-cssc compatibility --targets baseline-widely"
+ },
+ {
+ "id": "budgets",
+ "title": "Bind budgets to owners",
+ "body": "Direct files can name budget subjects manually. Asset Plans use an ownership sidecar to aggregate package and composed-route costs without guessing responsibility.",
+ "code": "pliego-cssc audit --asset-plan dist/pliego.assets.json \\\n --ownership pliego.ownership.json --budget-policy budgets.json"
+ },
+ {
+ "id": "accessibility",
+ "title": "Audit static accessibility contracts",
+ "body": "Contrast and interaction guards use explicit policy plus token evidence. Dynamic visual behavior still requires browser and assistive-technology testing.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/cascade.md",
+ "sourceSha256": "sha256:1ace78c08d0ff1f6c0f441f5e1ebae5be60dc1feb04fbeef54956f96fae5be57",
+ "route": "/docs/cascade/",
+ "title": "Explain the winning declaration.",
+ "category": "Standard CSS",
+ "eyebrow": "Cascade inspection",
+ "summary": "Inspect authored CSS by element and longhand to see matching selectors, precedence, and the final winner.",
+ "order": 15,
+ "sections": [
+ {
+ "id": "query",
+ "title": "Ask one precise question",
+ "body": "Provide a CSS file, a bounded element description, and one longhand. The command reports candidates instead of simulating an entire browser.",
+ "code": "pliego-cssc explain-cascade --input app.css \\\n --element 'button#save.action' --property background-color"
+ },
+ {
+ "id": "winner",
+ "title": "See why it won",
+ "body": "Origin, importance, layer, specificity, and source position are surfaced in comparison order with a stable machine-readable form.",
+ "code": null
+ },
+ {
+ "id": "boundary",
+ "title": "Keep the model honest",
+ "body": "This is a static bounded inspector. Runtime DOM state, animation interpolation, adopted sheets, and script mutation remain browser responsibilities.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/manifests.md",
+ "sourceSha256": "sha256:b301fa80c8b248fb5a91e2c3f26fbcc448c1cf68540cffe39752ec364755d036",
+ "route": "/docs/manifests/",
+ "title": "Trace source intent into emitted bytes.",
+ "category": "Evidence",
+ "eyebrow": "Style manifests",
+ "summary": "Manifest schema 5 connects source origins, semantic assignments, final selectors, physical declarations, and CSS ranges.",
+ "order": 16,
+ "sections": [
+ {
+ "id": "identity",
+ "title": "Identity chain",
+ "body": "StyleId identifies canonical semantics. Class identity, theme identity, configuration, and final CSS hash bind that style into one build.",
+ "code": "pliego-cssc compile --source src --manifest dist/app.manifest.json \\\n --manifest-version 5 --output dist/app.css"
+ },
+ {
+ "id": "physical",
+ "title": "Physical lineage",
+ "body": "Generated custom properties and synthesized declarations retain contributors, importance, conditions, selector, property, value, and output byte range.",
+ "code": null
+ },
+ {
+ "id": "versions",
+ "title": "Request the schema you consume",
+ "body": "Schema 3 supports style manifests, schema 4 adds reachability ownership, and schema 5 adds closed physical tracing. Consumers must pin their contract.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/control-artifacts.md",
+ "sourceSha256": "sha256:f80f80557d42574c8281bfcf6bb72ecd3884527f69ca1c81eb36d889a12a0e2d",
+ "route": "/docs/control-artifacts/",
+ "title": "Publish the receipt last.",
+ "category": "Evidence",
+ "eyebrow": "Control artifacts",
+ "summary": "Controlled builds stage related outputs, lock the destination, verify hashes, compensate failures, and expose one completed receipt.",
+ "order": 17,
+ "sections": [
+ {
+ "id": "group",
+ "title": "One rollback-capable group",
+ "body": "CSS, source map, style manifest, Token Graph, findings, control manifest, and receipt are generated from one exact input snapshot.",
+ "code": "pliego-cssc compile --source src --output dist/app.css \\\n --manifest dist/app.manifest.json --control-dir dist"
+ },
+ {
+ "id": "check",
+ "title": "Check without writing",
+ "body": "The same command with --check recomputes every artifact and fails on byte drift without taking publication locks or replacing files.",
+ "code": "pliego-cssc compile --source src --output dist/app.css \\\n --manifest dist/app.manifest.json --control-dir dist --check"
+ },
+ {
+ "id": "receipt",
+ "title": "Completion is explicit",
+ "body": "Consumers treat the fixed receipt as the publication boundary. Its absence means the group is incomplete, even if individual files are present.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/critical-css.md",
+ "sourceSha256": "sha256:85e43a1f36e743a2fc84328847e4e72ff42ce97f8b048d5233f43c3394dd3f1e",
+ "route": "/docs/critical-css/",
+ "title": "Prove what rendered above the fold.",
+ "category": "Evidence",
+ "eyebrow": "Critical style evidence",
+ "summary": "Join observed browser behavior to project topology and retained style identity without letting an observation silently authorize removal.",
+ "order": 18,
+ "sections": [
+ {
+ "id": "observe",
+ "title": "Observe a real route",
+ "body": "Capture viewport, route, interaction state, loaded assets, matched styles, and source identities from a browser run against the built application.",
+ "code": null
+ },
+ {
+ "id": "join",
+ "title": "Join exact universes",
+ "body": "Evidence is accepted only when its project index, asset plan, reachability, and CSS identities match the build under review.",
+ "code": null
+ },
+ {
+ "id": "authority",
+ "title": "Observation does not equal deletion",
+ "body": "A style unseen in one scope remains unknown outside that scope. Explicit retention and reachability policies control removal authority.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/migration/tailwind.md",
+ "sourceSha256": "sha256:f4aad2c2b31b7062736566d4c09690378354676239c5c039b6743794f8a42cf9",
+ "route": "/docs/migration/tailwind/",
+ "title": "Inventory Tailwind before translating it.",
+ "category": "Migration",
+ "eyebrow": "Tailwind migration",
+ "summary": "Recover the real utility surface, classify supported roles, and keep unsupported framework behavior visible.",
+ "order": 19,
+ "sections": [
+ {
+ "id": "inventory",
+ "title": "Scan without replacement",
+ "body": "The inventory recognizes bounded class-bearing surfaces and reports candidates with source ranges. It does not rewrite template semantics during discovery.",
+ "code": "pliego-cssc migration-inventory tailwind src/App.tsx"
+ },
+ {
+ "id": "plan",
+ "title": "Generate a hash-bound project plan",
+ "body": "A declaration selects roots and destinations. The plan records exact source and output hashes before any replacement can be applied.",
+ "code": "pliego-cssc migration-project-plan migration.json"
+ },
+ {
+ "id": "gaps",
+ "title": "Preserve unsupported behavior",
+ "body": "Plugins, generated variants, arbitrary selectors, and runtime class construction remain explicit migration findings unless a project-owned adapter proves them.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/migration/sass.md",
+ "sourceSha256": "sha256:e12a4888068eed66e73fdc900c47b10d2587a0c2a451f3c67e0312ae7a68b80a",
+ "route": "/docs/migration/sass/",
+ "title": "Separate syntax conversion from semantic adoption.",
+ "category": "Migration",
+ "eyebrow": "Sass migration",
+ "summary": "Inventory variables, nesting, mixins, imports, and generated surfaces without pretending every Sass abstraction is a utility.",
+ "order": 20,
+ "sections": [
+ {
+ "id": "inventory",
+ "title": "Map the source surface",
+ "body": "The inventory records statically visible Sass roles and unresolved dynamic behavior so the team can choose what stays authored CSS.",
+ "code": "pliego-cssc migration-inventory sass styles/main.scss"
+ },
+ {
+ "id": "css-first",
+ "title": "Ordinary CSS is a valid destination",
+ "body": "Use the standard-CSS transformer for syntax and compatibility work. Adopt typed PliegoCSS styles only where identity and provenance pay for themselves.",
+ "code": null
+ },
+ {
+ "id": "rollback",
+ "title": "Keep originals and rollback evidence",
+ "body": "Applied replacements preserve source backups and hash-bound rollback records. A changed destination aborts rather than overwriting concurrent edits.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/migration/css-modules.md",
+ "sourceSha256": "sha256:977a2a6184768213586d26b6ecd5da3b47ccc10ef0e88e96e26702a19af73752",
+ "route": "/docs/migration/css-modules/",
+ "title": "Preserve component ownership.",
+ "category": "Migration",
+ "eyebrow": "CSS Modules migration",
+ "summary": "Discover module exports and usage while keeping application-owned component topology separate from CSS semantics.",
+ "order": 21,
+ "sections": [
+ {
+ "id": "inventory",
+ "title": "Inventory exported classes",
+ "body": "The scanner identifies bounded module class definitions and their source locations without assuming a particular bundler naming strategy.",
+ "code": "pliego-cssc migration-inventory css-modules src/Card.module.css"
+ },
+ {
+ "id": "ownership",
+ "title": "Let the application own reachability",
+ "body": "Route and component reachability comes from the framework or product index, not from filename heuristics inside the CSS compiler.",
+ "code": null
+ },
+ {
+ "id": "mixed",
+ "title": "Mixed adoption is first-class",
+ "body": "A project can retain modules for component styles, audit their emitted CSS, and introduce typed identities only for new or shared surfaces.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/tooling/cli.md",
+ "sourceSha256": "sha256:e123d55383efc625fef6e59f9c9252514abf3277ba1ec8bf5aa72f7113b61509",
+ "route": "/docs/tooling/cli/",
+ "title": "One CLI, explicit modes.",
+ "category": "Tooling",
+ "eyebrow": "Command-line reference",
+ "summary": "Compile, inspect, audit, transform, explain, bundle, watch, migrate, format, plan, and verify with strict argument contracts.",
+ "order": 22,
+ "sections": [
+ {
+ "id": "discover",
+ "title": "Discover the real command surface",
+ "body": "Root and command help are generated from the maintained command synopsis. Unknown options, duplicate single-value options, and missing values fail.",
+ "code": "pliego-cssc --help\npliego-cssc compile --help"
+ },
+ {
+ "id": "diagnostics",
+ "title": "Select diagnostic transport globally",
+ "body": "Human and JSON diagnostics can be requested before or after the command without stealing option values that happen to resemble flags.",
+ "code": "pliego-cssc --diagnostic-format json check --style \"p-4 p-6\" --seed"
+ },
+ {
+ "id": "determinism",
+ "title": "Use --check in CI",
+ "body": "Commands that publish controlled or generated artifacts expose a read-only comparison path. CI should verify the checked-in or staged output, not regenerate it silently.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/tooling/lsp.md",
+ "sourceSha256": "sha256:22dbe9698cf617c210186f2edab5f0a6d2dcb061ffce39e6bac1a944743bda5e",
+ "route": "/docs/tooling/lsp/",
+ "title": "Compiler feedback without editor subprocesses.",
+ "category": "Tooling",
+ "eyebrow": "Language server",
+ "summary": "The LSP uses the same in-process `AnalysisHost` as CLI and watch without launching `pliego-cssc` or writing synthetic source files.",
+ "order": 23,
+ "sections": [
+ {
+ "id": "features",
+ "title": "Diagnostics, completion, and hover",
+ "body": "Visible `pc!` and `pcx!` literals receive shared diagnostics, catalog-backed completion, and compiler-emitted hover CSS under the exact active theme.",
+ "code": null
+ },
+ {
+ "id": "cancellation",
+ "title": "Suppress stale document work",
+ "body": "Document versions are debounced for 150 ms, and version checks around bounded in-process analysis prevent obsolete results from being published.",
+ "code": null
+ },
+ {
+ "id": "limits",
+ "title": "Bound analysis",
+ "body": "Each document is capped at 256 semantic checks, the shared cache is bounded and theme-invalidated, and excessive input emits `PCL002`.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/tooling/watch.md",
+ "sourceSha256": "sha256:3aabe8443539de2801d0270c155a0d47bea439203f73ce3fcbc5166765158cf2",
+ "route": "/docs/tooling/watch/",
+ "title": "Keep the last complete truth.",
+ "category": "Tooling",
+ "eyebrow": "Watch mode",
+ "summary": "Poll coherent snapshots, republish only valid groups, and retain the last valid output across transient edits.",
+ "order": 24,
+ "sections": [
+ {
+ "id": "run",
+ "title": "Watch inputs and output",
+ "body": "The watcher accepts the same source, theme, target, format, manifest, reachability, and controlled-output options as compile.",
+ "code": "pliego-cssc watch --source src --theme \\\n --output dist/app.css --manifest dist/app.manifest.json"
+ },
+ {
+ "id": "snapshot",
+ "title": "Confirm one coherent snapshot",
+ "body": "Inputs are observed twice before compilation so an editor save sequence does not publish a mixture of old and new files.",
+ "code": null
+ },
+ {
+ "id": "last-valid",
+ "title": "Never replace validity with a partial edit",
+ "body": "A parser error, invalid token document, or failed group publication leaves the previous complete artifact set in place.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/tooling/repair.md",
+ "sourceSha256": "sha256:e948e434445108c4360c04904234d048339e2153d149ec3964e5def5f2d7d5cf",
+ "route": "/docs/tooling/repair/",
+ "title": "Plan first. Authorize exact bytes.",
+ "category": "Tooling",
+ "eyebrow": "Repair agent",
+ "summary": "Generate bounded repair proposals, bind authorization to source hashes, verify in staging, then publish receipts.",
+ "order": 25,
+ "sections": [
+ {
+ "id": "plan",
+ "title": "Join findings to a proposal",
+ "body": "Repair plans name exact finding codes, files, byte ranges, replacements, expected hashes, and verification commands.",
+ "code": "pliego-cssc plan --findings findings.json \\\n --proposal proposal.json --source-root ."
+ },
+ {
+ "id": "dry-run",
+ "title": "Verify before mutation",
+ "body": "Dry-run applies the authorized patch inside a staging copy and executes bounded Rust or browser checks without replacing the source tree.",
+ "code": "pliego-cssc fix --plan plan.json --findings findings.json \\\n --source-root . --dry-run"
+ },
+ {
+ "id": "resource",
+ "title": "Time and output are part of safety",
+ "body": "Test, browser, toolchain, and version subprocesses use explicit deadlines and output limits while the repair lock is held.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/integrations/plain-html.md",
+ "sourceSha256": "sha256:9d84a07aad9047d542211be34d2f33f0b6a63d5dbe3358b7b817a338b6c4baf4",
+ "route": "/docs/integrations/plain-html/",
+ "title": "Audit and transform without a framework.",
+ "category": "Integrations",
+ "eyebrow": "Plain HTML",
+ "summary": "Use PliegoCSS as a standards control layer around an ordinary stylesheet and static HTML build.",
+ "order": 26,
+ "sections": [
+ {
+ "id": "audit",
+ "title": "Start with emitted CSS",
+ "body": "No adapter is required. Audit the exact stylesheet the browser loads and archive canonical findings beside the build.",
+ "code": "pliego-cssc audit --input public/app.css --targets baseline-widely"
+ },
+ {
+ "id": "transform",
+ "title": "Transform explicitly",
+ "body": "Compatibility transformation and minification are separate from analysis so a read-only audit never rewrites authored output.",
+ "code": "pliego-cssc transform-css --input src/app.css \\\n --output public/app.css --targets modern --format minified"
+ },
+ {
+ "id": "typed",
+ "title": "Typed authoring is optional",
+ "body": "Rust applications can compile typed styles into the same static site, but plain HTML consumers only need the final CSS asset.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/integrations/build-tools.md",
+ "sourceSha256": "sha256:3b684781f55390e9e981032107b208eb12470e0c789ca8a97d35419d862d0d60",
+ "route": "/docs/integrations/build-tools/",
+ "title": "Integrate through files, not hidden hooks.",
+ "category": "Integrations",
+ "eyebrow": "Generic build tools",
+ "summary": "Vite, webpack, esbuild, CI systems, and custom pipelines consume explicit CSS, manifests, findings, maps, and asset plans.",
+ "order": 27,
+ "sections": [
+ {
+ "id": "producer",
+ "title": "Run PliegoCSS as a producer",
+ "body": "Invoke compile or transform before the application bundler and pass its ordinary CSS output through the normal asset graph.",
+ "code": "\"scripts\": {\n \"css\": \"pliego-cssc compile --source src --output dist/pliego.css\",\n \"build\": \"npm run css && vite build\"\n}"
+ },
+ {
+ "id": "consumer",
+ "title": "Consume stable sidecars",
+ "body": "Use the numbered JSON schemas for manifests, Token Graphs, findings, reachability, project indexes, and Asset Plans instead of scraping terminal text.",
+ "code": null
+ },
+ {
+ "id": "ownership",
+ "title": "Keep topology in its owner",
+ "body": "A framework adapter may export route and component reachability. PliegoCSS validates and projects that sidecar but does not crawl application internals.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/reference/configuration.md",
+ "sourceSha256": "sha256:005f4656fdba04c614e19deb8addbe7628b287f2a609f11c8e01e7b1b73493ab",
+ "route": "/docs/reference/configuration/",
+ "title": "Configuration is part of identity.",
+ "category": "Reference",
+ "eyebrow": "Configuration",
+ "summary": "Theme, target, format, manifests, reachability, pruning, and token selections are explicit, validated, and hash-bound.",
+ "order": 28,
+ "sections": [
+ {
+ "id": "discovery",
+ "title": "Conventional TOML discovery only",
+ "body": "Without --config, --tokens, or --seed, the CLI searches for pliego.theme.toml from the workspace and input package roots. Ambiguous candidates fail.",
+ "code": null
+ },
+ {
+ "id": "exclusive",
+ "title": "Selectors are mutually exclusive",
+ "body": "--config, --tokens, and --seed cannot be combined. JSON token documents are never discovered implicitly.",
+ "code": null
+ },
+ {
+ "id": "hash",
+ "title": "Bind the complete choice",
+ "body": "Targets, format, emission settings, manifest version, pruning, theme bytes, resolver selections, and reachability bytes participate in the control configuration hash.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/reference/diagnostics.md",
+ "sourceSha256": "sha256:25661884b0cc22c67c23e9e53ed8ac92faf156e460356a9a20d6583148342c95",
+ "route": "/docs/reference/diagnostics/",
+ "title": "Stable codes, precise ranges.",
+ "category": "Reference",
+ "eyebrow": "Diagnostics",
+ "summary": "Parser, source scanner, composition, policy, invocation, migration, and formatter failures share a canonical transport.",
+ "order": 29,
+ "sections": [
+ {
+ "id": "human",
+ "title": "Human diagnostics",
+ "body": "Terminal output favors the next corrective action while retaining the stable code, severity, file, line, column, and byte range.",
+ "code": null
+ },
+ {
+ "id": "json",
+ "title": "Canonical JSON",
+ "body": "JSON mode writes one schema-1 document to stderr on failure and leaves stdout empty. Consumers should branch on codes, not localized prose.",
+ "code": "pliego-cssc --diagnostic-format json check --style \"p-4 p-6\" --seed"
+ },
+ {
+ "id": "sarif",
+ "title": "SARIF for code scanning",
+ "body": "Audit findings can be serialized as SARIF while preserving canonical rule identity and source regions for hosted review systems.",
+ "code": null
+ }
+ ]
+ },
+ {
+ "sourcePath": "docs/site/release-readiness.md",
+ "sourceSha256": "sha256:14334fe94cee9a55aca0e9c937bf7d2d160726c2c8da14bedbaba27e60a6854a",
+ "route": "/docs/release-readiness/",
+ "title": "Release truth bound to one exact candidate.",
+ "category": "Evidence",
+ "eyebrow": "Release readiness",
+ "summary": "The current 1.0 promotion state is blocked; no historical report can override these source-bound gates.",
+ "order": 30,
+ "sections": [
+ {
+ "id": "candidate",
+ "title": "Candidate identity",
+ "body": "Target 0.1.0; candidate 0.1.0-rc.2; tag v0.1.0-rc.2; commit 064dcbce96a3a5cc97a940d07566c008bb5e2d3e; Git tree 010a64f5d0bbd142a1d015fda4ccf573cc6e4133; observed 2026-07-22T14:10:00Z; expires 2026-07-29T14:10:00Z.",
+ "code": null
+ },
+ {
+ "id": "dimensions",
+ "title": "Readiness dimensions",
+ "body": "Technical: blocked; Operational: blocked; Adoption: blocked; Authorized: blocked; Promotion: blocked.",
+ "code": null
+ },
+ {
+ "id": "blockers",
+ "title": "Current blockers",
+ "body": "g0-corrections-on-candidate (pending): G0 corrections are not part of the exact RC.2 candidate. They require a newly named candidate plus hosted replay before they can become release evidence. browser-release-matrix (blocked): Every required hosted browser lane is not configured for the exact candidate source; local or historical browser runs have no promotion authority. registry-replay-refresh (pending): RC.2 was previously replayed from crates.io, but schema 3 requires a fresh, unexpired, artifact-hashed replay for the exact promotion candidate. production-deployment-refresh (pending): The production site has historical deployment evidence, but a current exact-source edge and browser replay with hashed artifacts is missing. signed-binary-distribution (blocked): Signed native binaries, checksums, SBOMs, attestations, and the npm launcher required by the LTS distribution boundary do not exist yet. external-adoption-evidence (pending): The required external interviews, real incidents, and pilots have not been recorded as reviewed evidence. final-promotion-authorization (not-authorized): Mario has not explicitly authorized publishing final 0.1.0 for this exact source commit and Git tree.",
+ "code": null
+ }
+ ]
+ }
+ ]
+}
diff --git a/site/src/pages.rs b/site/src/pages.rs
index a20ad0b..e9d2fcf 100644
--- a/site/src/pages.rs
+++ b/site/src/pages.rs
@@ -3,6 +3,7 @@
use pliego_css::{Style, pc};
use pliego_dom::{Element as DomElement, IntoView, View, el as dom_el};
use pliego_ssg::{Head, Page};
+use serde::Deserialize;
use serde_json::json;
use std::cell::Cell;
use std::collections::BTreeMap;
@@ -167,853 +168,54 @@ const ACTION_STYLE: Style = pc!(
const PANEL_STYLE: Style = pc!("rounded-xl border border-line bg-surface p-6 shadow-md");
const LABEL_STYLE: Style = pc!("text-xs font-semibold text-muted");
-struct DocPage {
- route: &'static str,
- title: &'static str,
- category: &'static str,
- eyebrow: &'static str,
- summary: &'static str,
- sections: &'static [DocSection],
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct DocManifest {
+ schema_version: u64,
+ kind: String,
+ documents: Vec,
}
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct DocPage {
+ source_path: String,
+ source_sha256: String,
+ route: String,
+ title: String,
+ category: String,
+ eyebrow: String,
+ summary: String,
+ order: usize,
+ sections: Vec,
+}
+
+#[derive(Deserialize)]
struct DocSection {
- id: &'static str,
- title: &'static str,
- body: &'static str,
- code: Option<&'static str>,
-}
-
-const DOCS: &[DocPage] = &[
- DocPage {
- route: "/docs/getting-started/",
- title: "Start with the CSS you already have.",
- category: "Start",
- eyebrow: "Getting started",
- summary: "Audit first, compile when it adds value, and keep the browser contract ordinary.",
- sections: &[
- DocSection {
- id: "install",
- title: "Install the release candidate",
- body: "The complete 0.1.0-rc.2 compatibility unit is published on crates.io and replayed from the registry with Rust 1.85.",
- code: Some(
- "cargo install pliego-cssc --version '=0.1.0-rc.2' --locked",
- ),
- },
- DocSection {
- id: "audit",
- title: "Audit ordinary CSS",
- body: "Start without changing authoring. The analyzer emits human, canonical JSON, or SARIF findings and fails closed on unknown surfaces.",
- code: Some(
- "pliego-cssc audit --input app.css \\\n --targets baseline-widely --format human",
- ),
- },
- DocSection {
- id: "compile",
- title: "Add typed identities selectively",
- body: "Rust literals are validated at compile time and compile to static standards-compliant CSS. There is no styling runtime.",
- code: Some(
- "use pliego_css::{pc, Style};\n\nconst BUTTON: Style = pc!(\n \"inline-flex gap-2 rounded-lg bg-accent px-4 py-2\"\n);",
- ),
- },
- ],
- },
- DocPage {
- route: "/docs/typed-styles/",
- title: "Types at author time. CSS at run time.",
- category: "Core",
- eyebrow: "Typed styles",
- summary: "Stable 128-bit identities connect Rust source, emitted classes, manifests, and physical declarations.",
- sections: &[
- DocSection {
- id: "pc",
- title: "One literal, one stable identity",
- body: "pc! accepts one visible string literal. The macro parses semantics, rejects conflicting slots, and returns a sealed Style.",
- code: Some(
- "const CARD: Style = pc!(\n \"grid gap-4 rounded-xl border border-line p-6 shadow-md\"\n);",
- ),
- },
- DocSection {
- id: "pcx",
- title: "Visible state spaces",
- body: "pcx! compiles every visible branch combination and selects one identity at runtime. Independent clauses that can collide are rejected.",
- code: Some(
- "let style = pcx!(\n \"block\",\n if active { \"opacity-100\" } else { \"opacity-50\" }\n);",
- ),
- },
- DocSection {
- id: "lineage",
- title: "Follow the physical effect",
- body: "Manifest schema 5 joins semantic declarations to final rules, conditions, byte ranges, and synthesized effects such as box-shadow.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/audit-and-transform/",
- title: "Standards-first, not framework-first.",
- category: "Standard CSS",
- eyebrow: "Audit and transform",
- summary: "Analyze standard CSS, enforce policy, and transform output without claiming ownership of authored source.",
- sections: &[
- DocSection {
- id: "classifier",
- title: "Bounded classification",
- body: "The classifier recognizes deliberate standard-CSS surfaces and reports unsupported constructs instead of guessing. A versioned corpus controls the claim.",
- code: None,
- },
- DocSection {
- id: "transform",
- title: "Deterministic transformation",
- body: "Target browsers, minify, and check output drift. Add --check in CI to remain read-only.",
- code: Some(
- "pliego-cssc transform-css --input app.css \\\n --output dist/app.css --targets modern --format minified",
- ),
- },
- DocSection {
- id: "policy",
- title: "Compatibility, accessibility, and budgets",
- body: "Policy documents bind findings to explicit targets and limits. The same canonical findings feed terminals, JSON automation, and SARIF.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/evidence/",
- title: "A green build should explain itself.",
- category: "Evidence",
- eyebrow: "Evidence and provenance",
- summary: "Artifacts carry hashes, source lineage, graphs, findings, and receipts designed for replay.",
- sections: &[
- DocSection {
- id: "manifest",
- title: "Canonical manifests",
- body: "Style identity, class identity, theme identity, origins, conditions, physical declarations, and final CSS hashes live in a deterministic document.",
- code: Some(
- "pliego-cssc compile --source src --seed --theme \\\n --output dist/app.css --manifest dist/app.manifest.json",
- ),
- },
- DocSection {
- id: "receipts",
- title: "Receipt-last publication",
- body: "Controlled operations stage output, lock the destination, verify inputs, publish artifacts, and write the receipt last.",
- code: None,
- },
- DocSection {
- id: "benchmarks",
- title: "Measured, inherited, pending, uncertain",
- body: "Release readiness preserves the evidence class. Old hosted evidence never silently passes changed source.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/migration/",
- title: "Adopt without surrendering the exit.",
- category: "Migration",
- eyebrow: "Controlled migration",
- summary: "Inventory first, generate bounded plans, preserve originals, and detect drift immediately before publication.",
- sections: &[
- DocSection {
- id: "inventory",
- title: "Read-only discovery",
- body: "Tailwind, Sass, CSS Modules, and standard CSS can be inventoried before any replacement plan exists.",
- code: Some("pliego-cssc migration-project-plan ./migration-declaration.json"),
- },
- DocSection {
- id: "reversible",
- title: "Reversible replacements",
- body: "Plans bind source and destination hashes. Apply and rollback use adjacent locks, pre-publication revalidation, backups, and group compensation.",
- code: None,
- },
- DocSection {
- id: "limits",
- title: "No speculative codemods",
- body: "Unsupported or context-dependent constructs stay in the inventory. The migration layer does not infer framework semantics it cannot prove.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/integrations/pliegors/",
- title: "Native together. Independent by contract.",
- category: "Integrations",
- eyebrow: "PliegoRS integration",
- summary: "PliegoRS is the first native consumer, not the only target. It supplies topology; PliegoCSS supplies style identity and artifacts.",
- sections: &[
- DocSection {
- id: "boundary",
- title: "Explicit ownership",
- body: "PliegoRS owns routes, pages, assets, and browser lifecycle. PliegoCSS owns parsing, semantic conflicts, CSS, manifests, and policy findings.",
- code: None,
- },
- DocSection {
- id: "bundles",
- title: "Route and island bundles",
- body: "A reachability sidecar projects application-owned topology into selected CSS bundles without PliegoCSS pretending to understand the framework.",
- code: None,
- },
- DocSection {
- id: "proof",
- title: "Replayed from published crates",
- body: "The maintained fixture locks PliegoRS 0.0.2 registry packages and verifies the remote-reachable framework revision separately.",
- code: Some("PLIEGORS_ROOT=/path/to/pliegors pnpm integration:pliegors"),
- },
- ],
- },
- DocPage {
- route: "/docs/installation/",
- title: "Choose an adoption surface.",
- category: "Start",
- eyebrow: "Installation",
- summary: "Install the CLI for any project, add the Rust crates only when typed authoring belongs in the application.",
- sections: &[
- DocSection {
- id: "cli",
- title: "CLI-only adoption",
- body: "The command-line compiler can audit, transform, explain, bundle, and migrate standard CSS without adding a runtime dependency to the application.",
- code: Some(
- "cargo install pliego-cssc --version '=0.1.0-rc.2' --locked\npliego-cssc --version",
- ),
- },
- DocSection {
- id: "rust",
- title: "Typed Rust authoring",
- body: "Add pliego-css and its macro crate to applications that want compile-time style identities. The emitted result remains ordinary CSS.",
- code: Some(
- "[dependencies]\npliego-css = \"=0.1.0-rc.2\"",
- ),
- },
- DocSection {
- id: "pin",
- title: "Pin the exact release identity",
- body: "Before the public release, use the signed project tag plus Cargo.lock. Do not track a moving branch in a production build.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/first-audit/",
- title: "Learn before changing.",
- category: "Start",
- eyebrow: "First audit",
- summary: "Run a read-only CSS audit, inspect canonical findings, then decide whether transformation or policy is justified.",
- sections: &[
- DocSection {
- id: "human",
- title: "Read the first report",
- body: "Human output is optimized for local diagnosis. Every finding carries a stable code and source location rather than an opaque score.",
- code: Some(
- "pliego-cssc audit --input app.css \\\n --targets baseline-widely --format human",
- ),
- },
- DocSection {
- id: "automation",
- title: "Feed automation safely",
- body: "Canonical JSON and SARIF preserve the same finding model for CI, code scanning, and archival evidence.",
- code: Some(
- "pliego-cssc audit --input app.css --targets modern --format sarif > findings.sarif",
- ),
- },
- DocSection {
- id: "unknown",
- title: "Unknown means unknown",
- body: "The standard-CSS classifier is bounded. Unsupported selectors, properties, or values fail closed under compatibility policy rather than being treated as safe.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/editor-setup/",
- title: "Put the compiler in the feedback loop.",
- category: "Start",
- eyebrow: "Editor setup",
- summary: "Use the language server for diagnostics, completion, hover evidence, and catalog-aware authoring.",
- sections: &[
- DocSection {
- id: "vscode",
- title: "VS Code extension",
- body: "The maintained extension launches pliego-css-lsp, discovers the project catalog, and renders diagnostics in Rust source.",
- code: Some("pnpm --filter pliegocss-vscode package"),
- },
- DocSection {
- id: "generic",
- title: "Any LSP client",
- body: "Launch the server over stdio and point it at the same compiler and workspace used by CI. The protocol stays editor-neutral.",
- code: Some("pliego-css-lsp --stdio"),
- },
- DocSection {
- id: "safety",
- title: "Bound external tools",
- body: "Compiler subprocesses have explicit deadlines and output caps. A stalled custom executable must not freeze the editor forever.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/syntax/",
- title: "A finite language with visible edges.",
- category: "Core",
- eyebrow: "Utility syntax",
- summary: "Utilities lower into semantic slots, conditions, and typed values before any CSS serialization occurs.",
- sections: &[
- DocSection {
- id: "shape",
- title: "Utility plus typed value",
- body: "Spacing, color, typography, layout, effects, and interactivity draw from one validated theme registry. Arbitrary escape hatches remain policy-visible.",
- code: Some("flex items-center gap-4 rounded-lg bg-surface px-4 py-2 text-sm"),
- },
- DocSection {
- id: "conditions",
- title: "Conditions are structure",
- body: "Responsive, state, attribute, container, layer, and media variants become normalized condition paths. Their order is not incidental string decoration.",
- code: Some("md:grid-cols-2 hover:bg-accent-strong aria-[expanded=true]:ring-2"),
- },
- DocSection {
- id: "important",
- title: "Importance follows the physical effect",
- body: "A trailing exclamation mark marks the semantic contribution important. Synthesized effects such as the composed box-shadow preserve that authority in final CSS and lineage.",
- code: Some("ring-2! shadow-md!"),
- },
- ],
- },
- DocPage {
- route: "/docs/composition/",
- title: "Compose by meaning, not source order.",
- category: "Core",
- eyebrow: "Composition",
- summary: "Compatible slots commute. Conflicting slots require an explicit branch-over-base operation.",
- sections: &[
- DocSection {
- id: "commute",
- title: "Equivalent input, equivalent identity",
- body: "Utilities that occupy independent semantic slots canonicalize to the same identity even when authored in a different order.",
- code: Some("flex gap-4 p-6\np-6 flex gap-4"),
- },
- DocSection {
- id: "conflict",
- title: "Ambiguous lists are rejected",
- body: "p-4 p-6 does not mean last one wins. Both declarations claim the same slot under the same condition, so compilation stops.",
- code: Some("pliego-cssc check --style \"p-4 p-6\" --seed"),
- },
- DocSection {
- id: "override",
- title: "Overrides are explicit operations",
- body: "When an application intentionally replaces one semantic assignment with another, use composition so the override relationship remains visible and inspectable.",
- code: Some("pliego-cssc compile --compose \"p-4\" \"p-6\" --seed"),
- },
- ],
- },
- DocPage {
- route: "/docs/variants/",
- title: "Conditions without string-order folklore.",
- category: "Core",
- eyebrow: "Variants and conditions",
- summary: "Every variant joins a typed condition path with deterministic normalization and specificity behavior.",
- sections: &[
- DocSection {
- id: "responsive",
- title: "Responsive paths",
- body: "Theme breakpoints are validated once and emitted as ordered media conditions. Assignments at different breakpoints do not conflict.",
- code: Some("grid-cols-1 md:grid-cols-2 lg:grid-cols-3"),
- },
- DocSection {
- id: "state",
- title: "State and attributes",
- body: "Pseudo-class and bounded attribute variants stay in semantic IR so policy and physical lineage can explain their selectors.",
- code: Some("hover:bg-accent focus-visible:ring-2 data-[state=open]:opacity-100"),
- },
- DocSection {
- id: "layers",
- title: "Cascade layers",
- body: "Fixed layer variants model author-controlled precedence without allowing arbitrary source-order dependence to leak into identity.",
- code: Some("layer-components:bg-surface layer-utilities:p-4"),
- },
- ],
- },
- DocPage {
- route: "/docs/themes/",
- title: "Resolve tokens before styles.",
- category: "Core",
- eyebrow: "Themes and tokens",
- summary: "TOML themes and DTCG Resolver documents feed the same typed registry, identity, graph, and emitted variables.",
- sections: &[
- DocSection {
- id: "toml",
- title: "Extend the seed",
- body: "A project theme can override or add typed colors, spacing, radii, typography, and breakpoints without changing utility grammar.",
- code: Some(
- "schema = 1\nextends = \"seed\"\n\n[tokens.color]\nbrand = \"oklch(62% 0.18 250)\"",
- ),
- },
- DocSection {
- id: "dtcg",
- title: "Select DTCG contexts explicitly",
- body: "Resolver inputs are never auto-discovered. Modifier selections participate in configuration identity even when two contexts resolve to equal values.",
- code: Some(
- "pliego-cssc compile --tokens product.resolver.json \\\n --token-input appearance=dark --style \"bg-brand\"",
- ),
- },
- DocSection {
- id: "graph",
- title: "Publish the complete Token Graph",
- body: "Controlled output binds selected CSS to canonical token provenance, aliases, permutations, and reachability-aware usage evidence.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/standard-css/policies/",
- title: "Make browser support and budgets executable.",
- category: "Standard CSS",
- eyebrow: "Policies",
- summary: "Compatibility, accessibility, payload, package, route, and ownership policies turn expectations into reviewable inputs.",
- sections: &[
- DocSection {
- id: "compatibility",
- title: "Version the target",
- body: "baseline-widely and modern are frozen policy profiles with explicit browser and source-data identities. They do not drift silently with a dependency update.",
- code: Some("pliego-cssc compatibility --targets baseline-widely"),
- },
- DocSection {
- id: "budgets",
- title: "Bind budgets to owners",
- body: "Direct files can name budget subjects manually. Asset Plans use an ownership sidecar to aggregate package and composed-route costs without guessing responsibility.",
- code: Some(
- "pliego-cssc audit --asset-plan dist/pliego.assets.json \\\n --ownership pliego.ownership.json --budget-policy budgets.json",
- ),
- },
- DocSection {
- id: "accessibility",
- title: "Audit static accessibility contracts",
- body: "Contrast and interaction guards use explicit policy plus token evidence. Dynamic visual behavior still requires browser and assistive-technology testing.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/cascade/",
- title: "Explain the winning declaration.",
- category: "Standard CSS",
- eyebrow: "Cascade inspection",
- summary: "Inspect authored CSS by element and longhand to see matching selectors, precedence, and the final winner.",
- sections: &[
- DocSection {
- id: "query",
- title: "Ask one precise question",
- body: "Provide a CSS file, a bounded element description, and one longhand. The command reports candidates instead of simulating an entire browser.",
- code: Some(
- "pliego-cssc explain-cascade --input app.css \\\n --element 'button#save.action' --property background-color",
- ),
- },
- DocSection {
- id: "winner",
- title: "See why it won",
- body: "Origin, importance, layer, specificity, and source position are surfaced in comparison order with a stable machine-readable form.",
- code: None,
- },
- DocSection {
- id: "boundary",
- title: "Keep the model honest",
- body: "This is a static bounded inspector. Runtime DOM state, animation interpolation, adopted sheets, and script mutation remain browser responsibilities.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/manifests/",
- title: "Trace source intent into emitted bytes.",
- category: "Evidence",
- eyebrow: "Style manifests",
- summary: "Manifest schema 5 connects source origins, semantic assignments, final selectors, physical declarations, and CSS ranges.",
- sections: &[
- DocSection {
- id: "identity",
- title: "Identity chain",
- body: "StyleId identifies canonical semantics. Class identity, theme identity, configuration, and final CSS hash bind that style into one build.",
- code: Some(
- "pliego-cssc compile --source src --manifest dist/app.manifest.json \\\n --manifest-version 5 --output dist/app.css",
- ),
- },
- DocSection {
- id: "physical",
- title: "Physical lineage",
- body: "Generated custom properties and synthesized declarations retain contributors, importance, conditions, selector, property, value, and output byte range.",
- code: None,
- },
- DocSection {
- id: "versions",
- title: "Request the schema you consume",
- body: "Schema 3 supports style manifests, schema 4 adds reachability ownership, and schema 5 adds closed physical tracing. Consumers must pin their contract.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/control-artifacts/",
- title: "Publish the receipt last.",
- category: "Evidence",
- eyebrow: "Control artifacts",
- summary: "Controlled builds stage related outputs, lock the destination, verify hashes, compensate failures, and expose one completed receipt.",
- sections: &[
- DocSection {
- id: "group",
- title: "One rollback-capable group",
- body: "CSS, source map, style manifest, Token Graph, findings, control manifest, and receipt are generated from one exact input snapshot.",
- code: Some(
- "pliego-cssc compile --source src --output dist/app.css \\\n --manifest dist/app.manifest.json --control-dir dist",
- ),
- },
- DocSection {
- id: "check",
- title: "Check without writing",
- body: "The same command with --check recomputes every artifact and fails on byte drift without taking publication locks or replacing files.",
- code: Some(
- "pliego-cssc compile --source src --output dist/app.css \\\n --manifest dist/app.manifest.json --control-dir dist --check",
- ),
- },
- DocSection {
- id: "receipt",
- title: "Completion is explicit",
- body: "Consumers treat the fixed receipt as the publication boundary. Its absence means the group is incomplete, even if individual files are present.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/critical-css/",
- title: "Prove what rendered above the fold.",
- category: "Evidence",
- eyebrow: "Critical style evidence",
- summary: "Join observed browser behavior to project topology and retained style identity without letting an observation silently authorize removal.",
- sections: &[
- DocSection {
- id: "observe",
- title: "Observe a real route",
- body: "Capture viewport, route, interaction state, loaded assets, matched styles, and source identities from a browser run against the built application.",
- code: None,
- },
- DocSection {
- id: "join",
- title: "Join exact universes",
- body: "Evidence is accepted only when its project index, asset plan, reachability, and CSS identities match the build under review.",
- code: None,
- },
- DocSection {
- id: "authority",
- title: "Observation does not equal deletion",
- body: "A style unseen in one scope remains unknown outside that scope. Explicit retention and reachability policies control removal authority.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/migration/tailwind/",
- title: "Inventory Tailwind before translating it.",
- category: "Migration",
- eyebrow: "Tailwind migration",
- summary: "Recover the real utility surface, classify supported roles, and keep unsupported framework behavior visible.",
- sections: &[
- DocSection {
- id: "inventory",
- title: "Scan without replacement",
- body: "The inventory recognizes bounded class-bearing surfaces and reports candidates with source ranges. It does not rewrite template semantics during discovery.",
- code: Some("pliego-cssc migration-inventory tailwind src/App.tsx"),
- },
- DocSection {
- id: "plan",
- title: "Generate a hash-bound project plan",
- body: "A declaration selects roots and destinations. The plan records exact source and output hashes before any replacement can be applied.",
- code: Some("pliego-cssc migration-project-plan migration.json"),
- },
- DocSection {
- id: "gaps",
- title: "Preserve unsupported behavior",
- body: "Plugins, generated variants, arbitrary selectors, and runtime class construction remain explicit migration findings unless a project-owned adapter proves them.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/migration/sass/",
- title: "Separate syntax conversion from semantic adoption.",
- category: "Migration",
- eyebrow: "Sass migration",
- summary: "Inventory variables, nesting, mixins, imports, and generated surfaces without pretending every Sass abstraction is a utility.",
- sections: &[
- DocSection {
- id: "inventory",
- title: "Map the source surface",
- body: "The inventory records statically visible Sass roles and unresolved dynamic behavior so the team can choose what stays authored CSS.",
- code: Some("pliego-cssc migration-inventory sass styles/main.scss"),
- },
- DocSection {
- id: "css-first",
- title: "Ordinary CSS is a valid destination",
- body: "Use the standard-CSS transformer for syntax and compatibility work. Adopt typed PliegoCSS styles only where identity and provenance pay for themselves.",
- code: None,
- },
- DocSection {
- id: "rollback",
- title: "Keep originals and rollback evidence",
- body: "Applied replacements preserve source backups and hash-bound rollback records. A changed destination aborts rather than overwriting concurrent edits.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/migration/css-modules/",
- title: "Preserve component ownership.",
- category: "Migration",
- eyebrow: "CSS Modules migration",
- summary: "Discover module exports and usage while keeping application-owned component topology separate from CSS semantics.",
- sections: &[
- DocSection {
- id: "inventory",
- title: "Inventory exported classes",
- body: "The scanner identifies bounded module class definitions and their source locations without assuming a particular bundler naming strategy.",
- code: Some("pliego-cssc migration-inventory css-modules src/Card.module.css"),
- },
- DocSection {
- id: "ownership",
- title: "Let the application own reachability",
- body: "Route and component reachability comes from the framework or product index, not from filename heuristics inside the CSS compiler.",
- code: None,
- },
- DocSection {
- id: "mixed",
- title: "Mixed adoption is first-class",
- body: "A project can retain modules for component styles, audit their emitted CSS, and introduce typed identities only for new or shared surfaces.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/tooling/cli/",
- title: "One CLI, explicit modes.",
- category: "Tooling",
- eyebrow: "Command-line reference",
- summary: "Compile, inspect, audit, transform, explain, bundle, watch, migrate, format, plan, and verify with strict argument contracts.",
- sections: &[
- DocSection {
- id: "discover",
- title: "Discover the real command surface",
- body: "Root and command help are generated from the maintained command synopsis. Unknown options, duplicate single-value options, and missing values fail.",
- code: Some("pliego-cssc --help\npliego-cssc compile --help"),
- },
- DocSection {
- id: "diagnostics",
- title: "Select diagnostic transport globally",
- body: "Human and JSON diagnostics can be requested before or after the command without stealing option values that happen to resemble flags.",
- code: Some("pliego-cssc --diagnostic-format json check --style \"p-4 p-6\" --seed"),
- },
- DocSection {
- id: "determinism",
- title: "Use --check in CI",
- body: "Commands that publish controlled or generated artifacts expose a read-only comparison path. CI should verify the checked-in or staged output, not regenerate it silently.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/tooling/lsp/",
- title: "Compiler feedback without editor lockups.",
- category: "Tooling",
- eyebrow: "Language server",
- summary: "The LSP shares parser and catalog contracts with the CLI while bounding subprocess time and output.",
- sections: &[
- DocSection {
- id: "features",
- title: "Diagnostics, completion, and hover",
- body: "Visible pc! and pcx! literals receive parser diagnostics, catalog-backed completion, and inspectable utility information.",
- code: None,
- },
- DocSection {
- id: "cancellation",
- title: "Cancel stale document work",
- body: "Document version changes cancel obsolete checks. Independent process deadlines prevent an unchanged document from waiting forever on a stalled compiler.",
- code: None,
- },
- DocSection {
- id: "limits",
- title: "Bound untrusted output",
- body: "stdout and stderr are read under explicit caps. Oversized or timed-out compiler responses become diagnostics instead of unbounded memory growth.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/tooling/watch/",
- title: "Keep the last complete truth.",
- category: "Tooling",
- eyebrow: "Watch mode",
- summary: "Poll coherent snapshots, republish only valid groups, and retain the last valid output across transient edits.",
- sections: &[
- DocSection {
- id: "run",
- title: "Watch inputs and output",
- body: "The watcher accepts the same source, theme, target, format, manifest, reachability, and controlled-output options as compile.",
- code: Some(
- "pliego-cssc watch --source src --theme \\\n --output dist/app.css --manifest dist/app.manifest.json",
- ),
- },
- DocSection {
- id: "snapshot",
- title: "Confirm one coherent snapshot",
- body: "Inputs are observed twice before compilation so an editor save sequence does not publish a mixture of old and new files.",
- code: None,
- },
- DocSection {
- id: "last-valid",
- title: "Never replace validity with a partial edit",
- body: "A parser error, invalid token document, or failed group publication leaves the previous complete artifact set in place.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/tooling/repair/",
- title: "Plan first. Authorize exact bytes.",
- category: "Tooling",
- eyebrow: "Repair agent",
- summary: "Generate bounded repair proposals, bind authorization to source hashes, verify in staging, then publish receipts.",
- sections: &[
- DocSection {
- id: "plan",
- title: "Join findings to a proposal",
- body: "Repair plans name exact finding codes, files, byte ranges, replacements, expected hashes, and verification commands.",
- code: Some(
- "pliego-cssc plan --findings findings.json \\\n --proposal proposal.json --source-root .",
- ),
- },
- DocSection {
- id: "dry-run",
- title: "Verify before mutation",
- body: "Dry-run applies the authorized patch inside a staging copy and executes bounded Rust or browser checks without replacing the source tree.",
- code: Some(
- "pliego-cssc fix --plan plan.json --findings findings.json \\\n --source-root . --dry-run",
- ),
- },
- DocSection {
- id: "resource",
- title: "Time and output are part of safety",
- body: "Test, browser, toolchain, and version subprocesses use explicit deadlines and output limits while the repair lock is held.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/integrations/plain-html/",
- title: "Audit and transform without a framework.",
- category: "Integrations",
- eyebrow: "Plain HTML",
- summary: "Use PliegoCSS as a standards control layer around an ordinary stylesheet and static HTML build.",
- sections: &[
- DocSection {
- id: "audit",
- title: "Start with emitted CSS",
- body: "No adapter is required. Audit the exact stylesheet the browser loads and archive canonical findings beside the build.",
- code: Some("pliego-cssc audit --input public/app.css --targets baseline-widely"),
- },
- DocSection {
- id: "transform",
- title: "Transform explicitly",
- body: "Compatibility transformation and minification are separate from analysis so a read-only audit never rewrites authored output.",
- code: Some(
- "pliego-cssc transform-css --input src/app.css \\\n --output public/app.css --targets modern --format minified",
- ),
- },
- DocSection {
- id: "typed",
- title: "Typed authoring is optional",
- body: "Rust applications can compile typed styles into the same static site, but plain HTML consumers only need the final CSS asset.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/integrations/build-tools/",
- title: "Integrate through files, not hidden hooks.",
- category: "Integrations",
- eyebrow: "Generic build tools",
- summary: "Vite, webpack, esbuild, CI systems, and custom pipelines consume explicit CSS, manifests, findings, maps, and asset plans.",
- sections: &[
- DocSection {
- id: "producer",
- title: "Run PliegoCSS as a producer",
- body: "Invoke compile or transform before the application bundler and pass its ordinary CSS output through the normal asset graph.",
- code: Some(
- "\"scripts\": {\n \"css\": \"pliego-cssc compile --source src --output dist/pliego.css\",\n \"build\": \"npm run css && vite build\"\n}",
- ),
- },
- DocSection {
- id: "consumer",
- title: "Consume stable sidecars",
- body: "Use the numbered JSON schemas for manifests, Token Graphs, findings, reachability, project indexes, and Asset Plans instead of scraping terminal text.",
- code: None,
- },
- DocSection {
- id: "ownership",
- title: "Keep topology in its owner",
- body: "A framework adapter may export route and component reachability. PliegoCSS validates and projects that sidecar but does not crawl application internals.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/reference/configuration/",
- title: "Configuration is part of identity.",
- category: "Reference",
- eyebrow: "Configuration",
- summary: "Theme, target, format, manifests, reachability, pruning, and token selections are explicit, validated, and hash-bound.",
- sections: &[
- DocSection {
- id: "discovery",
- title: "Conventional TOML discovery only",
- body: "Without --config, --tokens, or --seed, the CLI searches for pliego.theme.toml from the workspace and input package roots. Ambiguous candidates fail.",
- code: None,
- },
- DocSection {
- id: "exclusive",
- title: "Selectors are mutually exclusive",
- body: "--config, --tokens, and --seed cannot be combined. JSON token documents are never discovered implicitly.",
- code: None,
- },
- DocSection {
- id: "hash",
- title: "Bind the complete choice",
- body: "Targets, format, emission settings, manifest version, pruning, theme bytes, resolver selections, and reachability bytes participate in the control configuration hash.",
- code: None,
- },
- ],
- },
- DocPage {
- route: "/docs/reference/diagnostics/",
- title: "Stable codes, precise ranges.",
- category: "Reference",
- eyebrow: "Diagnostics",
- summary: "Parser, source scanner, composition, policy, invocation, migration, and formatter failures share a canonical transport.",
- sections: &[
- DocSection {
- id: "human",
- title: "Human diagnostics",
- body: "Terminal output favors the next corrective action while retaining the stable code, severity, file, line, column, and byte range.",
- code: None,
- },
- DocSection {
- id: "json",
- title: "Canonical JSON",
- body: "JSON mode writes one schema-1 document to stderr on failure and leaves stdout empty. Consumers should branch on codes, not localized prose.",
- code: Some("pliego-cssc --diagnostic-format json check --style \"p-4 p-6\" --seed"),
- },
- DocSection {
- id: "sarif",
- title: "SARIF for code scanning",
- body: "Audit findings can be serialized as SARIF while preserving canonical rule identity and source regions for hosted review systems.",
- code: None,
- },
- ],
- },
-];
+ id: String,
+ title: String,
+ body: String,
+ code: Option,
+}
+
+fn docs() -> &'static [DocPage] {
+ static DOCS: OnceLock> = OnceLock::new();
+ DOCS.get_or_init(|| {
+ let manifest: DocManifest = serde_json::from_str(include_str!("docs.generated.json"))
+ .expect("generated Markdown document manifest must parse");
+ assert_eq!(
+ manifest.schema_version, 1,
+ "unsupported site document schema"
+ );
+ assert_eq!(manifest.kind, "pliegocss-site-markdown");
+ for (index, document) in manifest.documents.iter().enumerate() {
+ assert_eq!(document.order, index + 1, "site document order drifted");
+ assert!(document.source_path.starts_with("docs/site/"));
+ assert!(document.source_sha256.starts_with("sha256:"));
+ }
+ manifest.documents
+ })
+}
pub fn all() -> Vec {
let mut pages = Vec::new();
@@ -1076,11 +278,14 @@ pub fn all() -> Vec {
legal_document(slug),
));
}
- localized.extend(
- DOCS.iter().map(|document| {
- page(locale, document.route, document.title, doc_page(document))
- }),
- );
+ localized.extend(docs().iter().map(|document| {
+ page(
+ locale,
+ document.route.as_str(),
+ document.title.as_str(),
+ doc_page(document),
+ )
+ }));
localized
}));
}
@@ -2515,12 +1720,12 @@ fn docs_index() -> View {
.child(el("span").child(*category))
.child(el("span").child(format!(
"{:02} entries",
- DOCS.iter()
+ docs().iter()
.filter(|document| document.category == *category)
.count()
))),
);
- for document in DOCS
+ for document in docs()
.iter()
.filter(|document| document.category == *category)
{
@@ -2562,7 +1767,7 @@ fn doc_categories() -> &'static [&'static str] {
fn doc_directory_item(document: &DocPage) -> Element {
el("a")
.class("doc-directory-item")
- .attr("href", document.route)
+ .attr("href", document.route.as_str())
.attr("data-doc-search-item", "")
.attr(
"data-search-text",
@@ -2574,18 +1779,19 @@ fn doc_directory_item(document: &DocPage) -> Element {
.child(
el("span")
.class("doc-index")
- .child(format!("{:02}", list_child_index(document.route))),
+ .child(format!("{:02}", list_child_index(document.route.as_str()))),
)
.child(
el("span")
- .child(el("strong").child(document.eyebrow))
- .child(el("span").child(document.summary)),
+ .child(el("strong").child(document.eyebrow.as_str()))
+ .child(el("span").child(document.summary.as_str())),
)
.child(el("span").attr("aria-hidden", "true").child("↗"))
}
fn list_child_index(route: &str) -> usize {
- DOCS.iter()
+ docs()
+ .iter()
.position(|document| document.route == route)
.map_or(1, |index| index + 1)
}
@@ -2596,18 +1802,18 @@ fn doc_page(document: &DocPage) -> View {
.attr("aria-label", "On this page")
.child(el("strong").child("ON THIS PAGE"));
let mut content = el("article").class("doc-article");
- for section in document.sections {
+ for section in &document.sections {
toc = toc.child(
el("a")
.attr("href", format!("#{}", section.id))
- .child(section.title),
+ .child(section.title.as_str()),
);
let mut block = el("section")
- .id(section.id)
+ .id(section.id.as_str())
.class("doc-section")
- .child(el("h2").child(section.title))
- .child(el("p").child(section.body));
- if let Some(code) = section.code {
+ .child(el("h2").child(section.title.as_str()))
+ .child(el("p").child(section.body.as_str()));
+ if let Some(code) = §ion.code {
block = block.child(
el("div")
.class("code-sample")
@@ -2619,22 +1825,30 @@ fn doc_page(document: &DocPage) -> View {
.attr("aria-label", "Copy code")
.child("COPY"),
)
- .child(el("pre").child(el("code").child(code))),
+ .child(el("pre").child(el("code").child(code.as_str()))),
);
}
content = content.child(block);
}
+ content = content.child(
+ el("p")
+ .class("doc-source")
+ .child("SOURCE ")
+ .child(el("code").child(document.source_path.as_str()))
+ .child(" · ")
+ .child(el("code").child(document.source_sha256.as_str())),
+ );
el("div")
.class("doc-page page-shell")
.child(page_intro(
- document.eyebrow,
- document.title,
- document.summary,
+ document.eyebrow.as_str(),
+ document.title.as_str(),
+ document.summary.as_str(),
))
.child(
el("div")
.class("doc-layout")
- .child(docs_sidebar(document.route))
+ .child(docs_sidebar(document.route.as_str()))
.child(content)
.child(toc),
)
@@ -2649,15 +1863,15 @@ fn docs_sidebar(active_route: &str) -> Element {
let mut group = el("div")
.class("docs-sidebar-group")
.child(el("strong").child(*category));
- for document in DOCS
+ for document in docs()
.iter()
.filter(|document| document.category == *category)
{
- let mut link = el("a").attr("href", document.route);
+ let mut link = el("a").attr("href", document.route.as_str());
if document.route == active_route {
link = link.class("is-active").attr("aria-current", "page");
}
- group = group.child(link.child(document.eyebrow));
+ group = group.child(link.child(document.eyebrow.as_str()));
}
if *category == "Reference" {
group = group.child(
@@ -2693,16 +1907,18 @@ fn benchmarks() -> View {
el("section")
.class("benchmark-notice")
.child(kicker("CURRENT RELEASE LIMIT"))
- .child(el("h2").child("Historical timing snapshots are being superseded."))
- .child(el("p").child("The prior snapshot commit is not recoverable from repository history. Current timings remain blocked until regenerated from the final clean, reachable source commit. Payload and correctness gates continue to run independently.")),
+ .child(el("h2").child("Benchmark Authority v2 now owns the competitor oracle."))
+ .child(el("p").child("Tailwind latest, upstream v3-LTS, and frozen-release lanes are pinned for seven days and measured in alternating pairs with peak memory, gzip, Brotli, and micro/medium/large corpora. Browser/output certification v1 adds a separate 3×3 computed and bounded visual comparison; its hosted 9/9 artifact is still pending. Legacy Gate A/B numbers remain historical until reviewed clean-tree schema-2 evidence is recorded.")),
)
.child(
el("div")
.class("benchmark-links")
.child(repo_link("Methodology", "docs/benchmarks/methodology.md"))
+ .child(repo_link("Benchmark Authority v2", "docs/benchmarks/tailwind-benchmark-authority-v2.md"))
.child(repo_link("Tailwind competitive matrix", "docs/benchmarks/tailwind-v4-competitive-matrix.md"))
.child(repo_link("Reachability pruning", "docs/benchmarks/reachability-pruning.md"))
- .child(repo_link("Browser validation", "docs/benchmarks/browser-validation.md")),
+ .child(repo_link("Browser validation", "docs/benchmarks/browser-validation.md"))
+ .child(repo_link("Browser/output certification v1", "docs/benchmarks/browser-output-certification-v1.md")),
)
.into_view()
}
@@ -3317,7 +2533,7 @@ pub fn sitemap() -> Vec {
"/legal/acceptable-use/",
"/accessibility/",
];
- routes.extend(DOCS.iter().map(|document| document.route));
+ routes.extend(docs().iter().map(|document| document.route.as_str()));
routes.sort_unstable();
routes.dedup();
diff --git a/site/src/site.css b/site/src/site.css
index 09eee81..b90de46 100644
--- a/site/src/site.css
+++ b/site/src/site.css
@@ -2897,6 +2897,16 @@ pre {
font-size: 9px;
}
+.doc-source {
+ padding: 20px 0;
+ border-top: 1px solid var(--line-light);
+ color: var(--carbon-500);
+ font-family: var(--font-mono);
+ font-size: 10px;
+ line-height: 1.7;
+ overflow-wrap: anywhere;
+}
+
/* Supporting pages */
.benchmark-hero {
diff --git a/standards/provenance.json b/standards/provenance.json
index 6a76d50..3a0f873 100644
--- a/standards/provenance.json
+++ b/standards/provenance.json
@@ -123,17 +123,104 @@
],
"integrity": []
},
+ {
+ "id": "pixelmatch",
+ "name": "pixelmatch",
+ "kind": "software",
+ "version": "7.2.0",
+ "status": "versioned-package",
+ "license": "ISC",
+ "url": "https://github.com/mapbox/pixelmatch",
+ "relationship": "browser-certification",
+ "usage": "test-only",
+ "evidence": [
+ "benchmarks/browser-output-certification-v1/authority.json",
+ "package.json",
+ "pnpm-lock.yaml",
+ "scripts/browser-output-certification.mjs"
+ ],
+ "integrity": [
+ {
+ "subject": "pixelmatch@7.2.0",
+ "value": "sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg=="
+ },
+ {
+ "subject": "pnpm-lock.yaml",
+ "value": "sha256:39f1ccd3f3b3262805d773e92bf8b2d0efc9ac755ba77460a0cb801d5ccea394"
+ }
+ ]
+ },
+ {
+ "id": "playwright",
+ "name": "Playwright",
+ "kind": "software",
+ "version": "1.61.1",
+ "status": "versioned-package",
+ "license": "Apache-2.0",
+ "url": "https://github.com/microsoft/playwright",
+ "relationship": "browser-certification",
+ "usage": "test-only",
+ "evidence": [
+ ".github/workflows/browser-output-certification.yml",
+ "benchmarks/browser-output-certification-v1/authority.json",
+ "package.json",
+ "pnpm-lock.yaml",
+ "scripts/browser-output-certification.mjs"
+ ],
+ "integrity": [
+ {
+ "subject": "playwright-core@1.61.1",
+ "value": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg=="
+ },
+ {
+ "subject": "playwright@1.61.1",
+ "value": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="
+ },
+ {
+ "subject": "pnpm-lock.yaml",
+ "value": "sha256:39f1ccd3f3b3262805d773e92bf8b2d0efc9ac755ba77460a0cb801d5ccea394"
+ }
+ ]
+ },
+ {
+ "id": "pngjs",
+ "name": "pngjs",
+ "kind": "software",
+ "version": "7.0.0",
+ "status": "versioned-package",
+ "license": "MIT",
+ "url": "https://github.com/pngjs/pngjs",
+ "relationship": "browser-certification",
+ "usage": "test-only",
+ "evidence": [
+ "benchmarks/browser-output-certification-v1/authority.json",
+ "package.json",
+ "pnpm-lock.yaml",
+ "scripts/browser-output-certification.mjs"
+ ],
+ "integrity": [
+ {
+ "subject": "pngjs@7.0.0",
+ "value": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="
+ },
+ {
+ "subject": "pnpm-lock.yaml",
+ "value": "sha256:39f1ccd3f3b3262805d773e92bf8b2d0efc9ac755ba77460a0cb801d5ccea394"
+ }
+ ]
+ },
{
"id": "tailwindcss",
"name": "Tailwind CSS and CLI",
"kind": "software",
- "version": "4.3.2",
+ "version": "4.3.3",
"status": "versioned-package",
"license": "MIT",
"url": "https://github.com/tailwindlabs/tailwindcss",
"relationship": "benchmark-baseline",
"usage": "benchmark-only",
"evidence": [
+ "benchmarks/benchmark-authority-v2/oracle.json",
"benchmarks/tailwind-v4/input.css",
"package.json",
"pnpm-lock.yaml"
@@ -143,13 +230,25 @@
"subject": "@tailwindcss/cli@4.3.2",
"value": "sha512-Fzt+HrIZHDlkRYKdLMBeufaroaPvwCBG70sMLdmurdeadNMO/LxbmT8Sbb+P83ep0iAlAImettb7Y+rO+37rXw=="
},
+ {
+ "subject": "@tailwindcss/cli@4.3.3",
+ "value": "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw=="
+ },
{
"subject": "pnpm-lock.yaml",
- "value": "sha256:333f16de16053c85708ea74473a3008f314e1c3ee45490106e1462df06de93f9"
+ "value": "sha256:39f1ccd3f3b3262805d773e92bf8b2d0efc9ac755ba77460a0cb801d5ccea394"
+ },
+ {
+ "subject": "tailwindcss@3.4.19",
+ "value": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="
},
{
"subject": "tailwindcss@4.3.2",
"value": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA=="
+ },
+ {
+ "subject": "tailwindcss@4.3.3",
+ "value": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="
}
]
},