跳转至

引擎:每个技能动词背后的 Rust 运行时

技能文本运行的每个命令都是 {{scripts_path}}/impeccable <verb>。技能旁边的启动器(skill/scripts/impeccable、impeccable.cmd)会为每个平台查找或下载一个静态二进制文件并执行它。该二进制文件由本仓库的 Cargo workspace 构建。运行时没有 Node。

本页面是面向所有构建或修改运行时的人员的地图。每个动词的可观测行为在 CLI-CONTRACT.md 中有明确定义,并由 tests/oracle/ 逐字节固定。

所有内容都位于本仓库中,基于 Apache-2.0 许可,并可从源码离线构建。引擎的任何部分都不会在构建时获取。

布局

Cargo.toml              the workspace (crates/*), release profile
rust-toolchain.toml     the channel plus the wasm32 target
ENGINE_VERSION          which engine release the launcher / npm shim download
.cargo/config.toml      the `cargo xtask` alias
browser-bundle/         the page JS the in-page bundle is built from
crates/
  cli          the `impeccable` binary: verb router, exit codes
  common       Io handle (stdout/stderr/stdin/env/cwd), path + process helpers,
               the HTTP/1.1 server core (`http`) behind every local server
  context      context, doctor, staleness, signals, concept-seed, pin, ...
  hook         the design hook (hook, hook-before-edit, hook-admin)
  live         live mode: server, wrap, accept, manual edits, Svelte/Vue
  skills       install / update / check / link (the old npm CLI verbs)
  comp         comp-fidelity pure libs (raster, png, metrics, fonts)
  comp-verbs   build-phase, comp-diff, comp-spec, font-match
  detect       `impeccable detect`: file walk, config, ignores, output, regex engine
  html         the static HTML engine: parser, cascade, static DOM, rule adapters
  browser      the URL engine: Chrome discovery, CDP, snapshot, visual pass
  foundation   JS-semantics helpers, color, findings, the rule registry, inline
               ignores, the Dom trait, SnapshotDom, and the plain-data types
               every check takes in and hands back
  core         the rule logic: every `check_*` / `scan_*` and its heuristics,
               the browser rule adapters, the visual-contrast decisions
  wasm         wasm-bindgen exports over `core` (the in-page bundle and the
               extension's offscreen core)
  bundle       the page JS plus the bundler: in-page bundle, extension
               pieces, registry JSON, the wasm-pack call
  xtask        `cargo xtask bundle`: the workspace's caller of `bundle`

crates/core 在自有路径下重新导出 foundation 模块,因此每个使用者只需指定一个 crate:impeccable_core::js、impeccable_core::color、impeccable_core::checks::rules::check_colors、impeccable_core::browser::driver::collect_browser_findings。这两个 crate 之间的划分取决于检查所面向的对象,而不是谁可以看到它。

构建与测试:

cargo build --release -p impeccable      # target/release/impeccable
cargo test --workspace
IMPECCABLE_BIN=target/release/impeccable node tests/oracle/run.mjs   # the behavior gate

bun run test 和 oracle 会通过 IMPECCABLE_BIN 查找二进制文件,然后依次查找 skill/scripts/bin/<os>-<arch>/(bun run fetch:engine 会把固定的 release 下载到那里;IMPECCABLE_BIN=target/release/impeccable bun run fetch:engine 会复制本地构建),再查找 target/release/impeccable,因此只需 cargo build --release -p impeccable 即可。

引擎运行的每个本地服务器(live-server、serve-question 决策页面、component-review serve)都基于 impeccable_common::http 构建:一个 TcpListener、每个连接一个线程、带请求体大小上限的请求解析,以及会关闭自身连接的响应。live server 自己驱动连接(turnstile 排序、SSE、驻留轮询);另外两个则通过 SerialServer 一次只取一个请求,因此它们的处理程序保持单线程,且不会有连接等待另一个连接被读取。这取代了 tiny_http,其 0.12 连接池可能会让浏览器连接突发中的一部分请求得不到响应(#1071)。新增服务器应建立在同一个 core 之上,而不是引入另一个 HTTP crate。

tests/oracle/vectors/calls/ 中冻结的函数级向量通过 impeccable_core::vectors::call(cargo test -p impeccable-core)重放,它是 foundation 的分发分支与 core 的分发分支的并集。

浏览器 bundle

与本地运行相同的规则也会在页面中运行,并被编译为 WebAssembly。cargo xtask bundle 是生成所有浏览器产物的唯一命令:

  1. 运行 wasm-pack build crates/wasm --target no-modules --release,输出到 target/wasm-bundle/(使用 opt-level z,然后执行 wasm-opt)。
  2. 按固定顺序将 browser-bundle/*.js 中的页面 JS 与 wasm-bindgen 胶水代码以及以 base64 内嵌的 .wasm 拼接在一起。页面 JS 只实现 Dom 探针、编组 JSON 并绘制 overlay;其中没有任何规则逻辑。
  3. 写入 dist/detect-antipatterns-browser.js 和 dist/antipatterns.json,并将两者复制到 crates/live/assets/。这两份副本是受跟踪的生成文件。crates/live/src/browser_assets.rs 通过 include_str! 内嵌该 bundle,然后 live server 以 /detect.js 将其交给浏览器,因此二进制文件必须携带它。antipatterns.json 是规则注册表([{ id, name, category, description }],对于声明了严重性的行还包含 severity,例如 "advisory"),从事源码 checkout 或仓库 tarball 的使用者在没有 Rust 工具链的情况下也能读取它,因为 extension/detector/ 在 gitignore 中;impeccable.style 会据此统计并渲染规则。
  4. 将五个扩展组件写入 extension/detector/(snapshot.js、overlay.js、core.js、core_bg.wasm、antipatterns.json)。该目录在 gitignore 中;bun run build:extension 会运行此任务,然后打包 zip 文件。

cargo xtask bundle --check 会重新构建,并在任一受跟踪资产过时时失败,这就是 CI 的过时性检查门禁。构建是确定性的:相同的源码,产生相同的字节。

wasm-pack 是这个项目唯一需要的额外工具(cargo install wasm-pack --locked),外加 rust-toolchain.toml 所请求的 wasm32-unknown-unknown 目标。 设置 IMPECCABLE_BUNDLE_SKIP_WASM_PACK=1 会复用 target/wasm-bundle/ 中已有的内容, 以便单独迭代页面 JS。设置 IMPECCABLE_EXTENSION_SKIP_BUNDLE=1 可以让 bun run build:extension 在 extension/detector/ 已经完成时跳过打包步骤, 适用于预先构建好它的 CI 矩阵。

在改动 crates/core、crates/foundation、crates/wasm 或 browser-bundle/ 之后, 运行 cargo xtask bundle,并提交刷新后的资源。

下游复用打包器

这些都不在任务中。impeccable-bundle(crates/bundle)通过 include_str! 内嵌 browser-bundle/*.js,并负责组装;因此,某个 crate 如果把自己的规则包与 impeccable-core + impeccable-wasm 链接到同一个 wasm 模块中,就能为该模块构建出 同样的产物,而无需从本仓库复制任何文件:

let (glue, wasm) = impeccable_bundle::wasm_pack_build(
    Path::new("crates/my-wasm"),      // engine + pack, not crates/wasm
    Path::new("target/wasm-bundle"),
    &[],                              // extra cargo args, after `--`
)?;
let js = impeccable_bundle::in_page_bundle(&glue, &wasm);   // /detect.js
let registry = impeccable_bundle::registry_json();          // built-ins + pack rows
let ext = impeccable_bundle::extension_pieces(&glue, &wasm, &registry);
impeccable_bundle::check_capture_contract()?;               // snapshot/core drift

这里没有任何代码会写文件或直接退出:由调用方放置字节并自行报告失败。一旦安装规则包, 它的注册表行就会出现在 registry_json 中,因为注册表会读取内置规则以及每个已注册的切片。 IMPECCABLE_BUNDLE_SKIP_WASM_PACK=1 是此库对跳过开关的命名(旧的 IMPECCABLE_XTASK_SKIP_WASM_PACK=1 仍然有效)。

规则包

内置规则会被编译进去并始终运行。规则包 是依赖此 workspace 的 crate 在不 fork 的 情况下添加自有规则的方式:一个进程生命周期的值,携带它自己的注册表行以及它拥有规则的钩子。 当未安装任何规则包时,一切保持不变——oracle 会逐字节地确保这一点。

这些 trait:

  • impeccable_core::rule_pack::RulePack(对象安全,Send + Sync + Debug) 包含三个钩子,每个默认都是空的:check_text(content, file_path, ext) 用于文本引擎, check_element_dom(dom, el) 和 check_page_dom(dom) 用于浏览器引擎。
  • impeccable_html::StaticRulePack 包含 check_document(doc, file_path)。 StaticDocument 模型属于 crates/html,而 detect 无法命名来自依赖于它的 crate 中的类型,因此静态引擎的钩子是一个独立的 trait。覆盖 HTML 的规则包需要同时实现这两个 trait。

对于下游 crate,有三个步骤:声明 static ROWS: &[Antipattern],使用带命名空间的 id (mypack/my-rule),并从 registry() 返回它们;在启动时调用一次 impeccable_core::rule_pack::install(&PACK),这会让 get_antipattern 解析规则包的 id, 从而让它的发现项拥有名称、描述、类别和严重级别;然后把规则包传给正在运行的引擎。

规则包引用的传递位置:

引擎 字段
文本 TextOptions.rule_pack、ScanOptions.rule_pack
静态 HTML DetectHtmlOptions.static_rule_pack 和 .rule_pack;StaticHtmlEngine.static_rule_pack 用于 Engines 接缝
浏览器 / 快照 BrowserConfig.rule_pack(#[serde(skip)]:规则包是 Rust 值,绝不会来自页面的 JSON)

每个钩子的运行位置及其原因:

  • 文本引擎(detect_text):在每个内置匹配器、样式块与 CSS-in-JS 阶段、设计系统扫描、 去重和页面分析器之后、内联忽略之前运行。放在最后会使内置输出保持不变;同时,因为处于 豁免步骤之内,impeccable-disable 对规则包规则的覆盖方式与对内置规则完全相同。
  • 静态 HTML 引擎(detect_html_source):在元素规则、设计系统合并、页面级检查和模式 检查之后,同样在内联忽略之前运行。一个 HTML 文件只会经历一次规则包传递:设置了 static_rule_pack 时用它,否则用 rule_pack.check_text 处理原始 HTML 源码——这样, 仅含文本的规则包也仍然能覆盖 .html 文件。同时实现了两个 trait 的规则包绝不会对同一文件 报告两次。
  • 浏览器驱动(collect_browser_findings):check_element_dom 在驱动的逐元素循环末尾 运行,经过同样的禁用规则过滤,并归入与内置发现项相同的元素;check_page_dom 在每个内置 页面传递之后运行,其归属方式与那些自行命名元素的内置检查相同(el: None 表示 document.body)。skipScan 也会跳过规则包。

注册表将 ANTIPATTERNS 作为内置列表,并在其后查询已注册的行(registry::extend、 registry::all_antipatterns)。extend 对每个切片是幂等的,并在 id 冲突时 panic, 因此规则包永远无法遮蔽内置规则。注册只能是追加式,且没有撤销:规则包是进程的属性, 而不是某次运行的属性。

wasm 的 detect 特性

crates/wasm 面向无法执行二进制文件的主机(Cloudflare Workers 和其他 wasm 沙箱)时使用 --features detect 构建。它在文件扫描引擎之上增加了两个导出,JSON 进、JSON 出:

  • detect_text_json(content, file_path, options_json)
  • detect_html_source_json(html, file_path, options_json)

两者都接受 { inlineIgnores?: boolean, designSystem?: { frontmatter?, sidecar? } }, 并返回 impeccable detect --json 所打印的发现项数组,键与顺序完全相同。designSystem 携带的是 DESIGN.md 的输入,而非规范化后的对象,因为 JS API 的规范化形式使用了 Set 和 Map,而 JSON 无法容纳它们。无法解析的选项会回退到默认值。antipatterns_json() 列出 内置规则以及其后任何规则包的行;immediate_tier_rules_json() 返回设计钩子的 immediate tier (即那些值得在编辑处修复的规则 id)。该列表位于 impeccable_core::registry::IMMEDIATE_TIER_RULES 中,impeccable-hook 会重新导出它, 因此 wasm 消费者读取到的是与钩子运行时相同的列表,而无需保留副本。

一个规则包通过 impeccable_wasm::set_rule_pack 和 exports_detect::set_static_rule_pack 到达这些导出,二者均仅限 Rust:使用方是一个 crate,它把 impeccable-wasm 作为 rlib 链接,注册自己的规则包,并对自身运行 wasm-pack。这里刻意没有提供面向 JS 的 setter。

cargo build -p impeccable-wasm --features detect --target wasm32-unknown-unknown --release

Pristine(PR 设计评审机器人)是第一个使用者:其 rules/ crate 在所有三个 hook 上承载 pristine/* 规则,并通过此特性接入引擎,取代它目前对 npm impeccable@3 包进行的 detectText 调用。

发布

远程技能 ZIP 在解压前需要 pinned-key 签名。请参阅 bundle signing 了解 1Password 设置以及必需的先签名后发布顺序。

有两种发布类型会触及运行时,按以下顺序:

  1. Engine(engine-v<ENGINE_VERSION>):bun run release:engine 会校验版本、npm 平台包的固定版本以及干净的工作树,然后打标签并推送;.github/workflows/release-engine.yml 构建五个目标平台,并发布带有 .sha256 伴生文件的二进制文件。启动器、npm shim 和 impeccable install 会从 github.com/pbakaus/impeccable/releases/download/engine-v<X>/ 下载。
  2. npm 平台包,然后是 skill 和 CLI 发布;scripts/check-engine-release.mjs 会以引擎发布作为这些发布的门禁。

扩展自带其 vendored WASM 核心,并且从不执行引擎二进制文件,因此 bun run release:ext 不受该门禁限制。但在附加 zip 之前,它确实需要运行 bun run build:extension(因而需要 Rust 工具链和 wasm-pack)。

CI 运行工作区构建和测试(rust、rust-windows),并针对被测 checkout 中的 release 构建重放 oracle。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 im/impeccable