Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/automation/autonomousRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2336,6 +2336,12 @@ export class AutonomousRunner {
this.registryScanAt.set(resolvedPath, Date.now());
void scanRepository(resolvedPath, projectId, { timeoutMs: 180_000 })
.then((result) => {
if (result.incomplete) {
this.syslog(
`Registry scan ${projectId} was INCOMPLETE after ${result.durationMs}ms: `
+ result.incompleteReasons.slice(0, 5).join('; '),
);
}
this.syslog(
`Registry scan ${projectId}: ${result.extracted} entities `
+ `(+${result.registered}/~${result.updated}) in ${result.durationMs}ms`,
Expand Down
14 changes: 14 additions & 0 deletions src/cli/checkHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ export async function handleCheck(
console.log(` Tests mapped: ${c.cyan(String(result.testsMapped))}`);
console.log(` Duration: ${c.dim(`${result.durationMs}ms`)}`);

if (result.incomplete) {
console.log(`\n ${c.yellow('Incomplete coverage — the scan was truncated:')}`);
for (const reason of result.incompleteReasons) {
console.log(` ${c.yellow(reason)}`);
}
}

if (Object.keys(result.languageBreakdown).length > 0) {
console.log(`\n ${c.dim('By language:')}`);
for (const [lang, count] of Object.entries(result.languageBreakdown).sort((a, b) => b[1] - a[1])) {
Expand Down Expand Up @@ -212,6 +219,13 @@ export async function handleCheck(
console.log(` WARNING: ${(result.warning > 0 ? c.yellow : c.green)(String(result.warning))}`);
console.log(` MINOR: ${(result.minor > 0 ? c.dim : c.green)(String(result.minor))}`);

if (result.incomplete) {
console.log(` ${c.yellow('INCOMPLETE — the scan was truncated:')}`);
for (const reason of result.incompleteReasons.slice(0, 5)) {
console.log(` ${c.dim(reason)}`);
}
}

if (result.issues.length > 0) {
// CRITICAL 먼저
const criticals = result.issues.filter(i => i.severity === 'critical');
Expand Down
18 changes: 14 additions & 4 deletions src/knowledge/gitInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,21 +145,31 @@ export async function getRecentlyChangedFiles(
projectPath: string,
sinceTimestamp: number,
): Promise<string[]> {
const files = new Set<string>();
try {
const sinceDate = new Date(sinceTimestamp).toISOString();
const output = await runGitCommand(projectPath, [
const committed = await runGitCommand(projectPath, [
'log',
`--since=${sinceDate}`,
'--name-only',
'--format=',
]);

const files = new Set<string>();
for (const line of output.split('\n')) {
for (const line of committed.split('\n')) {
const trimmed = line.trim();
if (trimmed) files.add(trimmed);
}

const untracked = await runGitCommand(projectPath, [
'ls-files',
'--others',
'--exclude-standard',
'-z',
]);
for (const token of untracked.split('\0')) {
const trimmed = token.trim();
if (trimmed) files.add(trimmed);
}

return Array.from(files);
} catch {
return [];
Expand Down
44 changes: 44 additions & 0 deletions src/knowledge/untrackedDiscovery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// AGT-3490 — incremental refresh must discover newly created untracked files.

import { execFile } from 'node:child_process';
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { getRecentlyChangedFiles } from './gitInfo.js';

const execFileAsync = promisify(execFile);
let tmp: string;

beforeEach(async () => {
tmp = await mkdtemp(join(tmpdir(), 'openswarm-knowledge-'));
await execFileAsync('git', ['init'], { cwd: tmp });
await execFileAsync('git', ['config', 'user.email', 'test@example.com'], { cwd: tmp });
await execFileAsync('git', ['config', 'user.name', 'test'], { cwd: tmp });
});

afterEach(async () => {
await rm(tmp, { recursive: true, force: true });
});

describe('incremental refresh discovery', () => {
it('includes newly created untracked source files alongside committed history', async () => {
await mkdir(join(tmp, 'src'), { recursive: true });
await writeFile(join(tmp, 'src/committed.ts'), 'export const committed = 1;\n', 'utf-8');
await execFileAsync('git', ['add', 'src/committed.ts'], { cwd: tmp });
await execFileAsync('git', ['commit', '-m', 'committed'], { cwd: tmp });

await writeFile(join(tmp, 'src/untracked.ts'), 'export const untracked = 1;\n', 'utf-8');
const since = new Date(Date.now() - 60_000).getTime();
const changed = await getRecentlyChangedFiles(tmp, since);

expect(changed).toContain('src/committed.ts');
expect(changed).toContain('src/untracked.ts');
});

it('returns an empty list when git discovery fails entirely', async () => {
const changed = await getRecentlyChangedFiles(join(tmp, 'not-a-repo'), Date.now());
expect(changed).toEqual([]);
});
});
34 changes: 30 additions & 4 deletions src/registry/bsDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface BsIssue {
export interface BsScanResult {
issues: BsIssue[];
filesScanned: number;
incomplete: boolean;
incompleteReasons: string[];
critical: number;
warning: number;
minor: number;
Expand Down Expand Up @@ -268,15 +270,28 @@ export async function scanFile(filePath: string): Promise<BsIssue[]> {

// ============ 결과 집계 ============

export function aggregateResults(issues: BsIssue[], filesScanned: number): BsScanResult {
export function aggregateResults(
issues: BsIssue[],
filesScanned: number,
incompleteReasons: string[] = [],
): BsScanResult {
const critical = issues.filter(i => i.severity === 'critical').length;
const warning = issues.filter(i => i.severity === 'warning').length;
const minor = issues.filter(i => i.severity === 'minor').length;
const bsScore = filesScanned > 0
? (critical * 10 + warning * 3 + minor * 1) / filesScanned
: 0;

return { issues, filesScanned, critical, warning, minor, bsScore };
return {
issues,
filesScanned,
incomplete: incompleteReasons.length > 0,
incompleteReasons: Array.from(new Set(incompleteReasons)).slice(0, 50),
critical,
warning,
minor,
bsScore,
};
}

// ============ 레포 전체 스캔 ============
Expand Down Expand Up @@ -332,6 +347,7 @@ export async function scanRepository(
let filesScanned = 0;
const deadline = Date.now() + 30_000;
const maxFiles = 10_000;
const incompleteReasons: string[] = [];

async function walk(dirPath: string, relPath: string): Promise<void> {
let entries;
Expand All @@ -344,7 +360,14 @@ export async function scanRepository(
}

for (const entry of entries) {
if (Date.now() >= deadline || filesScanned >= maxFiles) return;
if (Date.now() >= deadline) {
incompleteReasons.push(`${relPath || '.'}: bs scan timeout reached`);
return;
}
if (filesScanned >= maxFiles) {
incompleteReasons.push(`${relPath || '.'}: bs scan file cap ${maxFiles} reached`);
return;
}
const fullPath = join(dirPath, entry.name);
const entryRelPath = relPath ? `${relPath}/${entry.name}` : entry.name;

Expand All @@ -369,6 +392,9 @@ export async function scanRepository(
}
} catch (err) {
allIssues.push(makeFilesystemIssue('readFile', entryRelPath, err));
if (err instanceof Error && err.message.startsWith('Source file exceeds')) {
incompleteReasons.push(`${entryRelPath}: source file excluded by size limit`);
}
if (verbose) console.log(` [bs] readFile failed ${entryRelPath}: ${err instanceof Error ? err.message : String(err)}`);
continue;
}
Expand All @@ -377,5 +403,5 @@ export async function scanRepository(
}

await walk(projectPath, '');
return aggregateResults(allIssues, filesScanned);
return aggregateResults(allIssues, filesScanned, incompleteReasons);
}
23 changes: 20 additions & 3 deletions src/registry/entityScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,8 @@ export interface ScanResult {
updated: number;
removed: number;
testsMapped: number;
incomplete: boolean;
incompleteReasons: string[];
errors: string[];
durationMs: number;
languageBreakdown: Record<string, number>;
Expand Down Expand Up @@ -700,10 +702,17 @@ export async function scanRepository(
const languageBreakdown: Record<string, number> = {};
const scannedSourceFiles = new Set<string>();
let scannedFiles = 0;
const incompleteReasons: string[] = [];

async function walk(dirPath: string, relPath: string, depth: number): Promise<void> {
if (depth > maxDepth) return;
if (Date.now() - startTime > timeoutMs) return;
if (depth > maxDepth) {
incompleteReasons.push(`${relPath || '.'}: depth limit ${maxDepth} reached`);
return;
}
if (Date.now() - startTime > timeoutMs) {
incompleteReasons.push(`${relPath || '.'}: scan timeout ${timeoutMs}ms reached`);
return;
}

let entries;
try {
Expand All @@ -715,7 +724,10 @@ export async function scanRepository(
}

for (const entry of entries) {
if (Date.now() - startTime > timeoutMs) return;
if (Date.now() - startTime > timeoutMs) {
incompleteReasons.push(`${relPath || '.'}: scan timeout ${timeoutMs}ms reached`);
return;
}

const fullPath = join(dirPath, entry.name);
const entryRelPath = relPath ? `${relPath}/${entry.name}` : entry.name;
Expand Down Expand Up @@ -748,6 +760,9 @@ export async function scanRepository(
}
} catch (err) {
errors.push(`${entryRelPath}: ${err instanceof Error ? err.message : String(err)}`);
if (err instanceof Error && err.message.startsWith('Source file exceeds')) {
incompleteReasons.push(`${entryRelPath}: source file excluded by size limit`);
}
}
}
}
Expand Down Expand Up @@ -880,6 +895,8 @@ export async function scanRepository(
updated,
removed,
testsMapped,
incomplete: incompleteReasons.length > 0,
incompleteReasons: Array.from(new Set(incompleteReasons)).slice(0, 50),
errors,
durationMs: Date.now() - startTime,
languageBreakdown,
Expand Down
42 changes: 42 additions & 0 deletions src/registry/entityWarningsBudget.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// AGT-3490 — entityWarnings needs a request-wide result budget, not per-page caps.

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

let warned: Array<{ offset: number; limit: number }> = [];

vi.mock('./sqliteStore.js', () => ({
getRegistryStore: () => ({
getUnresolvedWarnings: (_severity: unknown, _projectId: unknown, limit: number, offset: number) => {
warned.push({ limit, offset });
return [];
},
}),
}));

let registryResolvers: (typeof import('./graphql/resolvers.js'))['registryResolvers'];

beforeEach(async () => {
warned = [];
({ registryResolvers } = await import('./graphql/resolvers.js'));
});

afterEach(() => {
vi.restoreAllMocks();
});

describe('entityWarnings request budget', () => {
it('accepts a page that fits within the request-wide budget', async () => {
await registryResolvers.Query.entityWarnings(undefined, { limit: 50, offset: 100 });
expect(warned).toEqual([{ limit: 50, offset: 100 }]);
});

it('rejects an offset+limit window beyond the request-wide budget', async () => {
try {
await registryResolvers.Query.entityWarnings(undefined, { limit: 200, offset: 150 });
expect.unreachable('the window should have been rejected');
} catch (err) {
expect((err as Error).message).toContain('exceeds the request budget of 200');
}
expect(warned).toEqual([]);
});
});
12 changes: 10 additions & 2 deletions src/registry/graphql/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const MAX_SEARCH_LIMIT = 100;
const DEFAULT_EVENT_LIMIT = 20;
const MAX_EVENT_LIMIT = 200;
const MAX_BULK_REGISTER_ENTITIES = 100;
const MAX_ENTITY_WARNINGS_RESULTS = 200;

function clampLimit(limit: number | undefined, defaultLimit: number, maxLimit: number): number {
if (limit === undefined || !Number.isInteger(limit)) return defaultLimit;
Expand Down Expand Up @@ -129,11 +130,18 @@ export const registryResolvers = {
entityWarnings: (_: unknown, { severity, projectId, limit, offset }: {
severity?: WarningSeverity; projectId?: string; limit?: number; offset?: number;
}) => {
const offsetValue = clampOffset(offset);
const pageSize = clampLimit(limit, DEFAULT_ENTITY_LIMIT, MAX_ENTITY_LIMIT);
if (offsetValue + pageSize > MAX_ENTITY_WARNINGS_RESULTS) {
throw new Error(
`entityWarnings result window exceeds the request budget of ${MAX_ENTITY_WARNINGS_RESULTS} (offset ${offsetValue} + limit ${pageSize})`,
);
}
return getRegistryStore().getUnresolvedWarnings(
severity,
projectId,
clampLimit(limit, DEFAULT_ENTITY_LIMIT, MAX_ENTITY_LIMIT),
clampOffset(offset),
pageSize,
offsetValue,
);
},

Expand Down
Loading
Loading