Merge branch 'thedotmack/file-read-timeline-inject' into integration/validation-batch

This commit is contained in:
Alex Newman
2026-04-07 11:18:58 -07:00
9 changed files with 783 additions and 2325 deletions
+39
View File
@@ -111,3 +111,42 @@ export function getObservationsForSession(
return stmt.all(memorySessionId) as ObservationSessionRow[];
}
/**
* Get observations associated with a given file path, scoped to specific projects.
* Matches on the full file path (not just basename) to avoid cross-project collisions.
*/
export function getObservationsByFilePath(
db: Database,
filePath: string,
options?: { projects?: string[]; limit?: number }
): ObservationRecord[] {
const rawLimit = options?.limit;
const limit = Number.isInteger(rawLimit) && (rawLimit as number) > 0
? Math.min(rawLimit as number, 100)
: 15;
const params: (string | number)[] = [filePath, filePath];
let projectClause = '';
if (options?.projects?.length) {
const placeholders = options.projects.map(() => '?').join(',');
projectClause = `AND project IN (${placeholders})`;
params.push(...options.projects);
}
params.push(limit);
const stmt = db.prepare(`
SELECT *
FROM observations
WHERE (
EXISTS (SELECT 1 FROM json_each(files_read) WHERE value = ?)
OR EXISTS (SELECT 1 FROM json_each(files_modified) WHERE value = ?)
)
${projectClause}
ORDER BY created_at_epoch DESC
LIMIT ?
`);
return stmt.all(...params) as ObservationRecord[];
}
@@ -18,6 +18,7 @@ import { SessionManager } from '../../SessionManager.js';
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
import type { WorkerService } from '../../../worker-service.js';
import { BaseRouteHandler } from '../BaseRouteHandler.js';
import { getObservationsByFilePath } from '../../../sqlite/observations/get.js';
export class DataRoutes extends BaseRouteHandler {
constructor(
@@ -39,6 +40,7 @@ export class DataRoutes extends BaseRouteHandler {
// Fetch by ID endpoints
app.get('/api/observation/:id', this.handleGetObservationById.bind(this));
app.get('/api/observations/by-file', this.handleGetObservationsByFile.bind(this));
app.post('/api/observations/batch', this.handleGetObservationsByIds.bind(this));
app.get('/api/session/:id', this.handleGetSessionById.bind(this));
app.post('/api/sdk-sessions/batch', this.handleGetSdkSessionsByIds.bind(this));
@@ -108,6 +110,28 @@ export class DataRoutes extends BaseRouteHandler {
res.json(observation);
});
/**
* Get observations associated with a file path, scoped to projects
* GET /api/observations/by-file?path=<file_path>&projects=<comma,separated>&limit=15
*/
private handleGetObservationsByFile = this.wrapHandler((req: Request, res: Response): void => {
const filePath = req.query.path as string | undefined;
if (!filePath) {
this.badRequest(res, 'path query parameter is required');
return;
}
const projectsParam = req.query.projects as string | undefined;
const projects = projectsParam ? projectsParam.split(',').filter(Boolean) : undefined;
const parsedLimit = req.query.limit ? parseInt(req.query.limit as string, 10) : undefined;
const limit = Number.isFinite(parsedLimit) && parsedLimit! > 0 ? parsedLimit : undefined;
const db = this.dbManager.getSessionStore().db;
const observations = getObservationsByFilePath(db, filePath, { projects, limit });
res.json({ observations, count: observations.length });
});
/**
* Get observations by array of IDs
* POST /api/observations/batch
@@ -474,4 +498,5 @@ export class DataRoutes extends BaseRouteHandler {
clearedCount
});
});
}