Compare commits

..
Author SHA1 Message Date
Abdul WahabandCursor 913d7b3fe2 Fix: honor detector.extensions in detect directory walks (#822)
Directory scans skipped configured template suffixes such as .html.erb, so Rails views looked clean. Walk those extras from cwd config.

AI assistance: Cursor Grok 4.6 implemented this change.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 14:18:58 +05:00
16 changed files with 119 additions and 76 deletions
+1 -1
View File
@@ -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()
+56
View File
@@ -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);
}
}
+21 -3
View File
@@ -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]
+1
View File
@@ -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)
}
+8 -65
View File
@@ -1093,26 +1093,12 @@ fn extract_inner_by_attr(text: &str, attr: &str) -> Option<String> {
None
}
/// Drop leading and trailing blank lines while keeping a single empty line
/// when the inner text is only whitespace.
fn trim_surrounding_blank_lines(lines: Vec<String>) -> Vec<String> {
let mut start = 0usize;
let mut end = lines.len();
while end - start > 1 && trim(&lines[start]).is_empty() {
start += 1;
}
while end - start > 1 && trim(&lines[end - 1]).is_empty() {
end -= 1;
}
lines[start..end].to_vec()
}
/// JS: extractOriginal(lines, block)
fn extract_original(lines: &[String], block: &MarkerBlock) -> Vec<String> {
let text = strip_style_and_join(lines, block);
match extract_inner_by_attr(&text, "data-impeccable-variant=\"original\"") {
None => Vec::new(),
Some(inner) => trim_surrounding_blank_lines(inner.split('\n').map(String::from).collect()),
Some(inner) => inner.split('\n').map(String::from).collect(),
}
}
@@ -1127,7 +1113,13 @@ fn extract_variant(
&text,
&format!("data-impeccable-variant=\"{}\"", variant_num),
)?;
let result = trim_surrounding_blank_lines(inner.split('\n').map(String::from).collect());
let mut result: Vec<String> = inner.split('\n').map(String::from).collect();
while result.len() > 1 && trim(&result[0]).is_empty() {
result.remove(0);
}
while result.len() > 1 && trim(result.last().unwrap()).is_empty() {
result.pop();
}
if result.is_empty() {
None
} else {
@@ -1257,55 +1249,6 @@ fn find_session_file(id: &str, cwd: &str) -> Option<(String, String, Vec<String>
Some((file, content, lines))
}
#[cfg(test)]
mod discard_tests {
use super::*;
#[test]
fn discard_restores_the_original_without_blank_lines_around_it() {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!(
"impeccable-discard-{}-{nanos}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("Login.tsx");
let src = [
" </div>",
" <div data-impeccable-variants=\"ab12cd34\" data-impeccable-variant-count=\"3\" style={{ display: \"contents\" }}>",
" {/* impeccable-variants-start ab12cd34 */}",
" <style data-impeccable-css=\"ab12cd34\">{`",
" @scope ([data-impeccable-variant=\"1\"]) { :scope > .x { color: red; } }",
" `}</style>",
" {/* Original */}",
" <div data-impeccable-variant=\"original\">",
" <Bar title=\"Log in\" />",
" </div>",
" {/* Variants: insert below this line */}",
" <div data-impeccable-variant=\"1\">",
" <Bar title=\"Log in\" />",
" </div>",
" {/* impeccable-variants-end ab12cd34 */}",
" </div>",
" </AuthPage>",
];
let lines: Vec<String> = src.iter().map(|s| s.to_string()).collect();
let path = file.to_string_lossy().into_owned();
std::fs::write(&file, lines.join("\n")).unwrap();
handle_discard_unlocked("ab12cd34", &lines, &path).unwrap();
let out = std::fs::read_to_string(&file).unwrap();
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(
out,
" </div>\n <Bar title=\"Log in\" />\n </AuthPage>"
);
}
}
#[cfg(test)]
mod bake_tests {
use super::*;
+3 -3
View File
@@ -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.
+3
View File
@@ -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": {}
}
@@ -16,6 +16,6 @@
"files": {
".impeccable/live/accept-receipts/ab12cd34.json": "{\n \"id\": \"ab12cd34\",\n \"operation\": \"discard\",\n \"variantId\": null,\n \"result\": {\n \"handled\": true,\n \"file\": \"index.html\",\n \"carbonize\": false\n },\n \"completedAt\": \"<ISO>\"\n}\n",
".impeccable/live/config.json": "{\n \"files\": [\"index.html\", \"public/**/*.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n",
"index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Oracle Live HTML Fixture</title>\n <style>\n .hero-title { font-size: 2rem; }\n .hero-hook { color: #555; }\n </style>\n </head>\n <body>\n <main class=\"page\">\n <h1 id=\"hero\" class=\"hero-title\">Oracle Fixture</h1>\n <p class=\"hero-hook\">Minimal static page for oracle live-mode goldens.</p>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n <aside class=\"side-note\">\n <h2 class=\"note-title\">Aside</h2>\n <p>Nested content.</p>\n </aside>\n </main>\n </body>\n</html>\n"
"index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Oracle Live HTML Fixture</title>\n <style>\n .hero-title { font-size: 2rem; }\n .hero-hook { color: #555; }\n </style>\n </head>\n <body>\n <main class=\"page\">\n\n <h1 id=\"hero\" class=\"hero-title\">Oracle Fixture</h1>\n\n <p class=\"hero-hook\">Minimal static page for oracle live-mode goldens.</p>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n <aside class=\"side-note\">\n <h2 class=\"note-title\">Aside</h2>\n <p>Nested content.</p>\n </aside>\n </main>\n </body>\n</html>\n"
}
}
@@ -6,6 +6,6 @@
"files": {
".impeccable/live/accept-receipts/ab12cd34.json": "{\n \"id\": \"ab12cd34\",\n \"operation\": \"discard\",\n \"variantId\": null,\n \"result\": {\n \"handled\": true,\n \"file\": \"index.html\",\n \"carbonize\": false\n },\n \"completedAt\": \"<ISO>\"\n}\n",
".impeccable/live/config.json": "{\n \"files\": [\"index.html\", \"public/**/*.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n",
"index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Oracle Live HTML Fixture</title>\n <style>\n .hero-title { font-size: 2rem; }\n .hero-hook { color: #555; }\n </style>\n </head>\n <body>\n <main class=\"page\">\n <h1 id=\"hero\" class=\"hero-title\">Oracle Fixture</h1>\n <p class=\"hero-hook\">Minimal static page for oracle live-mode goldens.</p>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n <aside class=\"side-note\">\n <h2 class=\"note-title\">Aside</h2>\n <p>Nested content.</p>\n </aside>\n </main>\n </body>\n</html>\n"
"index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Oracle Live HTML Fixture</title>\n <style>\n .hero-title { font-size: 2rem; }\n .hero-hook { color: #555; }\n </style>\n </head>\n <body>\n <main class=\"page\">\n\n <h1 id=\"hero\" class=\"hero-title\">Oracle Fixture</h1>\n\n <p class=\"hero-hook\">Minimal static page for oracle live-mode goldens.</p>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n <aside class=\"side-note\">\n <h2 class=\"note-title\">Aside</h2>\n <p>Nested content.</p>\n </aside>\n </main>\n </body>\n</html>\n"
}
}
@@ -6,7 +6,7 @@
"files": {
".impeccable/live/accept-receipts/ab12cd34.json": "{\n \"id\": \"ab12cd34\",\n \"operation\": \"discard\",\n \"variantId\": null,\n \"result\": {\n \"handled\": true,\n \"file\": \"src/App.jsx\",\n \"carbonize\": false\n },\n \"completedAt\": \"<ISO>\"\n}\n",
".impeccable/live/config.json": "{\n \"files\": [\"index.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n",
"src/App.jsx": "export default function App() {\n return (\n <main className=\"page\">\n <h1 className=\"hero-title\">Vite Fixture</h1>\n <p className=\"hero-hook\">Minimal React tree for oracle live-mode goldens.</p>\n <section id=\"features\" className=\"feature-grid\">\n <article className=\"feature-card\">One</article>\n <article className=\"feature-card\">Two</article>\n </section>\n <ul className=\"item-list\">\n {items.map((item) => (\n <li key={item.id} className=\"item-row\">{item.title}</li>\n ))}\n </ul>\n </main>\n );\n}\n\nconst items = [\n { id: 1, title: 'First' },\n { id: 2, title: 'Second' },\n];\n",
"src/App.jsx": "export default function App() {\n return (\n <main className=\"page\">\n\n <h1 className=\"hero-title\">Vite Fixture</h1>\n\n <p className=\"hero-hook\">Minimal React tree for oracle live-mode goldens.</p>\n <section id=\"features\" className=\"feature-grid\">\n <article className=\"feature-card\">One</article>\n <article className=\"feature-card\">Two</article>\n </section>\n <ul className=\"item-list\">\n {items.map((item) => (\n <li key={item.id} className=\"item-row\">{item.title}</li>\n ))}\n </ul>\n </main>\n );\n}\n\nconst items = [\n { id: 1, title: 'First' },\n { id: 2, title: 'Second' },\n];\n",
"src/main.jsx": "import { createRoot } from 'react-dom/client';\nimport App from './App.jsx';\n\ncreateRoot(document.getElementById('root')).render(<App />);\n",
"src/Panel.tsx": "type PanelProps = { title: string; children?: React.ReactNode };\n\nexport function Panel({ title, children }: PanelProps) {\n return (\n <section className=\"panel\">\n <header className=\"panel-header\">\n <h2 className=\"panel-title\">{title}</h2>\n </header>\n <div className=\"panel-body\">{children}</div>\n </section>\n );\n}\n"
}
@@ -17,6 +17,6 @@
".impeccable/live/accept-receipts/ab12cd34.json": "{\n \"id\": \"ab12cd34\",\n \"operation\": \"discard\",\n \"variantId\": null,\n \"result\": {\n \"handled\": true,\n \"file\": \"src/routes/+page.svelte\",\n \"carbonize\": false\n },\n \"completedAt\": \"<ISO>\"\n}\n",
".impeccable/live/config.json": "{\n \"files\": [\"src/app.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n",
"src/app.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>SvelteKit Fixture</title>\n %sveltekit.head%\n </head>\n <body data-sveltekit-preload-data=\"hover\">\n <div style=\"display: contents\">%sveltekit.body%</div>\n </body>\n</html>\n",
"src/routes/+page.svelte": "<script>\n let title = 'SvelteKit Fixture';\n let expenses = [\n { id: 1, label: 'Coffee', amount: 3 },\n { id: 2, label: 'Lunch', amount: 12 },\n ];\n</script>\n\n<main class=\"page\">\n <h1 class=\"hero-title\">{title}</h1>\n <p class=\"hero-hook\">Minimal SvelteKit route for oracle live-mode goldens.</p>\n <ul class=\"expense-list\">\n {#each expenses as expense (expense.id)}\n <li class=\"expense-row\">{expense.label}: {expense.amount}</li>\n {/each}\n </ul>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n</main>\n\n<style>\n .hero-title { font-size: 2rem; }\n .expense-list { list-style: none; padding: 0; }\n .expense-row { padding: 4px 0; }\n</style>\n"
"src/routes/+page.svelte": "<script>\n let title = 'SvelteKit Fixture';\n let expenses = [\n { id: 1, label: 'Coffee', amount: 3 },\n { id: 2, label: 'Lunch', amount: 12 },\n ];\n</script>\n\n<main class=\"page\">\n\n <h1 class=\"hero-title\">{title}</h1>\n\n <p class=\"hero-hook\">Minimal SvelteKit route for oracle live-mode goldens.</p>\n <ul class=\"expense-list\">\n {#each expenses as expense (expense.id)}\n <li class=\"expense-row\">{expense.label}: {expense.amount}</li>\n {/each}\n </ul>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n</main>\n\n<style>\n .hero-title { font-size: 2rem; }\n .expense-list { list-style: none; padding: 0; }\n .expense-row { padding: 4px 0; }\n</style>\n"
}
}
@@ -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>