@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/engine-cli.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { SCHEMA_VERSION, EXTRACTOR_VERSION, type FileRecord } from "./types.js";
|
|
4
4
|
import { ENGINE_VERSION } from "./types.js";
|
|
5
|
-
import { ensureGrammars,
|
|
6
|
-
import {
|
|
5
|
+
import { ensureGrammars, grammarKeysForExts, resolveGrammarsTier, sharedGrammarsCacheDir } from "./ast/loader.js";
|
|
6
|
+
import {
|
|
7
|
+
resolveGrammarsPullTarget,
|
|
8
|
+
fetchGrammarsTarball,
|
|
9
|
+
fetchExpectedSha256,
|
|
10
|
+
extractGrammarsTarball,
|
|
11
|
+
} from "./ast/grammars-pull.js";
|
|
12
|
+
import { buildIndexArtifacts, buildArtifactsFromScan, type BuildIndexOptions } from "./pipeline.js";
|
|
13
|
+
import { sha1 } from "./hash.js";
|
|
7
14
|
import { renderGraphJson } from "./render/graph-json.js";
|
|
8
15
|
import { renderSymbolsJson } from "./render/symbols-json.js";
|
|
9
16
|
import { renderScip } from "./render/scip.js";
|
|
10
17
|
import { scanRepo } from "./scan.js";
|
|
18
|
+
import { walk, type WalkResult } from "./walk.js";
|
|
11
19
|
import { buildCallerIndex } from "./callers.js";
|
|
12
20
|
import { detectWorkspaces } from "./workspaces.js";
|
|
13
21
|
import { gitChurn } from "./git.js";
|
|
@@ -19,7 +27,7 @@ import { symbolComplexity, riskHotspots } from "./complexity.js";
|
|
|
19
27
|
import { renderMermaid } from "./viz.js";
|
|
20
28
|
import { searchIndex } from "./bm25.js";
|
|
21
29
|
import { checkRules, parseRules } from "./rules.js";
|
|
22
|
-
import { EMBED_VERSION, resolveEmbedModelDir, loadEmbedModel, resolveEmbedPullUrl, fetchEmbedModel } from "./embed/model.js";
|
|
30
|
+
import { EMBED_VERSION, resolveEmbedModelDir, loadEmbedModel, parseEmbedModel, resolveEmbedPullUrl, fetchEmbedModel } from "./embed/model.js";
|
|
23
31
|
import { buildEmbeddingIndex, serializeEmbeddings } from "./embed/index.js";
|
|
24
32
|
import { searchSemantic } from "./embed/search.js";
|
|
25
33
|
import {
|
|
@@ -60,6 +68,14 @@ Commands:
|
|
|
60
68
|
the source with CODEINDEX_EMBED_URL
|
|
61
69
|
embed serve Print (or --run) the docker command that starts the
|
|
62
70
|
containerized embedding server (rich tier)
|
|
71
|
+
grammars Tree-sitter wasm grammars (optional AST tier; regex without them).
|
|
72
|
+
Precedence: bundle-adjacent > CODEINDEX_GRAMMARS_DIR > shared cache:
|
|
73
|
+
grammars status Active tier (adjacent/env/cache/none), resolved
|
|
74
|
+
dir, pinned ENGINE_VERSION, pull-needed (JSON)
|
|
75
|
+
grammars pull Fetch the per-release grammars-<version>.tar.gz
|
|
76
|
+
asset into the shared cache (sha256-verified,
|
|
77
|
+
atomic). Override the source with
|
|
78
|
+
CODEINDEX_GRAMMARS_URL
|
|
63
79
|
rules Architecture rules (forbidden edges, cycles, orphans) validated
|
|
64
80
|
against the link-graph: --config <codeindex.rules.json>; exits 1
|
|
65
81
|
on any error-severity violation (a CI gate)
|
|
@@ -80,8 +96,11 @@ Flags:
|
|
|
80
96
|
--exclude <glob> Exclude matching paths (repeatable)
|
|
81
97
|
--scope <dir> Restrict to one directory (sugar for --include '<dir>/**')
|
|
82
98
|
--no-gitignore Do not honor .gitignore files (default: honored)
|
|
99
|
+
--ignore-dir <name> Directory names to skip (repeatable) — REPLACES the
|
|
100
|
+
default ignored-directory set, never merges with it
|
|
83
101
|
--max-files <n> Cap walked files (default 20000)
|
|
84
102
|
--max-bytes <n> Skip files above this size (default 1 MiB)
|
|
103
|
+
--max-calls <n> Per-file call-site cap for extraction (default 512)
|
|
85
104
|
--no-ast Skip tree-sitter grammars even when present (regex tier)
|
|
86
105
|
--config <file> Rules config for \`rules\` (JSON: [{name, from, to, …}])
|
|
87
106
|
--limit <n> Max results for \`search\` (default 20)
|
|
@@ -103,8 +122,10 @@ interface CliFlags {
|
|
|
103
122
|
exclude: string[];
|
|
104
123
|
scope?: string;
|
|
105
124
|
gitignore: boolean;
|
|
125
|
+
ignoreDirs: string[];
|
|
106
126
|
maxFiles?: number;
|
|
107
127
|
maxBytes?: number;
|
|
128
|
+
maxCalls?: number;
|
|
108
129
|
noAst: boolean;
|
|
109
130
|
since?: string;
|
|
110
131
|
ignoreCase?: boolean;
|
|
@@ -121,7 +142,7 @@ interface CliFlags {
|
|
|
121
142
|
}
|
|
122
143
|
|
|
123
144
|
function parseFlags(args: string[]): CliFlags {
|
|
124
|
-
const flags: CliFlags = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false, fuzzy: true, semantic: false };
|
|
145
|
+
const flags: CliFlags = { repo: process.cwd(), include: [], exclude: [], gitignore: true, ignoreDirs: [], noAst: false, fuzzy: true, semantic: false };
|
|
125
146
|
for (let i = 0; i < args.length; i++) {
|
|
126
147
|
const a = args[i]!;
|
|
127
148
|
const next = (): string => {
|
|
@@ -144,8 +165,10 @@ function parseFlags(args: string[]): CliFlags {
|
|
|
144
165
|
else if (a === "--exclude") flags.exclude.push(next());
|
|
145
166
|
else if (a === "--scope") flags.scope = next();
|
|
146
167
|
else if (a === "--no-gitignore") flags.gitignore = false;
|
|
168
|
+
else if (a === "--ignore-dir") flags.ignoreDirs.push(next());
|
|
147
169
|
else if (a === "--max-files") flags.maxFiles = num();
|
|
148
170
|
else if (a === "--max-bytes") flags.maxBytes = num();
|
|
171
|
+
else if (a === "--max-calls") flags.maxCalls = num();
|
|
149
172
|
else if (a === "--ignore-case") flags.ignoreCase = true;
|
|
150
173
|
else if (a === "--max-hits") flags.maxHits = num();
|
|
151
174
|
else if (a === "--budget-tokens") flags.budgetTokens = num();
|
|
@@ -168,17 +191,32 @@ function emit(content: string, out?: string): void {
|
|
|
168
191
|
else process.stdout.write(content);
|
|
169
192
|
}
|
|
170
193
|
|
|
171
|
-
function scanOptions(flags: CliFlags): BuildIndexOptions {
|
|
194
|
+
function scanOptions(flags: CliFlags, precomputedWalk?: WalkResult): BuildIndexOptions {
|
|
172
195
|
return {
|
|
173
196
|
include: flags.include.length ? flags.include : undefined,
|
|
174
197
|
exclude: flags.exclude.length ? flags.exclude : undefined,
|
|
175
198
|
scope: flags.scope,
|
|
176
199
|
gitignore: flags.gitignore,
|
|
200
|
+
ignoreDirs: flags.ignoreDirs.length ? flags.ignoreDirs : undefined,
|
|
177
201
|
maxFiles: flags.maxFiles,
|
|
178
202
|
maxBytes: flags.maxBytes,
|
|
203
|
+
maxCallsPerFile: flags.maxCalls,
|
|
204
|
+
// The walk performed once in runCli to warm the present-language grammars,
|
|
205
|
+
// reused here so scanRepo does not traverse the tree a second time. Absent
|
|
206
|
+
// for --no-ast / scan-less commands: scanRepo walks itself, unchanged.
|
|
207
|
+
precomputedWalk,
|
|
179
208
|
};
|
|
180
209
|
}
|
|
181
210
|
|
|
211
|
+
// Commands that never walk/scan the file tree — they read git/grep directly or
|
|
212
|
+
// only manage the grammar cache, so they must warm NO tree-sitter grammar (the
|
|
213
|
+
// CLI previously warmed every one unconditionally). `embed` is scan-only for its
|
|
214
|
+
// `build` subcommand; the other embed subcommands (status/pull/serve) are
|
|
215
|
+
// excluded by the positional check at the warm site. `grammars` (status/pull)
|
|
216
|
+
// resolves/downloads the wasms itself and must not warm them.
|
|
217
|
+
// version/help/mcp return before we get there.
|
|
218
|
+
const SCANLESS_COMMANDS = new Set(["grep", "churn", "coupling", "workspaces", "grammars"]);
|
|
219
|
+
|
|
182
220
|
export async function runCli(argv: string[]): Promise<void> {
|
|
183
221
|
const [cmd, ...rest] = argv;
|
|
184
222
|
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
@@ -197,7 +235,25 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
197
235
|
|
|
198
236
|
const flags = parseFlags(rest);
|
|
199
237
|
if (!existsSync(flags.repo)) throw new Error(`--repo path does not exist: ${flags.repo}`);
|
|
200
|
-
|
|
238
|
+
|
|
239
|
+
// Warm ONLY the grammars for languages actually present, and only for commands
|
|
240
|
+
// that scan the file tree. Scan-less commands (grep, churn, coupling,
|
|
241
|
+
// workspaces, embed status|pull|serve) load no grammar at all; version/help/mcp
|
|
242
|
+
// already returned above. The walk is done ONCE here to derive the present
|
|
243
|
+
// extensions, then handed to the scan via precomputedWalk so the tree is
|
|
244
|
+
// traversed a single time. --no-ast keeps the regex tier: no walk, no warm —
|
|
245
|
+
// scanRepo walks itself, exactly as before.
|
|
246
|
+
const scans = !SCANLESS_COMMANDS.has(cmd) && !(cmd === "embed" && flags.positional !== "build");
|
|
247
|
+
let precomputedWalk: WalkResult | undefined;
|
|
248
|
+
if (scans && !flags.noAst) {
|
|
249
|
+
precomputedWalk = walk(flags.repo, {
|
|
250
|
+
maxFileBytes: flags.maxBytes,
|
|
251
|
+
maxFiles: flags.maxFiles,
|
|
252
|
+
gitignore: flags.gitignore,
|
|
253
|
+
ignoreDirs: flags.ignoreDirs.length ? flags.ignoreDirs : undefined,
|
|
254
|
+
});
|
|
255
|
+
await ensureGrammars(grammarKeysForExts(precomputedWalk.files.map((f) => f.ext)));
|
|
256
|
+
}
|
|
201
257
|
|
|
202
258
|
if (cmd === "index") {
|
|
203
259
|
if (!flags.out) throw new Error("index needs --out <dir>");
|
|
@@ -206,47 +262,143 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
206
262
|
// Incremental cache: reuse per-file records when (schema, extractor) match —
|
|
207
263
|
// same invalidation discipline as ultraindex's cache.json.
|
|
208
264
|
const cachePath = join(outDir, "cache.json");
|
|
209
|
-
|
|
265
|
+
type CacheEntry = { hash: string; record: FileRecord; size?: number; mtimeMs?: number };
|
|
266
|
+
// ADDITIVE meta keys describing the artifacts the cache-writing run put on
|
|
267
|
+
// disk. Old engines ignore them (they only check schema/extractor above);
|
|
268
|
+
// old caches lacking them simply never take the fastpath below (their
|
|
269
|
+
// per-file records are still reused). cache.json embeds mtimes, so it was
|
|
270
|
+
// never cross-machine byte-reproducible — no determinism surface changes.
|
|
271
|
+
type CacheMeta = {
|
|
272
|
+
engineVersion?: string;
|
|
273
|
+
commit?: string;
|
|
274
|
+
graphSha1?: string;
|
|
275
|
+
symbolsSha1?: string;
|
|
276
|
+
embed?: { embedVersion?: number; modelId?: string; sha1?: string };
|
|
277
|
+
};
|
|
278
|
+
let cache: Map<string, CacheEntry> | undefined;
|
|
279
|
+
let meta: CacheMeta = {};
|
|
210
280
|
try {
|
|
211
281
|
const parsed = JSON.parse(readFileSync(cachePath, "utf8")) as {
|
|
212
282
|
schemaVersion: number;
|
|
213
283
|
extractorVersion: number;
|
|
214
|
-
files: Record<string,
|
|
215
|
-
};
|
|
284
|
+
files: Record<string, CacheEntry>;
|
|
285
|
+
} & CacheMeta;
|
|
216
286
|
if (parsed.schemaVersion === SCHEMA_VERSION && parsed.extractorVersion === EXTRACTOR_VERSION) {
|
|
217
287
|
cache = new Map(Object.entries(parsed.files));
|
|
288
|
+
meta = {
|
|
289
|
+
engineVersion: parsed.engineVersion,
|
|
290
|
+
commit: parsed.commit,
|
|
291
|
+
graphSha1: parsed.graphSha1,
|
|
292
|
+
symbolsSha1: parsed.symbolsSha1,
|
|
293
|
+
embed: parsed.embed,
|
|
294
|
+
};
|
|
218
295
|
}
|
|
219
296
|
} catch {
|
|
220
297
|
// no cache yet (or unreadable) — cold build
|
|
221
298
|
}
|
|
222
|
-
const
|
|
223
|
-
writeFileSync(join(outDir, "graph.json"), renderGraphJson(graph));
|
|
224
|
-
writeFileSync(join(outDir, "symbols.json"), renderSymbolsJson(symbols));
|
|
225
|
-
const files: Record<string, { hash: string; record: FileRecord; size?: number; mtimeMs?: number }> = {};
|
|
226
|
-
for (const f of scan.files) {
|
|
227
|
-
const entry: { hash: string; record: FileRecord; size?: number; mtimeMs?: number } = { hash: f.hash, record: f, size: f.size };
|
|
228
|
-
const mtime = scan.mtimes.get(f.rel);
|
|
229
|
-
if (mtime !== undefined) entry.mtimeMs = mtime;
|
|
230
|
-
files[f.rel] = entry;
|
|
231
|
-
}
|
|
232
|
-
writeFileSync(
|
|
233
|
-
cachePath,
|
|
234
|
-
JSON.stringify({ schemaVersion: SCHEMA_VERSION, extractorVersion: EXTRACTOR_VERSION, files }) + "\n",
|
|
235
|
-
);
|
|
236
|
-
// Deterministic embeddings sidecar: written next to graph.json ONLY when a
|
|
237
|
-
// model asset is present (opt-in). Silently skipped otherwise — no model, no
|
|
238
|
-
// embeddings.bin, no impact on the graph/symbols consumers.
|
|
239
|
-
let embedNote = "";
|
|
299
|
+
const scan = scanRepo(flags.repo, { ...scanOptions(flags, precomputedWalk), cache, out: outDir });
|
|
240
300
|
const modelDir = resolveEmbedModelDir(flags.repo);
|
|
241
301
|
const model = modelDir ? loadEmbedModel(modelDir) : undefined;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
302
|
+
|
|
303
|
+
const graphPath = join(outDir, "graph.json");
|
|
304
|
+
const symbolsPath = join(outDir, "symbols.json");
|
|
305
|
+
const embedPath = join(outDir, "embeddings.bin");
|
|
306
|
+
// sha of an on-disk artifact, or undefined when it is missing/unreadable —
|
|
307
|
+
// never equal to a defined meta sha, so a deleted artifact fails the guard.
|
|
308
|
+
const artifactSha = (path: string): string | undefined => {
|
|
309
|
+
try {
|
|
310
|
+
return sha1(readFileSync(path));
|
|
311
|
+
} catch {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const writeCache = (out: Pick<CacheMeta, "graphSha1" | "symbolsSha1" | "embed">): void => {
|
|
316
|
+
const files: Record<string, CacheEntry> = {};
|
|
317
|
+
for (const f of scan.files) {
|
|
318
|
+
const entry: CacheEntry = { hash: f.hash, record: f, size: f.size };
|
|
319
|
+
const mtime = scan.mtimes.get(f.rel);
|
|
320
|
+
if (mtime !== undefined) entry.mtimeMs = mtime;
|
|
321
|
+
files[f.rel] = entry;
|
|
322
|
+
}
|
|
323
|
+
// Fixed key order; JSON.stringify drops the undefined-valued keys
|
|
324
|
+
// (commit outside a git worktree, embed without a model) cleanly.
|
|
325
|
+
writeFileSync(
|
|
326
|
+
cachePath,
|
|
327
|
+
JSON.stringify({
|
|
328
|
+
schemaVersion: SCHEMA_VERSION,
|
|
329
|
+
extractorVersion: EXTRACTOR_VERSION,
|
|
330
|
+
engineVersion: ENGINE_VERSION,
|
|
331
|
+
commit: scan.commit,
|
|
332
|
+
graphSha1: out.graphSha1,
|
|
333
|
+
symbolsSha1: out.symbolsSha1,
|
|
334
|
+
embed: out.embed,
|
|
335
|
+
files,
|
|
336
|
+
}) + "\n",
|
|
337
|
+
);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// FASTPATH GUARD — skip the whole downstream pipeline only when this scan
|
|
341
|
+
// is proven identical to the run that wrote the on-disk artifacts:
|
|
342
|
+
// contentUnchanged means this scan's records are object-identical to that
|
|
343
|
+
// run's; downstream is a pure function of (records, docText, commit,
|
|
344
|
+
// meta-opts) and the CLI never sets meta/previousCommunities;
|
|
345
|
+
// engineVersion pins the version stamp; commit must match because
|
|
346
|
+
// graph.json embeds it (identical trees under a new HEAD must rebuild);
|
|
347
|
+
// the shas prove the on-disk bytes are that run's output. ANY failure —
|
|
348
|
+
// deleted or tampered artifacts included — falls through to the full
|
|
349
|
+
// build, which rewrites everything (self-healing).
|
|
350
|
+
const embedUnchanged =
|
|
351
|
+
!model ||
|
|
352
|
+
(meta.embed !== undefined &&
|
|
353
|
+
meta.embed.embedVersion === EMBED_VERSION &&
|
|
354
|
+
meta.embed.modelId === model.modelId &&
|
|
355
|
+
meta.embed.sha1 !== undefined &&
|
|
356
|
+
artifactSha(embedPath) === meta.embed.sha1);
|
|
357
|
+
const fastpath =
|
|
358
|
+
scan.contentUnchanged &&
|
|
359
|
+
meta.engineVersion === ENGINE_VERSION &&
|
|
360
|
+
meta.commit === scan.commit &&
|
|
361
|
+
meta.graphSha1 !== undefined &&
|
|
362
|
+
artifactSha(graphPath) === meta.graphSha1 &&
|
|
363
|
+
meta.symbolsSha1 !== undefined &&
|
|
364
|
+
artifactSha(symbolsPath) === meta.symbolsSha1 &&
|
|
365
|
+
embedUnchanged;
|
|
366
|
+
|
|
367
|
+
if (fastpath) {
|
|
368
|
+
// Artifacts verified byte-identical to what this build would produce —
|
|
369
|
+
// leave them untouched. Rewrite cache.json only when the scan says its
|
|
370
|
+
// bytes would change (e.g. an mtime drifted); the meta is carried
|
|
371
|
+
// forward verbatim since the guard just proved it describes the disk.
|
|
372
|
+
if (scan.cacheDirty) writeCache(meta);
|
|
373
|
+
process.stderr.write(
|
|
374
|
+
`codeindex: ${scan.files.length} files → ${outDir}/graph.json + symbols.json${scan.capped ? " (capped)" : ""} (unchanged — artifacts reused)\n`,
|
|
375
|
+
);
|
|
376
|
+
} else {
|
|
377
|
+
const { graph, symbols } = buildArtifactsFromScan(scan);
|
|
378
|
+
const graphJson = renderGraphJson(graph);
|
|
379
|
+
const symbolsJson = renderSymbolsJson(symbols);
|
|
380
|
+
writeFileSync(graphPath, graphJson);
|
|
381
|
+
writeFileSync(symbolsPath, symbolsJson);
|
|
382
|
+
// Deterministic embeddings sidecar: written next to graph.json ONLY when a
|
|
383
|
+
// model asset is present (opt-in). Silently skipped otherwise — no model, no
|
|
384
|
+
// embeddings.bin, no impact on the graph/symbols consumers.
|
|
385
|
+
let embedNote = "";
|
|
386
|
+
let embedMeta: CacheMeta["embed"];
|
|
387
|
+
if (model) {
|
|
388
|
+
const index = buildEmbeddingIndex(scan, model);
|
|
389
|
+
const bytes = serializeEmbeddings(index);
|
|
390
|
+
writeFileSync(embedPath, bytes);
|
|
391
|
+
embedMeta = { embedVersion: EMBED_VERSION, modelId: model.modelId, sha1: sha1(bytes) };
|
|
392
|
+
embedNote = ` + embeddings.bin (${index.records.length} records, model ${model.modelId})`;
|
|
393
|
+
}
|
|
394
|
+
// cache.json is written LAST so its meta always describes artifacts that
|
|
395
|
+
// are already on disk — a crash mid-way leaves stale meta whose shas
|
|
396
|
+
// fail the guard on the next run (safe: it just rebuilds).
|
|
397
|
+
writeCache({ graphSha1: sha1(graphJson), symbolsSha1: sha1(symbolsJson), embed: embedMeta });
|
|
398
|
+
process.stderr.write(`codeindex: ${scan.files.length} files → ${outDir}/graph.json + symbols.json${embedNote}${scan.capped ? " (capped)" : ""}\n`);
|
|
246
399
|
}
|
|
247
|
-
process.stderr.write(`codeindex: ${scan.files.length} files → ${outDir}/graph.json + symbols.json${embedNote}${scan.capped ? " (capped)" : ""}\n`);
|
|
248
400
|
} else if (cmd === "scan") {
|
|
249
|
-
const { scan } = buildIndexArtifacts(flags.repo, scanOptions(flags));
|
|
401
|
+
const { scan } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
|
|
250
402
|
const summary = {
|
|
251
403
|
engineVersion: ENGINE_VERSION,
|
|
252
404
|
commit: scan.commit,
|
|
@@ -256,13 +408,13 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
256
408
|
};
|
|
257
409
|
emit(JSON.stringify(summary, null, 2) + "\n", flags.out);
|
|
258
410
|
} else if (cmd === "graph") {
|
|
259
|
-
const { graph } = buildIndexArtifacts(flags.repo, scanOptions(flags));
|
|
411
|
+
const { graph } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
|
|
260
412
|
emit(renderGraphJson(graph), flags.out);
|
|
261
413
|
} else if (cmd === "symbols") {
|
|
262
|
-
const { symbols } = buildIndexArtifacts(flags.repo, scanOptions(flags));
|
|
414
|
+
const { symbols } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
|
|
263
415
|
emit(renderSymbolsJson(symbols), flags.out);
|
|
264
416
|
} else if (cmd === "scip") {
|
|
265
|
-
const scan = scanRepo(flags.repo, scanOptions(flags));
|
|
417
|
+
const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
|
|
266
418
|
const bytes = renderScip(scan, { projectRoot: flags.projectRoot });
|
|
267
419
|
const out = flags.out ?? resolve("index.scip");
|
|
268
420
|
if (out === "-") process.stdout.write(Buffer.from(bytes));
|
|
@@ -271,14 +423,14 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
271
423
|
process.stderr.write(`codeindex: SCIP index → ${out} (${bytes.length} bytes)\n`);
|
|
272
424
|
}
|
|
273
425
|
} else if (cmd === "callers") {
|
|
274
|
-
const scan = scanRepo(flags.repo, scanOptions(flags));
|
|
426
|
+
const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
|
|
275
427
|
const index = buildCallerIndex(scan, undefined, { recall: flags.recall });
|
|
276
428
|
const obj: Record<string, unknown> = {};
|
|
277
429
|
for (const [name, entry] of index) obj[name] = entry;
|
|
278
430
|
emit(JSON.stringify(obj, null, 2) + "\n", flags.out);
|
|
279
431
|
} else if (cmd === "search") {
|
|
280
432
|
if (!flags.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
|
|
281
|
-
const scan = scanRepo(flags.repo, scanOptions(flags));
|
|
433
|
+
const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
|
|
282
434
|
if (flags.semantic) {
|
|
283
435
|
const endpoint = resolveEmbedEndpoint();
|
|
284
436
|
const lexical = (): void => {
|
|
@@ -383,7 +535,7 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
383
535
|
}
|
|
384
536
|
const model = loadEmbedModel(modelDir)!;
|
|
385
537
|
mkdirSync(flags.out, { recursive: true });
|
|
386
|
-
const scan = scanRepo(flags.repo, scanOptions(flags));
|
|
538
|
+
const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
|
|
387
539
|
const index = buildEmbeddingIndex(scan, model);
|
|
388
540
|
writeFileSync(join(flags.out, "embeddings.bin"), serializeEmbeddings(index));
|
|
389
541
|
process.stderr.write(`codeindex: ${index.records.length} embedding records → ${flags.out}/embeddings.bin (model ${model.modelId})\n`);
|
|
@@ -404,9 +556,14 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
404
556
|
return;
|
|
405
557
|
}
|
|
406
558
|
try {
|
|
407
|
-
JSON
|
|
408
|
-
|
|
409
|
-
|
|
559
|
+
// Shape-validate BEFORE writing: a JSON-valid but shape-invalid asset
|
|
560
|
+
// would otherwise land on disk and turn every later semantic search
|
|
561
|
+
// into a hard loadEmbedModel error instead of the documented degrade.
|
|
562
|
+
parseEmbedModel(JSON.parse(body), url);
|
|
563
|
+
} catch (e) {
|
|
564
|
+
process.stderr.write(
|
|
565
|
+
`codeindex: pull failed — response is not a valid model.json (${e instanceof Error ? e.message : String(e)}) (nothing written)\n`,
|
|
566
|
+
);
|
|
410
567
|
process.exitCode = 1;
|
|
411
568
|
return;
|
|
412
569
|
}
|
|
@@ -415,10 +572,109 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
415
572
|
} else {
|
|
416
573
|
throw new Error("embed needs a subcommand: status | build | pull | serve");
|
|
417
574
|
}
|
|
575
|
+
} else if (cmd === "grammars") {
|
|
576
|
+
const sub = flags.positional;
|
|
577
|
+
const cacheDir = sharedGrammarsCacheDir();
|
|
578
|
+
if (sub === "status") {
|
|
579
|
+
// Report which tier furnishes the wasms (adjacent/env/cache/none), the
|
|
580
|
+
// resolved dir, the pinned ENGINE_VERSION the cache is keyed on, and
|
|
581
|
+
// whether a pull is needed (no runtime wasm resolvable → AST off, regex).
|
|
582
|
+
const info = resolveGrammarsTier();
|
|
583
|
+
const runtimePresent = info.dir ? existsSync(join(info.dir, "web-tree-sitter.wasm")) : false;
|
|
584
|
+
const target = resolveGrammarsPullTarget();
|
|
585
|
+
const status: Record<string, unknown> = {
|
|
586
|
+
engineVersion: ENGINE_VERSION,
|
|
587
|
+
tier: info.tier,
|
|
588
|
+
dir: info.dir ?? null,
|
|
589
|
+
cacheDir,
|
|
590
|
+
runtimePresent,
|
|
591
|
+
pullNeeded: !runtimePresent,
|
|
592
|
+
url: target.url,
|
|
593
|
+
};
|
|
594
|
+
emit(JSON.stringify(status, null, 2) + "\n", flags.out);
|
|
595
|
+
} else if (sub === "pull") {
|
|
596
|
+
// Default: the official per-release asset + its `.sha256` sidecar. A
|
|
597
|
+
// user-set CODEINDEX_GRAMMARS_URL overrides both (mirror/custom, no
|
|
598
|
+
// verification). Fetch the expected digest from the sidecar first; a
|
|
599
|
+
// missing sidecar degrades to an unverified pull with a note (never fatal).
|
|
600
|
+
const target = resolveGrammarsPullTarget();
|
|
601
|
+
let expected: string | undefined;
|
|
602
|
+
if (target.sha256Url) {
|
|
603
|
+
try {
|
|
604
|
+
expected = await fetchExpectedSha256(target.sha256Url);
|
|
605
|
+
} catch (e) {
|
|
606
|
+
process.stderr.write(
|
|
607
|
+
`codeindex: could not fetch checksum (${e instanceof Error ? e.message : String(e)}) — proceeding unverified\n`,
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// Idempotent: the marker sibling records the digest of the tarball that
|
|
612
|
+
// populated cacheDir. If the runtime wasm is present AND the marker matches
|
|
613
|
+
// the freshly-fetched digest, the cache is already up to date — skip the
|
|
614
|
+
// ~22 MB download entirely. (Keeps cacheDir itself byte-identical to the
|
|
615
|
+
// tarball; the marker lives next to it, never inside it.)
|
|
616
|
+
const runtime = join(cacheDir, "web-tree-sitter.wasm");
|
|
617
|
+
const markerPath = join(dirname(cacheDir), `${ENGINE_VERSION}.sha256`);
|
|
618
|
+
if (existsSync(runtime) && expected && existsSync(markerPath)) {
|
|
619
|
+
let marker = "";
|
|
620
|
+
try {
|
|
621
|
+
marker = readFileSync(markerPath, "utf8").trim();
|
|
622
|
+
} catch {
|
|
623
|
+
// unreadable marker → fall through and re-pull
|
|
624
|
+
}
|
|
625
|
+
if (marker === expected) {
|
|
626
|
+
process.stderr.write(`codeindex: grammars already present at ${cacheDir} (up to date)\n`);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
process.stderr.write(`codeindex: fetching grammars from ${target.url} → ${cacheDir}\n`);
|
|
631
|
+
let bytes: Uint8Array;
|
|
632
|
+
try {
|
|
633
|
+
// Follows redirects (GitHub → CDN) and verifies sha256 when known.
|
|
634
|
+
bytes = await fetchGrammarsTarball(target.url, expected);
|
|
635
|
+
} catch (e) {
|
|
636
|
+
process.stderr.write(`codeindex: pull failed — ${e instanceof Error ? e.message : String(e)} (nothing written)\n`);
|
|
637
|
+
process.exitCode = 1;
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
// Atomic install: extract into a tmp dir SIBLING to cacheDir (same
|
|
641
|
+
// filesystem → rename is atomic), sanity-check the runtime wasm landed,
|
|
642
|
+
// then swap it into place. A failure at any step discards the tmp dir and
|
|
643
|
+
// leaves any existing cache untouched — a half-populated cache never shows.
|
|
644
|
+
let tmp: string | undefined;
|
|
645
|
+
try {
|
|
646
|
+
mkdirSync(dirname(cacheDir), { recursive: true });
|
|
647
|
+
tmp = mkdtempSync(join(dirname(cacheDir), ".grammars-tmp-"));
|
|
648
|
+
extractGrammarsTarball(bytes, tmp);
|
|
649
|
+
if (!existsSync(join(tmp, "web-tree-sitter.wasm"))) {
|
|
650
|
+
throw new Error("archive is missing web-tree-sitter.wasm");
|
|
651
|
+
}
|
|
652
|
+
if (existsSync(cacheDir)) rmSync(cacheDir, { recursive: true, force: true });
|
|
653
|
+
renameSync(tmp, cacheDir);
|
|
654
|
+
tmp = undefined;
|
|
655
|
+
if (expected) writeFileSync(markerPath, expected + "\n");
|
|
656
|
+
} catch (e) {
|
|
657
|
+
if (tmp) {
|
|
658
|
+
try {
|
|
659
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
660
|
+
} catch {
|
|
661
|
+
// best-effort cleanup
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
process.stderr.write(
|
|
665
|
+
`codeindex: pull failed — ${e instanceof Error ? e.message : String(e)} (nothing written)\n`,
|
|
666
|
+
);
|
|
667
|
+
process.exitCode = 1;
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
process.stderr.write(`codeindex: grammars extracted → ${cacheDir}\n`);
|
|
671
|
+
} else {
|
|
672
|
+
throw new Error("grammars needs a subcommand: status | pull");
|
|
673
|
+
}
|
|
418
674
|
} else if (cmd === "rules") {
|
|
419
675
|
if (!flags.config) throw new Error("rules needs --config <codeindex.rules.json>");
|
|
420
676
|
const rules = parseRules(JSON.parse(readFileSync(flags.config, "utf8")));
|
|
421
|
-
const { graph } = buildIndexArtifacts(flags.repo, scanOptions(flags));
|
|
677
|
+
const { graph } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
|
|
422
678
|
const violations = checkRules(graph, rules);
|
|
423
679
|
const errors = violations.filter((v) => v.severity === "error").length;
|
|
424
680
|
emit(JSON.stringify({ errors, warnings: violations.length - errors, violations }, null, 2) + "\n", flags.out);
|
|
@@ -439,26 +695,26 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
439
695
|
for (const k of [...churn.keys()].sort()) sorted[k] = churn.get(k)!;
|
|
440
696
|
emit(JSON.stringify({ ok, churn: sorted }, null, 2) + "\n", flags.out);
|
|
441
697
|
} else if (cmd === "repomap") {
|
|
442
|
-
const { scan, graph } = buildIndexArtifacts(flags.repo, scanOptions(flags));
|
|
698
|
+
const { scan, graph } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
|
|
443
699
|
emit(renderRepoMap(scan, graph, { budgetTokens: flags.budgetTokens }), flags.out);
|
|
444
700
|
} else if (cmd === "hotspots") {
|
|
445
|
-
const scan = scanRepo(flags.repo, scanOptions(flags));
|
|
701
|
+
const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
|
|
446
702
|
const { churn, ok } = gitChurn(flags.repo, { since: flags.since });
|
|
447
703
|
emit(JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan, churn) }, null, 2) + "\n", flags.out);
|
|
448
704
|
} else if (cmd === "coupling") {
|
|
449
705
|
const { ok, couplings } = changeCoupling(flags.repo, { since: flags.since });
|
|
450
706
|
emit(JSON.stringify({ ok, couplings }, null, 2) + "\n", flags.out);
|
|
451
707
|
} else if (cmd === "deadcode") {
|
|
452
|
-
emit(JSON.stringify(findDeadCode(scanRepo(flags.repo, scanOptions(flags))), null, 2) + "\n", flags.out);
|
|
708
|
+
emit(JSON.stringify(findDeadCode(scanRepo(flags.repo, scanOptions(flags, precomputedWalk))), null, 2) + "\n", flags.out);
|
|
453
709
|
} else if (cmd === "complexity") {
|
|
454
|
-
const scan = scanRepo(flags.repo, scanOptions(flags));
|
|
710
|
+
const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
|
|
455
711
|
emit(JSON.stringify(symbolComplexity(scan, flags.positional), null, 2) + "\n", flags.out);
|
|
456
712
|
} else if (cmd === "risk") {
|
|
457
|
-
const scan = scanRepo(flags.repo, scanOptions(flags));
|
|
713
|
+
const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
|
|
458
714
|
const { churn, ok } = gitChurn(flags.repo, { since: flags.since });
|
|
459
715
|
emit(JSON.stringify({ churnOk: ok, risks: riskHotspots(scan, churn) }, null, 2) + "\n", flags.out);
|
|
460
716
|
} else if (cmd === "mermaid") {
|
|
461
|
-
const { graph } = buildIndexArtifacts(flags.repo, scanOptions(flags));
|
|
717
|
+
const { graph } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
|
|
462
718
|
emit(renderMermaid(graph, { module: flags.positional }), flags.out);
|
|
463
719
|
} else if (cmd === "grep") {
|
|
464
720
|
if (!flags.positional) throw new Error("grep needs a pattern: cli.mjs grep <pattern> --repo <dir>");
|
package/src/engine.ts
CHANGED
|
@@ -45,8 +45,23 @@ export { extractMarkdown } from "./extract/markdown.js";
|
|
|
45
45
|
export type { MarkdownInfo } from "./extract/markdown.js";
|
|
46
46
|
|
|
47
47
|
// AST tier (optional — a no-op without the grammar wasm sidecar).
|
|
48
|
-
export { ensureGrammars, allGrammarKeys, grammarKeyForExt, grammarReady } from "./ast/loader.js";
|
|
48
|
+
export { ensureGrammars, allGrammarKeys, grammarKeysForExts, grammarKeyForExt, grammarReady } from "./ast/loader.js";
|
|
49
49
|
export { extractAst } from "./ast/extract.js";
|
|
50
|
+
// Grammars resolution + the slim pull/cache tier (v2.14.0): resolve the wasm
|
|
51
|
+
// dir across bundle-adjacent → env → shared cache → none, and fetch the
|
|
52
|
+
// committed wasms into the shared cache when none ship next to the bundle
|
|
53
|
+
// (`codeindex grammars pull|status`). Additive; opt-in; offline-safe.
|
|
54
|
+
export { resolveGrammarsDir, resolveGrammarsTier, sharedGrammarsCacheDir } from "./ast/loader.js";
|
|
55
|
+
export type { GrammarsTier, GrammarsTierName } from "./ast/loader.js";
|
|
56
|
+
export {
|
|
57
|
+
DEFAULT_GRAMMARS_URL,
|
|
58
|
+
resolveGrammarsPullTarget,
|
|
59
|
+
fetchGrammarsTarball,
|
|
60
|
+
fetchExpectedSha256,
|
|
61
|
+
extractTarInto,
|
|
62
|
+
extractGrammarsTarball,
|
|
63
|
+
} from "./ast/grammars-pull.js";
|
|
64
|
+
export type { GrammarsPullTarget } from "./ast/grammars-pull.js";
|
|
50
65
|
|
|
51
66
|
// Resolution + modules + graph tier.
|
|
52
67
|
export { buildResolveContext, resolveImport, resolveDocLink } from "./resolve.js";
|
|
@@ -78,8 +93,9 @@ export { renderGraphJson } from "./render/graph-json.js";
|
|
|
78
93
|
export { renderScip } from "./render/scip.js";
|
|
79
94
|
export type { RenderScipOptions } from "./render/scip.js";
|
|
80
95
|
|
|
81
|
-
// One-call pipeline
|
|
82
|
-
|
|
96
|
+
// One-call pipeline (buildArtifactsFromScan: the scan-onward half, for callers
|
|
97
|
+
// that already hold a RepoScan).
|
|
98
|
+
export { buildIndexArtifacts, buildArtifactsFromScan } from "./pipeline.js";
|
|
83
99
|
export type { BuildIndexOptions, IndexArtifacts } from "./pipeline.js";
|
|
84
100
|
|
|
85
101
|
// Git utilities.
|
|
@@ -114,7 +130,7 @@ export {
|
|
|
114
130
|
loadEmbedModel,
|
|
115
131
|
resolveEmbedPullUrl,
|
|
116
132
|
} from "./embed/model.js";
|
|
117
|
-
export type { StaticEmbedModel } from "./embed/model.js";
|
|
133
|
+
export type { StaticEmbedModel, EmbedPullTarget } from "./embed/model.js";
|
|
118
134
|
export { encode, quantize, tokenize, wordpiece, basicTokenize, roundHalfToEven, intDot } from "./embed/encode.js";
|
|
119
135
|
export { buildEmbeddingIndex, serializeEmbeddings, deserializeEmbeddings, embeddingUnits } from "./embed/index.js";
|
|
120
136
|
export type { EmbeddingIndex, EmbeddingRecord, EmbeddingUnit } from "./embed/index.js";
|
|
@@ -161,6 +177,7 @@ export type { RepoMapOptions } from "./repomap.js";
|
|
|
161
177
|
|
|
162
178
|
// MCP server over stdio (also reachable as `engine.mjs mcp`).
|
|
163
179
|
export { runMcpServer } from "./mcp.js";
|
|
180
|
+
export type { McpServerOptions } from "./mcp.js";
|
|
164
181
|
|
|
165
182
|
// General-purpose helpers shared by consumers (deterministic, dependency-free).
|
|
166
183
|
export { sha1, shortHash } from "./hash.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/hash.ts
CHANGED
|
@@ -2,8 +2,11 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
// Stable content hash used for the manifest's staleness oracle and for the
|
|
4
4
|
// `ui:gen` region fingerprints. sha1 is plenty for change detection (not
|
|
5
|
-
// security) and keeps the manifest compact.
|
|
6
|
-
|
|
5
|
+
// security) and keeps the manifest compact. Accepts raw bytes too (additive)
|
|
6
|
+
// so binary artifacts (e.g. embeddings.bin) hash without a lossy decode; a
|
|
7
|
+
// string is hashed as its UTF-8 bytes — identical to hashing the bytes
|
|
8
|
+
// writeFileSync would put on disk for that string.
|
|
9
|
+
export function sha1(s: string | Uint8Array): string {
|
|
7
10
|
return createHash("sha1").update(s).digest("hex");
|
|
8
11
|
}
|
|
9
12
|
|
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 },
|