[Bug] `detect` routes only .html/.htm to the DOM engine, so contrast/a11y rules never run on Blade, Vue, Svelte or configured template extensions
What happened?
impeccable detect routes a file to the DOM-aware engine only when its extension is exactly .html/.htm. Every other markup-bearing extension — including built-in ones like .blade.php, .vue, .svelte, and any detector.extensions entry such as .html.erb — is sent to the text engine instead.
The files are scanned; they are misrouted. Text-engine rules still fire, so the file looks covered. But DOM-dependent rules (contrast, tap targets, heading hierarchy, ARIA) never run, and detect exits 0 with []. A skipped dimension is indistinguishable from a passing one.
The hook path does not have this bug — it routes the same file correctly. So the same file scanned two ways gives two different answers.
This looks like the same class of path-divergence as #460, but on the detect-vs-hook axis rather than file-vs-directory.
Steps to reproduce
Identical bytes, two extensions, no config needed:
BODY='<a href="#" style="color:#ccc;background:#fff">low contrast</a>'
printf '%s\n' "$BODY" > page.html
printf '%s\n' "$BODY" > page.blade.php # built-in extension
printf '%s\n' "$BODY" > page.html.erb # via detector.extensions
impeccable detect --json page.html # -> low-contrast finding
impeccable detect --json page.blade.php # -> []
impeccable detect --json page.html.erb # -> []
Proof the file is scanned but misrouted — a text-engine rule fires on .vue while a DOM rule does not:
printf '<template>\n <div style="transition: all 0.3s cubic-bezier(0.68,-0.55,0.27,1.55)">y</div>\n</template>\n' > t.vue
cp t.vue t.html
impeccable detect --json t.html # -> ['bounce-easing']
impeccable detect --json t.vue # -> ['bounce-easing'] (text rule: fires)
# but the contrast case above returns [] for .vue and a finding for .html
And the hook disagrees with detect on the very same file:
cat > .impeccable/config.json <<'JSON'
{"detector":{"extensions":[{"ext":".html.erb","engine":"html"}]}}
JSON
impeccable detect --json page.html.erb # -> []
printf '{"tool_name":"Edit","tool_input":{"file_path":"'$PWD'/page.html.erb"},"cwd":"'$PWD'"}' \
| impeccable hook # -> reports the low-contrast finding
impeccable doctor reports "No drift found" throughout.
Expected
detect routes to the DOM engine for any extension that emits markup — the built-in list plus configured engine: "html" entries — and agrees with the hook on the same file.
Likely cause
crates/detect/src/file_system.rs:
pub const HTML_EXTENSIONS: &[&str] = &[".html", ".htm"];
pub fn is_html_path(file_path: &str) -> bool {
HTML_EXTENSIONS.contains(&to_lower_case(&jsp::extname(file_path)).as_str())
}
extname returns the last segment only, so show.html.erb yields .erb and page.blade.php yields .php. Neither is in HTML_EXTENSIONS. Note has_scannable_extension directly above it already handles multi-part extensions with an ends_with loop — is_html_path appears not to have received the same treatment, and does not consult configured extensions at all.
Impact
Any CI step or script calling impeccable detect on a Rails, Laravel, Vue, or Svelte project reports a clean pass over files whose accessibility rules were never evaluated. The failure is silent and green.
Environment
- skill 4.3.1, engine 0.1.5
- macOS arm64 (Darwin 25.5.0)
Source: pbakaus/impeccable