Files
pbakaus_impeccable/crates/html/tests/fixtures/gen-css-rules.mjs
T
Paul BakausandClaude Fable 5.1 0547ed6a63 reorg C: the open Rust runtime joins this repo as one Cargo workspace
The engine no longer lives in a separate repo. `crates/` is a snapshot of the
open crates (foundation, core, common, context, live, hook, skills, comp,
comp-verbs, html, browser, detect, cli) plus `Cargo.lock`, taken as a git
archive of the engine repo at the commit that finished the boundary split.
None of that repo's history comes with it, and none of it should: the closed
half stays private.

The closed half is the rule engine. It ships as a prebuilt native archive per
target, `libimpeccable_detector.a`, published as a `detector-v<X>` GitHub
Release on this repo. `crates/core/build.rs` resolves and links it three ways:
`IMPECCABLE_DETECTOR_LIB=<dir>` for a local detector build, else the
`~/.impeccable/detector/<version>/<target>/` cache, else a download verified
against its `.sha256` sidecar. `crates/core` is a thin shim over a three-symbol
C ABI; nothing above it knows the boundary exists.

What changed versus the engine repo copy:

- Every crate manifest moves from `license-file.workspace` to
  `license.workspace` (this workspace declares Apache-2.0), and the workspace
  gains the `postcard` dependency the boundary encoding needs.
- The launcher contract test reads `skill/scripts/impeccable{,.cmd}` instead of
  a sibling `launcher/` dir, and `engine_binary` downloads from
  `github.com/pbakaus/impeccable/releases/download/engine-v<version>/` instead
  of the retired dist repo. No oracle golden carried the old URL, so no
  re-recording was owed.
- The tests that hunted for a public repo through `IMPECCABLE_PUBLIC_REPO`,
  `../impeccable-second` or a hardcoded home directory now resolve the root as
  `CARGO_MANIFEST_DIR/../..`, because they are in it. The env var stays as an
  override for an out-of-tree checkout.
- The in-page bundle (`detect-antipatterns-browser.js`, 2 MB of generated wasm
  glue) is no longer tracked. `crates/core/build.rs` resolves it beside the
  archive, hands the path to `impeccable_core::browser::IN_PAGE_BUNDLE_JS`, and
  live mode serves that. `scripts/check-detector-release.mjs` now requires it
  and its `.sha256` in a detector release.
- The live crate embeds `skill/scripts/live-browser*.js` and
  `modern-screenshot.umd.js` directly rather than through vendored copies, so
  the binary and the installed skill cannot drift.
- `crates/browser/assets/` (an unused second copy of the bundle) is gone.
- `tests/lib/engine-bin.mjs` also accepts `target/release/impeccable`, so a
  plain `cargo build --release -p impeccable` is enough to run `bun run test`.

Verified with the archive from a local detector build: `cargo test --workspace`
267 pass, oracle 795 pass / 0 fail / 0 missing, `bun run build` clean, the
default suite green, and the launcher's `engine-probe` handshake answering
through `skill/scripts/impeccable`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
2026-09-01 15:31:26 -07:00

189 lines
10 KiB
JavaScript

// Regenerates css-rules.json: the expected output of the JS
// `collectStaticCssRules` / `unwrapCssAtLayer` (css-cascade.mjs + css-tree)
// over the synthetic corpus below plus every <style> block and .css file
// under the repo's tests/, skill/ and cli/.
//
// node crates/html/tests/fixtures/gen-css-rules.mjs
//
// FROZEN. `cli/engine/engines/static-html/css-cascade.mjs` was removed when
// the runtime went node-free, so this cannot run against this repo; it is kept
// as the record of how css-rules.json was produced. IMPECCABLE_PUBLIC_REPO
// points it at a checkout old enough to still have that module (css-tree is
// loaded from that checkout's node_modules).
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const candidates = [
process.env.IMPECCABLE_PUBLIC_REPO,
path.resolve(here, '..', '..', '..', '..'),
].filter(Boolean);
const root = candidates.find(p => fs.existsSync(path.join(p, 'cli', 'engine', 'engines', 'static-html', 'css-cascade.mjs')));
if (!root) throw new Error('css-cascade.mjs not found; set IMPECCABLE_PUBLIC_REPO to a pre-node-free checkout');
const require = createRequire(path.join(root, 'package.json'));
const csstree = require('css-tree');
const m = await import(path.join(root, 'cli', 'engine', 'engines', 'static-html', 'css-cascade.mjs'));
const synthetic = [
// nested @media / @supports / @layer
'@media (max-width:600px){ .a{color:red} @supports (display:grid){ .b{color:blue}}}',
'@media screen { @media (min-width:1px) { .a{color:red} } }',
'@supports (display:grid) and (not (display:inline-grid)) { .grid{display:grid} }',
'@layer base { .a{color:red} } @layer{ .b{color:red} } @layer a, b; @layer x.y { .c{color:green} }',
'@layer utilities { @media (min-width: 640px) { .sm\\:flex { display: flex } } }',
'@media print { @layer foo { .p{margin:0} } }',
'@layer a { @layer b { @layer c { .deep{x:y} } } }',
'@layer reset, base; @layer base { html{margin:0} } @layer reset { *{box-sizing:border-box} }',
'@supports not (display:grid) { @media (max-width: 400px) { @layer x { .m{n:o} } } }',
'@media (min-width: 640px) { .container { max-width: 640px } } @media (min-width: 768px) { .container { max-width: 768px } }',
'@media (prefers-reduced-motion: reduce) { *, ::before, ::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important } }',
'@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) { .glass { backdrop-filter: blur(12px) } }',
'@media (a{b) { .x{c:d} } .y{e:f}',
'@media (a; b) { .x{c:d} } .y{e:f}',
'@media (a; b { .x{c:d} } .y{e:f}',
'@media (a) foo { .x{c:d} } .y{e:f}',
'@media { .x{c:d} } @media; .y{e:f} @media',
'@media{}.b{c:d} @MEDIA (x){a{b:c}} @Layer X{a{b:c}} @supports(x:y){a{b:c}}',
'@media (x){ .b {c:d} .c{ }} ',
'a{b:x;@media (y){c:d}} e{f:g}',
// :hover shapes
'.a:hover, .b:hover > .c, .d:hover.e, .f:hover ~ , .g:hover{color:red}',
'a:hover>b{c:d} a:hover+b{c:d} a:hover b{c:d} a:hover >{c:d} a:hover b:hover{c:d} :hover{c:d} a:not(:hover){c:d} a:hover:focus{c:d}',
'a:hover, :hover.b, .b :hover .c, > a:hover{c:d}',
'.a:HOVER{color:red} .a:hover-x{c:d} .hover{m:n} a:hovering{o:p} a\\:hover{c:d} a[data-x=":hover"]{c:d}',
'.a :hover .b{c:d} .a :hover .b{c:d} :hover > .b{c:d} .a > :hover{c:d} .a ~ :hover ~ .b{c:d}',
'.a:hover::before{content:""} .btn:hover:not(:disabled){color:red}',
// comma selectors
'a, b, c , d{color:red}',
'.a[data-x="a,b"], .b:is(.x, .y){color:red}',
// !important
'.a{color:red !important; b: c ! important ; d: e!IMPORTANT; f: g !ie; h: i!important !important}',
'a{b:c!important !important} a{b:c !important d} a{b:c!importantx} a{b:!important} a{ b : c ; } a{b: c!important} a{b:c ! important}',
'.a{b: 1px !important; c: 2px ! important; d: 3px !important ; e:4px!important;}',
// custom properties
'.a { --x: red ; color: var(--x); --y: { a: b } ; --z:; --w: ; --empty: ;}',
':root{--brand: #87a8ff; --pad: 4px 8px } .card{border-left: 5px solid var(--brand); padding: var(--pad, 1px)}',
'.a{ color : var( --x , red ) ; width: var(--w); z: var(--a,var(--b, 1px))}',
// malformed CSS recovery
'.a{color:red;;;} .b{color:red',
'.a{color:red} } .b{color:blue}',
'.a{color:red;}}}.b{color:blue}',
'.a{color:red}@media (a){.b{color:blue}',
'.a { color: red } .b { color: blue',
'a{b:c;d',
'a{b}',
'a{color:}',
'{a:b}',
'}',
'{',
'a{',
'a{}',
'',
' ',
'a',
'.a{color:red}"str}"{}.b{color:blue}',
'a{color:red;background:blue{}} a{color:red;background:blue{c:d} e:f;g:h;} i{j:k}} l{m:n}',
'a{b:x;c:d{e:f}} a{b:x;c:hover{d:e}} a{b:x;c{d:e}} a{color:red;--y} a{;b:c} a{b:c;;d:e}',
'a{b:x;&:hover{c:d}} a{&:hover{c:d}} a{b:x;> c{d:e}} a{b:x;.c{d:e}}',
'.a{content:"\\\n"} .b{content:"multi\\\nline"} .c{content:"bad\nstring"; color:red}',
'.a{c:"unterminated} .b{color:red}',
'.a{c:url(unterminated} .b{color:red}',
'.a{c:url("unterminated} .b{color:red}',
'.a{c:calc(1px + } .b{color:red}',
'.a{c:[ } .b{color:red}',
'.a{width:50%} 50%{c:d} 5{c:d} .5{c:d} 5px{c:d} +a{c:d} .a + {c:d} .a > {c:d} > .a{c:d} ~{c:d}',
// comments
'/* only */',
'.a{color: /* comment */ red; /*c*/ background:blue} /* between */ .b{c:d}',
'.a{color:red}/*}*/.b{color:blue}',
'.a{color:red}\n/* trailing',
'/*! bang */ .a{color:red}',
'a::after{content:"}"} b{c:d} c{d:"}"} e{f:g} h{i:c /* } */} d{e:f} j{b:url(}) } k{e:f}',
'a{b:red /* x */ blue; c: red\n blue\t green; d:(1+2); e:[a b]; f:{a:b}; g:foo(bar(baz)); h:a,b,,c; i:a , , b; j:foo( , ); k:foo(1 2)}',
'a{b:1px !important;} a{b: x } a{b:x\n} a{b:x;\n} a{b:x /**/} a{b:x/**/y} a{b:x/**/} a{b:/**/x}',
// @import / @font-face / @keyframes / other at-rules
'.a{color:red}<!-- .b{color:blue} -->',
"@import url(x.css); @import 'y.css' layer(base) supports(display:grid) screen; .z{color:red}",
'@font-face{font-family:x; src: url(x.woff2) format("woff2")} a{b:c}',
'@keyframes x { from{opacity:0} to{opacity:1} } @-webkit-keyframes k2{from{a:b}} @media(x){@keyframes k3{from{a:b}} a{b:c}} .z{color:red}',
'@page{margin:0} @page :first{margin:1px} @container (min-width: 1px){ .a{color:red}} @scope (.x){ .y{c:d}} @starting-style{ .z{opacity:0}}',
'@charset "utf-8"; @namespace svg url(http://www.w3.org/2000/svg); a{b:c}',
'@font-face{font-family:x} @media (x){ @font-face{font-family:y} .z{c:d} }',
'a{b:c}@font-face{d:e}f{g:h}',
'.a{color:red}@media{}.b{c:d}',
// value normalization
'.A > .B{COLOR:Red} html{-webkit-text-size-adjust:100%} a{width:100PX;color:Rgb(1,2,3)}',
'.a{background: url( foo.png ) no-repeat , linear-gradient( 90deg , red 0% , blue 100% )}',
'.a{transition: all .3s ease-in-out,color 1s; animation: spin 1s linear infinite}',
'.a{font: italic bold 12px/30px Georgia, serif; font: 12px / 1.5 x}',
'.a{width: calc( 100% - 2px ); height: calc(1px + 2px*3); x: calc( (1 + 2) * 3 )}',
'.a{margin:-1px +2px .5em 1.50px 010px; b:1 -2; c:1 - 2; d:1-2; e: 1px+2px; f: a+b; g:1*2}',
'.a{transform: translate(10px,20px) rotate(45deg); filter: blur( 2px ); grid-template-columns: repeat( auto-fill , minmax( 100px , 1fr ) )}',
'.a{color:#FFF; background:#abcdef80; content:"a b"; font-family: "Foo Bar" , sans-serif; x: \'single\'; y: "esc\\"aped"}',
'.a::before{content:"\\201C"} .b::after{content:"\\"}"} .c{content:\'}\'}',
'.a{background:URL(x.png); b:url( "x" ); c:url("x y.png"); d:url(x y.png); e:url(x)y; f:image-set(url(x) 1x)}',
'.a{color:rgb(0 0 0 / 50%); b: rgb( 1 , 2 , 3 ); c: hsl(120deg 100% 50%)}',
'.a{unicode-range: U+0025-00FF, u+4??; x: 1/2 ; y: a/b; aspect-ratio: 16 / 9}',
'a{b:1E3;c:1e-3px;d:.5;e:0.50;f:+.5;g:1.2.3;h:1..2;i:.;j:-;k:--;l:--x;m:@x;n:$x;o:x$;p:x @y;q:#;r:#ggg}',
'a{*b:c;_d:e;b:1\\9;c:expression(1);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#80000000,endColorstr=#80000000)}',
'a{b:c;}\r\nd{e:f} g{h:i;}\f j{k:l} m{n:o;}\v p{q:r}',
'.a{margin: 0 0 0 0} .b{margin: 0 auto 0px 1e3px} .c{padding: 1px 2px 3px}',
'.a{b:1px 2px} .a{b:1px2px} .a{b:1e3 1e-3} .a{b:#fff #000} .a{b:x#fff} .a{b:#fff x} .a{b:1px#fff} .a{b:x -y} .a{b:x --y} .a{b:x -1} .a{b:) x} .a{b:x ) y}',
// selector normalization
'*{a:b} :root{c:d} ::selection{e:f} :root:hover{g:h}',
'a:is(.x,.y):where(#z){c:d} a:not( .x , .y ){c:d} :where(.a .b, .c) > d{e:f} :has(> img){c:d}',
'li:nth-child(2n+1){c:d} li:nth-child( 2n + 1 ){c:d} li:nth-child(odd){c:d} li:nth-child(-n+3){c:d} li:nth-child(n){c:d} li:nth-of-type(3){c:d} li:nth-child(2 of .x){c:d} li:nth-last-child(+2n-1){c:d}',
'p:lang(en, "fr"){c:d} ::slotted(span){c:d} :host(.x){c:d} :host-context(body.dark){c:d} :dir(rtl){c:d} a:foo( bar baz ){c:d} a:foo(){c:d} a::part(x){c:d}',
'a > b , c+d ~ e f{color:red} g > h{i:j} k\n>\nl{m:n} a /deep/ b{c:d} *|* {c:d} |a{c:d} svg|circle{c:d}',
'#id{c:d} a#id{c:d} #id.cls{c:d} .cls#id{c:d} #a#b{c:d} a#b c{d:e} #\\31 23{c:d}',
'a[b]{c:d} a[b=c]{c:d} a[b="c"]{c:d} a[b=\'c\']{c:d} a[ b |= c ]{c:d} a[b="c" i]{c:d} a[b=c s]{c:d} a[ data-x = "y" i ]{c:d} a[*|b]{c:d} a[|b]{c:d}',
// unicode / escapes / BOM
'\uFEFF.a{color:red}',
'.é{color:red} .日本{color:blue} a[data-x="ünïcode"]{c:d} .a{content:"emoji 😀"} .b{font-family:"Nöto"}',
'.a{c:\\26 B; d: \\000026x; e:\\26x} .\\31 0{c:d} .a\\ b{c:d}',
'.a{b:c} \u00a0 d{e:f}',
'.a{b:c} \u2028 d{e:f}',
];
function extractStyles(html) {
const out = [];
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
let m;
while ((m = re.exec(html)) !== null) out.push(m[1]);
return out.join('\n');
}
const cases = [];
for (const css of synthetic) cases.push({ name: 'synthetic', css });
function walk(dir, acc) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
const p = path.join(dir, e.name);
if (e.isDirectory()) walk(p, acc);
else if (/\.(html|css)$/.test(e.name)) acc.push(p);
}
}
const files = [];
walk(path.join(root, 'tests'), files);
walk(path.join(root, 'skill'), files);
walk(path.join(root, 'cli'), files);
files.sort();
for (const f of files) {
const text = fs.readFileSync(f, 'utf8');
const css = f.endsWith('.css') ? text : extractStyles(text);
if (!css.trim()) continue;
cases.push({ name: path.relative(root, f), css });
}
let total = 0;
for (const c of cases) {
c.rules = m.collectStaticCssRules(c.css, csstree);
c.unwrapped = m.unwrapCssAtLayer(c.css);
total += c.rules.length;
}
fs.writeFileSync(path.join(here, 'css-rules.json'), JSON.stringify(cases));
console.log(cases.length, 'cases', total, 'rules', files.length, 'files');