@maxgfr/codeindex 2.12.0 → 2.14.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 +35 -11
- package/docs/MIGRATION.md +155 -36
- package/docs/SEMANTIC.md +12 -5
- package/package.json +1 -1
- package/scripts/engine.d.mts +49 -7
- package/scripts/engine.mjs +1233 -749
- package/src/ast/extract.ts +7 -5
- package/src/ast/grammars-pull.ts +178 -0
- package/src/ast/loader.ts +77 -15
- package/src/bm25.ts +16 -6
- package/src/callers.ts +0 -0
- package/src/complexity.ts +7 -1
- package/src/deadcode.ts +5 -4
- package/src/derived.ts +128 -0
- package/src/embed/model.ts +23 -14
- package/src/engine-cli.ts +307 -51
- package/src/engine.ts +21 -4
- package/src/extract/code.ts +9 -5
- package/src/hash.ts +5 -2
- package/src/lang/js-ts.ts +1 -1
- package/src/mcp.ts +220 -26
- package/src/pipeline.ts +16 -5
- package/src/query.ts +8 -5
- package/src/scan.ts +74 -8
- package/src/types.ts +6 -3
- package/src/walk.ts +41 -11
package/src/walk.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { readdirSync, statSync, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
1
|
+
import { readdirSync, statSync, lstatSync, readFileSync, realpathSync, type Dirent } from "node:fs";
|
|
2
2
|
import { join, relative, sep, extname } from "node:path";
|
|
3
3
|
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;
|
|
@@ -100,32 +113,49 @@ export function walk(root: string, opts: WalkOptions = {}): WalkResult {
|
|
|
100
113
|
if (seenDirs.has(real)) continue;
|
|
101
114
|
seenDirs.add(real);
|
|
102
115
|
if (!contained(real)) continue; // dir symlink escaping the repo
|
|
103
|
-
let entries:
|
|
116
|
+
let entries: Dirent[];
|
|
104
117
|
try {
|
|
105
118
|
// Sorted so the walk order — and therefore WHICH files survive a
|
|
106
|
-
// maxFiles cap — is identical across filesystems and machines.
|
|
107
|
-
|
|
119
|
+
// maxFiles cap — is identical across filesystems and machines. Dirents
|
|
120
|
+
// sort by .name under the same code-unit comparison the bare-string
|
|
121
|
+
// sort used, so the order is byte-identical to the previous readdir.
|
|
122
|
+
entries = readdirSync(frame.dir, { withFileTypes: true }).sort((a, b) =>
|
|
123
|
+
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
124
|
+
);
|
|
108
125
|
} catch {
|
|
109
126
|
continue;
|
|
110
127
|
}
|
|
111
128
|
let rules = frame.rules;
|
|
112
|
-
if (useGitignore && entries.
|
|
129
|
+
if (useGitignore && entries.some((e) => e.name === ".gitignore")) {
|
|
113
130
|
const parsed = parseGitignore(readText(join(frame.dir, ".gitignore")), frame.rel);
|
|
114
131
|
if (parsed.length) rules = [...rules, ...parsed];
|
|
115
132
|
}
|
|
116
|
-
for (const
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
const name = entry.name;
|
|
117
135
|
const abs = join(frame.dir, name);
|
|
118
136
|
const rel = frame.rel ? `${frame.rel}/${name}` : name;
|
|
137
|
+
// The dirent type IS the lstat type (Node lstats internally when the
|
|
138
|
+
// filesystem can't supply it), so no lstat is needed to detect links.
|
|
139
|
+
const isLink = entry.isSymbolicLink();
|
|
140
|
+
// Ignored-directory boundary (node_modules, .git…): skip on the dirent
|
|
141
|
+
// type alone — ZERO stat syscalls. A symlink reports isDirectory()
|
|
142
|
+
// false on its dirent and falls through to the stat-based
|
|
143
|
+
// classification below, so a link named node_modules still classifies
|
|
144
|
+
// by its target exactly as before.
|
|
145
|
+
if (entry.isDirectory() && ignoreDirs.has(name)) continue;
|
|
119
146
|
let st;
|
|
120
|
-
let isLink: boolean;
|
|
121
147
|
try {
|
|
122
|
-
|
|
123
|
-
|
|
148
|
+
// Non-links: a single lstatSync supplies isDirectory/isFile/size/
|
|
149
|
+
// mtimeMs — field-for-field what the previous statSync returned,
|
|
150
|
+
// since with no link to follow the two calls are identical. Links:
|
|
151
|
+
// keep the following statSync so the entry classifies by its TARGET;
|
|
152
|
+
// a broken link throws here and is skipped, same as before.
|
|
153
|
+
st = isLink ? statSync(abs) : lstatSync(abs);
|
|
124
154
|
} catch {
|
|
125
155
|
continue;
|
|
126
156
|
}
|
|
127
157
|
if (st.isDirectory()) {
|
|
128
|
-
if (
|
|
158
|
+
if (ignoreDirs.has(name)) continue;
|
|
129
159
|
// An in-repo DIRECTORY symlink is skipped entirely: its target is (or
|
|
130
160
|
// will be) walked under its canonical name, and letting both paths race
|
|
131
161
|
// through the cycle guard would keep whichever readdir served first —
|