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
69 changes: 69 additions & 0 deletions packages/review-tutor/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ export const LIMITS = {
stderr: 32 * 1024,
} as const;

export const STRUCTURE_LIMITS = {
maxFiles: 200,
maxEdges: 2000,
maxEvidencePerEdge: 4,
evidenceText: 200,
lineLength: 4000,
diffLines: 200_000,
omittedRows: 200,
statementsPerFile: 2000,
externalNames: 8,
} as const;

export type QuizOutcome = "got_it" | "almost" | "review_again";

export type SourceRequest =
Expand Down Expand Up @@ -93,6 +105,63 @@ export interface LearningEntry {
updatedAt: string;
}

export interface StructureComparison {
kind: InputSnapshot["kind"];
label: string;
from: string;
to: string;
partial: boolean;
reasons: string[];
}

export interface StructureFile {
path: string;
status: "added" | "removed" | "modified" | "renamed";
renamedFrom?: string;
additions: number;
deletions: number;
analyzed: boolean;
reason?: string;
}

export interface StructureEvidence {
path: string;
line: number;
text: string;
}

export interface StructureEdge {
from: string;
to: string;
kind: "import" | "reexport" | "require" | "dynamic-import";
typeOnly: boolean;
status: "added" | "removed" | "modified" | "unchanged";
specifier: string;
evidence: StructureEvidence[];
}

export interface StructureOmission {
path?: string;
reason: string;
}

export interface StructureLimits {
maxFiles: typeof STRUCTURE_LIMITS.maxFiles;
maxEdges: typeof STRUCTURE_LIMITS.maxEdges;
maxEvidencePerEdge: typeof STRUCTURE_LIMITS.maxEvidencePerEdge;
truncated: boolean;
omitted: StructureOmission[];
}

export interface StructureSnapshot {
protocol: "rt/1";
inputId: string;
comparison: StructureComparison;
files: StructureFile[];
edges: StructureEdge[];
limits: StructureLimits;
}

export type SseEventType = "hello" | "state" | "question" | "answer_delta" | "source" | "log_update" | "error" | "bye";
export interface SseEvent { id: number; type: SseEventType; data: unknown }

Expand Down
14 changes: 13 additions & 1 deletion packages/review-tutor/src/server-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { exportLearningHtml } from "./export-html.ts";
import { loadInput, type ExecFile } from "./inputs.ts";
import { appendEntry, foldLog, persistInput, updateEntry } from "./log.ts";
import { buildTutorPrompt } from "./prompt.ts";
import { LIMITS, validateAskRequest, validateLogPatch, validateSourceRequest, type AskRequest, type InputSnapshot, type LearningEntry, type ModelChoice, type QuestionView } from "./protocol.ts";
import { LIMITS, validateAskRequest, validateLogPatch, validateSourceRequest, type AskRequest, type InputSnapshot, type LearningEntry, type ModelChoice, type QuestionView, type StructureSnapshot } from "./protocol.ts";
import type { StatePaths } from "./paths.ts";
import type { SseHub } from "./sse.ts";
import { structureSnapshotFor } from "./structure.ts";

export interface RunnerLike {
run(request: { provider: string; model: string; thinking: string; cwd: string; prompt: string }, delta: (text: string) => void): Promise<{ answer: string; usage?: Record<string, number> }>;
Expand Down Expand Up @@ -64,6 +65,7 @@ export class ReviewTutorSession {
private readonly queue: string[] = [];
private readonly threadHistory: LearningEntry[] = [];
private currentInput?: InputSnapshot;
private structureCache?: { inputId: string; snapshot: StructureSnapshot };
private running?: string;
private lastHeartbeat = 0;
private closing = false;
Expand Down Expand Up @@ -280,6 +282,16 @@ export class ReviewTutorSession {
}
}

structure(): SessionReply {
const input = this.currentInput;
if (!input) return { status: 409, value: {
error: "structure failed: expected a loaded input snapshot, received none; load a source and retry" } };
if (this.structureCache?.inputId !== input.id) {
this.structureCache = { inputId: input.id, snapshot: structureSnapshotFor(input) };
}
return { status: 200, value: this.structureCache.snapshot };
}

async log(url: URL): Promise<SessionReply> {
const limit = parseLogLimit(url.searchParams.get("limit"));
const entries = await foldLog(this.paths);
Expand Down
1 change: 1 addition & 0 deletions packages/review-tutor/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ async function primaryRoute(request: IncomingMessage, response: ServerResponse,
async function secondaryRoute(request: IncomingMessage, response: ServerResponse, url: URL, session: ReviewTutorSession): Promise<void> {
const method = request.method;
const path = url.pathname;
if (method === "GET" && path === "/api/structure") return send(response, session.structure());
if (method === "GET" && path === "/api/log") return send(response, await session.log(url));
const log = path.match(/^\/api\/log\/([^/]+)$/);
if (method === "PATCH" && log) return send(response, await session.patchLog(request, log[1]!));
Expand Down
Loading