Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
ff4f4eb
refactor: comment hygiene + enforce no-vscode-in-core (stabilization …
dgtalbug Jun 7, 2026
4630091
fix(core): produce IMPORTS edges for every language, not just TS/JS (…
dgtalbug Jun 7, 2026
1269c97
refactor(core): confine DuckDB driver to a storage adapter (group 3, …
dgtalbug Jun 7, 2026
bb901df
refactor: serializer strategy + precise-resolver interface (group 4, …
dgtalbug Jun 7, 2026
e6a98c0
refactor: typed edge metadata + boundary hardening (group 5, ARCH-006…
dgtalbug Jun 7, 2026
ff16c09
refactor: decompose backend god-objects (group 6, backend splits)
dgtalbug Jun 7, 2026
f02af8b
refactor(exporters): typed serializer result; drop regex-on-message +…
dgtalbug Jun 7, 2026
9ae8ddb
refactor(extension): extract useGraphLenses hook from GraphView (6.3,…
dgtalbug Jun 7, 2026
e2935bb
refactor(extension): extract export-shortcut commands from activate (…
dgtalbug Jun 7, 2026
51cfb83
ci(core): guard against spec-tracking tags in shipped source (1.6)
dgtalbug Jun 7, 2026
7f3fe31
refactor(core): drop unused ExtractionResult.modules/tests fields (8.1)
dgtalbug Jun 7, 2026
df5c52c
refactor(core): route clear.ts through runInTransaction; per-connecti…
dgtalbug Jun 7, 2026
9d5b86d
refactor(exporters): cap helper, indexed sequence lookups, split clas…
dgtalbug Jun 7, 2026
e4c19d2
refactor: single source of truth for Mermaid option strings (8.4, par…
dgtalbug Jun 7, 2026
90d741a
feat(extension): nest methods under enclosing class in symbols tree (…
dgtalbug Jun 7, 2026
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
2 changes: 2 additions & 0 deletions .github/workflows/_static-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
run: pnpm format:check
- name: ESLint
run: pnpm lint
- name: No spec-tracking tags in source
run: pnpm lint:spec-tags
- name: TypeScript
run: pnpm typecheck
- name: pnpm dedupe check
Expand Down
28 changes: 28 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,33 @@ export default [
},
},
},
{
// core must run in Node, browser (WASM), and VS Code contexts — it can never
// depend on the `vscode` host API. The DuckDB driver is confined to the
// storage adapter (`storage/adapters/**`); every other core module depends on
// the connection type re-exported from `storage/db.ts`, not the driver.
// Enforces the no-vscode-in-core + driver-behind-adapter boundaries.
files: ["packages/core/src/**/*.ts"],
ignores: ["**/*.test.ts", "**/__fixtures__/**", "packages/core/src/storage/adapters/**"],
rules: {
"no-restricted-imports": [
"error",
{
paths: [
{
name: "vscode",
message:
"core must not import vscode — it runs in Node/WASM/browser. Inject host behaviour via an interface.",
},
{
name: "@duckdb/node-api",
message:
"Import the DuckDB driver only in storage/adapters/. Other modules use GraphDbConnection from storage/db.ts.",
},
],
},
],
},
},
prettier,
];
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"format:check": "prettier --check .",
"lint": "turbo run lint",
"lint:fix": "eslint --fix \"packages/**/*.ts\"",
"lint:spec-tags": "node scripts/check-no-spec-tags.mjs",
"package": "pnpm --filter dextree run package",
"prepare": "husky",
"test": "turbo run test",
Expand Down
36 changes: 23 additions & 13 deletions packages/core/scripts/gen-tags-providers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,24 @@ const outDir = resolve(coreRoot, "src/extractors/languages/generated");

// One entry per language. The ONLY per-language data: package name + which
// detectLanguage value(s) it serves + the default capture→kind config.
// `imports` = top-level AST node types that represent an import in that grammar,
// used to emit IMPORTS edges. Empty/omitted → the language emits no IMPORTS.
const LANGUAGES = [
{ pkg: "tree-sitter-python", languages: ["python"] },
{ pkg: "tree-sitter-go", languages: ["go"] },
{ pkg: "tree-sitter-java", languages: ["java"] },
{ pkg: "tree-sitter-ruby", languages: ["ruby"] },
{ pkg: "tree-sitter-rust", languages: ["rust"] },
{ pkg: "tree-sitter-c", languages: ["c"] },
{ pkg: "tree-sitter-cpp", languages: ["cpp"] },
{ pkg: "tree-sitter-c-sharp", languages: ["csharp"] },
{ pkg: "tree-sitter-php", languages: ["php"] },
{ pkg: "tree-sitter-elixir", languages: ["elixir"] },
{ pkg: "tree-sitter-scala", languages: ["scala"] },
{
pkg: "tree-sitter-python",
languages: ["python"],
imports: ["import_statement", "import_from_statement"],
},
{ pkg: "tree-sitter-go", languages: ["go"], imports: ["import_declaration"] },
{ pkg: "tree-sitter-java", languages: ["java"], imports: ["import_declaration"] },
{ pkg: "tree-sitter-ruby", languages: ["ruby"], imports: [] },
{ pkg: "tree-sitter-rust", languages: ["rust"], imports: ["use_declaration"] },
{ pkg: "tree-sitter-c", languages: ["c"], imports: ["preproc_include"] },
{ pkg: "tree-sitter-cpp", languages: ["cpp"], imports: ["preproc_include"] },
{ pkg: "tree-sitter-c-sharp", languages: ["csharp"], imports: ["using_directive"] },
{ pkg: "tree-sitter-php", languages: ["php"], imports: ["namespace_use_declaration"] },
{ pkg: "tree-sitter-elixir", languages: ["elixir"], imports: [] },
{ pkg: "tree-sitter-scala", languages: ["scala"], imports: ["import_declaration"] },
];

// Standard tree-sitter tags `definition.<suffix>` → Dextree SymbolKind. Suffixes
Expand All @@ -48,10 +54,11 @@ function tagsFor(pkg) {
return readFileSync(p, "utf8");
}

function emit({ pkg, languages }) {
function emit({ pkg, languages, imports = [] }) {
const tags = tagsFor(pkg);
const constName = pkg.replace(/[^a-z0-9]/gi, "_").toUpperCase();
const escaped = tags.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
const importsLine = imports.length > 0 ? ` importNodeTypes: ${JSON.stringify(imports)},\n` : "";
const lines = [
`// AUTO-GENERATED from ${pkg}/queries/tags.scm — do not edit by hand.`,
`// Regenerate: pnpm --filter @dextree/core gen:tags`,
Expand All @@ -62,15 +69,18 @@ function emit({ pkg, languages }) {
`const CONFIG: ProviderConfig = {`,
` symbolKinds: ${JSON.stringify(DEFAULT_SYMBOL_KINDS, null, 2).replace(/\n/g, "\n ")},`,
` callCaptures: ["reference.call"],`,
importsLine ? importsLine.replace(/\n$/, "") : null,
`};`,
].filter((l) => l !== null);
lines.push(
``,
`export const ${constName}_PROVIDERS: readonly LanguageProvider[] = [`,
...languages.map(
(l) => ` { language: ${JSON.stringify(l)}, tagsQuery: TAGS, config: CONFIG },`,
),
`];`,
``,
];
);
const out = resolve(outDir, `${pkg}.ts`);
writeFileSync(out, lines.join("\n"), "utf8");
return { pkg, constName, file: `${pkg}.ts`, languages };
Expand Down
25 changes: 13 additions & 12 deletions packages/core/src/extractors/GenericTagsExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Language, Node, Tree } from "web-tree-sitter";

import { detectLanguage, extractImportRefs } from "../parser/extractor.js";
import { loadGrammar } from "../parser/grammars.js";
import { EDGE_META_KEYS } from "../storage/edgeMetadata.js";
import type { StoredSymbol, SymbolKind, SymbolRange } from "../types.js";
import { getLanguageProvider } from "./languages/registry.js";
import type { LanguageProvider } from "./languages/types.js";
Expand Down Expand Up @@ -60,6 +61,8 @@ export class GenericTagsExtractor implements Extractor {
input.absolutePath,
input.workspaceRoot,
input.fileId,
input.language,
provider.config.importNodeTypes,
);

const edges = provider.config.structuralOnly
Expand Down Expand Up @@ -194,9 +197,9 @@ function buildCallEdges(calls: CallSite[], symbols: StoredSymbol[], fileId: stri
targetId: null,
kind: "CALLS",
metadata: {
callee_name: call.callee,
source_fqn: source ? source.fqn : null,
call_site_range: rangeOf(call.node),
[EDGE_META_KEYS.calleeName]: call.callee,
[EDGE_META_KEYS.sourceFqn]: source ? source.fqn : null,
[EDGE_META_KEYS.callSiteRange]: rangeOf(call.node),
},
});
}
Expand All @@ -207,11 +210,11 @@ function buildCallEdges(calls: CallSite[], symbols: StoredSymbol[], fileId: stri
// REFERENCES resolves to a symbol by name; RE_EXPORTS names a module path
// (resolved at query time like IMPORTS), so its key is distinct.
const TARGET_NAME_KEY: Record<string, string> = {
INHERITS: "parent_name",
INSTANTIATES: "class_name",
IMPLEMENTS: "interface_name",
REFERENCES: "referenced_name",
RE_EXPORTS: "reexport_path",
INHERITS: EDGE_META_KEYS.parentName,
INSTANTIATES: EDGE_META_KEYS.className,
IMPLEMENTS: EDGE_META_KEYS.interfaceName,
REFERENCES: EDGE_META_KEYS.referencedName,
RE_EXPORTS: EDGE_META_KEYS.reexportPath,
};

/**
Expand All @@ -235,8 +238,8 @@ function buildRelationEdges(
kind: rel.kind,
metadata: {
[targetKey]: rel.targetName,
source_fqn: source ? source.fqn : null,
reference_range: rangeOf(rel.node),
[EDGE_META_KEYS.sourceFqn]: source ? source.fqn : null,
[EDGE_META_KEYS.referenceRange]: rangeOf(rel.node),
},
});
}
Expand Down Expand Up @@ -350,8 +353,6 @@ function emptyResult(): ExtractionResult {
imports: [],
edges: [],
annotations: [],
modules: [],
tests: [],
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ const UNCLASSIFIED_ENTRY: EntryKind = "unclassified";
const UNKNOWN_LAYER: ArchitecturalLayer = "unknown";

// Each branch is wrapped so the trailing `$` on the file-extension branch
// cannot be misread as also anchoring the directory-segment branch
// (CodeQL js/regex/missing-regexp-anchor).
// cannot be misread as also anchoring the directory-segment branch.
const TEST_PATH_RX =
/(?:(?:^|\/)(?:__tests__|__test__|tests?)\/)|(?:\.(?:test|spec)\.[mc]?[jt]sx?$)/;

Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/extractors/frameworks/detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import type {

/**
* Pure entrypoint for workspace-level framework detection. Walks the
* registry; per framework wraps both matchers in try/catch (FR-007). Returns
* results in registry order. Empty array — never null — when nothing matched.
* registry; per framework wraps both matchers in try/catch for isolation.
* Returns results in registry order. Empty array — never null — when nothing
* matched.
*/
export const detectFrameworks: DetectFrameworksFn = async (
params: DetectFrameworksParams,
Expand Down Expand Up @@ -45,7 +46,7 @@ async function runMatchersFor(
tryMatchStructural(def.structural, structuralIO),
]);

// FR-003: BOTH signals required.
// BOTH signals required.
if (!manifestHit || !structuralHit) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ function extractValue(content: string, keyPath: ManifestKeyPath): string | undef
}
if (lower.endsWith(".toml") || lower === "go.mod") {
// go.mod is technically not TOML, but its `require ( ... )` blocks are
// matched by a different path — for slice 018 we treat the whole file as
// text and rely on the regex policy. The TOML reader will return
// matched by a different path — we treat the whole file as text and rely on
// the regex policy. The TOML reader will return
// undefined for go.mod, so we fall through to whole-file regex below.
const tomlValue = readTomlKeypath(content, keyPath.keypath);
if (tomlValue !== undefined) return tomlValue;
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/extractors/frameworks/matchers/toml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
* Minimal TOML keypath reader. Returns the string value at a dot-separated
* keypath, or undefined if missing. Handles `[section]` and `[section.sub]`
* headers, plus `key = "value"` and `key = value` lines. Comments (`#`) are
* ignored. Arrays-of-tables (`[[name]]`) are out of scope for slice 018;
* if a framework needs them, upgrade to a library and keep the contract.
* ignored. Arrays-of-tables (`[[name]]`) are out of scope for this minimal
* parser; if a framework needs them, upgrade to a library and keep the contract.
*
* Why inline: we only need keypath lookup. A full TOML parser is ~5 KB
* of dependency surface we don't otherwise use.
Expand All @@ -26,7 +26,7 @@ export function readTomlKeypath(content: string, keypath: string): string | unde
}

if (line.startsWith("[[")) {
// Array-of-tables not supported in this slice; skip the section.
// Array-of-tables not supported; skip the section.
currentSection = "__unsupported__";
continue;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/extractors/frameworks/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* - manifest: one or more `ManifestKeyPath` rules. Detection requires at
* least one manifest match.
* - structural: one `StructuralSignal`. Detection requires this AND
* manifest to fire — both signals are needed (FR-003).
* manifest to fire — both signals are needed.
* - fileRole: pure (filePath, fileContent) → role | null. Returns the
* per-file role attribution for files in workspaces where
* this framework was detected.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const CONFIG: ProviderConfig = {
variable: "variable",
},
callCaptures: ["reference.call"],
importNodeTypes: ["using_directive"],
};

export const TREE_SITTER_C_SHARP_PROVIDERS: readonly LanguageProvider[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const CONFIG: ProviderConfig = {
variable: "variable",
},
callCaptures: ["reference.call"],
importNodeTypes: ["preproc_include"],
};

export const TREE_SITTER_C_PROVIDERS: readonly LanguageProvider[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const CONFIG: ProviderConfig = {
variable: "variable",
},
callCaptures: ["reference.call"],
importNodeTypes: ["preproc_include"],
};

export const TREE_SITTER_CPP_PROVIDERS: readonly LanguageProvider[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const CONFIG: ProviderConfig = {
variable: "variable",
},
callCaptures: ["reference.call"],
importNodeTypes: ["import_declaration"],
};

export const TREE_SITTER_GO_PROVIDERS: readonly LanguageProvider[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const CONFIG: ProviderConfig = {
variable: "variable",
},
callCaptures: ["reference.call"],
importNodeTypes: ["import_declaration"],
};

export const TREE_SITTER_JAVA_PROVIDERS: readonly LanguageProvider[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const CONFIG: ProviderConfig = {
variable: "variable",
},
callCaptures: ["reference.call"],
importNodeTypes: ["namespace_use_declaration"],
};

export const TREE_SITTER_PHP_PROVIDERS: readonly LanguageProvider[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const CONFIG: ProviderConfig = {
variable: "variable",
},
callCaptures: ["reference.call"],
importNodeTypes: ["import_statement", "import_from_statement"],
};

export const TREE_SITTER_PYTHON_PROVIDERS: readonly LanguageProvider[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const CONFIG: ProviderConfig = {
variable: "variable",
},
callCaptures: ["reference.call"],
importNodeTypes: ["use_declaration"],
};

export const TREE_SITTER_RUST_PROVIDERS: readonly LanguageProvider[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const CONFIG: ProviderConfig = {
variable: "variable",
},
callCaptures: ["reference.call"],
importNodeTypes: ["import_declaration"],
};

export const TREE_SITTER_SCALA_PROVIDERS: readonly LanguageProvider[] = [
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/extractors/languages/multiLanguage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,33 @@ describe("multi-language extraction (data-only providers)", () => {
expect(result.symbols.every((s) => s.language === c.language)).toBe(true);
});
}

// IMPORTS edges must exist for non-TS/JS languages whose provider declares
// import node types (previously the import walk was TS/JS-only).
const IMPORT_CASES = [
{
language: "python",
path: "a.py",
source: "import os\nfrom a.b import c\n",
expect: ["os", "a.b"],
},
{ language: "go", path: "a.go", source: 'package m\nimport "fmt"\n', expect: ["fmt"] },
{ language: "rust", path: "a.rs", source: "use std::io;\n", expect: ["std::io"] },
];

for (const c of IMPORT_CASES) {
it(`produces IMPORTS edges for ${c.language}`, async () => {
const result = await extract(c.source, c.path, c.language);
expect(result.imports.length).toBeGreaterThan(0);
// Every import edge is tagged with the file's language, not hardcoded TS.
expect(result.imports.every((i) => i.language === c.language)).toBe(true);
const paths = result.imports.map((i) => i.importPath);
for (const want of c.expect) {
expect(
paths.some((p) => p.includes(want)),
`${c.language} import path should include ${want}; got ${paths.join(", ")}`,
).toBe(true);
}
});
}
});
8 changes: 8 additions & 0 deletions packages/core/src/extractors/languages/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ export interface ProviderConfig {
* no such edges for the language.
*/
readonly relationCaptures?: Readonly<Record<string, string>>;
/**
* Top-level AST node types that represent an import in this grammar (e.g. TS
* `["import_statement"]`, Python `["import_statement","import_from_statement"]`,
* Go `["import_declaration"]`, Rust `["use_declaration"]`). The engine reads the
* module specifier from these and emits an `IMPORTS` edge per import. Omitted →
* no IMPORTS edges for the language (e.g. structural formats).
*/
readonly importNodeTypes?: readonly string[];
/**
* Structural-only formats (markdown/yaml/json) that have no call graph. When
* true the engine still records file + definition nodes but fabricates no
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/extractors/languages/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ const TS_CONFIG: ProviderConfig = {
module: "type",
},
callCaptures: ["reference.call"],
importNodeTypes: ["import_statement"],
relationCaptures: {
"reference.extends": "INHERITS",
"reference.implements": "IMPLEMENTS",
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/extractors/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ function emptyResult(): ExtractionResult {
imports: [],
edges: [],
annotations: [],
modules: [],
tests: [],
};
}

Expand Down
Loading
Loading