pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
1,386 lines (1,318 loc) • 41 kB
JavaScript
/**
* Analyze pi-lens' own logs for operational/code-quality smells across all projects.
*
* Sources:
* ~/.pi-lens/latency*.log JSONL dispatch/runner/phase timings
* ~/.pi-lens/sessionstart*.log text lifecycle/tool availability logs
* ~/.pi-lens/cascade*.log JSONL impact-cascade logs
* ~/.pi-lens/read-guard*.log JSONL read-guard friction logs
* ~/.pi-lens/tree-sitter*.log JSONL structural runner logs
* ~/.pi-lens/actionable-warnings*.log JSONL advisory pipeline (inject/suppress)
* ~/.pi-lens/ast-grep-tools*.log JSONL MCP ast-grep search/replace telemetry
* ~/.pi-lens/logs/*.jsonl JSONL diagnostic findings
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import readline from "node:readline";
import { fileURLToPath } from "node:url";
const DEFAULT_ROOT = path.join(os.homedir(), ".pi-lens");
const DEFAULT_SINCE = "2d";
const DEFAULT_LIMIT = 12;
const args = parseArgs(process.argv.slice(2));
const root = path.resolve(expandHome(args.root ?? DEFAULT_ROOT));
const since = parseSince(args.since ?? DEFAULT_SINCE);
const limit =
Number.parseInt(args.limit ?? `${DEFAULT_LIMIT}`, 10) || DEFAULT_LIMIT;
const outputJson = Boolean(args.json);
const includeArchived = Boolean(args.archived);
const thresholds = {
startupSlowMs: Number.parseInt(args.startupSlowMs ?? "500", 10),
backgroundSlowMs: Number.parseInt(args.backgroundSlowMs ?? "3000", 10),
totalSlowMs: Number.parseInt(args.totalSlowMs ?? "5000", 10),
runnerSlowMs: Number.parseInt(args.runnerSlowMs ?? "2500", 10),
cascadeGraphSlowMs: Number.parseInt(args.cascadeGraphSlowMs ?? "1000", 10),
};
main().catch((err) => {
console.error(`log-smell analysis failed: ${err?.stack || err}`);
process.exitCode = 1;
});
async function main() {
if (args.help) {
printHelp();
return;
}
const files = discoverLogFiles(root, includeArchived);
const state = createState(files);
await Promise.all([
analyzeLatency(files.latency, state),
analyzeDiagnosticLogs(files.diagnostics, state),
analyzeCascade(files.cascade, state),
analyzeReadGuard(files.readGuard, state),
analyzeTreeSitter(files.treeSitter, state),
analyzeSessionStart(files.sessionStart, state),
analyzeActionableWarnings(files.actionableWarnings, state),
analyzeAstGrepTools(files.astGrepTools, state),
]);
const report = buildReport(state);
if (outputJson) {
console.log(JSON.stringify(report, null, 2));
} else {
printReport(report);
}
}
function parseArgs(argv) {
const out = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--help" || arg === "-h") out.help = true;
else if (arg === "--json") out.json = true;
else if (arg === "--archived") out.archived = true;
else if (arg.startsWith("--")) {
const key = arg.slice(2);
const next = argv[i + 1];
if (!next || next.startsWith("--")) out[key] = "true";
else out[key] = argv[++i];
}
}
return out;
}
function printHelp() {
console.log(
`Usage: node scripts/analyze-pi-lens-logs.mjs [options]\n\nOptions:\n --since <2d|24h|YYYY-MM-DD|all> Time window (default: ${DEFAULT_SINCE})\n --root <dir> pi-lens log root (default: ~/.pi-lens)\n --limit <n> Top-N rows per section (default: ${DEFAULT_LIMIT})\n --archived Include archived rotated logs too\n --json Emit machine-readable JSON\n\nThresholds:\n --startupSlowMs <n> session_start total threshold (default: ${thresholds.startupSlowMs})\n --backgroundSlowMs <n> session_start background task threshold (default: ${thresholds.backgroundSlowMs})\n --totalSlowMs <n> tool total phase threshold (default: ${thresholds.totalSlowMs})\n --runnerSlowMs <n> runner duration threshold (default: ${thresholds.runnerSlowMs})\n --cascadeGraphSlowMs <n> cascade graph build threshold (default: ${thresholds.cascadeGraphSlowMs})\n`,
);
}
function parseSince(value) {
if (!value || value === "all") return null;
const now = Date.now();
const rel = /^(\d+)([hdw])$/.exec(value);
if (rel) {
const amount = Number.parseInt(rel[1], 10);
const unitMs =
rel[2] === "h" ? 3600_000 : rel[2] === "d" ? 86_400_000 : 7 * 86_400_000;
return new Date(now - amount * unitMs);
}
const date = new Date(value);
if (!Number.isNaN(date.getTime())) return date;
throw new Error(`Invalid --since value: ${value}`);
}
function expandHome(input) {
return input.startsWith("~")
? path.join(os.homedir(), input.slice(1))
: input;
}
function discoverLogFiles(logRoot, archived) {
const allRootFiles = safeReaddir(logRoot).map((name) =>
path.join(logRoot, name),
);
const logsDir = path.join(logRoot, "logs");
const dailyLogs = safeReaddir(logsDir)
.filter((name) => name.endsWith(".jsonl"))
.map((name) => path.join(logsDir, name));
const byPrefix = (prefix) =>
allRootFiles.filter((file) => {
const base = path.basename(file);
if (!base.startsWith(prefix)) return false;
if (!archived && /\.\d{4}-\d{2}-\d{2}T/.test(base)) return false;
return base.endsWith(".log") || base.includes(".log.");
});
return {
latency: byPrefix("latency"),
sessionStart: byPrefix("sessionstart"),
cascade: byPrefix("cascade"),
readGuard: byPrefix("read-guard"),
treeSitter: byPrefix("tree-sitter"),
actionableWarnings: byPrefix("actionable-warnings"),
astGrepTools: byPrefix("ast-grep-tools"),
diagnostics: dailyLogs,
};
}
function safeReaddir(dir) {
try {
return fs.readdirSync(dir);
} catch {
return [];
}
}
function createState(files) {
return {
window: { since: since?.toISOString() ?? "all", root },
files,
parseErrors: counter(),
seen: counter(),
projects: counter(),
smellTotals: counter(),
latency: {
runnerStatus: counter(),
runnerFailureKinds: counter(),
runnerBlockingFindings: counter(),
runnerFailures: [],
slowRunners: [],
slowTotals: [],
toolResults: counter(),
phaseCounts: counter(),
phaseTimeouts: counter(),
workspace: {
started: 0,
completed: 0,
aborted: 0,
timedOutSweeps: 0,
timedOutFilesTotal: 0,
sweeps: [],
progress: [],
},
},
diagnostics: {
bySeverity: counter(),
byTool: counter(),
byRule: counter(),
byFile: counter(),
shownInline: counter(),
errors: [],
topMessages: counter(),
},
cascade: {
phases: counter(),
slowGraphs: [],
fallbacks: [],
missingSnapshots: counter(),
noNeighbors: counter(),
largeFanout: [],
},
readGuard: {
events: counter(),
byFile: counter(),
byReason: counter(),
preflightReasons: counter(),
snapshotStatus: counter(),
snapshotEnforcement: counter(),
blocked: [],
oldTextIssues: [],
staleRanges: [],
zeroReads: [],
unavailableSnapshots: [],
},
treeSitter: {
phases: counter(),
failures: [],
blocking: [],
highDiagnostics: [],
queryCache: counter(),
riskFlags: counter(),
},
session: {
starts: 0,
cwds: counter(),
slowStarts: [],
slowTasks: [],
toolNoise: counter(),
lspNoise: counter(),
errors: [],
rotations: counter(),
},
actionable: {
events: counter(),
reports: 0,
injected: 0,
injectedAdvisories: 0,
suppressed: 0,
autoFixEligible: 0,
lspSource: counter(),
fileSkipReasons: counter(),
errors: [],
},
astGrep: {
outcomes: counter(),
errorKinds: counter(),
truncated: 0,
calls: 0,
errors: [],
slow: [],
},
};
}
async function analyzeLatency(files, state) {
for (const file of files) {
await forEachJsonLine(file, "latency", state, (entry) => {
const ts = dateOf(entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("latency");
trackProject(state, entry.filePath);
if (entry.type === "runner") {
const status = entry.status ?? "unknown";
const runner = entry.runnerId ?? "unknown";
state.latency.runnerStatus.inc(`${runner}:${status}`);
if (status === "failed" || status === "crashed") {
// Separate a genuine runner breakage from "the check ran and found
// blocking issues" (e.g. the LSP runner reports status:failed when a
// file has type errors). Prefer the logged failureKind; fall back to
// the heuristic that a failure carrying diagnostics is found-errors.
const kind =
entry.metadata?.failureKind ??
(status === "crashed"
? "crashed"
: (entry.diagnosticCount ?? 0) > 0
? "blocking_diagnostics"
: "unknown");
state.latency.runnerFailureKinds.inc(`${runner}:${kind}`);
if (kind === "blocking_diagnostics") {
state.latency.runnerBlockingFindings.inc(runner);
} else {
state.smellTotals.inc("runner-failures");
pushTop(
state.latency.runnerFailures,
summarizeLatency(entry),
limit * 3,
byDuration,
);
}
}
if (
(entry.durationMs ?? 0) >= thresholds.runnerSlowMs &&
status !== "skipped"
) {
state.smellTotals.inc("slow-runners");
pushTop(
state.latency.slowRunners,
summarizeLatency(entry),
limit * 3,
byDuration,
);
}
} else if (entry.type === "phase") {
const phase = entry.phase ?? "unknown";
state.latency.phaseCounts.inc(phase);
if (/_timeout$/.test(phase)) state.latency.phaseTimeouts.inc(phase);
if (
phase === "total" &&
(entry.durationMs ?? 0) >= thresholds.totalSlowMs
) {
state.smellTotals.inc("slow-hook-path");
pushTop(
state.latency.slowTotals,
summarizeLatency(entry),
limit * 3,
byDuration,
);
} else if (phase === "lsp_workspace_diagnostics_start") {
state.latency.workspace.started += 1;
} else if (phase === "lsp_workspace_diagnostics_progress") {
// Keep the heartbeats so an incomplete sweep can show how far it
// got (completed X/Y) before it went silent — the hang forensics.
pushTop(
state.latency.workspace.progress,
summarizeWorkspaceSweep(entry),
limit * 3,
byDuration,
);
} else if (phase === "lsp_workspace_diagnostics") {
const ws = state.latency.workspace;
ws.completed += 1;
if (entry.metadata?.aborted) ws.aborted += 1;
const timedOut = Number(entry.metadata?.timedOutFiles ?? 0);
if (timedOut > 0) {
ws.timedOutSweeps += 1;
ws.timedOutFilesTotal += timedOut;
state.smellTotals.inc("lsp-workspace-file-timeouts");
pushTop(
ws.sweeps,
summarizeWorkspaceSweep(entry),
limit * 3,
byDuration,
);
}
}
} else if (entry.type === "tool_result") {
state.latency.toolResults.inc(entry.result ?? "unknown");
}
});
}
}
async function analyzeDiagnosticLogs(files, state) {
for (const file of files) {
await forEachJsonLine(file, "diagnostics", state, (entry) => {
const ts = dateOf(entry.timestamp ?? entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("diagnostics");
trackProject(state, entry.filePath);
const severity = entry.severity ?? "unknown";
const tool = entry.tool ?? "unknown";
const rule = entry.ruleId ?? entry.rule ?? "unknown";
const filePath = shortPath(entry.filePath);
state.diagnostics.bySeverity.inc(severity);
state.diagnostics.byTool.inc(tool);
state.diagnostics.byRule.inc(`${tool}/${rule}`);
state.diagnostics.byFile.inc(filePath);
state.diagnostics.shownInline.inc(
`${Boolean(entry.shownInline)}:${Boolean(entry.shownToAgent)}:${Boolean(entry.unresolved)}`,
);
state.diagnostics.topMessages.inc(
`${tool}/${rule}: ${normalizeMessage(entry.message)}`,
);
if (
severity === "error" ||
entry.shownToAgent === true ||
entry.shownInline === true
) {
state.smellTotals.inc("diagnostic-blockers");
pushTop(
state.diagnostics.errors,
summarizeDiagnostic(entry),
limit * 4,
bySeverityThenLine,
);
}
});
}
}
async function analyzeCascade(files, state) {
for (const file of files) {
await forEachJsonLine(file, "cascade", state, (entry) => {
const ts = dateOf(entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("cascade");
trackProject(state, entry.filePath);
const phase = entry.phase ?? "unknown";
state.cascade.phases.inc(phase);
if ((entry.graphBuiltMs ?? 0) >= thresholds.cascadeGraphSlowMs) {
state.smellTotals.inc("cascade-slow-graphs");
pushTop(
state.cascade.slowGraphs,
summarizeCascade(entry),
limit * 3,
(a, b) => b.graphBuiltMs - a.graphBuiltMs,
);
}
if (entry.fallbackUsed || phase === "neighbor_fallback" || entry.error) {
state.smellTotals.inc("cascade-fallbacks");
pushTop(
state.cascade.fallbacks,
summarizeCascade(entry),
limit * 3,
byDuration,
);
}
if (entry.snapshotMissing)
state.cascade.missingSnapshots.inc(
shortPath(entry.neighborFile ?? entry.filePath),
);
if (entry.metadata?.noNeighbors)
state.cascade.noNeighbors.inc(projectOf(entry.filePath));
if (
(entry.totalNeighborCount ?? 0) >= 20 ||
(entry.neighborCount ?? 0) >= 10
) {
pushTop(
state.cascade.largeFanout,
summarizeCascade(entry),
limit * 3,
(a, b) => (b.totalNeighborCount ?? 0) - (a.totalNeighborCount ?? 0),
);
}
});
}
}
async function analyzeReadGuard(files, state) {
for (const file of files) {
await forEachJsonLine(file, "read-guard", state, (entry) => {
const ts = dateOf(entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("read-guard");
trackProject(state, entry.filePath);
const event = entry.event ?? "unknown";
state.readGuard.events.inc(event);
state.readGuard.byFile.inc(shortPath(entry.filePath));
const reasonKind = entry.metadata?.reasonKind;
if (reasonKind) state.readGuard.byReason.inc(reasonKind);
if (event === "edit_preflight_blocked") {
state.readGuard.preflightReasons.inc(reasonKind ?? "unknown");
}
if (event === "range_snapshot_validation") {
const status = entry.metadata?.status ?? "unknown";
state.readGuard.snapshotStatus.inc(status);
state.readGuard.snapshotEnforcement.inc(
entry.metadata?.enforced ? "enforced" : "not_enforced",
);
if (status === "mismatch") {
state.smellTotals.inc("read-guard-stale-ranges");
pushTop(
state.readGuard.staleRanges,
summarizeReadGuard(entry),
limit * 3,
byLine,
);
} else if (status === "unavailable") {
pushTop(
state.readGuard.unavailableSnapshots,
summarizeReadGuard(entry),
limit * 3,
byLine,
);
}
}
if (event === "edit_blocked" || event === "edit_warned") {
state.smellTotals.inc("read-guard-friction");
const summary = summarizeReadGuard(entry);
pushTop(state.readGuard.blocked, summary, limit * 3, byLine);
if (reasonKind === "zero_read") {
pushTop(state.readGuard.zeroReads, summary, limit * 3, byLine);
}
}
if (
event === "oldtext_not_found" ||
event === "oldtext_duplicate" ||
event === "touched_lines_missing" ||
event === "edit_preflight_blocked"
) {
state.smellTotals.inc("read-guard-friction");
pushTop(
state.readGuard.oldTextIssues,
summarizeReadGuard(entry),
limit * 3,
byLine,
);
}
});
}
}
async function analyzeTreeSitter(files, state) {
for (const file of files) {
await forEachJsonLine(file, "tree-sitter", state, (entry) => {
const ts = dateOf(entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("tree-sitter");
trackProject(state, entry.filePath);
const phase = entry.phase ?? "unknown";
state.treeSitter.phases.inc(phase);
if (phase === "queries_loaded") {
state.treeSitter.queryCache.inc(entry.cacheHit ? "hit" : "miss");
}
if (
entry.status &&
entry.status !== "succeeded" &&
entry.status !== "skipped"
) {
state.smellTotals.inc("tree-sitter-failures");
pushTop(
state.treeSitter.failures,
summarizeTreeSitter(entry),
limit * 3,
byDiagnostics,
);
}
if ((entry.blocking ?? 0) > 0) {
state.smellTotals.inc("tree-sitter-blocking");
pushTop(
state.treeSitter.blocking,
summarizeTreeSitter(entry),
limit * 3,
byDiagnostics,
);
}
if ((entry.diagnostics ?? 0) >= 20) {
pushTop(
state.treeSitter.highDiagnostics,
summarizeTreeSitter(entry),
limit * 3,
byDiagnostics,
);
}
for (const flag of entry.metadata?.riskFlags ?? [])
state.treeSitter.riskFlags.inc(flag);
});
}
}
async function analyzeSessionStart(files, state) {
const lineRe = /^\[([^\]]+)\]\s*(.*)$/;
for (const file of files) {
await forEachLine(file, async (line) => {
const match = lineRe.exec(line);
if (!match) return;
const ts = dateOf(match[1]);
if (!inWindow(ts)) return;
const message = match[2];
state.seen.inc("sessionstart");
const cwd = /session_start cwd:\s*(.*)$/.exec(message)?.[1];
if (cwd) {
state.session.starts++;
state.session.cwds.inc(projectOf(cwd));
trackProject(state, cwd);
}
const total = /session_start total:\s*(\d+)ms/.exec(message);
if (total && Number(total[1]) >= thresholds.startupSlowMs) {
state.smellTotals.inc("slow-session-start");
pushTop(
state.session.slowStarts,
{ ts: iso(ts), durationMs: Number(total[1]), message },
limit * 3,
byDuration,
);
}
// Runtime logs "success runMs=<n> queuedMs=<n>"; older logs used
// "success (<n>ms)". Match both so a format drift can't silently zero
// this smell again (it did: the `(<n>ms)` regex matched 0 of ~2k rows).
const task =
/session_start task ([^:]+): success runMs=(\d+)/.exec(message) ??
/session_start task ([^:]+): success \((\d+)ms\)/.exec(message);
if (task && Number(task[2]) >= thresholds.backgroundSlowMs) {
state.smellTotals.inc("slow-background-tasks");
pushTop(
state.session.slowTasks,
{ ts: iso(ts), task: task[1], durationMs: Number(task[2]), message },
limit * 3,
byDuration,
);
}
if (message.includes("log_cleanup: rotated"))
state.session.rotations.inc(message.replace(/^log_cleanup:\s*/, ""));
const lower = message.toLowerCase();
if (
/(auto-install|preinstall|installation).*(failed|unavailable|exception)/i.test(
message,
)
) {
state.smellTotals.inc("tool-install-noise");
state.session.toolNoise.inc(normalizeSessionNoise(message));
}
if (
/lsp .*?(unavailable|failed|timeout|skipped_broken|exited immediately|binary not found)/i.test(
message,
)
) {
state.smellTotals.inc("lsp-availability-noise");
state.session.lspNoise.inc(normalizeSessionNoise(message));
}
if (
lower.includes("error") ||
lower.includes("exception") ||
lower.includes("timeout")
) {
pushTop(
state.session.errors,
{ ts: iso(ts), message },
limit * 4,
(a, b) => String(b.ts).localeCompare(String(a.ts)),
);
}
});
}
}
async function analyzeActionableWarnings(files, state) {
for (const file of files) {
await forEachJsonLine(file, "actionable-warnings", state, (entry) => {
const ts = dateOf(entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("actionable-warnings");
const event = entry.event ?? "unknown";
state.actionable.events.inc(event);
const meta = entry.metadata ?? {};
if (event === "report_complete") {
state.actionable.reports++;
const summary = meta.summary ?? {};
state.actionable.suppressed += Number(summary.suppressed ?? 0);
state.actionable.autoFixEligible += Number(summary.autoFixEligible ?? 0);
}
if (event === "advisory_injected") {
state.actionable.injected++;
state.actionable.injectedAdvisories += Number(meta.unsuppressed ?? 0);
}
if (event === "lsp_file_checked" && meta.lspSource) {
state.actionable.lspSource.inc(meta.lspSource);
}
if (event === "lsp_file_skipped") {
state.actionable.fileSkipReasons.inc(meta.reason ?? "unknown");
}
if (/error|exception|failed/i.test(event) || meta.error) {
state.smellTotals.inc("actionable-warning-errors");
pushTop(
state.actionable.errors,
{
ts: iso(ts),
event,
message: String(meta.error ?? meta.message ?? "").slice(0, 200),
},
limit * 3,
(a, b) => String(b.ts).localeCompare(String(a.ts)),
);
}
});
}
}
async function analyzeAstGrepTools(files, state) {
for (const file of files) {
await forEachJsonLine(file, "ast-grep-tools", state, (entry) => {
const ts = dateOf(entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("ast-grep-tools");
state.astGrep.calls++;
const tool = entry.tool ?? "unknown";
const outcome = entry.outcome ?? "unknown";
state.astGrep.outcomes.inc(`${tool}:${outcome}`);
if (entry.truncated) state.astGrep.truncated++;
if (outcome === "error") {
state.smellTotals.inc("ast-grep-tool-errors");
state.astGrep.errorKinds.inc(entry.errorKind ?? "unknown");
pushTop(
state.astGrep.errors,
{
ts: entry.ts,
tool,
errorKind: entry.errorKind,
durationMs: entry.durationMs,
message: String(entry.errorRaw ?? "").replace(/\s+/g, " ").slice(0, 180),
pattern: String(entry.pattern ?? "").replace(/\s+/g, " ").slice(0, 80),
},
limit * 3,
(a, b) => String(b.ts).localeCompare(String(a.ts)),
);
}
if ((entry.durationMs ?? 0) >= 1000) {
pushTop(
state.astGrep.slow,
{
ts: entry.ts,
tool,
durationMs: entry.durationMs,
matchCount: entry.matchCount,
pattern: String(entry.pattern ?? "").replace(/\s+/g, " ").slice(0, 80),
},
limit * 3,
byDuration,
);
}
});
}
}
async function forEachJsonLine(file, bucket, state, visitor) {
await forEachLine(file, async (line) => {
if (!line.trim()) return;
try {
visitor(JSON.parse(line));
} catch {
state.parseErrors.inc(`${bucket}:${path.basename(file)}`);
}
});
}
async function forEachLine(file, visitor) {
if (!fs.existsSync(file)) return;
const rl = readline.createInterface({
input: fs.createReadStream(file, { encoding: "utf8" }),
crlfDelay: Infinity,
});
for await (const line of rl) await visitor(line);
}
function dateOf(value) {
if (!value) return null;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
function inWindow(date) {
if (!since) return true;
return date && date >= since;
}
function iso(date) {
return date?.toISOString?.() ?? "unknown";
}
function counter() {
const map = new Map();
return {
inc(key, by = 1) {
map.set(
String(key ?? "unknown"),
(map.get(String(key ?? "unknown")) ?? 0) + by,
);
},
entries() {
return [...map.entries()];
},
top(n = limit) {
return [...map.entries()]
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
.slice(0, n)
.map(([key, count]) => ({ key, count }));
},
toJSON() {
return Object.fromEntries(
[...map.entries()].sort(
(a, b) => b[1] - a[1] || a[0].localeCompare(b[0]),
),
);
},
get(key) {
return map.get(String(key ?? "unknown")) ?? 0;
},
get size() {
return map.size;
},
};
}
function pushTop(array, item, max, compare) {
array.push(item);
array.sort(compare);
if (array.length > max) array.length = max;
}
function byDuration(a, b) {
return (b.durationMs ?? 0) - (a.durationMs ?? 0);
}
function byDiagnostics(a, b) {
return (b.diagnostics ?? 0) - (a.diagnostics ?? 0);
}
function byLine(a, b) {
return (
String(b.ts ?? "").localeCompare(String(a.ts ?? "")) ||
(a.line ?? 0) - (b.line ?? 0)
);
}
function bySeverityThenLine(a, b) {
const rank = { error: 3, warning: 2, info: 1, hint: 0 };
return (
(rank[b.severity] ?? 0) - (rank[a.severity] ?? 0) ||
String(b.ts ?? "").localeCompare(String(a.ts ?? ""))
);
}
function summarizeLatency(entry) {
return {
ts: entry.ts,
durationMs: entry.durationMs,
type: entry.type,
phase: entry.phase,
toolName: entry.toolName,
runnerId: entry.runnerId,
status: entry.status,
result: entry.result,
diagnosticCount: entry.diagnosticCount,
filePath: shortPath(entry.filePath),
project: projectOf(entry.filePath),
metadata: pick(entry.metadata, [
"failureKind",
"failureMessage",
"skipReason",
"completed",
"finalContent",
"runners",
"totalDiagnostics",
"blockers",
]),
};
}
function summarizeWorkspaceSweep(entry) {
const md = entry.metadata ?? {};
const bits = [];
if (md.completed != null)
bits.push(`completed ${md.completed}/${md.total ?? md.fileCount ?? "?"}`);
else if (md.filesChecked != null) bits.push(`checked ${md.filesChecked}`);
if (md.timedOutFiles) bits.push(`timedOutFiles=${md.timedOutFiles}`);
if (md.aborted) bits.push("aborted");
if (md.perFileMs) bits.push(`perFileMs=${md.perFileMs}`);
return {
ts: entry.ts,
durationMs: entry.durationMs,
project: projectOf(entry.filePath),
filePath: shortPath(entry.filePath),
message: bits.join(", "),
};
}
function summarizeDiagnostic(entry) {
return {
ts: entry.timestamp ?? entry.ts,
severity: entry.severity,
tool: entry.tool,
ruleId: entry.ruleId ?? entry.rule,
filePath: shortPath(entry.filePath),
project: projectOf(entry.filePath),
line: entry.line,
column: entry.column,
shownInline: Boolean(entry.shownInline),
shownToAgent: Boolean(entry.shownToAgent),
unresolved: Boolean(entry.unresolved),
message: entry.message,
};
}
function summarizeCascade(entry) {
return {
ts: entry.ts,
phase: entry.phase,
filePath: shortPath(entry.filePath),
neighborFile: shortPath(entry.neighborFile),
project: projectOf(entry.filePath),
graphBuiltMs: entry.graphBuiltMs,
durationMs: entry.durationMs,
neighborCount: entry.neighborCount,
totalNeighborCount: entry.totalNeighborCount,
diagnosticCount: entry.diagnosticCount,
fallbackUsed: Boolean(entry.fallbackUsed),
snapshotMissing: Boolean(entry.snapshotMissing),
error: entry.error,
};
}
function summarizeReadGuard(entry) {
return {
ts: entry.ts,
event: entry.event,
filePath: shortPath(entry.filePath),
project: projectOf(entry.filePath),
requestedOffset: entry.requestedOffset,
requestedLimit: entry.requestedLimit,
effectiveOffset: entry.effectiveOffset,
effectiveLimit: entry.effectiveLimit,
symbol: entry.symbol,
symbolKind: entry.symbolKind,
line: entry.symbolStartLine,
metadata: entry.metadata,
};
}
function summarizeTreeSitter(entry) {
return {
ts: entry.ts,
phase: entry.phase,
status: entry.status,
filePath: shortPath(entry.filePath),
project: projectOf(entry.filePath),
languageId: entry.languageId,
diagnostics: entry.diagnostics,
blocking: entry.blocking,
queryCount: entry.queryCount,
effectiveQueryCount: entry.effectiveQueryCount,
metadata: pick(entry.metadata, [
"riskFlags",
"changedSymbols",
"neighborFiles",
"error",
]),
};
}
function pick(obj, keys) {
if (!obj || typeof obj !== "object") return undefined;
const out = {};
for (const key of keys) if (key in obj) out[key] = obj[key];
return Object.keys(out).length ? out : undefined;
}
function normalizeMessage(message) {
return String(message ?? "")
.replace(/\d+/g, "<n>")
.replace(/"[^"]+"/g, '"…"')
.replace(/'[^']+'/g, "'…'")
.slice(0, 180);
}
function normalizeSessionNoise(message) {
return message
.replace(/\([0-9]+ms\)/g, "(<ms>)")
.replace(/retryInMs":\d+/g, "retryInMs:<n>")
.replace(/C:[^\s]+/g, "<path>")
.replace(/\b\d+ms\b/g, "<ms>")
.slice(0, 220);
}
function trackProject(state, filePath) {
if (!filePath) return;
state.projects.inc(projectOf(filePath));
}
function projectOf(filePath) {
const p = normalizePath(filePath);
let match = /\/Desktop\/([^/]+)/i.exec(p);
if (match) return match[1];
match = /\/AppData\/Local\/Temp\/([^/]+)/i.exec(p);
if (match) return `temp:${match[1]}`;
match = /\/\.pi\/agent\/extensions\/([^/]+)/i.exec(p);
if (match) return `extension:${match[1]}`;
match = /\/\.pi-lens\//i.exec(p);
if (match) return ".pi-lens";
match = /^([A-Za-z]:)?\/([^/]+)/.exec(p);
return match?.[2] ?? "unknown";
}
function shortPath(filePath) {
if (!filePath) return undefined;
const p = normalizePath(filePath);
const desktop = /\/Desktop\/([^/]+\/.*)$/i.exec(p);
if (desktop) return desktop[1];
const temp = /\/AppData\/Local\/Temp\/([^/]+\/.*)$/i.exec(p);
if (temp) return `Temp/${temp[1]}`;
const home = normalizePath(os.homedir());
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
}
function normalizePath(filePath) {
return String(filePath).replace(/\\/g, "/");
}
function buildReport(state) {
const smells = [];
const smellCount = (id) => state.smellTotals.get(id);
addSmell(
smells,
"diagnostic-blockers",
smellCount("diagnostic-blockers"),
"Diagnostics shown inline/to agent or severity=error",
state.diagnostics.errors.slice(0, limit),
);
addSmell(
smells,
"runner-failures",
smellCount("runner-failures"),
"Dispatch runners failed/crashed",
state.latency.runnerFailures.slice(0, limit),
);
addSmell(
smells,
"slow-hook-path",
smellCount("slow-hook-path"),
`Tool total phases >= ${thresholds.totalSlowMs}ms`,
state.latency.slowTotals.slice(0, limit),
);
addSmell(
smells,
"slow-runners",
smellCount("slow-runners"),
`Runner durations >= ${thresholds.runnerSlowMs}ms`,
state.latency.slowRunners.slice(0, limit),
);
addSmell(
smells,
"tool-install-noise",
smellCount("tool-install-noise"),
"Repeated unavailable/failed preinstall or auto-install messages",
state.session.toolNoise.top(limit),
);
addSmell(
smells,
"lsp-availability-noise",
smellCount("lsp-availability-noise"),
"Repeated LSP unavailable/failed/timeout messages",
state.session.lspNoise.top(limit),
);
addSmell(
smells,
"slow-session-start",
smellCount("slow-session-start"),
`session_start total >= ${thresholds.startupSlowMs}ms`,
state.session.slowStarts.slice(0, limit),
);
addSmell(
smells,
"slow-background-tasks",
smellCount("slow-background-tasks"),
`session_start background tasks >= ${thresholds.backgroundSlowMs}ms`,
state.session.slowTasks.slice(0, limit),
);
addSmell(
smells,
"cascade-fallbacks",
smellCount("cascade-fallbacks"),
"Cascade used fallback/degraded path or logged errors",
state.cascade.fallbacks.slice(0, limit),
);
addSmell(
smells,
"cascade-slow-graphs",
smellCount("cascade-slow-graphs"),
`Cascade graph build >= ${thresholds.cascadeGraphSlowMs}ms`,
state.cascade.slowGraphs.slice(0, limit),
);
addSmell(
smells,
"read-guard-friction",
smellCount("read-guard-friction"),
"Read-guard blocked/warned edits or exact replacement misses",
[...state.readGuard.blocked, ...state.readGuard.oldTextIssues].slice(
0,
limit,
),
);
addSmell(
smells,
"read-guard-stale-ranges",
smellCount("read-guard-stale-ranges"),
"Covered edit ranges whose read snapshot no longer matched current content",
state.readGuard.staleRanges.slice(0, limit),
);
addSmell(
smells,
"tree-sitter-blocking",
smellCount("tree-sitter-blocking"),
"Tree-sitter runner produced blocking diagnostics",
state.treeSitter.blocking.slice(0, limit),
);
addSmell(
smells,
"tree-sitter-failures",
smellCount("tree-sitter-failures"),
"Tree-sitter runner failures",
state.treeSitter.failures.slice(0, limit),
);
addSmell(
smells,
"ast-grep-tool-errors",
smellCount("ast-grep-tool-errors"),
"MCP ast-grep search/replace calls that errored",
state.astGrep.errors.slice(0, limit),
);
addSmell(
smells,
"actionable-warning-errors",
smellCount("actionable-warning-errors"),
"Actionable-warnings advisory pipeline logged an error",
state.actionable.errors.slice(0, limit),
);
const ws = state.latency.workspace;
const incompleteSweeps = Math.max(0, ws.started - ws.completed);
addSmell(
smells,
"lsp-workspace-diagnostics-incomplete",
incompleteSweeps,
"Full LSP workspace sweeps that logged a start but never a completion (hang/kill signature — the 8h-hang class #383 hardened)",
ws.progress.slice(0, limit),
);
addSmell(
smells,
"lsp-workspace-file-timeouts",
smellCount("lsp-workspace-file-timeouts"),
`Full LSP sweeps where files hit the per-file budget (${ws.timedOutFilesTotal} files across ${ws.timedOutSweeps} sweeps)`,
ws.sweeps.slice(0, limit),
);
return {
window: state.window,
filesScanned: Object.fromEntries(
Object.entries(state.files).map(([key, list]) => [key, list.length]),
),
rowsSeen: state.seen.toJSON(),
parseErrors: state.parseErrors.toJSON(),
projects: state.projects.top(limit),
smells,
diagnostics: {
bySeverity: state.diagnostics.bySeverity.toJSON(),
byTool: state.diagnostics.byTool.toJSON(),
byRule: state.diagnostics.byRule.top(limit),
byFile: state.diagnostics.byFile.top(limit),
topMessages: state.diagnostics.topMessages.top(limit),
shownInlineShownAgentUnresolved: state.diagnostics.shownInline.toJSON(),
},
latency: {
runnerStatus: state.latency.runnerStatus.top(limit * 2),
runnerFailureKinds: state.latency.runnerFailureKinds.top(limit * 2),
runnerBlockingFindings: state.latency.runnerBlockingFindings.toJSON(),
toolResults: state.latency.toolResults.toJSON(),
phaseCounts: state.latency.phaseCounts.top(limit),
phaseTimeouts: state.latency.phaseTimeouts.toJSON(),
workspaceDiagnostics: {
started: ws.started,
completed: ws.completed,
incomplete: incompleteSweeps,
aborted: ws.aborted,
timedOutSweeps: ws.timedOutSweeps,
timedOutFilesTotal: ws.timedOutFilesTotal,
},
},
cascade: {
phases: state.cascade.phases.toJSON(),
missingSnapshots: state.cascade.missingSnapshots.top(limit),
noNeighborsByProject: state.cascade.noNeighbors.top(limit),
largeFanout: state.cascade.largeFanout.slice(0, limit),
},
readGuard: {
events: state.readGuard.events.toJSON(),
byReason: state.readGuard.byReason.toJSON(),
preflightReasons: state.readGuard.preflightReasons.toJSON(),
snapshotStatus: state.readGuard.snapshotStatus.toJSON(),
snapshotEnforcement: state.readGuard.snapshotEnforcement.toJSON(),
byFile: state.readGuard.byFile.top(limit),
staleRanges: state.readGuard.staleRanges.slice(0, limit),
zeroReads: state.readGuard.zeroReads.slice(0, limit),
unavailableSnapshots: state.readGuard.unavailableSnapshots.slice(
0,
limit,
),
},
treeSitter: {
phases: state.treeSitter.phases.toJSON(),
queryCache: state.treeSitter.queryCache.toJSON(),
riskFlags: state.treeSitter.riskFlags.toJSON(),
highDiagnostics: state.treeSitter.highDiagnostics.slice(0, limit),
},
session: {
starts: state.session.starts,
cwds: state.session.cwds.top(limit),
rotations: state.session.rotations.toJSON(),
errors: state.session.errors.slice(0, limit),
},
actionable: {
events: state.actionable.events.toJSON(),
reports: state.actionable.reports,
advisoriesInjected: state.actionable.injected,
advisoryWarningsInjected: state.actionable.injectedAdvisories,
warningsSuppressed: state.actionable.suppressed,
autoFixEligible: state.actionable.autoFixEligible,
lspSource: state.actionable.lspSource.toJSON(),
fileSkipReasons: state.actionable.fileSkipReasons.toJSON(),
errors: state.actionable.errors.slice(0, limit),
},
astGrep: {
calls: state.astGrep.calls,
outcomes: state.astGrep.outcomes.toJSON(),
errorKinds: state.astGrep.errorKinds.toJSON(),
truncated: state.astGrep.truncated,
errors: state.astGrep.errors.slice(0, limit),
slow: state.astGrep.slow.slice(0, limit),
},
};
}
function addSmell(smells, id, count, description, examples) {
if (!count) return;
const severity = count >= 20 ? "high" : count >= 5 ? "medium" : "low";
smells.push({ id, severity, count, description, examples });
}
function printReport(report) {
console.log(`pi-lens log smell report`);
console.log(`window: ${report.window.since} → now`);
console.log(`root: ${report.window.root}`);
console.log(
`rows: ${
Object.entries(report.rowsSeen)
.map(([k, v]) => `${k}=${v}`)
.join(", ") || "none"
}`,
);
console.log(
`files scanned: ${Object.entries(report.filesScanned)
.map(([k, v]) => `${k}=${v}`)
.join(", ")}`,
);
if (Object.keys(report.parseErrors).length)
console.log(`parse errors: ${JSON.stringify(report.parseErrors)}`);
section(
"Projects touched",
report.projects,
(x) => `${x.count.toString().padStart(5)} ${x.key}`,
);
console.log("\nSmells");
if (!report.smells.length) {
console.log(" none above thresholds");
} else {
for (const smell of report.smells) {
console.log(`\n [${smell.severity}] ${smell.id}: ${smell.count}`);
console.log(` ${smell.description}`);
for (const ex of smell.examples.slice(0, Math.min(5, limit)))
console.log(` - ${formatExample(ex)}`);
}
}
section(
"Diagnostic rules",
report.diagnostics.byRule,
(x) => `${x.count.toString().padStart(5)} ${x.key}`,
);
section(
"Diagnostic files",
report.diagnostics.byFile,
(x) => `${x.count.toString().padStart(5)} ${x.key}`,
);
section(
"Runner statuses",
report.latency.runnerStatus,
(x) => `${x.count.toString().padStart(5)} ${x.key}`,
);
section(
"Read-guard events",
Object.entries(report.readGuard.events).map(([key, count]) => ({
key,
count,
})),
(x) => `${String(x.count).padStart(5)} ${x.key}`,
);
section(
"Read-guard block reasons",
Object.entries(report.readGuard.byReason).map(([key, count]) => ({
key,
count,
})),
(x) => `${String(x.count).padStart(5)} ${x.key}`,
);
section(
"Read-guard snapshot status",
Object.entries(report.readGuard.snapshotStatus).map(([key, count]) => ({
key,
count,
})),
(x) => `${String(x.count).padStart(5)} ${x.key}`,
);
section(
"Cascade phases",
Object.entries(report.cascade.phases).map(([key, count]) => ({
key,
count,
})),
(x) => `${String(x.count).padStart(5)} ${x.key}`,
);
section(
"Runner failure kinds (infra vs found-errors)",
report.latency.runnerFailureKinds,
(x) => `${x.count.toString().padStart(5)} ${x.key}`,
);
const a = report.actionable;
if (a.reports || a.advisoriesInjected) {
console.log("\nActionable warnings");
console.log(
` reports=${a.reports} advisoriesInjected=${a.advisoriesInjected} warningsInjected=${a.advisoryWarningsInjected} suppressed=${a.warningsSuppressed} autoFixEligible=${a.autoFixEligible}`,
);
const lspSrc = Object.entries(a.lspSource);
if (lspSrc.length)
console.log(
` lspSource: ${lspSrc.map(([k, v]) => `${k}=${v}`).join(", ")}`,
);
const skips = Object.entries(a.fileSkipReasons);
if (skips.length)
console.log(
` lsp skip reasons: ${skips.map(([k, v]) => `${k}=${v}`).join(", ")}`,
);
}
const ag = report.astGrep;
if (ag.calls) {
console.log("\nast-grep tools");
console.log(` calls=${ag.calls} truncated=${ag.truncated}`);
const outcomes = Object.entries(ag.outcomes);
if (outcomes.length)
console.log(
` outcomes: ${outcomes.map(([k, v]) => `${k}=${v}`).join(", ")}`,
);
if (Object.keys(ag.errorKinds).length)
console.log(` error kinds: ${JSON.stringify(ag.errorKinds)}`);
}
const ws = report.latency.workspaceDiagnostics;
if (ws && (ws.started || ws.completed)) {
console.log("\nLSP workspace sweeps (lens_diagnostics full)");
console.log(
` started=${ws.started} completed=${ws.completed} incomplete=${ws.incomplete} aborted=${ws.aborted} fileTimeouts=${ws.timedOutFilesTotal} (in ${ws.timedOutSweeps} sweeps)`,
);
}
const timeouts = Object.entries(report.latency.phaseTimeouts ?? {});
if (timeouts.length) {
console.log("\nPhase timeouts");
for (const [k, v] of timeouts)
console.log(` ${String(v).padStart(5)} ${k}`);
}
}
function section(title, rows, formatter) {
console.log(`\n${title}`);
if (!rows?.length) {
console.log(" none");
return;
}
for (const row of rows.slice(0, limit)) console.log(` ${formatter(row)}`);
}
function formatExample(ex) {
if (!ex) return "";
const bits = [];
if (ex.durationMs != null) bits.push(`${ex.durationMs}ms`);
if (ex.graphBuiltMs != null) bits.push(`graph=${ex.graphBuiltMs}ms`);
if (ex.runnerId) bits.push(ex.runnerId);
if (ex.phase) bits.push(ex.phase);
if (ex.status) bits.push(ex.status);
if (ex.tool) bits.push(`${ex.tool}/${ex.ruleId}`);
if (ex.event) bits.push(ex.event);
if (ex.project) bits.push(`[${ex.project}]`);
if (ex.filePath) bits.push(ex.filePath);
if (ex.line) bits.push(`:${ex.line}`);
if (ex.message) bits.push(`— ${String(ex.message).slice(0, 160)}`);
if (ex.error) bits.push(`— ${String(ex.error).slice(0, 160)}`);
if (!bits.length && ex.key) bits.push(`${ex.count} × ${ex.key}`);
return bits.join(" ");
}
if (process.argv[1] === fileURLToPath(import.meta.url) && args.help) {
printHelp();
}