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
54 changes: 53 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,59 @@ This repository implements VectorLint — a prompt‑driven, structured‑output
- `presets/` — bundled rule packs (e.g., `VectorLint/`)
- `tests/` — Vitest specs for config, scanning, evaluation, providers

## Agent Behavior Guidelines

These guidelines reduce common LLM coding mistakes. They bias toward caution over speed; use judgment for trivial tasks.

### Think Before Coding

- State assumptions explicitly before implementing.
- If multiple interpretations exist, present them instead of picking silently.
- If a simpler approach exists, say so and push back when warranted.
- If something is unclear, stop, name the confusion, and ask.

### Simplicity First

- Write the minimum code that solves the problem.
- Do not add features beyond what was asked.
- Do not introduce abstractions for single-use code.
- Do not add flexibility or configurability that was not requested.
- Do not add error handling for impossible scenarios.
- If a change becomes larger than needed, simplify before finishing.

### Surgical Changes

- Touch only the files and lines required by the request.
- Do not improve adjacent code, comments, or formatting unless required.
- Match existing style, even when another style is tempting.
- If you notice unrelated dead code, mention it instead of deleting it.
- Remove imports, variables, functions, or files made unused by your own changes.
- Do not remove pre-existing dead code unless asked.

### Goal-Driven Execution

- Turn requests into verifiable goals before implementing.
- For bug fixes, write or identify a reproduction, then make it pass.
- For validation changes, cover invalid inputs, then make the checks pass.
- For refactors, verify behavior before and after the change.
- For multi-step tasks, state a brief plan with verification for each step.
- Loop until the success criteria are verified or a blocker is clear.

### Error Organization

- Put domain error classes in focused error files and import them into the modules that throw them.
- Reuse the repository's custom error hierarchy instead of throwing native `Error` directly.
- Catch errors as `unknown`; narrow or convert them with the existing error utilities.
- Introduce error handling only for real failure modes, not speculative or impossible scenarios.

### Comments

- Write comments only when they explain non-obvious current behavior, invariants, or constraints.
- Do not preserve planning discussions, rejected alternatives, absent fields, or speculative future architecture in production comments.
- Do not use comments to restate code that is already clear from names and types.
- Keep security and scope-boundary comments when they explain constraints that the implementation must preserve.
- Test supported behavior and current invariants; do not add negative tests solely to memorialize rejected designs.

## Configuration System

### Quick Start
Expand Down Expand Up @@ -132,7 +185,6 @@ For documents >600 words, VectorLint automatically chunks content:
- Separation of concerns: rules define rubric; schemas enforce structure; CLI orchestrates; evaluators process; reporters format
- Separation of concerns: when a file starts combining contracts, orchestration, and utility logic, extract shared helpers and types into focused modules
- Extensibility: add providers by implementing `LLMProvider` or `SearchProvider`; add evaluators via registry pattern
- Error handling: prefer the repository's custom error hierarchy over native `Error`; catch blocks use `unknown` type and extend existing custom error types before introducing raw exceptions
- Shared domain constants: avoid magic strings for core runtime concepts; define shared constants, enums, or types and import them where needed
- Naming: choose domain-accurate names that reflect the real abstraction level; avoid use-case-specific terminology in shared runtime code
- Logging: route runtime logging through an injected logger interface; keep concrete logger implementations behind the abstraction
Expand Down
35 changes: 35 additions & 0 deletions src/review/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# `src/review/` — Review-Domain Contract

The neutral, implementation-neutral contract every executor programs against.
A caller builds a `ReviewRequest`; an executor returns a `ReviewResult`.

## What lives here

- `types.ts` — all review-domain interfaces (`ReviewTarget`, `ReviewRule`,
`ReviewContext`, `ReviewBudget`, `ReviewFinding`, `ReviewScore`,
`ReviewDiagnostic`, `ReviewResult`, `ReviewRequest`, `ReviewModelCall`).
- `schemas.ts` — strict Zod schemas mirroring every external shape (boundary
validation).
- `budget.ts` — `DEFAULT_REVIEW_BUDGET`, `REVIEW_BUDGET_SCHEMA`,
and `enforceBudget()`.
- `errors.ts` — review-domain errors, including `BudgetExceededError`.
- `boundary.ts` — `buildScope()` / `isInScope()` enforcing the on-page boundary.
- `executor.ts` — `ReviewExecutor` interface, `REVIEW_MODEL_CALLS`
(`single | agent | auto`), `chooseModelCall()`.
- `request-builder.ts` — `buildReviewRequest()` bridging `PromptFile` to
`ReviewRequest`.

## On-page boundary

- Target content is always in scope.
- Caller-supplied context is in scope.
- Workspace reads are out of scope unless the caller includes them as context.
- Agent model calls page through target content only.
- Rule bodies are source-backed and caller-authored, never model-authored.

## Execution strategy

`modelCall: single | agent | auto` selects how the reviewer model is invoked,
not how rules are scored. `single` sends target + rule in one structured call;
`agent` gives the executor a target-scoped read-section capability; `auto`
picks `single` for normal-sized inputs and `agent` for large ones.
66 changes: 66 additions & 0 deletions src/review/boundary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { ReviewScope } from './types';

const FILE_URI_RE = /^file:\/\/(?<authority>[^/]*)(?<path>\/.*)?$/;

/**
* Lexically resolves '.' and '..' segments in a path string. Pure: performs no
* filesystem reads. The helper is kept local so src/review/ has no agent
* import and no filesystem access.
*
* The contract does not perform symlink canonicalization. Callers must
* canonicalize real files into target/context URIs before building a
* ReviewRequest.
*/
function resolveDotSegments(input: string): string {
const segments: string[] = [];
for (const segment of input.split('/')) {
if (segment === '' || segment === '.') continue;
if (segment === '..') {
segments.pop();
continue;
}
segments.push(segment);
}
return (input.startsWith('/') ? '/' : '') + segments.join('/');
}

/**
* Normalizes a review URI by resolving '.'/'..' segments in its path. file://
* URIs are normalized structurally; other (virtual, in-memory) URIs are
* returned as-is after dot resolution of their path-like suffix.
*/
export function normalizeReviewUri(uri: string): string {
const match = FILE_URI_RE.exec(uri);
if (match?.groups) {
const authority = match.groups['authority'] ?? '';
const rawPath = match.groups['path'] ?? '/';
return `file://${authority}${resolveDotSegments(rawPath)}`;
}
return uri;
}

/**
* Builds the on-page boundary scope from the target URI and
* any caller-supplied context URIs. Only these URIs are in scope; arbitrary
* workspace files are out of scope unless the caller explicitly includes them.
*/
export function buildScope(params: {
targetUri: string;
contextUris?: readonly string[];
}): ReviewScope {
const allowedUris = new Set<string>();
allowedUris.add(normalizeReviewUri(params.targetUri));
for (const uri of params.contextUris ?? []) {
allowedUris.add(normalizeReviewUri(uri));
}
return { allowedUris };
}

/**
* Returns true iff `uri` (after normalization) is the target or caller-supplied
* context. Path-traversal segments are resolved before comparison, so a
* traversal-style path cannot masquerade as an in-scope URI.
*/
export function isInScope(scope: ReviewScope, uri: string): boolean {
return scope.allowedUris.has(normalizeReviewUri(uri));
}
63 changes: 63 additions & 0 deletions src/review/budget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { z } from 'zod';
import type { ReviewBudget } from './types';
import { BudgetExceededError } from './errors';

/**
* Sensible default bounds for a single review. Frozen so a
* shared default object cannot be accidentally mutated by a caller.
*/
export const DEFAULT_REVIEW_BUDGET: Readonly<ReviewBudget> = Object.freeze({
maxTargetBytes: 1_000_000,
maxCallerContextBytes: 500_000,
maxChunksPerRule: 20,
maxModelCallsPerReview: 50,
maxWallClockMs: 5 * 60_000,
});

/** Zod schema mirroring {@link ReviewBudget}, applying defaults for omitted fields. */
export const REVIEW_BUDGET_SCHEMA = z
.object({
maxTargetBytes: z.number().int().positive().default(DEFAULT_REVIEW_BUDGET.maxTargetBytes),
maxCallerContextBytes: z
.number()
.int()
.positive()
.default(DEFAULT_REVIEW_BUDGET.maxCallerContextBytes),
maxChunksPerRule: z.number().int().positive().default(DEFAULT_REVIEW_BUDGET.maxChunksPerRule),
maxModelCallsPerReview: z
.number()
.int()
.positive()
.default(DEFAULT_REVIEW_BUDGET.maxModelCallsPerReview),
maxWallClockMs: z.number().int().positive().default(DEFAULT_REVIEW_BUDGET.maxWallClockMs),
})
.strict();

/** Current resource usage, checked against a {@link ReviewBudget}. */
export interface BudgetUsage {
modelCalls: number;
elapsedMs: number;
}

/**
* Throws {@link BudgetExceededError} if current usage violates a hard limit.
* Executors call this before/after each model call and on timeout checks.
* Only runtime counters (model calls, wall clock) are enforced here; size
* limits (target/context bytes, chunks) are enforced at request-build time.
*/
export function enforceBudget(budget: ReviewBudget, usage: BudgetUsage): void {
if (usage.modelCalls > budget.maxModelCallsPerReview) {
throw new BudgetExceededError(
`model calls (${usage.modelCalls}) exceed maxModelCallsPerReview (${budget.maxModelCallsPerReview})`,
'maxModelCallsPerReview',
usage.modelCalls,
);
}
if (usage.elapsedMs > budget.maxWallClockMs) {
throw new BudgetExceededError(
`elapsed time (${usage.elapsedMs}ms) exceeds maxWallClockMs (${budget.maxWallClockMs}ms)`,
'maxWallClockMs',
usage.elapsedMs,
);
}
}
13 changes: 13 additions & 0 deletions src/review/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { VectorlintError } from '../errors';
import type { ReviewBudget } from './types';

export class BudgetExceededError extends VectorlintError {
constructor(
message: string,
public readonly limit: keyof ReviewBudget,
public readonly actual: number,
) {
super(message, 'BUDGET_EXCEEDED');
this.name = 'BudgetExceededError';
}
}
51 changes: 51 additions & 0 deletions src/review/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { ReviewModelCall, ReviewRequest, ReviewResult } from './types';

/**
* Above this target byte size (roughly the existing chunking threshold in
* bytes), `auto` model-call selection prefers the agent executor so the
* reviewer can page through target content for context management.
*/
export const AGENT_MODEL_CALL_BYTE_THRESHOLD = 600_000;

/**
* The single source of truth for reviewer model-call strategies. Kept in sync
* with the {@link ReviewModelCall} union via `satisfies`.
*/
export const REVIEW_MODEL_CALLS = ['single', 'agent', 'auto'] as const satisfies readonly ReviewModelCall[];

/**
* The stable domain-level interface every executor implements. Single and
* agent executors are implementations behind this contract.
*/
export interface ReviewExecutor {
run(request: ReviewRequest): Promise<ReviewResult>;
}

/**
* Agent-call capability: a bounded way to page through target content for
* context management. NOT a workspace tool. An agent
* executor receives this capability scoped to the target only.
*/
export interface ReviewTargetReadCapability {
/** Read a 1-based [startLine, endLine] window of the target content. */
readTargetSection(
startLine: number,
endLine: number,
): Promise<{ startLine: number; endLine: number; content: string }>;
}

/**
* Resolves 'auto' to 'single' or 'agent'. Single for normal-sized inputs;
* agent for large inputs or multi-rule runs that benefit from paging.
*/
export function chooseModelCall(
modelCall: ReviewModelCall,
signal: { targetBytes: number; rules: number },
): 'single' | 'agent' {
if (modelCall === 'single') return 'single';
if (modelCall === 'agent') return 'agent';
if (signal.targetBytes > AGENT_MODEL_CALL_BYTE_THRESHOLD || signal.rules > 5) {
return 'agent';
}
return 'single';
}
16 changes: 16 additions & 0 deletions src/review/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Neutral review-domain contract for the VectorLint harness.
*
* Everything an executor needs to review target content against
* source-backed rules flows through this module. The CLI and external callers
* build a ReviewRequest and receive a ReviewResult.
*
* See README.md for the contract overview.
*/
export * from './types';
export * from './schemas';
export * from './errors';
export * from './budget';
export * from './boundary';
export * from './executor';
export * from './request-builder';
Loading
Loading