pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
324 lines (316 loc) • 14.2 kB
JavaScript
import { createRequire as __pilensCreateRequire } from "node:module"; const require = __pilensCreateRequire(import.meta.url);
import {
getProjectDataDir
} from "./chunk-VN2AXLEX.js";
import {
createNdjsonLogger,
getGlobalPiLensLogDir,
getProcessSingleton,
getTurnId,
isTestMode
} from "./chunk-UK3CMEAL.js";
import {
BoundedFifoMap
} from "./chunk-ABBOA7UD.js";
import {
normalizeFilePath,
normalizeMapKey
} from "./chunk-Q5U6FMDI.js";
// dist/clients/hashline-anchor.js
var HASHLINE_ANCHOR_LENGTH = 3;
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var HASH_SPACE = ALPHABET.length ** HASHLINE_ANCHOR_LENGTH;
var HASH_PROBE_STRIDE = ALPHABET.length ** 2 + ALPHABET.length + 1;
var BITSET_WORDS = Math.ceil(HASH_SPACE / 32);
var MAX_ANCHOR_FILE_BYTES = 4 * 1024 * 1024;
var ANCHOR_INDEX_CACHE_LIMIT = 8;
var ANCHOR_INDEX_CACHE = new BoundedFifoMap(ANCHOR_INDEX_CACHE_LIMIT);
// dist/clients/mutation-attribution.js
import * as fs from "node:fs";
import * as path from "node:path";
var MUTATION_ATTRIBUTION_FILE_VERSION = 1;
var MUTATION_ATTRIBUTION_FILE = "observed-mutating-tools.json";
var MUTATION_ATTRIBUTION_MAX_TOOLS = 64;
var ATTRIBUTION_FAMILY = "mutation-attribution";
var ATTRIBUTION_VERSION = 2;
function state() {
return getProcessSingleton(ATTRIBUTION_FAMILY, ATTRIBUTION_VERSION, () => ({
session: new BoundedFifoMap(MUTATION_ATTRIBUTION_MAX_TOOLS),
fromDisk: void 0,
primedCwd: void 0
}));
}
function attributionFilePath(cwd) {
return path.join(getProjectDataDir(cwd), MUTATION_ATTRIBUTION_FILE);
}
function isPersistedEntry(entry) {
const candidate = entry;
return typeof candidate?.name === "string" && candidate.name.length > 0 && typeof candidate.observations === "number";
}
function readPersisted(cwd) {
try {
const raw = fs.readFileSync(attributionFilePath(cwd), "utf-8");
const parsed = JSON.parse(raw);
if (parsed?.version !== MUTATION_ATTRIBUTION_FILE_VERSION)
return void 0;
if (!Array.isArray(parsed.tools))
return void 0;
return {
version: MUTATION_ATTRIBUTION_FILE_VERSION,
tools: parsed.tools.filter(isPersistedEntry).slice(0, MUTATION_ATTRIBUTION_MAX_TOOLS).map((entry) => ({
name: entry.name,
observations: entry.observations,
lastSeen: typeof entry.lastSeen === "number" ? entry.lastSeen : 0
}))
};
} catch {
return void 0;
}
}
function primePersistedMutationAttribution(cwd) {
const current = state();
current.primedCwd = cwd;
current.fromDisk = /* @__PURE__ */ new Set();
if (!cwd)
return;
const file = readPersisted(cwd);
if (!file)
return;
for (const entry of file.tools)
current.fromDisk.add(entry.name);
}
function resetMutationAttribution() {
const current = state();
current.session.clear();
current.fromDisk = void 0;
current.primedCwd = void 0;
}
// dist/clients/read-guard-logger.js
import * as path3 from "node:path";
// dist/clients/finding-identity.js
import { createHash } from "node:crypto";
import * as path2 from "node:path";
var FINDING_ID_HASH_LENGTH = 12;
function normalizeMessage(message) {
return message.replace(/\s+/g, " ").trim().toLowerCase();
}
function hashText(value, length = FINDING_ID_HASH_LENGTH) {
return createHash("sha256").update(value).digest("hex").slice(0, length);
}
function relativeFile(filePath, cwd) {
const canonicalCwd = normalizeMapKey(cwd);
const canonicalFile = normalizeMapKey(filePath);
const rel = path2.relative(canonicalCwd, canonicalFile).replace(/\\/g, "/");
return rel && !rel.startsWith("..") ? rel : canonicalFile;
}
function stableFindingId(prefix, args) {
const joined = [
relativeFile(args.filePath, args.cwd),
...args.parts.map((part) => part === void 0 || part === null ? "" : String(part))
].join("|");
return `${prefix}${hashText(joined)}`;
}
// dist/clients/read-guard-logger.js
var READ_GUARD_LOG_DIR = getGlobalPiLensLogDir();
var READ_GUARD_LOG_FILE = path3.join(READ_GUARD_LOG_DIR, "read-guard.log");
var READ_GUARD_LOG_BACKUP_FILE = path3.join(READ_GUARD_LOG_DIR, "read-guard.log.1");
var MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_READ_GUARD_MAX_BYTES ?? "1048576", 10) || 1048576);
var writer = createNdjsonLogger({
filePath: READ_GUARD_LOG_FILE,
maxBytes: MAX_LOG_BYTES,
backupPath: READ_GUARD_LOG_BACKUP_FILE
});
var VERBOSE_READ_GUARD_LOG = process.env.PI_LENS_READ_GUARD_VERBOSE === "1" || process.env.PI_LENS_READ_GUARD_LOG === "verbose";
var LOG_ALLOWED_EDITS = process.env.PI_LENS_READ_GUARD_LOG_ALLOWS === "1";
var SNAPSHOT_LOG_SETTING = (process.env.PI_LENS_READ_GUARD_LOG_SNAPSHOTS ?? "1").toLowerCase();
var LOG_SNAPSHOT_VALIDATION = !["0", "false", "off"].includes(SNAPSHOT_LOG_SETTING);
var MAX_EDIT_BATCH_ITEMS = 100;
var generatedCorrelationCounter = 0;
var CORRELATION_ID_MAX = 64;
var CORRELATION_ID_HASH_LENGTH = 16;
function sanitizeCorrelationId(value) {
if (typeof value !== "string" && typeof value !== "number")
return void 0;
const trimmed = String(value).trim();
const sanitized = trimmed.replace(/[^a-zA-Z0-9._:-]/g, "_");
if (sanitized.length <= CORRELATION_ID_MAX) {
return sanitized.length > 0 ? sanitized : void 0;
}
const prefixLength = CORRELATION_ID_MAX - CORRELATION_ID_HASH_LENGTH - 1;
return `${sanitized.slice(0, prefixLength)}-${hashText(trimmed, CORRELATION_ID_HASH_LENGTH)}`;
}
function getReadGuardCorrelationId(event) {
const value = event ?? {};
const details = value.details ?? {};
for (const candidate of [
details.readGuardCorrelationId,
details.toolCallId,
value.toolCallId,
value.callId,
value.requestId,
value.id
]) {
const sanitized = sanitizeCorrelationId(candidate);
if (sanitized)
return sanitized;
}
generatedCorrelationCounter = (generatedCorrelationCounter + 1) % 1e6;
return `rg-${Date.now().toString(36)}-${generatedCorrelationCounter}`;
}
function validIndexes(indexes) {
return indexes.filter((index) => Number.isInteger(index) && index >= 0);
}
function boundedEditIndexes(indexes) {
return [...new Set(validIndexes(indexes))].slice(0, MAX_EDIT_BATCH_ITEMS);
}
function createReadGuardEditBatchSummary(args) {
const requestedSource = validIndexes(args.requestedIndexes);
const resolvedSource = validIndexes(args.resolvedIndexes ?? []);
const rejectedSource = (args.rejectedReasons ?? []).filter((entry) => Number.isInteger(entry.index) && entry.index >= 0);
const appliedSource = validIndexes(args.appliedIndexes ?? []);
const alreadyAppliedSource = validIndexes(args.alreadyAppliedIndexes ?? []);
const requestedIndexes = boundedEditIndexes(requestedSource);
const resolvedIndexes = boundedEditIndexes(resolvedSource);
const rejectedReasons = rejectedSource.slice(0, MAX_EDIT_BATCH_ITEMS);
const rejectedIndexes = boundedEditIndexes(rejectedSource.map((entry) => entry.index));
const appliedIndexes = boundedEditIndexes(appliedSource);
const alreadyAppliedIndexes = boundedEditIndexes(alreadyAppliedSource);
const participantSource = (args.participantIds ?? []).filter((id) => typeof id === "string" && id.length > 0);
const participantIds = [...new Set(participantSource)].slice(0, MAX_EDIT_BATCH_ITEMS);
const commitStatus = args.commitStatus ?? "not_attempted";
const postEditStatus = args.postEditStatus ?? "not_run";
const terminalStatus = args.terminalStatus ?? (postEditStatus === "failed" || commitStatus === "failed" ? "failed" : commitStatus === "not_attempted" && rejectedSource.length > 0 ? "blocked" : commitStatus === "no_changes" ? "skipped" : "success");
return {
requestedCount: requestedSource.length,
requestedTotal: args.requestedTotal ?? requestedSource.length,
requestedIndexes,
resolvedCount: resolvedSource.length,
resolvedTotal: args.resolvedTotal ?? resolvedSource.length,
resolvedIndexes,
rejectedCount: rejectedSource.length,
rejectedTotal: args.rejectedTotal ?? rejectedSource.length,
rejectedIndexes,
rejectedReasons,
appliedCount: appliedSource.length,
appliedTotal: args.appliedTotal ?? appliedSource.length,
appliedIndexes,
alreadyAppliedCount: alreadyAppliedSource.length,
alreadyAppliedTotal: args.alreadyAppliedTotal ?? alreadyAppliedSource.length,
alreadyAppliedIndexes,
participantIds,
participantTotal: args.participantTotal ?? participantSource.length,
participantIdsTruncated: (args.participantTotal ?? participantSource.length) > participantIds.length,
indexesTruncated: (args.requestedTotal ?? requestedSource.length) > requestedIndexes.length || (args.resolvedTotal ?? resolvedSource.length) > resolvedIndexes.length || (args.rejectedTotal ?? rejectedSource.length) > rejectedIndexes.length || (args.appliedTotal ?? appliedSource.length) > appliedIndexes.length || (args.alreadyAppliedTotal ?? alreadyAppliedSource.length) > alreadyAppliedIndexes.length,
commitStatus,
postEditStatus,
terminalStatus,
durationMs: Math.max(0, Math.min(args.durationMs ?? 0, 864e5))
};
}
function shouldLogEvent(event) {
if (VERBOSE_READ_GUARD_LOG)
return true;
if (event === "edit_allowed")
return LOG_ALLOWED_EDITS;
if (event === "range_snapshot_validation")
return LOG_SNAPSHOT_VALIDATION;
return event === "edit_blocked" || event === "edit_warned" || event === "exemption_added" || event === "oldtext_not_found" || event === "oldtext_duplicate" || event === "oldtext_indent_autopatched" || event === "oldtext_trailing_ws_autopatched" || event === "oldtext_escape_autopatched" || event === "edit_range_relocated" || event === "edit_preflight_blocked" || event === "edit_partial_apply" || event === "edit_partial_apply_skipped" || // #2402: pre-write batch rejections and recognized exact retries are the
// two new partial-apply outcomes; each is rare by construction.
event === "edit_partial_apply_rejected" || event === "edit_already_applied_retry" || event === "edit_post_edit_pipeline_failed" || event === "edit_batch_summary" || event === "edit_batch_summary_overflow" || event === "touched_lines_missing" || // #1913: read-guard record-cap evictions are rare by construction (only
// READ_GUARD_MAX_RECORDS_PER_FILE overflow triggers one), so this trim
// record bypasses the per-read verbosity gate — a live eviction
// regression must be visible without PI_LENS_READ_GUARD_VERBOSE=1.
event === "read_cap_trimmed" || // #1918: the record-cap trim's population siblings — whole-file
// eviction under real pressure (file cap or an external delete) and
// the per-file edits-cap trim. Idle-timeout eviction is deliberately
// EXCLUDED from this event (see read-guard.ts's evictFile doc
// comment) — it's routine housekeeping, not a fault, and its
// per-file cardinality is unbounded in a healthy session — except
// when it drops a write record (#3520), once per file per session.
// Same rarity argument as #1913 for the reasons that DO fire: each is
// rising-edge gated once per file per session, so always-on
// visibility costs nothing.
event === "read_file_evicted" || event === "edits_cap_trimmed";
}
var MAX_TELEMETRY_STRING = 512;
var MAX_TELEMETRY_KEYS = 64;
function boundTelemetryValue(value, pathName, depth) {
if (typeof value === "string") {
return value.length > MAX_TELEMETRY_STRING ? {
value: `${value.slice(0, MAX_TELEMETRY_STRING)}\u2026`,
truncated: true,
arrayTotals: {}
} : { value, truncated: false, arrayTotals: {} };
}
if (Array.isArray(value)) {
const sampled = value.slice(0, MAX_EDIT_BATCH_ITEMS);
const bounded = sampled.map((item, index) => boundTelemetryValue(item, `${pathName}[${index}]`, depth + 1));
const arrayTotals2 = value.length > MAX_EDIT_BATCH_ITEMS ? { [pathName || "array"]: value.length } : {};
let truncated2 = value.length > MAX_EDIT_BATCH_ITEMS;
for (const item of bounded) {
truncated2 ||= item.truncated;
Object.assign(arrayTotals2, item.arrayTotals);
}
return {
value: bounded.map((item) => item.value),
truncated: truncated2,
arrayTotals: arrayTotals2
};
}
if (!value || typeof value !== "object" || depth >= 4) {
return { value, truncated: false, arrayTotals: {} };
}
const output = {};
let truncated = false;
const arrayTotals = {};
for (const [key, child] of Object.entries(value).slice(0, MAX_TELEMETRY_KEYS)) {
const bounded = boundTelemetryValue(child, pathName ? `${pathName}.${key}` : key, depth + 1);
output[key] = bounded.value;
truncated ||= bounded.truncated;
Object.assign(arrayTotals, bounded.arrayTotals);
}
if (Object.keys(value).length > MAX_TELEMETRY_KEYS)
truncated = true;
return { value: output, truncated, arrayTotals };
}
function logReadGuardEvent(entry) {
if (isTestMode() || !shouldLogEvent(entry.event)) {
return;
}
const rawMetadata = entry.correlationId ? { ...entry.metadata, correlationId: entry.correlationId } : entry.metadata;
const bounded = boundTelemetryValue(rawMetadata, "metadata", 0);
const metadata = bounded.truncated && bounded.value && typeof bounded.value === "object" ? {
...bounded.value,
telemetryTruncated: true,
telemetryArrayTotals: bounded.arrayTotals
} : bounded.value && typeof bounded.value === "object" ? bounded.value : void 0;
const { correlationId: _correlationId, ...logEntry } = entry;
writer.log({
ts: (/* @__PURE__ */ new Date()).toISOString(),
...logEntry,
turnId: getTurnId(),
filePath: normalizeFilePath(logEntry.filePath),
metadata
});
}
// dist/clients/mutating-tool.js
var BUILTIN_MUTATING_TOOLS = /* @__PURE__ */ new Map([
["write", "write"],
["edit", "edit"]
]);
function isMutatingToolName(toolName) {
return BUILTIN_MUTATING_TOOLS.has(toolName);
}
var RESOLVED_RANGE_CARRY_LIMIT = 64;
var RESOLVED_RANGE_CARRY = new BoundedFifoMap(RESOLVED_RANGE_CARRY_LIMIT);
export {
normalizeMessage,
hashText,
stableFindingId,
getReadGuardCorrelationId,
createReadGuardEditBatchSummary,
logReadGuardEvent,
primePersistedMutationAttribution,
resetMutationAttribution,
isMutatingToolName
};