mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 12:15:29 +02:00
perf(gateguard): profile a change only when used and check its window first
This commit is contained in:
@@ -254,12 +254,25 @@ against the file it applies to.
|
||||
eight levels.
|
||||
- Go files that import `"C"` are never trivial: the comment before the import
|
||||
is C code.
|
||||
- The window comparison runs before the prefix scan. Both must pass, so the
|
||||
order changes no result, but most first-touch edits change code and fail
|
||||
the window comparison, which spares them a scan of everything above the
|
||||
edit.
|
||||
|
||||
The scanner was checked against real tokenizers over this repository: every
|
||||
line start it calls plain code in 801 JS files (espree) and 146 Python files
|
||||
(`tokenize`) is outside strings, templates, regex literals and comments and
|
||||
not a backslash continuation.
|
||||
|
||||
## When a change is profiled
|
||||
|
||||
The profile is computed only when its result is used: for classes whose
|
||||
questions depend on it (code), for Edit and MultiEdit calls on the classes
|
||||
that can pass as trivial (code, test, prose), and for every call when
|
||||
decision metrics are on, so the metrics lines keep their profile fields.
|
||||
Instruction and config targets, and new test or prose files, skip it and the
|
||||
lexer modules it needs.
|
||||
|
||||
## Questions from the change profile
|
||||
|
||||
Every question has a stable id, and `questionIdsFor(class, isWrite, profile)`
|
||||
|
||||
@@ -1674,6 +1674,11 @@ function countTrivialAllow() {
|
||||
return saveState({ ...state, trivial_allows: getTrivialAllowCount(state) + 1 });
|
||||
}
|
||||
|
||||
// see docs/gateguard/change-profile.md#when-a-change-is-profiled
|
||||
function needsProfile(toolName, cls) {
|
||||
return targetClass().questionsUseProfile(cls) || ((toolName === 'Edit' || toolName === 'MultiEdit') && TRIVIAL_CLASSES.has(cls)) || isMetricsEnabled();
|
||||
}
|
||||
|
||||
function isTrivialChange(cls, restricted, profile) {
|
||||
return !restricted && TRIVIAL_CLASSES.has(cls) && Boolean(profile) && profile.known === true && profile.trivial === true;
|
||||
}
|
||||
@@ -2332,7 +2337,9 @@ function gate(rawInput) {
|
||||
recordDecision('credit', 'prior-search', { target: filePath, cls, sensitive });
|
||||
return { additionalContext: creditNote(credit, filePath), exitCode: 0 };
|
||||
}
|
||||
const profile = restricted || toolName === 'NotebookEdit' ? null : changeProfileFor(toolName, filePath, [toolInput], toolInput.content, data);
|
||||
const profile = restricted || toolName === 'NotebookEdit' || !needsProfile(toolName, cls)
|
||||
? null
|
||||
: changeProfileFor(toolName, filePath, [toolInput], toolInput.content, data);
|
||||
if (toolName === 'Edit' && isTrivialChange(cls, restricted, profile)) {
|
||||
if (!countTrivialAllow()) {
|
||||
return allowWithStateWarning();
|
||||
@@ -2406,7 +2413,9 @@ function gate(rawInput) {
|
||||
notes.push(creditNote(credit, filePath));
|
||||
continue;
|
||||
}
|
||||
const profile = restricted ? null : changeProfileFor('Edit', filePath, entriesFor(edits, fileKey, data), undefined, data);
|
||||
const profile = restricted || !needsProfile('MultiEdit', cls)
|
||||
? null
|
||||
: changeProfileFor('Edit', filePath, entriesFor(edits, fileKey, data), undefined, data);
|
||||
if (isTrivialChange(cls, restricted, profile)) {
|
||||
if (!countTrivialAllow()) {
|
||||
return allowWithStateWarning();
|
||||
|
||||
@@ -708,12 +708,13 @@ function trivialInFile(pairs, fileText, spec) {
|
||||
const start = windowStart(text, found.first);
|
||||
const end = windowEnd(text, found.last + oldString.length);
|
||||
work += start + 2 * (end - start);
|
||||
if (end - start > MAX_WINDOW_CHARS || work > MAX_CONTEXT_WORK || !spec.startsFresh(text.slice(0, start), spec)) return false;
|
||||
if (end - start > MAX_WINDOW_CHARS || work > MAX_CONTEXT_WORK) return false;
|
||||
const before = text.slice(start, end);
|
||||
const after = replaceAll
|
||||
? before.split(oldString).join(newString)
|
||||
: `${text.slice(start, found.first)}${newString}${text.slice(found.first + oldString.length, end)}`;
|
||||
if (!isTrivialEdit(before, after, spec)) return false;
|
||||
// see docs/gateguard/change-profile.md#file-context
|
||||
if (!isTrivialEdit(before, after, spec) || !spec.startsFresh(text.slice(0, start), spec)) return false;
|
||||
text = `${text.slice(0, start)}${after}${text.slice(end)}`;
|
||||
if (text.length > MAX_FILE_CHARS) return false;
|
||||
}
|
||||
|
||||
@@ -193,6 +193,11 @@ function codeQuestionIds(isWrite, profile) {
|
||||
}
|
||||
|
||||
/** Stable ids of the first-touch questions for a class, action and change profile. */
|
||||
/** True when a class's questions depend on the change profile. */
|
||||
function questionsUseProfile(cls) {
|
||||
return !Object.hasOwn(CLASS_QUESTION_IDS, cls);
|
||||
}
|
||||
|
||||
function questionIdsFor(cls, isWrite, profile) {
|
||||
const ids = Object.hasOwn(CLASS_QUESTION_IDS, cls) ? CLASS_QUESTION_IDS[cls](Boolean(isWrite)) : codeQuestionIds(Boolean(isWrite), profile);
|
||||
return [...ids, 'quote-instruction'];
|
||||
@@ -407,6 +412,7 @@ function collapseGateDir(filePath, data, cls) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
questionsUseProfile,
|
||||
WINDOWS_PATH_PATTERN,
|
||||
COLLAPSIBLE_CLASSES,
|
||||
CLASS_QUESTIONS,
|
||||
|
||||
@@ -7411,7 +7411,8 @@ function runTests() {
|
||||
['a first shell command loads only the read-only check', lazyBash('npm test'), [], ['gateguard-readonly-shell', 'gateguard-state']],
|
||||
['a shell command after the routine gate loads only the state helpers', lazyBash('npm run build'), [lazyBash('npm test')], ['gateguard-state']],
|
||||
['an edit of a checked file loads only the target classification', lazyEdit, [lazyEdit], ['gateguard-state', 'gateguard-target-class']],
|
||||
['a first touch without a transcript loads no search matching', lazyEdit, [], ['file-tail', 'gateguard-change-profile', 'gateguard-code-lexer', 'gateguard-file-context', 'gateguard-state', 'gateguard-target-class', 'gateguard-turn-scan']]
|
||||
['a first touch without a transcript loads no search matching', lazyEdit, [], ['file-tail', 'gateguard-change-profile', 'gateguard-code-lexer', 'gateguard-file-context', 'gateguard-state', 'gateguard-target-class', 'gateguard-turn-scan']],
|
||||
['a first touch of an instruction file skips the change profile', { ...lazyEdit, tool_input: { ...lazyEdit.tool_input, file_path: '/tmp/gateguard-lazy/CLAUDE.md' } }, [], ['file-tail', 'gateguard-state', 'gateguard-target-class', 'gateguard-turn-scan']]
|
||||
];
|
||||
for (const [label, payload, setup, expected] of lazyCases) {
|
||||
if (test(`lazy loading: ${label}`, () => assert.deepStrictEqual(loadedLibs(payload, setup), expected))) passed++;
|
||||
|
||||
Reference in New Issue
Block a user