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
This commit is contained in:
Paul Bakaus
2026-09-01 15:31:26 -07:00
co-authored by Claude Fable 5.1
parent e355ebf714
commit 0547ed6a63
279 changed files with 105655 additions and 21 deletions
+243
View File
@@ -0,0 +1,243 @@
//! The headless-browser side of `impeccable font-match`, wired over
//! `crates/browser`'s CDP client. This is the one piece the open
//! `impeccable-comp-verbs` crate cannot do on its own; it is injected as a
//! `FontRenderer` so the browser (and its `core` dependency) stays out of that
//! crate. Ported from `font-match.mjs` `renderCandidates` / `renderProofSheet`,
//! which drove Playwright/Puppeteer; here the same steps run over CDP against a
//! discovered Chrome (the browser the URL engine already uses).
use std::collections::HashMap;
use std::time::Duration;
use base64::Engine as _;
use impeccable_browser::cdp::{default_chrome_args, Browser, EvalOutcome, Page, Viewport};
use impeccable_browser::discovery;
use impeccable_comp::font_fingerprint::{fingerprint, FpOpts};
use impeccable_comp::png_io;
use impeccable_comp::raster::Image;
use impeccable_comp_verbs::font_match::{FontRenderer, RankCandidate, RenderedCandidate};
const NAV_TIMEOUT: Duration = Duration::from_secs(30);
/// A renderer that discovers and drives an installed Chrome over CDP.
pub struct CdpFontRenderer {
env: HashMap<String, String>,
}
impl CdpFontRenderer {
pub fn from_process_env() -> Self {
CdpFontRenderer { env: std::env::vars().collect() }
}
fn launch(&self) -> Option<Browser> {
let exe = discovery::find_browser(&self.env).ok()?;
// JS launchArgs: `process.env.CI ? ['--no-sandbox','--disable-setuid-sandbox'] : []`.
let mut user_args: Vec<String> = Vec::new();
if self.env.get("CI").map(|v| !v.is_empty()).unwrap_or(false) {
user_args.push("--no-sandbox".into());
user_args.push("--disable-setuid-sandbox".into());
}
let dangerous = self.env.get("PUPPETEER_DANGEROUS_NO_SANDBOX").map(String::as_str) == Some("true");
let _ = default_chrome_args(&user_args, dangerous); // parity: same flag set the URL engine uses
Browser::launch(&exe, &user_args, dangerous).ok()
}
}
fn b64(bytes: &[u8]) -> String {
base64::engine::general_purpose::STANDARD.encode(bytes)
}
fn data_url(html: &str) -> String {
format!("data:text/html;base64,{}", b64(html.as_bytes()))
}
/// encodeURIComponent(fam).replace(/%20/g,'+') for the Google Fonts css2 URL.
fn encode_family(fam: &str) -> String {
let mut out = String::new();
for ch in fam.chars() {
if ch == ' ' {
out.push('+');
} else if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | '!' | '~' | '*' | '\'' | '(' | ')') {
out.push(ch);
} else {
let mut buf = [0u8; 4];
for byte in ch.encode_utf8(&mut buf).bytes() {
out.push_str(&format!("%{byte:02X}"));
}
}
}
out
}
fn wfmt(w: f64) -> String {
(w as i64).to_string()
}
fn js_string(s: &str) -> String {
serde_json::to_string(s).unwrap_or_else(|_| "\"\"".to_string())
}
fn links_html(candidates: &[RankCandidate]) -> String {
candidates
.iter()
.map(|c| {
format!(
"<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family={}:wght@{}&display=block\">",
encode_family(&c.family),
wfmt(c.weight)
)
})
.collect()
}
fn eval_bool(page: &mut Page<'_>, expr: &str) -> bool {
match page.evaluate(expr) {
Ok(EvalOutcome::Value(v)) => v.as_bool().unwrap_or(false),
_ => false,
}
}
fn eval_value(page: &mut Page<'_>, expr: &str) -> Option<serde_json::Value> {
match page.evaluate(expr) {
Ok(EvalOutcome::Value(v)) => Some(v),
_ => None,
}
}
impl FontRenderer for CdpFontRenderer {
fn render_candidates(
&mut self,
candidates: &[RankCandidate],
text: &str,
target_cap_px: f64,
transform: &str,
) -> Option<Vec<RenderedCandidate>> {
let mut browser = self.launch()?;
let links = links_html(candidates);
let html = format!(
"<!doctype html><html><head><meta charset=\"utf-8\">{links}<style>body{{margin:0;background:#fff}}div.s{{position:absolute;left:0;top:0;white-space:nowrap;color:#000;line-height:1;padding:8px;text-transform:{transform}}}</style></head><body></body></html>"
);
let size0 = 12f64.max((target_cap_px * 1.4).round());
let mut results: Vec<RenderedCandidate> = Vec::new();
let outcome = (|| -> Option<()> {
let mut page = browser.new_page().ok()?;
page.set_viewport(Viewport { width: 1600, height: 400 }).ok()?;
page.goto(&data_url(&html), "load", NAV_TIMEOUT).ok()?;
std::thread::sleep(Duration::from_millis(800));
for c in candidates {
let mut size = size0;
let mut fp = None;
let mut ok = true;
for pass in 0..2 {
let div = format!(
"<div class=\"s\" style=\"font-family:'{}',sans-serif;font-weight:{};font-size:{}px\">{}</div>",
c.family,
wfmt(c.weight),
size as i64,
text
);
let set = format!("(() => {{ document.body.innerHTML = {}; }})()", js_string(&div));
let _ = page.evaluate(&set);
// Loaded means a real face of this family covers the weight.
let check = format!(
"(async () => {{ const f = {{ family: {}, weight: {} }}; const faces = await document.fonts.load(f.weight + \" 32px '\" + f.family + \"'\"); await document.fonts.ready; const covers = (face) => {{ const w = String(face.weight || '400').split(/\\s+/).map(Number); const lo = w[0], hi = w[1] ?? w[0]; return f.weight >= lo - 50 && f.weight <= hi + 50; }}; return faces.some((face) => face.family.replace(/[\"']/g, '') === f.family && face.status === 'loaded' && covers(face)); }})()",
js_string(&c.family),
wfmt(c.weight)
);
let loaded = eval_bool(&mut page, &check);
std::thread::sleep(Duration::from_millis(100));
if !loaded {
ok = false;
}
let box_v = eval_value(&mut page, "(() => { const r = document.querySelector('div.s').getBoundingClientRect(); return { w: Math.ceil(r.width) + 8, h: Math.ceil(r.height) + 8 }; })()");
let (bw, bh) = box_v
.as_ref()
.map(|v| (v.get("w").and_then(|x| x.as_f64()).unwrap_or(0.0), v.get("h").and_then(|x| x.as_f64()).unwrap_or(0.0)))
.unwrap_or((0.0, 0.0));
let clip_w = 1600f64.min(bw);
let clip_h = 400f64.min(bh);
let shot = page.screenshot_clip(0.0, 0.0, clip_w, clip_h).ok()?;
let png = base64::engine::general_purpose::STANDARD.decode(shot.as_bytes()).ok()?;
fp = png_io::decode_png(&png).ok().and_then(|d| fingerprint(&d.image, &FpOpts::default()));
if fp.is_none() || pass == 1 {
break;
}
let cap = fp.as_ref().unwrap().cap_height_px;
size = 8f64.max((size * (target_cap_px / cap)).round());
}
results.push(RenderedCandidate {
family: c.family.clone(),
weight: c.weight,
loaded: ok,
font_size_px: size as i64,
fp,
});
}
page.close();
Some(())
})();
browser.close();
outcome.map(|_| results)
}
fn render_proof_sheet(
&mut self,
comp_crop: &Image,
top: &[RenderedCandidate],
text: &str,
_cap_px: f64,
transform: &str,
) -> Option<Vec<u8>> {
let comp_png = png_io::encode_png(comp_crop, &[]).ok()?;
let comp_b64 = b64(&comp_png);
let links: String = top
.iter()
.map(|c| {
format!(
"<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family={}:wght@{}&display=block\">",
encode_family(&c.family),
wfmt(c.weight)
)
})
.collect();
let rows: String = top
.iter()
.map(|c| {
format!(
"<div class=\"row\"><div class=\"lab\">{} {} · {}px</div><div class=\"s\" style=\"font-family:'{}';font-weight:{};font-size:{}px;text-transform:{}\">{}</div></div>",
&c.family,
wfmt(c.weight),
c.font_size_px,
c.family,
wfmt(c.weight),
c.font_size_px,
transform,
text
)
})
.collect();
let html = format!(
"<!doctype html><html><head><meta charset=\"utf-8\">{links}<style>body{{margin:0;background:#fff;padding:12px;font-family:system-ui}}img{{display:block;max-width:100%}}.lab{{font:12px system-ui;color:#666;margin:10px 0 2px}}.s{{white-space:nowrap;line-height:1.05;color:#111}}</style></head><body><div class=\"lab\">COMP</div><img src=\"data:image/png;base64,{comp_b64}\">{rows}</body></html>"
);
let mut browser = self.launch()?;
let vw = 1600u32.min(600u32.max(comp_crop.width as u32 + 24));
let outcome = (|| -> Option<Vec<u8>> {
let mut page = browser.new_page().ok()?;
page.set_viewport(Viewport { width: vw, height: 200 }).ok()?;
page.goto(&data_url(&html), "load", NAV_TIMEOUT).ok()?;
let _ = page.evaluate("(async () => { await document.fonts.ready; })()");
std::thread::sleep(Duration::from_millis(600));
let size = eval_value(&mut page, "(() => ({ w: Math.ceil(document.documentElement.scrollWidth), h: Math.ceil(document.documentElement.scrollHeight) }))()");
let (w, h) = size
.as_ref()
.map(|v| (v.get("w").and_then(|x| x.as_f64()).unwrap_or(vw as f64), v.get("h").and_then(|x| x.as_f64()).unwrap_or(200.0)))
.unwrap_or((vw as f64, 200.0));
let shot = page.screenshot_clip(0.0, 0.0, w, h).ok()?;
let png = base64::engine::general_purpose::STANDARD.decode(shot.as_bytes()).ok()?;
page.close();
Some(png)
})();
browser.close();
outcome
}
}
+133
View File
@@ -0,0 +1,133 @@
//! `impeccable` binary: verb router.
//!
//! Every skill script and CLI subcommand is a verb here. Verb crates expose
//! `run(args: &[String], io: &mut Io) -> i32` (exit code) and never call
//! `std::process::exit` themselves, so this file is the single place exit codes
//! and stream flushing are decided (contract: docs/CLI-CONTRACT.md in the
//! public repo). Verb names are the JS script basenames; a few carry aliases
//! (`signals` for context-signals, `hooks` for hook-admin).
use std::io::Write;
use impeccable_common::Io;
mod font_render;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let mut io = Io::stdio();
let code = run(&args, &mut io);
let _ = io.stdout.flush();
let _ = io.stderr.flush();
std::process::exit(code);
}
fn run(args: &[String], io: &mut Io) -> i32 {
// cli/bin/cli.js dispatch: help / version / detect / ignores / skills verbs
let Some(verb) = args.first().map(String::as_str) else {
io.out(impeccable_detect::ROOT_USAGE);
return 0;
};
let rest = &args[1..];
match verb {
"--help" | "-h" => {
io.out(impeccable_detect::ROOT_USAGE);
0
}
"--version" | "-v" => {
io.out(&format!("{CLI_VERSION}\n"));
0
}
// Launcher handshake: a cheap discriminator so the launchers can tell
// this engine apart from the retired 3.x npm CLI (which answers any
// unknown verb with `Unknown command`, exit 1) before exec'ing a
// candidate found on PATH or in the unversioned user cache. Kept out
// of --help on purpose; not part of the user-facing contract.
"engine-probe" => {
io.out(&format!("impeccable-engine {VERSION}\n"));
0
}
"detect" => impeccable_detect::run_detect(rest, io, &engines()),
"ignores" | "ignore" => impeccable_detect::run_ignores(rest, io),
"skills" => impeccable_skills::run(rest, io),
"help" | "install" | "link" | "update" | "check" => impeccable_skills::run(args, io),
// skill scripts
"context" => impeccable_context::run_context(rest, io),
"pin" => impeccable_context::run_pin(rest, io),
"detect-csp" => impeccable_context::run_detect_csp(rest, io),
"palette" => impeccable_context::run_palette(rest, io),
"surface-brief" => impeccable_context::run_surface_brief(rest, io),
"critique-storage" => impeccable_context::run_critique_storage(rest, io),
"embed-prompt" => impeccable_context::run_embed_prompt(rest, io),
"signals" | "context-signals" => impeccable_context::run_signals(rest, io),
"doctor" => impeccable_context::run_doctor(rest, io),
"concept-seed" => impeccable_context::run_concept_seed(rest, io),
"generate-image" => impeccable_context::run_generate_image(rest, io),
"serve-question" => impeccable_context::run_serve_question(rest, io),
// comp-fidelity verbs (crates/comp-verbs over crates/comp)
"comp-spec" => impeccable_comp_verbs::run_comp_spec(rest, io),
"comp-diff" => impeccable_comp_verbs::run_comp_diff(rest, io),
"font-match" => {
let mut renderer = font_render::CdpFontRenderer::from_process_env();
impeccable_comp_verbs::run_font_match(rest, io, &mut renderer)
}
"build-phase" => {
// Inject the organic-clip-path CSS scanner (a rule that lives in the
// closed `core` crate) so comp-verbs stays core-free.
let organic = |html: &str| -> Vec<(Option<String>, String)> {
impeccable_core::checks::css_scan::scan_css_text_for_organic_clip_path(html)
.into_iter()
.map(|f| (f.selector, f.snippet))
.collect()
};
impeccable_comp_verbs::run_build_phase(rest, io, &organic)
}
"hook" => impeccable_hook::run_hook(rest, io, engines().html),
"hook-before-edit" => impeccable_hook::run_hook_before_edit(rest, io, engines().html),
"hooks" | "hook-admin" => impeccable_hook::run_hook_admin(rest, io),
v if v.starts_with("live") => impeccable_live::run(v, rest, io),
// `npx impeccable src/` shorthand: a path-shaped, flag, URL, or existing
// first arg is a detect target (cli.js looksLikeDetectTarget).
v if impeccable_detect::looks_like_detect_target(v, &io.cwd.to_string_lossy()) => {
impeccable_detect::run_detect(args, io, &engines())
}
"init" => {
io.err(impeccable_detect::INIT_MESSAGE);
1
}
other => {
io.err(&format!(
"Unknown command: \"{other}\"\n\nTo see a list of supported commands, run:\n impeccable --help\n"
));
1
}
}
}
/// The npm `impeccable` package version `cli.js --version` prints (its
/// `package.json`), tracked separately from the crate version.
pub const CLI_VERSION: &str = "3.6.0";
/// The engines wired into `impeccable detect`: the static HTML engine
/// (crates/html). The browser engine (crates/browser) plugs in here once it
/// lands; until then URL scans report the puppeteer message.
fn engines() -> impeccable_detect::Engines<'static> {
static HTML: impeccable_html::StaticHtmlEngine = impeccable_html::StaticHtmlEngine;
impeccable_detect::Engines {
html: &HTML,
url: Some(url_engine()),
}
}
// --- browser engine (crates/browser) -------------------------------------
/// The URL engine, built once from the process environment (browser
/// discovery reads `IMPECCABLE_BROWSER` / `PUPPETEER_EXECUTABLE_PATH` /
/// `CHROME_PATH`, sandbox flags read `CI`).
fn url_engine() -> &'static impeccable_browser::BrowserEngine {
static ENGINE: std::sync::OnceLock<impeccable_browser::BrowserEngine> =
std::sync::OnceLock::new();
ENGINE.get_or_init(impeccable_browser::BrowserEngine::from_process_env)
}
// -------------------------------------------------------------------------