socraticode
Version:
SocratiCode — MCP server for local codebase indexing, semantic search, and code dependency graphs. All private, all local via Docker.
123 lines • 4.49 kB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
/**
* Entry-point detection — three-heuristic union:
* 1. Graph orphans with outgoing calls (files that nothing imports but
* that call into other files)
* 2. Conventional names (`main`, `Main`, `__main__`, etc.)
* 3. Framework routes (Express/Flask/FastAPI/NestJS/Spring/ASP.NET/...)
*
* Each detected entry point carries a `reason` so the AI sees why.
*/
import { ENTRY_POINT_NAMES } from "../constants.js";
/**
* Detect entry points across all per-file payloads. The caller must supply
* the file-import graph (for orphan detection) plus all per-file payloads.
*/
export function detectEntryPoints(fileGraph, payloads) {
const out = [];
const seen = new Set();
const push = (e) => {
const key = `${e.id}::${e.reason}`;
if (seen.has(key))
return;
seen.add(key);
out.push(e);
};
// Heuristic 1: orphans with outgoing calls
const payloadByFile = new Map(payloads.map((p) => [p.file, p]));
for (const node of fileGraph.nodes) {
if (node.dependents.length > 0)
continue;
const payload = payloadByFile.get(node.relativePath);
if (!payload || payload.outgoingCalls.length === 0)
continue;
push({
id: node.relativePath,
name: node.relativePath,
file: node.relativePath,
reason: "orphan",
});
}
// Heuristic 2: conventional names
for (const p of payloads) {
const conventional = ENTRY_POINT_NAMES[p.language];
if (!conventional)
continue;
for (const sym of p.symbols) {
if (conventional.has(sym.name) && sym.name !== "<module>") {
push({
id: sym.id,
name: sym.qualifiedName,
file: sym.file,
line: sym.line,
reason: `well-known-name:${sym.name}`,
});
}
}
}
// Heuristic 3: framework routes (regex over source not always available
// at this stage — instead inspect symbol decorators recorded as siblings,
// or fall back to outgoing call patterns naming framework router methods).
for (const p of payloads) {
for (const reason of detectFrameworkReasons(p)) {
push({
id: reason.symbol.id,
name: reason.symbol.qualifiedName,
file: reason.symbol.file,
line: reason.symbol.line,
reason: reason.reason,
});
}
}
return out;
}
/** Per-file framework heuristics based on call sites + symbol names. */
function detectFrameworkReasons(p) {
const out = [];
// Build a lookup from line → enclosing symbol
const symbolsByStartLine = [...p.symbols].sort((a, b) => a.line - b.line);
const findSymbolAt = (line) => {
let best = null;
for (const s of symbolsByStartLine) {
if (s.line <= line && line <= s.endLine && s.name !== "<module>") {
if (!best || s.line >= best.line)
best = s;
}
}
return best;
};
// Inspect outgoingCalls for framework router method calls
// (e.g. `app.get`, `router.post`, `Route::get`, `app.route`)
const framework = (calleeName) => {
const lower = calleeName.toLowerCase();
if (["get", "post", "put", "delete", "patch", "head", "options", "all", "use"].includes(lower)) {
return `framework:http-${lower}`;
}
if (lower === "route")
return "framework:route";
return null;
};
for (const e of p.outgoingCalls) {
const reason = framework(e.calleeName);
if (!reason)
continue;
const enclosing = findSymbolAt(e.callSite.line);
if (!enclosing)
continue;
out.push({ symbol: enclosing, reason });
}
// Test functions: names starting with `test_`, `Test`, or matching `it`/`describe`
for (const s of p.symbols) {
if (s.name === "<module>")
continue;
if (/^test[_A-Z]/.test(s.name) ||
/^Test[A-Z_]/.test(s.name) ||
s.name === "it" ||
s.name === "describe") {
out.push({ symbol: s, reason: "test" });
}
}
return out;
}
//# sourceMappingURL=graph-entrypoints.js.map