Hook: stand down for the whole edit when the primary carries live markers

Field-testing the generate command on a Vite React app showed the
per-file stand-down was not enough: an edit to the wrapped App.jsx skipped
that file but still co-scanned the stylesheet it imports and spoke up
about it (a clean ack or findings) mid-session, which is exactly the noise
the stand-down exists to prevent. When the edited primary file carries the
markers, the whole PostToolUse event now returns skipped: live-preview
with the audit naming that primary, co-scanned stylesheets included; a
marked file that is only co-scanned still skips alone. A unit case covers
both, and the contract documents the event-level stand-down.

AI-assisted: found by field tests and fixed with Claude Code under
maintainer direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Abdul Wahab
2026-09-15 05:45:49 +05:00
committed by Abdul Wahab
co-authored by Claude Fable 5
parent da403a3410
commit 8fb7f7fec5
3 changed files with 44 additions and 1 deletions
+20
View File
@@ -185,6 +185,7 @@ pub fn run_hook(rt: &Runtime, stdin: &str) -> RunResult {
let quiet_mode = truthy(rt.env("IMPECCABLE_HOOK_QUIET")) || config.quiet;
let mut detector_threw_any = false;
let mut last_skip = "no-scannable-file";
let mut live_preview_edit: Option<String> = None;
let mut suppressed_hit = false;
let mut cache_dirty = false;
let mut deferred_total: usize = 0;
@@ -277,6 +278,14 @@ pub fn run_hook(rt: &Runtime, stdin: &str) -> RunResult {
}
};
if crate::hook_lib::has_live_preview_markers(&content) {
// A live variant session owns this file. When it is the edited
// (primary) file, the whole event stands down, co-scanned
// stylesheets included: a clean ack or a finding about the
// companion file is the same mid-session noise the stand-down
// exists to prevent.
if primary_files.contains(file_path) && live_preview_edit.is_none() {
live_preview_edit = Some(file_path.clone());
}
last_skip = "live-preview";
continue;
}
@@ -358,6 +367,17 @@ pub fn run_hook(rt: &Runtime, stdin: &str) -> RunResult {
}
}
}
if let Some(file) = live_preview_edit {
audit.insert("file".into(), Value::String(file));
return result(
&audit,
vec![
("emitted", Value::Bool(false)),
("skipped", Value::from("live-preview")),
("durationMs", ms_since(started)),
],
);
}
if !fresh_groups.is_empty() {
let scan = &scans[&fresh_groups[0].file_path];
+23
View File
@@ -2258,3 +2258,26 @@ fn before_edit_stands_down_on_live_preview_markers() {
assert_eq!(code, 0);
assert_eq!(out, "{\"permission\":\"allow\"}");
}
#[test]
fn run_hook_stands_down_for_the_whole_edit_when_the_primary_carries_live_markers() {
// The edited JSX carries the wrapper; the stylesheet it imports does not.
// Co-scanning would still speak up about the stylesheet mid-session, so
// the whole event stands down. The same files without markers prove the
// co-scan is otherwise live.
let t = Tmp::new();
let cwd = t.path();
let r = rt(&cwd);
t.write("src/styles.css", GRADIENT_CSS);
let plain = t.write("src/Plain.jsx", "import './styles.css';\nexport default function Plain() { return <h1 className=\"hero-title\">Hi</h1>; }\n");
let reported = hook::run_hook(&r, &edit_event(&cwd, &plain, "s1"));
assert!(reported.stdout.contains("[gradient-text]"), "co-scanned stylesheet is reported without markers: {}", reported.stdout);
let wrapped = t.write(
"src/App.jsx",
"import './styles.css';\n{/* impeccable-variants-start ab12cd34 */}<div data-impeccable-variants=\"ab12cd34\" data-impeccable-variant-count=\"3\"></div>\n",
);
let skipped = hook::run_hook(&r, &edit_event(&cwd, &wrapped, "s2"));
assert_eq!(skipped.stdout, "", "nothing is emitted while the edited file is in a live session");
assert_eq!(skipped.audit["skipped"], json!("live-preview"));
assert!(audit_str(&skipped.audit, "file").unwrap_or("").ends_with("src/App.jsx"), "the audit names the edited file, not the companion");
}
+1 -1
View File
@@ -1105,7 +1105,7 @@ Candidates in order: `<scripts>/detector/detect-antipatterns.mjs` (built skill l
4. `config = readConfig(projectCwd)`; `enabled === false``'config-disabled'`.
5. native platform → `skipped:'native-platform', platform`.
6. `cache = readCache(projectCwd)`; `sessionId = event.session_id || 'unknown'`; detector missing → `'detector-missing'`; `scanOptions = designSystemOptions(...)`; `tiered = perEditTieringActive(config, harness)`; `quietMode = truthy(IMPECCABLE_HOOK_QUIET) || config.quiet`.
7. For each target file (audit.file updated each iteration): skip with `lastSkip` = `'sensitive'` (contains `..` or SENSITIVE_PATH), `'generated'`, `'extension'` (not ALLOWED and not configured), `'config-ignore-file'` (`matchesAnyGlob(relativized)` or `(absolute)` vs `config.ignoreFiles`), `'file-missing'`, `'outside-project'`, `'too-large'` (records `skippedBytes`), and, once the content is read, `'live-preview'` (LIVE_PREVIEW_MARKERS). If the file is a PRIMARY (not co-scanned): `editCount = bumpEditCount(...)`; if `editCount > 6` → if `=== 7` and no suppression winner yet → `suppressionWinner={filePath}`; `lastSkip='suppressed'`, `suppressedHit=true`, continue. Read content, run detector (throw → `findings=[]`, `detectorThrew=true`). `filtered = filterFindings(...)`; if tiered split into immediate/deferred else all immediate. If deferred non-empty → `touchFile`, `deferredTotal += n`. `fresh = dedupeAgainstCache(immediate, ...)`. `audit.findings = raw count`, `audit.freshFindings = fresh.length`, `audit.deferred = deferredTotal` (if >0). If detectorThrew → `detectorThrewAny=true`, continue (cache untouched for that file). `rememberFindings(cache, sid, file, immediate)` (replace). If fresh>0 → push `{filePath, findings: fresh}` to `freshGroups`, continue. Else if immediate>0 and no pendingWinner → `pendingWinner={filePath, known: immediate.map(findingCacheKey)}`; else if immediate==0 and no cleanWinner: if quiet or not ack-eligible → `cleanWinner={filePath}` (without consuming `cleanAcked`); else if `fileEntry.cleanAcked``cleanAckDeduped=true` (keep scanning); else set `cleanAcked=true`, `cleanWinner={filePath}`, `cleanAckDeduped=false`.
7. For each target file (audit.file updated each iteration): skip with `lastSkip` = `'sensitive'` (contains `..` or SENSITIVE_PATH), `'generated'`, `'extension'` (not ALLOWED and not configured), `'config-ignore-file'` (`matchesAnyGlob(relativized)` or `(absolute)` vs `config.ignoreFiles`), `'file-missing'`, `'outside-project'`, `'too-large'` (records `skippedBytes`), and, once the content is read, `'live-preview'` (LIVE_PREVIEW_MARKERS; when the marked file is the edited PRIMARY, the whole event returns `{emitted:false, skipped:'live-preview'}` with `file` = that primary, co-scanned stylesheets included). If the file is a PRIMARY (not co-scanned): `editCount = bumpEditCount(...)`; if `editCount > 6` → if `=== 7` and no suppression winner yet → `suppressionWinner={filePath}`; `lastSkip='suppressed'`, `suppressedHit=true`, continue. Read content, run detector (throw → `findings=[]`, `detectorThrew=true`). `filtered = filterFindings(...)`; if tiered split into immediate/deferred else all immediate. If deferred non-empty → `touchFile`, `deferredTotal += n`. `fresh = dedupeAgainstCache(immediate, ...)`. `audit.findings = raw count`, `audit.freshFindings = fresh.length`, `audit.deferred = deferredTotal` (if >0). If detectorThrew → `detectorThrewAny=true`, continue (cache untouched for that file). `rememberFindings(cache, sid, file, immediate)` (replace). If fresh>0 → push `{filePath, findings: fresh}` to `freshGroups`, continue. Else if immediate>0 and no pendingWinner → `pendingWinner={filePath, known: immediate.map(findingCacheKey)}`; else if immediate==0 and no cleanWinner: if quiet or not ack-eligible → `cleanWinner={filePath}` (without consuming `cleanAcked`); else if `fileEntry.cleanAcked``cleanAckDeduped=true` (keep scanning); else set `cleanAcked=true`, `cleanWinner={filePath}`, `cleanAckDeduped=false`.
8. If `freshGroups` non-empty: `text = appendDesignSystemNoteOnce(renderGroupedTemplate(freshGroups, config, {cwd:projectCwd, footer: footerModeForSession, reserveChars: designNoteReserve}), ...)`; `commitFooterShown`; **`persistCache` always** (creates `.impeccable/` if needed); return `stdout = payload(text,'PostToolUse',harness)`, audit `{..., file: firstGroup.filePath, emitted:true, freshFiles, freshFindings(total), chars, durationMs}`, `emission:{kind:'fresh', file, findings, groups}`.
9. Else compute `ack`: not quiet AND pendingWinner AND ack-eligible → `{kind:'pending', text: appendDesignSystemNoteOnce(renderPendingAck(...))}`; else not quiet AND no suppressionWinner AND cleanWinner AND !cleanAckDeduped AND ack-eligible → `{kind:'clean', text: appendDesignSystemNoteOnce(renderCleanAck(...))}`.
10. Persist cache only if `deferredTotal > 0 || (cacheDirty && exists(<projectCwd>/.impeccable))` (a clean edit in a project with no `.impeccable/` footprint writes nothing to disk).