diff --git a/README.md b/README.md index 88612dd..23298fb 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,21 @@ published-wheel identity, complete inventory and original file manifest before copying PDFs and previews into the built site. Large evidence files stay in release storage. Pass `--archive /path/to/verified.zip` to use a local copy. After another MkDocs build, run the preparation step again before serving `site`. +Earlier published manifests live in `docs/assets/css-corpus/history/`. The same +preparation command verifies and materializes them too, preserving their versioned +PDF, image, and index URLs when the current explorer advances. Keep the historical +manifest bytes unchanged; the browser check verifies old PASS, FAIL, and disputed +PDF URLs alongside the current interactive results. +The package selector uses the generated `versions.json` inventory. Shared case +URLs include `version=...`, and changing versions preserves the selected case +and filters so readers can inspect the same fixture across retained releases. +Its markup loads `explorer-v2.js` and `explorer-v2.css`; keep the original +`explorer.js` and `explorer.css` unchanged so cached earlier pages retain a +compatible script and stylesheet during deployment. +The new script loads after its markup and before the theme script. This lets it +migrate legacy `q` fixture links to `query` before the theme can open its global +search overlay. Keep the mobile shared-link and download checks when changing +this loading order. With Playwright 1.63.0, its Chrome browser, and pypdf 6.19.0 installed, run `python tools/check_css_explorer.py --site site --out output/css-explorer-ui`. diff --git a/docs/assets/css-corpus/explorer-v2.css b/docs/assets/css-corpus/explorer-v2.css new file mode 100644 index 0000000..f70f11d --- /dev/null +++ b/docs/assets/css-corpus/explorer-v2.css @@ -0,0 +1,46 @@ +.css-explorer { margin: 1.5rem 0; } +.css-explorer [hidden] { display: none !important; } +.css-explorer input, .css-explorer select, .css-explorer button { + font: inherit; color: var(--md-default-fg-color); background: var(--md-default-bg-color); + border: 1px solid var(--md-default-fg-color--light); border-radius: .2rem; padding: .5rem; +} +.css-explorer button { cursor: pointer; font-weight: 600; } +.css-explorer button:disabled { opacity: .45; cursor: default; } +.css-explorer :focus-visible { outline: 3px solid var(--md-accent-fg-color); outline-offset: 3px; } +.ce-version { max-width: 18rem; margin: .75rem 0; } +.ce-version label { display: block; font-size: .72rem; font-weight: 600; } +.ce-version select { display: block; width: 100%; min-width: 0; margin-top: .3rem; min-height: 2.3rem; } +.ce-filters { display: grid; grid-template-columns: 2fr 1fr 1fr; gap: .7rem; } +.ce-filters label { min-width: 0; font-size: .72rem; font-weight: 600; } +.ce-filters input, .ce-filters select { display: block; width: 100%; margin-top: .3rem; min-height: 2.3rem; } +.ce-counts { display: flex; flex-wrap: wrap; gap: .5rem 1rem; padding: .6rem 0; border-bottom: 1px solid var(--md-default-fg-color--lightest); } +.ce-result { padding: .8rem 0; border-bottom: 1px solid var(--md-default-fg-color--lightest); } +.ce-result button { text-align: left; padding: 0; color: var(--md-typeset-a-color); border: 0; background: transparent; overflow-wrap: anywhere; } +.ce-result p { margin: .3rem 0; font-size: .75rem; } +.ce-meta { display: flex; flex-wrap: wrap; gap: .4rem .8rem; font-size: .65rem; } +.ce-verdict { font-weight: 700; } +.ce-verdict[data-verdict="FAIL"] { color: #b52338; } +.ce-verdict[data-verdict="PASS"] { color: #166454; } +.ce-verdict[data-verdict="REFERENCE-DISPUTED"] { color: #845000; } +[data-md-color-scheme="slate"] .ce-verdict[data-verdict="FAIL"] { color: #ff9da9; } +[data-md-color-scheme="slate"] .ce-verdict[data-verdict="PASS"] { color: #8ed9be; } +[data-md-color-scheme="slate"] .ce-verdict[data-verdict="REFERENCE-DISPUTED"] { color: #f2c27b; } +.ce-pagination { display: flex; align-items: center; justify-content: space-between; gap: .7rem; margin: 1rem 0; font-size: .75rem; } +.ce-detail { margin: 1.2rem 0; padding: 1rem; border: 1px solid var(--md-default-fg-color--light); border-radius: .3rem; background: var(--md-code-bg-color); overflow-wrap: anywhere; scroll-margin-top: 4rem; } +.ce-detail h2 { margin-top: 0; font-size: 1.1rem; } +.ce-detail h3 { font-size: .8rem; } +.ce-detail p { font-size: .75rem; } +.ce-detail details { border-color: var(--md-default-fg-color--lightest); box-shadow: none; } +.ce-detail summary { background: transparent; } +.ce-detail summary::before { background-color: var(--md-typeset-a-color); } +.ce-links { display: flex; flex-wrap: wrap; gap: .6rem 1rem; } +.ce-images { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .7rem; } +.ce-images figure { min-width: 0; margin: 0; } +.ce-images figcaption { font-size: .65rem; text-align: left; margin: .4rem 0; } +.ce-images img { display: block; width: 100%; height: auto; border: 1px solid var(--md-default-fg-color--light); background: white; } +.ce-error { border-left: 3px solid #b52338; padding-left: .7rem; } +@media (max-width: 700px) { + .ce-filters { grid-template-columns: 1fr; } + .ce-images { grid-template-columns: 1fr; } + .ce-detail { padding: .6rem; scroll-margin-top: 6rem; } +} diff --git a/docs/assets/css-corpus/explorer-v2.js b/docs/assets/css-corpus/explorer-v2.js new file mode 100644 index 0000000..db5dc38 --- /dev/null +++ b/docs/assets/css-corpus/explorer-v2.js @@ -0,0 +1,207 @@ +(() => { + 'use strict'; + const root = document.getElementById('css-explorer'); + if (!root) return; + // Old fixture links used the theme's global-search parameter. Migrate it + // before the theme starts so a shared mobile link leaves the PDF accessible. + const initialUrl = new URL(location.href); + if (initialUrl.searchParams.has('q')) { + if (!initialUrl.searchParams.has('query')) initialUrl.searchParams.set('query', initialUrl.searchParams.get('q')); + initialUrl.searchParams.delete('q'); + history.replaceState(null, '', initialUrl); + } + const find = (id) => document.getElementById(`ce-${id}`); + const create = (tag, text, className) => { + const element = document.createElement(tag); + if (text !== undefined) element.textContent = text; + if (className) element.className = className; + return element; + }; + const versionsUrl = new URL(root.dataset.versions, location.href); + let indexUrl; + const perPage = 12; + const verdicts = ['PASS', 'FAIL', 'REFERENCE-DISPUTED']; + let data, fixtures, selected, pageNumber = 0; + const badge = (status) => { + const element = create('span', status, 'ce-verdict'); + element.dataset.verdict = status; + return element; + }; + const assetUrl = (path) => { + if (!/^(?:pdfs|refs|out)\/[a-z0-9_-]+\/[a-z0-9_.-]+\.(?:pdf|png)$/.test(path)) throw new Error('Invalid evidence path'); + return new URL(path, indexUrl).href; + }; + const link = (title, href, download = false) => { + const element = create('a', title); + element.href = href; + if (download) element.download = ''; + return element; + }; + const saveUrl = (push = false) => { + const url = new URL(location.href); + for (const [key, value] of [['version', data.version], ['query', find('search').value], ['category', find('category').value], + ['verdict', find('verdict').value], ['case', selected || '']]) { + if (value) url.searchParams.set(key, value); else url.searchParams.delete(key); + } + history[push ? 'pushState' : 'replaceState'](null, '', url); + }; + function showDetail(key, focus = false) { + const detail = find('detail'); + detail.replaceChildren(); + const fixture = fixtures.get(key); + if (!fixture) { detail.hidden = true; selected = null; return; } + selected = key; + detail.hidden = false; + const title = create('h2', fixture.id); + title.id = 'ce-detail-title'; + detail.append(title, badge(fixture.status), create('p', fixture.description)); + const difference = Number(fixture.differencePercent).toFixed(4); + detail.append(create('p', `${fixture.category} · ${fixture.feature} · Above-floor page difference: ${difference}%`)); + detail.append(create('p', `Reference renderer: ${fixture.referenceRenderer}. Reference metadata: ${fixture.referenceStatus}.`)); + if (fixture.diagnosis) detail.append(create('p', `Comparator diagnosis: ${fixture.diagnosis}`)); + const links = create('p', undefined, 'ce-links'); + links.append(link('Download Fullbleed PDF', assetUrl(fixture.pdf), true), link('View HTML/CSS fixture', fixture.sourceUrl)); + const share = link('Link to this case', location.href); + const shareUrl = new URL(location.href); + shareUrl.searchParams.set('case', key); + share.href = shareUrl.href; + links.append(share); + detail.append(links); + if (fixture.referenceNote) { + const note = create('details'); + note.append(create('summary', 'Upstream reference review note'), create('p', fixture.referenceNote)); + detail.append(note); + } + const digest = create('details'); + digest.append(create('summary', 'PDF checksum'), create('p', `SHA-256: ${fixture.pdfSha256}`)); + detail.append(digest); + fixture.pages.forEach((page, index) => { + detail.append(create('h3', `Page ${index + 1}`)); + const images = create('div', undefined, 'ce-images'); + for (const [key, label] of [['reference', `${fixture.referenceRenderer} reference`], ['candidate', `Fullbleed ${data.version}`]]) { + const figure = create('figure'); + figure.append(create('figcaption', label)); + if (page[key]) { + const url = assetUrl(page[key]); + const anchor = link('', url); + anchor.setAttribute('aria-label', `Open full-size ${label}, page ${index + 1}`); + const image = document.createElement('img'); + image.src = url; + image.alt = `${label}, ${fixture.id}, page ${index + 1}`; + image.loading = 'lazy'; + image.addEventListener('error', () => { image.replaceWith(create('p', 'This preview could not load. Use the PDF or complete report.')); }); + anchor.append(image); + figure.append(anchor); + } else figure.append(create('p', 'No page on this side of the comparison.')); + images.append(figure); + } + detail.append(images); + }); + if (focus) { detail.focus({preventScroll: true}); detail.scrollIntoView({block: 'start', behavior: 'auto'}); } + } + function renderResults() { + const query = find('search').value.trim().toLowerCase(); + const category = find('category').value; + const verdict = find('verdict').value; + const filtered = data.fixtures.filter((f) => (!category || f.category === category) && (!verdict || f.status === verdict) + && `${f.key} ${f.feature} ${f.description}`.toLowerCase().includes(query)); + const pages = Math.max(1, Math.ceil(filtered.length / perPage)); + pageNumber = Math.min(pageNumber, pages - 1); + const start = pageNumber * perPage; + find('results-status').textContent = filtered.length ? `${filtered.length.toLocaleString()} matching fixtures. Showing ${start + 1}–${Math.min(start + perPage, filtered.length)}.` : 'No fixtures match these filters. Try a broader term or another verdict.'; + const results = find('results'); + results.replaceChildren(); + for (const fixture of filtered.slice(start, start + perPage)) { + const article = create('article', undefined, 'ce-result'); + const button = create('button', fixture.id); + button.type = 'button'; + button.setAttribute('aria-label', `Inspect ${fixture.id}`); + button.addEventListener('click', () => { selected = fixture.key; saveUrl(true); showDetail(fixture.key, true); }); + const metadata = create('p', undefined, 'ce-meta'); + metadata.append(badge(fixture.status), create('span', fixture.category), create('span', `${fixture.pages.length} page${fixture.pages.length === 1 ? '' : 's'}`)); + article.append(button, metadata, create('p', fixture.description)); + results.append(article); + } + find('previous').disabled = pageNumber === 0; + find('next').disabled = pageNumber >= pages - 1; + find('page').textContent = `Page ${pageNumber + 1} of ${pages}`; + } + function restoreUrl() { + const params = new URL(location.href).searchParams; + if (params.get('version') !== data.version) { location.reload(); return; } + find('search').value = params.get('query') || ''; + find('category').value = params.get('category') || ''; + find('verdict').value = params.get('verdict') || ''; + if (find('category').selectedIndex < 0) find('category').value = ''; + if (find('verdict').selectedIndex < 0) find('verdict').value = ''; + pageNumber = 0; + renderResults(); + showDetail(params.get('case')); + } + async function start() { + const versionsResponse = await fetch(versionsUrl); + if (!versionsResponse.ok) throw new Error('Package versions unavailable'); + const versions = await versionsResponse.json(); + if (versions.schema !== 'fullbleed.css-explorer-versions.v1' || !Array.isArray(versions.versions) + || !versions.versions.length || versions.versions.some(v => typeof v !== 'string' || !/^\d+\.\d+\.\d+$/.test(v)) + || new Set(versions.versions).size !== versions.versions.length || !versions.versions.includes(versions.current)) { + throw new Error('Invalid package version inventory'); + } + const requested = new URL(location.href).searchParams.get('version'); + const version = versions.versions.includes(requested) ? requested : versions.current; + indexUrl = new URL(`${version}/index.json`, versionsUrl); + for (const value of versions.versions) { + const option = create('option', `${value}${value === versions.current ? ' (current)' : ' (historical)'}`); + option.value = value; + find('version').append(option); + } + find('version').value = version; + const response = await fetch(indexUrl); + if (!response.ok) throw new Error('Fixture index unavailable'); + data = await response.json(); + if (data.schema !== 'fullbleed.css-explorer.v1' || data.packageSource !== 'published' || data.version !== version + || !Array.isArray(data.fixtures) || data.fixtures.length !== data.total) throw new Error('Invalid fixture index'); + fixtures = new Map(data.fixtures.map((f) => [f.key, f])); + if (fixtures.size !== data.total || data.fixtures.some((f) => !verdicts.includes(f.status))) throw new Error('Invalid verdict inventory'); + for (const status of verdicts) { + const count = data.fixtures.filter((f) => f.status === status).length; + if (data.counts[status] !== count) throw new Error('Inconsistent verdict counts'); + const span = create('span'); + span.append(badge(status), document.createTextNode(` ${count.toLocaleString()}`)); + find('counts').append(span); + } + for (const category of [...new Set(data.fixtures.map((f) => f.category))].sort()) { + const option = create('option', category); + option.value = category; + find('category').append(option); + } + find('provenance').append(link('Complete evidence ZIP', data.archiveUrl), + link('Raw comparison JSON', new URL('report.json', indexUrl).href), + link('Run and wheel record', new URL('fullbleed-run.json', indexUrl).href)); + const changeFilter = () => { pageNumber = 0; selected = null; saveUrl(); showDetail(null); renderResults(); }; + find('search').addEventListener('input', changeFilter); + find('category').addEventListener('change', changeFilter); + find('verdict').addEventListener('change', changeFilter); + find('version').addEventListener('change', () => { + const url = new URL(location.href); + url.searchParams.set('version', find('version').value); + location.assign(url.href); + }); + for (const [name, direction] of [['previous', -1], ['next', 1]]) find(name).addEventListener('click', () => { + pageNumber += direction; renderResults(); find('results-status').scrollIntoView({block: 'center'}); + }); + addEventListener('popstate', restoreUrl); + find('loading').textContent = `${data.total.toLocaleString()} fixtures · published Fullbleed ${data.version} · full comparison gate ${data.gatePassed ? 'passed' : 'failed'}`; + find('controls').hidden = false; + const normalized = new URL(location.href); + normalized.searchParams.set('version', data.version); + history.replaceState(null, '', normalized); + restoreUrl(); + saveUrl(); + } + start().catch(() => { + find('loading').textContent = 'The fixture browser could not load. Follow the evidence guide link above to download the complete report.'; + find('loading').classList.add('ce-error'); + find('controls').hidden = true; + }); +})(); diff --git a/docs/assets/css-corpus/history/2.5.22.json b/docs/assets/css-corpus/history/2.5.22.json new file mode 100644 index 0000000..e1ca6a8 --- /dev/null +++ b/docs/assets/css-corpus/history/2.5.22.json @@ -0,0 +1,21 @@ +{ + "schema": "fullbleed.css-corpus-publication.v1", + "packageSource": "published", + "version": "2.5.22", + "archiveUrl": "https://github.com/fullbleed-engine/fullbleed-official/releases/download/v2.5.22/fullbleed-2.5.22-published-css-corpus.zip", + "archiveSha256": "31e0072d40c77bcbb56dd73da91f0c9ab614d11d44a5ab5f8b6873f72da5c1e7", + "archiveBytes": 60212323, + "wheelSha256": "b48954edd2c1adbb17011e07f4ad4e6cc416f4d88ff7f072dcd9bc31a8b1b0e9", + "wheelFilename": "fullbleed-2.5.22-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "upstreamCommit": "0d1e53b6d8174d0a5059a8696c24e62759381f6d", + "invocationId": "fullbleed-f76772e29a594e7884800598a61c2410", + "overall": { + "score_pct": 98.48, + "pass": 1619, + "fail": 25, + "reference_disputed": 18, + "total": 1662 + }, + "gatePassed": false, + "runUrl": "https://github.com/fullbleed-engine/fullbleed-official/actions/runs/38600917475" +} diff --git a/docs/assets/css-corpus/published.json b/docs/assets/css-corpus/published.json index e1ca6a8..2043e7f 100644 --- a/docs/assets/css-corpus/published.json +++ b/docs/assets/css-corpus/published.json @@ -1,21 +1,21 @@ { "schema": "fullbleed.css-corpus-publication.v1", "packageSource": "published", - "version": "2.5.22", - "archiveUrl": "https://github.com/fullbleed-engine/fullbleed-official/releases/download/v2.5.22/fullbleed-2.5.22-published-css-corpus.zip", - "archiveSha256": "31e0072d40c77bcbb56dd73da91f0c9ab614d11d44a5ab5f8b6873f72da5c1e7", - "archiveBytes": 60212323, - "wheelSha256": "b48954edd2c1adbb17011e07f4ad4e6cc416f4d88ff7f072dcd9bc31a8b1b0e9", - "wheelFilename": "fullbleed-2.5.22-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "version": "2.5.34", + "archiveUrl": "https://github.com/fullbleed-engine/fullbleed-official/releases/download/v2.5.34/fullbleed-2.5.34-published-css-corpus.zip", + "archiveSha256": "86a6ead53b6e0ec947ea3cc3c8a4d2fd1d371756749293bd23cfd80cea3e34bc", + "archiveBytes": 60230681, + "wheelSha256": "17b75f0448fa731a0d5e875e1f40f14652579962aeb23f43a85d2fc554a5b28f", + "wheelFilename": "fullbleed-2.5.34-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", "upstreamCommit": "0d1e53b6d8174d0a5059a8696c24e62759381f6d", - "invocationId": "fullbleed-f76772e29a594e7884800598a61c2410", + "invocationId": "fullbleed-ec9e3de34cbd470db971777862a68680", "overall": { - "score_pct": 98.48, - "pass": 1619, - "fail": 25, + "score_pct": 98.54, + "pass": 1620, + "fail": 24, "reference_disputed": 18, "total": 1662 }, "gatePassed": false, - "runUrl": "https://github.com/fullbleed-engine/fullbleed-official/actions/runs/38600917475" + "runUrl": "https://github.com/fullbleed-engine/fullbleed-official/actions/runs/39337511855" } diff --git a/docs/css-coverage.md b/docs/css-coverage.md index 0a412a7..001a675 100644 --- a/docs/css-coverage.md +++ b/docs/css-coverage.md @@ -1,16 +1,15 @@ # CSS Coverage and Parity Status -The [latest reviewed source comparison](guides/css-corpus.md#latest-reviewed-source-inset-shadow-corrections) -reports **1,620 PASS, 24 FAIL and 18 REFERENCE-DISPUTED** across 1,662 fixtures. -Its overall conclusion remains **failure**, with the clipping classification and -rounded-blur difference retained. This source check does not by itself verify a -downloaded release package. - -Browse the [published 2.5.22 CSS results](guides/css-explorer.md) to search all -1,662 fixtures and inspect actual PDFs, reference images, failures and disputes. -That explorer deliberately retains its exact wheel and evidence pin; it is not -the newer source result. The [method and evidence](guides/css-corpus.md) identify -both runs. Earlier zero-failure reports below are historical, not the current gate. +The [current stable published-wheel comparison](guides/css-corpus.md#current-stable-published-wheel-corpus-2534) +tests **Fullbleed 2.5.34** and reports **1,620 PASS, 24 FAIL and 18 REFERENCE-DISPUTED** +across 1,662 fixtures. Its overall conclusion remains **failure**. The exact PyPI +wheel, report, PDFs, images and manifest verification are retained. + +[Browse the actual results](guides/css-explorer.md), or read the +[method, package identity and historical comparisons](guides/css-corpus.md). +The [component alpha source result](guides/css-corpus.md#component-alpha-source-comparison-260a9) +is recorded separately and has different verdict totals. Earlier zero-failure +reports below are historical; none establishes complete CSS conformance. Reference imported from [v2.5.22](https://github.com/fullbleed-engine/fullbleed-official/blob/e41c1b4b4395c9bd3c024935c926ba46cb791440/docs/css-coverage.md). Check the installed runtime for your exact version. diff --git a/docs/guides/comparison.md b/docs/guides/comparison.md index 4ff6ee7..f66dad7 100644 --- a/docs/guides/comparison.md +++ b/docs/guides/comparison.md @@ -109,8 +109,8 @@ Start with the integration closest to your application: - [Variable-data documents](bank-statements.md), for compiled fixed and reflowing workflows. For the current CSS evidence, [browse all 1,662 fixtures](css-explorer.md) from -the published Fullbleed **2.5.22** Linux wheel. The recorded results include -**25 FAIL** and **18 REFERENCE-DISPUTED** cases; the unchanged upstream gate +the published Fullbleed **2.5.34** Linux wheel. The recorded results include +**24 FAIL** and **18 REFERENCE-DISPUTED** cases; the unchanged upstream gate fails. Inspect the relevant cases and test your own templates. A CSS comparison does not establish PDF/A, PDF/UA, or PDF/X conformance. diff --git a/docs/guides/css-corpus.md b/docs/guides/css-corpus.md index d048cc9..09d3ef2 100644 --- a/docs/guides/css-corpus.md +++ b/docs/guides/css-corpus.md @@ -1,10 +1,58 @@ --- title: Fullbleed CSS comparison on the IronPress corpus -description: Inspect the latest reviewed source results and browse all 1,662 fixtures tested with the published Fullbleed 2.5.22 wheel, including PDFs, reference images, failures and disputes. +description: Browse all 1,662 fixtures tested with the published Fullbleed 2.5.34 wheel. Inspect exact package evidence, source comparisons, PDFs, failures and disputed references. --- # Inspect the CSS comparison -## Latest reviewed source: inset-shadow corrections +## Current stable published-wheel corpus: 2.5.34 + +[Browse every fixture](css-explorer.md){ .md-button .md-button--primary } + +The complete October 19, 2026 run tests the **published Fullbleed 2.5.34 Linux +wheel** against all 1,662 fixtures at pinned IronPress revision +`0d1e53b6d8174d0a5059a8696c24e62759381f6d`. It records +**1,620 PASS, 24 FAIL and 18 REFERENCE-DISPUTED**. The unchanged +comparison gate **fails**; its original reasons, failed cases and disputed +references remain in the report. + +The wheel was downloaded from PyPI, and its SHA-256 matches PyPI's file record. +All **1,662 candidate PDFs** and all **5,489 compared PDF/PNG files** match the +[reviewed 2.5.34 source run](https://github.com/fullbleed-engine/fullbleed-official/actions/runs/39208485335) +byte for byte. Fixture verdicts, raw and above-floor differences, reference +metadata and raster fingerprints also match. The downloadable summary records +the complete manifest verification and the exact scope of that comparison. + +[Complete published-wheel evidence (57.4 MiB)](https://github.com/fullbleed-engine/fullbleed-official/releases/download/v2.5.34/fullbleed-2.5.34-published-css-corpus.zip) +· [Structured verification](https://github.com/fullbleed-engine/fullbleed-official/releases/download/v2.5.34/fullbleed-2.5.34-published-css-summary.json) +· [SHA-256 checksums](https://github.com/fullbleed-engine/fullbleed-official/releases/download/v2.5.34/fullbleed-2.5.34-published-css-SHA256SUMS.txt) +· [Workflow run](https://github.com/fullbleed-engine/fullbleed-official/actions/runs/39337511855) +· [Explorer build pin](../assets/css-corpus/published.json) + +Extract the ZIP once and open `ironpress-evidence/current/reports/index.html` +for the offline report. The upstream report calls its candidate “ironpress”; +the candidate renderer in this run is Fullbleed. The online explorer preserves +the original candidate/reference images, tested PDFs and fixture classifications. + +These results apply to this exact Linux wheel, explicit assets and pinned +comparison policy. They do not establish complete CSS support, arbitrary website +rendering, or PDF standards conformance. The historical runs below keep their +original versions and results. + +## Component alpha source comparison: 2.6.0a9 + +The [alpha 9 source run](https://github.com/fullbleed-engine/fullbleed-official/actions/runs/39310439709) +tests commit `18154ba3003ead91be7ce324e302201942848a96` against the same 1,662 +fixtures. It records **1,619 PASS, 25 FAIL and 18 REFERENCE-DISPUTED** and retains +the failed gate. This is a separate **source-built wheel** result; it is not the +stable 2.5.34 result or a complete corpus check of the downloaded alpha package. + +[Download the unchanged alpha source evidence](https://github.com/fullbleed-engine/fullbleed-official/releases/download/v2.6.0a9/fullbleed-css-corpus-2.6.0a9.zip) +or read the [alpha guide and its qualification limits](component-preview-9.md). +The current interactive explorer uses the published stable wheel above. + + + +## Historical source comparison: inset-shadow corrections The [inset-shadow fix in PR #92](https://github.com/fullbleed-engine/fullbleed-official/pull/92) was checked against the complete pinned 1,662-fixture corpus on October 16, 2026. @@ -27,19 +75,21 @@ Chrome print. These differences and the failed verdict are retained in the No comparator reference or threshold was changed. This is **source verification**, not a complete corpus run against a downloaded -2.5.28 package. The online explorer below continues to show the exact published -2.5.22 wheel identified in its build pin. Older zero-failure reports are -historical; they do not describe the current corpus result or establish complete -CSS conformance. +2.5.28 package. The current explorer shows the published 2.5.34 wheel identified +above; the historical 2.5.22 package record is retained below. Older zero-failure +reports do not describe the current corpus result or establish complete CSS +conformance. -## Latest complete published-wheel corpus: 2.5.22 + -[Browse every fixture](css-explorer.md){ .md-button .md-button--primary } +## Historical published-wheel corpus: 2.5.22 -Search by CSS feature, category or verdict, compare the printed pages, and -download the actual PDF without extracting an archive. +[Browse the historical 2.5.22 results](css-explorer.md?version=2.5.22) or use the +complete archive below and its retained +[versioned fixture index](https://docs.fullbleed.dev/assets/css-corpus/2.5.22/index.json). Existing versioned +PDF and preview URLs retain their original bytes. The complete October 14, 2026 run tests the **published Fullbleed 2.5.22 Linux wheel** against the same pinned 1,662-fixture corpus. It records **1,619 PASS, @@ -58,10 +108,10 @@ also match. The downloaded wheel's SHA-256 matches PyPI's published file record. For the original offline report, extract this ZIP once and open `ironpress-evidence/current/reports/index.html`. The upstream report labels its -candidate “ironpress”; that candidate is Fullbleed in this run. The online -browser presents the same original PDF and preview bytes, verdicts and reference -notes. It does not change the comparator's policy or promote disputed cases to -passes. +candidate “ironpress”; that candidate is Fullbleed in this run. The retained +versioned files preserve the original PDF and preview bytes, verdicts and reference +notes. Neither this record nor the current explorer changes the comparator's +policy or promotes disputed cases to passes. These results apply to the exact Linux wheel, explicit assets, pinned references and comparison policy. They do not establish complete CSS support, arbitrary diff --git a/docs/guides/css-explorer.md b/docs/guides/css-explorer.md index 5ebb0f4..3cb925a 100644 --- a/docs/guides/css-explorer.md +++ b/docs/guides/css-explorer.md @@ -1,6 +1,6 @@ --- title: Browse Fullbleed's CSS comparison results -description: Search all 1,662 CSS fixtures tested with the published Fullbleed 2.5.22 wheel. Inspect original verdicts, reference images and generated PDFs, including failures and disputed references. +description: Search all 1,662 CSS fixtures tested with the published Fullbleed 2.5.34 wheel. Inspect original verdicts, reference images and generated PDFs, including failures and disputed references. hide: - toc --- @@ -8,19 +8,25 @@ hide: # See what the CSS tests render Find a layout or CSS feature, compare the printed pages, and download the actual -Fullbleed PDF. This is a maintainer-run comparison of the **published Fullbleed -2.5.22 Linux wheel** against the pinned, independent IronPress corpus. +Fullbleed PDF. The default view shows a maintainer-run comparison of the **published Fullbleed +2.5.34 Linux wheel** against the pinned, independent IronPress corpus. + +Choose a package version to inspect its own results. Shared case links keep the +selected version, filters and fixture. You can also [open the historical 2.5.22 +results](css-explorer.md?version=2.5.22) or read their +[evidence record](css-corpus.md#latest-published-wheel-2522). **The comparison gate fails.** Passing fixtures meet the comparator's policy; they do not establish complete CSS support or pixel-identical output. Failures and disputed references retain their original labels. [Read the method and limits](css-corpus.md). - -
+ +

Loading the fixture index…

- + diff --git a/docs/guides/renderer-comparison.md b/docs/guides/renderer-comparison.md index d5707f7..28bde42 100644 --- a/docs/guides/renderer-comparison.md +++ b/docs/guides/renderer-comparison.md @@ -98,4 +98,4 @@ The merged revision above contains the same benchmark files as the measured sour Fullbleed is a print-document engine. A browser remains relevant when JavaScript or live browser layout is the required behavior. See [Choose a PDF library](comparison.md) for that distinction and [CSS coverage](../css-coverage.md) for Fullbleed's tested surface. Three small fixtures on one host cannot establish a universal speed, memory, or quality ranking. -For a larger fixture-level comparison, [browse the published Fullbleed 2.5.22 results on the pinned IronPress CSS corpus](css-explorer.md). That run uses different inputs, versions, and comparison rules; evaluate its results separately from these application-document timings. The [corpus evidence guide](css-corpus.md) retains the failures, disputed references, and earlier release results. +For a larger fixture-level comparison, [browse the published Fullbleed 2.5.34 results on the pinned IronPress CSS corpus](css-explorer.md). That run uses different inputs, versions, and comparison rules; evaluate its results separately from these application-document timings. The [corpus evidence guide](css-corpus.md) retains the failures, disputed references, and earlier release results. diff --git a/tools/check_css_explorer.py b/tools/check_css_explorer.py index 11b626e..b8861af 100644 --- a/tools/check_css_explorer.py +++ b/tools/check_css_explorer.py @@ -7,7 +7,7 @@ from pathlib import Path import re import threading -from urllib.parse import quote, unquote, urlsplit +from urllib.parse import parse_qs, quote, unquote, urlsplit from playwright.sync_api import expect, sync_playwright from pypdf import PdfReader @@ -27,6 +27,7 @@ def main(): index_bytes = (root / index_path).read_bytes() index = json.loads(index_bytes) fixtures = {row['key']: row for row in index['fixtures']} + historical_indices = [] checks, errors, downloads = [], [], [] prefix = '/nested/docs/' @@ -63,7 +64,7 @@ def check(name, passed): url = base + 'guides/css-explorer/' result = {'startedAt': datetime.now(timezone.utc).isoformat(), 'url': url, 'version': index['version'], 'archiveSha256': publication['archiveSha256'], - 'checks': checks, 'downloads': downloads, 'errors': errors} + 'checks': checks, 'downloads': downloads, 'historicalDownloads': [], 'errors': errors} try: with sync_playwright() as pw: browser = pw.chromium.launch(**({'channel': 'chrome'} if args.browser == 'chrome' else {})) @@ -74,10 +75,34 @@ def check(name, passed): try: response = context.request.get(base + index_path) check('served index preserves all verified bytes', response.ok and response.body() == index_bytes) + for asset in ('explorer.js', 'explorer.css'): + asset_path = 'assets/css-corpus/' + asset + response = context.request.get(base + asset_path) + check('legacy browser asset URL remains available unchanged: ' + asset, + response.ok and response.body() == (root / asset_path).read_bytes()) + for config_path in sorted((root / 'assets/css-corpus/history').glob('*.json')): + historical = json.loads(config_path.read_text(encoding='utf-8')) + historical_prefix = f"assets/css-corpus/{historical['version']}/" + historical_bytes = (root / historical_prefix / 'index.json').read_bytes() + response = context.request.get(base + historical_prefix + 'index.json') + check('historical index remains byte-identical: ' + historical['version'], + response.ok and response.body() == historical_bytes) + historical_index = json.loads(historical_bytes) + assert historical_index['version'] == historical['version'] + assert historical_index['archiveUrl'] == historical['archiveUrl'] + historical_indices.append((historical, historical_index)) + for verdict in ('PASS', 'FAIL', 'REFERENCE-DISPUTED'): + fixture = next(row for row in historical_index['fixtures'] if row['status'] == verdict) + response = context.request.get(base + historical_prefix + fixture['pdf']) + check(f"historical {verdict} PDF URL preserves its bytes: {historical['version']}", + response.ok and sha256(response.body()).hexdigest() == fixture['pdfSha256']) page.goto(url, wait_until='domcontentloaded') expect(page.locator('#ce-controls')).to_be_visible() expect(page.locator('#ce-loading')).to_contain_text('1,662 fixtures') expect(page.locator('#ce-loading')).to_contain_text('full comparison gate failed') + expect(page.get_by_label('Package version', exact=True)).to_have_value(index['version']) + check('current package is explicit in the selectable version and shared URL', + parse_qs(urlsplit(page.url).query).get('version') == [index['version']]) for verdict, count in index['counts'].items(): expect(page.locator('#ce-counts')).to_contain_text(f'{verdict} {count:,}') check('all original verdict totals and failed gate are visible', True) @@ -91,7 +116,7 @@ def check(name, passed): page.locator('#ce-previous').click() expect(page.locator('.ce-result button').first).to_have_text(first) page.evaluate('scrollTo(0, 0)') - page.screenshot(path=str(out / 'desktop.png')) + page.screenshot(path=str(out / 'desktop.png'), animations='disabled') page.locator('#ce-verdict').select_option('FAIL') expect(page.locator('#ce-results-status')).to_contain_text(f"{index['counts']['FAIL']} matching fixtures") @@ -149,7 +174,7 @@ def download(key): page.wait_for_function('(img) => img.complete && img.naturalWidth > 0', arg=item.element_handle()) check('both original page previews load in a real browser', images.count() == 2) page.locator('#ce-detail').evaluate('(el) => el.scrollIntoView({block: "start"})') - page.screenshot(path=str(out / 'desktop-detail.png')) + page.screenshot(path=str(out / 'desktop-detail.png'), animations='disabled') page.get_by_title('Switch to dark mode', exact=True).click() expect(page.locator('body')).to_have_attribute('data-md-color-scheme', 'slate') expect(page.get_by_role('link', name='Download Fullbleed PDF')).to_have_css('color', 'rgb(142, 222, 195)') @@ -172,12 +197,67 @@ def download(key): expect(page.locator('#ce-results-status')).to_contain_text('No fixtures match') expect(page.locator('#ce-detail')).to_be_hidden() check('empty results clear selection without interpreting input as HTML', page.locator('#css-explorer img').count() == 0) - page.goto(url + '?category=unknown&verdict=unknown&case=unknown', wait_until='domcontentloaded') + page.goto(url + '?version=unknown&category=unknown&verdict=unknown&case=unknown', wait_until='domcontentloaded') expect(page.locator('#ce-controls')).to_be_visible() expect(page.locator('#ce-category')).to_have_value('') expect(page.locator('#ce-verdict')).to_have_value('') expect(page.locator('#ce-detail')).to_be_hidden() check('invalid shared filters fall back to all fixtures', page.locator('.ce-result').count() == 12) + expect(page.get_by_label('Package version', exact=True)).to_have_value(index['version']) + check('unsupported package versions normalize to the current published version', + parse_qs(urlsplit(page.url).query).get('version') == [index['version']]) + + for historical, historical_index in historical_indices: + changed = next((row for row in historical_index['fixtures'] + if row['status'] != fixtures[row['key']]['status']), historical_index['fixtures'][0]) + selected_url = (url + '?case=' + quote(changed['key']) + '&category=' + quote(changed['category']) + + '&q=' + quote(changed['id']) + '&version=' + quote(index['version'])) + page.goto(selected_url, wait_until='domcontentloaded') + expect(page.locator('#ce-detail .ce-verdict')).to_have_text(fixtures[changed['key']]['status']) + query = parse_qs(urlsplit(page.url).query) + check('legacy fixture search links migrate without opening site search: ' + historical['version'], + query.get('query') == [changed['id']] and 'q' not in query + and not page.locator('#__search').is_checked()) + page.get_by_label('Package version', exact=True).select_option(historical['version']) + expect(page.locator('#ce-loading')).to_contain_text('published Fullbleed ' + historical['version']) + expect(page.locator('#ce-detail-title')).to_have_text(changed['id']) + expect(page.locator('#ce-search')).to_have_value(changed['id']) + expect(page.locator('#ce-category')).to_have_value(changed['category']) + expect(page.locator('#ce-detail .ce-verdict')).to_have_text(changed['status']) + for verdict, count in historical_index['counts'].items(): + expect(page.locator('#ce-counts')).to_contain_text(f'{verdict} {count:,}') + expect(page.get_by_role('link', name='Complete evidence ZIP')).to_have_attribute('href', historical['archiveUrl']) + shared = page.get_by_role('link', name='Link to this case').get_attribute('href') + check('switching package versions preserves the selected case and filters: ' + historical['version'], + parse_qs(urlsplit(shared).query).get('version') == [historical['version']]) + page.reload(wait_until='domcontentloaded') + expect(page.locator('#ce-loading')).to_contain_text('published Fullbleed ' + historical['version']) + expect(page.locator('#ce-detail .ce-verdict')).to_have_text(changed['status']) + check('shared historical selection survives reload: ' + historical['version'], page.url == shared) + check('shared historical link leaves mobile site search closed: ' + historical['version'], + not page.locator('#__search').is_checked()) + with page.expect_download() as pending: + page.get_by_role('link', name='Download Fullbleed PDF').click() + target = out / f"historical-{historical['version']}-{changed['id']}.pdf" + pending.value.save_as(target) + digest = sha256(target.read_bytes()).hexdigest() + pages = len(PdfReader(target).pages) + check('selected historical PDF downloads the original package bytes: ' + historical['version'], + digest == changed['pdfSha256'] and pages == sum(bool(p['candidate']) for p in changed['pages'])) + result['historicalDownloads'].append({'version': historical['version'], 'key': changed['key'], + 'sha256': digest, 'pages': pages, 'file': target.name}) + page.locator('#ce-detail').evaluate('(el) => el.scrollIntoView({block: "start"})') + page.screenshot(path=str(out / f"historical-{historical['version']}.png"), animations='disabled') + top = page.get_by_role('button', name='Back to top', exact=True) + check('historical heading clears the mobile floating navigation: ' + historical['version'], + not top.is_visible() or page.locator('#ce-detail-title').bounding_box()['y'] + >= top.bounding_box()['y'] + top.bounding_box()['height']) + page.go_back(wait_until='domcontentloaded') + expect(page.locator('#ce-loading')).to_contain_text('published Fullbleed ' + index['version']) + expect(page.locator('#ce-detail-title')).to_have_text(changed['id']) + expect(page.locator('#ce-detail .ce-verdict')).to_have_text(fixtures[changed['key']]['status']) + check('browser Back restores the previous package and case: ' + historical['version'], + parse_qs(urlsplit(page.url).query).get('version') == [index['version']]) unequal = next(row for row in index['fixtures'] if any(not p['candidate'] or not p['reference'] for p in row['pages'])) @@ -200,6 +280,13 @@ def download(key): expect(page.locator('#ce-controls')).to_be_hidden() expect(page.get_by_role('link', name='Read the method and limits')).to_be_visible() check('index outage preserves a useful evidence-guide fallback', True) + page.unroute('**/' + index_path) + page.route('**/assets/css-corpus/versions.json*', lambda route: route.fulfill(status=503, body='Unavailable')) + page.reload(wait_until='domcontentloaded') + expect(page.locator('#ce-loading')).to_contain_text('The fixture browser could not load') + expect(page.locator('#ce-controls')).to_be_hidden() + expect(page.get_by_role('link', name='Read the method and limits')).to_be_visible() + check('version inventory outage preserves the evidence-guide fallback', True) check('no uncaught browser errors', not errors) result['passed'] = True except Exception: diff --git a/tools/prepare_css_corpus_site.py b/tools/prepare_css_corpus_site.py index a4afbab..872a7f1 100644 --- a/tools/prepare_css_corpus_site.py +++ b/tools/prepare_css_corpus_site.py @@ -130,7 +130,7 @@ def verified(name): summary = {'schema': 'fullbleed.css-explorer-build.v1', 'archiveSha256': config['archiveSha256'], 'counts': expected, 'fixtures': len(fixtures), 'files': written, 'scope': 'Presentation copy of verified published-wheel evidence. Original fixture verdicts, reference metadata, images and PDF bytes are preserved.'} - (output / 'site-manifest.json').write_text(json.dumps(summary, indent=2) + '\n', encoding='utf-8') + (output / 'site-manifest.json').write_text(json.dumps(summary, indent=2) + '\n', encoding='utf-8', newline='\n') print(json.dumps({'fixtures': len(fixtures), 'counts': expected, 'files': len(written), 'bytes': sum(item['bytes'] for item in written.values()), 'gatePassed': provenance['gate_passed']})) @@ -141,4 +141,20 @@ def verified(name): parser.add_argument('--config', type=Path, default=ROOT / 'docs/assets/css-corpus/published.json') parser.add_argument('--archive', type=Path) args = parser.parse_args() + # Previously published evidence URLs must continue to resolve when the + # current explorer advances to another package. Each historical manifest + # retains its own immutable archive and is checked just like the current one. + historical = sorted((args.config.parent / 'history').glob('*.json')) + versions = set() + for config_path in [*historical, args.config]: + config = json.loads(config_path.read_text(encoding='utf-8')) + assert config['version'] not in versions, f'Duplicate published evidence version: {config["version"]}' + versions.add(config['version']) + for config_path in historical: + prepare(args.site, config_path) prepare(args.site, args.config, args.archive) + current = json.loads(args.config.read_text(encoding='utf-8'))['version'] + inventory = {'schema': 'fullbleed.css-explorer-versions.v1', 'current': current, + 'versions': [current, *[json.loads(path.read_text(encoding='utf-8'))['version'] for path in historical]]} + (args.site / 'assets/css-corpus/versions.json').write_text( + json.dumps(inventory, indent=2) + '\n', encoding='utf-8', newline='\n') diff --git a/tools/sync_reference.py b/tools/sync_reference.py index 8a1e030..35f70bd 100644 --- a/tools/sync_reference.py +++ b/tools/sync_reference.py @@ -30,9 +30,10 @@ "regular and bold Noto Sans Arabic files with a font manifest and glyph checks." ), "css-coverage.md": ( - "See the [complete 1,662-fixture CSS comparison and release follow-ups]" - "(guides/css-corpus.md), including the published 2.5.14 baseline, " - "the reviewed 2.5.22 source candidate, remaining failures, PDFs, images, and hashes. " + "See the [current published-wheel comparison and release follow-ups]" + "(guides/css-corpus.md), or [browse every fixture](guides/css-explorer.md). " + "Those pages identify the exact tested package, remaining failures, disputed " + "references, PDFs, images, and hashes. " "The imported reference below preserves earlier version-specific reports." ), "guides/performance.md": (