npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zipbul/gildash

v0.28.0

Published

TypeScript code indexing and dependency graph engine for Bun

Downloads

2,582

Readme

@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'] | File extensions to index | | ignorePatterns | string[] | [] | Glob patterns to exclude | | parseCacheCapacity | number | 500 | LRU parse-cache capacity | | logger | Logger | console | Custom logger ({ error(...args): void }) | | watchMode | boolean | true | false disables the file watcher (scan-only mode) | | semantic | boolean | false | Enable tsc TypeChecker-backed semantic analysis |

Returns Promise<Gildash>. Throws GildashError on failure.

Note: semantic: true requires a tsconfig.json in the project root. If not found, Gildash.open() throws a GildashError.

🔍 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 | | 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 | | 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 |

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 | | 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 | | 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.

// ── 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;
}

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;
  }>;
}

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