mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-16 16:16:32 +03:00
The detector is open source. The rules it ships were already public in this repo's git history and in every npm tarball of the JS engine, so a closed binary bought nothing it could keep; the moat is the service (the catalog, the labs, the review pipeline), not the check functions. Keeping them behind a prebuilt archive cost a C-ABI, an exact toolchain pin, a build-time download, a second release to order ahead of every engine release, and a serde layer that had to serve two encodings. Deleted - crates/core/src/ffi.rs, crates/core/build.rs, crates/core/tests/boundary.rs and the shim modules under src/checks and src/browser. - crates/foundation/src/boundary.rs and the postcard dependency. - DETECTOR_VERSION, scripts/check-detector-release.mjs and its test, the check:detector-release script, the detector gate and IMPECCABLE_SKIP_DETECTOR_CHECK in scripts/release.mjs. - scripts/lib/detector-bundle.mjs and tests/detector-bundle.test.mjs (the vendoring path for the closed browser bundle). - scripts/build-browser-detector.js and the build:browser script (a stub since the JS engine left the tree). - xtask's detector-archive subcommand and its public-repo lookup. Came back - crates/core is now the rule logic itself: every check_* / scan_*, the browser adapters, the visual-contrast decisions. It re-exports foundation as before, so no consumer changed. Its vectors dispatcher is the union of both id tables again, and tests/vectors.rs replays the frozen vectors straight through it. - crates/wasm and crates/xtask join the workspace. cargo xtask bundle builds the in-page bundle from browser-bundle/ plus the wasm core, writes dist/, refreshes the tracked crates/live/assets/detect-antipatterns- browser.js, and writes extension/detector/. bun run build:extension runs it instead of downloading. - crates/live/assets/detect-antipatterns-browser.js is tracked again; live mode embeds it and serves it as /detect.js. - Serde is back to plain derives: no is_human_readable branch in js::json_number, derived Serialize for Rgba and BrowserFinding with their skip_serializing_if attributes. - profile.release has lto = "fat" again; rust-toolchain.toml is plain stable plus the wasm32 target. The rust, rust-windows and oracle CI jobs lose continue-on-error and can be required. Verified - cargo build --workspace --all-targets: clean, no warnings. - cargo test --workspace: 346 pass, 0 fail (the 8 boundary tests are gone with the boundary). - cargo build -p impeccable-wasm --target wasm32-unknown-unknown --release: ok. - cargo xtask bundle && cargo xtask bundle --check: reproducible; the regenerated bundle is committed (it differs from the archived one, which was built with a pinned rustc and lto = false). - cargo build --release -p impeccable: no linker warnings, 12.5 MB (the same source at lto = false is 13.1 MB). - oracle: 795 pass, 0 fail, 0 accepted deltas, 0 missing goldens. - bun run build, bun run build:extension, web-ext lint (0 errors, 8 warnings), bun run test: 363 + 80 + 1 + 1 + 133 + 180 + 4 pass, 0 fail. - impeccable detect --no-config --json tests/fixtures/antipatterns: 128.7 ms median of 5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
203 lines
7.2 KiB
Rust
203 lines
7.2 KiB
Rust
//! Replays the recorded JS call vectors through the Rust port and requires
|
|
//! every one to match. Vectors live at
|
|
//! `tests/oracle/vectors/calls/<module>/<fn>.jsonl` in this repo;
|
|
//! `IMPECCABLE_PUBLIC_REPO` overrides the root for an out-of-tree checkout.
|
|
|
|
use impeccable_core::vectors::{call, KNOWN_FUNCTIONS};
|
|
use serde_json::Value;
|
|
use std::collections::BTreeMap;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
const MODULES: &[&str] = &["shared.color", "shared.inline-ignores", "rules.checks"];
|
|
|
|
/// The repo root. `IMPECCABLE_PUBLIC_REPO` overrides it for an out-of-tree
|
|
/// checkout.
|
|
fn repo_root() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join("..")
|
|
}
|
|
|
|
fn vectors_dir() -> Option<PathBuf> {
|
|
let mut candidates: Vec<PathBuf> = Vec::new();
|
|
if let Ok(p) = std::env::var("IMPECCABLE_PUBLIC_REPO") {
|
|
candidates.push(PathBuf::from(p));
|
|
}
|
|
candidates.push(repo_root());
|
|
candidates
|
|
.into_iter()
|
|
.map(|repo| {
|
|
repo.join("tests")
|
|
.join("oracle")
|
|
.join("vectors")
|
|
.join("calls")
|
|
})
|
|
.find(|dir| dir.is_dir())
|
|
}
|
|
|
|
/// Canonical form for comparison: numbers by f64 value, objects key-order
|
|
/// insensitive, `{"$undef":true}` object values dropped (JSON.stringify would
|
|
/// omit them).
|
|
#[derive(Debug, PartialEq)]
|
|
enum Canon {
|
|
Null,
|
|
Bool(bool),
|
|
Num(f64),
|
|
Str(String),
|
|
Arr(Vec<Canon>),
|
|
Obj(BTreeMap<String, Canon>),
|
|
Undef,
|
|
}
|
|
|
|
fn is_undef(v: &Value) -> bool {
|
|
matches!(v, Value::Object(m) if m.len() == 1 && m.get("$undef").is_some())
|
|
}
|
|
|
|
fn canon(v: &Value) -> Canon {
|
|
match v {
|
|
Value::Null => Canon::Null,
|
|
Value::Bool(b) => Canon::Bool(*b),
|
|
Value::Number(n) => Canon::Num(n.as_f64().unwrap_or(f64::NAN)),
|
|
Value::String(s) => Canon::Str(s.clone()),
|
|
Value::Array(a) => Canon::Arr(a.iter().map(canon).collect()),
|
|
Value::Object(m) => {
|
|
if is_undef(v) {
|
|
return Canon::Undef;
|
|
}
|
|
if m.len() == 1 {
|
|
if let Some(n) = m.get("$negzero") {
|
|
if n.as_bool() == Some(true) {
|
|
return Canon::Num(-0.0);
|
|
}
|
|
}
|
|
}
|
|
Canon::Obj(
|
|
m.iter()
|
|
.filter(|(_, v)| !is_undef(v))
|
|
.map(|(k, v)| (k.clone(), canon(v)))
|
|
.collect(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn same(a: &Canon, b: &Canon) -> bool {
|
|
match (a, b) {
|
|
(Canon::Num(x), Canon::Num(y)) => {
|
|
(x.is_nan() && y.is_nan()) || (x == y && x.is_sign_negative() == y.is_sign_negative())
|
|
}
|
|
(Canon::Arr(x), Canon::Arr(y)) => {
|
|
x.len() == y.len() && x.iter().zip(y).all(|(p, q)| same(p, q))
|
|
}
|
|
(Canon::Obj(x), Canon::Obj(y)) => {
|
|
x.len() == y.len()
|
|
&& x.iter()
|
|
.all(|(k, v)| y.get(k).map(|w| same(v, w)).unwrap_or(false))
|
|
}
|
|
_ => a == b,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn replay_recorded_vectors() {
|
|
let Some(dir) = vectors_dir() else {
|
|
panic!(
|
|
"vectors dir not found; set IMPECCABLE_PUBLIC_REPO to a checkout of this repo that has \
|
|
tests/oracle/vectors/calls (generate with `node tests/oracle/vectors/record-calls.mjs`)"
|
|
);
|
|
};
|
|
let mut total_pass = 0usize;
|
|
let mut total_fail = 0usize;
|
|
let mut lines_out: Vec<String> = Vec::new();
|
|
for module in MODULES {
|
|
let mod_dir = dir.join(module);
|
|
// `KNOWN_FUNCTIONS` is the union of foundation's arms and this
|
|
// crate's.
|
|
let mut known: Vec<&str> = Vec::new();
|
|
for (m, fns) in KNOWN_FUNCTIONS.iter() {
|
|
if m == module {
|
|
known.extend(fns.iter().copied());
|
|
}
|
|
}
|
|
let mut seen: Vec<String> = Vec::new();
|
|
let mut entries: Vec<PathBuf> = match std::fs::read_dir(&mod_dir) {
|
|
Ok(rd) => rd
|
|
.filter_map(|e| e.ok().map(|e| e.path()))
|
|
.filter(|p| p.extension().map(|e| e == "jsonl").unwrap_or(false))
|
|
.collect(),
|
|
Err(_) => Vec::new(),
|
|
};
|
|
entries.sort();
|
|
for path in entries {
|
|
let fn_name = path.file_stem().unwrap().to_string_lossy().to_string();
|
|
seen.push(fn_name.clone());
|
|
if !known.iter().any(|k| *k == fn_name) {
|
|
lines_out.push(format!("SKIP {}/{}: not ported yet", module, fn_name));
|
|
continue;
|
|
}
|
|
let text = std::fs::read_to_string(&path).unwrap();
|
|
let mut pass = 0usize;
|
|
let mut fail = 0usize;
|
|
let mut first_failures: Vec<String> = Vec::new();
|
|
for (lineno, line) in text.lines().enumerate() {
|
|
if line.trim().is_empty() {
|
|
continue;
|
|
}
|
|
let rec: Value = serde_json::from_str(line)
|
|
.unwrap_or_else(|e| panic!("{}:{}: {e}", path.display(), lineno + 1));
|
|
let args = rec
|
|
.get("args")
|
|
.and_then(|a| a.as_array())
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
let expected = rec.get("result").cloned().unwrap_or(Value::Null);
|
|
match call(module, &fn_name, &args) {
|
|
None => {
|
|
fail += 1;
|
|
if first_failures.len() < 3 {
|
|
first_failures.push(format!(
|
|
" line {}: dispatcher has no handler / rejected args {}",
|
|
lineno + 1,
|
|
Value::Array(args.clone())
|
|
));
|
|
}
|
|
}
|
|
Some(actual) => {
|
|
if same(&canon(&actual), &canon(&expected)) {
|
|
pass += 1;
|
|
} else {
|
|
fail += 1;
|
|
if first_failures.len() < 3 {
|
|
first_failures.push(format!(
|
|
" line {}: args {}\n expected {}\n actual {}",
|
|
lineno + 1,
|
|
Value::Array(args.clone()),
|
|
expected,
|
|
actual
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
total_pass += pass;
|
|
total_fail += fail;
|
|
lines_out.push(format!(
|
|
"{} {}/{}: {} pass, {} fail",
|
|
if fail == 0 { "PASS" } else { "FAIL" },
|
|
module,
|
|
fn_name,
|
|
pass,
|
|
fail
|
|
));
|
|
lines_out.extend(first_failures);
|
|
}
|
|
for f in &known {
|
|
if !seen.iter().any(|s| s == *f) {
|
|
lines_out.push(format!("SKIP {}/{}: no vectors recorded", module, f));
|
|
}
|
|
}
|
|
}
|
|
let summary = lines_out.join("\n");
|
|
println!("{summary}\nTOTAL: {total_pass} pass, {total_fail} fail");
|
|
assert_eq!(total_fail, 0, "vector mismatches:\n{summary}");
|
|
}
|