Edition 006
Architecture as evidence
A bounded card with semantic heading and descriptive copy.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ba6681c..66b9f5c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -15,7 +15,7 @@ body: attributes: label: PliegoCSS version description: Exact crate version, CLI version, or commit SHA. - placeholder: 0.1.0-rc.2 + placeholder: 0.1.0-rc.3 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 8095032..3b9dba0 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,3 +6,6 @@ contact_links: - name: Security report url: https://github.com/celiumsai/pliegocss/security/advisories/new about: Report vulnerabilities privately. Do not open a public issue. + - name: G7 external adoption cohort + url: https://github.com/celiumsai/pliegocss/issues/7 + about: Read the cohort boundary and apply without posting private evidence. diff --git a/.github/ISSUE_TEMPLATE/g7_pilot.yml b/.github/ISSUE_TEMPLATE/g7_pilot.yml new file mode 100644 index 0000000..e846cc7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/g7_pilot.yml @@ -0,0 +1,73 @@ +name: G7 pilot interest +description: Signal interest in the external adoption cohort without posting private material. +title: "[G7 pilot]: " +labels: + - g7-pilot +body: + - type: markdown + attributes: + value: | + This public form is a recruitment signal only. It is not consent and does not count as + evidence. Do not post names, email addresses, credentials, private repository URLs, + proprietary source, transcripts, or incident material. + + Read the [cohort notice](https://github.com/celiumsai/pliegocss/issues/7) and + [privacy notice](https://pliegocss.dev/legal/privacy/). Private intake uses + hello@pliegocss.dev after eligibility review. + - type: dropdown + id: adapter + attributes: + label: Candidate adapter + options: + - Static HTML + - Vite 8.1.5 + - Pinned PliegoRS rendered output + - Outside the certified pilot matrix + validations: + required: true + - type: dropdown + id: tailwind + attributes: + label: Tailwind baseline + options: + - Tailwind CSS 3.4.19 + - Tailwind CSS 4.3.3 + - Another version + - Not currently using Tailwind + validations: + required: true + - type: dropdown + id: visibility + attributes: + label: Project visibility + options: + - Public project + - Private project — details will be shared only through private intake + validations: + required: true + - type: input + id: public_project + attributes: + label: Public project URL + description: Optional. Leave blank for a private project; never paste a private URL. + placeholder: https://github.com/example/project + - type: textarea + id: goal + attributes: + label: Pilot goal + description: Describe the adoption question without source code or identifying private details. + validations: + required: true + - type: checkboxes + id: boundary + attributes: + label: Eligibility and privacy boundary + options: + - label: I maintain or control the candidate project. + required: true + - label: The project is not owned by Celiums Solutions, PliegoCSS, or celiumsai. + required: true + - label: This issue contains no personal data, credentials, proprietary source, or private evidence. + required: true + - label: I understand this public issue is not research consent and does not count as G7 evidence. + required: true diff --git a/.github/workflows/adapter-coexistence.yml b/.github/workflows/adapter-coexistence.yml new file mode 100644 index 0000000..ef43146 --- /dev/null +++ b/.github/workflows/adapter-coexistence.yml @@ -0,0 +1,139 @@ +name: Adapter coexistence 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 + - host: linux-x64-chromium + runner: ubuntu-24.04 + - host: macos-arm64-chromium + runner: macos-15 + 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 adapter authority + run: pnpm check:adapter-coexistence-authority + - name: Install Rust toolchain + run: rustup toolchain install 1.96.0 --profile minimal + - name: Install Chromium and Linux dependencies + if: runner.os == 'Linux' + run: pnpm exec playwright install --with-deps chromium + - name: Install Chromium + if: runner.os != 'Linux' + run: pnpm exec playwright install chromium + - name: Execute adapter coexistence host + run: pnpm check:adapter-coexistence -- --browser=chromium --host=${{ matrix.host }} --output=target/adapter-coexistence-certification/evidence/${{ matrix.host }}.json --require-clean + - name: Upload host evidence + if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: adapter-coexistence-${{ matrix.host }}-${{ github.sha }} + path: target/adapter-coexistence-certification/evidence/${{ matrix.host }}.json + if-no-files-found: error + retention-days: 30 + + pliegors-framework: + name: PliegoRS framework/browser adapter + runs-on: windows-latest + timeout-minutes: 35 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + path: pliegocss + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + repository: celiumsai/pliegors + ref: abb8653e75da4a5cb4dd9b51200114fbb1e760c7 + path: pliegors + - 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 + working-directory: pliegocss + - name: Install PliegoRS Rust toolchain + run: | + rustup toolchain install 1.86.0 --profile minimal + rustup toolchain install 1.96.0 --profile minimal + rustup target add wasm32-unknown-unknown --toolchain 1.86.0 + - name: Install pinned wasm-bindgen CLI + run: | + cargo +1.96.0 install wasm-bindgen-cli --version 0.2.126 --locked --root "$env:RUNNER_TEMP\wasm-bindgen-cli" + "$env:RUNNER_TEMP\wasm-bindgen-cli\bin" | Out-File -FilePath $env:GITHUB_PATH -Append + - name: Execute pinned PliegoRS browser gate + run: pnpm integration:pliegors-browser + working-directory: pliegocss + env: + PLIEGORS_ROOT: ${{ github.workspace }}\pliegors + PLIEGOCSS_PLIEGORS_BROWSER_EVIDENCE: target/adapter-coexistence-certification/pliegors-framework.json + - name: Upload PliegoRS evidence + if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: adapter-coexistence-pliegors-framework-${{ github.sha }} + path: pliegocss/target/adapter-coexistence-certification/pliegors-framework.json + if-no-files-found: error + retention-days: 30 + + matrix: + name: Aggregate adapter coexistence matrix + needs: [host, pliegors-framework] + 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 adapter authority + run: pnpm check:adapter-coexistence-authority + - name: Download host evidence + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: adapter-coexistence-*-chromium-${{ github.sha }} + path: target/adapter-coexistence-certification/incoming + merge-multiple: true + - name: Download PliegoRS evidence + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: adapter-coexistence-pliegors-framework-${{ github.sha }} + path: target/adapter-coexistence-certification/pliegors + - name: Validate complete matrix + run: pnpm check:adapter-coexistence-matrix -- --evidence-root=target/adapter-coexistence-certification/incoming --pliegors-evidence=target/adapter-coexistence-certification/pliegors/pliegors-framework.json --output=target/adapter-coexistence-certification/adapter-coexistence-matrix.json --require-clean + - name: Upload complete matrix + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: adapter-coexistence-matrix-${{ github.sha }} + path: target/adapter-coexistence-certification/adapter-coexistence-matrix.json + if-no-files-found: error + retention-days: 30 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..8acbfc5 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,18 @@ 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: Check adapter coexistence authority + run: pnpm check:adapter-coexistence-authority + - name: Check external adoption authority + run: pnpm check:external-adoption-authority + - name: Check repository-only pnpm distribution contract + run: pnpm check:repository-distribution + - 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/.github/workflows/distribution.yml b/.github/workflows/distribution.yml new file mode 100644 index 0000000..024ebf0 --- /dev/null +++ b/.github/workflows/distribution.yml @@ -0,0 +1,254 @@ +name: Repository distribution + +on: + push: + pull_request: + workflow_dispatch: + inputs: + attest: + description: Generate GitHub/Sigstore attestations for this exact source + required: true + type: boolean + default: false + +concurrency: + group: repository-distribution-${{ github.ref }} + cancel-in-progress: ${{ github.ref_type != 'tag' }} + +permissions: + contents: read + +jobs: + native: + name: Native ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + permissions: + contents: read + id-token: write + attestations: write + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-pc-windows-msvc + runner: windows-latest + extension: .exe + - target: x86_64-unknown-linux-gnu + runner: ubuntu-24.04 + extension: "" + - target: aarch64-apple-darwin + runner: macos-15 + extension: "" + env: + CARGO_TARGET_DIR: target/distribution-build/${{ matrix.target }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22.13.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: 11.7.0 + run_install: false + - name: Install pinned Rust and SBOM tooling + run: | + rustup toolchain install 1.96.0 --profile minimal --target ${{ matrix.target }} + cargo +1.96.0 install cargo-cyclonedx --version '=0.5.9' --locked + - name: Build native CLI and LSP + run: cargo +1.96.0 build --release --locked --target ${{ matrix.target }} -p pliego-cssc -p pliego-css-lsp + - name: Generate target-specific CycloneDX SBOMs + run: cargo cyclonedx --format json --target ${{ matrix.target }} --all-features --spec-version 1.5 + - name: Stage exact-source native payload + id: stage + shell: bash + run: | + set -euo pipefail + tree="$(git show -s --format=%T HEAD)" + args=( + "--target=${{ matrix.target }}" + "--binary-dir=target/distribution-build/${{ matrix.target }}/${{ matrix.target }}/release" + "--cli-sbom=crates/pliego-cssc/pliego-cssc.cdx.json" + "--lsp-sbom=crates/pliego-css-lsp/pliego-css-lsp.cdx.json" + "--output=target/distribution-native/${{ matrix.target }}" + "--source-commit=${GITHUB_SHA}" + "--git-tree=${tree}" + ) + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + args+=("--tag=${GITHUB_REF_NAME}") + fi + node scripts/prepare-native-distribution.mjs "${args[@]}" + "target/distribution-native/${{ matrix.target }}/pliego-cssc${{ matrix.extension }}" --version + "target/distribution-native/${{ matrix.target }}/pliego-css-lsp${{ matrix.extension }}" --version + - name: Attest CLI binary and SBOM + if: ${{ github.ref_type == 'tag' || (github.event_name == 'workflow_dispatch' && inputs.attest) }} + uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + with: + subject-path: target/distribution-native/${{ matrix.target }}/pliego-cssc${{ matrix.extension }} + sbom-path: target/distribution-native/${{ matrix.target }}/pliego-cssc.cdx.json + - name: Attest LSP binary and SBOM + if: ${{ github.ref_type == 'tag' || (github.event_name == 'workflow_dispatch' && inputs.attest) }} + uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + with: + subject-path: target/distribution-native/${{ matrix.target }}/pliego-css-lsp${{ matrix.extension }} + sbom-path: target/distribution-native/${{ matrix.target }}/pliego-css-lsp.cdx.json + - name: Upload native payload + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: native-${{ matrix.target }} + path: target/distribution-native/${{ matrix.target }} + if-no-files-found: error + retention-days: 30 + + assemble: + name: Assemble repository assets + needs: native + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22.13.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: 11.7.0 + run_install: false + - name: Verify pinned pnpm + run: test "$(pnpm --version)" = "11.7.0" + - name: Install locked repository dependencies + run: pnpm install --frozen-lockfile + - name: Validate repository distribution source contract + run: pnpm check:repository-distribution + - name: Download all native payloads + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: native-* + path: target/distribution-incoming + merge-multiple: false + - name: Assemble and validate release assets + shell: bash + run: | + set -euo pipefail + tree="$(git show -s --format=%T HEAD)" + args=( + "--input=target/distribution-incoming" + "--output=target/distribution" + "--source-commit=${GITHUB_SHA}" + "--git-tree=${tree}" + ) + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + args+=("--tag=${GITHUB_REF_NAME}") + fi + node scripts/build-repository-distribution.mjs "${args[@]}" + pnpm check:distribution-bundle -- --root=target/distribution/release-assets + - name: Upload complete repository distribution + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: repository-distribution-${{ github.sha }} + path: target/distribution/release-assets + if-no-files-found: error + retention-days: 30 + + install: + name: pnpm install ${{ matrix.host }} + needs: assemble + runs-on: ${{ matrix.runner }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - host: windows-x64 + runner: windows-latest + - host: linux-x64 + runner: ubuntu-24.04 + - host: macos-arm64 + runner: macos-15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22.13.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: 11.7.0 + run_install: false + - name: Verify pinned pnpm + shell: bash + run: test "$(pnpm --version)" = "11.7.0" + - name: Download complete repository distribution + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: repository-distribution-${{ github.sha }} + path: target/distribution/release-assets + - name: Install verified local asset with pnpm and execute both binaries + shell: bash + run: | + version="$(node -p "require('./packages/cli/package.json').version")" + node scripts/check-installed-repository-package.mjs \ + "--tarball=target/distribution/release-assets/pliegocss-pnpm-${version}.tgz" + + attest-release-assets: + name: Attest release assets + if: ${{ github.ref_type == 'tag' || (github.event_name == 'workflow_dispatch' && inputs.attest) }} + needs: install + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + id-token: write + attestations: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: repository-distribution-${{ github.sha }} + path: target/distribution/release-assets + - name: Generate GitHub/Sigstore provenance for every release asset + uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + with: + subject-path: target/distribution/release-assets/* + + draft-release: + name: Create draft GitHub Release + if: ${{ github.ref_type == 'tag' }} + needs: attest-release-assets + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - name: Require immutable releases before creating a draft + env: + GH_TOKEN: ${{ github.token }} + run: | + gh api "repos/${GITHUB_REPOSITORY}/immutable-releases" \ + -H "X-GitHub-Api-Version: 2026-03-10" \ + --jq 'select(.enabled == true) | .enabled' | grep -qx true + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: repository-distribution-${{ github.sha }} + path: target/distribution/release-assets + - name: Create draft release with complete assets + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + args=( + "${GITHUB_REF_NAME}" + target/distribution/release-assets/* + --repo "${GITHUB_REPOSITORY}" + --draft + --verify-tag + --title "PliegoCSS ${GITHUB_REF_NAME}" + --generate-notes + ) + if [[ "${GITHUB_REF_NAME}" == *-* ]]; then + args+=(--prerelease) + fi + gh release create "${args[@]}" diff --git a/.gitignore b/.gitignore index b9df146..461e74d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ /benchmarks/results/pliego-gate-b.local.json /benchmarks/results/wasm-overhead.json /benchmarks/pliegors-integration/runtime/ +/benchmarks/external-adoption-v1/private/ /integration-tests/pliegors-smoke/target/ /integration-tests/pliegors-smoke/.pliegocss-gate/ /integration-tests/pliegors-dev-loop/target/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d41377..e9c42c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,54 @@ pre-1.0 compatibility policy. ## [Unreleased] +## [0.1.0-rc.3] - TBD + +Candidate contents are prepared below. The date remains unset until the exact +source commit passes the required hosted gates and publication is authorized. + +### Added + +- Add the G5 repository-only native distribution pipeline: Windows x64, Linux + x64 GNU, and macOS arm64 CLI/LSP archives; CycloneDX SBOMs; SHA-256 + manifests; GitHub/Sigstore attestations; and a universal no-lifecycle pnpm + package that is never published to npmjs. + +### 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. +- Keep release-readiness honest after RC.2 evidence expiration, preserve the + intentional removal of the superseded execution plan, and support native + pnpm executables in the VS Code packaging gate. + ## [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 +64,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..95921d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,7 +397,7 @@ dependencies = [ [[package]] name = "pliego-css" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "pliego-css-ir", "pliego-css-macros", @@ -406,7 +406,7 @@ dependencies = [ [[package]] name = "pliego-css-agent" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "pliego-css-build", "pliego-css-config", @@ -417,14 +417,14 @@ dependencies = [ [[package]] name = "pliego-css-basic-example" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "pliego-css", ] [[package]] name = "pliego-css-build" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "cssparser", "lightningcss", @@ -441,7 +441,7 @@ dependencies = [ [[package]] name = "pliego-css-cascade" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "cssparser", "lightningcss", @@ -451,7 +451,7 @@ dependencies = [ [[package]] name = "pliego-css-compiler" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "pliego-css-ir", "pliego-css-parser", @@ -461,7 +461,7 @@ dependencies = [ [[package]] name = "pliego-css-config" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "cssparser", "libc", @@ -477,7 +477,7 @@ dependencies = [ [[package]] name = "pliego-css-control" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "cssparser", "lightningcss", @@ -493,7 +493,7 @@ dependencies = [ [[package]] name = "pliego-css-custom-theme-example" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "pliego-css", "pliego-css-build", @@ -501,22 +501,26 @@ dependencies = [ [[package]] name = "pliego-css-io" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "libc", ] [[package]] name = "pliego-css-ir" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" [[package]] name = "pliego-css-lsp" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" 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", @@ -524,7 +528,7 @@ dependencies = [ [[package]] name = "pliego-css-macros" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "pliego-css-compiler", "pliego-css-ir", @@ -537,7 +541,7 @@ dependencies = [ [[package]] name = "pliego-css-ownership" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "serde", "serde_json", @@ -546,14 +550,14 @@ dependencies = [ [[package]] name = "pliego-css-parser" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "pliego-css-ir", ] [[package]] name = "pliego-css-publication" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "fs2", "libc", @@ -562,7 +566,7 @@ dependencies = [ [[package]] name = "pliego-css-source" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "libc", "pliego-css-io", @@ -575,7 +579,7 @@ dependencies = [ [[package]] name = "pliego-css-theme" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "pliego-css-ir", "sha2", @@ -583,7 +587,7 @@ dependencies = [ [[package]] name = "pliego-css-usage" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "pliego-css-build", "pliego-css-compiler", @@ -596,7 +600,7 @@ dependencies = [ [[package]] name = "pliego-css-watch" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "inotify", "pliego-css-io", @@ -605,7 +609,7 @@ dependencies = [ [[package]] name = "pliego-cssc" -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" dependencies = [ "libc", "pliego-css-agent", diff --git a/Cargo.toml b/Cargo.toml index 007d749..ac1dddf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ ] [workspace.package] -version = "0.1.0-rc.2" +version = "0.1.0-rc.3" edition = "2024" rust-version = "1.85" license = "Apache-2.0" @@ -38,24 +38,24 @@ keywords = ["css", "rust", "static-analysis", "pliegors", "web"] categories = ["compilers", "development-tools", "web-programming"] [workspace.dependencies] -pliego-css = { path = "crates/pliego-css", version = "=0.1.0-rc.2" } -pliego-css-agent = { path = "crates/pliego-css-agent", version = "=0.1.0-rc.2" } -pliego-css-build = { path = "crates/pliego-css-build", version = "=0.1.0-rc.2" } -pliego-css-cascade = { path = "crates/pliego-css-cascade", version = "=0.1.0-rc.2" } -pliego-css-compiler = { path = "crates/pliego-css-compiler", version = "=0.1.0-rc.2" } -pliego-css-config = { path = "crates/pliego-css-config", version = "=0.1.0-rc.2" } -pliego-css-control = { path = "crates/pliego-css-control", version = "=0.1.0-rc.2" } -pliego-css-ir = { path = "crates/pliego-css-ir", version = "=0.1.0-rc.2" } -pliego-css-io = { path = "crates/pliego-css-io", version = "=0.1.0-rc.2" } -pliego-css-lsp = { path = "crates/pliego-css-lsp", version = "=0.1.0-rc.2" } -pliego-css-macros = { path = "crates/pliego-css-macros", version = "=0.1.0-rc.2" } -pliego-css-ownership = { path = "crates/pliego-css-ownership", version = "=0.1.0-rc.2" } -pliego-css-publication = { path = "crates/pliego-css-publication", version = "=0.1.0-rc.2" } -pliego-css-parser = { path = "crates/pliego-css-parser", version = "=0.1.0-rc.2" } -pliego-css-source = { path = "crates/pliego-css-source", version = "=0.1.0-rc.2" } -pliego-css-theme = { path = "crates/pliego-css-theme", version = "=0.1.0-rc.2" } -pliego-css-usage = { path = "crates/pliego-css-usage", version = "=0.1.0-rc.2" } -pliego-css-watch = { path = "crates/pliego-css-watch", version = "=0.1.0-rc.2" } +pliego-css = { path = "crates/pliego-css", version = "=0.1.0-rc.3" } +pliego-css-agent = { path = "crates/pliego-css-agent", version = "=0.1.0-rc.3" } +pliego-css-build = { path = "crates/pliego-css-build", version = "=0.1.0-rc.3" } +pliego-css-cascade = { path = "crates/pliego-css-cascade", version = "=0.1.0-rc.3" } +pliego-css-compiler = { path = "crates/pliego-css-compiler", version = "=0.1.0-rc.3" } +pliego-css-config = { path = "crates/pliego-css-config", version = "=0.1.0-rc.3" } +pliego-css-control = { path = "crates/pliego-css-control", version = "=0.1.0-rc.3" } +pliego-css-ir = { path = "crates/pliego-css-ir", version = "=0.1.0-rc.3" } +pliego-css-io = { path = "crates/pliego-css-io", version = "=0.1.0-rc.3" } +pliego-css-lsp = { path = "crates/pliego-css-lsp", version = "=0.1.0-rc.3" } +pliego-css-macros = { path = "crates/pliego-css-macros", version = "=0.1.0-rc.3" } +pliego-css-ownership = { path = "crates/pliego-css-ownership", version = "=0.1.0-rc.3" } +pliego-css-publication = { path = "crates/pliego-css-publication", version = "=0.1.0-rc.3" } +pliego-css-parser = { path = "crates/pliego-css-parser", version = "=0.1.0-rc.3" } +pliego-css-source = { path = "crates/pliego-css-source", version = "=0.1.0-rc.3" } +pliego-css-theme = { path = "crates/pliego-css-theme", version = "=0.1.0-rc.3" } +pliego-css-usage = { path = "crates/pliego-css-usage", version = "=0.1.0-rc.3" } +pliego-css-watch = { path = "crates/pliego-css-watch", version = "=0.1.0-rc.3" } [workspace.lints.rust] missing_docs = "warn" diff --git a/README.md b/README.md index ba2c6d9..111f600 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,10 @@
> [!IMPORTANT] -> `0.1.0-rc.2` is public-preview software distributed from the public -> canonical repository. +> This workspace prepares `0.1.0-rc.3` as the next public-preview candidate. > The nineteen PliegoCSS crates form one exact-version compatibility unit and -> are published on crates.io and replayed from the registry with Rust 1.85 -> after the complete hosted CI and CodeQL matrices passed. `0.1.0` remains blocked by the +> must not be mixed across versions. RC.3 publication and registry replay remain +> blocked until the exact-source hosted gates pass. `0.1.0` remains blocked by the > [machine readiness record](./docs/product/release-readiness-0.1.0.md). > Core formats and selected APIs are frozen candidates; beta and experimental > capabilities may still change. @@ -76,7 +75,7 @@ PliegoCSS is designed around four principles: The exact maturity and claim boundaries are maintained in the [product maturity map](./docs/product/maturity-map.md). Competitive coverage is tracked honestly in -the [Tailwind CSS v4.3.2 matrix](./docs/benchmarks/tailwind-v4-competitive-matrix.md). +the [Tailwind CSS v4.3.3 matrix](./docs/benchmarks/tailwind-v4-competitive-matrix.md). ## Quick start @@ -84,14 +83,20 @@ the [Tailwind CSS v4.3.2 matrix](./docs/benchmarks/tailwind-v4-competitive-matri - Rust **1.85 or newer** - Git -- Node.js and pnpm are required only for repository development and full verification +- Node.js and pnpm are required for repository development/full verification and for the optional + repository-hosted pnpm launcher; they are not Rust crate dependencies -Install the public-preview CLI from crates.io: +After RC.3 is published, install the exact CLI from crates.io: ```console -cargo install pliego-cssc --version '=0.1.0-rc.2' --locked +cargo install pliego-cssc --version '=0.1.0-rc.3' --locked ``` +RC.3 is designed to attach a repository-only universal pnpm package to its immutable GitHub +Release. It is never published to npmjs, contains no dependencies or lifecycle scripts, and must be +downloaded, verified, and installed as a local `.tgz` with pnpm. RC.2 predates that G5 artifact; see +the [installation guide](./docs/getting-started/installation.md#repository-hosted-pnpm-package). + Or clone the repository and build locally: ```console diff --git a/SECURITY.md b/SECURITY.md index 17f338e..78aca1b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,7 +13,8 @@ and the default branch. | Version | Supported | | --- | --- | -| `0.1.0-rc.2` | Yes | +| `0.1.0-rc.3` candidate and `main` | Yes | +| `0.1.0-rc.2` | No | | `0.1.0-rc.1` | No | | Earlier snapshots | No | diff --git a/SUPPORT.md b/SUPPORT.md index badee16..63680ce 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -7,6 +7,8 @@ Read the [project documentation](docs/index.md), CLI help, and [troubleshooting guides](docs/troubleshooting/lsp.md) before opening a request. Include stable diagnostic identifiers whenever one is emitted. +The exact certified adapter/version matrix and explicit exclusions are published in the +[0.1.x adapter support policy](docs/reference/adapter-support-policy-0.1.x.md). ## Public support @@ -24,5 +26,9 @@ General Rust or CSS consulting, private-project debugging, and requests without a reproduction may be redirected or closed. Use `hello@pliegocss.dev` for private project correspondence. +External teams interested in a consented G7 interview or bounded adoption pilot should use the same +address and review the [evidence intake protocol](docs/contributing/external-adoption-intake.md). +The public cohort notice is [GitHub issue #7](https://github.com/celiumsai/pliegocss/issues/7); +public comments are recruitment signals only and must not contain private evidence or personal data. Vulnerabilities must follow [SECURITY.md](SECURITY.md). PliegoCSS is provided under Apache-2.0 without a support SLA or warranty. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 2c6f03a..17714ca 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 + `b841e850a294c98603dd98fa8ba847ecd67d4fa490439726ac07939635ac2a51`. + +## 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/adapter-coexistence-v1/authority.json b/benchmarks/adapter-coexistence-v1/authority.json new file mode 100644 index 0000000..f1e8c22 --- /dev/null +++ b/benchmarks/adapter-coexistence-v1/authority.json @@ -0,0 +1,158 @@ +{ + "schemaVersion": 1, + "status": "active-contract", + "kind": "pliegocss-adapter-coexistence-certification", + "corpus": { + "path": "benchmarks/adapter-coexistence-v1/corpus.json", + "sha256": "2d569964591d4a03981c2762cdeda3daa99a5323f1fe97460b74afa85e878a8c", + "minimumProjects": 20 + }, + "sharedReset": { + "path": "benchmarks/browser-output-certification-v1/shared-reset.css", + "sha256": "5d190d599bf4b966fd76cf1c456b5904c8135506c9a6a4e4853ac4fc6cac6f38" + }, + "tailwindProfiles": [ + { + "id": "tailwind-v3-lts", + "packageAlias": "tailwindcss-v3-lts", + "packageName": "tailwindcss", + "version": "3.4.19", + "sourceContract": "v3-directives-no-preflight" + }, + { + "id": "tailwind-v4-current", + "packageAlias": "tailwindcss", + "cliPackageAlias": "@tailwindcss/cli", + "packageName": "tailwindcss", + "version": "4.3.3", + "sourceContract": "v4-import-no-preflight" + } + ], + "adapters": [ + { + "id": "html", + "minimumProjects": 7, + "execution": "direct-document" + }, + { + "id": "vite", + "minimumProjects": 7, + "execution": "vite-production-build", + "package": "vite", + "version": "8.1.5" + }, + { + "id": "pliegors", + "minimumProjects": 7, + "execution": "rendered-output-contract", + "upstreamGate": "integration:pliegors-browser" + } + ], + "hosts": [ + { + "id": "windows-x64-chromium", + "runner": "windows-latest", + "os": "win32", + "arch": "x64", + "browser": "chromium" + }, + { + "id": "linux-x64-chromium", + "runner": "ubuntu-24.04", + "os": "linux", + "arch": "x64", + "browser": "chromium" + }, + { + "id": "macos-arm64-chromium", + "runner": "macos-15", + "os": "darwin", + "arch": "arm64", + "browser": "chromium" + } + ], + "migration": { + "mode": "static-complete-class-group", + "applyCommand": "migration-group-apply", + "rollbackCommand": "migration-group-rollback", + "tailwindRuntimeRetained": true, + "dynamicClassPolicy": "reject", + "unsupportedUtilityPolicy": "reject", + "rollback": "restore-exact-source-bytes" + }, + "comparison": { + "dom": "Body element/text tree and every non-class attribute must be identical.", + "aria": "Playwright body ARIA snapshot must be byte-identical.", + "computedStyle": "Every frozen property on each migrated class-bearing node must be equal after color and browser alias normalization.", + "layoutGeometry": "Each migrated node border box may differ by at most 0.1 CSS px.", + "maximumLayoutGeometryDeltaCssPx": 0.1, + "computedProperties": [ + "display", + "position", + "box-sizing", + "width", + "height", + "min-width", + "min-height", + "max-width", + "max-height", + "margin-top", + "margin-right", + "margin-bottom", + "margin-left", + "padding-top", + "padding-right", + "padding-bottom", + "padding-left", + "row-gap", + "column-gap", + "align-items", + "justify-content", + "flex-direction", + "flex-wrap", + "flex-grow", + "flex-shrink", + "grid-template-columns", + "color", + "background-color", + "border-top-width", + "border-right-width", + "border-bottom-width", + "border-left-width", + "border-top-style", + "border-top-color", + "border-radius", + "outline-width", + "outline-style", + "outline-color", + "outline-offset", + "font-family", + "font-size", + "font-weight", + "line-height", + "letter-spacing", + "text-transform", + "opacity", + "cursor", + "resize" + ] + }, + "requiredCoverage": [ + "at-least-20-projects", + "html", + "vite-production-build", + "pliegors-rendered-output", + "pinned-pliegors-framework-browser", + "tailwind-v3-lts", + "tailwind-v4-current", + "tailwind-source-inventory", + "tailwind-output-audit", + "reversible-group-apply", + "exact-byte-rollback", + "dom-equivalence", + "aria-equivalence", + "computed-style-equivalence", + "layout-geometry-equivalence" + ], + "claimBoundary": "Passing proves the bounded, static complete-class-group migration protocol for the tracked corpus and named hosts. Tailwind remains loaded during coexistence. Dynamic class construction, arbitrary plugin execution, general Tailwind compatibility, and untracked PliegoRS applications are outside this claim." +} diff --git a/benchmarks/adapter-coexistence-v1/corpus.json b/benchmarks/adapter-coexistence-v1/corpus.json new file mode 100644 index 0000000..97e32c4 --- /dev/null +++ b/benchmarks/adapter-coexistence-v1/corpus.json @@ -0,0 +1,174 @@ +{ + "schemaVersion": 1, + "kind": "pliegocss-adapter-coexistence-corpus", + "projects": [ + { + "id": "html-action-buttons", + "adapter": "html", + "tailwindProfile": "tailwind-v3-lts", + "title": "Action buttons", + "viewport": { "width": 390, "height": 844 }, + "body": "Edition 006
A bounded card with semantic heading and descriptive copy.
Build time
12 ms
CSS gzip
4.2 KB
Release candidate
Every promoted artifact stays bound to its source commit.
Read notesAll systems operational
Verified on the selected source commit.
Community
Free
LTS
Stable
Founder
Building deterministic systems from Medellin.
Windows
Passed
Linux
Passed
Draft a release and attach every artifact before publication.
PliegoRS rendered
Server output preserves semantic markup while styles migrate.
Open docsResumable island
Source
Exact
Rollback
Verified
Receipts
21
Bound
ARIA drift
0
Equivalent
Assets cannot be replaced after publication.
");
+ 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..b6c1908 100644
--- a/crates/pliego-css-build/tests/css_audit.rs
+++ b/crates/pliego-css-build/tests/css_audit.rs
@@ -56,7 +56,7 @@ fn valid_standard_css_emits_deterministic_inventory_evidence() {
assert_eq!(metric("maximum-specificity"), "1,1,0");
assert_eq!(
sha256_hex(json.as_bytes()),
- "82d0c41ecff10f8a79263df5b064b09132444ae0b054a23f0d0d2acee0f60ff5"
+ "e639a6c1901c6a8e90f9e61b9847cceb1ee4a5f637cc218810dc95f4626d2f14"
);
}
@@ -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-control/README.md b/crates/pliego-css-control/README.md
index 95d1447..5a8cc06 100644
--- a/crates/pliego-css-control/README.md
+++ b/crates/pliego-css-control/README.md
@@ -9,4 +9,4 @@ SARIF, and bounded accessibility analysis. Schema-only consumers leave it disabl
## Stability
-Published as exact-version public-preview tooling in `0.1.0-rc.2`; final `0.1.0` is not published.
+Prepared as exact-version public-preview tooling for `0.1.0-rc.3`; final `0.1.0` is not published.
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..67d51f3 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.3' --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/migration-inventory-schema-1.md b/docs/reference/migration-inventory-schema-1.md
index 3ba05d7..9a3c5f8 100644
--- a/docs/reference/migration-inventory-schema-1.md
+++ b/docs/reference/migration-inventory-schema-1.md
@@ -1,7 +1,7 @@
# Migration inventory schema 1
Status: **Rust producer and fail-closed CLI implemented for bounded single-file Sass, Tailwind CSS
-v4 entry CSS, and CSS Modules inventory; project graphs remain open**
+v3/v4 entry CSS, and CSS Modules inventory; project graphs remain open**
`pliego-css-source` exposes a read-only migration bridge that accepts an explicit source kind,
portable logical path, and exact UTF-8 source bytes. It never runs Sass, Tailwind, PostCSS,
@@ -77,15 +77,17 @@ it can prove from the supplied source and leaves execution semantics to the orig
## Covered constructs
-### Tailwind CSS v4 entry CSS
+### Tailwind CSS v3/v4 entry CSS
+- v3 `@tailwind base`, `@tailwind components`, and `@tailwind utilities` layer directives;
- `@import "tailwindcss"` and `@import "tailwindcss/preflight"`;
- `@source`, including dynamic `inline(...)` candidate generation;
- `@theme`, `@utility`, `@variant`, `@custom-variant`, and `@reference`;
- unsupported `@apply`, `@plugin`, and `@config` seams.
`preflightReliance` is `not-observed`, `implicit`, `explicit`, or `mixed`. A full
-`@import "tailwindcss"` is implicit reliance; the dedicated preflight import is explicit. This
+`@import "tailwindcss"` is implicit reliance; the dedicated preflight import or v3
+`@tailwind base` directive is explicit. Unknown v3 layer names are retained as unsupported. This
classification does not inspect generated CSS or prove that later tooling did not remove Preflight.
### Sass/SCSS
diff --git a/docs/reference/public-api.md b/docs/reference/public-api.md
index 96b6027..b229b1f 100644
--- a/docs/reference/public-api.md
+++ b/docs/reference/public-api.md
@@ -1,6 +1,6 @@
# Public API candidate
-Status: **application, build-macro, and adapter surfaces published at `0.1.0-rc.2`; the
+Status: **application, build-macro, and adapter surfaces prepared for `0.1.0-rc.3`; the
Cargo DTCG, ownership, usage-sidecar, and application-topology collector bridges are covered by the
Rust 1.85 downstream smoke; this remains a prerelease contract**
@@ -215,6 +215,13 @@ attests only the declared roots and graph: it does not discover the product Carg
registry, infer identities from paths, or derive bundle partitions. Scanner DTOs and standalone
scanner functions remain advanced tooling rather than promoted adapter API.
+Frameworks that publish `pliegors-product-topology/1` can instead use the
+closed `ProductTopology::from_json` decoder and `collect_css_inputs`. The
+returned `ProductCssInputs` exposes canonical reachability, a deterministic
+bundle plan, and read-only `ProductBundle` IDs, source partitions, and theme
+ownership. This wire consumer does not link PliegoRS types or move styling into
+runtime code.
+
## Migration-inventory tooling bridge
Exact-version audit and migration tooling may use the selected `pliego-css-source` inventory
@@ -345,6 +352,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/reversible-migration-plan-schema-1.md b/docs/reference/reversible-migration-plan-schema-1.md
index 051d4fa..96121e3 100644
--- a/docs/reference/reversible-migration-plan-schema-1.md
+++ b/docs/reference/reversible-migration-plan-schema-1.md
@@ -39,3 +39,9 @@ proposal only when explicitly invoked. Apply refuses existing output/receipt pat
receipt bound to exact output bytes. Rollback refuses drift and removes the receipt only after the
sidecar is safely removed. This is still not a Tailwind codemod: authored sources and templates stay
untouched, and the plan's executable edit set remains empty.
+
+G6 does not change that command's schema or read-only promise. The separate
+[`adapter coexistence protocol`](./adapter-coexistence.md) composes compiler-manifest mappings with
+an explicitly authored `migration-group-apply` manifest for literal complete class groups. That
+bounded protocol has browser and exact-rollback evidence; it does not silently populate or execute
+the inventory-only plan's `edits` field.
diff --git a/docs/reference/semantic-ir-binary-v2.md b/docs/reference/semantic-ir-binary-v2.md
index fc3aacf..70e46cb 100644
--- a/docs/reference/semantic-ir-binary-v2.md
+++ b/docs/reference/semantic-ir-binary-v2.md
@@ -1,6 +1,6 @@
# Semantic IR binary format 2
-Status: **machine-enforced prerelease contract at `0.1.0-rc.2`**
+Status: **machine-enforced prerelease contract at `0.1.0-rc.3`**
Format 2 is the current persisted, provenance-bearing semantic IR envelope exposed by
`pliego-css-compiler`. It is distinct from [StyleId format 2](./style-id-format-v2.md): StyleId is a
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..be64793 100644
--- a/docs/reference/style-id-format-v2.md
+++ b/docs/reference/style-id-format-v2.md
@@ -1,6 +1,6 @@
# StyleId binary format 2
-Status: **implemented and protected by exact prerelease vectors at `0.1.0-rc.2`**
+Status: **implemented and protected by exact prerelease vectors at `0.1.0-rc.3`**
This document specifies the canonical byte stream used by
`STYLE_ID_FORMAT_VERSION = 2`, the derivation of the 128-bit `StyleId`, and its relationship to a
@@ -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/reference/utilities.md b/docs/reference/utilities.md
index f1effa9..662db79 100644
--- a/docs/reference/utilities.md
+++ b/docs/reference/utilities.md
@@ -264,7 +264,7 @@ uses the first 128 digest bits in big-endian order. UTF-8 content, not intern-ta
`Debug` output, enters the stream. The class encoder remains format 1, but candidate format-1 class
strings changed because their StyleId input changed. See the exact
[StyleId format-2 contract](./style-id-format-v2.md). These vectors are machine-enforced in
-public preview `0.1.0-rc.2`; final `0.1.0` promotion remains open. `Style::EMPTY.class_name()` returns an empty
+public-preview candidate `0.1.0-rc.3`; final `0.1.0` promotion remains open. `Style::EMPTY.class_name()` returns an empty
string and does not represent an emitted rule.
Run the checked Rust example and compile its CSS from the workspace root:
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/releases/0.1.0-rc.3.md b/docs/releases/0.1.0-rc.3.md
new file mode 100644
index 0000000..b8dc40a
--- /dev/null
+++ b/docs/releases/0.1.0-rc.3.md
@@ -0,0 +1,60 @@
+# PliegoCSS 0.1.0-rc.3
+
+> Candidate release record. Publication is not authorized by this document.
+> The schema-3 [machine readiness record](../product/release-readiness-0.1.0.json)
+> remains the current promotion authority.
+
+RC.3 consolidates the post-RC.2 verification, distribution, and adapter work
+into the next exact-version nineteen-crate compatibility unit.
+
+## Highlights
+
+- A shared pure `CompileRequest` / `CompileResult` engine across CLI, watch,
+ macros, source scanning, and LSP.
+- LSP semantic analysis without compiler child processes or synthetic source
+ files.
+- ThemeId and theme binary format 3, catalog schema 4, and inspection schema 3
+ with explicit regeneration requirements.
+- Benchmark Authority v2 against Tailwind CSS 4.3.3, Tailwind v3 LTS 3.4.19,
+ and the frozen 4.3.2 regression lane.
+- Browser/output certification contracts for Chromium, Firefox, and WebKit on
+ Windows x64, Linux x64, and macOS ARM64.
+- A repository-only native CLI/LSP distribution with SHA-256 manifests,
+ CycloneDX SBOMs, GitHub/Sigstore attestations, and a no-lifecycle pnpm
+ package that is never published to npmjs.
+- Bounded Tailwind coexistence certification across 21 HTML, Vite, and
+ PliegoRS project snapshots with exact rollback.
+- Governed external-adoption intake that remains blocked until real reviewed
+ interviews, incidents, and pilots satisfy G7.
+- Framework-neutral consumption of the canonical
+ `pliegors-product-topology/1` snapshot, keeping PliegoRS topology and
+ PliegoCSS reachability/bundle planning aligned without a runtime dependency.
+
+## Compatibility
+
+The candidate preserves StyleId format 2, class-name format 1, and semantic IR
+binary format 2. ThemeId and theme binary format move to 3. Consumers must
+regenerate theme binaries, macro output, CSS, manifests, catalogs, indexes,
+receipts, and class-keyed caches as one coordinated revision.
+
+## Install
+
+These commands become valid only after all nineteen crates are published and
+the registry replay passes:
+
+```console
+cargo install pliego-cssc --version '=0.1.0-rc.3' --locked
+cargo install pliego-css-lsp --version '=0.1.0-rc.3' --locked
+```
+
+## Open gates
+
+- exact-candidate hosted CI and CodeQL;
+- complete hosted browser and adapter matrices;
+- crates.io and production replay;
+- signed repository release assets;
+- external G7 adoption evidence; and
+- explicit publication authorization.
+
+No local result, historical artifact, or this release note can clear those
+source-bound gates.
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/external-adoption.md b/docs/site/external-adoption.md
new file mode 100644
index 0000000..250b88d
--- /dev/null
+++ b/docs/site/external-adoption.md
@@ -0,0 +1,23 @@
+
+
+# External adoption is a measured gate.
+
+G7 is currently blocked. Owned fixtures and synthetic incidents cannot satisfy it.
+
+## Qualified evidence {#status}
+
+Interviews: 0/10. Real incidents: 0/20. Completed pilots: 0/3. Independent projects: 0/3. Independent organizations: 0/3.
+
+## Applications are open {#apply}
+
+Read the [public cohort notice](https://github.com/celiumsai/pliegocss/issues/7) or apply privately at [hello@pliegocss.dev](mailto:hello@pliegocss.dev?subject=G7%20external%20adoption). Do not post names, email addresses, credentials, private repository URLs, proprietary source, transcripts, or incident material on GitHub. A public application is not consent and is not evidence.
+
+## Admission boundary {#boundary}
+
+Only consented, redacted, reviewed external records bound to private source evidence by SHA-256 count. Projects owned by Celiums Solutions, PliegoCSS, or celiumsai do not count.
+
+## Certified adapter scope {#support}
+
+The frozen 0.1.x policy covers complete literal class groups in static HTML, Vite 8.1.5, and pinned PliegoRS rendered output against Tailwind CSS 3.4.19 or 4.3.3. Tailwind stays loaded during coexistence. Dynamic classes, arbitrary config/plugin execution, and Tailwind removal are excluded.
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..0cc3da6
--- /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 0.1.0-rc.3 compatibility unit is under exact-source verification. Use this command after all nineteen crates are published and replayed with Rust 1.85.
+
+```console
+cargo install pliego-cssc --version '=0.1.0-rc.3' --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..02bdee3
--- /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.3' --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.3"
+```
+
+## 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..f7c8ba3
--- /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}
+
+exact-candidate-ci (pending): GitHub Actions run 29864046865 passed the exact RC.2 commit across Ubuntu, Windows, and macOS with Rust 1.85 and 1.96 plus the listed release jobs, but its promotion window has expired and requires a fresh candidate replay. exact-candidate-codeql (pending): CodeQL run 29864046876 passed Actions, JavaScript/TypeScript, and Rust analysis on the exact RC.2 commit, but its promotion window has expired and requires a fresh candidate replay. 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. adapter-coexistence-matrix (blocked): RC.2 predates the G6 adapter authority. It has no clean exact-source 21-project HTML/Vite/PliegoRS matrix, Tailwind v3/v4 source/output audit, DOM/ARIA/style equivalence, exact rollback, or pinned PliegoRS framework replay. 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): RC.2 predates the repository-only G5 distribution contract. It has no exact-source native archives, checksums, CycloneDX SBOMs, GitHub/Sigstore attestations, immutable GitHub Release, or verified pnpm package asset; npmjs publication is forbidden. external-adoption-evidence (pending): RC.2 predates the G7 authority. Recruitment is open, but the required external interviews, real incidents, and pilots have not been recorded as consented, redacted, reviewed, hash-bound 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/g5.md b/docs/status/g5.md
new file mode 100644
index 0000000..e4c4dac
--- /dev/null
+++ b/docs/status/g5.md
@@ -0,0 +1,33 @@
+# G5 repository distribution status
+
+Status: **architecture and local contracts implemented; exact-source hosted distribution evidence is
+required before G5 can close for a candidate**
+
+## Scope
+
+G5 owns the native CLI/LSP and optional Node launcher distribution boundary. It does not change the
+nineteen-crate crates.io compatibility unit and does not authorize a final release.
+
+## Implemented contracts
+
+- [x] Repository-only machine policy with npmjs publication forbidden.
+- [x] Private universal `@pliegocss/cli` package with no dependencies or lifecycle scripts.
+- [x] Shell-free host selection for Windows x64, Linux x64 GNU, and macOS arm64.
+- [x] Native payload and aggregate manifest validators with PE/ELF/Mach-O guards.
+- [x] SHA-256 asset inventory and CycloneDX 1.5 SBOM validation.
+- [x] Local pnpm pack/bundle contract using three format-correct fixtures.
+- [x] Hosted three-platform native build and three-platform pnpm install workflow.
+- [x] Conditional GitHub/Sigstore binary, SBOM, and release-asset attestations.
+- [x] Tag path creates only a draft release and fails closed when release immutability is disabled.
+
+## Evidence still required
+
+- [ ] A clean hosted workflow run on the final G5 source across all native and install lanes.
+- [ ] One reviewed manual exact-source run with `attest=true` and successful `gh attestation verify`.
+- [ ] Repository immutable releases enabled before any tag path is used.
+- [ ] A newly named candidate whose tag, commit, tree, artifacts, SBOMs, checksums, and attestations
+ all agree.
+- [ ] Owner authorization before creating that candidate tag or publishing its draft Release.
+
+RC.2 remains blocked because it predates these artifacts. Branch or later-candidate evidence cannot
+be copied into its readiness record.
diff --git a/docs/status/portability.md b/docs/status/portability.md
index 0380219..e9deba6 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 | `79e4817107cbb7a20b5c0be1a8a881b11ebfc3f9e94f83a93f9d318f74059cf9` findings | `711c183edb566062d3feec8e7a7e42117a8b6b6b0d05d237afc0469abd09d78b` | `1162d5743e2ccd7d03614605a700cd55099bde9380c19df5d95d9cfb65875479` |
+| Bundle control | `65dbbcff74b6886fdd749b1c7c0587eaff06b4f4720405e10716c2b0dfee9bbc` CSS | `cf7e29c1a78cc63120b53a0b88b7c9395edd51f9322a0c6c2b565a8a03458475` | `da835752a54b00ec3d7922ec1394958c4c1465c728c86f21f6474540242e1d7c` |
+| Reopened Asset Plan | `e036dcb3587507db07e39a55cca5404dd074b4ede3abb70e487005e18ab478b7` findings | `a4e31d7f69c2ed88447eaf984c0bfdf6e85180a2ea80990234a51cb5141cc9ba` | `43d2297a39dd74dacd71a49ffee43febee1ba9e83d80bf0a55e1435eb9944fb8` |
+| Compile control | `65dbbcff74b6886fdd749b1c7c0587eaff06b4f4720405e10716c2b0dfee9bbc` CSS | `92e17db715bef4de971f75da73c790b68c2cc1b017edae91b4df283d5d9b6277` | `4a4321f4142bfe0a06ebb419223fce693b82966ed2e72d4230cba71d52beaf3c` |
+| Watch control | `65dbbcff74b6886fdd749b1c7c0587eaff06b4f4720405e10716c2b0dfee9bbc` CSS | `8fc31e3d221d70fe99931fcb8c1a2e414ed4a6e78145a667a179d7c3ada03543` | `3f43fa1238c25ed4f17942c4b6ef42d2c1610fe6ffdc80e4153397066c2335c1` |
+| Direct DTCG | `9a349f5059ffb3ace6c985dfe1fadcd9cb9ac45668b5cd0c00e26625e5a6c6c3` CSS | `8758c9d6b69e42f5adc48ba7d84f4fb026618af8737fea690185bb53dd7f1994` | `aafe05a73d83a00aaa3be700bc554624389c60fb351e2f7a1c8d2cd6f0cae5aa` |
-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..c6a4d33 100644
--- a/editors/vscode/scripts/check-host.mjs
+++ b/editors/vscode/scripts/check-host.mjs
@@ -1,17 +1,37 @@
import { spawnSync } from "node:child_process";
+import { createHash } from "node:crypto";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
-import { resolve, sep } from "node:path";
+import { isAbsolute, join, resolve, sep } from "node:path";
import { runTests } from "@vscode/test-electron";
const EXTENSION = resolve(import.meta.dirname, "..");
const ROOT = resolve(EXTENSION, "..", "..");
-const target = process.env.CARGO_TARGET_DIR
- ? resolve(ROOT, process.env.CARGO_TARGET_DIR)
+const configuredBase = process.env.PLIEGOCSS_TARGET_BASE ?? process.env.CARGO_TARGET_DIR;
+const targetBase = configuredBase
+ ? (isAbsolute(configuredBase) ? configuredBase : resolve(ROOT, configuredBase))
: resolve(ROOT, "target");
+const rustc = spawnSync("rustup", ["run", "1.85.0", "rustc", "-Vv"], {
+ encoding: "utf8",
+ windowsHide: true,
+});
+if (rustc.error || rustc.status !== 0) {
+ throw new Error(`cannot identify Rust 1.85.0: ${rustc.error?.message ?? rustc.stderr}`);
+}
+const identity = rustc.stdout.replaceAll("\r\n", "\n").trim();
+const release = identity.match(/^release:\s*(.+)$/mu)?.[1] ?? "unknown";
+const host = identity.match(/^host:\s*(.+)$/mu)?.[1] ?? `${process.platform}-${process.arch}`;
+/** @param {string} value */
+const safeSegment = (value) =>
+ value.toLowerCase().replaceAll(/[^a-z0-9._-]+/gu, "-").replaceAll(/^-+|-+$/gu, "");
+const targetSegment = `${safeSegment(host)}-rustc-${safeSegment(release)}-${createHash("sha256").update(identity).digest("hex").slice(0, 16)}`;
+const target = process.env.PLIEGOCSS_ISOLATED_TARGET === "1" && process.env.CARGO_TARGET_DIR
+ ? (isAbsolute(process.env.CARGO_TARGET_DIR)
+ ? process.env.CARGO_TARGET_DIR
+ : resolve(ROOT, process.env.CARGO_TARGET_DIR))
+ : join(targetBase, "toolchains", targetSegment);
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) {
@@ -22,7 +42,13 @@ if (!workspace.startsWith(`${target}${sep}`)) fail("unsafe VS Code host fixture
const fixture = spawnSync(process.execPath, [resolve(ROOT, "scripts", "check-lsp-integration.mjs")], {
cwd: ROOT,
encoding: "utf8",
- env: { ...process.env, PLIEGOCSS_KEEP_LSP_FIXTURE: "1" },
+ env: {
+ ...process.env,
+ CARGO_TARGET_DIR: target,
+ PLIEGOCSS_ISOLATED_TARGET: "1",
+ PLIEGOCSS_KEEP_LSP_FIXTURE: "1",
+ PLIEGOCSS_TARGET_BASE: targetBase,
+ },
stdio: ["ignore", "pipe", "pipe"],
});
if (fixture.error) fail(`cannot prepare VS Code host fixture: ${fixture.error.message}`);
@@ -34,7 +60,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 +93,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/fuzz/Cargo.lock b/fuzz/Cargo.lock
index b7aae1b..82cd4a4 100644
--- a/fuzz/Cargo.lock
+++ b/fuzz/Cargo.lock
@@ -119,7 +119,7 @@ dependencies = [
[[package]]
name = "pliego-css-compiler"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
"pliego-css-parser",
@@ -139,18 +139,18 @@ dependencies = [
[[package]]
name = "pliego-css-ir"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
[[package]]
name = "pliego-css-parser"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
]
[[package]]
name = "pliego-css-theme"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
"sha2",
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-dev-loop/Cargo.lock b/integration-tests/pliegors-dev-loop/Cargo.lock
index 97ed556..5c50fd2 100644
--- a/integration-tests/pliegors-dev-loop/Cargo.lock
+++ b/integration-tests/pliegors-dev-loop/Cargo.lock
@@ -269,7 +269,7 @@ dependencies = [
[[package]]
name = "pliego-css"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
"pliego-css-macros",
@@ -277,7 +277,7 @@ dependencies = [
[[package]]
name = "pliego-css-compiler"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
"pliego-css-parser",
@@ -287,11 +287,11 @@ dependencies = [
[[package]]
name = "pliego-css-ir"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
[[package]]
name = "pliego-css-macros"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-compiler",
"pliego-css-ir",
@@ -304,14 +304,14 @@ dependencies = [
[[package]]
name = "pliego-css-parser"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
]
[[package]]
name = "pliego-css-theme"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
"sha2",
diff --git a/integration-tests/pliegors-smoke/Cargo.lock b/integration-tests/pliegors-smoke/Cargo.lock
index b0b99a3..5ca9dff 100644
--- a/integration-tests/pliegors-smoke/Cargo.lock
+++ b/integration-tests/pliegors-smoke/Cargo.lock
@@ -43,9 +43,9 @@ dependencies = [
[[package]]
name = "cap-primitives"
-version = "4.0.2"
+version = "4.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdadbd7c002d3a484b35243669abdae85a0ebaded5a61117169dc3400f9a7ff0"
+checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18"
dependencies = [
"ambient-authority",
"fs-set-times",
@@ -150,21 +150,21 @@ dependencies = [
[[package]]
name = "futures-core"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
@@ -206,9 +206,9 @@ checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96"
[[package]]
name = "ipnet"
-version = "2.12.0"
+version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
[[package]]
name = "itoa"
@@ -229,9 +229,9 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.186"
+version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linux-raw-sys"
@@ -265,9 +265,7 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pliego-artifact"
-version = "0.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e2e2ab68b4e274215ef7b31819189d17f42b41c0c93f158380952abdd857451"
+version = "0.4.0-beta.1"
dependencies = [
"cap-fs-ext",
"cap-std",
@@ -280,7 +278,7 @@ dependencies = [
[[package]]
name = "pliego-css"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
"pliego-css-macros",
@@ -288,7 +286,7 @@ dependencies = [
[[package]]
name = "pliego-css-compiler"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
"pliego-css-parser",
@@ -298,18 +296,18 @@ dependencies = [
[[package]]
name = "pliego-css-io"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"libc",
]
[[package]]
name = "pliego-css-ir"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
[[package]]
name = "pliego-css-macros"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-compiler",
"pliego-css-ir",
@@ -322,14 +320,14 @@ dependencies = [
[[package]]
name = "pliego-css-parser"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
]
[[package]]
name = "pliego-css-source"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"libc",
"pliego-css-io",
@@ -342,7 +340,7 @@ dependencies = [
[[package]]
name = "pliego-css-theme"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
"sha2",
@@ -350,9 +348,7 @@ dependencies = [
[[package]]
name = "pliego-dom"
-version = "0.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf311f3addadbfed7653dbb2ab5360987f55d9e42d3f76f0a0bb0c85db53d7ef"
+version = "0.4.0-beta.1"
dependencies = [
"js-sys",
"pliego-reactive",
@@ -362,9 +358,7 @@ dependencies = [
[[package]]
name = "pliego-macros"
-version = "0.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9aa6e1c4e1fc0a45f4d19e0397bea933723b4328e7c977bb0d9ea3413136c9b"
+version = "0.4.0-beta.1"
dependencies = [
"proc-macro2",
"quote",
@@ -374,15 +368,11 @@ dependencies = [
[[package]]
name = "pliego-reactive"
-version = "0.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97562ecb0ec7d6165178d1a2a5403dcb084060d9252b1ca62b251d4fa75dc62d"
+version = "0.4.0-beta.1"
[[package]]
name = "pliego-resume"
-version = "0.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ae3bbaae6a955fc8a763eb75b07d8a43b0a4533533e7c50842657d8be80f07b"
+version = "0.4.0-beta.1"
dependencies = [
"pliego-dom",
"serde_json",
@@ -390,15 +380,14 @@ dependencies = [
[[package]]
name = "pliego-ssg"
-version = "0.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0358ddefc33312fed302f92d78c8ccf2b0e5384c7eadf5ef0b88964ae0856f06"
+version = "0.4.0-beta.1"
dependencies = [
"cap-fs-ext",
"cap-std",
"fs2",
"pliego-artifact",
"pliego-dom",
+ "serde",
"serde_json",
]
@@ -548,7 +537,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.2",
+ "syn 3.0.4",
]
[[package]]
@@ -594,9 +583,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "3.0.2"
+version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3"
+checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
dependencies = [
"proc-macro2",
"quote",
@@ -617,22 +606,22 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "2.0.19"
+version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "2.0.19"
+version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.2",
+ "syn 3.0.4",
]
[[package]]
diff --git a/integration-tests/pliegors-smoke/Cargo.toml b/integration-tests/pliegors-smoke/Cargo.toml
index d24bfd8..6207cfb 100644
--- a/integration-tests/pliegors-smoke/Cargo.toml
+++ b/integration-tests/pliegors-smoke/Cargo.toml
@@ -28,10 +28,10 @@ path = "src/bin/collector.rs"
[dependencies]
pliego-css = { path = "../../crates/pliego-css" }
pliego-css-source = { path = "../../crates/pliego-css-source" }
-pliego-dom = "=0.0.2"
-pliego-macros = "=0.0.2"
-pliego-resume = "=0.0.2"
-pliego-ssg = "=0.0.2"
+pliego-dom = { version = "=0.4.0-beta.1", path = "../../../pliegors/crates/pliego-dom" }
+pliego-macros = { version = "=0.4.0-beta.1", path = "../../../pliegors/crates/pliego-macros" }
+pliego-resume = { version = "=0.4.0-beta.1", path = "../../../pliegors/crates/pliego-resume" }
+pliego-ssg = { version = "=0.4.0-beta.1", path = "../../../pliegors/crates/pliego-ssg" }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
sha2 = "0.10"
diff --git a/integration-tests/pliegors-smoke/pliegors-contract.json b/integration-tests/pliegors-smoke/pliegors-contract.json
index cecd44e..074531d 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": "abb8653e75da4a5cb4dd9b51200114fbb1e760c7",
+ "sourceSha256": "2ba965b8d72831e3f452d91a5ba98d7297638fac661bcf1a2f7edb8ca1da68fa",
"surfaces": [
"Cargo.toml",
"Cargo.lock",
@@ -11,6 +11,7 @@
"crates/pliego-resume/**",
"crates/pliego-ssg/**",
"crates/pliego-starters/**",
- "crates/pliego-cli/**"
+ "crates/pliego-cli/**",
+ "schemas/pliego.product-topology.schema.json"
]
}
diff --git a/integration-tests/pliegors-smoke/src/bin/collector.rs b/integration-tests/pliegors-smoke/src/bin/collector.rs
index 17d6e5d..306f805 100644
--- a/integration-tests/pliegors-smoke/src/bin/collector.rs
+++ b/integration-tests/pliegors-smoke/src/bin/collector.rs
@@ -5,9 +5,8 @@ use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
-use pliegocss_pliegors_smoke::css_adapter::{
- GeneratedBundle, generate_css_inputs_from_inventory,
-};
+use pliego_css_source::ProductBundle;
+use pliegocss_pliegors_smoke::css_adapter::generate_css_inputs_from_inventory;
use pliegocss_pliegors_smoke::product::application_registry;
#[derive(Serialize)]
@@ -20,7 +19,7 @@ struct CollectorSummary<'a> {
style_sites: usize,
reachability_bytes: usize,
bundle_plan_bytes: usize,
- bundles: &'a [GeneratedBundle],
+ bundles: &'a [ProductBundle],
}
#[derive(Deserialize)]
diff --git a/integration-tests/pliegors-smoke/src/bin/ssg.rs b/integration-tests/pliegors-smoke/src/bin/ssg.rs
index 4691887..3bbe3f8 100644
--- a/integration-tests/pliegors-smoke/src/bin/ssg.rs
+++ b/integration-tests/pliegors-smoke/src/bin/ssg.rs
@@ -371,7 +371,7 @@ fn validate_plan(plan: &AssetPlan) -> Result<(), Box> {
|| plan.application_coverage != "adapter-attested-complete"
|| plan.style_id_format_version != 2
|| plan.class_name_format_version != 1
- || plan.theme_id_format_version != 2
+ || plan.theme_id_format_version != 3
|| !lower_hex(&plan.theme_id, 32)
|| plan.targets != "modern"
|| plan.format != "minified"
diff --git a/integration-tests/pliegors-smoke/src/css_adapter.rs b/integration-tests/pliegors-smoke/src/css_adapter.rs
index 0b486af..fc5f41b 100644
--- a/integration-tests/pliegors-smoke/src/css_adapter.rs
+++ b/integration-tests/pliegors-smoke/src/css_adapter.rs
@@ -1,32 +1,17 @@
//! PliegoCSS adapter generated from the framework-owned PliegoRS product registry.
-use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::path::Path;
-use pliego_css_source::{
- ApplicationComponent, ApplicationIsland, ApplicationRoute, ApplicationTopology,
- CollectedReachability,
-};
+use pliego_css_source::{CollectedReachability, ProductBundle, ProductTopology};
use pliego_ssg::ProductRegistry;
-use serde::Serialize;
-use sha2::{Digest, Sha256};
-
-/// One automatically derived physical CSS partition.
-#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct GeneratedBundle {
- pub id: String,
- pub sources: Vec,
- pub emit_theme: bool,
-}
/// Reachability and bundle-plan bytes derived from one immutable product registry.
#[derive(Debug)]
pub struct GeneratedCssInputs {
pub reachability: CollectedReachability,
pub bundle_plan: Vec,
- pub bundles: Vec,
+ pub bundles: Vec,
}
/// Failure to convert or collect one product registry.
@@ -35,12 +20,6 @@ pub struct AdapterError {
message: String,
}
-#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
-enum Root {
- Route(String),
- Island(String),
-}
-
/// Generates canonical reachability and an automatic source partition from one product registry.
pub fn generate_css_inputs(
registry: &ProductRegistry,
@@ -66,13 +45,13 @@ fn generate_css_inputs_internal(
project_root: &Path,
source_units: Option<&[String]>,
) -> Result {
- registry.validate().map_err(|error| invalid(error.to_string()))?;
- let topology = topology(registry, source_units);
- let reachability = topology
- .collect(project_root)
+ let topology_bytes = registry.to_topology_json().map_err(|error| invalid(error.to_string()))?;
+ let topology = ProductTopology::from_json(&topology_bytes)
+ .map_err(|error| invalid(error.to_string()))?;
+ let generated = topology
+ .collect_css_inputs(project_root, source_units)
.map_err(|error| invalid(error.to_string()))?;
- let bundles = partition(registry)?;
- let bundle_plan = render_bundle_plan(&bundles)?;
+ let (reachability, bundle_plan, bundles) = generated.into_parts();
Ok(GeneratedCssInputs {
reachability,
bundle_plan,
@@ -88,189 +67,6 @@ impl fmt::Display for AdapterError {
impl std::error::Error for AdapterError {}
-fn topology(registry: &ProductRegistry, inventory: Option<&[String]>) -> ApplicationTopology {
- let mut topology = ApplicationTopology::new();
- let mut source_roots = BTreeSet::new();
- for component in registry.components() {
- let mut application_component = ApplicationComponent::new(component.id());
- for source in component.source_units() {
- application_component = application_component.source_unit(source);
- let path = Path::new(source);
- let root = path
- .parent()
- .filter(|parent| !parent.as_os_str().is_empty())
- .and_then(Path::to_str)
- .unwrap_or(source)
- .replace('\\', "/");
- source_roots.insert(root);
- }
- topology = topology.component(application_component);
- }
- if let Some(inventory) = inventory {
- source_roots = inventory.iter().cloned().collect();
- }
- for source_root in source_roots {
- topology = topology.source_root(source_root);
- }
- for route in registry.routes() {
- let mut application_route = ApplicationRoute::new(route.id(), route.path());
- for component in route.components() {
- application_route = application_route.component(component);
- }
- topology = topology.route(application_route);
- }
- for island in registry.islands() {
- let mut application_island = ApplicationIsland::new(island.id(), island.name());
- for component in island.components() {
- application_island = application_island.component(component);
- }
- topology = topology.island(application_island);
- }
- topology
-}
-
-fn partition(registry: &ProductRegistry) -> Result, AdapterError> {
- let mut memberships = registry
- .components()
- .iter()
- .map(|component| (component.id(), BTreeSet::new()))
- .collect::>();
- for route in registry.routes() {
- for component in route.components() {
- memberships
- .get_mut(component.as_str())
- .ok_or_else(|| invalid(format!("unknown route component `{component}`")))?
- .insert(Root::Route(route.id().to_owned()));
- }
- }
- for island in registry.islands() {
- for component in island.components() {
- memberships
- .get_mut(component.as_str())
- .ok_or_else(|| invalid(format!("unknown island component `{component}`")))?
- .insert(Root::Island(island.id().to_owned()));
- }
- }
-
- let mut sources = BTreeMap::>::new();
- for component in registry.components() {
- let roots = memberships
- .get(component.id())
- .ok_or_else(|| invalid(format!("component `{}` has no membership", component.id())))?;
- for source in component.source_units() {
- sources
- .entry(source.clone())
- .or_default()
- .extend(roots.iter().cloned());
- }
- }
-
- let mut groups = BTreeMap::, BTreeSet>::new();
- for (source, roots) in sources {
- groups.entry(roots).or_default().insert(source);
- }
- let route_ids = registry
- .routes()
- .iter()
- .map(|route| route.id().to_owned())
- .collect::>();
- let mut bundles = BTreeMap::new();
- let mut universal = Vec::new();
- for (roots, sources) in groups {
- let id = bundle_id(&roots, &route_ids);
- if bundles.contains_key(&id) {
- return Err(invalid(format!("automatic bundle ID collision `{id}`")));
- }
- if route_ids
- .iter()
- .all(|route| roots.contains(&Root::Route(route.clone())))
- {
- universal.push(id.clone());
- }
- bundles.insert(
- id.clone(),
- GeneratedBundle {
- id,
- sources: sources.into_iter().collect(),
- emit_theme: false,
- },
- );
- }
- universal.sort();
- let theme_bundle = universal
- .first()
- .ok_or_else(|| invalid("automatic partition requires one bundle shared by every route"))?;
- bundles
- .get_mut(theme_bundle)
- .expect("selected theme bundle came from the bundle map")
- .emit_theme = true;
- Ok(bundles.into_values().collect())
-}
-
-fn bundle_id(roots: &BTreeSet, route_ids: &BTreeSet) -> String {
- if roots.is_empty() {
- return "unreachable".to_owned();
- }
- let routes = roots
- .iter()
- .filter_map(|root| match root {
- Root::Route(id) => Some(id.as_str()),
- Root::Island(_) => None,
- })
- .collect::>();
- let islands = roots
- .iter()
- .filter_map(|root| match root {
- Root::Island(id) => Some(id.as_str()),
- Root::Route(_) => None,
- })
- .collect::>();
- if islands.is_empty()
- && routes.len() == route_ids.len()
- && routes.iter().all(|route| route_ids.contains(*route))
- {
- return "shared".to_owned();
- }
- if routes.len() == 1 && islands.is_empty() {
- return format!("route-{}", routes[0]);
- }
- if routes.is_empty() && islands.len() == 1 {
- return format!("island-{}", islands[0]);
- }
- let mut digest = Sha256::new();
- for root in roots {
- match root {
- Root::Route(id) => digest.update(format!("route\0{id}\0")),
- Root::Island(id) => digest.update(format!("island\0{id}\0")),
- }
- }
- format!("shared-{}", &format!("{:x}", digest.finalize())[..12])
-}
-
-fn render_bundle_plan(bundles: &[GeneratedBundle]) -> Result, AdapterError> {
- let mut output = String::from(
- "schema = 1\ntargets = \"modern\"\nformat = \"minified\"\n\n[theme]\nkind = \"seed\"\n",
- );
- for bundle in bundles {
- output.push_str("\n[bundles.");
- output.push_str(&bundle.id);
- output.push_str("]\nsources = [");
- for (index, source) in bundle.sources.iter().enumerate() {
- if index > 0 {
- output.push_str(", ");
- }
- output.push_str(
- &serde_json::to_string(source)
- .map_err(|error| invalid(format!("cannot encode bundle source: {error}")))?,
- );
- }
- output.push_str("]\nemit-theme = ");
- output.push_str(if bundle.emit_theme { "true" } else { "false" });
- output.push('\n');
- }
- Ok(output.into_bytes())
-}
-
fn invalid(message: impl Into) -> AdapterError {
AdapterError {
message: message.into(),
@@ -279,6 +75,8 @@ fn invalid(message: impl Into) -> AdapterError {
#[cfg(test)]
mod tests {
+ use std::collections::BTreeSet;
+
use super::*;
use crate::product::application_registry;
@@ -295,7 +93,7 @@ mod tests {
generated
.bundles
.iter()
- .map(|bundle| bundle.id.as_str())
+ .map(ProductBundle::id)
.collect::>(),
[
"island-visit-counter",
@@ -309,8 +107,8 @@ mod tests {
generated
.bundles
.iter()
- .filter(|bundle| bundle.emit_theme)
- .map(|bundle| bundle.id.as_str())
+ .filter(|bundle| bundle.emits_theme())
+ .map(ProductBundle::id)
.collect::>(),
["shared"]
);
diff --git a/integration-tests/pliegors-smoke/src/product.rs b/integration-tests/pliegors-smoke/src/product.rs
index 86f4cb3..deeacfb 100644
--- a/integration-tests/pliegors-smoke/src/product.rs
+++ b/integration-tests/pliegors-smoke/src/product.rs
@@ -62,4 +62,12 @@ mod tests {
&& component.source_units()[0].starts_with("src/styles/")
}));
}
+
+ #[test]
+ fn product_registry_emits_the_native_topology_seam() {
+ let bytes = application_registry().to_topology_json().unwrap();
+ let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
+ assert_eq!(value["schema"], "pliegors-product-topology/1");
+ assert_eq!(value["routes"][1]["islands"][0], VISIT_COUNTER_ID);
+ }
}
diff --git a/integration-tests/public-api-smoke/Cargo.lock b/integration-tests/public-api-smoke/Cargo.lock
index 55abc0e..7915bb6 100644
--- a/integration-tests/public-api-smoke/Cargo.lock
+++ b/integration-tests/public-api-smoke/Cargo.lock
@@ -371,7 +371,7 @@ dependencies = [
[[package]]
name = "pliego-css"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
"pliego-css-macros",
@@ -379,7 +379,7 @@ dependencies = [
[[package]]
name = "pliego-css-agent"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-build",
"pliego-css-config",
@@ -390,7 +390,7 @@ dependencies = [
[[package]]
name = "pliego-css-build"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"cssparser",
"lightningcss",
@@ -405,7 +405,7 @@ dependencies = [
[[package]]
name = "pliego-css-compiler"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
"pliego-css-parser",
@@ -415,7 +415,7 @@ dependencies = [
[[package]]
name = "pliego-css-config"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"cssparser",
"libc",
@@ -431,18 +431,18 @@ dependencies = [
[[package]]
name = "pliego-css-io"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"libc",
]
[[package]]
name = "pliego-css-ir"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
[[package]]
name = "pliego-css-macros"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-compiler",
"pliego-css-ir",
@@ -455,7 +455,7 @@ dependencies = [
[[package]]
name = "pliego-css-ownership"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"serde",
"serde_json",
@@ -464,7 +464,7 @@ dependencies = [
[[package]]
name = "pliego-css-parser"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
]
@@ -483,7 +483,7 @@ dependencies = [
[[package]]
name = "pliego-css-publication"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"fs2",
"libc",
@@ -492,7 +492,7 @@ dependencies = [
[[package]]
name = "pliego-css-source"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"libc",
"pliego-css-io",
@@ -505,7 +505,7 @@ dependencies = [
[[package]]
name = "pliego-css-theme"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-ir",
"sha2",
@@ -513,7 +513,7 @@ dependencies = [
[[package]]
name = "pliego-css-usage"
-version = "0.1.0-rc.2"
+version = "0.1.0-rc.3"
dependencies = [
"pliego-css-build",
"pliego-css-compiler",
diff --git a/integration-tests/public-api-smoke/Cargo.toml b/integration-tests/public-api-smoke/Cargo.toml
index 362a2fa..3d04592 100644
--- a/integration-tests/public-api-smoke/Cargo.toml
+++ b/integration-tests/public-api-smoke/Cargo.toml
@@ -6,14 +6,14 @@ rust-version = "1.85"
publish = false
[dependencies]
-css = { package = "pliego-css", path = "../../crates/pliego-css", version = "=0.1.0-rc.2" }
-pliego-css-agent = { path = "../../crates/pliego-css-agent", version = "=0.1.0-rc.2" }
-pliego-css-ownership = { path = "../../crates/pliego-css-ownership", version = "=0.1.0-rc.2" }
-pliego-css-source = { path = "../../crates/pliego-css-source", version = "=0.1.0-rc.2" }
-pliego-css-usage = { path = "../../crates/pliego-css-usage", version = "=0.1.0-rc.2" }
+css = { package = "pliego-css", path = "../../crates/pliego-css", version = "=0.1.0-rc.3" }
+pliego-css-agent = { path = "../../crates/pliego-css-agent", version = "=0.1.0-rc.3" }
+pliego-css-ownership = { path = "../../crates/pliego-css-ownership", version = "=0.1.0-rc.3" }
+pliego-css-source = { path = "../../crates/pliego-css-source", version = "=0.1.0-rc.3" }
+pliego-css-usage = { path = "../../crates/pliego-css-usage", version = "=0.1.0-rc.3" }
[build-dependencies]
-pliego-css-build = { path = "../../crates/pliego-css-build", version = "=0.1.0-rc.2" }
+pliego-css-build = { path = "../../crates/pliego-css-build", version = "=0.1.0-rc.3" }
[workspace]
resolver = "2"
diff --git a/integration-tests/public-api-smoke/src/main.rs b/integration-tests/public-api-smoke/src/main.rs
index 8f80f08..99098a9 100644
--- a/integration-tests/public-api-smoke/src/main.rs
+++ b/integration-tests/public-api-smoke/src/main.rs
@@ -32,8 +32,8 @@ use pliego_css_source::{
MigrationDependencyKind, MigrationDependencyResolution, MigrationDisposition,
MigrationInventory, MigrationInventoryError, MigrationPreflightReliance, MigrationProject,
MigrationProjectAuxiliary, MigrationProjectConsumer, MigrationProjectInventory,
- MigrationProjectSource, MigrationSourceKind, discover_migration_project,
- inspect_utility_format, scan_source_named,
+ MigrationProjectSource, MigrationSourceKind, PRODUCT_TOPOLOGY_SCHEMA, ProductBundle,
+ ProductTopology, discover_migration_project, inspect_utility_format, scan_source_named,
inventory_migration_auxiliary_file,
inventory_migration_auxiliary_source, inventory_migration_consumer_file,
inventory_migration_consumer_source, inventory_migration_file, inventory_migration_source,
@@ -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",
@@ -461,6 +461,31 @@ fn exercise_application_collector_surface() {
}));
}
+fn exercise_product_topology_surface() {
+ let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
+ let document = format!(
+ r#"{{"schema":"{PRODUCT_TOPOLOGY_SCHEMA}","components":[{{"id":"public-api-smoke::main","sourceUnits":["src/main.rs"]}}],"routes":[{{"id":"home","path":"/","components":["public-api-smoke::main"],"islands":[]}}],"islands":[]}}"#,
+ );
+ let topology = ProductTopology::from_json(document.as_bytes())
+ .expect("public product topology must decode");
+ let generated = topology
+ .collect_css_inputs(root, None)
+ .expect("public product topology must collect");
+ assert!(generated.reachability().as_bytes().ends_with(b"\n"));
+ assert!(generated.bundle_plan().ends_with(b"\n"));
+ assert_eq!(
+ generated
+ .bundles()
+ .iter()
+ .map(ProductBundle::id)
+ .collect::>(),
+ ["shared"]
+ );
+ assert!(generated.bundles()[0].emits_theme());
+ assert_eq!(generated.bundles()[0].sources(), ["src/main.rs"]);
+ let _parts = generated.into_parts();
+}
+
fn exercise_source_formatting_surface() {
let inspection = inspect_utility_format(&[], |value| Ok::<_, String>(value.to_owned()))
.expect("empty source-format inspection");
@@ -483,6 +508,7 @@ fn main() {
exercise_ownership_adapter_surface();
exercise_usage_adapter_surface();
exercise_application_collector_surface();
+ exercise_product_topology_surface();
exercise_source_formatting_surface();
exercise_typed_parse_diagnostic_surface();
require_style_traits::`,
+ );
+}
+
+function normalizeComputed(value) {
+ return value
+ .replaceAll(/\s+/gu, " ")
+ .replaceAll("flex-end", "end")
+ .trim();
+}
+
+async function browserSnapshot(browser, document, css, viewport, properties) {
+ const page = await browser.newPage({ viewport });
+ const consoleErrors = [];
+ const pageErrors = [];
+ page.on("console", (message) => {
+ if (message.type() === "error") consoleErrors.push(message.text());
+ });
+ page.on("pageerror", (error) => pageErrors.push(error.message));
+ try {
+ await page.setContent(injectCss(document, css), { waitUntil: "load" });
+ const dom = await page.evaluate(() => {
+ function project(node) {
+ if (node.nodeType === Node.TEXT_NODE) {
+ const text = node.textContent.replace(/\s+/gu, " ").trim();
+ return text ? { text } : null;
+ }
+ if (node.nodeType !== Node.ELEMENT_NODE) return null;
+ const attributes = [...node.attributes]
+ .filter((attribute) => attribute.name !== "class" && attribute.name !== "data-g6-node")
+ .map((attribute) => [attribute.name, attribute.value])
+ .sort(([left], [right]) => left.localeCompare(right, "en"));
+ return {
+ tag: node.tagName.toLowerCase(),
+ attributes,
+ children: [...node.childNodes].map(project).filter(Boolean),
+ };
+ }
+ return project(document.body);
+ });
+ const aria = await page.locator("body").ariaSnapshot();
+ const nodes = await page.locator("[data-g6-node]").evaluateAll(
+ (elements, frozenProperties) =>
+ elements.map((element) => {
+ const style = getComputedStyle(element);
+ const rectangle = element.getBoundingClientRect();
+ return {
+ id: element.getAttribute("data-g6-node"),
+ computed: Object.fromEntries(
+ frozenProperties.map((property) => [property, style.getPropertyValue(property)]),
+ ),
+ geometry: {
+ x: rectangle.x,
+ y: rectangle.y,
+ width: rectangle.width,
+ height: rectangle.height,
+ },
+ };
+ }),
+ properties,
+ );
+ if (consoleErrors.length || pageErrors.length) {
+ fail(`browser emitted errors: ${JSON.stringify({ consoleErrors, pageErrors })}`);
+ }
+ return { dom, aria, nodes };
+ } finally {
+ await page.close();
+ }
+}
+
+function compareSnapshots(project, baseline, candidate, maximumGeometryDelta) {
+ if (JSON.stringify(baseline.dom) !== JSON.stringify(candidate.dom)) {
+ fail(`${project.id} DOM semantics changed`);
+ }
+ if (baseline.aria !== candidate.aria) fail(`${project.id} ARIA snapshot changed`);
+ if (baseline.nodes.length !== candidate.nodes.length || baseline.nodes.length === 0) {
+ fail(`${project.id} class-bearing node inventory changed`);
+ }
+ let maximumObservedGeometryDelta = 0;
+ for (let index = 0; index < baseline.nodes.length; index += 1) {
+ const left = baseline.nodes[index];
+ const right = candidate.nodes[index];
+ if (left.id !== right.id) fail(`${project.id} node identity changed at ${index}`);
+ for (const property of Object.keys(left.computed)) {
+ const expected = normalizeComputed(left.computed[property]);
+ const actual = normalizeComputed(right.computed[property]);
+ if (expected !== actual) {
+ fail(
+ `${project.id} computed ${property} changed on ${left.id}: ${JSON.stringify(expected)} != ${JSON.stringify(actual)}`,
+ );
+ }
+ }
+ for (const coordinate of ["x", "y", "width", "height"]) {
+ const delta = Math.abs(left.geometry[coordinate] - right.geometry[coordinate]);
+ maximumObservedGeometryDelta = Math.max(maximumObservedGeometryDelta, delta);
+ if (delta > maximumGeometryDelta) {
+ fail(`${project.id} geometry ${coordinate} changed by ${delta} CSS px on ${left.id}`);
+ }
+ }
+ }
+ return {
+ domEquivalent: true,
+ ariaEquivalent: true,
+ computedStyleEquivalent: true,
+ geometryEquivalent: true,
+ nodes: baseline.nodes.length,
+ maximumObservedGeometryDeltaCssPx: maximumObservedGeometryDelta,
+ ariaSha256: sha256(Buffer.from(baseline.aria)),
+ domSha256: sha256(Buffer.from(JSON.stringify(baseline.dom))),
+ };
+}
+
+function instant(date = new Date()) {
+ return date.toISOString().replace(/\.\d{3}Z$/u, "Z");
+}
+
+const options = parseOptions(process.argv.slice(2));
+const contract = loadContract();
+const host = hostFor(contract.authority, options.browser, options.host);
+options.output ??= resolve(
+ repositoryRoot,
+ "target",
+ "adapter-coexistence-certification",
+ `${host.id}.local.json`,
+);
+const outputChild = relative(repositoryRoot, options.output);
+if (
+ !outputChild ||
+ outputChild === ".." ||
+ outputChild.startsWith(`..${sep}`) ||
+ isAbsolute(outputChild) ||
+ !options.output.endsWith(".json")
+) {
+ fail("--output must be a .json path inside the repository");
+}
+
+const source = gitState(options.requireClean);
+const compiler = prepareCompiler();
+const resetBytes = readFileSync(resolve(repositoryRoot, contract.authority.sharedReset.path));
+const workRoot = resolve(
+ repositoryRoot,
+ "target",
+ "adapter-coexistence-certification",
+ `work-${process.pid}-${Date.now()}`,
+);
+mkdirSync(workRoot, { recursive: true });
+
+const browser = await chromium.launch({ headless: true });
+const startedAt = instant();
+const projects = [];
+try {
+ for (const project of contract.corpus.projects) {
+ process.stdout.write(`[adapter-coexistence] ${project.id}\n`);
+ const stage = join(workRoot, project.id);
+ mkdirSync(stage, { recursive: true });
+ const document = decorateClassNodes(renderProjectDocument(project), project.id);
+ writeFileSync(join(stage, "index.html"), document);
+ const profile = profileById(contract.authority, project.tailwindProfile);
+ const tailwind = compileTailwind(profile, stage);
+ const sourceInventory = inventoryTailwindSource(compiler, stage, profile);
+ const audit = auditTailwind(compiler, stage);
+ const pliego = compilePliego(compiler, stage, document);
+ const baselineSource = {
+ document,
+ css: Buffer.concat([resetBytes, tailwind.output]),
+ };
+ let baselineBuild = null;
+ if (project.adapter === "vite") {
+ writeFileSync(join(stage, "pliego.css"), Buffer.alloc(0));
+ baselineBuild = await viteBuildSnapshot(stage, "dist-baseline");
+ }
+ const migration = prepareMigration(compiler, stage, document, pliego);
+ let candidateBuild = null;
+ if (project.adapter === "vite") candidateBuild = await viteBuildSnapshot(stage, "dist-candidate");
+ const baselineDocument = baselineBuild?.html ?? baselineSource.document;
+ const baselineCss = baselineBuild
+ ? Buffer.concat([resetBytes, baselineBuild.css])
+ : baselineSource.css;
+ const candidateDocument = candidateBuild?.html ?? migration.after.document.toString("utf8");
+ const candidateCss = candidateBuild
+ ? Buffer.concat([resetBytes, candidateBuild.css])
+ : Buffer.concat([resetBytes, migration.after.tailwind, migration.after.pliego]);
+ const baselineSnapshot = await browserSnapshot(
+ browser,
+ baselineDocument,
+ baselineCss,
+ project.viewport,
+ contract.authority.comparison.computedProperties,
+ );
+ const candidateSnapshot = await browserSnapshot(
+ browser,
+ candidateDocument,
+ candidateCss,
+ project.viewport,
+ contract.authority.comparison.computedProperties,
+ );
+ const equivalence = compareSnapshots(
+ project,
+ baselineSnapshot,
+ candidateSnapshot,
+ contract.authority.comparison.maximumLayoutGeometryDeltaCssPx,
+ );
+ const rollback = rollbackMigration(compiler, stage, migration);
+ projects.push({
+ id: project.id,
+ adapter: project.adapter,
+ tailwindProfile: project.tailwindProfile,
+ viewport: project.viewport,
+ source: {
+ documentSha256: sha256(Buffer.from(document)),
+ classGroups: extractStaticClassGroups(document, project.id).groups.length,
+ },
+ tailwind: {
+ version: profile.version,
+ inputSha256: tailwind.inputSha256,
+ outputSha256: tailwind.outputSha256,
+ outputBytes: tailwind.outputBytes,
+ sourceInventory,
+ audit,
+ },
+ pliego: {
+ outputSha256: pliego.cssSha256,
+ outputBytes: pliego.css.length,
+ manifestSha256: pliego.manifestSha256,
+ styleGroups: pliego.styleGroups,
+ },
+ migration: {
+ mode: contract.authority.migration.mode,
+ replacements: migration.rewrite.replacements,
+ groupSha256: migration.groupSha256,
+ receiptSha256: migration.receiptSha256,
+ receiptSchemaVersion: migration.receipt.schemaVersion,
+ beforeDocumentSha256: sha256(migration.before.document),
+ afterDocumentSha256: sha256(migration.after.document),
+ tailwindRetained: sha256(migration.before.tailwind) === sha256(migration.after.tailwind),
+ rollback,
+ },
+ vite:
+ project.adapter === "vite"
+ ? {
+ version: contract.authority.adapters.find((entry) => entry.id === "vite").version,
+ baseline: {
+ htmlSha256: baselineBuild.htmlSha256,
+ cssSha256: baselineBuild.cssSha256,
+ assets: baselineBuild.assets,
+ },
+ candidate: {
+ htmlSha256: candidateBuild.htmlSha256,
+ cssSha256: candidateBuild.cssSha256,
+ assets: candidateBuild.assets,
+ },
+ }
+ : null,
+ equivalence,
+ passed: true,
+ });
+ }
+} finally {
+ await browser.close();
+}
+
+const completedAt = instant();
+const adapterCounts = Object.fromEntries(
+ contract.authority.adapters.map((adapter) => [
+ adapter.id,
+ projects.filter((project) => project.adapter === adapter.id).length,
+ ]),
+);
+const profileCounts = Object.fromEntries(
+ contract.authority.tailwindProfiles.map((profile) => [
+ profile.id,
+ projects.filter((project) => project.tailwindProfile === profile.id).length,
+ ]),
+);
+const evidence = {
+ schemaVersion: 1,
+ kind: "pliegocss-adapter-coexistence-host-evidence",
+ result: projects.length >= contract.authority.corpus.minimumProjects ? "pass" : "fail",
+ startedAt,
+ completedAt,
+ source,
+ authority: {
+ path: relative(repositoryRoot, authorityPath).replaceAll("\\", "/"),
+ sha256: contract.authoritySha256,
+ corpusPath: relative(repositoryRoot, contract.corpusPath).replaceAll("\\", "/"),
+ corpusSha256: contract.corpusSha256,
+ },
+ host: {
+ ...host,
+ browserVersion: browser.version(),
+ playwright: readJson(resolve(repositoryRoot, "node_modules", "playwright", "package.json")).version,
+ },
+ compiler,
+ summary: {
+ projects: projects.length,
+ passed: projects.filter((project) => project.passed).length,
+ adapters: adapterCounts,
+ tailwindProfiles: profileCounts,
+ domEquivalent: projects.filter((project) => project.equivalence.domEquivalent).length,
+ ariaEquivalent: projects.filter((project) => project.equivalence.ariaEquivalent).length,
+ computedStyleEquivalent: projects.filter(
+ (project) => project.equivalence.computedStyleEquivalent,
+ ).length,
+ geometryEquivalent: projects.filter((project) => project.equivalence.geometryEquivalent).length,
+ exactRollbacks: projects.filter((project) => project.migration.rollback.exact).length,
+ },
+ projects,
+ claimBoundary: contract.authority.claimBoundary,
+};
+if (evidence.result !== "pass") fail("project floor was not met");
+mkdirSync(dirname(options.output), { recursive: true });
+writeFileSync(options.output, `${JSON.stringify(evidence, null, 2)}\n`);
+rmSync(workRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
+process.stdout.write(
+ `${JSON.stringify(
+ {
+ passed: true,
+ host: host.id,
+ projects: projects.length,
+ adapters: adapterCounts,
+ tailwindProfiles: profileCounts,
+ output: relative(repositoryRoot, options.output).replaceAll("\\", "/"),
+ },
+ null,
+ 2,
+ )}\n`,
+);
diff --git a/scripts/adapter-coexistence-v1.mjs b/scripts/adapter-coexistence-v1.mjs
new file mode 100644
index 0000000..14bda8c
--- /dev/null
+++ b/scripts/adapter-coexistence-v1.mjs
@@ -0,0 +1,154 @@
+import { createHash } from "node:crypto";
+import { readFileSync } from "node:fs";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+export const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+export const authorityPath = resolve(
+ repositoryRoot,
+ "benchmarks",
+ "adapter-coexistence-v1",
+ "authority.json",
+);
+
+export function sha256(bytes) {
+ return createHash("sha256").update(bytes).digest("hex");
+}
+
+export function readJson(path) {
+ return JSON.parse(readFileSync(path, "utf8"));
+}
+
+export function loadContract() {
+ const authorityBytes = readFileSync(authorityPath);
+ const authority = JSON.parse(authorityBytes.toString("utf8"));
+ const corpusPath = resolve(repositoryRoot, authority.corpus.path);
+ const corpusBytes = readFileSync(corpusPath);
+ if (sha256(corpusBytes) !== authority.corpus.sha256) {
+ throw new Error("adapter coexistence corpus hash drifted");
+ }
+ return {
+ authority,
+ authorityBytes,
+ authoritySha256: sha256(authorityBytes),
+ corpus: JSON.parse(corpusBytes.toString("utf8")),
+ corpusBytes,
+ corpusPath,
+ corpusSha256: sha256(corpusBytes),
+ };
+}
+
+export function renderProjectDocument(project) {
+ return `\n\n\n\n\n${escapeText(project.title)} \n\n\n\n${project.body}\n\n`;
+}
+
+function escapeText(value) {
+ return value
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">");
+}
+
+const classAttribute = /\bclass="([^"]+)"/gu;
+
+export function extractStaticClassGroups(document, role = "document") {
+ for (const forbidden of [
+ /\bclassName\s*=/u,
+ /\bclass\s*=\s*'/u,
+ /(?:^|\s)(?::class|v-bind:class|\[class\]|class:list)\s*=/u,
+ /\$\{|\{\{|<%|#\{/u,
+ /<(?:script|style)\b/iu,
+ ]) {
+ if (forbidden.test(document)) {
+ throw new Error(`${role} contains unsupported or dynamic class syntax ${forbidden}`);
+ }
+ }
+ const matches = [...document.matchAll(classAttribute)];
+ const rawClassMarkers = document.match(/\bclass\s*=/gu)?.length ?? 0;
+ if (matches.length === 0 || matches.length !== rawClassMarkers) {
+ throw new Error(`${role} must use only non-empty double-quoted class attributes`);
+ }
+ const groups = matches.map((match) => {
+ const normalized = match[1].trim().split(/\s+/u).join(" ");
+ if (normalized !== match[1]) {
+ throw new Error(`${role} class groups must already use canonical single spacing`);
+ }
+ return normalized;
+ });
+ return { groups, uniqueGroups: [...new Set(groups)] };
+}
+
+export function decorateClassNodes(document, role = "document") {
+ let index = 0;
+ const decorated = document.replace(classAttribute, (attribute) => {
+ const node = `g6-node-${String(index).padStart(3, "0")}`;
+ index += 1;
+ return `data-g6-node="${node}" ${attribute}`;
+ });
+ if (index !== extractStaticClassGroups(document, role).groups.length) {
+ throw new Error(`${role} class decoration count drifted`);
+ }
+ return decorated;
+}
+
+export function rewriteStaticClassGroups(document, manifest, role = "document") {
+ const mappings = new Map();
+ for (const style of manifest.styles ?? []) {
+ if (typeof style.className !== "string" || !style.className.startsWith("pc_")) {
+ throw new Error(`${role} manifest contains an invalid generated class`);
+ }
+ for (const origin of style.origins ?? []) {
+ if (typeof origin.source !== "string") continue;
+ const previous = mappings.get(origin.source);
+ if (previous && previous !== style.className) {
+ throw new Error(`${role} maps one source class group to multiple generated classes`);
+ }
+ mappings.set(origin.source, style.className);
+ }
+ }
+ let replacements = 0;
+ const rewritten = document.replace(classAttribute, (_, group) => {
+ const generated = mappings.get(group);
+ if (!generated) {
+ throw new Error(`${role} manifest has no generated class for ${JSON.stringify(group)}`);
+ }
+ replacements += 1;
+ return `class="${generated}"`;
+ });
+ const expected = extractStaticClassGroups(document, role).groups.length;
+ if (replacements !== expected) throw new Error(`${role} did not rewrite every class group`);
+ return { document: rewritten, mappings, replacements };
+}
+
+export function exactKeys(value, expected, role) {
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
+ throw new Error(`${role} must be an object`);
+ }
+ const actual = Object.keys(value).sort();
+ const wanted = [...expected].sort();
+ if (JSON.stringify(actual) !== JSON.stringify(wanted)) {
+ throw new Error(`${role} keys drifted: ${actual.join(", ")}`);
+ }
+}
+
+export function profileById(authority, id) {
+ const profile = authority.tailwindProfiles.find((entry) => entry.id === id);
+ if (!profile) throw new Error(`unknown Tailwind profile ${id}`);
+ return profile;
+}
+
+export function hostFor(authority, browser, requestedId) {
+ const host = requestedId
+ ? authority.hosts.find((entry) => entry.id === requestedId)
+ : authority.hosts.find(
+ (entry) =>
+ entry.browser === browser && entry.os === process.platform && entry.arch === process.arch,
+ );
+ if (!host) throw new Error("current host is outside the adapter coexistence authority");
+ if (host.browser !== browser || host.os !== process.platform || host.arch !== process.arch) {
+ throw new Error(
+ `host ${host.id} requires ${host.os}/${host.arch}/${host.browser}, found ${process.platform}/${process.arch}/${browser}`,
+ );
+ }
+ return host;
+}
diff --git a/scripts/adapter-coexistence-v1.test.mjs b/scripts/adapter-coexistence-v1.test.mjs
new file mode 100644
index 0000000..137eebb
--- /dev/null
+++ b/scripts/adapter-coexistence-v1.test.mjs
@@ -0,0 +1,72 @@
+import {
+ decorateClassNodes,
+ extractStaticClassGroups,
+ rewriteStaticClassGroups,
+} from "./adapter-coexistence-v1.mjs";
+
+function expectFailure(action, fragment) {
+ try {
+ action();
+ } catch (error) {
+ if (error.message.includes(fragment)) return;
+ throw error;
+ }
+ throw new Error(`expected failure containing ${JSON.stringify(fragment)}`);
+}
+
+const source = '';
+const inventory = extractStaticClassGroups(source, "contract-test");
+if (
+ JSON.stringify(inventory.groups) !== JSON.stringify(["flex gap-4", "px-4 py-2"]) ||
+ !decorateClassNodes(source).includes('data-g6-node="g6-node-001"')
+) {
+ throw new Error("static class extraction or decoration drifted");
+}
+const manifest = {
+ styles: [
+ { className: "pc_one", origins: [{ source: "flex gap-4" }] },
+ { className: "pc_two", origins: [{ source: "px-4 py-2" }] },
+ ],
+};
+const rewritten = rewriteStaticClassGroups(source, manifest, "contract-test");
+if (
+ rewritten.replacements !== 2 ||
+ rewritten.document !== ''
+) {
+ throw new Error("static manifest-authorized rewrite drifted");
+}
+
+for (const dynamic of [
+ '',
+ "",
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+]) {
+ expectFailure(() => extractStaticClassGroups(dynamic, "dynamic-test"), "unsupported or dynamic");
+}
+expectFailure(
+ () => rewriteStaticClassGroups(source, { styles: manifest.styles.slice(0, 1) }, "missing-test"),
+ "manifest has no generated class",
+);
+expectFailure(
+ () =>
+ rewriteStaticClassGroups(
+ source,
+ {
+ styles: [
+ ...manifest.styles,
+ { className: "pc_other", origins: [{ source: "flex gap-4" }] },
+ ],
+ },
+ "ambiguous-test",
+ ),
+ "multiple generated classes",
+);
+
+process.stdout.write("adapter coexistence static protocol contract: pass\n");
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/build-repository-distribution.mjs b/scripts/build-repository-distribution.mjs
new file mode 100644
index 0000000..ab68ba1
--- /dev/null
+++ b/scripts/build-repository-distribution.mjs
@@ -0,0 +1,314 @@
+import { spawnSync } from "node:child_process";
+import {
+ chmodSync,
+ copyFileSync,
+ existsSync,
+ mkdirSync,
+ readFileSync,
+ readdirSync,
+ rmSync,
+ statSync,
+ writeFileSync,
+} from "node:fs";
+import { basename, resolve } from "node:path";
+import {
+ ROOT,
+ assertChildPath,
+ digest,
+ exactKeys,
+ fail,
+ fileSha256,
+ readJson,
+ repositoryVersion,
+ safeFilename,
+ validateBinary,
+ validateCycloneDx,
+ validatePackageManifest,
+ validatePolicy,
+ validateSource,
+} from "./repository-distribution.mjs";
+
+function parse(args) {
+ const value = { allowDirty: false };
+ for (const arg of args) {
+ if (arg === "--") continue;
+ if (arg === "--allow-dirty") {
+ value.allowDirty = true;
+ continue;
+ }
+ const match = /^--([a-z-]+)=(.+)$/u.exec(arg);
+ if (!match || Object.hasOwn(value, match[1])) fail(`invalid option: ${arg}`);
+ value[match[1]] = match[2];
+ }
+ return value;
+}
+
+function run(command, args, { cwd = ROOT, capture = false } = {}) {
+ const executable =
+ process.platform === "win32" && command === "pnpm"
+ ? process.env.ComSpec ?? "cmd.exe"
+ : command;
+ const commandArgs =
+ process.platform === "win32" && command === "pnpm"
+ ? ["/d", "/s", "/c", command, ...args]
+ : args;
+ const result = spawnSync(executable, commandArgs, {
+ cwd,
+ encoding: "utf8",
+ stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
+ windowsHide: true,
+ });
+ if (result.error || result.status !== 0) {
+ const details = capture ? `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() : "";
+ fail(
+ `${command} ${args.join(" ")} failed: ${result.error?.message ?? (details || result.status)}`,
+ );
+ }
+ return result.stdout?.trim() ?? "";
+}
+
+function safeOutput(path) {
+ return assertChildPath(resolve(ROOT, "target"), path, "distribution output");
+}
+
+function artifact(path, kind, extra = {}) {
+ return {
+ kind,
+ file: basename(path),
+ bytes: statSync(path).size,
+ sha256: fileSha256(path),
+ ...extra,
+ };
+}
+
+const options = parse(process.argv.slice(2));
+if (!options.input || !options.output) fail("--input and --output are required");
+const inputRoot = resolve(ROOT, options.input);
+const outputRoot = safeOutput(resolve(ROOT, options.output));
+const dirty = run("git", ["status", "--porcelain=v1"], { capture: true });
+if (dirty && !options.allowDirty) fail("distribution build requires a clean worktree");
+const version = repositoryVersion();
+const source = {
+ commit: options["source-commit"] ?? run("git", ["rev-parse", "HEAD"], { capture: true }),
+ gitTree:
+ options["git-tree"] ??
+ run("git", ["show", "-s", "--format=%T", "HEAD"], { capture: true }),
+ tag: options.tag ?? null,
+};
+validateSource(source);
+if (source.tag !== null && source.tag !== `v${version}`) {
+ fail(`source tag ${source.tag} does not match package version ${version}`);
+}
+const policy = validatePolicy();
+validatePackageManifest(undefined, policy);
+
+rmSync(outputRoot, { recursive: true, force: true });
+const releaseRoot = resolve(outputRoot, "release-assets");
+const stageRoot = resolve(outputRoot, "stage");
+mkdirSync(releaseRoot, { recursive: true });
+mkdirSync(stageRoot, { recursive: true });
+
+const nativeInputs = [];
+for (const target of policy.targets) {
+ const root = resolve(inputRoot, `native-${target.id}`);
+ const receiptPath = resolve(root, "native.json");
+ if (!existsSync(receiptPath)) fail(`native receipt missing for ${target.id}`);
+ const receipt = readJson(receiptPath, `${target.id} native receipt`);
+ exactKeys(
+ receipt,
+ ["schemaVersion", "kind", "version", "source", "target", "binaries", "sboms"],
+ `${target.id} native receipt`,
+ );
+ if (
+ receipt.schemaVersion !== 1 ||
+ receipt.kind !== "pliegocss-native-distribution-input" ||
+ receipt.version !== version ||
+ receipt.target.id !== target.id
+ ) {
+ fail(`${target.id} native receipt identity drifted`);
+ }
+ exactKeys(receipt.target, ["id", "os", "arch", "format"], `${target.id} receipt target`);
+ const expectedTarget = {
+ id: target.id,
+ os: target.os,
+ arch: target.arch,
+ format: target.format,
+ };
+ if (JSON.stringify(receipt.target) !== JSON.stringify(expectedTarget)) {
+ fail(`${target.id} native receipt target drifted`);
+ }
+ validateSource(receipt.source);
+ if (JSON.stringify(receipt.source) !== JSON.stringify(source)) {
+ fail(`${target.id} native input is not bound to the aggregate source`);
+ }
+ if (!Array.isArray(receipt.binaries) || receipt.binaries.length !== policy.binaries.length) {
+ fail(`${target.id} native receipt binary inventory drifted`);
+ }
+ for (const [index, binary] of receipt.binaries.entries()) {
+ exactKeys(binary, ["name", "file", "bytes", "sha256"], `${target.id} binary receipt`);
+ const expectedName = policy.binaries[index];
+ const expectedFile = `${expectedName}${target.extension}`;
+ if (
+ binary.name !== expectedName ||
+ binary.file !== expectedFile ||
+ !Number.isSafeInteger(binary.bytes) ||
+ binary.bytes <= 0
+ ) {
+ fail(`${target.id} binary receipt inventory drifted`);
+ }
+ safeFilename(binary.file, `${target.id} binary file`);
+ digest(binary.sha256, `${target.id}/${binary.file}.sha256`);
+ const path = resolve(root, binary.file);
+ const details = validateBinary(path, target);
+ if (details.sha256 !== binary.sha256 || details.bytes !== binary.bytes) {
+ fail(`${target.id}/${binary.file} drifted after native staging`);
+ }
+ }
+ if (!Array.isArray(receipt.sboms) || receipt.sboms.length !== policy.binaries.length) {
+ fail(`${target.id} native receipt SBOM inventory drifted`);
+ }
+ for (const [index, sbom] of receipt.sboms.entries()) {
+ exactKeys(sbom, ["binary", "file", "sha256"], `${target.id} SBOM receipt`);
+ const expectedBinary = policy.binaries[index];
+ if (
+ sbom.binary !== expectedBinary ||
+ sbom.file !== `${expectedBinary}.cdx.json`
+ ) {
+ fail(`${target.id} SBOM receipt inventory drifted`);
+ }
+ safeFilename(sbom.file, `${target.id} SBOM file`);
+ digest(sbom.sha256, `${target.id}/${sbom.file}.sha256`);
+ const path = resolve(root, sbom.file);
+ validateCycloneDx(path, sbom.binary, version);
+ if (fileSha256(path) !== sbom.sha256) {
+ fail(`${target.id}/${sbom.file} drifted after native staging`);
+ }
+ }
+ nativeInputs.push({ target, root, receipt });
+}
+
+const packageStage = resolve(stageRoot, "pnpm-package");
+mkdirSync(resolve(packageStage, "bin"), { recursive: true });
+mkdirSync(resolve(packageStage, "vendor"), { recursive: true });
+mkdirSync(resolve(packageStage, "SBOM"), { recursive: true });
+for (const file of ["package.json", "README.md"]) {
+ copyFileSync(resolve(ROOT, "packages", "cli", file), resolve(packageStage, file));
+}
+copyFileSync(resolve(ROOT, "LICENSE"), resolve(packageStage, "LICENSE"));
+for (const file of readdirSync(resolve(ROOT, "packages", "cli", "bin"))) {
+ copyFileSync(
+ resolve(ROOT, "packages", "cli", "bin", file),
+ resolve(packageStage, "bin", file),
+ );
+}
+for (const { target, root, receipt } of nativeInputs) {
+ const vendor = resolve(packageStage, "vendor", target.id);
+ mkdirSync(vendor, { recursive: true });
+ for (const binary of receipt.binaries) {
+ const destination = resolve(vendor, binary.file);
+ copyFileSync(resolve(root, binary.file), destination);
+ if (target.os !== "win32") chmodSync(destination, 0o755);
+ }
+ for (const sbom of receipt.sboms) {
+ copyFileSync(
+ resolve(root, sbom.file),
+ resolve(packageStage, "SBOM", `${target.id}-${sbom.file}`),
+ );
+ }
+}
+run("pnpm", ["pack", "--pack-destination", releaseRoot], { cwd: packageStage });
+const packed = readdirSync(releaseRoot).filter((file) => file.endsWith(".tgz"));
+if (packed.length !== 1) fail(`pnpm pack produced ${packed.length} tarballs`);
+const pnpmAsset = resolve(releaseRoot, `pliegocss-pnpm-${version}.tgz`);
+const originalPnpmAsset = resolve(releaseRoot, packed[0]);
+if (originalPnpmAsset !== pnpmAsset) {
+ copyFileSync(originalPnpmAsset, pnpmAsset);
+ rmSync(originalPnpmAsset);
+}
+
+const artifacts = [];
+for (const { target, root, receipt } of nativeInputs) {
+ const nativeStage = resolve(stageRoot, target.id);
+ mkdirSync(nativeStage, { recursive: true });
+ for (const file of ["LICENSE", "native.json", ...receipt.binaries.map((x) => x.file), ...receipt.sboms.map((x) => x.file)]) {
+ copyFileSync(resolve(root, file), resolve(nativeStage, file));
+ }
+ for (const binary of receipt.binaries) {
+ if (target.os !== "win32") chmodSync(resolve(nativeStage, binary.file), 0o755);
+ }
+ const archive = resolve(
+ releaseRoot,
+ `pliegocss-${version}-${target.id}.tar.gz`,
+ );
+ run("tar", ["-czf", archive, "-C", nativeStage, "."]);
+ artifacts.push(
+ artifact(archive, "native-archive", {
+ target: target.id,
+ binaries: receipt.binaries,
+ sboms: receipt.sboms,
+ }),
+ );
+ for (const sbom of receipt.sboms) {
+ const sbomAsset = resolve(releaseRoot, `${target.id}-${sbom.file}`);
+ copyFileSync(resolve(root, sbom.file), sbomAsset);
+ artifacts.push(
+ artifact(sbomAsset, "sbom", { target: target.id, binary: sbom.binary }),
+ );
+ }
+}
+artifacts.push(
+ artifact(pnpmAsset, "pnpm-package", {
+ target: "universal-declared-hosts",
+ package: policy.package.name,
+ lifecycleScripts: false,
+ dependencies: 0,
+ registryPublication: false,
+ }),
+);
+artifacts.sort((left, right) => left.file.localeCompare(right.file, "en"));
+
+const manifest = {
+ schemaVersion: 1,
+ kind: "pliegocss-repository-distribution",
+ version,
+ repository: policy.repository,
+ source,
+ packageManager: {
+ name: "pnpm",
+ version: policy.package.packageManagerVersion,
+ installSource: "verified-local-github-release-asset",
+ npmjsPublished: false,
+ lifecycleScripts: false,
+ },
+ immutableReleaseRequired: true,
+ supportedTargets: policy.targets.map(({ id, os, arch }) => ({ id, os, arch })),
+ artifacts,
+ claimBoundary: policy.claimBoundary,
+};
+const manifestPath = resolve(
+ releaseRoot,
+ `pliegocss-distribution-${version}.json`,
+);
+writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
+
+const checksumFiles = [...artifacts.map((entry) => entry.file), basename(manifestPath)].sort();
+const checksumText = `${checksumFiles
+ .map((file) => `${fileSha256(resolve(releaseRoot, file))} ${file}`)
+ .join("\n")}\n`;
+writeFileSync(resolve(releaseRoot, "SHA256SUMS"), checksumText);
+
+process.stdout.write(
+ `${JSON.stringify(
+ {
+ schemaVersion: 1,
+ version,
+ source,
+ releaseRoot,
+ artifacts: artifacts.length + 2,
+ pnpmPackage: basename(pnpmAsset),
+ worktree: dirty ? "dirty-explicitly-allowed" : "clean",
+ },
+ null,
+ 2,
+ )}\n`,
+);
diff --git a/scripts/check-adapter-coexistence-authority.mjs b/scripts/check-adapter-coexistence-authority.mjs
new file mode 100644
index 0000000..1e3d6c9
--- /dev/null
+++ b/scripts/check-adapter-coexistence-authority.mjs
@@ -0,0 +1,153 @@
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import {
+ exactKeys,
+ extractStaticClassGroups,
+ loadContract,
+ profileById,
+ renderProjectDocument,
+ repositoryRoot,
+ sha256,
+} from "./adapter-coexistence-v1.mjs";
+
+function fail(message) {
+ throw new Error(`adapter coexistence authority: ${message}`);
+}
+
+const { authority, corpus } = loadContract();
+exactKeys(
+ authority,
+ [
+ "schemaVersion",
+ "status",
+ "kind",
+ "corpus",
+ "sharedReset",
+ "tailwindProfiles",
+ "adapters",
+ "hosts",
+ "migration",
+ "comparison",
+ "requiredCoverage",
+ "claimBoundary",
+ ],
+ "authority",
+);
+if (
+ authority.schemaVersion !== 1 ||
+ authority.status !== "active-contract" ||
+ authority.kind !== "pliegocss-adapter-coexistence-certification"
+) {
+ fail("header drifted");
+}
+if (corpus.schemaVersion !== 1 || corpus.kind !== "pliegocss-adapter-coexistence-corpus") {
+ fail("corpus header drifted");
+}
+if (!Array.isArray(corpus.projects) || corpus.projects.length < authority.corpus.minimumProjects) {
+ fail(`requires at least ${authority.corpus.minimumProjects} projects`);
+}
+if (authority.corpus.minimumProjects < 20) fail("minimum project floor cannot be below 20");
+
+const ids = new Set();
+const documentHashes = new Set();
+const adapterCounts = Object.fromEntries(authority.adapters.map((adapter) => [adapter.id, 0]));
+const profileCounts = Object.fromEntries(authority.tailwindProfiles.map((profile) => [profile.id, 0]));
+let classGroups = 0;
+for (const project of corpus.projects) {
+ exactKeys(
+ project,
+ ["id", "adapter", "tailwindProfile", "title", "viewport", "body"],
+ `project ${project.id ?? "unknown"}`,
+ );
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(project.id) || ids.has(project.id)) {
+ fail(`invalid or duplicate project id ${JSON.stringify(project.id)}`);
+ }
+ ids.add(project.id);
+ if (!(project.adapter in adapterCounts)) fail(`${project.id} uses an unknown adapter`);
+ profileById(authority, project.tailwindProfile);
+ if (!(project.tailwindProfile in profileCounts)) fail(`${project.id} uses an unknown profile`);
+ exactKeys(project.viewport, ["width", "height"], `${project.id}.viewport`);
+ if (
+ !Number.isInteger(project.viewport.width) ||
+ !Number.isInteger(project.viewport.height) ||
+ project.viewport.width < 320 ||
+ project.viewport.height < 480
+ ) {
+ fail(`${project.id} viewport is outside the bounded browser range`);
+ }
+ const document = renderProjectDocument(project);
+ const groups = extractStaticClassGroups(document, project.id);
+ if (groups.groups.length < 3) fail(`${project.id} is not a meaningful class-bearing project`);
+ if (!/\b(?:aria-[a-z-]+|role)=/u.test(project.body)) {
+ fail(`${project.id} must expose explicit accessible semantics`);
+ }
+ if (project.adapter === "pliegors" && !project.body.includes("data-pliegors-")) {
+ fail(`${project.id} is missing a PliegoRS rendered-output marker`);
+ }
+ const documentHash = sha256(Buffer.from(document));
+ if (documentHashes.has(documentHash)) fail(`${project.id} duplicates another project document`);
+ documentHashes.add(documentHash);
+ adapterCounts[project.adapter] += 1;
+ profileCounts[project.tailwindProfile] += 1;
+ classGroups += groups.groups.length;
+}
+
+for (const adapter of authority.adapters) {
+ if (adapterCounts[adapter.id] < adapter.minimumProjects) {
+ fail(`${adapter.id} requires ${adapter.minimumProjects} projects, found ${adapterCounts[adapter.id]}`);
+ }
+}
+for (const profile of authority.tailwindProfiles) {
+ if (profileCounts[profile.id] < 7) fail(`${profile.id} requires at least seven projects`);
+ const installed = JSON.parse(
+ readFileSync(resolve(repositoryRoot, "node_modules", profile.packageAlias, "package.json"), "utf8"),
+ );
+ if (installed.name !== profile.packageName || installed.version !== profile.version) {
+ fail(
+ `${profile.id} expected ${profile.packageName}@${profile.version}, found ${installed.name}@${installed.version}`,
+ );
+ }
+}
+const viteAdapter = authority.adapters.find((adapter) => adapter.id === "vite");
+const vite = JSON.parse(readFileSync(resolve(repositoryRoot, "node_modules", "vite", "package.json"), "utf8"));
+if (vite.version !== viteAdapter.version) fail(`Vite expected ${viteAdapter.version}, found ${vite.version}`);
+
+const resetBytes = readFileSync(resolve(repositoryRoot, authority.sharedReset.path));
+if (sha256(resetBytes) !== authority.sharedReset.sha256) fail("shared reset hash drifted");
+
+const required = new Set(authority.requiredCoverage);
+for (const item of [
+ "at-least-20-projects",
+ "html",
+ "vite-production-build",
+ "pliegors-rendered-output",
+ "pinned-pliegors-framework-browser",
+ "tailwind-v3-lts",
+ "tailwind-v4-current",
+ "tailwind-source-inventory",
+ "tailwind-output-audit",
+ "reversible-group-apply",
+ "exact-byte-rollback",
+ "dom-equivalence",
+ "aria-equivalence",
+ "computed-style-equivalence",
+ "layout-geometry-equivalence",
+]) {
+ if (!required.has(item)) fail(`required coverage is missing ${item}`);
+}
+
+process.stdout.write(
+ `${JSON.stringify(
+ {
+ schemaVersion: 1,
+ passed: true,
+ projects: corpus.projects.length,
+ uniqueDocuments: documentHashes.size,
+ classGroups,
+ adapters: adapterCounts,
+ tailwindProfiles: profileCounts,
+ },
+ null,
+ 2,
+ )}\n`,
+);
diff --git a/scripts/check-adapter-coexistence-matrix.mjs b/scripts/check-adapter-coexistence-matrix.mjs
new file mode 100644
index 0000000..f7de1a5
--- /dev/null
+++ b/scripts/check-adapter-coexistence-matrix.mjs
@@ -0,0 +1,300 @@
+import { execFileSync } from "node:child_process";
+import { readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from "node:fs";
+import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
+import {
+ loadContract,
+ repositoryRoot,
+ sha256,
+} from "./adapter-coexistence-v1.mjs";
+
+function fail(message) {
+ throw new Error(`adapter coexistence matrix: ${message}`);
+}
+
+function parseOptions(arguments_) {
+ const options = {
+ evidenceRoot: null,
+ pliegorsEvidence: null,
+ output: null,
+ requireClean: false,
+ };
+ for (const argument of arguments_) {
+ if (argument === "--") continue;
+ if (argument === "--require-clean") {
+ options.requireClean = true;
+ } else if (argument.startsWith("--evidence-root=")) {
+ options.evidenceRoot = resolve(repositoryRoot, argument.slice(16));
+ } else if (argument.startsWith("--pliegors-evidence=")) {
+ options.pliegorsEvidence = resolve(repositoryRoot, argument.slice(20));
+ } else if (argument.startsWith("--output=")) {
+ options.output = resolve(repositoryRoot, argument.slice(9));
+ } else {
+ fail(
+ "usage: node scripts/check-adapter-coexistence-matrix.mjs --evidence-root=PATH --pliegors-evidence=PATH [--output=PATH] [--require-clean]",
+ );
+ }
+ }
+ if (!options.evidenceRoot) fail("--evidence-root is required");
+ if (!options.pliegorsEvidence) fail("--pliegors-evidence is required");
+ options.output ??= resolve(
+ repositoryRoot,
+ "target",
+ "adapter-coexistence-certification",
+ "adapter-coexistence-matrix.json",
+ );
+ for (const [role, path] of Object.entries({
+ "evidence root": options.evidenceRoot,
+ "PliegoRS evidence": options.pliegorsEvidence,
+ output: options.output,
+ })) {
+ const child = relative(repositoryRoot, path);
+ if (!child || child === ".." || child.startsWith(`..${sep}`) || isAbsolute(child)) {
+ fail(`${role} must be inside the repository`);
+ }
+ }
+ if (!options.output.endsWith(".json")) fail("--output must be a JSON path");
+ if (!options.pliegorsEvidence.endsWith(".json")) fail("--pliegors-evidence must be a JSON path");
+ return options;
+}
+
+function filesBelow(directory) {
+ return readdirSync(directory, { withFileTypes: true })
+ .sort((left, right) => left.name.localeCompare(right.name, "en"))
+ .flatMap((entry) => {
+ const path = join(directory, entry.name);
+ return entry.isDirectory() ? filesBelow(path) : [path];
+ });
+}
+
+const options = parseOptions(process.argv.slice(2));
+if (
+ options.requireClean &&
+ execFileSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], {
+ cwd: repositoryRoot,
+ encoding: "utf8",
+ }).length !== 0
+) {
+ fail("--require-clean found a dirty matrix checkout");
+}
+if (!statSync(options.evidenceRoot).isDirectory()) fail("evidence root is not a directory");
+const contract = loadContract();
+const evidence = filesBelow(options.evidenceRoot)
+ .filter((path) => path.endsWith(".json"))
+ .map((path) => ({ path, value: JSON.parse(readFileSync(path, "utf8")) }))
+ .filter(({ value }) => value.kind === "pliegocss-adapter-coexistence-host-evidence");
+const expectedHosts = contract.authority.hosts.map((host) => host.id).sort();
+const actualHosts = evidence.map(({ value }) => value.host?.id).sort();
+if (JSON.stringify(actualHosts) !== JSON.stringify(expectedHosts)) {
+ fail(`host inventory drifted: expected ${expectedHosts.join(", ")}, found ${actualHosts.join(", ")}`);
+}
+
+const expectedProjects = contract.corpus.projects.map((project) => project.id);
+const projectContract = new Map(contract.corpus.projects.map((project) => [project.id, project]));
+let sourceIdentity = null;
+const hosts = [];
+for (const { path, value } of evidence) {
+ if (
+ value.schemaVersion !== 1 ||
+ value.result !== "pass" ||
+ value.kind !== "pliegocss-adapter-coexistence-host-evidence"
+ ) {
+ fail(`${path} header or result drifted`);
+ }
+ const expectedHost = contract.authority.hosts.find((host) => host.id === value.host.id);
+ for (const key of ["os", "arch", "browser"]) {
+ if (value.host[key] !== expectedHost[key]) fail(`${value.host.id} ${key} drifted`);
+ }
+ if (!value.host.browserVersion || !value.host.playwright) {
+ fail(`${value.host.id} browser/tooling identity is incomplete`);
+ }
+ if (
+ value.authority?.sha256 !== contract.authoritySha256 ||
+ value.authority?.corpusSha256 !== contract.corpusSha256
+ ) {
+ fail(`${value.host.id} authority identity drifted`);
+ }
+ if (value.source?.dirty !== false || !/^[0-9a-f]{40}$/u.test(value.source?.commit ?? "")) {
+ fail(`${value.host.id} source is dirty or incomplete`);
+ }
+ const identity = `${value.source.commit}:${value.source.gitTree}`;
+ sourceIdentity ??= identity;
+ if (identity !== sourceIdentity) fail(`${value.host.id} is detached from the matrix source`);
+ if (
+ value.summary?.projects !== expectedProjects.length ||
+ value.summary?.passed !== expectedProjects.length ||
+ value.summary?.domEquivalent !== expectedProjects.length ||
+ value.summary?.ariaEquivalent !== expectedProjects.length ||
+ value.summary?.computedStyleEquivalent !== expectedProjects.length ||
+ value.summary?.geometryEquivalent !== expectedProjects.length ||
+ value.summary?.exactRollbacks !== expectedProjects.length ||
+ JSON.stringify(value.summary?.adapters) !==
+ JSON.stringify(
+ Object.fromEntries(
+ contract.authority.adapters.map((adapter) => [
+ adapter.id,
+ contract.corpus.projects.filter((project) => project.adapter === adapter.id).length,
+ ]),
+ ),
+ ) ||
+ JSON.stringify(value.summary?.tailwindProfiles) !==
+ JSON.stringify(
+ Object.fromEntries(
+ contract.authority.tailwindProfiles.map((profile) => [
+ profile.id,
+ contract.corpus.projects.filter(
+ (project) => project.tailwindProfile === profile.id,
+ ).length,
+ ]),
+ ),
+ )
+ ) {
+ fail(`${value.host.id} summary is incomplete`);
+ }
+ if (JSON.stringify(value.projects?.map((project) => project.id)) !== JSON.stringify(expectedProjects)) {
+ fail(`${value.host.id} project order or coverage drifted`);
+ }
+ for (const project of value.projects) {
+ const expectedProject = projectContract.get(project.id);
+ const expectedProfile = contract.authority.tailwindProfiles.find(
+ (profile) => profile.id === expectedProject.tailwindProfile,
+ );
+ if (
+ project.passed !== true ||
+ project.adapter !== expectedProject.adapter ||
+ project.tailwindProfile !== expectedProject.tailwindProfile ||
+ project.tailwind?.version !== expectedProfile.version ||
+ project.migration?.tailwindRetained !== true ||
+ project.migration?.rollback?.exact !== true ||
+ !Number.isInteger(project.source?.classGroups) ||
+ project.source.classGroups < 1 ||
+ project.migration?.replacements !== project.source.classGroups ||
+ project.equivalence?.domEquivalent !== true ||
+ project.equivalence?.ariaEquivalent !== true ||
+ project.equivalence?.computedStyleEquivalent !== true ||
+ project.equivalence?.geometryEquivalent !== true ||
+ project.tailwind?.sourceInventory?.dynamic !== 0 ||
+ project.tailwind?.sourceInventory?.unsupported !== 0 ||
+ project.tailwind?.sourceInventory?.preflightReliance !== "not-observed" ||
+ project.tailwind?.audit?.severities?.error !== 0 ||
+ !project.tailwind?.audit?.codes?.includes("PCSS-AUDIT-000")
+ ) {
+ fail(`${value.host.id}/${project.id} did not close every required gate`);
+ }
+ if (
+ expectedProject.adapter === "vite" &&
+ (project.vite?.version !==
+ contract.authority.adapters.find((adapter) => adapter.id === "vite").version ||
+ !project.vite?.baseline?.assets?.length ||
+ !project.vite?.candidate?.assets?.length)
+ ) {
+ fail(`${value.host.id}/${project.id} has no complete Vite production-build evidence`);
+ }
+ if (
+ !Number.isFinite(project.equivalence.maximumObservedGeometryDeltaCssPx) ||
+ project.equivalence.maximumObservedGeometryDeltaCssPx >
+ contract.authority.comparison.maximumLayoutGeometryDeltaCssPx
+ ) {
+ fail(`${value.host.id}/${project.id} exceeded the geometry bound`);
+ }
+ }
+ hosts.push({
+ id: value.host.id,
+ os: value.host.os,
+ arch: value.host.arch,
+ browser: value.host.browser,
+ browserVersion: value.host.browserVersion,
+ evidenceSha256: sha256(readFileSync(path)),
+ projects: value.summary.projects,
+ passed: true,
+ });
+}
+hosts.sort((left, right) => left.id.localeCompare(right.id, "en"));
+
+const [commit, gitTree] = sourceIdentity.split(":");
+const checkoutCommit = execFileSync("git", ["rev-parse", "HEAD"], {
+ cwd: repositoryRoot,
+ encoding: "utf8",
+}).trim();
+const checkoutTree = execFileSync("git", ["rev-parse", "HEAD^{tree}"], {
+ cwd: repositoryRoot,
+ encoding: "utf8",
+}).trim();
+if (commit !== checkoutCommit || gitTree !== checkoutTree) {
+ fail("host evidence does not belong to the matrix checkout");
+}
+
+const pliegorsEvidenceBytes = readFileSync(options.pliegorsEvidence);
+const pliegors = JSON.parse(pliegorsEvidenceBytes.toString("utf8"));
+const expectedPliegorsContract = JSON.parse(
+ readFileSync(resolve(repositoryRoot, "integration-tests", "pliegors-smoke", "pliegors-contract.json"), "utf8"),
+);
+if (
+ pliegors.schema !== "pliegocss/pliegors-browser-gate/2" ||
+ pliegors.passed !== true ||
+ JSON.stringify(pliegors.pliegorsContract) !== JSON.stringify(expectedPliegorsContract) ||
+ pliegors.pliegocssSource?.dirty !== false ||
+ pliegors.pliegocssSource?.commit !== commit ||
+ pliegors.pliegocssSource?.gitTree !== gitTree ||
+ !pliegors.browser?.product ||
+ pliegors.final?.documentIdentity !== true ||
+ pliegors.final?.nodeIdentity?.island !== true ||
+ pliegors.final?.nodeIdentity?.button !== true ||
+ pliegors.final?.nodeIdentity?.value !== true ||
+ pliegors.final?.eventCount !== 1 ||
+ pliegors.final?.finalMinutes !== pliegors.expected?.finalMinutes ||
+ (pliegors.relevantEvents?.length ?? -1) !== 0 ||
+ (pliegors.serverErrors?.length ?? -1) !== 0
+) {
+ fail("PliegoRS framework/browser evidence is incomplete or detached from the matrix source");
+}
+const matrix = {
+ schemaVersion: 1,
+ kind: "pliegocss-adapter-coexistence-matrix",
+ result: "pass",
+ source: { commit, gitTree },
+ authority: {
+ sha256: contract.authoritySha256,
+ corpusSha256: contract.corpusSha256,
+ },
+ coverage: {
+ hosts: hosts.length,
+ projectsPerHost: expectedProjects.length,
+ totalProjectReplays: hosts.length * expectedProjects.length,
+ adapters: Object.fromEntries(
+ contract.authority.adapters.map((adapter) => [
+ adapter.id,
+ contract.corpus.projects.filter((project) => project.adapter === adapter.id).length,
+ ]),
+ ),
+ tailwindProfiles: Object.fromEntries(
+ contract.authority.tailwindProfiles.map((profile) => [
+ profile.id,
+ contract.corpus.projects.filter((project) => project.tailwindProfile === profile.id).length,
+ ]),
+ ),
+ },
+ hosts,
+ pliegorsFramework: {
+ revision: pliegors.pliegorsContract.revision,
+ sourceSha256: pliegors.pliegorsContract.sourceSha256,
+ browser: pliegors.browser.product,
+ evidenceSha256: sha256(pliegorsEvidenceBytes),
+ passed: true,
+ },
+ claimBoundary: contract.authority.claimBoundary,
+};
+mkdirSync(dirname(options.output), { recursive: true });
+writeFileSync(options.output, `${JSON.stringify(matrix, null, 2)}\n`);
+process.stdout.write(
+ `${JSON.stringify(
+ {
+ passed: true,
+ hosts: hosts.length,
+ projectsPerHost: expectedProjects.length,
+ totalProjectReplays: matrix.coverage.totalProjectReplays,
+ output: relative(repositoryRoot, options.output).replaceAll("\\", "/"),
+ },
+ null,
+ 2,
+ )}\n`,
+);
diff --git a/scripts/check-adapter-coexistence-matrix.test.mjs b/scripts/check-adapter-coexistence-matrix.test.mjs
new file mode 100644
index 0000000..a82d67a
--- /dev/null
+++ b/scripts/check-adapter-coexistence-matrix.test.mjs
@@ -0,0 +1,174 @@
+import { spawnSync } from "node:child_process";
+import { mkdirSync, rmSync, writeFileSync } from "node:fs";
+import { join, relative, resolve } from "node:path";
+import { loadContract, readJson, repositoryRoot } from "./adapter-coexistence-v1.mjs";
+
+const contract = loadContract();
+const source = {
+ commit: spawnSync("git", ["rev-parse", "HEAD"], {
+ cwd: repositoryRoot,
+ encoding: "utf8",
+ windowsHide: true,
+ }).stdout.trim(),
+ gitTree: spawnSync("git", ["rev-parse", "HEAD^{tree}"], {
+ cwd: repositoryRoot,
+ encoding: "utf8",
+ windowsHide: true,
+ }).stdout.trim(),
+ dirty: false,
+};
+const targetRoot = resolve(
+ repositoryRoot,
+ "target",
+ "adapter-coexistence-certification",
+ "matrix-contract-test",
+);
+rmSync(targetRoot, { recursive: true, force: true });
+
+function evidence(host) {
+ const count = contract.corpus.projects.length;
+ return {
+ schemaVersion: 1,
+ kind: "pliegocss-adapter-coexistence-host-evidence",
+ result: "pass",
+ source: { ...source },
+ authority: {
+ sha256: contract.authoritySha256,
+ corpusSha256: contract.corpusSha256,
+ },
+ host: {
+ ...host,
+ browserVersion: "Chromium contract test",
+ playwright: "1.61.1",
+ },
+ summary: {
+ projects: count,
+ passed: count,
+ domEquivalent: count,
+ ariaEquivalent: count,
+ computedStyleEquivalent: count,
+ geometryEquivalent: count,
+ exactRollbacks: count,
+ adapters: Object.fromEntries(
+ contract.authority.adapters.map((adapter) => [
+ adapter.id,
+ contract.corpus.projects.filter((project) => project.adapter === adapter.id).length,
+ ]),
+ ),
+ tailwindProfiles: Object.fromEntries(
+ contract.authority.tailwindProfiles.map((profile) => [
+ profile.id,
+ contract.corpus.projects.filter(
+ (project) => project.tailwindProfile === profile.id,
+ ).length,
+ ]),
+ ),
+ },
+ projects: contract.corpus.projects.map((project) => ({
+ id: project.id,
+ adapter: project.adapter,
+ tailwindProfile: project.tailwindProfile,
+ passed: true,
+ source: { classGroups: 3 },
+ tailwind: {
+ version: contract.authority.tailwindProfiles.find(
+ (profile) => profile.id === project.tailwindProfile,
+ ).version,
+ sourceInventory: { dynamic: 0, unsupported: 0, preflightReliance: "not-observed" },
+ audit: { severities: { error: 0 }, codes: ["PCSS-AUDIT-000"] },
+ },
+ migration: { replacements: 3, tailwindRetained: true, rollback: { exact: true } },
+ vite:
+ project.adapter === "vite"
+ ? {
+ version: contract.authority.adapters.find((adapter) => adapter.id === "vite").version,
+ baseline: { assets: [{ path: "index.html" }] },
+ candidate: { assets: [{ path: "index.html" }] },
+ }
+ : null,
+ equivalence: {
+ domEquivalent: true,
+ ariaEquivalent: true,
+ computedStyleEquivalent: true,
+ geometryEquivalent: true,
+ maximumObservedGeometryDeltaCssPx: 0,
+ },
+ })),
+ };
+}
+
+function pliegorsEvidence() {
+ return {
+ schema: "pliegocss/pliegors-browser-gate/2",
+ passed: true,
+ pliegocssSource: { ...source },
+ pliegorsContract: readJson(
+ resolve(repositoryRoot, "integration-tests", "pliegors-smoke", "pliegors-contract.json"),
+ ),
+ browser: { product: "Chrome contract test" },
+ expected: { finalMinutes: 20 },
+ final: {
+ documentIdentity: true,
+ nodeIdentity: { island: true, button: true, value: true },
+ eventCount: 1,
+ finalMinutes: 20,
+ },
+ relevantEvents: [],
+ serverErrors: [],
+ };
+}
+
+function writeCase(name, mutate = () => {}) {
+ const root = join(targetRoot, name);
+ mkdirSync(root, { recursive: true });
+ const documents = contract.authority.hosts.map(evidence);
+ const pliegors = pliegorsEvidence();
+ mutate(documents, pliegors);
+ for (const document of documents) {
+ writeFileSync(join(root, `${document.host.id}.json`), `${JSON.stringify(document, null, 2)}\n`);
+ }
+ const pliegorsPath = join(root, "pliegors-framework.json");
+ writeFileSync(pliegorsPath, `${JSON.stringify(pliegors, null, 2)}\n`);
+ return { root, pliegorsPath };
+}
+
+function run({ root, pliegorsPath }) {
+ return spawnSync(
+ process.execPath,
+ [
+ resolve(repositoryRoot, "scripts", "check-adapter-coexistence-matrix.mjs"),
+ `--evidence-root=${relative(repositoryRoot, root)}`,
+ `--pliegors-evidence=${relative(repositoryRoot, pliegorsPath)}`,
+ `--output=${relative(repositoryRoot, join(root, "matrix.json"))}`,
+ ],
+ { cwd: repositoryRoot, encoding: "utf8", windowsHide: true },
+ );
+}
+
+const valid = run(writeCase("valid"));
+if (valid.status !== 0) throw new Error(`valid matrix failed\n${valid.stdout}${valid.stderr}`);
+
+for (const [name, mutate] of [
+ ["missing-host", (documents) => documents.pop()],
+ ["dirty-source", (documents) => {
+ documents[0].source.dirty = true;
+ }],
+ ["aria-drift", (documents) => {
+ documents[1].projects[0].equivalence.ariaEquivalent = false;
+ }],
+ ["audit-error", (documents) => {
+ documents[2].projects[0].tailwind.audit.severities.error = 1;
+ }],
+ ["vite-evidence-missing", (documents) => {
+ documents[0].projects.find((project) => project.adapter === "vite").vite = null;
+ }],
+ ["pliegors-dirty-source", (_documents, pliegors) => {
+ pliegors.pliegocssSource.dirty = true;
+ }],
+]) {
+ const result = run(writeCase(name, mutate));
+ if (result.status === 0) throw new Error(`${name} matrix unexpectedly passed`);
+}
+
+rmSync(targetRoot, { recursive: true, force: true });
+process.stdout.write("adapter coexistence matrix fail-closed contract: pass\n");
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-distribution-bundle.mjs b/scripts/check-distribution-bundle.mjs
new file mode 100644
index 0000000..521b893
--- /dev/null
+++ b/scripts/check-distribution-bundle.mjs
@@ -0,0 +1,287 @@
+import { spawnSync } from "node:child_process";
+import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
+import { basename, resolve } from "node:path";
+import {
+ ROOT,
+ digest,
+ exactKeys,
+ fail,
+ fileSha256,
+ readJson,
+ repositoryVersion,
+ safeFilename,
+ validateCycloneDx,
+ validatePackageManifest,
+ validatePolicy,
+ validateSource,
+} from "./repository-distribution.mjs";
+
+function parse(args) {
+ const parsed = {};
+ for (const arg of args) {
+ if (arg === "--") continue;
+ const match = /^--([a-z-]+)=(.+)$/u.exec(arg);
+ if (!match || Object.hasOwn(parsed, match[1])) fail(`invalid option: ${arg}`);
+ parsed[match[1]] = match[2];
+ }
+ return parsed;
+}
+
+function tar(args) {
+ const result = spawnSync("tar", args, {
+ cwd: ROOT,
+ encoding: "utf8",
+ windowsHide: true,
+ });
+ if (result.error || result.status !== 0) {
+ fail(`tar ${args.join(" ")} failed: ${result.error?.message ?? result.stderr.trim()}`);
+ }
+ return result.stdout;
+}
+
+const options = parse(process.argv.slice(2));
+if (!options.root) fail("--root is required");
+const bundleRoot = resolve(ROOT, options.root);
+const version = repositoryVersion();
+const policy = validatePolicy();
+const manifestPath = resolve(bundleRoot, `pliegocss-distribution-${version}.json`);
+if (!existsSync(manifestPath)) fail(`distribution manifest is missing: ${manifestPath}`);
+const manifest = readJson(manifestPath, "distribution manifest");
+exactKeys(
+ manifest,
+ [
+ "schemaVersion",
+ "kind",
+ "version",
+ "repository",
+ "source",
+ "packageManager",
+ "immutableReleaseRequired",
+ "supportedTargets",
+ "artifacts",
+ "claimBoundary",
+ ],
+ "distribution manifest",
+);
+if (
+ manifest.schemaVersion !== 1 ||
+ manifest.kind !== "pliegocss-repository-distribution" ||
+ manifest.version !== version ||
+ manifest.repository !== policy.repository ||
+ manifest.immutableReleaseRequired !== true ||
+ manifest.claimBoundary !== policy.claimBoundary
+) {
+ fail("distribution manifest identity drifted");
+}
+validateSource(manifest.source);
+exactKeys(
+ manifest.packageManager,
+ ["name", "version", "installSource", "npmjsPublished", "lifecycleScripts"],
+ "distribution package manager",
+);
+if (
+ manifest.packageManager.name !== "pnpm" ||
+ manifest.packageManager.version !== policy.package.packageManagerVersion ||
+ manifest.packageManager.installSource !== "verified-local-github-release-asset" ||
+ manifest.packageManager.npmjsPublished !== false ||
+ manifest.packageManager.lifecycleScripts !== false
+) {
+ fail("distribution package-manager boundary drifted");
+}
+const expectedTargets = policy.targets.map(({ id, os, arch }) => ({ id, os, arch }));
+if (JSON.stringify(manifest.supportedTargets) !== JSON.stringify(expectedTargets)) {
+ fail("supported target inventory drifted");
+}
+if (!Array.isArray(manifest.artifacts) || manifest.artifacts.length !== 10) {
+ fail("distribution must contain three archives, six SBOMs, and one pnpm package");
+}
+const expectedArtifacts = new Map();
+for (const target of policy.targets) {
+ expectedArtifacts.set(`pliegocss-${version}-${target.id}.tar.gz`, {
+ kind: "native-archive",
+ target: target.id,
+ });
+ for (const binary of policy.binaries) {
+ expectedArtifacts.set(`${target.id}-${binary}.cdx.json`, {
+ kind: "sbom",
+ target: target.id,
+ binary,
+ });
+ }
+}
+expectedArtifacts.set(`pliegocss-pnpm-${version}.tgz`, {
+ kind: "pnpm-package",
+ target: "universal-declared-hosts",
+});
+const artifactNames = new Set();
+for (const artifact of manifest.artifacts) {
+ if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) {
+ fail("distribution artifact must be an object");
+ }
+ safeFilename(artifact.file, "distribution artifact file");
+ const expected = expectedArtifacts.get(artifact.file);
+ if (!expected || artifact.kind !== expected.kind || artifact.target !== expected.target) {
+ fail(`${artifact.file} type or target drifted`);
+ }
+ if (artifact.kind === "native-archive") {
+ exactKeys(
+ artifact,
+ ["kind", "file", "bytes", "sha256", "target", "binaries", "sboms"],
+ `${artifact.file} manifest entry`,
+ );
+ if (
+ !Array.isArray(artifact.binaries) ||
+ !Array.isArray(artifact.sboms) ||
+ artifact.binaries.length !== policy.binaries.length ||
+ artifact.sboms.length !== policy.binaries.length
+ ) {
+ fail(`${artifact.file} receipt inventory drifted`);
+ }
+ } else if (artifact.kind === "sbom") {
+ exactKeys(
+ artifact,
+ ["kind", "file", "bytes", "sha256", "target", "binary"],
+ `${artifact.file} manifest entry`,
+ );
+ if (artifact.binary !== expected.binary) fail(`${artifact.file} binary drifted`);
+ } else {
+ exactKeys(
+ artifact,
+ [
+ "kind",
+ "file",
+ "bytes",
+ "sha256",
+ "target",
+ "package",
+ "lifecycleScripts",
+ "dependencies",
+ "registryPublication",
+ ],
+ `${artifact.file} manifest entry`,
+ );
+ if (
+ artifact.package !== policy.package.name ||
+ artifact.lifecycleScripts !== false ||
+ artifact.dependencies !== 0 ||
+ artifact.registryPublication !== false
+ ) {
+ fail("pnpm package release boundary drifted");
+ }
+ }
+ if (artifactNames.has(artifact.file)) fail(`duplicate artifact ${artifact.file}`);
+ artifactNames.add(artifact.file);
+ if (!Number.isSafeInteger(artifact.bytes) || artifact.bytes <= 0) {
+ fail(`${artifact.file}.bytes must be a positive safe integer`);
+ }
+ digest(artifact.sha256, `${artifact.file}.sha256`);
+ const path = resolve(bundleRoot, artifact.file);
+ if (!existsSync(path) || !statSync(path).isFile()) fail(`artifact missing: ${artifact.file}`);
+ if (fileSha256(path) !== artifact.sha256 || statSync(path).size !== artifact.bytes) {
+ fail(`artifact hash or size drifted: ${artifact.file}`);
+ }
+ if (artifact.kind === "sbom") {
+ validateCycloneDx(path, artifact.binary, version);
+ }
+}
+const expectedArtifactNames = new Set(expectedArtifacts.keys());
+if (
+ artifactNames.size !== expectedArtifactNames.size ||
+ [...expectedArtifactNames].some((file) => !artifactNames.has(file))
+) {
+ fail("distribution artifact filename inventory drifted");
+}
+for (const target of policy.targets) {
+ const archiveName = `pliegocss-${version}-${target.id}.tar.gz`;
+ if (!artifactNames.has(archiveName)) fail(`native archive missing for ${target.id}`);
+ const archiveFiles = tar(["-tzf", resolve(bundleRoot, archiveName)])
+ .replaceAll("\\", "/")
+ .split(/\r?\n/u)
+ .map((line) => line.replace(/^\.\//u, ""))
+ .filter((line) => line && !line.endsWith("/"));
+ const expectedArchiveFiles = new Set([
+ ...policy.binaries.map((binary) => `${binary}${target.extension}`),
+ ...policy.binaries.map((binary) => `${binary}.cdx.json`),
+ "native.json",
+ "LICENSE",
+ ]);
+ if (
+ archiveFiles.length !== expectedArchiveFiles.size ||
+ archiveFiles.some((file) => !expectedArchiveFiles.has(file))
+ ) {
+ fail(`${archiveName} file inventory drifted`);
+ }
+}
+const packageName = `pliegocss-pnpm-${version}.tgz`;
+if (!artifactNames.has(packageName)) fail("pnpm package artifact is missing");
+const packagePath = resolve(bundleRoot, packageName);
+const packageFiles = tar(["-tzf", packagePath])
+ .replaceAll("\\", "/")
+ .split(/\r?\n/u)
+ .filter((line) => line && !line.endsWith("/"));
+const expectedPackageFiles = new Set([
+ "package/package.json",
+ "package/bin/pliego-cssc.mjs",
+ "package/bin/pliego-css-lsp.mjs",
+ "package/bin/launcher.mjs",
+ "package/LICENSE",
+ "package/README.md",
+]);
+for (const target of policy.targets) {
+ for (const binary of policy.binaries) {
+ expectedPackageFiles.add(`package/vendor/${target.id}/${binary}${target.extension}`);
+ expectedPackageFiles.add(`package/SBOM/${target.id}-${binary}.cdx.json`);
+ }
+}
+if (
+ packageFiles.length !== expectedPackageFiles.size ||
+ packageFiles.some((file) => !expectedPackageFiles.has(file))
+) {
+ fail("pnpm package file inventory drifted");
+}
+const packedManifest = JSON.parse(tar(["-xOzf", packagePath, "package/package.json"]));
+validatePackageManifest(packedManifest, policy, { packed: true });
+
+const checksumsPath = resolve(bundleRoot, "SHA256SUMS");
+if (!existsSync(checksumsPath)) fail("SHA256SUMS is missing");
+const checksumLines = readFileSync(checksumsPath, "utf8").trimEnd().split(/\r?\n/u);
+const checksums = new Map();
+for (const line of checksumLines) {
+ const match = /^([0-9a-f]{64}) ([A-Za-z0-9@._-]+)$/u.exec(line);
+ if (!match || checksums.has(match[2])) fail(`invalid checksum line: ${line}`);
+ checksums.set(match[2], match[1]);
+}
+const expectedChecksums = new Set([...artifactNames, basename(manifestPath)]);
+if (
+ checksums.size !== expectedChecksums.size ||
+ [...expectedChecksums].some((file) => !checksums.has(file))
+) {
+ fail("SHA256SUMS inventory drifted");
+}
+for (const [file, expected] of checksums) {
+ if (fileSha256(resolve(bundleRoot, file)) !== expected) fail(`checksum mismatch for ${file}`);
+}
+const allowedFiles = new Set([...expectedChecksums, "SHA256SUMS"]);
+const actualFiles = readdirSync(bundleRoot).filter((file) => statSync(resolve(bundleRoot, file)).isFile());
+if (actualFiles.some((file) => !allowedFiles.has(file)) || actualFiles.length !== allowedFiles.size) {
+ fail("release asset directory contains an undeclared file");
+}
+
+process.stdout.write(
+ `${JSON.stringify(
+ {
+ schemaVersion: 1,
+ result: "passed",
+ version,
+ source: manifest.source,
+ targets: policy.targets.length,
+ nativeArchives: 3,
+ sboms: 6,
+ pnpmPackage: packageName,
+ npmjsPublished: false,
+ lifecycleScripts: false,
+ },
+ null,
+ 2,
+ )}\n`,
+);
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-external-adoption.mjs b/scripts/check-external-adoption.mjs
new file mode 100644
index 0000000..3315814
--- /dev/null
+++ b/scripts/check-external-adoption.mjs
@@ -0,0 +1,30 @@
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import {
+ loadExternalAdoptionBundle,
+ validateExternalAdoptionBundle,
+} from "./external-adoption-v1.mjs";
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const allowed = new Set(["--validate", "--require-promotion-ready"]);
+const args = process.argv.slice(2);
+if (args.some((arg) => !allowed.has(arg))) {
+ process.stderr.write(
+ "usage: node scripts/check-external-adoption.mjs [--validate] [--require-promotion-ready]\n",
+ );
+ process.exit(2);
+}
+
+try {
+ const status = validateExternalAdoptionBundle(loadExternalAdoptionBundle(root), { root });
+ process.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
+ if (args.includes("--require-promotion-ready") && status.result !== "ready") {
+ process.stderr.write(
+ `external adoption is blocked: ${status.missingCoverage.join(", ")}\n`,
+ );
+ process.exitCode = 1;
+ }
+} catch (error) {
+ process.stderr.write(`${error.message}\n`);
+ process.exitCode = 1;
+}
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-installed-repository-package.mjs b/scripts/check-installed-repository-package.mjs
new file mode 100644
index 0000000..1240f2d
--- /dev/null
+++ b/scripts/check-installed-repository-package.mjs
@@ -0,0 +1,99 @@
+import { spawnSync } from "node:child_process";
+import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { basename, isAbsolute, relative, resolve } from "node:path";
+import { ROOT, fail, repositoryVersion } from "./repository-distribution.mjs";
+
+function parse(args) {
+ const parsed = {};
+ for (const arg of args) {
+ if (arg === "--") continue;
+ const match = /^--([a-z-]+)=(.+)$/u.exec(arg);
+ if (!match || Object.hasOwn(parsed, match[1])) fail(`invalid option: ${arg}`);
+ parsed[match[1]] = match[2];
+ }
+ return parsed;
+}
+
+function run(command, args, cwd, { input } = {}) {
+ const executable = process.platform === "win32" && command === "pnpm"
+ ? process.env.ComSpec ?? "cmd.exe"
+ : command;
+ const commandArgs = process.platform === "win32" && command === "pnpm"
+ ? ["/d", "/s", "/c", command, ...args]
+ : args;
+ const result = spawnSync(executable, commandArgs, {
+ cwd,
+ encoding: "utf8",
+ input,
+ timeout: 30_000,
+ windowsHide: true,
+ });
+ if (result.error || result.status !== 0) {
+ fail(
+ `${command} ${args.join(" ")} failed: ${result.error?.message ?? `${result.stdout}${result.stderr}`.trim()}`,
+ );
+ }
+ return `${result.stdout}${result.stderr}`.trim();
+}
+
+const options = parse(process.argv.slice(2));
+if (!options.tarball) fail("--tarball is required");
+const tarball = resolve(ROOT, options.tarball);
+const version = repositoryVersion();
+const temporaryRoot = mkdtempSync(resolve(tmpdir(), "pliegocss-pnpm-consumer-"));
+try {
+ writeFileSync(
+ resolve(temporaryRoot, "package.json"),
+ `${JSON.stringify(
+ {
+ name: "pliegocss-repository-distribution-consumer",
+ version: "0.0.0",
+ private: true,
+ packageManager: "pnpm@11.7.0",
+ },
+ null,
+ 2,
+ )}\n`,
+ );
+ writeFileSync(
+ resolve(temporaryRoot, "pnpm-workspace.yaml"),
+ "packages: []\nblockExoticSubdeps: true\nonlyBuiltDependencies: []\n",
+ );
+ run(
+ "pnpm",
+ ["add", "--save-dev", "--save-exact", "--offline", "--ignore-scripts", tarball],
+ temporaryRoot,
+ );
+ const packageManifest = JSON.parse(readFileSync(resolve(temporaryRoot, "package.json"), "utf8"));
+ const installed = packageManifest.devDependencies?.["@pliegocss/cli"];
+ if (typeof installed !== "string" || !installed.startsWith("file:")) {
+ fail("consumer did not record the verified local release asset");
+ }
+ const cliVersion = run("pnpm", ["exec", "pliego-cssc", "--version"], temporaryRoot);
+ const lspVersion = run("pnpm", ["exec", "pliego-css-lsp", "--version"], temporaryRoot);
+ if (!cliVersion.includes(version) || !lspVersion.includes(version)) {
+ fail(`installed binaries do not report ${version}`);
+ }
+ process.stdout.write(
+ `${JSON.stringify(
+ {
+ schemaVersion: 1,
+ result: "passed",
+ host: `${process.platform}:${process.arch}`,
+ tarball: basename(tarball),
+ version,
+ install: "pnpm-local-verified-asset",
+ scripts: "ignored-and-absent",
+ },
+ null,
+ 2,
+ )}\n`,
+ );
+} finally {
+ const local = relative(resolve(tmpdir()), temporaryRoot);
+ if (!local || local.startsWith("..") || isAbsolute(local) || !basename(temporaryRoot).startsWith("pliegocss-pnpm-consumer-")) {
+ fail(`refusing to remove unsafe consumer path: ${temporaryRoot}`);
+ }
+ rmSync(temporaryRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
+}
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-browser.mjs b/scripts/check-pliegors-browser.mjs
index b4ee77a..ec5cd3b 100644
--- a/scripts/check-pliegors-browser.mjs
+++ b/scripts/check-pliegors-browser.mjs
@@ -1,14 +1,16 @@
import { spawn, spawnSync } from "node:child_process";
import {
existsSync,
+ mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
+ writeFileSync,
} from "node:fs";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
-import { basename, extname, isAbsolute, join, relative, resolve } from "node:path";
+import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
@@ -34,12 +36,41 @@ const expected = Object.freeze({
const skipBuild = process.env.PLIEGOCSS_SKIP_PLIEGORS_BUILD === "1";
const keepSite = process.env.PLIEGOCSS_KEEP_PLIEGORS_SITE === "1";
const agentReport = process.env.PLIEGOCSS_AGENT_REPORT === "1";
+const evidencePath = process.env.PLIEGOCSS_PLIEGORS_BROWSER_EVIDENCE
+ ? resolve(root, process.env.PLIEGOCSS_PLIEGORS_BROWSER_EVIDENCE)
+ : null;
function fail(message, details = undefined) {
const suffix = details === undefined ? "" : `\n${JSON.stringify(details, null, 2)}`;
throw new Error(`${message}${suffix}`);
}
+function gitOutput(arguments_) {
+ const result = spawnSync("git", arguments_, {
+ cwd: root,
+ encoding: "utf8",
+ windowsHide: true,
+ });
+ if (result.error) throw result.error;
+ if (result.status !== 0) {
+ fail(`git ${arguments_.join(" ")} failed`, {
+ exitCode: result.status,
+ stdout: result.stdout,
+ stderr: result.stderr,
+ });
+ }
+ return result.stdout;
+}
+
+function pliegocssSourceIdentity() {
+ const status = gitOutput(["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
+ return {
+ commit: gitOutput(["rev-parse", "HEAD"]).trim(),
+ gitTree: gitOutput(["rev-parse", "HEAD^{tree}"]).trim(),
+ dirty: status.length !== 0,
+ };
+}
+
function sleep(milliseconds) {
return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
}
@@ -614,6 +645,10 @@ try {
const report = {
schema: "pliegocss/pliegors-browser-gate/2",
passed,
+ pliegocssSource: pliegocssSourceIdentity(),
+ pliegorsContract: JSON.parse(
+ readFileSync(join(root, "integration-tests", "pliegors-smoke", "pliegors-contract.json"), "utf8"),
+ ),
browser: {
executable: basename(chromePath),
jsVersion: browserVersion.jsVersion,
@@ -629,6 +664,20 @@ try {
serverErrors: site.errors,
serverRequests,
};
+ if (evidencePath) {
+ const child = relative(root, evidencePath);
+ if (
+ !child ||
+ child === ".." ||
+ child.startsWith(`..${sep}`) ||
+ isAbsolute(child) ||
+ !evidencePath.endsWith(".json")
+ ) {
+ fail("PLIEGOCSS_PLIEGORS_BROWSER_EVIDENCE must be a JSON path inside the repository");
+ }
+ mkdirSync(dirname(evidencePath), { recursive: true });
+ writeFileSync(evidencePath, `${JSON.stringify(report, null, 2)}\n`);
+ }
const compactReport = {
schemaVersion: "1.0.0",
passed,
diff --git a/scripts/check-pliegors-dev-loop.mjs b/scripts/check-pliegors-dev-loop.mjs
index 2f3fe44..8bcb40c 100644
--- a/scripts/check-pliegors-dev-loop.mjs
+++ b/scripts/check-pliegors-dev-loop.mjs
@@ -168,6 +168,19 @@ function materializeFixture(runtime) {
join(cargoOverrideDirectory, "config.toml"),
`# Generated by the PliegoCSS cross-repository development-loop gate.\npaths = ${JSON.stringify(cargoOverridePaths)}\n`,
);
+ manifest = manifest
+ .replace(
+ 'pliego-dom = "=0.0.2"',
+ `pliego-dom = { version = "=0.4.0-beta.1", path = ${JSON.stringify(normalizedPath(join(PLIEGORS_ROOT, "crates", "pliego-dom")))} }`,
+ )
+ .replace(
+ 'pliego-ssg = "=0.0.2"',
+ `pliego-ssg = { version = "=0.4.0-beta.1", path = ${JSON.stringify(normalizedPath(join(PLIEGORS_ROOT, "crates", "pliego-ssg")))} }`,
+ );
+ writeFileSync(manifestPath, manifest);
+ run("cargo", [`+${TOOLCHAIN}`, "generate-lockfile", "--manifest-path", manifestPath], {
+ cwd: project,
+ });
const driver = join(runtime, "driver.styles.txt");
writeFileSync(driver, "block\n");
return {
@@ -306,6 +319,11 @@ async function fetchText(url) {
return response.text();
}
+async function fetchResponseText(url) {
+ const response = await fetch(url, { cache: "no-store" });
+ return { status: response.status, text: await response.text() };
+}
+
async function fetchBytes(url) {
const response = await fetch(url, { cache: "no-store" });
if (!response.ok) fail(`${url} returned HTTP ${response.status}`);
@@ -318,7 +336,8 @@ function attribute(tag, name) {
function htmlState(html) {
const tag = html.match(/]*>/gu)?.find((candidate) => candidate.includes('id="style-probe"'));
- const generation = Number(/\/_pliego\/reload\?since=(\d+)/u.exec(html)?.[1]);
+ const reload = /