mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-16 16:16:32 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
913d7b3fe2 |
@@ -734,7 +734,7 @@ fn scan_targets(
|
||||
// Unreadable directories and files are reported, not silently
|
||||
// skipped, and each one forces exit 1 (#711).
|
||||
let mut walk_failures: Vec<(String, String)> = Vec::new();
|
||||
let files: Vec<String> = walk_dir_reporting(&resolved, &mut |dir, err| {
|
||||
let files: Vec<String> = walk_dir_reporting(&resolved, &ctx.config.extensions, &mut |dir, err| {
|
||||
walk_failures.push((dir.to_string(), node_scan_error(dir, err)));
|
||||
})
|
||||
.into_iter()
|
||||
|
||||
@@ -88,6 +88,7 @@ pub struct DetectionConfig {
|
||||
pub ignore_values: Vec<IgnoreValueEntry>,
|
||||
pub design_system_enabled: Option<bool>,
|
||||
pub advisory_rules: Option<String>,
|
||||
pub extensions: Vec<String>,
|
||||
}
|
||||
|
||||
impl DetectionConfig {
|
||||
@@ -131,6 +132,40 @@ fn apply_detection_config_source(config: &mut DetectionConfig, raw: Option<&Map<
|
||||
if let Some(Value::Array(values)) = raw.get("ignoreValues") {
|
||||
config.ignore_values = merge_ignore_values(&config.ignore_values, values);
|
||||
}
|
||||
if let Some(Value::Array(list)) = raw.get("extensions") {
|
||||
config.extensions = unique_strings(
|
||||
config
|
||||
.extensions
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(normalize_detection_extensions(list))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_detection_extensions(entries: &[Value]) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for entry in entries {
|
||||
let raw = match entry {
|
||||
Value::String(s) => Some(s.as_str()),
|
||||
Value::Object(o) => match o.get("ext") {
|
||||
Some(Value::String(s)) => Some(s.as_str()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
let Some(raw) = raw else { continue };
|
||||
let mut ext = js::to_lower_case(js::trim(raw));
|
||||
if ext.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !ext.starts_with('.') {
|
||||
ext = format!(".{ext}");
|
||||
}
|
||||
out.push(ext);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn unique_strings(values: Vec<String>) -> Vec<String> {
|
||||
@@ -1121,4 +1156,25 @@ mod tests {
|
||||
assert_eq!(decode_uri_component("Open%20Sans"), "Open Sans");
|
||||
assert_eq!(decode_uri_component("bad%zz"), "bad%zz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detection_extensions() {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"impeccable-detect-ext-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let impeccable = dir.join(".impeccable");
|
||||
std::fs::create_dir_all(&impeccable).unwrap();
|
||||
std::fs::write(
|
||||
impeccable.join("config.json"),
|
||||
r#"{"detector":{"extensions":[{"ext":".html.erb","engine":"html"},"blade.php"]}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = read_detection_config(dir.to_str().unwrap());
|
||||
assert_eq!(
|
||||
config.extensions,
|
||||
vec![".html.erb".to_string(), ".blade.php".to_string()]
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,11 @@ pub const HTML_EXTENSIONS: &[&str] = &[".html", ".htm"];
|
||||
|
||||
/// JS: file-system.mjs#hasScannableExtension
|
||||
pub fn has_scannable_extension(filename: &str) -> bool {
|
||||
has_scannable_extension_with(filename, &[])
|
||||
}
|
||||
|
||||
/// Built-in scannable extensions plus configured suffix matches (#822).
|
||||
fn has_scannable_extension_with(filename: &str, extra_exts: &[String]) -> bool {
|
||||
let lower = impeccable_core::js::to_lower_case(filename);
|
||||
if SCANNABLE_EXTENSIONS.contains(&jsp::extname(&lower).as_str()) {
|
||||
return true;
|
||||
@@ -46,6 +51,13 @@ pub fn has_scannable_extension(filename: &str) -> bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
let name_len = lower.encode_utf16().count();
|
||||
for ext in extra_exts {
|
||||
let ext_len = ext.encode_utf16().count();
|
||||
if name_len > ext_len && lower.ends_with(ext.as_str()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
@@ -57,13 +69,14 @@ pub fn is_html_path(file_path: &str) -> bool {
|
||||
/// JS: file-system.mjs#walkDir. Files in `readdirSync` order (the OS order,
|
||||
/// which Node does not sort either), recursive; an unreadable dir yields [].
|
||||
pub fn walk_dir(dir: &str) -> Vec<String> {
|
||||
walk_dir_reporting(dir, &mut |_, _| {})
|
||||
walk_dir_reporting(dir, &[], &mut |_, _| {})
|
||||
}
|
||||
|
||||
/// JS: file-system.mjs#walkDir(dir, onReadError). An unreadable directory is
|
||||
/// reported and skipped rather than silently yielding nothing (#711).
|
||||
pub fn walk_dir_reporting(
|
||||
dir: &str,
|
||||
extra_exts: &[String],
|
||||
on_read_error: &mut dyn FnMut(&str, &std::io::Error),
|
||||
) -> Vec<String> {
|
||||
let mut files = Vec::new();
|
||||
@@ -95,8 +108,8 @@ pub fn walk_dir_reporting(
|
||||
}
|
||||
let full = jsp::join(&[dir, &name]);
|
||||
if is_dir {
|
||||
files.extend(walk_dir_reporting(&full, on_read_error));
|
||||
} else if has_scannable_extension(&name) {
|
||||
files.extend(walk_dir_reporting(&full, extra_exts, on_read_error));
|
||||
} else if has_scannable_extension_with(&name, extra_exts) {
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
@@ -543,6 +556,11 @@ mod tests {
|
||||
assert!(has_scannable_extension("A.HTML"));
|
||||
assert!(!has_scannable_extension("a.php"));
|
||||
assert!(is_html_path("/x/y.HTM"));
|
||||
let erb = vec![".html.erb".to_string()];
|
||||
assert!(!has_scannable_extension("first.html.erb"));
|
||||
assert!(has_scannable_extension_with("first.html.erb", &erb));
|
||||
assert!(has_scannable_extension_with("A.HTML.ERB", &erb));
|
||||
assert!(!has_scannable_extension_with("notes.txt", &erb));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -991,6 +991,7 @@ pub fn filter_findings(findings: Vec<Finding>, config: &HookConfig) -> Vec<Findi
|
||||
ignore_values: config.ignore_values.clone(),
|
||||
design_system_enabled: None,
|
||||
advisory_rules: None,
|
||||
extensions: vec![],
|
||||
};
|
||||
filter_detection_findings(kept, &dc)
|
||||
}
|
||||
|
||||
@@ -140,26 +140,25 @@ fn locale_compare(a: &str, b: &str) -> std::cmp::Ordering {
|
||||
fn check(io: &mut Io) -> R<()> {
|
||||
let (sys, _) = ctx(io);
|
||||
let root = sys.find_project_root();
|
||||
// A home-rooted check is the user-level equivalent of `update --global`.
|
||||
// Keep both verbs on the canonical provider paths so stale legacy paths
|
||||
// (for example ~/.pi/skills) cannot make only `check` report drift.
|
||||
let scope = if sys.is_home_dir(&root) { Some(Scope::User) } else { None };
|
||||
if sys.is_already_installed(&root, scope).is_none() {
|
||||
if sys.is_already_installed(&root, None).is_none() {
|
||||
out(io, "Impeccable is not installed in this project.");
|
||||
out(io, "Run `npx impeccable install` to install.");
|
||||
return Err(Flow::Exit(0));
|
||||
}
|
||||
let providers = sys.find_installed_providers(&root, scope);
|
||||
let providers = sys.find_installed_providers(&root, None);
|
||||
out(io, "Checking for updates...\n");
|
||||
let result = (|| -> Result<bool, String> {
|
||||
let bundle_dir = bundle::download_and_extract_bundle(&sys)?;
|
||||
let up_to_date = bundle::is_up_to_date(&sys, &root, &providers, &bundle_dir, scope, scope)?;
|
||||
// JS: agentScope 'user' for a home-rooted checkout (d2a9efb9), so
|
||||
// check() judges agent freshness against the user agent dirs.
|
||||
let agent_scope = if sys.is_home_dir(&root) { Some(Scope::User) } else { None };
|
||||
let up_to_date = bundle::is_up_to_date(&sys, &root, &providers, &bundle_dir, None, agent_scope)?;
|
||||
util::rm_rf(&bundle_dir);
|
||||
Ok(up_to_date)
|
||||
})();
|
||||
match result {
|
||||
Ok(true) => {
|
||||
let v = sys.get_skills_version(&root, scope);
|
||||
let v = sys.get_skills_version(&root, None);
|
||||
out(io, &format!("Skills are up to date{}.", version_suffix(&v)));
|
||||
}
|
||||
Ok(false) => {
|
||||
|
||||
@@ -460,40 +460,6 @@ fn check_accepts_current_copilot_user_agents_in_home_rooted_checkout() {
|
||||
std::fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_ignores_stale_legacy_pi_skills_when_the_user_install_is_current() {
|
||||
let root = temp_root("pi-check-home-scope");
|
||||
let home = format!("{root}/home");
|
||||
let tmpdir = format!("{root}/tmp");
|
||||
for d in [&home, &tmpdir] {
|
||||
std::fs::create_dir_all(d).unwrap();
|
||||
}
|
||||
let bundle_root = create_fake_universal_bundle(&root, &[".pi"]);
|
||||
let env = base_env(&home, &tmpdir, &bundle_root);
|
||||
|
||||
let r = run_cli(
|
||||
&["install", "-y", "--scope=global", "--no-hooks", "--providers=pi"],
|
||||
&home,
|
||||
&env,
|
||||
);
|
||||
assert_eq!(r.code, 0, "{}\n{}", r.stdout, r.stderr);
|
||||
|
||||
let canonical = format!("{home}/.pi/agent/skills/impeccable");
|
||||
let legacy = format!("{home}/.pi/skills/impeccable");
|
||||
std::fs::create_dir_all(format!("{home}/.pi/skills")).unwrap();
|
||||
std::fs::create_dir_all(&legacy).unwrap();
|
||||
write(&format!("{legacy}/SKILL.md"), "---\nname: impeccable\nversion: 1.0.0-stale\n---\n");
|
||||
assert!(std::path::Path::new(&canonical).exists());
|
||||
|
||||
let update = run_cli(&["update", "--global", "-y", "--no-hooks"], &home, &env);
|
||||
assert!(update.stdout.contains("Skills are up to date"), "{}\n{}", update.stdout, update.stderr);
|
||||
|
||||
let check = run_cli(&["check"], &home, &env);
|
||||
assert!(check.stdout.contains("Skills are up to date"), "{}\n{}", check.stdout, check.stderr);
|
||||
assert!(!check.stdout.contains("Updates available"), "{}", check.stdout);
|
||||
std::fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
|
||||
// ─── inferred agent update scope (d2a9efb9) ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -181,7 +181,7 @@ Examples:
|
||||
- listening & matched: `\n${name} dev server detected on localhost:${port}.\nFor more accurate results, scan the running site:\n npx impeccable detect http://localhost:${port}\n\n`
|
||||
- listening & !matched: `\n${name} project detected (${basename(configPath)}).\nPort ${port} is in use by another service. Start the ${name} dev server and scan via URL for best results.\n\n`
|
||||
- not listening: `\n${name} project detected (${basename(configPath)}).\nStart the dev server and scan via URL for best results:\n npx impeccable detect http://localhost:${port}\n\n`
|
||||
Then `files = walkDir(resolved).filter(f => !shouldIgnoreDetectionFile(f, cwd, config))`. If `files.length > 50 && stdin.isTTY && !json && !quiet`: `stderr> \nFound ${n} files (${htmlCount} HTML) in ${target}.\nScanning may take a while${htmlCount > 10 ? ' (static HTML/CSS processes each HTML file individually)' : ''}.\nTarget a specific subdirectory to narrow scope.\n` then readline prompt `Continue? [Y/n] ` on stderr; empty or `/^y(es)?$/i` continues; otherwise `stderr> Aborted.\n`, `exit 0`. Then `buildImportGraph(files)` → reverse map; each file scanned with its own options; findings from a file that is imported get `f.importedBy = [basename(importer), ...]` (Set iteration order).
|
||||
Then `files = walkDir(resolved, config.extensions).filter(f => !shouldIgnoreDetectionFile(f, cwd, config))`. The walk collects built-in scannable extensions plus any `detector.extensions` from cwd config (empty under `--no-config`). If `files.length > 50 && stdin.isTTY && !json && !quiet`: `stderr> \nFound ${n} files (${htmlCount} HTML) in ${target}.\nScanning may take a while${htmlCount > 10 ? ' (static HTML/CSS processes each HTML file individually)' : ''}.\nTarget a specific subdirectory to narrow scope.\n` then readline prompt `Continue? [Y/n] ` on stderr; empty or `/^y(es)?$/i` continues; otherwise `stderr> Aborted.\n`, `exit 0`. Then `buildImportGraph(files)` → reverse map; each file scanned with its own options; findings from a file that is imported get `f.importedBy = [basename(importer), ...]` (Set iteration order).
|
||||
- **File**: skipped if `shouldIgnoreDetectionFile`; else `detectLocalFile`.
|
||||
- `detectLocalFile(fp, opts)`: extension (lowercased) in `HTML_EXTENSIONS = {'.html','.htm'}` → `detectHtml(fp, opts)`; else `detectText(readFileSync(fp,'utf-8'), fp, opts)`.
|
||||
4. Post-filter: `filterDetectionFindings(all, config)` (ignoreRules/ignoreValues), then `filterByScopes(all, scopes)` (keeps findings whose rule declares any requested scope; empty scopes = no filter), then `--no-advisory` drop.
|
||||
@@ -253,7 +253,7 @@ Optional keys added later by engines (appended after the above): `ignoreValue` (
|
||||
#### `cli/engine/node/file-system.mjs`
|
||||
|
||||
- `SKIP_DIRS = {'node_modules','dist','build','__pycache__'}`; any directory whose name starts with `.` is skipped **except** `HIDDEN_SOURCE_DIRS = {'.vitepress','.vuepress','.storybook'}`. The root passed to `walkDir` is never name-checked (an explicit hidden dir scans).
|
||||
- `SCANNABLE_EXTENSIONS = {'.html','.htm','.css','.scss','.sass','.less','.jsx','.tsx','.js','.ts','.vue','.svelte','.astro','.blade.php'}`; `hasScannableExtension` lowercases and also matches multi-dot exts by `endsWith` (`.blade.php`).
|
||||
- `SCANNABLE_EXTENSIONS = {'.html','.htm','.css','.scss','.sass','.less','.jsx','.tsx','.js','.ts','.vue','.svelte','.astro','.blade.php'}`; `hasScannableExtension` lowercases and also matches multi-dot exts by `endsWith` (`.blade.php`). CLI directory walks pass configured `detector.extensions` as a second suffix match: `name` lowercased; `name.length > ext.length && name.endsWith(ext)`.
|
||||
- `walkDir` returns files in `readdirSync` order, recursive, unreadable dirs → `[]`.
|
||||
- **There is no generated-file detection in the CLI** (`skill/scripts/lib/is-generated.mjs` is hook-side only and not imported by `cli/`).
|
||||
- Import graph: `IMPORT_SPECIFIER_PATTERNS = [/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, /@(?:use|forward)\s+['"]([^'"]+)['"]/g]`; `resolveImport` only for specifiers matching `/^[./]/`: exact, `base+ext` for each scannable ext, then `base/index+ext`.
|
||||
@@ -281,7 +281,7 @@ Optional keys added later by engines (appended after the above): `ignoreValue` (
|
||||
"designSystem": { "enabled": true }, "advisoryRules": "include"|"exclude" },
|
||||
"hook": { "consent": "accepted"|"declined", ... }, "updateCheck": true }
|
||||
```
|
||||
- `readDetectionConfig(root)`: start `{ignoreRules:[],ignoreFiles:[],ignoreValues:[],designSystem:{enabled:true}}`; for shared then local: apply legacy `raw.hook.*` section then `raw.detector.*`. Arrays are unioned (`uniqueStrings`, String-coerced); ignoreValues merged by key `rule\0value\0sortedFiles.join('\x1f')` (later wins); `designSystem.enabled` false only when literally `false`; `advisoryRules` copied only if `'include'|'exclude'`. Invalid JSON / non-object files are ignored silently. **No validation errors are ever raised by the CLI**; the only validation of ignore lists lives in `skill/scripts/lib/staleness-deep.mjs checkDetectorIgnores` (doctor): unknown `ignoreRules` ids vs live `ANTIPATTERNS` → finding `detector-ignore-rules-unknown` (severity `mention`); non-glob `ignoreFiles` entries that don't exist → `detector-ignore-files-missing`.
|
||||
- `readDetectionConfig(root)`: start `{ignoreRules:[],ignoreFiles:[],ignoreValues:[],designSystem:{enabled:true},extensions:[]}`; for shared then local: apply legacy `raw.hook.*` section then `raw.detector.*`. Arrays are unioned (`uniqueStrings`, String-coerced); ignoreValues merged by key `rule\0value\0sortedFiles.join('\x1f')` (later wins); `designSystem.enabled` false only when literally `false`; `advisoryRules` copied only if `'include'|'exclude'`; `extensions` unions normalized `detector.extensions` entries (string or `{ext, engine}` → leading-dot lowercase; engine ignored by detect). Invalid/non-array `extensions` skipped. Invalid JSON / non-object files are ignored silently. **No validation errors are ever raised by the CLI**; the only validation of ignore lists lives in `skill/scripts/lib/staleness-deep.mjs checkDetectorIgnores` (doctor): unknown `ignoreRules` ids vs live `ANTIPATTERNS` → finding `detector-ignore-rules-unknown` (severity `mention`); non-glob `ignoreFiles` entries that don't exist → `detector-ignore-files-missing`.
|
||||
- `normalizeIgnoreValue(v)`: trim, strip one leading/trailing quote, `+`→space, collapse whitespace, lowercase. Rules lowercased/trimmed.
|
||||
- `normalizeIgnoreValueEntries`: keeps `{rule, value, [files], [createdAt], [reason]}` in **that key order**; `file` (string) and `files` merged, trimmed, deduped.
|
||||
- Glob → regex: `**` → `.*` (swallowing a following `/`), `*` → `[^/]*`, `?` → `[^/]`, `{a,b}` → `(?:a|b)`, regex specials escaped; anchored `^...$`. `matchesAnyGlob` tests the `/`-normalized path and its basename.
|
||||
@@ -383,7 +383,7 @@ retain their local-development trust behavior. See [bundle signing](BUNDLE-SIGNI
|
||||
Already installed (and not `--force`): `Impeccable skills are already installed (found in ${provider}/).`; compares tree hashes (`sha256` of file content with `\.(claude|cursor|...)\/skills\/` normalized to `.PROVIDER/skills/`); if differs → refresh + `Updated ${n} skill(s) to v${v}.`; missing hooks repaired; else `Skills are up to date (v${v}).` + `Run with --force to reinstall.`; offline → `Could not check for skill updates: ${msg}` + `Existing skills were left unchanged.`; ends `Done!` or the above; `exit 0`. Version read from `^version:\s*(.+)$` in installed `impeccable/SKILL.md`.
|
||||
- **update flags**: `-y|--yes`, `--force`, `--no-hooks`, scope flags as above (unknown → `Unknown update scope: ${v}. Use --project or --user.`). Resolves project vs user installs holding an `impeccable`/`*-impeccable`/`teach-impeccable` skill; none → `No impeccable skill folders found in this project or at the user level.` + `Run \`npx impeccable install\` to install first.`, exit 1; both → prompt `Update which? [project]/user: ` (non-TTY defaults project). Prints `Updating the ${label} install: ${root} (${providers})`, linked providers note, `Checking for updates...`; up to date → `Skills are up to date (vX).` [+hooks] + `Nothing else to do.`, exit 0; else `Found skills in: ...`, prompt `Update skills in N provider folder(s)? (Y/n) ` (n/no → `Aborted.` exit 0), refresh, `Updated N skill(s) to vX.`, `Done!`.
|
||||
- **link**: `--source=<path>` (default `.impeccable`), `--providers`, `--force`, `-y`. Source must contain `dist/universal/` or provider `*/skills` dirs, else `Could not find compiled skills in ${src}. Expected dist/universal/ or provider skill folders.` Prompts `Link impeccable skills into N folder(s)? (Y/n) `; creates relative dir symlinks; existing non-link skipped with warning unless `--force`; output `Linked impeccable into: ... (N linked, N already linked, N skipped).` + submodule hint.
|
||||
- **check**: not installed → `Impeccable is not installed in this project.` + `Run \`npx impeccable install\` to install.` exit 0; else `Checking for updates...\n` then `Skills are up to date (vX).` or `Updates available.` + `Run \`npx impeccable update\` to update.`; failure → `Could not check for updates: ${msg}` exit 1. A home-rooted check uses user scope, matching `update --global`: provider-specific canonical global paths are compared, while stale legacy duplicates such as `~/.pi/skills` do not create false update notices.
|
||||
- **check**: not installed → `Impeccable is not installed in this project.` + `Run \`npx impeccable install\` to install.` exit 0; else `Checking for updates...\n` then `Skills are up to date (vX).` or `Updates available.` + `Run \`npx impeccable update\` to update.`; failure → `Could not check for updates: ${msg}` exit 1.
|
||||
- Prompts: non-TTY `ask()` reads answers line-by-line from stdin (fd 0) after echoing the question; TTY SIGINT → `PromptAbortError` (`code IMPECCABLE_PROMPT_ABORT`) → cli.js prints `\nAborted.` exit 130. ANSI (`\x1b[36m` accent, `\x1b[1m` bold, `\x1b[2m` dim, `\x1b[32m` good) only when stdout is TTY, `NO_COLOR` unset, `TERM !== 'dumb'`.
|
||||
- Tests: `tests/skills-cli.test.js`, `tests/cli-remote-e2e` (opt-in).
|
||||
|
||||
|
||||
@@ -101,6 +101,9 @@ export default function cases() {
|
||||
{ id: 'detect-config-css-text', verb: 'detect', workspace: 'detect-config', args: ['src/styles.css'] },
|
||||
{ id: 'detect-config-vendor-ignored', verb: 'detect', workspace: 'detect-config', args: ['--json', 'src/vendor/ignored.html'] },
|
||||
{ id: 'detect-config-from-subdir', verb: 'detect', workspace: 'detect-config', cwd: 'src', args: ['--json', 'page.html'] },
|
||||
// detector.extensions in directory walks (#822)
|
||||
{ id: 'detect-config-extensions-dir-json', verb: 'detect', workspace: 'detect-extensions', args: ['--json', '--no-design-system', 'app/views'] },
|
||||
{ id: 'detect-config-extensions-dir-no-config', verb: 'detect', workspace: 'detect-extensions', args: ['--no-config', '--json', 'app/views'] },
|
||||
// A file in one project must not pick up another project's DESIGN.md
|
||||
{ id: 'detect-config-cross-project', verb: 'detect', workspace: 'detect-config', args: ['--json', `<REPO>/tests/fixtures/antipatterns/blinking-cursor.html`], isolateHome: false },
|
||||
);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"overused-font\",\n \"name\": \"Overused font\",\n \"description\": \"Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<WS>/app/views/first.html.erb\",\n \"line\": 1,\n \"snippet\": \"font-family: Inter\"\n },\n {\n \"antipattern\": \"broken-image\",\n \"name\": \"Broken or placeholder image\",\n \"description\": \"<img> tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/app/views/first.html.erb\",\n \"line\": 1,\n \"snippet\": \"<img alt=\\\"probe\\\">\"\n },\n {\n \"antipattern\": \"broken-image\",\n \"name\": \"Broken or placeholder image\",\n \"description\": \"<img> tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/app/views/plain.html\",\n \"line\": 0,\n \"snippet\": \"<img> with no src attribute\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"broken-image\",\n \"name\": \"Broken or placeholder image\",\n \"description\": \"<img> tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/app/views/plain.html\",\n \"line\": 0,\n \"snippet\": \"<img> with no src attribute\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"detector": {
|
||||
"extensions": [{ "ext": ".html.erb", "engine": "html" }],
|
||||
"designSystem": { "enabled": false }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html><head><style>body { font-family: Inter; }</style></head><body><img alt="probe"></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html><head><style>body { font-family: Inter; }</style></head><body><img alt="probe"></body></html>
|
||||
Reference in New Issue
Block a user