From c0f7f2c12ddfbffec537f5e2a314a34b03584c2d Mon Sep 17 00:00:00 2001 From: ejclaw Date: Wed, 8 Apr 2026 03:49:54 +0900 Subject: [PATCH] Trim verification snapshot scope --- shared/verification-snapshot.d.ts | 4 ++ shared/verification-snapshot.js | 40 ++++++++++++++++-- src/verification.test.ts | 70 +++++++++++++++++++++++++++++++ src/verification.ts | 8 ++-- 4 files changed, 114 insertions(+), 8 deletions(-) diff --git a/shared/verification-snapshot.d.ts b/shared/verification-snapshot.d.ts index 0102eda..854ac85 100644 --- a/shared/verification-snapshot.d.ts +++ b/shared/verification-snapshot.d.ts @@ -2,6 +2,10 @@ export declare const VERIFICATION_SNAPSHOT_EXCLUDE_NAMES: ReadonlySet; export declare function isVerificationSnapshotExcludedName( name: string, ): boolean; +export declare function isVerificationSnapshotExcludedPath( + repoDir: string, + currentPath: string, +): boolean; export declare function computeVerificationSnapshotId(repoDir: string): string; export declare function resolveVerificationResponsesDir( hostIpcDir: string, diff --git a/shared/verification-snapshot.js b/shared/verification-snapshot.js index 9e0a968..81ded9c 100644 --- a/shared/verification-snapshot.js +++ b/shared/verification-snapshot.js @@ -3,15 +3,46 @@ import fs from 'fs'; import path from 'path'; export const VERIFICATION_SNAPSHOT_EXCLUDE_NAMES = new Set([ - '.git', 'node_modules', - '.env', ]); +const VERIFICATION_SNAPSHOT_ROOT_EXCLUDE_NAMES = new Set([ + '.git', + '.env', + 'data', + 'logs', + 'cache', + '.ejclaw-reviewer-runtime', +]); + +const VERIFICATION_SNAPSHOT_ROOT_EXCLUDE_PREFIXES = [ + 'store.local-backup-', +]; + export function isVerificationSnapshotExcludedName(name) { return VERIFICATION_SNAPSHOT_EXCLUDE_NAMES.has(name); } +export function isVerificationSnapshotExcludedPath(repoDir, currentPath) { + const name = path.basename(currentPath); + if (isVerificationSnapshotExcludedName(name)) { + return true; + } + + const relPath = path.relative(repoDir, currentPath); + const isRepoRootEntry = relPath !== '' && !relPath.includes(path.sep); + if (!isRepoRootEntry) { + return false; + } + + return ( + VERIFICATION_SNAPSHOT_ROOT_EXCLUDE_NAMES.has(name) || + VERIFICATION_SNAPSHOT_ROOT_EXCLUDE_PREFIXES.some((prefix) => + name.startsWith(prefix), + ) + ); +} + function updateVerificationSnapshotHash(hash, repoDir, currentPath) { const relPath = path.relative(repoDir, currentPath) || '.'; const stat = fs.lstatSync(currentPath); @@ -21,8 +52,9 @@ function updateVerificationSnapshotHash(hash, repoDir, currentPath) { hash.update(`dir\0${relPath}\0`); } for (const entry of fs.readdirSync(currentPath).sort()) { - if (isVerificationSnapshotExcludedName(entry)) continue; - updateVerificationSnapshotHash(hash, repoDir, path.join(currentPath, entry)); + const nextPath = path.join(currentPath, entry); + if (isVerificationSnapshotExcludedPath(repoDir, nextPath)) continue; + updateVerificationSnapshotHash(hash, repoDir, nextPath); } return; } diff --git a/src/verification.test.ts b/src/verification.test.ts index 4d1ba94..d90ca68 100644 --- a/src/verification.test.ts +++ b/src/verification.test.ts @@ -83,6 +83,76 @@ describe('verification helpers', () => { expect(third).not.toBe(first); }); + it('ignores runtime state directories and local backup folders', () => { + const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-snapshot-')); + fs.mkdirSync(path.join(repoDir, 'src'), { recursive: true }); + fs.mkdirSync(path.join(repoDir, 'data'), { recursive: true }); + fs.mkdirSync(path.join(repoDir, 'logs'), { recursive: true }); + fs.mkdirSync(path.join(repoDir, 'cache'), { recursive: true }); + fs.mkdirSync(path.join(repoDir, '.ejclaw-reviewer-runtime'), { + recursive: true, + }); + fs.mkdirSync(path.join(repoDir, 'store.local-backup-20260408_024500'), { + recursive: true, + }); + + fs.writeFileSync( + path.join(repoDir, 'src', 'index.ts'), + 'export const x = 1;\n', + ); + fs.writeFileSync(path.join(repoDir, 'data', 'state.json'), '{"x":1}\n'); + fs.writeFileSync(path.join(repoDir, 'logs', 'service.log'), 'line-1\n'); + fs.writeFileSync(path.join(repoDir, 'cache', 'tmp.txt'), 'cache-a\n'); + fs.writeFileSync( + path.join(repoDir, '.ejclaw-reviewer-runtime', 'runtime.json'), + '{"ok":true}\n', + ); + fs.writeFileSync( + path.join(repoDir, 'store.local-backup-20260408_024500', 'db.sqlite'), + 'backup-a\n', + ); + + const first = computeVerificationSnapshot(repoDir).snapshotId; + + fs.writeFileSync(path.join(repoDir, 'data', 'state.json'), '{"x":2}\n'); + fs.writeFileSync(path.join(repoDir, 'logs', 'service.log'), 'line-2\n'); + fs.writeFileSync(path.join(repoDir, 'cache', 'tmp.txt'), 'cache-b\n'); + fs.writeFileSync( + path.join(repoDir, '.ejclaw-reviewer-runtime', 'runtime.json'), + '{"ok":false}\n', + ); + fs.writeFileSync( + path.join(repoDir, 'store.local-backup-20260408_024500', 'db.sqlite'), + 'backup-b\n', + ); + + const second = computeVerificationSnapshot(repoDir).snapshotId; + + expect(second).toBe(first); + }); + + it('still includes nested source directories named like excluded roots', () => { + const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-snapshot-')); + fs.mkdirSync(path.join(repoDir, 'src', 'fixtures', 'data'), { + recursive: true, + }); + fs.writeFileSync( + path.join(repoDir, 'src', 'fixtures', 'data', 'sample.json'), + '{"v":1}\n', + ); + + const first = computeVerificationSnapshot(repoDir).snapshotId; + + fs.writeFileSync( + path.join(repoDir, 'src', 'fixtures', 'data', 'sample.json'), + '{"v":2}\n', + ); + + const second = computeVerificationSnapshot(repoDir).snapshotId; + + expect(second).not.toBe(first); + }); + it('backfills legacy node_modules state before verification', async () => { const repoDir = fs.mkdtempSync( path.join(os.tmpdir(), 'ejclaw-verification-legacy-'), diff --git a/src/verification.ts b/src/verification.ts index 82d1e1a..5cf7658 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -20,7 +20,7 @@ import { } from './workspace-package-manager.js'; import { computeVerificationSnapshotId, - isVerificationSnapshotExcludedName, + isVerificationSnapshotExcludedPath, resolveVerificationResponsesDir, } from '../shared/verification-snapshot.js'; @@ -95,8 +95,8 @@ export function buildVerificationCommand( }; } -function shouldExcludePath(name: string): boolean { - return isVerificationSnapshotExcludedName(name); +function shouldExcludePath(repoDir: string, source: string): boolean { + return isVerificationSnapshotExcludedPath(repoDir, source); } export function computeVerificationSnapshot( @@ -130,7 +130,7 @@ function copyWorkspaceToScratch(repoDir: string, scratchDir: string): void { recursive: true, filter: (source) => { if (source === repoDir) return true; - return !shouldExcludePath(path.basename(source)); + return !shouldExcludePath(repoDir, source); }, }); }