Skip to content

Repository files navigation

@zipbul/gildash

English | 한국어

npm CI License: MIT

A Bun-native TypeScript code intelligence engine.

gildash indexes your TypeScript codebase into a local SQLite database, then lets you search symbols, trace cross-file relationships, analyze dependency graphs, and match structural patterns — all with incremental, file-watcher-driven updates.

💡 Why gildash?

Problem How gildash solves it
"Which files break if I change this module?" Directed import graph with transitive impact analysis
"Are there any circular dependencies?" Cycle detection across the full import graph
"Where is this symbol actually defined?" Re-export chain resolution to the original source
"Find every console.log(...) call" AST-level structural pattern search via ast-grep

✨ Features

  • Symbol extraction — Functions, classes, variables, types, interfaces, enums, and properties extracted via oxc-parser AST
  • Relation trackingimport, re-exports, type-references, calls, extends, implements relationships across files
  • Full-text search — SQLite FTS5-powered symbol name search with exact match, regex, and decorator filtering
  • Dependency graph — Directed import graph with cycle detection, transitive impact analysis, and internal caching
  • Structural pattern matching — AST-level code search via @ast-grep/napi
  • Incremental indexing@parcel/watcher-based file change detection; only re-indexes modified files
  • Symbol-level diffchangedSymbols in IndexResult tracks added/modified/removed symbols per index cycle
  • Annotation extraction — Generic @tag value extraction from JSDoc, line, and block comments with automatic symbol linking and FTS5 search
  • Symbol changelog — Tracks added/modified/removed/renamed/moved symbols across index runs, with structural fingerprint-based rename detection
  • Multi-process safe — Owner/reader role separation guarantees a single writer per database
  • Scan-only modewatchMode: false for one-shot indexing without file watcher overhead
  • tsconfig.json JSONC — Path alias resolution parses comments and trailing commas in tsconfig.json
  • Semantic layer (opt-in) — tsc TypeChecker integration for resolved types, references, implementations, and module interface analysis

📋 Requirements

  • Bun v1.3 or higher
  • TypeScript source files: .ts, .mts, .cts

📦 Installation

bun add @zipbul/gildash

Optional peertypescript (>=5.0.0) is needed only when using semantic: true.


🚀 Quick Start

import { Gildash } from '@zipbul/gildash';

// 1. Open — indexes every .ts file on first run, then watches for changes
const ledger = await Gildash.open({
  projectRoot: '/absolute/path/to/project',
});

// 2. Search — find symbols by name
const symbols = ledger.searchSymbols({ text: 'UserService', kind: 'class' });
symbols.forEach(s => console.log(`${s.name}${s.filePath}`));

// 3. Close — release resources
await ledger.close();

That's it. Project discovery (monorepo-aware), incremental re-indexing, and multi-process safety are handled automatically.


📖 Usage Guide

Symbol Search

Search indexed symbols with FTS5 full-text, exact match, regex, or decorator filters.

// Full-text search (FTS5 prefix matching)
const hits = ledger.searchSymbols({ text: 'handle' });

// Exact name match
const exact = ledger.searchSymbols({ text: 'UserService', exact: true });

// Regex pattern
const handlers = ledger.searchSymbols({ regex: '^handle.*Click$' });

// Decorator filter
const injectables = ledger.searchSymbols({ decorator: 'Injectable' });

// Combine filters
const exportedClasses = ledger.searchSymbols({
  kind: 'class',
  isExported: true,
  limit: 50,
});

Use searchRelations() to find cross-file relationships:

const imports = ledger.searchRelations({ srcFilePath: 'src/app.ts', type: 'imports' });
const callers = ledger.searchRelations({ dstSymbolName: 'processOrder', type: 'calls' });

For monorepo projects, searchAllSymbols() and searchAllRelations() search across every discovered project.


Dependency Analysis

Analyze import graphs, detect cycles, and compute change impact.

// Direct imports / importers
const deps = ledger.getDependencies('src/app.ts');
const importers = ledger.getDependents('src/utils.ts');

// Transitive impact — which files are affected by a change?
const affected = await ledger.getAffected(['src/utils.ts']);

// Full import graph (adjacency list)
const graph = await ledger.getImportGraph();

// Transitive dependencies (forward BFS)
const transitive = await ledger.getTransitiveDependencies('src/app.ts');

// Circular dependency detection
const hasCycles = await ledger.hasCycle();
const cyclePaths = await ledger.getCyclePaths();          // all elementary circuits
const limited   = await ledger.getCyclePaths(undefined, { maxCycles: 100 }); // undefined = use default project

Annotations

Search @tag value patterns extracted from JSDoc, line (//), and block (/* */) comments.

// Find all @deprecated annotations
const deprecated = ledger.searchAnnotations({ tag: 'deprecated' });
deprecated.forEach(a => console.log(`${a.symbolName}: ${a.value}`));

// Full-text search across annotation values
const caching = ledger.searchAnnotations({ text: 'caching' });

// Filter by source type and file
const todos = ledger.searchAnnotations({ tag: 'todo', source: 'line', filePath: 'src/app.ts' });

Symbol Changelog

Track symbol-level changes across index runs. Rename and move detection uses structural fingerprinting.

// Get all incremental changes since a given date
const changes = ledger.getSymbolChanges(new Date('2024-01-01'));

// Include full-index entries and filter by change type
const renames = ledger.getSymbolChanges(new Date(0), {
  includeFullIndex: true,
  changeTypes: ['renamed', 'moved'],
});
renames.forEach(c => console.log(`${c.oldName}${c.symbolName} (${c.changeType})`));

// Prune old entries (returns deleted count)
const pruned = ledger.pruneChangelog(new Date('2024-01-01'));

Code Quality Analysis

Inspect module interfaces and measure coupling.

// File statistics — line count, symbol count, size
const stats = ledger.getFileStats('src/app.ts');

// Fan-in / fan-out coupling metrics
const fan = await ledger.getFanMetrics('src/app.ts');

// Module public interface — all exported symbols with metadata
const iface = ledger.getModuleInterface('src/services/user.ts');

// Full symbol detail — members, jsDoc, decorators, type info
const full = ledger.getFullSymbol('UserService', 'src/services/user.ts');

Pattern Matching & Tracing

Search code by AST structure and trace symbol origins through re-export chains.

// Structural pattern search (ast-grep syntax)
const logs = await ledger.findPattern('console.log($$$)');
const hooks = await ledger.findPattern('useState($A)', {
  filePaths: ['src/components/App.tsx'],
});

// Resolve re-export chains — find where a symbol is actually defined
const resolved = ledger.resolveSymbol('MyComponent', 'src/index.ts');

// Heritage chain — extends/implements tree traversal
const tree = await ledger.getHeritageChain('UserService', 'src/services/user.ts');

🔧 Scan-only Mode

For CI pipelines or one-shot analysis, disable the file watcher:

const ledger = await Gildash.open({
  projectRoot: '/path/to/project',
  watchMode: false,        // no watcher, no heartbeat
});

// ... run your queries ...

await ledger.close({ cleanup: true });   // delete DB files after use

❌ Error Handling

Public methods return values directly and throw GildashError on failure. Use instanceof to branch:

import { Gildash, GildashError } from '@zipbul/gildash';

try {
  const symbols = ledger.searchSymbols({ text: 'foo' });
  console.log(`Found ${symbols.length} symbols`);
} catch (e) {
  if (e instanceof GildashError) {
    console.error(`[${e.type}] ${e.message}`);
  }
}

⚙️ Configuration

Gildash.open(options)

Option Type Default Description
projectRoot string Absolute path to project root (required)
extensions string[] ['.ts', '.mts', '.cts', '.tsx'] File extensions to index (add '.jsx' for JS-React files)
ignorePatterns string[] [] Glob patterns to exclude — applies to file indexing and package-boundary discovery (an ignored path's package.json does not become a boundary)
parseCacheCapacity number 500 LRU parse-cache capacity
logger Logger console Custom logger ({ error(...args): void; warn?(...args): void }) — warn is optional and used for notices like multi-boundary discovery
watchMode boolean true false disables the file watcher (scan-only mode)
semantic boolean false Enable tsc TypeChecker-backed semantic analysis
tsconfigs string[] Explicit tsconfig paths for the semantic layer (authoritative; skips auto-discovery). Use for non-standard config names or ambiguous layouts
semanticScope 'auto' | 'root' 'auto' 'auto' discovers every tsconfig.json under projectRoot (plus referenced projects) and resolves each file under its own config; 'root' uses only <projectRoot>/tsconfig.json (single-program, legacy)
plugins LanguagePlugin[] Language plugins for framework files (e.g. createVuePlugin() for Vue SFCs, createSveltePlugin() for Svelte). Plugin files are indexed at raw coordinates and answer semantic queries via virtual TS modules; add their extensions to extensions

Returns Promise<Gildash>. Throws GildashError on failure.

Monorepo (multi-tsconfig): with semantic: true and the default semanticScope: 'auto', gildash builds one tsc program per governing tsconfig.json, so files in a sub-project (e.g. an app with its own config) are analyzed under that project's compiler options. A tsconfig.json that cannot build degrades only its own files (see isFileInSemanticProgram) — open() no longer throws because one project's config is missing or broken.

Current limitations: changes to tsconfig files mid-session require reopening; cross-project two-file queries (e.g. isTypeAssignableTo across projects) and cross-project references are not resolved.


🔍 API Reference

Search

Method Returns Description
searchSymbols(query) SymbolSearchResult[] FTS5 full-text + exact / regex / decorator filters
searchRelations(query) StoredCodeRelation[] Filter by file, symbol, or relation type
searchAllSymbols(query) SymbolSearchResult[] Cross-project symbol search
searchAllRelations(query) StoredCodeRelation[] Cross-project relation search
listIndexedFiles(project?) FileRecord[] All indexed files for a project
getSymbolsByFile(filePath) SymbolSearchResult[] All symbols in a single file

Dependency Graph

Method Returns Description
getDependencies(filePath) string[] Files imported by filePath
getDependents(filePath) string[] Files that import filePath
getAffected(changedFiles) Promise<string[]> Transitive impact set
hasCycle(project?) Promise<boolean> Circular dependency check
getCyclePaths(project?, opts?) Promise<string[][]> All cycle paths (Tarjan SCC + Johnson's). opts.maxCycles limits results.
getImportGraph(project?) Promise<Map> Full adjacency list
getTransitiveDependencies(filePath) Promise<string[]> Forward transitive BFS (files this depends on)
getTransitiveDependents(filePath) Promise<string[]> Reverse transitive BFS (files that depend on this)

Analysis

Method Returns Description
getFullSymbol(name, filePath) FullSymbol | null Members, jsDoc, decorators, type info
getFileStats(filePath) FileStats Line count, symbol count, size
getFanMetrics(filePath) Promise<FanMetrics> Fan-in / fan-out coupling
getModuleInterface(filePath) ModuleInterface Public exports with metadata
getInternalRelations(filePath) StoredCodeRelation[] Intra-file relations
diffSymbols(before, after) SymbolDiff Snapshot diff (added / removed / modified)

Annotations

Method Returns Description
searchAnnotations(query) AnnotationSearchResult[] Search @tag value annotations in JSDoc, line, and block comments

Symbol Changelog

Method Returns Description
getSymbolChanges(since, opts?) SymbolChange[] Symbol-level change history (added/modified/removed/renamed/moved)
pruneChangelog(before) number Delete changelog entries older than the given date

Semantic (opt-in)

Requires semantic: true at open time.

By symbol name

Method Returns Description
getResolvedType(name, filePath) ResolvedType | null Resolved type via tsc TypeChecker
getSemanticReferences(name, filePath) SemanticReference[] All references to a symbol
getEnrichedReferences(name, filePath) EnrichedReference[] References enriched with writeKind / isAmbient / enclosingScope (dataflow)
getImplementations(name, filePath) Implementation[] Interface / abstract class implementations
getSemanticModuleInterface(filePath) SemanticModuleInterface Module exports with resolved types
isTypeAssignableTo(opts) boolean Whether one symbol's type is assignable to another's
isTypeAssignableToType(opts) boolean Whether a symbol's type is assignable to an arbitrary type string

By line/column or byte position

Method Returns Description
getResolvedTypeAt(filePath, line, column) ResolvedType | null Resolved type at a line/column
isTypeAssignableToAt(opts) boolean Assignability check between two line/column positions
getResolvedTypeAtPosition(filePath, position) ResolvedType | null Resolved type at a byte position
getResolvedTypesAtPositions(filePath, positions) Map<number, ResolvedType> Batch type lookup across positions
getSemanticReferencesAtPosition(filePath, position) SemanticReference[] References to the symbol at a position
getEnrichedReferencesAtPosition(filePath, position) EnrichedReference[] Enriched references to the symbol at a position
getFileBindings(filePath) FileBinding[] All bindings in a file (single pass), each with its in-file enriched references — O(identifiers) for dataflow
getFileBindingsBatch(files) Map<string, FileBinding[]> Batch getFileBindings over many in-memory { filePath, content } — one tsc rebuild for the whole batch
getStandaloneFileBindings(filePath, content) FileBinding[] Isolated O(file) bindings for a self-contained source — never touches the shared program (no cross-file/global resolution)
isFileInSemanticProgram(filePath) boolean Whether the file is in a healthy semantic program — true only when full type/reference/binding answers are available. false for files outside every discovered tsconfig, files whose tsconfig failed to build, or when semantic is disabled. Use to degrade per-file (e.g. fall back to getStandaloneFileBindings)
notifyFileChanged(filePath, content) void Register/replace an ad-hoc in-memory source (idempotent on identical content)
notifyFileDeleted(filePath) void Remove an ad-hoc in-memory source
getImplementationsAtPosition(filePath, position) Implementation[] Implementations of the symbol at a position
isTypeAssignableToAtPosition(opts) boolean Assignability check between two byte positions
isTypeAssignableToTypeAtPositions(opts) boolean Assignability check from a position to an arbitrary type string

By byte span (expression-level)

Resolve a type fact for the expression node exactly spanning a byte range. ByteSpan is { start: number; end: number } (0-based, half-open) and aligns 1:1 with oxc-parser node offsets. Each matches the node whose [getStart(), getEnd()) equals the span exactly, else returns null — no nearest-node fallback.

Method Returns Description
getExpressionTypeAtSpan(filePath, span) ResolvedType | null Type of the spanned expression — a call f() → result type, obj.m() → method return, obj.prop → property type (any expression, unlike the identifier-only position lookups)
isThenableAtSpan(filePath, span, opts?) boolean | null Whether the spanned expression's type is a thenable (callable then with ≥1 param); recurses union/intersection, excludes any. opts.anyConstituent (default true): a union is thenable if some member is
getContextualCallReturnsAtSpan(filePath, span) ResolvedType[] | null Return types of the contextual type's call signatures at an argument span (overload-selected, undefined/null stripped). [] = not a callable slot; null = no contextual type
isTypeAssignableToTypeAtSpan(filePath, span, target, opts?) boolean | null Whether the spanned expression's type is assignable to a type string target (e.g. 'Error'). Resolves any expression (so new CustomError() works, unlike the position resolver). opts.anyConstituent for union sources

File-level / utilities / diagnostics

Method Returns Description
getFileTypes(filePath) Map<number, ResolvedType> All resolved types for symbols in a file
getSymbolNode(filePath, position) SymbolNode | null Underlying symbol-graph node at a position
getBaseTypes(filePath, position) ResolvedType[] | null Direct base types of the symbol at a position
lineColumnToPosition(filePath, line, column) number | null Convert line/column to byte offset
findNamePosition(filePath, declarationPos, name) number | null Locate an identifier's byte offset within a declaration
getSemanticDiagnostics(filePath, opts?) SemanticDiagnostic[] tsc diagnostics for a file

getFullSymbol() automatically enriches the result with a resolvedType field when semantic is enabled. searchSymbols({ resolvedType }) filters symbols by their resolved type string.

Advanced

Method Returns Description
findPattern(pattern, opts?) Promise<PatternMatch[]> AST structural search (ast-grep)
resolveSymbol(name, filePath) ResolvedSymbol Follow re-export chain to original; "default" resolves to the local default definition
getHeritageChain(name, filePath) Promise<HeritageNode> extends / implements tree
batchParse(filePaths, opts?) Promise<BatchParseResult> Concurrent multi-file parsing. Returns { parsed, failures }. opts: oxc-parser ParserOptions.

Lifecycle & Low-level

Method Returns Description
reindex() Promise<IndexResult> Force full re-index (owner only)
onIndexed(callback) () => void Subscribe to index-complete events
onFileChanged(callback) () => void Subscribe to file-change events
onError(callback) () => void Subscribe to error events
onRoleChanged(callback) () => void Subscribe to owner/reader role-change events
parseSource(filePath, src, opts?) ParsedFile Parse & cache a single file. opts: oxc-parser ParserOptions.
extractSymbols(parsed) ExtractedSymbol[] Extract symbols from parsed AST
extractRelations(parsed) CodeRelation[] Extract relations from parsed AST

| getParsedAst(filePath) | ParsedFile \| undefined | Cached AST lookup (read-only) | | getFileInfo(filePath) | FileRecord \| null | File metadata (hash, mtime, size) | | getStats(project?) | SymbolStats | Symbol / file count statistics | | projects | ProjectBoundary[] | Discovered project boundaries (always includes a root boundary) | | defaultProject | string | The root boundary — the project that scoped queries target when query.project is omitted | | close(opts?) | Promise<void> | Shutdown (pass { cleanup: true } to delete DB) |

AST Primitives

gildash exposes the underlying oxc-parser AST surface and a set of helpers for consumers building their own static analysis on top of the same parser. These are standalone exports — no Gildash instance required — and follow the upstream oxc-parser / oxc-walker semver.

Parsing & extraction

Export Returns Description
parseSource(file, src, opts?) Result<ParsedFile, GildashError> Parse a single source string
extractSymbols(parsed) ExtractedSymbol[] Symbols from a parsed AST
extractRelations(ast, file, ...) CodeRelation[] Relations from a parsed AST

Traversal

Re-exported from oxc-walker and oxc-parser:

Export Description
walk(node, { enter, leave }) Generic preorder walker. enter receives (node, parent, ctx); call this.skip() to prune the subtree.
parseAndWalk(code, file, cb) Parse + walk in one call
ScopeTracker Identifier-declaration tracker that integrates with walk
Visitor Per-node-type callback object form (new Visitor({ CallExpression(...) }))
visitorKeys Record<string, string[]> — child key list per node type

Type predicates over Node

is namespace — primary surface

is.X(node) returns true when node.type === 'X' and narrows the type to Node & { type: 'X' }. Covers every Node['type'] discriminator that oxc-parser exposes — and any future discriminator the upstream union adds — without per-type hand-written code.

import { parseSource, walk, is } from '@zipbul/gildash';

const parsed = parseSource('a.ts', 'class A {}; foo();');
if (!('stack' in parsed)) {
  walk(parsed.program, {
    enter(node) {
      if (is.CallExpression(node)) console.log(node.arguments);
      if (is.ClassDeclaration(node)) console.log(node.id?.name);
      if (is.ImportDeclaration(node)) console.log(node.source.value);
    },
  });
}

Predicate functions are cached by discriminator, so is.CallExpression === is.CallExpression holds across calls — safe to pass directly to Array#filter. Only PascalCase property names mint predicates; symbols, lowercase typos (is.callExpression), and Object-prototype names (then, toString, toJSON, valueOf, constructor, hasOwnProperty) fall through so String(is), JSON.stringify(is), and await Promise.resolve(is) behave like a plain object. Calling a predicate with null / undefined returns false.

Node & { type: K } is used at the type level (rather than Extract<Node, { type: K }>) so that fields on backing interfaces with multi-literal type unions — Function (4-way), Class (2-way) — remain accessible inside the narrowed branch.

Hand-written predicates — kept for documented runtime semantics

These remain exported alongside is.X because their JSDoc encodes runtime semantics that the bare discriminator does not. Use them when the caveat matters at the call site; otherwise prefer is.X.

Predicate Why hand-written
isFunctionDeclaration / isFunctionExpression Function interface caveat — TSDeclareFunction / TSEmptyBodyFunctionExpression structurally satisfy the interface but are excluded by the runtime discriminator check.
isIdentifier 6-way collision: IdentifierName, IdentifierReference, BindingIdentifier, LabelIdentifier, TSThisParameter, TSIndexSignatureName. All expose .name.
isMemberExpression 3-way collision: ComputedMemberExpression, StaticMemberExpression, PrivateFieldExpression. All expose .object.
isTSQualifiedName 2-way collision: TSQualifiedName, TSImportTypeQualifiedName. Both expose .left / .right (different shapes).
isFunctionNode Union shorthand for FunctionDeclaration | FunctionExpression | ArrowFunctionExpression. The only published predicate that narrows to a union of multiple discriminators — no is.X equivalent.

isArrowFunctionExpression, isAssignmentExpression, isCallExpression, isVariableDeclaration are also exported for backward compatibility; pointwise-equivalent to is.X.

Migration from named predicates
Named import is-namespace equivalent
isArrowFunctionExpression(n) is.ArrowFunctionExpression(n)
isAssignmentExpression(n) is.AssignmentExpression(n)
isCallExpression(n) is.CallExpression(n)
isFunctionDeclaration(n) is.FunctionDeclaration(n)
isFunctionExpression(n) is.FunctionExpression(n)
isIdentifier(n) is.Identifier(n)
isMemberExpression(n) is.MemberExpression(n)
isTSQualifiedName(n) is.TSQualifiedName(n)
isVariableDeclaration(n) is.VariableDeclaration(n)
isFunctionNode(n) (no is.X equivalent — keep the named import)
Composing the is namespace with the index layer

Gildash.searchSymbols (semantic / indexed view) and parseSource + walk (raw / positional view) are designed as two layers that compose. Use searchSymbols to locate the symbol, then re-enter the raw AST for the same file to drill into sub-expressions and slice source by node.start / node.end:

import { Gildash, parseSource, walk, is } from '@zipbul/gildash';

const ledger = await Gildash.open({ projectRoot });

// 1. Index layer: locate the symbol
const [sym] = ledger.searchSymbols({ text: 'target', exact: true, kind: 'function' });
if (!sym) return;

// 2. Raw layer: re-parse the file (gildash also caches it via `getParsedAst`)
const src = await Bun.file(sym.filePath).text();
const parsed = parseSource(sym.filePath, src);
if ('stack' in parsed) throw parsed;

// 3. Compose: outer-node walk + name match → drill into the matched subtree
walk(parsed.program, {
  enter(node) {
    if (is.FunctionDeclaration(node) && node.id?.name === sym.name) {
      // Inside the symbol's subtree — use `is.X` predicates and raw byte
      // offsets directly. `src.slice(n.start, n.end)` recovers source text.
      if (node.body) {
        walk(node.body, {
          enter(inner) {
            if (is.CallExpression(inner)) {
              const calleeText = src.slice(inner.callee.start, inner.callee.end);
              const argSpans = inner.arguments.map((a) => [a.start, a.end] as const);
              // ...transform / collect / inject
              void calleeText;
              void argSpans;
            }
          },
        });
      }
      this.skip();
    }
  },
});

This pattern keeps the two layers cleanly separated: the indexed view answers which symbols and where (sym.filePath, sym.span); the raw view answers what shape and what byte range (is.X(node), node.start / node.end).

AST utility types

Program, Node, VisitorObject, WalkOptions, WalkerEnter, WalkerLeave, WalkerCallbackContext, ScopeTrackerNode, ScopeTrackerOptions, SourcePosition, SourceSpan, ParsedFile, IsNamespace, NodeTypePredicate, plus buildLineOffsets / getLineColumn for byte ↔ line/column conversion.


Type Definitions
// ── Search ──────────────────────────────────────────────────────────────

interface SymbolSearchQuery {
  text?: string;        // FTS5 full-text query
  exact?: boolean;      // exact name match (not prefix)
  kind?: SymbolKind;    // 'function' | 'method' | 'class' | 'variable' | 'type' | 'interface' | 'enum' | 'namespace' | 'property'
  filePath?: string;
  isExported?: boolean;
  project?: string;
  limit?: number;       // when omitted, no limit is applied
  decorator?: string;   // e.g. 'Injectable'
  regex?: string;       // regex applied to symbol name
  resolvedType?: string;  // semantic-layer filter; requires `semantic: true`
}

interface SymbolSearchResult {
  id: number;
  filePath: string;
  kind: SymbolKind;
  name: string;                    // qualified for members, e.g. "ClassName.methodName"
  memberName: string | null;       // unqualified member name, or null for top-level
  span: { start: { line: number; column: number }; end: { line: number; column: number } };
  isExported: boolean;
  signature: string | null;
  fingerprint: string | null;
  detail: SymbolDetail;            // typed; see below
}

interface SymbolDetail {
  parameters?: Parameter[];
  returnType?: string;
  heritage?: Array<{ kind: 'extends' | 'implements'; name: string; typeArguments?: string[] }>;
  decorators?: Decorator[];
  typeParameters?: string[];
  modifiers?: Modifier[];
  initializer?: ExpressionValue;
  members?: Array<{
    name: string;
    kind: string;
    type?: string;
    visibility?: string;
    isStatic?: boolean;
    isReadonly?: boolean;
    initializer?: ExpressionValue;
    decorators?: Decorator[];
  }>;
  jsDoc?: JsDocBlock;
  isDefault?: boolean;             // true when this symbol is the module's default export (any form)
}

interface Parameter {
  name: string;
  type?: string;                   // type annotation as source text
  typeImportSource?: string;       // import specifier when the type is imported
  isOptional: boolean;
  defaultValue?: string;
  decorators?: Decorator[];
}

interface Decorator {
  name: string;
  arguments?: ExpressionValue[];   // structured decorator call arguments
}

type Modifier =
  | 'async' | 'static' | 'abstract' | 'readonly'
  | 'private' | 'protected' | 'public'
  | 'override' | 'declare' | 'const'
  | 'accessor';                    // TC39 auto-accessor

interface RelationSearchQuery {
  srcFilePath?: string;
  srcSymbolName?: string;
  dstFilePath?: string;
  dstSymbolName?: string;
  dstProject?: string;            // filter by destination project
  /** Glob (Bun.Glob) for source file path; mutually exclusive with srcFilePath. */
  srcFilePathPattern?: string;
  /** Glob (Bun.Glob) for destination file path; mutually exclusive with dstFilePath. */
  dstFilePathPattern?: string;
  type?: 'imports' | 'type-references' | 're-exports' | 'calls' | 'extends' | 'implements';
  project?: string;
  /** Filter by raw import specifier (e.g. `'./foo'`, `'lodash'`). */
  specifier?: string;
  /** Filter by external (bare specifier) flag. */
  isExternal?: boolean;
  limit?: number;                 // when omitted, no limit is applied
}

interface CodeRelation {
  type: 'imports' | 'type-references' | 're-exports' | 'calls' | 'extends' | 'implements';
  srcFilePath: string;
  srcSymbolName: string | null;
  /** `null` when the import target could not be resolved (external package, missing file). */
  dstFilePath: string | null;
  dstSymbolName: string | null;
  metaJson?: string;
  meta?: Record<string, unknown>;   // auto-parsed from metaJson
  /**
   * Verbatim module specifier text as written in the source (`'./foo'`,
   * `'@zipbul/core'`, `'lodash'`). Always present on module-source-bearing
   * relations (`'imports'`, `'re-exports'`, `'type-references'`, dynamic
   * `import()`, `require()`) regardless of `dstFilePath` resolution.
   * Absent on `'calls'` / `'extends'` / `'implements'`.
   */
  specifier?: string;
}

/** CodeRelation enriched with destination-project identifier and external flag. */
interface StoredCodeRelation extends Omit<CodeRelation, 'specifier'> {
  /** Destination project, or `null` for cross-project / unresolved relations. */
  dstProject: string | null;
  /** Whether the relation targets an external (bare-specifier) package. */
  isExternal: boolean;
  /** Verbatim source specifier (see {@link CodeRelation.specifier}); `null` only on `'calls'` / `'extends'` / `'implements'`. */
  specifier: string | null;
}

// ── Analysis ────────────────────────────────────────────────────────────

interface FullSymbol extends SymbolSearchResult {
  members?: SymbolDetail['members'];
  jsDoc?: JsDocBlock;
  parameters?: Parameter[];
  returnType?: string;
  heritage?: SymbolDetail['heritage'];
  decorators?: Decorator[];
  typeParameters?: string[];
  initializer?: ExpressionValue;
  /** Resolved type from the Semantic Layer (available when `semantic: true`). */
  resolvedType?: ResolvedType;
}

interface FileStats {
  filePath: string;
  lineCount: number;
  symbolCount: number;
  relationCount: number;
  size: number;
  exportedSymbolCount: number;
}

interface FanMetrics {
  filePath: string;
  fanIn: number;    // files importing this file
  fanOut: number;   // files this file imports
}

interface ModuleInterface {
  filePath: string;
  exports: Array<{
    name: string;
    kind: SymbolKind;
    parameters?: string;
    returnType?: string;
    jsDoc?: string;
    isDefault?: boolean;   // true for the module's default export (named "default" when anonymous)
  }>;
}

interface SymbolDiff {
  added: SymbolSearchResult[];
  removed: SymbolSearchResult[];
  modified: Array<{ before: SymbolSearchResult; after: SymbolSearchResult }>;
}

// ── Advanced ────────────────────────────────────────────────────────────

interface PatternMatch {
  filePath: string;
  startLine: number;
  endLine: number;
  matchedText: string;
}

interface ResolvedSymbol {
  originalName: string;
  originalFilePath: string;
  reExportChain: Array<{ filePath: string; exportedAs: string }>;
  circular: boolean;    // true when re-export chain contains a cycle
}

interface HeritageNode {
  symbolName: string;
  filePath: string;
  kind?: 'extends' | 'implements';
  children: HeritageNode[];
}

// ── Indexing ────────────────────────────────────────────────────────────

interface IndexResult {
  indexedFiles: number;
  removedFiles: number;
  totalSymbols: number;
  totalRelations: number;
  totalAnnotations: number;
  durationMs: number;
  changedFiles: string[];
  deletedFiles: string[];
  failedFiles: string[];
  /** Symbol-level diff against previous index state. */
  changedSymbols: {
    added: Array<{ name: string; filePath: string; kind: string; isExported: boolean }>;
    modified: Array<{ name: string; filePath: string; kind: string; isExported: boolean }>;
    removed: Array<{ name: string; filePath: string; kind: string; isExported: boolean }>;
  };
  /** Symbols renamed (matched via structural fingerprint). */
  renamedSymbols: Array<{
    oldName: string;
    newName: string;
    filePath: string;
    kind: string;
    isExported: boolean;
  }>;
  /** Symbols that moved to a different file (incremental only; matched via fingerprint). */
  movedSymbols: Array<{
    name: string;
    oldFilePath: string;
    newFilePath: string;
    kind: string;
    isExported: boolean;
  }>;
  /** Relation-level diff against previous index state. */
  changedRelations: {
    added: Array<{
      type: string;
      srcFilePath: string;
      dstFilePath: string | null;
      srcSymbolName: string | null;
      dstSymbolName: string | null;
      dstProject: string | null;
      metaJson: string | null;
    }>;
    removed: Array<{
      type: string;
      srcFilePath: string;
      dstFilePath: string | null;
      srcSymbolName: string | null;
      dstSymbolName: string | null;
      dstProject: string | null;
      metaJson: string | null;
    }>;
  };
}

interface FileRecord {
  project: string;
  filePath: string;
  mtimeMs: number;
  size: number;
  contentHash: string;
  updatedAt: string;
  lineCount?: number | null;
}

// ── Annotations ─────────────────────────────────────────────────────────

interface AnnotationSearchQuery {
  text?: string;       // FTS5 full-text query
  tag?: string;        // exact tag name (e.g. 'deprecated', 'todo')
  filePath?: string;
  symbolName?: string;
  source?: 'jsdoc' | 'line' | 'block';
  project?: string;
  limit?: number;      // when omitted, no limit is applied
}

interface AnnotationSearchResult {
  tag: string;
  value: string;
  source: 'jsdoc' | 'line' | 'block';
  filePath: string;
  symbolName: string | null;
  span: { start: { line: number; column: number }; end: { line: number; column: number } };
}

// ── Symbol Changelog ────────────────────────────────────────────────────

type SymbolChangeType = 'added' | 'modified' | 'removed' | 'renamed' | 'moved';

interface SymbolChange {
  changeType: SymbolChangeType;
  symbolName: string;
  symbolKind: string;
  filePath: string;
  oldName: string | null;       // set for 'renamed'
  oldFilePath: string | null;   // set for 'moved'
  fingerprint: string | null;
  changedAt: string;
  isFullIndex: boolean;
  indexRunId: string;
}

interface SymbolChangeQueryOptions {
  symbolName?: string;
  changeTypes?: SymbolChangeType[];
  filePath?: string;
  includeFullIndex?: boolean;   // default: false
  indexRunId?: string;
  afterId?: number;             // pagination cursor
  limit?: number;               // default: 1000
  project?: string;
}

// ── Errors ──────────────────────────────────────────────────────────────

class GildashError extends Error {
  readonly type: GildashErrorType;   // see Error Types table below
  readonly message: string;
  readonly cause?: unknown;          // inherited from Error
}

⚠️ Error Types

Type When
watcher File watcher start / stop failure
parse AST parsing failure
extract Symbol / relation extraction failure
index Indexing pipeline failure
store Database operation failure
search Search query failure
closed Operation on a closed instance
semantic Semantic layer not enabled or tsc error
validation Invalid input (e.g. missing node_modules package)
close Error during shutdown

🏗 Architecture

Gildash (Facade)
├── Parser      — oxc-parser-based TypeScript AST parsing
├── Extractor   — Symbol & relation extraction (imports, re-exports, type-refs, calls, heritage)
├── Store       — bun:sqlite + drizzle-orm (files · symbols · relations · FTS5) at `.gildash/gildash.db`
├── Indexer     — File change → parse → extract → store pipeline, symbol-level diff
├── Search      — FTS + regex + decorator search, relation queries, dependency graph, ast-grep
├── Semantic    — tsc TypeChecker integration (opt-in): types, references, implementations
└── Watcher     — @parcel/watcher + owner/reader role management

Owner / Reader Pattern

When multiple processes share the same SQLite database, gildash enforces a single-writer guarantee:

  • Owner — Runs the file watcher, performs indexing, sends a heartbeat every 30 s
  • Reader — Read-only access; polls owner health every 15 s and self-promotes if the owner goes stale (60 s threshold)

📄 License

MIT © zipbul

About

Watch, parse, index, and search codebases — shared infrastructure for static analysis tools.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages