@maxgfr/codeindex 2.12.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/docs/MIGRATION.md +37 -0
- package/docs/SEMANTIC.md +12 -5
- package/package.json +1 -1
- package/scripts/engine.d.mts +12 -5
- package/scripts/engine.mjs +76 -44
- package/src/ast/extract.ts +7 -5
- package/src/callers.ts +0 -0
- package/src/embed/model.ts +23 -14
- package/src/engine-cli.ts +19 -5
- package/src/engine.ts +1 -1
- package/src/extract/code.ts +9 -5
- package/src/lang/js-ts.ts +1 -1
- package/src/mcp.ts +30 -3
- package/src/scan.ts +9 -1
- package/src/types.ts +6 -3
- package/src/walk.ts +15 -2
package/README.md
CHANGED
|
@@ -129,9 +129,13 @@ and the `embeddings.bin` artifact are byte-identical across builds and platforms
|
|
|
129
129
|
It is **opt-in by asset**: with no model on disk the engine silently stays
|
|
130
130
|
lexical, and `--semantic` without a model returns lexical results on **exit 0**
|
|
131
131
|
(a stderr note only). Models are **never** shipped in the package; a model is
|
|
132
|
-
resolved from `CODEINDEX_EMBED_DIR` or `<repo>/.codeindex/models/`.
|
|
132
|
+
resolved from `CODEINDEX_EMBED_DIR` or `<repo>/.codeindex/models/`. Getting one
|
|
133
|
+
is zero-config: `codeindex embed pull` fetches the official `embed-model-v1`
|
|
134
|
+
release asset, sha256-verified before anything is written.
|
|
133
135
|
|
|
134
136
|
```sh
|
|
137
|
+
codeindex embed pull --repo . # fetch the official model asset into
|
|
138
|
+
# CODEINDEX_EMBED_DIR (or <repo>/.codeindex/models/); sha256-verified
|
|
135
139
|
codeindex embed status --repo . # effective mode + reachability (JSON)
|
|
136
140
|
codeindex embed build --repo . --out .codeindex # write embeddings.bin
|
|
137
141
|
codeindex search "http client retry" --repo . --semantic
|
package/docs/MIGRATION.md
CHANGED
|
@@ -93,6 +93,43 @@ embeddings live in a separate `embeddings.bin` sidecar keyed by a dedicated
|
|
|
93
93
|
`--semantic` without a model degrades to lexical results on **exit 0** (a stderr
|
|
94
94
|
note only) — so wiring it on is safe before an asset exists.
|
|
95
95
|
|
|
96
|
+
## v2.12.0 — two return-shape changes (check your call sites)
|
|
97
|
+
|
|
98
|
+
Not additive: two public return shapes changed in this release without a
|
|
99
|
+
compat flag, so a consumer re-pinning across v2.12.0 must check both call
|
|
100
|
+
sites. Artifact schemas are untouched (`SCHEMA_VERSION` / `EMBED_VERSION`
|
|
101
|
+
unchanged) — this is API shape only.
|
|
102
|
+
|
|
103
|
+
- `resolveEmbedPullUrl()` now returns an `EmbedPullTarget`
|
|
104
|
+
(`{ url: string; sha256?: string }`) — it previously returned
|
|
105
|
+
`string | undefined`. It always resolves: `CODEINDEX_EMBED_URL` wins
|
|
106
|
+
outright and carries **no** `sha256` (a custom mirror keeps the
|
|
107
|
+
un-verified behavior); with no env it falls back to the built-in official
|
|
108
|
+
asset **with** its pinned `sha256` so `embed pull` verifies the default
|
|
109
|
+
download. Replace `const url = resolveEmbedPullUrl()` with
|
|
110
|
+
`const { url, sha256 } = resolveEmbedPullUrl()`. The `EmbedPullTarget`
|
|
111
|
+
type is exported from the barrel.
|
|
112
|
+
- MCP `search` with `semantic: true` now returns
|
|
113
|
+
`{ results, tier, degradedReason? }` — it previously returned the bare
|
|
114
|
+
ranked array. `tier` is `"endpoint" | "static" | "lexical"` and
|
|
115
|
+
`degradedReason` is present only when the semantic tier degraded to
|
|
116
|
+
lexical, so a caller can tell "fusion happened" apart from "degraded".
|
|
117
|
+
Plain lexical `search` (no `semantic: true`) still returns the bare
|
|
118
|
+
array, byte-compatible with existing consumers.
|
|
119
|
+
|
|
120
|
+
## v2.13.0 — `.codeindex` excluded from the walk
|
|
121
|
+
|
|
122
|
+
`.codeindex/` — the engine's own output directory (index artifacts, pulled
|
|
123
|
+
models, MCP memories) — joined `IGNORE_DIRS`, so `walk`/`scanRepo` no longer
|
|
124
|
+
descend into it and memories stop entering BM25/embedding results (previously
|
|
125
|
+
every `write_memory` also churned the scan fingerprint, busting the MCP
|
|
126
|
+
server's memoized embedding index). Access memories through the MCP memory
|
|
127
|
+
tools (`read_memory` / `list_memories` / `delete_memory`), never `search`.
|
|
128
|
+
This is a file-set change only — extraction shape and `SCHEMA_VERSION` are
|
|
129
|
+
untouched, so **no re-pin is required**; a consumer that deliberately wants
|
|
130
|
+
`.codeindex` walked can pass `ignoreDirs` (replace semantics, also new in
|
|
131
|
+
this release) with its own set.
|
|
132
|
+
|
|
96
133
|
## Per-skill mapping (what to replace with what)
|
|
97
134
|
|
|
98
135
|
| Skill | Replace | With (engine export) |
|
package/docs/SEMANTIC.md
CHANGED
|
@@ -78,7 +78,7 @@ rows at one global scale. The download is **sha256-verified** against
|
|
|
78
78
|
`EMBED_ASSET_SHA256` (`src/embed/model.ts`) before it is written; a mismatch
|
|
79
79
|
fails the pull and writes nothing. The asset is **never** committed to git or
|
|
80
80
|
shipped in the npm tarball — it lives only in the release. Reproduce it
|
|
81
|
-
byte-for-byte with the pinned toolchain in [`scripts/embed-asset/`](
|
|
81
|
+
byte-for-byte with the pinned toolchain in [`scripts/embed-asset/`](https://github.com/maxgfr/codeindex/tree/main/scripts/embed-asset).
|
|
82
82
|
|
|
83
83
|
## CLI
|
|
84
84
|
|
|
@@ -110,13 +110,20 @@ codeindex search "http client retry" --repo <dir> --semantic
|
|
|
110
110
|
|
|
111
111
|
## Artifact: `embeddings.bin`
|
|
112
112
|
|
|
113
|
+
### Layout (the `deserializeEmbeddings` contract)
|
|
114
|
+
|
|
113
115
|
```
|
|
114
|
-
"CIE1"
|
|
115
|
-
uint32 LE
|
|
116
|
-
UTF-8 JSON header
|
|
117
|
-
int8 body
|
|
116
|
+
offset 0 "CIE1" 4-byte ASCII magic
|
|
117
|
+
offset 4 uint32 LE header length (headerLen)
|
|
118
|
+
offset 8 UTF-8 JSON header { embedVersion, modelId, dim, count, records:[{file,symbol,line}] }
|
|
119
|
+
offset 8+headerLen int8 body count × dim signed bytes (row-major)
|
|
118
120
|
```
|
|
119
121
|
|
|
122
|
+
This is exactly what `deserializeEmbeddings` (`src/embed/index.ts`) reads back:
|
|
123
|
+
it throws on a bad magic (a corrupt or foreign file fails loudly instead of
|
|
124
|
+
being misread), parses the JSON header, and slices the packed body at
|
|
125
|
+
`8 + headerLen`.
|
|
126
|
+
|
|
120
127
|
The header carries **no absolute path and no timestamp**; records follow scan
|
|
121
128
|
order (files sorted by `rel`, symbols in declaration order). Two builds of an
|
|
122
129
|
unchanged repo produce byte-identical `embeddings.bin`. A dedicated
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxgfr/codeindex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "Self-contained, deterministic repo-indexing engine: walk + language detection + symbol/import extraction (tree-sitter AST with regex fallback) + import resolution + typed cross-file link-graph + analytics. Ships as a single zero-dependency engine.mjs that consumer tools vendor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|
package/scripts/engine.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
declare const ENGINE_VERSION = "2.
|
|
1
|
+
declare const ENGINE_VERSION = "2.13.0";
|
|
2
2
|
declare const SCHEMA_VERSION = 4;
|
|
3
|
-
declare const EXTRACTOR_VERSION =
|
|
3
|
+
declare const EXTRACTOR_VERSION = 10;
|
|
4
4
|
type FileKind = "code" | "doc" | "config" | "asset" | "other";
|
|
5
5
|
type EdgeKind = "contains" | "doc-link" | "import" | "call" | "use" | "mention";
|
|
6
6
|
type Tier = 0 | 1 | 2;
|
|
@@ -121,6 +121,7 @@ interface WalkOptions {
|
|
|
121
121
|
maxFileBytes?: number;
|
|
122
122
|
maxFiles?: number;
|
|
123
123
|
gitignore?: boolean;
|
|
124
|
+
ignoreDirs?: string[];
|
|
124
125
|
}
|
|
125
126
|
interface WalkedFile {
|
|
126
127
|
rel: string;
|
|
@@ -153,8 +154,10 @@ interface ScanOptions {
|
|
|
153
154
|
exclude?: string[];
|
|
154
155
|
scope?: string;
|
|
155
156
|
gitignore?: boolean;
|
|
157
|
+
ignoreDirs?: string[];
|
|
156
158
|
maxBytes?: number;
|
|
157
159
|
maxFiles?: number;
|
|
160
|
+
maxCallsPerFile?: number;
|
|
158
161
|
out?: string;
|
|
159
162
|
cache?: Map<string, {
|
|
160
163
|
hash: string;
|
|
@@ -202,7 +205,9 @@ interface CodeInfo {
|
|
|
202
205
|
}[];
|
|
203
206
|
importedNames?: string[];
|
|
204
207
|
}
|
|
205
|
-
declare function extractCode(rel: string, ext: string, content: string
|
|
208
|
+
declare function extractCode(rel: string, ext: string, content: string, opts?: {
|
|
209
|
+
maxCallsPerFile?: number;
|
|
210
|
+
}): CodeInfo;
|
|
206
211
|
|
|
207
212
|
interface MarkdownInfo {
|
|
208
213
|
title?: string;
|
|
@@ -229,7 +234,9 @@ interface AstResult {
|
|
|
229
234
|
}[];
|
|
230
235
|
importedNames: string[];
|
|
231
236
|
}
|
|
232
|
-
declare function extractAst(rel: string, ext: string, content: string
|
|
237
|
+
declare function extractAst(rel: string, ext: string, content: string, opts?: {
|
|
238
|
+
maxCalls?: number;
|
|
239
|
+
}): AstResult | undefined;
|
|
233
240
|
|
|
234
241
|
type Resolution = {
|
|
235
242
|
kind: "resolved";
|
|
@@ -705,4 +712,4 @@ declare function rrf<T>(lists: T[][], keyOf: (item: T) => string, k?: number): M
|
|
|
705
712
|
|
|
706
713
|
declare function runCli(argv: string[]): Promise<void>;
|
|
707
714
|
|
|
708
|
-
export { type ArchRule, type BuildIndexOptions, type BuiltinRule, type CallerEntry, type CallerIndex, type CallerIndexOptions, type CallerSite, type ChangeCoupling, type CodeInfo, type CodeSymbol, type CouplingOptions, DEFAULT_MAX_FILES, type DeadSymbol, type DiffFile, type DiffSpec, EMBED_VERSION, ENGINE_VERSION, EXTRACTOR_VERSION, type Edge, type EdgeKind, type EditResult, type EmbedEndpointOptions, type EmbeddingIndex, type EmbeddingRecord, type EmbeddingUnit, type FileCategory, type FileKind, type FileNode, type FileRecord, type FindSymbolOptions, type ForbiddenEdgeRule, type Graph, type GrepOptions, type Hotspot, type Hunk, type IgnoreRule, type IndexArtifacts, MARKDOWN_EXT, type MarkdownInfo, type MermaidOptions, type ModuleInfo, type ModuleNode, type RawCallerIndex, type RawCallerSite, type RawRef, type RenderScipOptions, type RepoMapOptions, type RepoScan, type Resolution, type ResolveContext, type RiskHotspot, type RuleSeverity, type RuleViolation, SCHEMA_VERSION, type ScanOptions, type SearchHit, type SearchOptions, type SearchResult, type SemanticSearchOptions, type SemanticSearchResult, type ShResult, type StaticEmbedModel, type SurpriseEdge, type SymbolComplexity, type SymbolIndex, type SymbolMatch, type SymbolReferences, type TestMap, type Tier, type WalkOptions, type WalkResult, type WalkedFile, type WorkspaceInfo, type WorkspaceKind, type WorkspacePackage, allGrammarKeys, applyCentrality, basicTokenize, betweennessOf, buildCallerIndex, buildEmbeddingIndex, buildEndpointIndex, buildGraph, buildIndexArtifacts, buildModules, buildRawCallerIndex, buildResolveContext, buildSymbolIndex, byKey, byStr, categorize, changeCoupling, changedSince, checkRules, classify, clip, clipInline, communityOf, compileGlobs, complexityOfSource, computeImportPairs, computeSurprises, computeSymbolRefs, computeTestMap, deleteMemory, deserializeEmbeddings, detectCommunities, detectWorkspaces, diffFiles, diffHunks, embedEndpointUrl, embedViaEndpoint, embeddingUnits, enclosingSymbol, encode, encodeQueryViaEndpoint, ensureGrammars, escapeRegExp, extToLang, extractAst, extractCode, extractMarkdown, extractSymbols, findDeadCode, findReferences, findSymbol, foldText, gitChurn, grammarKeyForExt, grammarReady, grepRepo, hasEmbedModel, have, headCommit, healthzUrl, insertAfterSymbol, insertBeforeSymbol, intDot, isCode, isDoc, isGitWorktree, isIgnored, isSurprising, isTestFile, isTestPath, keywords, languageOf, listMemories, loadEmbedModel, pagerankOf, parseGitignore, parseRules, probeEndpoint, quantize, rankHotspots, rankedKeywords, readMemory, readText, renderGraphJson, renderMermaid, renderRepoMap, renderScip, renderSymbolsJson, replaceSymbolBody, resolveBaseRef, resolveCallEdges, resolveDocLink, resolveEmbedEndpoint, resolveEmbedModelDir, resolveEmbedPullUrl, resolveImport, resolveUniqueSymbol, riskHotspots, roundHalfToEven, rrf, runCli, runMcpServer, scanRepo, searchIndex, searchSemantic, serializeEmbeddings, sh, sha1, shortHash, slugify, subtokens, symbolComplexity, symbolsOverview, testsForModule, tierForPath, tokenize, uniqueSymbolDefs, untestedModules, untrackedFiles, walk, wordpiece, writeMemory };
|
|
715
|
+
export { type ArchRule, type BuildIndexOptions, type BuiltinRule, type CallerEntry, type CallerIndex, type CallerIndexOptions, type CallerSite, type ChangeCoupling, type CodeInfo, type CodeSymbol, type CouplingOptions, DEFAULT_MAX_FILES, type DeadSymbol, type DiffFile, type DiffSpec, EMBED_VERSION, ENGINE_VERSION, EXTRACTOR_VERSION, type Edge, type EdgeKind, type EditResult, type EmbedEndpointOptions, type EmbedPullTarget, type EmbeddingIndex, type EmbeddingRecord, type EmbeddingUnit, type FileCategory, type FileKind, type FileNode, type FileRecord, type FindSymbolOptions, type ForbiddenEdgeRule, type Graph, type GrepOptions, type Hotspot, type Hunk, type IgnoreRule, type IndexArtifacts, MARKDOWN_EXT, type MarkdownInfo, type MermaidOptions, type ModuleInfo, type ModuleNode, type RawCallerIndex, type RawCallerSite, type RawRef, type RenderScipOptions, type RepoMapOptions, type RepoScan, type Resolution, type ResolveContext, type RiskHotspot, type RuleSeverity, type RuleViolation, SCHEMA_VERSION, type ScanOptions, type SearchHit, type SearchOptions, type SearchResult, type SemanticSearchOptions, type SemanticSearchResult, type ShResult, type StaticEmbedModel, type SurpriseEdge, type SymbolComplexity, type SymbolIndex, type SymbolMatch, type SymbolReferences, type TestMap, type Tier, type WalkOptions, type WalkResult, type WalkedFile, type WorkspaceInfo, type WorkspaceKind, type WorkspacePackage, allGrammarKeys, applyCentrality, basicTokenize, betweennessOf, buildCallerIndex, buildEmbeddingIndex, buildEndpointIndex, buildGraph, buildIndexArtifacts, buildModules, buildRawCallerIndex, buildResolveContext, buildSymbolIndex, byKey, byStr, categorize, changeCoupling, changedSince, checkRules, classify, clip, clipInline, communityOf, compileGlobs, complexityOfSource, computeImportPairs, computeSurprises, computeSymbolRefs, computeTestMap, deleteMemory, deserializeEmbeddings, detectCommunities, detectWorkspaces, diffFiles, diffHunks, embedEndpointUrl, embedViaEndpoint, embeddingUnits, enclosingSymbol, encode, encodeQueryViaEndpoint, ensureGrammars, escapeRegExp, extToLang, extractAst, extractCode, extractMarkdown, extractSymbols, findDeadCode, findReferences, findSymbol, foldText, gitChurn, grammarKeyForExt, grammarReady, grepRepo, hasEmbedModel, have, headCommit, healthzUrl, insertAfterSymbol, insertBeforeSymbol, intDot, isCode, isDoc, isGitWorktree, isIgnored, isSurprising, isTestFile, isTestPath, keywords, languageOf, listMemories, loadEmbedModel, pagerankOf, parseGitignore, parseRules, probeEndpoint, quantize, rankHotspots, rankedKeywords, readMemory, readText, renderGraphJson, renderMermaid, renderRepoMap, renderScip, renderSymbolsJson, replaceSymbolBody, resolveBaseRef, resolveCallEdges, resolveDocLink, resolveEmbedEndpoint, resolveEmbedModelDir, resolveEmbedPullUrl, resolveImport, resolveUniqueSymbol, riskHotspots, roundHalfToEven, rrf, runCli, runMcpServer, scanRepo, searchIndex, searchSemantic, serializeEmbeddings, sh, sha1, shortHash, slugify, subtokens, symbolComplexity, symbolsOverview, testsForModule, tierForPath, tokenize, uniqueSymbolDefs, untestedModules, untrackedFiles, walk, wordpiece, writeMemory };
|
package/scripts/engine.mjs
CHANGED
|
@@ -14,9 +14,9 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
|
|
|
14
14
|
var init_types = __esm({
|
|
15
15
|
"src/types.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
ENGINE_VERSION = "2.
|
|
17
|
+
ENGINE_VERSION = "2.13.0";
|
|
18
18
|
SCHEMA_VERSION = 4;
|
|
19
|
-
EXTRACTOR_VERSION =
|
|
19
|
+
EXTRACTOR_VERSION = 10;
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -320,6 +320,7 @@ function walk(root, opts = {}) {
|
|
|
320
320
|
const maxFileBytes = opts.maxFileBytes ?? 1024 * 1024;
|
|
321
321
|
const maxFiles = opts.maxFiles ?? DEFAULT_MAX_FILES;
|
|
322
322
|
const useGitignore = opts.gitignore !== false;
|
|
323
|
+
const ignoreDirs = opts.ignoreDirs ? new Set(opts.ignoreDirs) : IGNORE_DIRS;
|
|
323
324
|
const out2 = [];
|
|
324
325
|
let capped = false;
|
|
325
326
|
let excluded = 0;
|
|
@@ -368,7 +369,7 @@ function walk(root, opts = {}) {
|
|
|
368
369
|
continue;
|
|
369
370
|
}
|
|
370
371
|
if (st.isDirectory()) {
|
|
371
|
-
if (
|
|
372
|
+
if (ignoreDirs.has(name2)) continue;
|
|
372
373
|
if (isLink) continue;
|
|
373
374
|
if (useGitignore && rules.length && isIgnored(rules, rel, true)) continue;
|
|
374
375
|
stack.push({ dir: abs, rel, rules });
|
|
@@ -463,6 +464,7 @@ var init_walk = __esm({
|
|
|
463
464
|
".cache",
|
|
464
465
|
"tmp",
|
|
465
466
|
".ultraindex",
|
|
467
|
+
".codeindex",
|
|
466
468
|
"Pods",
|
|
467
469
|
"DerivedData",
|
|
468
470
|
".terraform",
|
|
@@ -901,7 +903,7 @@ var init_js_ts = __esm({
|
|
|
901
903
|
RULES = [
|
|
902
904
|
{ re: /^\s*export\s+(?:async\s+)?function\s+(?<name>[\w$]+)/, kind: "function", exported: true },
|
|
903
905
|
{ re: /^\s*export\s+default\s+(?:async\s+)?function\s+(?<name>[\w$]+)/, kind: "function", exported: true },
|
|
904
|
-
{ re: /^\s*export\s+default\s+(?:abstract\s+)?class\s+(?<name>[\w$]+)/, kind: "class", exported: true },
|
|
906
|
+
{ re: /^\s*export\s+default\s+(?:abstract\s+)?class\s+(?!extends\b)(?<name>[\w$]+)/, kind: "class", exported: true },
|
|
905
907
|
{ re: /^\s*(?:async\s+)?function\s+(?<name>[\w$]+)/, kind: "function", exported: false },
|
|
906
908
|
{ re: /^\s*export\s+(?:abstract\s+)?class\s+(?<name>[\w$]+)/, kind: "class", exported: true },
|
|
907
909
|
{ re: /^\s*(?:abstract\s+)?class\s+(?<name>[\w$]+)/, kind: "class", exported: false },
|
|
@@ -5718,7 +5720,7 @@ function readReceiver(node) {
|
|
|
5718
5720
|
const name2 = obj ? readName(obj) : void 0;
|
|
5719
5721
|
return name2 && /^[A-Za-z_]\w*$/.test(name2) ? name2 : void 0;
|
|
5720
5722
|
}
|
|
5721
|
-
function collectCalls(root, spec) {
|
|
5723
|
+
function collectCalls(root, spec, maxCalls = MAX_CALLS) {
|
|
5722
5724
|
if (!spec.calls) return [];
|
|
5723
5725
|
const out2 = [];
|
|
5724
5726
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -5749,7 +5751,7 @@ function collectCalls(root, spec) {
|
|
|
5749
5751
|
};
|
|
5750
5752
|
visit(root);
|
|
5751
5753
|
out2.sort((a, b) => byStr(a.name, b.name) || a.line - b.line);
|
|
5752
|
-
return out2.slice(0,
|
|
5754
|
+
return out2.slice(0, maxCalls);
|
|
5753
5755
|
}
|
|
5754
5756
|
function collectImportedNames(root, spec) {
|
|
5755
5757
|
if (!spec.imports?.import_statement) return [];
|
|
@@ -5776,7 +5778,7 @@ function collectImportedNames(root, spec) {
|
|
|
5776
5778
|
visit(root);
|
|
5777
5779
|
return [...found].sort(byStr).slice(0, MAX_IMPORTED_NAMES);
|
|
5778
5780
|
}
|
|
5779
|
-
function extractAst(rel, ext, content) {
|
|
5781
|
+
function extractAst(rel, ext, content, opts = {}) {
|
|
5780
5782
|
const key = grammarKeyForExt(ext);
|
|
5781
5783
|
if (!key || !grammarReady(key)) return void 0;
|
|
5782
5784
|
const spec = SPECS[key];
|
|
@@ -5962,7 +5964,7 @@ function extractAst(rel, ext, content) {
|
|
|
5962
5964
|
}
|
|
5963
5965
|
const refs = collectImports(root, spec);
|
|
5964
5966
|
const idents = collectRefIdents(root, new Set(symbols.map((s) => s.name)));
|
|
5965
|
-
const calls = collectCalls(root, spec);
|
|
5967
|
+
const calls = collectCalls(root, spec, opts.maxCalls);
|
|
5966
5968
|
const importedNames = collectImportedNames(root, spec);
|
|
5967
5969
|
let pkg;
|
|
5968
5970
|
if (spec.lang === "java") {
|
|
@@ -6398,12 +6400,12 @@ function extractImports(ext, content) {
|
|
|
6398
6400
|
}
|
|
6399
6401
|
return [...specs].map((spec) => ({ kind: "import", spec }));
|
|
6400
6402
|
}
|
|
6401
|
-
function collectCallsRegex(content, symbols = []) {
|
|
6403
|
+
function collectCallsRegex(content, symbols = [], maxCalls = 512) {
|
|
6402
6404
|
const out2 = /* @__PURE__ */ new Map();
|
|
6403
6405
|
const ownDefLines = new Set(symbols.map((s) => `${s.name} ${s.line}`));
|
|
6404
6406
|
const lines = content.split("\n");
|
|
6405
6407
|
const CALL_RE = /(?:\bnew\s+)?(?:([A-Za-z_$][\w$]*)\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
6406
|
-
for (let i2 = 0; i2 < lines.length && out2.size <
|
|
6408
|
+
for (let i2 = 0; i2 < lines.length && out2.size < maxCalls; i2++) {
|
|
6407
6409
|
const line = lines[i2];
|
|
6408
6410
|
const trimmed = line.trimStart();
|
|
6409
6411
|
if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*")) continue;
|
|
@@ -6418,7 +6420,7 @@ function collectCallsRegex(content, symbols = []) {
|
|
|
6418
6420
|
CALL_RE.lastIndex = 0;
|
|
6419
6421
|
let m;
|
|
6420
6422
|
const fallbackExcluded = /* @__PURE__ */ new Set();
|
|
6421
|
-
while ((m = CALL_RE.exec(line)) !== null && out2.size <
|
|
6423
|
+
while ((m = CALL_RE.exec(line)) !== null && out2.size < maxCalls) {
|
|
6422
6424
|
const receiver = m[1];
|
|
6423
6425
|
const name2 = m[2];
|
|
6424
6426
|
if (name2.length < 2 || CALL_KEYWORDS.has(name2)) continue;
|
|
@@ -6435,8 +6437,8 @@ function collectCallsRegex(content, symbols = []) {
|
|
|
6435
6437
|
}
|
|
6436
6438
|
return [...out2.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : a.line - b.line);
|
|
6437
6439
|
}
|
|
6438
|
-
function extractCode(rel, ext, content) {
|
|
6439
|
-
const ast = extractAst(rel, ext, content);
|
|
6440
|
+
function extractCode(rel, ext, content, opts = {}) {
|
|
6441
|
+
const ast = extractAst(rel, ext, content, { maxCalls: opts.maxCallsPerFile });
|
|
6440
6442
|
const symbols = (ast ? ast.symbols : extractSymbols(rel, ext, content)).slice(0, 400);
|
|
6441
6443
|
const known = new Set(symbols.map((s) => s.name));
|
|
6442
6444
|
const reexports = extractReexports(rel, content, symbols).filter((s) => !known.has(s.name));
|
|
@@ -6452,7 +6454,7 @@ function extractCode(rel, ext, content) {
|
|
|
6452
6454
|
// collector otherwise, so caller indexes exist without the wasm sidecar.
|
|
6453
6455
|
// `symbols` (this file's own regex-extracted defs) lets the collector
|
|
6454
6456
|
// exclude a definition's own name+line from its call candidates.
|
|
6455
|
-
calls: ast ? ast.calls : collectCallsRegex(content, symbols),
|
|
6457
|
+
calls: ast ? ast.calls : collectCallsRegex(content, symbols, opts.maxCallsPerFile),
|
|
6456
6458
|
importedNames: ast?.importedNames
|
|
6457
6459
|
};
|
|
6458
6460
|
}
|
|
@@ -6524,7 +6526,8 @@ function scanRepo(root, opts = {}) {
|
|
|
6524
6526
|
const { files: walked, capped, excluded } = walk(root, {
|
|
6525
6527
|
maxFileBytes: opts.maxBytes,
|
|
6526
6528
|
maxFiles: opts.maxFiles,
|
|
6527
|
-
gitignore: opts.gitignore
|
|
6529
|
+
gitignore: opts.gitignore,
|
|
6530
|
+
ignoreDirs: opts.ignoreDirs
|
|
6528
6531
|
});
|
|
6529
6532
|
const outPrefix = opts.out ? opts.out.replace(/\/+$/, "") + "/" : null;
|
|
6530
6533
|
const files = [];
|
|
@@ -6573,7 +6576,7 @@ function scanRepo(root, opts = {}) {
|
|
|
6573
6576
|
} else if (kind === "doc") {
|
|
6574
6577
|
record.title = basename(f.rel);
|
|
6575
6578
|
} else if (kind === "code") {
|
|
6576
|
-
const code = extractCode(f.rel, f.ext, content);
|
|
6579
|
+
const code = extractCode(f.rel, f.ext, content, { maxCallsPerFile: opts.maxCallsPerFile });
|
|
6577
6580
|
record.title = basename(f.rel);
|
|
6578
6581
|
record.summary = code.summary;
|
|
6579
6582
|
record.symbols = code.symbols;
|
|
@@ -9418,16 +9421,12 @@ function resolveEmbedModelDir(repo) {
|
|
|
9418
9421
|
function hasEmbedModel(repo) {
|
|
9419
9422
|
return resolveEmbedModelDir(repo) !== void 0;
|
|
9420
9423
|
}
|
|
9421
|
-
function
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
if (!
|
|
9425
|
-
const raw = JSON.parse(readFileSync5(path, "utf8"));
|
|
9426
|
-
const { modelId, dim, vocab, weights } = raw;
|
|
9427
|
-
if (typeof modelId !== "string" || !modelId) throw new Error(`embed model: missing modelId in ${path}`);
|
|
9428
|
-
if (!Number.isInteger(dim) || dim <= 0) throw new Error(`embed model: bad dim ${dim} in ${path}`);
|
|
9424
|
+
function parseEmbedModel(raw, source) {
|
|
9425
|
+
const { modelId, dim, vocab, weights, unk: rawUnk } = raw ?? {};
|
|
9426
|
+
if (typeof modelId !== "string" || !modelId) throw new Error(`embed model: missing modelId in ${source}`);
|
|
9427
|
+
if (!Number.isInteger(dim) || dim <= 0) throw new Error(`embed model: bad dim ${dim} in ${source}`);
|
|
9429
9428
|
if (!Array.isArray(vocab) || !Array.isArray(weights) || vocab.length !== weights.length) {
|
|
9430
|
-
throw new Error(`embed model: vocab/weights length mismatch in ${
|
|
9429
|
+
throw new Error(`embed model: vocab/weights length mismatch in ${source}`);
|
|
9431
9430
|
}
|
|
9432
9431
|
const vocabSize = vocab.length;
|
|
9433
9432
|
const flat = new Float64Array(vocabSize * dim);
|
|
@@ -9442,10 +9441,17 @@ function loadEmbedModel(dir) {
|
|
|
9442
9441
|
}
|
|
9443
9442
|
for (let d = 0; d < dim; d++) flat[i2 * dim + d] = Number(row[d]);
|
|
9444
9443
|
}
|
|
9445
|
-
const unk = typeof
|
|
9444
|
+
const unk = typeof rawUnk === "string" ? rawUnk : "[UNK]";
|
|
9446
9445
|
const unkId = vmap.has(unk) ? vmap.get(unk) : -1;
|
|
9447
9446
|
return { modelId, dim, unk, unkId, vocabSize, vocab: vmap, weights: flat };
|
|
9448
9447
|
}
|
|
9448
|
+
function loadEmbedModel(dir) {
|
|
9449
|
+
if (!dir) return void 0;
|
|
9450
|
+
const path = join10(dir, "model.json");
|
|
9451
|
+
if (!existsSync3(path)) return void 0;
|
|
9452
|
+
const raw = JSON.parse(readFileSync5(path, "utf8"));
|
|
9453
|
+
return parseEmbedModel(raw, path);
|
|
9454
|
+
}
|
|
9449
9455
|
function resolveEmbedPullUrl() {
|
|
9450
9456
|
const env = process.env.CODEINDEX_EMBED_URL;
|
|
9451
9457
|
if (env && env.trim()) return { url: env.trim() };
|
|
@@ -10172,10 +10178,13 @@ var init_viz = __esm({
|
|
|
10172
10178
|
// src/mcp.ts
|
|
10173
10179
|
var mcp_exports = {};
|
|
10174
10180
|
__export(mcp_exports, {
|
|
10181
|
+
memoizedEmbedModel: () => memoizedEmbedModel,
|
|
10175
10182
|
memoizedEmbeddingIndex: () => memoizedEmbeddingIndex,
|
|
10176
10183
|
runMcpServer: () => runMcpServer,
|
|
10177
10184
|
scanFingerprint: () => scanFingerprint
|
|
10178
10185
|
});
|
|
10186
|
+
import { statSync as statSync4 } from "fs";
|
|
10187
|
+
import { join as join12 } from "path";
|
|
10179
10188
|
import { createInterface } from "readline";
|
|
10180
10189
|
function str(v) {
|
|
10181
10190
|
return typeof v === "string" && v ? v : void 0;
|
|
@@ -10196,6 +10205,19 @@ async function memoizedEmbeddingIndex(key, build) {
|
|
|
10196
10205
|
embeddingIndexCache = { key: cacheKey, index };
|
|
10197
10206
|
return index;
|
|
10198
10207
|
}
|
|
10208
|
+
function memoizedEmbedModel(modelDir) {
|
|
10209
|
+
let stat;
|
|
10210
|
+
try {
|
|
10211
|
+
stat = statSync4(join12(modelDir, "model.json"));
|
|
10212
|
+
} catch {
|
|
10213
|
+
return void 0;
|
|
10214
|
+
}
|
|
10215
|
+
const key = `${modelDir}:${stat.mtimeMs}:${stat.size}`;
|
|
10216
|
+
if (embedModelCache && embedModelCache.key === key) return embedModelCache.model;
|
|
10217
|
+
const model = loadEmbedModel(modelDir);
|
|
10218
|
+
if (model) embedModelCache = { key, model };
|
|
10219
|
+
return model;
|
|
10220
|
+
}
|
|
10199
10221
|
async function callTool(name2, args2) {
|
|
10200
10222
|
const repo = str(args2.repo);
|
|
10201
10223
|
if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
|
|
@@ -10350,7 +10372,7 @@ async function callTool(name2, args2) {
|
|
|
10350
10372
|
}
|
|
10351
10373
|
}
|
|
10352
10374
|
const modelDir = resolveEmbedModelDir(repo);
|
|
10353
|
-
const model = modelDir ?
|
|
10375
|
+
const model = modelDir ? memoizedEmbedModel(modelDir) : void 0;
|
|
10354
10376
|
if (model) {
|
|
10355
10377
|
const index = await memoizedEmbeddingIndex(
|
|
10356
10378
|
{ mode: "static", identity: `${modelDir}#${model.modelId}`, scan: scan2 },
|
|
@@ -10370,7 +10392,7 @@ async function callTool(name2, args2) {
|
|
|
10370
10392
|
}
|
|
10371
10393
|
if (name2 === "embed_status") {
|
|
10372
10394
|
const modelDir = resolveEmbedModelDir(repo);
|
|
10373
|
-
const model = modelDir ?
|
|
10395
|
+
const model = modelDir ? memoizedEmbedModel(modelDir) : void 0;
|
|
10374
10396
|
const endpoint = resolveEmbedEndpoint();
|
|
10375
10397
|
const mode = endpoint ? "endpoint" : model ? "static" : "none";
|
|
10376
10398
|
const status = {
|
|
@@ -10445,7 +10467,7 @@ async function runMcpServer() {
|
|
|
10445
10467
|
}
|
|
10446
10468
|
}
|
|
10447
10469
|
}
|
|
10448
|
-
var repoProp, scopeProps, TOOLS, embeddingIndexCache;
|
|
10470
|
+
var repoProp, scopeProps, TOOLS, embeddingIndexCache, embedModelCache;
|
|
10449
10471
|
var init_mcp = __esm({
|
|
10450
10472
|
"src/mcp.ts"() {
|
|
10451
10473
|
"use strict";
|
|
@@ -11148,7 +11170,7 @@ init_pipeline();
|
|
|
11148
11170
|
init_graph_json();
|
|
11149
11171
|
init_symbols_json();
|
|
11150
11172
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
11151
|
-
import { join as
|
|
11173
|
+
import { join as join13, resolve } from "path";
|
|
11152
11174
|
init_scan();
|
|
11153
11175
|
init_callers();
|
|
11154
11176
|
init_workspaces();
|
|
@@ -11216,8 +11238,11 @@ Flags:
|
|
|
11216
11238
|
--exclude <glob> Exclude matching paths (repeatable)
|
|
11217
11239
|
--scope <dir> Restrict to one directory (sugar for --include '<dir>/**')
|
|
11218
11240
|
--no-gitignore Do not honor .gitignore files (default: honored)
|
|
11241
|
+
--ignore-dir <name> Directory names to skip (repeatable) \u2014 REPLACES the
|
|
11242
|
+
default ignored-directory set, never merges with it
|
|
11219
11243
|
--max-files <n> Cap walked files (default 20000)
|
|
11220
11244
|
--max-bytes <n> Skip files above this size (default 1 MiB)
|
|
11245
|
+
--max-calls <n> Per-file call-site cap for extraction (default 512)
|
|
11221
11246
|
--no-ast Skip tree-sitter grammars even when present (regex tier)
|
|
11222
11247
|
--config <file> Rules config for \`rules\` (JSON: [{name, from, to, \u2026}])
|
|
11223
11248
|
--limit <n> Max results for \`search\` (default 20)
|
|
@@ -11232,7 +11257,7 @@ Flags:
|
|
|
11232
11257
|
each site corroborated|unique-name
|
|
11233
11258
|
`;
|
|
11234
11259
|
function parseFlags(args2) {
|
|
11235
|
-
const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false, fuzzy: true, semantic: false };
|
|
11260
|
+
const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, ignoreDirs: [], noAst: false, fuzzy: true, semantic: false };
|
|
11236
11261
|
for (let i2 = 0; i2 < args2.length; i2++) {
|
|
11237
11262
|
const a = args2[i2];
|
|
11238
11263
|
const next = () => {
|
|
@@ -11255,8 +11280,10 @@ function parseFlags(args2) {
|
|
|
11255
11280
|
else if (a === "--exclude") flags2.exclude.push(next());
|
|
11256
11281
|
else if (a === "--scope") flags2.scope = next();
|
|
11257
11282
|
else if (a === "--no-gitignore") flags2.gitignore = false;
|
|
11283
|
+
else if (a === "--ignore-dir") flags2.ignoreDirs.push(next());
|
|
11258
11284
|
else if (a === "--max-files") flags2.maxFiles = num();
|
|
11259
11285
|
else if (a === "--max-bytes") flags2.maxBytes = num();
|
|
11286
|
+
else if (a === "--max-calls") flags2.maxCalls = num();
|
|
11260
11287
|
else if (a === "--ignore-case") flags2.ignoreCase = true;
|
|
11261
11288
|
else if (a === "--max-hits") flags2.maxHits = num();
|
|
11262
11289
|
else if (a === "--budget-tokens") flags2.budgetTokens = num();
|
|
@@ -11283,8 +11310,10 @@ function scanOptions(flags2) {
|
|
|
11283
11310
|
exclude: flags2.exclude.length ? flags2.exclude : void 0,
|
|
11284
11311
|
scope: flags2.scope,
|
|
11285
11312
|
gitignore: flags2.gitignore,
|
|
11313
|
+
ignoreDirs: flags2.ignoreDirs.length ? flags2.ignoreDirs : void 0,
|
|
11286
11314
|
maxFiles: flags2.maxFiles,
|
|
11287
|
-
maxBytes: flags2.maxBytes
|
|
11315
|
+
maxBytes: flags2.maxBytes,
|
|
11316
|
+
maxCallsPerFile: flags2.maxCalls
|
|
11288
11317
|
};
|
|
11289
11318
|
}
|
|
11290
11319
|
async function runCli(argv) {
|
|
@@ -11309,7 +11338,7 @@ async function runCli(argv) {
|
|
|
11309
11338
|
if (!flags2.out) throw new Error("index needs --out <dir>");
|
|
11310
11339
|
const outDir = flags2.out;
|
|
11311
11340
|
mkdirSync2(outDir, { recursive: true });
|
|
11312
|
-
const cachePath =
|
|
11341
|
+
const cachePath = join13(outDir, "cache.json");
|
|
11313
11342
|
let cache;
|
|
11314
11343
|
try {
|
|
11315
11344
|
const parsed = JSON.parse(readFileSync6(cachePath, "utf8"));
|
|
@@ -11319,8 +11348,8 @@ async function runCli(argv) {
|
|
|
11319
11348
|
} catch {
|
|
11320
11349
|
}
|
|
11321
11350
|
const { scan: scan2, graph, symbols } = buildIndexArtifacts(flags2.repo, { ...scanOptions(flags2), cache, out: outDir });
|
|
11322
|
-
writeFileSync3(
|
|
11323
|
-
writeFileSync3(
|
|
11351
|
+
writeFileSync3(join13(outDir, "graph.json"), renderGraphJson(graph));
|
|
11352
|
+
writeFileSync3(join13(outDir, "symbols.json"), renderSymbolsJson(symbols));
|
|
11324
11353
|
const files = {};
|
|
11325
11354
|
for (const f of scan2.files) {
|
|
11326
11355
|
const entry = { hash: f.hash, record: f, size: f.size };
|
|
@@ -11337,7 +11366,7 @@ async function runCli(argv) {
|
|
|
11337
11366
|
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
11338
11367
|
if (model) {
|
|
11339
11368
|
const index = buildEmbeddingIndex(scan2, model);
|
|
11340
|
-
writeFileSync3(
|
|
11369
|
+
writeFileSync3(join13(outDir, "embeddings.bin"), serializeEmbeddings(index));
|
|
11341
11370
|
embedNote = ` + embeddings.bin (${index.records.length} records, model ${model.modelId})`;
|
|
11342
11371
|
}
|
|
11343
11372
|
process.stderr.write(`codeindex: ${scan2.files.length} files \u2192 ${outDir}/graph.json + symbols.json${embedNote}${scan2.capped ? " (capped)" : ""}
|
|
@@ -11469,14 +11498,14 @@ async function runCli(argv) {
|
|
|
11469
11498
|
mkdirSync2(flags2.out, { recursive: true });
|
|
11470
11499
|
const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
|
|
11471
11500
|
const index = buildEmbeddingIndex(scan2, model);
|
|
11472
|
-
writeFileSync3(
|
|
11501
|
+
writeFileSync3(join13(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
|
|
11473
11502
|
process.stderr.write(`codeindex: ${index.records.length} embedding records \u2192 ${flags2.out}/embeddings.bin (model ${model.modelId})
|
|
11474
11503
|
`);
|
|
11475
11504
|
} else if (sub === "pull") {
|
|
11476
11505
|
const { url, sha256 } = resolveEmbedPullUrl();
|
|
11477
|
-
const destDir = process.env.CODEINDEX_EMBED_DIR ??
|
|
11506
|
+
const destDir = process.env.CODEINDEX_EMBED_DIR ?? join13(flags2.repo, ".codeindex", "models");
|
|
11478
11507
|
mkdirSync2(destDir, { recursive: true });
|
|
11479
|
-
process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${
|
|
11508
|
+
process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join13(destDir, "model.json")}
|
|
11480
11509
|
`);
|
|
11481
11510
|
let body2;
|
|
11482
11511
|
try {
|
|
@@ -11488,14 +11517,17 @@ async function runCli(argv) {
|
|
|
11488
11517
|
return;
|
|
11489
11518
|
}
|
|
11490
11519
|
try {
|
|
11491
|
-
JSON.parse(body2);
|
|
11492
|
-
} catch {
|
|
11493
|
-
process.stderr.write(
|
|
11520
|
+
parseEmbedModel(JSON.parse(body2), url);
|
|
11521
|
+
} catch (e) {
|
|
11522
|
+
process.stderr.write(
|
|
11523
|
+
`codeindex: pull failed \u2014 response is not a valid model.json (${e instanceof Error ? e.message : String(e)}) (nothing written)
|
|
11524
|
+
`
|
|
11525
|
+
);
|
|
11494
11526
|
process.exitCode = 1;
|
|
11495
11527
|
return;
|
|
11496
11528
|
}
|
|
11497
|
-
writeFileSync3(
|
|
11498
|
-
process.stderr.write(`codeindex: model written to ${
|
|
11529
|
+
writeFileSync3(join13(destDir, "model.json"), body2);
|
|
11530
|
+
process.stderr.write(`codeindex: model written to ${join13(destDir, "model.json")}
|
|
11499
11531
|
`);
|
|
11500
11532
|
} else {
|
|
11501
11533
|
throw new Error("embed needs a subcommand: status | build | pull | serve");
|
package/src/ast/extract.ts
CHANGED
|
@@ -399,8 +399,9 @@ function readReceiver(node: TSNode | null): string | undefined {
|
|
|
399
399
|
// dedicated member-call node's `name`; "constructor" reads the constructed type.
|
|
400
400
|
// A qualified call also carries the immediate `receiver` name (see readReceiver).
|
|
401
401
|
// Names are filtered to plausible identifiers (≥ 2 chars), deduped by name+line,
|
|
402
|
-
// sorted, and capped, so the set
|
|
403
|
-
|
|
402
|
+
// sorted, and capped (default MAX_CALLS; overridable via maxCalls), so the set
|
|
403
|
+
// stays small and deterministic.
|
|
404
|
+
function collectCalls(root: TSNode, spec: LangSpec, maxCalls: number = MAX_CALLS): { name: string; line: number; receiver?: string }[] {
|
|
404
405
|
if (!spec.calls) return [];
|
|
405
406
|
const out: { name: string; line: number; receiver?: string }[] = [];
|
|
406
407
|
const seen = new Set<string>();
|
|
@@ -439,7 +440,7 @@ function collectCalls(root: TSNode, spec: LangSpec): { name: string; line: numbe
|
|
|
439
440
|
};
|
|
440
441
|
visit(root);
|
|
441
442
|
out.sort((a, b) => byStr(a.name, b.name) || a.line - b.line);
|
|
442
|
-
return out.slice(0,
|
|
443
|
+
return out.slice(0, maxCalls);
|
|
443
444
|
}
|
|
444
445
|
|
|
445
446
|
// Collect JS/TS named-import bindings: `import { a, b as c } from "x"` →
|
|
@@ -476,7 +477,8 @@ function collectImportedNames(root: TSNode, spec: LangSpec): string[] {
|
|
|
476
477
|
// Extract declared symbols from one file via its committed grammar. Returns
|
|
477
478
|
// undefined when no grammar is loaded for the extension (caller falls back to the
|
|
478
479
|
// regex extractor). Walks top-level declarations plus one level of nested members.
|
|
479
|
-
|
|
480
|
+
// `opts.maxCalls` overrides the per-file call-site cap (default MAX_CALLS).
|
|
481
|
+
export function extractAst(rel: string, ext: string, content: string, opts: { maxCalls?: number } = {}): AstResult | undefined {
|
|
480
482
|
const key = grammarKeyForExt(ext);
|
|
481
483
|
if (!key || !grammarReady(key)) return undefined;
|
|
482
484
|
const spec = SPECS[key];
|
|
@@ -697,7 +699,7 @@ export function extractAst(rel: string, ext: string, content: string): AstResult
|
|
|
697
699
|
|
|
698
700
|
const refs = collectImports(root, spec);
|
|
699
701
|
const idents = collectRefIdents(root, new Set(symbols.map((s) => s.name)));
|
|
700
|
-
const calls = collectCalls(root, spec);
|
|
702
|
+
const calls = collectCalls(root, spec, opts.maxCalls);
|
|
701
703
|
const importedNames = collectImportedNames(root, spec);
|
|
702
704
|
let pkg: string | undefined;
|
|
703
705
|
if (spec.lang === "java") {
|
package/src/callers.ts
CHANGED
|
Binary file
|
package/src/embed/model.ts
CHANGED
|
@@ -87,20 +87,17 @@ export function hasEmbedModel(repo?: string): boolean {
|
|
|
87
87
|
return resolveEmbedModelDir(repo) !== undefined;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
98
|
-
|
|
99
|
-
const { modelId, dim, vocab, weights } = raw;
|
|
100
|
-
if (typeof modelId !== "string" || !modelId) throw new Error(`embed model: missing modelId in ${path}`);
|
|
101
|
-
if (!Number.isInteger(dim) || dim <= 0) throw new Error(`embed model: bad dim ${dim} in ${path}`);
|
|
90
|
+
// Validate a parsed model.json body and construct the loaded model. Throws on
|
|
91
|
+
// a malformed shape (missing modelId, bad dim, vocab/weights mismatch, ragged
|
|
92
|
+
// rows) with a message citing `source` (a file path, a URL, …) so a corrupt
|
|
93
|
+
// asset fails loudly wherever it enters — at load, or at `embed pull` BEFORE
|
|
94
|
+
// the shape-invalid file is ever written to disk.
|
|
95
|
+
export function parseEmbedModel(raw: unknown, source: string): StaticEmbedModel {
|
|
96
|
+
const { modelId, dim, vocab, weights, unk: rawUnk } = (raw ?? {}) as ModelFile;
|
|
97
|
+
if (typeof modelId !== "string" || !modelId) throw new Error(`embed model: missing modelId in ${source}`);
|
|
98
|
+
if (!Number.isInteger(dim) || dim <= 0) throw new Error(`embed model: bad dim ${dim} in ${source}`);
|
|
102
99
|
if (!Array.isArray(vocab) || !Array.isArray(weights) || vocab.length !== weights.length) {
|
|
103
|
-
throw new Error(`embed model: vocab/weights length mismatch in ${
|
|
100
|
+
throw new Error(`embed model: vocab/weights length mismatch in ${source}`);
|
|
104
101
|
}
|
|
105
102
|
const vocabSize = vocab.length;
|
|
106
103
|
const flat = new Float64Array(vocabSize * dim);
|
|
@@ -115,11 +112,23 @@ export function loadEmbedModel(dir?: string): StaticEmbedModel | undefined {
|
|
|
115
112
|
}
|
|
116
113
|
for (let d = 0; d < dim; d++) flat[i * dim + d] = Number(row[d]);
|
|
117
114
|
}
|
|
118
|
-
const unk = typeof
|
|
115
|
+
const unk = typeof rawUnk === "string" ? rawUnk : "[UNK]";
|
|
119
116
|
const unkId = vmap.has(unk) ? vmap.get(unk)! : -1;
|
|
120
117
|
return { modelId, dim, unk, unkId, vocabSize, vocab: vmap, weights: flat };
|
|
121
118
|
}
|
|
122
119
|
|
|
120
|
+
// Load and validate a static model from a directory containing model.json.
|
|
121
|
+
// Throws on a malformed file (bad dim, ragged weights) so a corrupt asset fails
|
|
122
|
+
// loudly at load rather than silently misranking. Returns undefined only when
|
|
123
|
+
// the directory has no model.json (the not-present case).
|
|
124
|
+
export function loadEmbedModel(dir?: string): StaticEmbedModel | undefined {
|
|
125
|
+
if (!dir) return undefined;
|
|
126
|
+
const path = join(dir, "model.json");
|
|
127
|
+
if (!existsSync(path)) return undefined;
|
|
128
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
129
|
+
return parseEmbedModel(raw, path);
|
|
130
|
+
}
|
|
131
|
+
|
|
123
132
|
// What `embed pull` fetches, and whether to verify it. CODEINDEX_EMBED_URL wins
|
|
124
133
|
// outright (the user's explicit override / private mirror) and carries NO
|
|
125
134
|
// sha256, so a custom asset keeps the current un-verified behavior. With no env,
|
package/src/engine-cli.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { symbolComplexity, riskHotspots } from "./complexity.js";
|
|
|
19
19
|
import { renderMermaid } from "./viz.js";
|
|
20
20
|
import { searchIndex } from "./bm25.js";
|
|
21
21
|
import { checkRules, parseRules } from "./rules.js";
|
|
22
|
-
import { EMBED_VERSION, resolveEmbedModelDir, loadEmbedModel, resolveEmbedPullUrl, fetchEmbedModel } from "./embed/model.js";
|
|
22
|
+
import { EMBED_VERSION, resolveEmbedModelDir, loadEmbedModel, parseEmbedModel, resolveEmbedPullUrl, fetchEmbedModel } from "./embed/model.js";
|
|
23
23
|
import { buildEmbeddingIndex, serializeEmbeddings } from "./embed/index.js";
|
|
24
24
|
import { searchSemantic } from "./embed/search.js";
|
|
25
25
|
import {
|
|
@@ -80,8 +80,11 @@ Flags:
|
|
|
80
80
|
--exclude <glob> Exclude matching paths (repeatable)
|
|
81
81
|
--scope <dir> Restrict to one directory (sugar for --include '<dir>/**')
|
|
82
82
|
--no-gitignore Do not honor .gitignore files (default: honored)
|
|
83
|
+
--ignore-dir <name> Directory names to skip (repeatable) — REPLACES the
|
|
84
|
+
default ignored-directory set, never merges with it
|
|
83
85
|
--max-files <n> Cap walked files (default 20000)
|
|
84
86
|
--max-bytes <n> Skip files above this size (default 1 MiB)
|
|
87
|
+
--max-calls <n> Per-file call-site cap for extraction (default 512)
|
|
85
88
|
--no-ast Skip tree-sitter grammars even when present (regex tier)
|
|
86
89
|
--config <file> Rules config for \`rules\` (JSON: [{name, from, to, …}])
|
|
87
90
|
--limit <n> Max results for \`search\` (default 20)
|
|
@@ -103,8 +106,10 @@ interface CliFlags {
|
|
|
103
106
|
exclude: string[];
|
|
104
107
|
scope?: string;
|
|
105
108
|
gitignore: boolean;
|
|
109
|
+
ignoreDirs: string[];
|
|
106
110
|
maxFiles?: number;
|
|
107
111
|
maxBytes?: number;
|
|
112
|
+
maxCalls?: number;
|
|
108
113
|
noAst: boolean;
|
|
109
114
|
since?: string;
|
|
110
115
|
ignoreCase?: boolean;
|
|
@@ -121,7 +126,7 @@ interface CliFlags {
|
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
function parseFlags(args: string[]): CliFlags {
|
|
124
|
-
const flags: CliFlags = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false, fuzzy: true, semantic: false };
|
|
129
|
+
const flags: CliFlags = { repo: process.cwd(), include: [], exclude: [], gitignore: true, ignoreDirs: [], noAst: false, fuzzy: true, semantic: false };
|
|
125
130
|
for (let i = 0; i < args.length; i++) {
|
|
126
131
|
const a = args[i]!;
|
|
127
132
|
const next = (): string => {
|
|
@@ -144,8 +149,10 @@ function parseFlags(args: string[]): CliFlags {
|
|
|
144
149
|
else if (a === "--exclude") flags.exclude.push(next());
|
|
145
150
|
else if (a === "--scope") flags.scope = next();
|
|
146
151
|
else if (a === "--no-gitignore") flags.gitignore = false;
|
|
152
|
+
else if (a === "--ignore-dir") flags.ignoreDirs.push(next());
|
|
147
153
|
else if (a === "--max-files") flags.maxFiles = num();
|
|
148
154
|
else if (a === "--max-bytes") flags.maxBytes = num();
|
|
155
|
+
else if (a === "--max-calls") flags.maxCalls = num();
|
|
149
156
|
else if (a === "--ignore-case") flags.ignoreCase = true;
|
|
150
157
|
else if (a === "--max-hits") flags.maxHits = num();
|
|
151
158
|
else if (a === "--budget-tokens") flags.budgetTokens = num();
|
|
@@ -174,8 +181,10 @@ function scanOptions(flags: CliFlags): BuildIndexOptions {
|
|
|
174
181
|
exclude: flags.exclude.length ? flags.exclude : undefined,
|
|
175
182
|
scope: flags.scope,
|
|
176
183
|
gitignore: flags.gitignore,
|
|
184
|
+
ignoreDirs: flags.ignoreDirs.length ? flags.ignoreDirs : undefined,
|
|
177
185
|
maxFiles: flags.maxFiles,
|
|
178
186
|
maxBytes: flags.maxBytes,
|
|
187
|
+
maxCallsPerFile: flags.maxCalls,
|
|
179
188
|
};
|
|
180
189
|
}
|
|
181
190
|
|
|
@@ -404,9 +413,14 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
404
413
|
return;
|
|
405
414
|
}
|
|
406
415
|
try {
|
|
407
|
-
JSON
|
|
408
|
-
|
|
409
|
-
|
|
416
|
+
// Shape-validate BEFORE writing: a JSON-valid but shape-invalid asset
|
|
417
|
+
// would otherwise land on disk and turn every later semantic search
|
|
418
|
+
// into a hard loadEmbedModel error instead of the documented degrade.
|
|
419
|
+
parseEmbedModel(JSON.parse(body), url);
|
|
420
|
+
} catch (e) {
|
|
421
|
+
process.stderr.write(
|
|
422
|
+
`codeindex: pull failed — response is not a valid model.json (${e instanceof Error ? e.message : String(e)}) (nothing written)\n`,
|
|
423
|
+
);
|
|
410
424
|
process.exitCode = 1;
|
|
411
425
|
return;
|
|
412
426
|
}
|
package/src/engine.ts
CHANGED
|
@@ -114,7 +114,7 @@ export {
|
|
|
114
114
|
loadEmbedModel,
|
|
115
115
|
resolveEmbedPullUrl,
|
|
116
116
|
} from "./embed/model.js";
|
|
117
|
-
export type { StaticEmbedModel } from "./embed/model.js";
|
|
117
|
+
export type { StaticEmbedModel, EmbedPullTarget } from "./embed/model.js";
|
|
118
118
|
export { encode, quantize, tokenize, wordpiece, basicTokenize, roundHalfToEven, intDot } from "./embed/encode.js";
|
|
119
119
|
export { buildEmbeddingIndex, serializeEmbeddings, deserializeEmbeddings, embeddingUnits } from "./embed/index.js";
|
|
120
120
|
export type { EmbeddingIndex, EmbeddingRecord, EmbeddingUnit } from "./embed/index.js";
|
package/src/extract/code.ts
CHANGED
|
@@ -302,12 +302,13 @@ const DEF_INTRODUCERS = /(?:\bfunction|\bdef|\bfunc|\bfun|\bfn|\bclass|\bsub|\bm
|
|
|
302
302
|
export function collectCallsRegex(
|
|
303
303
|
content: string,
|
|
304
304
|
symbols: Pick<CodeSymbol, "name" | "line">[] = [],
|
|
305
|
+
maxCalls: number = 512,
|
|
305
306
|
): { name: string; line: number; receiver?: string }[] {
|
|
306
307
|
const out = new Map<string, { name: string; line: number; receiver?: string }>();
|
|
307
308
|
const ownDefLines = new Set(symbols.map((s) => `${s.name} ${s.line}`));
|
|
308
309
|
const lines = content.split("\n");
|
|
309
310
|
const CALL_RE = /(?:\bnew\s+)?(?:([A-Za-z_$][\w$]*)\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
310
|
-
for (let i = 0; i < lines.length && out.size <
|
|
311
|
+
for (let i = 0; i < lines.length && out.size < maxCalls; i++) {
|
|
311
312
|
const line = lines[i]!;
|
|
312
313
|
// Cheap comment guard: a line-leading comment marker means no calls here
|
|
313
314
|
// (block-comment interiors and strings stay best-effort, like the symbol
|
|
@@ -332,7 +333,7 @@ export function collectCallsRegex(
|
|
|
332
333
|
CALL_RE.lastIndex = 0;
|
|
333
334
|
let m: RegExpExecArray | null;
|
|
334
335
|
const fallbackExcluded = new Set<string>();
|
|
335
|
-
while ((m = CALL_RE.exec(line)) !== null && out.size <
|
|
336
|
+
while ((m = CALL_RE.exec(line)) !== null && out.size < maxCalls) {
|
|
336
337
|
const receiver = m[1];
|
|
337
338
|
const name = m[2]!;
|
|
338
339
|
if (name.length < 2 || CALL_KEYWORDS.has(name)) continue;
|
|
@@ -350,13 +351,16 @@ export function collectCallsRegex(
|
|
|
350
351
|
return [...out.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : a.line - b.line));
|
|
351
352
|
}
|
|
352
353
|
|
|
353
|
-
|
|
354
|
+
// `opts.maxCallsPerFile` overrides the per-file call-site cap (default 512) on
|
|
355
|
+
// BOTH extraction tiers — AST and regex — so recall-oriented consumers can raise
|
|
356
|
+
// it. Dedup/sort semantics are unchanged; absent, output is byte-identical.
|
|
357
|
+
export function extractCode(rel: string, ext: string, content: string, opts: { maxCallsPerFile?: number } = {}): CodeInfo {
|
|
354
358
|
// Symbols come from tree-sitter when a grammar is loaded for this extension
|
|
355
359
|
// (AST-exact: real nesting, precise kinds, structural export), else the regex
|
|
356
360
|
// extractors. Imports/pkg stay on the battle-tested regex path here — their
|
|
357
361
|
// resolution is covered by resolve tests and the e2e ratchet; the new-language
|
|
358
362
|
// AST importers land with their resolvers.
|
|
359
|
-
const ast = extractAst(rel, ext, content);
|
|
363
|
+
const ast = extractAst(rel, ext, content, { maxCalls: opts.maxCallsPerFile });
|
|
360
364
|
const symbols = (ast ? ast.symbols : extractSymbols(rel, ext, content)).slice(0, 400);
|
|
361
365
|
// Add barrel re-exports the local def didn't already cover.
|
|
362
366
|
const known = new Set(symbols.map((s) => s.name));
|
|
@@ -378,7 +382,7 @@ export function extractCode(rel: string, ext: string, content: string): CodeInfo
|
|
|
378
382
|
// collector otherwise, so caller indexes exist without the wasm sidecar.
|
|
379
383
|
// `symbols` (this file's own regex-extracted defs) lets the collector
|
|
380
384
|
// exclude a definition's own name+line from its call candidates.
|
|
381
|
-
calls: ast ? ast.calls : collectCallsRegex(content, symbols),
|
|
385
|
+
calls: ast ? ast.calls : collectCallsRegex(content, symbols, opts.maxCallsPerFile),
|
|
382
386
|
importedNames: ast?.importedNames,
|
|
383
387
|
};
|
|
384
388
|
}
|
package/src/lang/js-ts.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { scan, type Rule } from "./common.js";
|
|
|
7
7
|
const RULES: Rule[] = [
|
|
8
8
|
{ re: /^\s*export\s+(?:async\s+)?function\s+(?<name>[\w$]+)/, kind: "function", exported: true },
|
|
9
9
|
{ re: /^\s*export\s+default\s+(?:async\s+)?function\s+(?<name>[\w$]+)/, kind: "function", exported: true },
|
|
10
|
-
{ re: /^\s*export\s+default\s+(?:abstract\s+)?class\s+(?<name>[\w$]+)/, kind: "class", exported: true },
|
|
10
|
+
{ re: /^\s*export\s+default\s+(?:abstract\s+)?class\s+(?!extends\b)(?<name>[\w$]+)/, kind: "class", exported: true },
|
|
11
11
|
{ re: /^\s*(?:async\s+)?function\s+(?<name>[\w$]+)/, kind: "function", exported: false },
|
|
12
12
|
{ re: /^\s*export\s+(?:abstract\s+)?class\s+(?<name>[\w$]+)/, kind: "class", exported: true },
|
|
13
13
|
{ re: /^\s*(?:abstract\s+)?class\s+(?<name>[\w$]+)/, kind: "class", exported: false },
|
package/src/mcp.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// `repo` path and returns JSON text content.
|
|
6
6
|
//
|
|
7
7
|
// Register in an MCP client as: node scripts/engine.mjs mcp
|
|
8
|
+
import { statSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
8
10
|
import { createInterface } from "node:readline";
|
|
9
11
|
import { ENGINE_VERSION } from "./types.js";
|
|
10
12
|
import { ensureGrammars, allGrammarKeys } from "./ast/loader.js";
|
|
@@ -25,7 +27,7 @@ import { replaceSymbolBody, insertAfterSymbol, insertBeforeSymbol } from "./edit
|
|
|
25
27
|
import { writeMemory, readMemory, deleteMemory, listMemories } from "./memory.js";
|
|
26
28
|
import { searchIndex } from "./bm25.js";
|
|
27
29
|
import { checkRules, parseRules } from "./rules.js";
|
|
28
|
-
import { EMBED_VERSION, resolveEmbedModelDir, loadEmbedModel } from "./embed/model.js";
|
|
30
|
+
import { EMBED_VERSION, resolveEmbedModelDir, loadEmbedModel, type StaticEmbedModel } from "./embed/model.js";
|
|
29
31
|
import { buildEmbeddingIndex, type EmbeddingIndex } from "./embed/index.js";
|
|
30
32
|
import { searchSemantic } from "./embed/search.js";
|
|
31
33
|
import { resolveEmbedEndpoint, buildEndpointIndex, encodeQueryViaEndpoint, probeEndpoint } from "./embed/endpoint.js";
|
|
@@ -366,6 +368,31 @@ export async function memoizedEmbeddingIndex(
|
|
|
366
368
|
return index;
|
|
367
369
|
}
|
|
368
370
|
|
|
371
|
+
// A SINGLE entry — never an unbounded map — holding the most recent parse.
|
|
372
|
+
let embedModelCache: { key: string; model: StaticEmbedModel } | undefined;
|
|
373
|
+
|
|
374
|
+
// model.json is 10-30 MB with the real asset; reading + JSON.parsing it on
|
|
375
|
+
// EVERY request dominates static-tier latency, so the parsed model is memoized
|
|
376
|
+
// across requests. One statSync per request keys the cache on
|
|
377
|
+
// (dir, mtimeMs, size) so an in-place re-pull invalidates on the next call.
|
|
378
|
+
// Same discipline as memoizedEmbeddingIndex: a failed load is NEVER cached —
|
|
379
|
+
// the throw propagates and the cache is left as it was, so the next request
|
|
380
|
+
// retries from scratch. A missing model.json returns undefined (the
|
|
381
|
+
// not-present case, exactly like loadEmbedModel).
|
|
382
|
+
export function memoizedEmbedModel(modelDir: string): StaticEmbedModel | undefined {
|
|
383
|
+
let stat;
|
|
384
|
+
try {
|
|
385
|
+
stat = statSync(join(modelDir, "model.json"));
|
|
386
|
+
} catch {
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
const key = `${modelDir}:${stat.mtimeMs}:${stat.size}`;
|
|
390
|
+
if (embedModelCache && embedModelCache.key === key) return embedModelCache.model;
|
|
391
|
+
const model = loadEmbedModel(modelDir);
|
|
392
|
+
if (model) embedModelCache = { key, model };
|
|
393
|
+
return model;
|
|
394
|
+
}
|
|
395
|
+
|
|
369
396
|
async function callTool(name: string, args: Record<string, unknown>): Promise<string> {
|
|
370
397
|
const repo = str(args.repo);
|
|
371
398
|
if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
|
|
@@ -530,7 +557,7 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<st
|
|
|
530
557
|
}
|
|
531
558
|
}
|
|
532
559
|
const modelDir = resolveEmbedModelDir(repo);
|
|
533
|
-
const model = modelDir ?
|
|
560
|
+
const model = modelDir ? memoizedEmbedModel(modelDir) : undefined;
|
|
534
561
|
if (model) {
|
|
535
562
|
const index = await memoizedEmbeddingIndex(
|
|
536
563
|
{ mode: "static", identity: `${modelDir}#${model.modelId}`, scan },
|
|
@@ -552,7 +579,7 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<st
|
|
|
552
579
|
}
|
|
553
580
|
if (name === "embed_status") {
|
|
554
581
|
const modelDir = resolveEmbedModelDir(repo);
|
|
555
|
-
const model = modelDir ?
|
|
582
|
+
const model = modelDir ? memoizedEmbedModel(modelDir) : undefined;
|
|
556
583
|
const endpoint = resolveEmbedEndpoint();
|
|
557
584
|
const mode: "none" | "static" | "endpoint" = endpoint ? "endpoint" : model ? "static" : "none";
|
|
558
585
|
const status: Record<string, unknown> = {
|
package/src/scan.ts
CHANGED
|
@@ -36,8 +36,15 @@ export interface ScanOptions {
|
|
|
36
36
|
scope?: string;
|
|
37
37
|
// Honor .gitignore files (default true — see WalkOptions.gitignore).
|
|
38
38
|
gitignore?: boolean;
|
|
39
|
+
// Directory names to skip — REPLACES the default set (see
|
|
40
|
+
// WalkOptions.ignoreDirs; compose with the IGNORE_DIRS export to extend it).
|
|
41
|
+
ignoreDirs?: string[];
|
|
39
42
|
maxBytes?: number;
|
|
40
43
|
maxFiles?: number;
|
|
44
|
+
// Per-file call-site cap for extraction (default 512, both AST and regex
|
|
45
|
+
// tiers). Raising it trades index size for call-graph recall; dedup/sort
|
|
46
|
+
// semantics are unchanged. Absent, output is byte-identical to before.
|
|
47
|
+
maxCallsPerFile?: number;
|
|
41
48
|
out?: string; // absolute output dir to exclude from the scan (self-index guard)
|
|
42
49
|
// Previous build's extraction cache (rel → {hash, record, size?, mtimeMs?}). A
|
|
43
50
|
// file whose (size,mtime) key matches skips read+hash entirely (the stat
|
|
@@ -65,6 +72,7 @@ export function scanRepo(root: string, opts: ScanOptions = {}): RepoScan {
|
|
|
65
72
|
maxFileBytes: opts.maxBytes,
|
|
66
73
|
maxFiles: opts.maxFiles,
|
|
67
74
|
gitignore: opts.gitignore,
|
|
75
|
+
ignoreDirs: opts.ignoreDirs,
|
|
68
76
|
});
|
|
69
77
|
// Never index our own output (e.g. a committed `docs/ultraindex/`), or builds
|
|
70
78
|
// would describe the encyclopedia instead of the code.
|
|
@@ -144,7 +152,7 @@ export function scanRepo(root: string, opts: ScanOptions = {}): RepoScan {
|
|
|
144
152
|
// Non-markdown prose (.rst/.txt): title from basename, no link graph.
|
|
145
153
|
record.title = basename(f.rel);
|
|
146
154
|
} else if (kind === "code") {
|
|
147
|
-
const code = extractCode(f.rel, f.ext, content);
|
|
155
|
+
const code = extractCode(f.rel, f.ext, content, { maxCallsPerFile: opts.maxCallsPerFile });
|
|
148
156
|
record.title = basename(f.rel);
|
|
149
157
|
record.summary = code.summary;
|
|
150
158
|
record.symbols = code.symbols;
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Single source of truth for the engine version the bundle reports. Kept in
|
|
2
2
|
// lockstep with package.json by the release pipeline. Do not edit by hand
|
|
3
3
|
// outside a release.
|
|
4
|
-
export const ENGINE_VERSION = "2.
|
|
4
|
+
export const ENGINE_VERSION = "2.13.0";
|
|
5
5
|
|
|
6
6
|
// Bumped whenever the on-disk artifact shape changes, so a consumer can reject
|
|
7
7
|
// an index written by an incompatible engine instead of misreading it. The
|
|
@@ -37,8 +37,11 @@ export const SCHEMA_VERSION = 4;
|
|
|
37
37
|
// resolved export-alias symbol also cite the original declaration's own line
|
|
38
38
|
// (and endLine, when the AST tier populated one) instead of the export
|
|
39
39
|
// statement's line — a citation-precision fix (issue #9) for consumers
|
|
40
|
-
// (ultradoc) that use file:line as evidence
|
|
41
|
-
|
|
40
|
+
// (ultradoc) that use file:line as evidence; v10 stops the regex tier
|
|
41
|
+
// emitting a spurious exported class symbol named "extends" for
|
|
42
|
+
// `export default class extends Base {}` (issue #11) — the file-stem
|
|
43
|
+
// default symbol is unchanged.
|
|
44
|
+
export const EXTRACTOR_VERSION = 10;
|
|
42
45
|
|
|
43
46
|
// How a file is classified. `code` gets symbol/import extraction; `doc` gets
|
|
44
47
|
// link/heading extraction; the rest are catalogued but not deeply parsed.
|
package/src/walk.ts
CHANGED
|
@@ -4,12 +4,15 @@ import { parseGitignore, isIgnored, type IgnoreRule } from "./ignore.js";
|
|
|
4
4
|
|
|
5
5
|
// Directories that never carry signal for a documentation/code question and
|
|
6
6
|
// would bloat the index (dependencies, build output, VCS internals, caches).
|
|
7
|
+
// .codeindex is the engine's OWN output (index artifacts, pulled models, MCP
|
|
8
|
+
// memories) — indexing it would feed memories into search and churn the scan
|
|
9
|
+
// fingerprint on every write_memory (issue #12).
|
|
7
10
|
// Exported so grep.ts can align ripgrep's universe with the walker's.
|
|
8
11
|
export const IGNORE_DIRS = new Set([
|
|
9
12
|
".git", "node_modules", ".pnpm", "bower_components", "vendor", "dist", "build", "out",
|
|
10
13
|
"target", ".next", ".nuxt", ".svelte-kit", ".turbo", "coverage", "__pycache__", ".venv",
|
|
11
14
|
"venv", ".tox", ".mypy_cache", ".pytest_cache", ".gradle", ".idea", ".vscode", ".cache",
|
|
12
|
-
"tmp", ".ultraindex", "Pods", "DerivedData", ".terraform", "elm-stuff", ".dart_tool",
|
|
15
|
+
"tmp", ".ultraindex", ".codeindex", "Pods", "DerivedData", ".terraform", "elm-stuff", ".dart_tool",
|
|
13
16
|
]);
|
|
14
17
|
|
|
15
18
|
// Lockfiles: huge, machine-generated, and pure noise for a code/docs question —
|
|
@@ -36,6 +39,13 @@ export interface WalkOptions {
|
|
|
36
39
|
// semantics — see ignore.ts). Default TRUE: an ignored file is noise for
|
|
37
40
|
// every consumer; pass false to index generated/ignored trees deliberately.
|
|
38
41
|
gitignore?: boolean;
|
|
42
|
+
// Directory names to skip, REPLACING the default set entirely (not merging
|
|
43
|
+
// with it). IGNORE_DIRS is a public export, so consumers compose
|
|
44
|
+
// `[...IGNORE_DIRS, "extra"]` — or filter it — themselves; replace is the
|
|
45
|
+
// simplest contract. Deliberate scope boundary: grep.ts (the ripgrep
|
|
46
|
+
// universe) and the MCP server keep the DEFAULT set — recall consumers
|
|
47
|
+
// (e.g. ultrasec) consume scan/extract, not grep.
|
|
48
|
+
ignoreDirs?: string[];
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
export interface WalkedFile {
|
|
@@ -65,6 +75,9 @@ export function walk(root: string, opts: WalkOptions = {}): WalkResult {
|
|
|
65
75
|
const maxFileBytes = opts.maxFileBytes ?? 1024 * 1024;
|
|
66
76
|
const maxFiles = opts.maxFiles ?? DEFAULT_MAX_FILES;
|
|
67
77
|
const useGitignore = opts.gitignore !== false;
|
|
78
|
+
// Effective ignored-directory set, built once: the caller's replacement when
|
|
79
|
+
// given (see WalkOptions.ignoreDirs — replace, never merge), else the default.
|
|
80
|
+
const ignoreDirs = opts.ignoreDirs ? new Set(opts.ignoreDirs) : IGNORE_DIRS;
|
|
68
81
|
const out: WalkedFile[] = [];
|
|
69
82
|
let capped = false;
|
|
70
83
|
let excluded = 0;
|
|
@@ -125,7 +138,7 @@ export function walk(root: string, opts: WalkOptions = {}): WalkResult {
|
|
|
125
138
|
continue;
|
|
126
139
|
}
|
|
127
140
|
if (st.isDirectory()) {
|
|
128
|
-
if (
|
|
141
|
+
if (ignoreDirs.has(name)) continue;
|
|
129
142
|
// An in-repo DIRECTORY symlink is skipped entirely: its target is (or
|
|
130
143
|
// will be) walked under its canonical name, and letting both paths race
|
|
131
144
|
// through the cycle guard would keep whichever readdir served first —
|