Sync generated provider output

This commit is contained in:
github-actions[bot]
2026-07-06 00:12:08 +00:00
parent 751ec31dd6
commit 88f52ac4e6
26 changed files with 741 additions and 91 deletions
@@ -27,6 +27,7 @@ import {
readCache,
readConfig,
renderTemplate,
resolveCacheCwd,
resolveProjectCwd,
truthy,
writeAuditLog,
@@ -379,9 +380,12 @@ async function main() {
return allow({ skipped: 'stdin-empty' });
}
const cwd = resolveProjectCwd(event);
const sessionCwd = resolveProjectCwd(event);
const started = Date.now();
const filePath = proposedFilePath(event, cwd);
const filePath = proposedFilePath(event, sessionCwd);
// Re-key config/cache to the edited file's project root when the session
// was launched from a non-project umbrella directory (issue #305).
const cwd = resolveCacheCwd(filePath, sessionCwd);
const audit = {
harness: 'cursor',
cwd,
@@ -10,7 +10,7 @@
* truthy(value)
* readConfig(cwd) / DEFAULT_CONFIG / getConfigPath(cwd) / getLocalConfigPath(cwd)
* normalizeIgnoreValue(value)
* readCache(cwd) / persistCache(cwd, cache)
* readCache(cwd) / persistCache(cwd, cache) / resolveCacheCwd(primaryFile, sessionCwd)
* bumpEditCount(cache, sessionId, filePath) -> number
* suppressionNotice(filePath)
* filterFindings(findings, content, ext, config)
@@ -35,6 +35,7 @@
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { pathToFileURL, fileURLToPath } from 'node:url';
@@ -134,6 +135,39 @@ export function resolveProjectCwd(event, fallback = process.cwd()) {
|| fallback;
}
function looksLikeProjectRoot(dir) {
return ['.git', 'package.json', '.impeccable'].some((marker) => {
try { return fs.existsSync(path.join(dir, marker)); } catch { return false; }
});
}
// Where `.impeccable/` (cache + config) lives for this event. Normally the
// session cwd, untouched. But when the agent was launched from an umbrella
// directory that is not itself a project (no .git, package.json, or
// .impeccable), key to the edited file's nearest project root instead, so a
// multi-project launch dir doesn't accumulate a shared cross-project cache
// (issue #305). Climbing stops at the home dir, falling back to the session
// cwd when no marker is found.
export function resolveCacheCwd(primaryFile, sessionCwd) {
const base = path.resolve(sessionCwd || process.cwd());
if (!primaryFile || typeof primaryFile !== 'string' || hasPathTraversal(primaryFile)) return base;
if (looksLikeProjectRoot(base)) return base;
let dir;
try {
dir = path.dirname(path.resolve(primaryFile));
} catch {
return base;
}
const home = path.resolve(os.homedir());
while (true) {
if (dir === home) return base;
if (looksLikeProjectRoot(dir)) return dir;
const parent = path.dirname(dir);
if (parent === dir) return base;
dir = parent;
}
}
export function readConfig(cwd) {
const config = cloneDefaultConfig();
// Hook runtime settings live under `hook`; detector filters live under
@@ -1402,9 +1436,10 @@ export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now =
event = normalizeHookEvent(event, cwd, harness);
audit.harness = harness;
const projectCwd = event.cwd || cwd;
const sessionCwd = event.cwd || cwd;
const primaryFiles = normalizeScanTargets(resolveTargetFiles(event, sessionCwd), sessionCwd);
const projectCwd = resolveCacheCwd(primaryFiles[0], sessionCwd);
audit.cwd = projectCwd;
const primaryFiles = normalizeScanTargets(resolveTargetFiles(event, projectCwd), projectCwd);
const primaryFileSet = new Set(primaryFiles);
const targetFiles = expandScanTargets(primaryFiles, projectCwd);
audit.session = event.session_id || null;
@@ -1423,7 +1458,7 @@ export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now =
const sessionId = event.session_id || 'unknown';
const det = detector || await loadDetector();
if (!det || typeof det.detectText !== 'function') {
persistCache(projectCwd, cache);
// Cache is not mutated yet at this point; nothing to persist.
return result({ skipped: 'detector-missing', durationMs: Date.now() - started });
}
const scanOptions = designSystemOptions(config, det, projectCwd);
@@ -1435,6 +1470,7 @@ export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now =
let detectorThrewAny = false;
let lastSkip = 'no-scannable-file';
let suppressedHit = false;
let cacheDirty = false;
for (const filePath of targetFiles) {
audit.file = filePath;
@@ -1467,6 +1503,7 @@ export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now =
if (primaryFileSet.has(filePath)) {
const editCount = bumpEditCount(cache, sessionId, filePath);
cacheDirty = true;
audit.editCount = editCount;
if (editCount > EDIT_COUNT_THRESHOLD) {
@@ -1496,6 +1533,7 @@ export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now =
if (fresh.length > 0) {
rememberFindings(cache, sessionId, filePath, fresh);
cacheDirty = true;
freshGroups.push({ filePath, findings: fresh });
continue;
}
@@ -1513,7 +1551,15 @@ export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now =
}
}
persistCache(projectCwd, cache);
// Persist only when the write is earned: fresh findings justify creating
// `.impeccable/` (dedup and suppression need it), and an already-present
// `.impeccable/` dir marks a project that opted in. A non-UI edit, or a
// clean UI edit in a project with no Impeccable footprint, must be a
// no-op on disk (issues #344, #305).
if (freshGroups.length > 0
|| (cacheDirty && fs.existsSync(path.join(projectCwd, '.impeccable')))) {
persistCache(projectCwd, cache);
}
if (freshGroups.length > 0) {
const firstGroup = freshGroups[0];