Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ A language server improves the graph with semantically resolved edges. Install t

| Language | Server | Install |
|---|---|---|
| TypeScript | `ttscserver` (0.18+) | `npm i -D ttsc typescript` |
| TypeScript | `ttscgraph` / `ttscserver` | `npm i -D ttsc@^0.19.4 typescript` |
| Python | `pyright-langserver` | `npm i -D pyright` |
| Go | `gopls` | `go install golang.org/x/tools/gopls@latest` |
| Rust | `rust-analyzer` | `rustup component add rust-analyzer` |
Expand All @@ -59,6 +59,8 @@ A language server improves the graph with semantically resolved edges. Install t

Each server must be on `PATH`. If none is present for a file's language, that language falls back to the static indexer automatically.

TypeScript uses the compiler-owned `ttscgraph` snapshot first. The binary is resolved from the target project's `ttsc` installation; `TTSC_GRAPH_BINARY` can point to an exact absolute binary for development or release verification. If the binary is unavailable, its schema/provenance cannot be trusted, or the requested build is deliberately capped, indexing states the reason and falls back to `ttscserver`, then to the static indexer when no server is available. `ttscgraph` schema 5 is the complete contract; older schema 3 producers are accepted only as an explicitly warned compatibility snapshot and do not carry the newer member-relation and object-member facts.

JavaScript is intentionally not indexed. In an arbitrary repository, `.js`/`.jsx`/`.mjs`/`.cjs` files are as often build output or vendored bundles as handwritten source, and the graph cannot tell which without project-specific provenance.

## Benchmark
Expand Down Expand Up @@ -131,7 +133,7 @@ One-time cost per repository. The server re-scans only changed files after that

kotlin-language-server, jdtls, and csharp-ls are particularly slow: each resolves the whole project before answering anything.

Closing that gap needs what `@ttsc/graph` already does for TypeScript: a compiler-native indexer instead of a generic LSP server. Not done here.
TypeScript already closes that gap through the compiler-owned `ttscgraph` snapshot. The remaining languages use their listed language servers until their compiler-owned bulk providers land.

### Reproduction

Expand Down Expand Up @@ -318,8 +320,9 @@ export namespace ISamchonGraphApplication {
* The audit is operation-aware. For the walks from a named handle (`trace`,
* `overview`) it reports the structure held for the named handles, bounded
* where `truncated` says. For `details` it reports the two halves of a
* resolved symbol: its own shape returned whole, its fan-out returned as a
* slice with `trace` for the rest. For ranked operations (`lookup`,
* resolved symbol: its own shape returned whole unless the caller explicitly
* capped members, and its fan-out returned as a slice with `trace` for the
* rest. For ranked operations (`lookup`,
* `entrypoints`, `tour`) it additionally says that selection was matched,
* scored, ranked, and limited against the question, so the facts are checked
* but shortlist coverage is yours to judge.
Expand Down
105 changes: 102 additions & 3 deletions packages/graph-sitter/src/indexer/staticGraphParts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ interface IDeclaration {
ownerName?: string;
}

interface ISwiftExtension {
file: string;
receiverName: string;
header: string;
evidence: ISamchonGraphEvidence;
}

interface IParsedDeclaration {
kind: GraphNodeKind;
name: string;
Expand Down Expand Up @@ -71,6 +78,7 @@ export function graphSitterParts(
const edges: ISamchonGraphEdge[] = [];
const sources = new Map<string, string>();
const declarationsByFile = new Map<string, IDeclaration[]>();
const swiftExtensions: ISwiftExtension[] = [];
const byName = new Map<string, ISamchonGraphNode[]>();
const externalNodes = new Map<string, ISamchonGraphNode>();
const absolutePathByRelativePath = new Map(
Expand All @@ -88,7 +96,12 @@ export function graphSitterParts(
const lines = text.split(/\r?\n/);
const rel = input.relativePath;
const language = input.language;
const declarations = declarationsOf(rel, language, lines);
const declarations = declarationsOf(
rel,
language,
lines,
swiftExtensions,
);
declarationsByFile.set(rel, declarations);
for (const declaration of declarations) {
nodes.push(declaration.node);
Expand Down Expand Up @@ -116,6 +129,12 @@ export function graphSitterParts(
}

connectProjectWideCppOwners(declarationsByFile, edges);
connectProjectWideSwiftOwners(
declarationsByFile,
swiftExtensions,
byName,
edges,
);

for (const [rel, declarations] of declarationsByFile) {
const abs = absolutePathByRelativePath.get(rel)!;
Expand Down Expand Up @@ -248,6 +267,7 @@ function declarationsOf(
file: string,
language: GraphLanguage,
lines: readonly string[],
swiftExtensions: ISwiftExtension[],
): IDeclaration[] {
const declarations: IDeclaration[] = [];
// Owners that a whole-file scan names rather than nests, indexed by the
Expand Down Expand Up @@ -325,6 +345,17 @@ function declarationsOf(
// `impl` block does, resolved below once the file has been scanned.
if (parsed.extensionOwner !== undefined) {
const endIndex = declarationEndIndexOf(language, lexicalLines, i);
swiftExtensions.push({
file,
receiverName: parsed.extensionOwner,
header: header.trim(),
evidence: {
file,
startLine: i + 1,
startCol: Math.max(1, line.indexOf(parsed.extensionOwner) + 1),
endLine: i + 1,
},
});
if (endIndex > i) {
const owner = swiftTypes.get(parsed.extensionOwner);
ownerStack.push({
Expand Down Expand Up @@ -522,6 +553,8 @@ function declarationHeader(
if (language === "kotlin")
return KotlinDeclarations.kotlinDeclarationHeader(lines, start);
if (language === "php") return PhpDeclarations.phpDeclarationHeader(lines, start);
if (language === "scala")
return ScalaDeclarations.scalaDeclarationHeader(lines, start);
if (language === "swift")
return SwiftDeclarations.swiftDeclarationHeader(lines, start);
return lines[start]!;
Expand Down Expand Up @@ -654,6 +687,66 @@ function connectProjectWideCppOwners(
}
}

/**
* Connect Swift extension members and conformance headers to one resolved
* receiver. A same-file declaration is authoritative when unique there;
* otherwise the qualified receiver must be unique across the project.
*/
function connectProjectWideSwiftOwners(
declarationsByFile: ReadonlyMap<string, IDeclaration[]>,
extensions: readonly ISwiftExtension[],
byName: Map<string, ISamchonGraphNode[]>,
edges: ISamchonGraphEdge[],
): void {
const declarations = [...declarationsByFile.values()]
.flat()
.filter((declaration) => declaration.node.language === "swift");
const types = new Map<string, IDeclaration[]>();
for (const declaration of declarations) {
if (!isSwiftExtendable(declaration.node.kind)) continue;
push(
types,
declaration.node.qualifiedName ?? declaration.node.name,
declaration,
);
}
for (const declaration of declarations) {
if (declaration.ownerId !== undefined || declaration.ownerName === undefined)
continue;
const candidates = types.get(declaration.ownerName);
if (candidates?.length !== 1) continue;
const owner = candidates[0]!;
declaration.ownerId = owner.node.id;
edges.push({
from: owner.node.id,
to: declaration.node.id,
kind: "contains",
evidence: declaration.node.evidence,
});
}
for (const extension of extensions) {
const candidates = types.get(extension.receiverName) ?? [];
const local = candidates.filter(
(candidate) => candidate.node.file === extension.file,
);
const owner =
local.length === 1
? local[0]
: local.length === 0 && candidates.length === 1
? candidates[0]
: undefined;
if (owner === undefined) continue;
edges.push(
...inheritanceEdges(
owner.node,
extension.header,
byName,
extension.evidence,
),
);
}
}

function cppQualifiedOwners(
lexical: readonly string[],
explicit: readonly string[],
Expand Down Expand Up @@ -1269,8 +1362,14 @@ function inheritanceEdges(
source: ISamchonGraphNode,
header: string,
byName: Map<string, ISamchonGraphNode[]>,
evidence: ISamchonGraphEvidence | undefined = source.evidence,
): ISamchonGraphEdge[] {
if (source.kind !== "class" && source.kind !== "interface") return [];
if (
source.kind !== "class" &&
source.kind !== "interface" &&
!(source.language === "swift" && source.kind === "enum")
)
return [];
const out: ISamchonGraphEdge[] = [];
const seen = new Set<string>();
for (const supertype of source.language === "swift"
Expand All @@ -1295,7 +1394,7 @@ function inheritanceEdges(
from: source.id,
to: target.id,
kind: relation,
evidence: source.evidence,
evidence,
});
}
return out;
Expand Down
3 changes: 2 additions & 1 deletion packages/graph/src/SamchonGraphApplication.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AsyncSamchonGraphSource } from "./AsyncSamchonGraphSource";
import { RESULT_AUDIT } from "./operations/RESULT_AUDIT";
import { RESULT_AUDIT_DETAILS } from "./operations/RESULT_AUDIT_DETAILS";
import { RESULT_AUDIT_SELECTION } from "./operations/RESULT_AUDIT_SELECTION";
import { RESULT_AUDIT_ESCAPE } from "./operations/RESULT_AUDIT_ESCAPE";
import { resultNext } from "./operations/resultNext";
Expand Down Expand Up @@ -86,7 +87,7 @@ export class SamchonGraphApplication implements ISamchonGraphApplication {
case "details": {
const r = runDetails(graph, props.request);
return {
audit: RESULT_AUDIT(graph.indexer),
audit: RESULT_AUDIT_DETAILS(graph.indexer, props.request.memberLimit),
next: r.next,
result: r.result,
};
Expand Down
72 changes: 22 additions & 50 deletions packages/graph/src/SamchonGraphMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import {
} from "./structures";
import { GraphLanguage } from "./typings";
import { basename } from "./utils/path";
import { SamchonGraphSourceReader } from "./SamchonGraphSourceReader";

/**
* The in-memory resident graph the MCP tools answer from.
*
* It loads one dump — the indexer-resolved fact graph — then synthesizes the
* structural layer the dump deliberately leaves to this layer: `file` container
* nodes and the `contains` ownership tree, plus member implementation edges and
* class-member property refinement. Every tool call is then a lookup or a
* nodes and the `contains` ownership tree, plus class-member property
* refinement. Export and member implementation relationships are producer
* facts already present in the dump. Every tool call is then a lookup or a
* traversal over the indexes built here; nothing re-indexes.
*/
export class SamchonGraphMemory {
Expand All @@ -32,17 +34,20 @@ export class SamchonGraphMemory {
public readonly indexer: ISamchonGraphDump["indexer"];
/** Every node, raw plus synthesized (file containers). */
public readonly nodes: readonly ISamchonGraphNode[];
/** Every edge, raw plus synthesized (contains and member relations). */
/** Every edge, raw plus synthesized containment. */
public readonly edges: readonly ISamchonGraphEdge[];
/** Fused compiler and plugin diagnostics, when the build collected any. */
public readonly diagnostics: readonly ISamchonGraphDiagnostic[];
/** Non-fatal problems encountered while building the graph. */
public readonly warnings: readonly string[];
/** Provenance-gated source display facts owned by this exact snapshot. */
public readonly source: SamchonGraphSourceReader;

private constructor(
dump: ISamchonGraphDump,
nodes: ISamchonGraphNode[],
edges: ISamchonGraphEdge[],
source: SamchonGraphSourceReader,
) {
this.project = dump.project;
this.languages = dump.languages;
Expand All @@ -51,6 +56,7 @@ export class SamchonGraphMemory {
this.edges = edges;
this.diagnostics = dump.diagnostics ?? [];
this.warnings = dump.warnings ?? [];
this.source = source;

this.byId = new Map(nodes.map((node) => [node.id, node]));
this.outEdges = new Map();
Expand All @@ -71,10 +77,18 @@ export class SamchonGraphMemory {
}
}

/** Build a model from a parsed dump, synthesizing structural relationships. */
public static from(dump: ISamchonGraphDump): SamchonGraphMemory {
/**
* Build a model from a parsed dump, synthesizing structural relationships.
* A caller that has only a dump has no proof for current disk bytes, so
* source-derived display facts fail closed unless an explicit reader is
* supplied.
*/
public static from(
dump: ISamchonGraphDump,
source: SamchonGraphSourceReader = SamchonGraphSourceReader.none(dump.project),
): SamchonGraphMemory {
const { nodes, edges } = synthesize(dump);
return new SamchonGraphMemory(dump, nodes, edges);
return new SamchonGraphMemory(dump, nodes, edges, source);
}

/** The node with this id, or undefined. */
Expand Down Expand Up @@ -147,7 +161,7 @@ function fileOfNodeId(id: string): string {
* Derive the structural layer from a dump's faithful facts: refine class-member
* variables to properties, put back the file the indexer left out of every
* span, add a `file` node per workspace source, and connect the `contains`
* ownership tree and member implementation relations.
* ownership tree.
*
* `exports` edges are not synthesized here. The indexer resolves them from the
* project's own export syntax and follows them through its barrels (§4k), so
Expand Down Expand Up @@ -249,11 +263,9 @@ function synthesize(dump: ISamchonGraphDump): {
edges.map((edge) => `${edge.kind}\0${edge.from}\0${edge.to}`),
);
const structural: ISamchonGraphEdge[] = [];
const membersByOwner = new Map<string, ISamchonGraphNode[]>();
for (const node of nodes) {
if (node.external || node.file === "" || node.kind === "file") continue;
const parent = owner(node);
if (parent !== undefined) push(membersByOwner, parent.id, node);
const container = parent?.id ?? node.file;
const key = `contains\0${container}\0${node.id}`;
if (edgeKeys.has(key)) continue;
Expand All @@ -265,48 +277,8 @@ function synthesize(dump: ISamchonGraphDump): {
});
}

const synthesized = [...edges, ...structural];
for (const edge of edges) {
const kind: ISamchonGraphEdge["kind"] | undefined =
edge.kind === "implements"
? "implements"
: edge.kind === "extends"
? "overrides"
: undefined;
if (kind === undefined) continue;
const derived = byId.get(edge.from);
const base = byId.get(edge.to);
if (derived === undefined || base === undefined) continue;
const derivedMembers = membersByOwner.get(derived.id) ?? [];
const baseMembers = membersByOwner.get(base.id) ?? [];
for (const baseMember of baseMembers) {
const derivedMember = derivedMembers.find(
(member) =>
member.name === baseMember.name &&
IMPLEMENTATION_MEMBER_KINDS.has(member.kind) &&
IMPLEMENTATION_MEMBER_KINDS.has(baseMember.kind),
);
if (derivedMember === undefined) continue;
const key = `${kind}\0${derivedMember.id}\0${baseMember.id}`;
if (edgeKeys.has(key)) continue;
edgeKeys.add(key);
synthesized.push({
from: derivedMember.id,
to: baseMember.id,
kind,
evidence: derivedMember.implementation ?? derivedMember.evidence,
});
}
}

return {
nodes: [...nodes, ...files.values()],
edges: synthesized,
edges: [...edges, ...structural],
};
}

// Canonical ttsc emits methods and refines class-owned variables to properties.
// Other supported language servers may already distinguish a field, which has
// the same member-satisfaction semantics. Constructors are intentionally absent:
// declaring a subclass constructor does not override its base constructor.
const IMPLEMENTATION_MEMBER_KINDS = new Set(["method", "property", "field"]);
Loading
Loading