@maxgfr/codeindex 2.14.0 → 2.16.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 +1 -1
- package/docs/MIGRATION.md +52 -0
- package/package.json +1 -1
- package/scripts/engine.d.mts +35 -3
- package/scripts/engine.mjs +193 -94
- package/src/ast/grammars-pull.ts +95 -1
- package/src/ast/warm.ts +74 -0
- package/src/engine-cli.ts +9 -81
- package/src/engine.ts +7 -1
- package/src/mcp.ts +135 -3
- package/src/types.ts +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ engine as a single file instead of taking an npm dependency.
|
|
|
17
17
|
and count caps (`capped` flag, never silent truncation), symlink-cycle guard.
|
|
18
18
|
- **Scan** every file into a `FileRecord`: classification, language, symbols,
|
|
19
19
|
imports, headings, hashes — with an incremental cache fastpath.
|
|
20
|
-
- **Extract symbols** via tree-sitter (
|
|
20
|
+
- **Extract symbols** via tree-sitter (13 languages, when the wasm sidecar is
|
|
21
21
|
present) or per-language regex rules (15 languages, always available).
|
|
22
22
|
- **Resolve imports** across languages: tsconfig paths, package `exports`,
|
|
23
23
|
go.mod, Cargo, Java packages, PSR-4, C# namespaces.
|
package/docs/MIGRATION.md
CHANGED
|
@@ -220,6 +220,58 @@ surface is additive (semver-minor).
|
|
|
220
220
|
`resolveGrammarsPullTarget`, `fetchGrammarsTarball`, `fetchExpectedSha256`,
|
|
221
221
|
`extractGrammarsTarball`, `GrammarsPullTarget`.
|
|
222
222
|
|
|
223
|
+
## v2.15.0 — MCP serves from a persisted index (additive, no re-pin required)
|
|
224
|
+
|
|
225
|
+
Pure fastpath, MCP-only: on the **first** tool call for a repo the long-lived
|
|
226
|
+
server seeds its session from a committed `.codeindex/` index (written by
|
|
227
|
+
`codeindex index`) instead of doing every step cold. Served tool responses stay
|
|
228
|
+
**byte-identical** to a cold-process build on the same repo state, so **no
|
|
229
|
+
re-pin is required** and no consumer needs to act. `SCHEMA_VERSION` /
|
|
230
|
+
`EMBED_VERSION` / `EXTRACTOR_VERSION` are untouched, the public API is unchanged
|
|
231
|
+
(**no new exports** — the preload is entirely internal to `mcp.ts`), and a repo
|
|
232
|
+
with no `.codeindex/` behaves exactly as before.
|
|
233
|
+
|
|
234
|
+
- **Scan seed from `cache.json`.** `getScan`'s first touch reads
|
|
235
|
+
`.codeindex/cache.json` and re-expresses its per-file records as the session
|
|
236
|
+
`cache`, so scan.ts's stat fastpath / exact content-hash reuse rebuilds the
|
|
237
|
+
`RepoScan` value-identically to a cold scan (the T3/T4 determinism the CLI's
|
|
238
|
+
`index` fastpath already relies on) without a read + hash + extraction per
|
|
239
|
+
unchanged file. Records are only trusted when the cache's
|
|
240
|
+
`(schemaVersion, extractorVersion)` match this engine — the same gate the CLI
|
|
241
|
+
applies — otherwise the whole cache is discarded and the scan runs cold. A
|
|
242
|
+
file whose content drifted since `cache.json` is re-read/extracted here exactly
|
|
243
|
+
as a cold scan would, so the scan stays correct and only the derived freshness
|
|
244
|
+
flags differ (they never feed artifacts).
|
|
245
|
+
- **Artifact preload from `graph.json` / `symbols.json` — gated by the T4
|
|
246
|
+
oracle.** Only when the **exact** T4 freshness guard holds does the session
|
|
247
|
+
deserialize the on-disk artifacts straight in, skipping the whole downstream
|
|
248
|
+
pipeline (`buildArtifactsFromScan`) for the first
|
|
249
|
+
`graph`/`symbols`/`mermaid`/`repo_map`/`check_rules` call. The guard is the one
|
|
250
|
+
the CLI `index` fastpath uses to prove the on-disk bytes equal a fresh build:
|
|
251
|
+
`scan.contentUnchanged` **and** `cache.json`'s `engineVersion === ENGINE_VERSION`
|
|
252
|
+
**and** its `commit === scan.commit` **and**
|
|
253
|
+
`sha1(graph.json) === meta.graphSha1` **and**
|
|
254
|
+
`sha1(symbols.json) === meta.symbolsSha1`. When it holds, the on-disk
|
|
255
|
+
`graph.json`/`symbols.json` are byte-equal to `buildArtifactsFromScan(scan)` run
|
|
256
|
+
here, so deserializing them equals rebuilding.
|
|
257
|
+
- **Deserialize is `JSON.parse`, not a new codec — no new exports.** `Graph` and
|
|
258
|
+
`SymbolIndex` are pure JSON POJOs (no `Map`/`Set`/typed fields), so
|
|
259
|
+
`JSON.parse` is a lossless round-trip and a `schemaVersion` assert is the only
|
|
260
|
+
reconstruction needed. `renderGraphJson` re-sorts `graph.languages` anyway, so
|
|
261
|
+
render→parse→render reproduces the same bytes (numbers reproduce their
|
|
262
|
+
shortest-round-trip form, absent optionals stay absent, V8's integer-key
|
|
263
|
+
hoisting is identical). No (de)serializer was added to the barrel; the preload
|
|
264
|
+
helpers are private to `mcp.ts`.
|
|
265
|
+
- **Fallback is today's build-on-demand path, EXACTLY — never a throw.** No
|
|
266
|
+
`.codeindex/`, a `(schemaVersion, extractorVersion)` mismatch, a stale scan, a
|
|
267
|
+
version/commit/sha mismatch, or a missing/corrupt/partial/tampered
|
|
268
|
+
`graph.json`/`symbols.json` each returns `undefined` and falls through to a
|
|
269
|
+
fresh `scanRepo` / `buildArtifactsFromScan`. So the preload self-heals on
|
|
270
|
+
corruption and a deleted or truncated artifact never crashes the session — the
|
|
271
|
+
worst case is the cold cost the server paid before this release. The embed
|
|
272
|
+
sidecar keeps its own memoization path (the graph/symbols shas are all the
|
|
273
|
+
guard checks).
|
|
274
|
+
|
|
223
275
|
## Typical mapping (what to replace with what)
|
|
224
276
|
|
|
225
277
|
What a consumer usually deletes from its own codebase, and the engine export
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxgfr/codeindex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.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,4 +1,4 @@
|
|
|
1
|
-
declare const ENGINE_VERSION = "2.
|
|
1
|
+
declare const ENGINE_VERSION = "2.16.0";
|
|
2
2
|
declare const SCHEMA_VERSION = 4;
|
|
3
3
|
declare const EXTRACTOR_VERSION = 10;
|
|
4
4
|
type FileKind = "code" | "doc" | "config" | "asset" | "other";
|
|
@@ -255,7 +255,7 @@ declare function extractAst(rel: string, ext: string, content: string, opts?: {
|
|
|
255
255
|
maxCalls?: number;
|
|
256
256
|
}): AstResult | undefined;
|
|
257
257
|
|
|
258
|
-
declare const DEFAULT_GRAMMARS_URL = "https://github.com/maxgfr/codeindex/releases/download/v2.
|
|
258
|
+
declare const DEFAULT_GRAMMARS_URL = "https://github.com/maxgfr/codeindex/releases/download/v2.16.0/grammars-2.16.0.tar.gz";
|
|
259
259
|
interface GrammarsPullTarget {
|
|
260
260
|
url: string;
|
|
261
261
|
sha256Url?: string;
|
|
@@ -265,6 +265,38 @@ declare function fetchGrammarsTarball(url: string, expectedSha256?: string): Pro
|
|
|
265
265
|
declare function fetchExpectedSha256(url: string): Promise<string>;
|
|
266
266
|
declare function extractTarInto(rawTar: Uint8Array, destDir: string): string[];
|
|
267
267
|
declare function extractGrammarsTarball(bytes: Uint8Array, destDir: string): string[];
|
|
268
|
+
interface GrammarsPullResult {
|
|
269
|
+
ok: boolean;
|
|
270
|
+
status: "up-to-date" | "pulled" | "failed";
|
|
271
|
+
cacheDir: string;
|
|
272
|
+
/** One human-readable line; the CLI writes it to stderr, a library caller may log or drop it. */
|
|
273
|
+
message: string;
|
|
274
|
+
}
|
|
275
|
+
declare function pullGrammars(cacheDir: string, opts?: {
|
|
276
|
+
onNote?: (msg: string) => void;
|
|
277
|
+
}): Promise<GrammarsPullResult>;
|
|
278
|
+
|
|
279
|
+
interface WarmGrammarsResult {
|
|
280
|
+
/** Tier AFTER the warm-up (a successful pull moves "none" → "cache"). */
|
|
281
|
+
tier: GrammarsTierName;
|
|
282
|
+
/** True when at least one requested grammar is loaded ⇒ the AST tier is live. */
|
|
283
|
+
ready: boolean;
|
|
284
|
+
/** True when this call populated the shared cache over the network. */
|
|
285
|
+
pulled: boolean;
|
|
286
|
+
/** Everything written to `onNote`, in order — so a caller can persist the trail in its run artifacts. */
|
|
287
|
+
notes: string[];
|
|
288
|
+
}
|
|
289
|
+
interface WarmGrammarsOptions {
|
|
290
|
+
/** Grammars to load. Default: every shipped grammar. Narrow it with `grammarKeysForExts` when the repo's languages are known. */
|
|
291
|
+
keys?: Iterable<string>;
|
|
292
|
+
/** Fetch the wasms into the shared cache when nothing is resolvable. Default true; `CODEINDEX_NO_GRAMMARS_PULL=1` forces false. */
|
|
293
|
+
pull?: boolean;
|
|
294
|
+
/** Prefix for the diagnostics ("ultrasec: …"). Default "codeindex". */
|
|
295
|
+
label?: string;
|
|
296
|
+
/** Where diagnostics go. Default: process.stderr. Pass a sink to keep stdout/stderr clean. */
|
|
297
|
+
onNote?: (msg: string) => void;
|
|
298
|
+
}
|
|
299
|
+
declare function warmGrammars(opts?: WarmGrammarsOptions): Promise<WarmGrammarsResult>;
|
|
268
300
|
|
|
269
301
|
type Resolution = {
|
|
270
302
|
kind: "resolved";
|
|
@@ -747,4 +779,4 @@ declare function rrf<T>(lists: T[][], keyOf: (item: T) => string, k?: number): M
|
|
|
747
779
|
|
|
748
780
|
declare function runCli(argv: string[]): Promise<void>;
|
|
749
781
|
|
|
750
|
-
export { type ArchRule, type BuildIndexOptions, type BuiltinRule, type CallerEntry, type CallerIndex, type CallerIndexOptions, type CallerSite, type ChangeCoupling, type CodeInfo, type CodeSymbol, type CouplingOptions, DEFAULT_GRAMMARS_URL, 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 GrammarsPullTarget, type GrammarsTier, type GrammarsTierName, type Graph, type GrepOptions, type Hotspot, type Hunk, type IgnoreRule, type IndexArtifacts, MARKDOWN_EXT, type MarkdownInfo, type McpServerOptions, 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, buildArtifactsFromScan, 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, extractGrammarsTarball, extractMarkdown, extractSymbols, extractTarInto, fetchExpectedSha256, fetchGrammarsTarball, findDeadCode, findReferences, findSymbol, foldText, gitChurn, grammarKeyForExt, grammarKeysForExts, 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, resolveGrammarsDir, resolveGrammarsPullTarget, resolveGrammarsTier, resolveImport, resolveUniqueSymbol, riskHotspots, roundHalfToEven, rrf, runCli, runMcpServer, scanRepo, searchIndex, searchSemantic, serializeEmbeddings, sh, sha1, sharedGrammarsCacheDir, shortHash, slugify, subtokens, symbolComplexity, symbolsOverview, testsForModule, tierForPath, tokenize, uniqueSymbolDefs, untestedModules, untrackedFiles, walk, wordpiece, writeMemory };
|
|
782
|
+
export { type ArchRule, type BuildIndexOptions, type BuiltinRule, type CallerEntry, type CallerIndex, type CallerIndexOptions, type CallerSite, type ChangeCoupling, type CodeInfo, type CodeSymbol, type CouplingOptions, DEFAULT_GRAMMARS_URL, 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 GrammarsPullResult, type GrammarsPullTarget, type GrammarsTier, type GrammarsTierName, type Graph, type GrepOptions, type Hotspot, type Hunk, type IgnoreRule, type IndexArtifacts, MARKDOWN_EXT, type MarkdownInfo, type McpServerOptions, 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 WarmGrammarsOptions, type WarmGrammarsResult, type WorkspaceInfo, type WorkspaceKind, type WorkspacePackage, allGrammarKeys, applyCentrality, basicTokenize, betweennessOf, buildArtifactsFromScan, 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, extractGrammarsTarball, extractMarkdown, extractSymbols, extractTarInto, fetchExpectedSha256, fetchGrammarsTarball, findDeadCode, findReferences, findSymbol, foldText, gitChurn, grammarKeyForExt, grammarKeysForExts, grammarReady, grepRepo, hasEmbedModel, have, headCommit, healthzUrl, insertAfterSymbol, insertBeforeSymbol, intDot, isCode, isDoc, isGitWorktree, isIgnored, isSurprising, isTestFile, isTestPath, keywords, languageOf, listMemories, loadEmbedModel, pagerankOf, parseGitignore, parseRules, probeEndpoint, pullGrammars, quantize, rankHotspots, rankedKeywords, readMemory, readText, renderGraphJson, renderMermaid, renderRepoMap, renderScip, renderSymbolsJson, replaceSymbolBody, resolveBaseRef, resolveCallEdges, resolveDocLink, resolveEmbedEndpoint, resolveEmbedModelDir, resolveEmbedPullUrl, resolveGrammarsDir, resolveGrammarsPullTarget, resolveGrammarsTier, resolveImport, resolveUniqueSymbol, riskHotspots, roundHalfToEven, rrf, runCli, runMcpServer, scanRepo, searchIndex, searchSemantic, serializeEmbeddings, sh, sha1, sharedGrammarsCacheDir, shortHash, slugify, subtokens, symbolComplexity, symbolsOverview, testsForModule, tierForPath, tokenize, uniqueSymbolDefs, untestedModules, untrackedFiles, walk, warmGrammars, wordpiece, writeMemory };
|
package/scripts/engine.mjs
CHANGED
|
@@ -14,7 +14,7 @@ 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.16.0";
|
|
18
18
|
SCHEMA_VERSION = 4;
|
|
19
19
|
EXTRACTOR_VERSION = 10;
|
|
20
20
|
}
|
|
@@ -8259,7 +8259,7 @@ var init_query = __esm({
|
|
|
8259
8259
|
});
|
|
8260
8260
|
|
|
8261
8261
|
// src/edit.ts
|
|
8262
|
-
import { readFileSync as
|
|
8262
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
8263
8263
|
import { join as join9 } from "path";
|
|
8264
8264
|
function resolveUniqueSymbol(scan2, namePath, file) {
|
|
8265
8265
|
let matches = findSymbol(scan2, namePath);
|
|
@@ -8273,7 +8273,7 @@ function resolveUniqueSymbol(scan2, namePath, file) {
|
|
|
8273
8273
|
throw new Error(`"${namePath}" is ambiguous (${matches.length} matches: ${list}) \u2014 qualify with \`file\` or a Parent/name path`);
|
|
8274
8274
|
}
|
|
8275
8275
|
function readLines(abs) {
|
|
8276
|
-
return
|
|
8276
|
+
return readFileSync4(abs, "utf8").split("\n");
|
|
8277
8277
|
}
|
|
8278
8278
|
function replaceSymbolBody(scan2, namePath, body2, file) {
|
|
8279
8279
|
const sym = resolveUniqueSymbol(scan2, namePath, file);
|
|
@@ -8317,7 +8317,7 @@ var init_edit = __esm({
|
|
|
8317
8317
|
});
|
|
8318
8318
|
|
|
8319
8319
|
// src/memory.ts
|
|
8320
|
-
import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as
|
|
8320
|
+
import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
8321
8321
|
import { dirname as dirname3, join as join10 } from "path";
|
|
8322
8322
|
function sanitize(name2) {
|
|
8323
8323
|
const clean = name2.replace(/^mem:/, "").replace(/\.md$/, "");
|
|
@@ -8342,7 +8342,7 @@ function writeMemory(repo, name2, content) {
|
|
|
8342
8342
|
}
|
|
8343
8343
|
function readMemory(repo, name2) {
|
|
8344
8344
|
try {
|
|
8345
|
-
return
|
|
8345
|
+
return readFileSync5(memoryPath(repo, name2), "utf8");
|
|
8346
8346
|
} catch {
|
|
8347
8347
|
return void 0;
|
|
8348
8348
|
}
|
|
@@ -8354,7 +8354,7 @@ function deleteMemory(repo, name2) {
|
|
|
8354
8354
|
} catch {
|
|
8355
8355
|
return false;
|
|
8356
8356
|
}
|
|
8357
|
-
|
|
8357
|
+
rmSync2(path);
|
|
8358
8358
|
return true;
|
|
8359
8359
|
}
|
|
8360
8360
|
function listMemories(repo) {
|
|
@@ -8384,7 +8384,7 @@ var init_memory = __esm({
|
|
|
8384
8384
|
});
|
|
8385
8385
|
|
|
8386
8386
|
// src/workspaces.ts
|
|
8387
|
-
import { existsSync as
|
|
8387
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
8388
8388
|
import { join as join11 } from "path";
|
|
8389
8389
|
function readJson(path, label, warnings) {
|
|
8390
8390
|
const raw = readText(path);
|
|
@@ -8437,7 +8437,7 @@ function wsGlobToRegExp(pat) {
|
|
|
8437
8437
|
}
|
|
8438
8438
|
function probeNodePkg(root, dir, kind, warnings) {
|
|
8439
8439
|
const path = join11(root, dir, "package.json");
|
|
8440
|
-
if (!
|
|
8440
|
+
if (!existsSync3(path)) return void 0;
|
|
8441
8441
|
const manifest = `${dir}/package.json`;
|
|
8442
8442
|
const pkg = readJson(path, manifest, warnings);
|
|
8443
8443
|
const out2 = {
|
|
@@ -8451,7 +8451,7 @@ function probeNodePkg(root, dir, kind, warnings) {
|
|
|
8451
8451
|
}
|
|
8452
8452
|
function probeCargo(root, dir) {
|
|
8453
8453
|
const path = join11(root, dir, "Cargo.toml");
|
|
8454
|
-
if (!
|
|
8454
|
+
if (!existsSync3(path)) return void 0;
|
|
8455
8455
|
const body2 = tomlSectionBody(readText(path), "package");
|
|
8456
8456
|
const out2 = {
|
|
8457
8457
|
name: tomlString(body2, "name") ?? dir,
|
|
@@ -8465,18 +8465,18 @@ function probeCargo(root, dir) {
|
|
|
8465
8465
|
}
|
|
8466
8466
|
function probeGoMod(root, dir) {
|
|
8467
8467
|
const path = join11(root, dir, "go.mod");
|
|
8468
|
-
if (!
|
|
8468
|
+
if (!existsSync3(path)) return void 0;
|
|
8469
8469
|
const name2 = readText(path).match(/^module\s+(\S+)/m)?.[1] ?? dir;
|
|
8470
8470
|
return { name: name2, dir, kind: "go", manifest: `${dir}/go.mod` };
|
|
8471
8471
|
}
|
|
8472
8472
|
function probeMaven(root, dir) {
|
|
8473
8473
|
const path = join11(root, dir, "pom.xml");
|
|
8474
|
-
if (!
|
|
8474
|
+
if (!existsSync3(path)) return void 0;
|
|
8475
8475
|
return { name: ownArtifactId(readText(path)) ?? dir, dir, kind: "maven", manifest: `${dir}/pom.xml` };
|
|
8476
8476
|
}
|
|
8477
8477
|
function probePyproject(root, dir) {
|
|
8478
8478
|
const path = join11(root, dir, "pyproject.toml");
|
|
8479
|
-
if (!
|
|
8479
|
+
if (!existsSync3(path)) return void 0;
|
|
8480
8480
|
const toml = readText(path);
|
|
8481
8481
|
const project = tomlSectionBody(toml, "project");
|
|
8482
8482
|
const poetry = tomlSectionBody(toml, "tool.poetry");
|
|
@@ -8492,7 +8492,7 @@ function probePyproject(root, dir) {
|
|
|
8492
8492
|
}
|
|
8493
8493
|
function probeComposer(root, dir, warnings) {
|
|
8494
8494
|
const path = join11(root, dir, "composer.json");
|
|
8495
|
-
if (!
|
|
8495
|
+
if (!existsSync3(path)) return void 0;
|
|
8496
8496
|
const manifest = `${dir}/composer.json`;
|
|
8497
8497
|
const pkg = readJson(path, manifest, warnings);
|
|
8498
8498
|
const out2 = {
|
|
@@ -8506,7 +8506,7 @@ function probeComposer(root, dir, warnings) {
|
|
|
8506
8506
|
}
|
|
8507
8507
|
function probeNxProject(root, dir, warnings) {
|
|
8508
8508
|
const path = join11(root, dir, "project.json");
|
|
8509
|
-
if (!
|
|
8509
|
+
if (!existsSync3(path)) return void 0;
|
|
8510
8510
|
const manifest = `${dir}/project.json`;
|
|
8511
8511
|
const proj = readJson(path, manifest, warnings);
|
|
8512
8512
|
return {
|
|
@@ -8518,7 +8518,7 @@ function probeNxProject(root, dir, warnings) {
|
|
|
8518
8518
|
}
|
|
8519
8519
|
function probeGradle(root, dir) {
|
|
8520
8520
|
for (const f of ["build.gradle", "build.gradle.kts"]) {
|
|
8521
|
-
if (
|
|
8521
|
+
if (existsSync3(join11(root, dir, f))) {
|
|
8522
8522
|
return { name: dir, dir, kind: "gradle", manifest: `${dir}/${f}` };
|
|
8523
8523
|
}
|
|
8524
8524
|
}
|
|
@@ -9565,7 +9565,7 @@ var init_grep = __esm({
|
|
|
9565
9565
|
|
|
9566
9566
|
// src/embed/model.ts
|
|
9567
9567
|
import { createHash as createHash3 } from "crypto";
|
|
9568
|
-
import { existsSync as
|
|
9568
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
9569
9569
|
import { join as join13 } from "path";
|
|
9570
9570
|
function resolveEmbedModelDir(repo) {
|
|
9571
9571
|
const env = process.env.CODEINDEX_EMBED_DIR;
|
|
@@ -9574,7 +9574,7 @@ function resolveEmbedModelDir(repo) {
|
|
|
9574
9574
|
if (repo) candidates.push(join13(repo, ".codeindex", DEFAULT_EMBED_DIRNAME));
|
|
9575
9575
|
candidates.push(join13(process.cwd(), ".codeindex", DEFAULT_EMBED_DIRNAME));
|
|
9576
9576
|
for (const c2 of candidates) {
|
|
9577
|
-
if (
|
|
9577
|
+
if (existsSync4(join13(c2, "model.json"))) return c2;
|
|
9578
9578
|
}
|
|
9579
9579
|
return void 0;
|
|
9580
9580
|
}
|
|
@@ -9608,8 +9608,8 @@ function parseEmbedModel(raw, source) {
|
|
|
9608
9608
|
function loadEmbedModel(dir) {
|
|
9609
9609
|
if (!dir) return void 0;
|
|
9610
9610
|
const path = join13(dir, "model.json");
|
|
9611
|
-
if (!
|
|
9612
|
-
const raw = JSON.parse(
|
|
9611
|
+
if (!existsSync4(path)) return void 0;
|
|
9612
|
+
const raw = JSON.parse(readFileSync6(path, "utf8"));
|
|
9613
9613
|
return parseEmbedModel(raw, path);
|
|
9614
9614
|
}
|
|
9615
9615
|
function resolveEmbedPullUrl() {
|
|
@@ -10303,7 +10303,7 @@ __export(mcp_exports, {
|
|
|
10303
10303
|
toCacheMap: () => toCacheMap,
|
|
10304
10304
|
warmGrammarsForRepo: () => warmGrammarsForRepo
|
|
10305
10305
|
});
|
|
10306
|
-
import { statSync as statSync4 } from "fs";
|
|
10306
|
+
import { readFileSync as readFileSync7, statSync as statSync4 } from "fs";
|
|
10307
10307
|
import { join as join14 } from "path";
|
|
10308
10308
|
import { createInterface } from "readline";
|
|
10309
10309
|
function str(v) {
|
|
@@ -10357,6 +10357,57 @@ function toCacheMap(scan2) {
|
|
|
10357
10357
|
for (const f of scan2.files) m.set(f.rel, { hash: f.hash, record: f, size: f.size, mtimeMs: scan2.mtimes.get(f.rel) });
|
|
10358
10358
|
return m;
|
|
10359
10359
|
}
|
|
10360
|
+
function readPersistedIndex(repo) {
|
|
10361
|
+
let parsed;
|
|
10362
|
+
try {
|
|
10363
|
+
parsed = JSON.parse(readFileSync7(join14(repo, ".codeindex", "cache.json"), "utf8"));
|
|
10364
|
+
} catch {
|
|
10365
|
+
return void 0;
|
|
10366
|
+
}
|
|
10367
|
+
if (!parsed || parsed.schemaVersion !== SCHEMA_VERSION || parsed.extractorVersion !== EXTRACTOR_VERSION || !parsed.files) {
|
|
10368
|
+
return void 0;
|
|
10369
|
+
}
|
|
10370
|
+
const cacheMap = new Map(Object.entries(parsed.files));
|
|
10371
|
+
const meta = {
|
|
10372
|
+
engineVersion: parsed.engineVersion,
|
|
10373
|
+
commit: parsed.commit,
|
|
10374
|
+
graphSha1: parsed.graphSha1,
|
|
10375
|
+
symbolsSha1: parsed.symbolsSha1
|
|
10376
|
+
};
|
|
10377
|
+
return { cacheMap, meta };
|
|
10378
|
+
}
|
|
10379
|
+
function preloadArtifacts(repo, scan2, meta) {
|
|
10380
|
+
if (!scan2.contentUnchanged || meta.engineVersion !== ENGINE_VERSION || meta.commit !== scan2.commit || meta.graphSha1 === void 0 || meta.symbolsSha1 === void 0) {
|
|
10381
|
+
return void 0;
|
|
10382
|
+
}
|
|
10383
|
+
const dir = join14(repo, ".codeindex");
|
|
10384
|
+
let graphBytes;
|
|
10385
|
+
let symbolsBytes;
|
|
10386
|
+
try {
|
|
10387
|
+
graphBytes = readFileSync7(join14(dir, "graph.json"));
|
|
10388
|
+
symbolsBytes = readFileSync7(join14(dir, "symbols.json"));
|
|
10389
|
+
} catch {
|
|
10390
|
+
return void 0;
|
|
10391
|
+
}
|
|
10392
|
+
if (sha1(graphBytes) !== meta.graphSha1 || sha1(symbolsBytes) !== meta.symbolsSha1) {
|
|
10393
|
+
return void 0;
|
|
10394
|
+
}
|
|
10395
|
+
try {
|
|
10396
|
+
const graph = JSON.parse(graphBytes.toString("utf8"));
|
|
10397
|
+
const symbols = JSON.parse(symbolsBytes.toString("utf8"));
|
|
10398
|
+
if (graph.schemaVersion !== SCHEMA_VERSION || symbols.schemaVersion !== SCHEMA_VERSION) return void 0;
|
|
10399
|
+
return { scan: scan2, graph, symbols };
|
|
10400
|
+
} catch {
|
|
10401
|
+
return void 0;
|
|
10402
|
+
}
|
|
10403
|
+
}
|
|
10404
|
+
function preloadSession(repo, opts) {
|
|
10405
|
+
const persisted = readPersistedIndex(repo);
|
|
10406
|
+
if (!persisted) return void 0;
|
|
10407
|
+
const scan2 = scanRepo(repo, { ...opts, cache: persisted.cacheMap });
|
|
10408
|
+
const arts = preloadArtifacts(repo, scan2, persisted.meta);
|
|
10409
|
+
return { scan: scan2, cacheMap: toCacheMap(scan2), arts };
|
|
10410
|
+
}
|
|
10360
10411
|
function getScan(repo, opts = {}) {
|
|
10361
10412
|
const key = sessionKey(repo, opts);
|
|
10362
10413
|
if (sessionCache && sessionCache.key === key) {
|
|
@@ -10369,6 +10420,11 @@ function getScan(repo, opts = {}) {
|
|
|
10369
10420
|
sessionCache = { key, scan: fresh, cacheMap: toCacheMap(fresh) };
|
|
10370
10421
|
return fresh;
|
|
10371
10422
|
}
|
|
10423
|
+
const preloaded = preloadSession(repo, opts);
|
|
10424
|
+
if (preloaded) {
|
|
10425
|
+
sessionCache = { key, scan: preloaded.scan, cacheMap: preloaded.cacheMap, arts: preloaded.arts };
|
|
10426
|
+
return preloaded.scan;
|
|
10427
|
+
}
|
|
10372
10428
|
const scan2 = scanRepo(repo, opts);
|
|
10373
10429
|
sessionCache = { key, scan: scan2, cacheMap: toCacheMap(scan2) };
|
|
10374
10430
|
return scan2;
|
|
@@ -11074,8 +11130,8 @@ init_loader();
|
|
|
11074
11130
|
// src/ast/grammars-pull.ts
|
|
11075
11131
|
init_types();
|
|
11076
11132
|
import { createHash as createHash2 } from "crypto";
|
|
11077
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
11078
|
-
import { dirname as dirname2, resolve, sep as sep2 } from "path";
|
|
11133
|
+
import { existsSync as existsSync2, mkdirSync, mkdtempSync, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync } from "fs";
|
|
11134
|
+
import { dirname as dirname2, join as join3, resolve, sep as sep2 } from "path";
|
|
11079
11135
|
import { gunzipSync } from "zlib";
|
|
11080
11136
|
var DEFAULT_GRAMMARS_URL = `https://github.com/maxgfr/codeindex/releases/download/v${ENGINE_VERSION}/grammars-${ENGINE_VERSION}.tar.gz`;
|
|
11081
11137
|
function resolveGrammarsPullTarget() {
|
|
@@ -11167,6 +11223,109 @@ function extractGrammarsTarball(bytes, destDir) {
|
|
|
11167
11223
|
const raw = b.length >= 2 && b[0] === 31 && b[1] === 139 ? gunzipSync(b) : b;
|
|
11168
11224
|
return extractTarInto(raw, destDir);
|
|
11169
11225
|
}
|
|
11226
|
+
async function pullGrammars(cacheDir, opts = {}) {
|
|
11227
|
+
const note = opts.onNote ?? (() => {
|
|
11228
|
+
});
|
|
11229
|
+
const target = resolveGrammarsPullTarget();
|
|
11230
|
+
let expected;
|
|
11231
|
+
if (target.sha256Url) {
|
|
11232
|
+
try {
|
|
11233
|
+
expected = await fetchExpectedSha256(target.sha256Url);
|
|
11234
|
+
} catch (e) {
|
|
11235
|
+
note(`codeindex: could not fetch checksum (${e instanceof Error ? e.message : String(e)}) \u2014 proceeding unverified
|
|
11236
|
+
`);
|
|
11237
|
+
}
|
|
11238
|
+
}
|
|
11239
|
+
const runtime = join3(cacheDir, "web-tree-sitter.wasm");
|
|
11240
|
+
const markerPath = join3(dirname2(cacheDir), `${ENGINE_VERSION}.sha256`);
|
|
11241
|
+
if (existsSync2(runtime) && expected && existsSync2(markerPath)) {
|
|
11242
|
+
let marker = "";
|
|
11243
|
+
try {
|
|
11244
|
+
marker = readFileSync3(markerPath, "utf8").trim();
|
|
11245
|
+
} catch {
|
|
11246
|
+
}
|
|
11247
|
+
if (marker === expected) {
|
|
11248
|
+
return { ok: true, status: "up-to-date", cacheDir, message: `codeindex: grammars already present at ${cacheDir} (up to date)
|
|
11249
|
+
` };
|
|
11250
|
+
}
|
|
11251
|
+
}
|
|
11252
|
+
note(`codeindex: fetching grammars from ${target.url} \u2192 ${cacheDir}
|
|
11253
|
+
`);
|
|
11254
|
+
let bytes;
|
|
11255
|
+
try {
|
|
11256
|
+
bytes = await fetchGrammarsTarball(target.url, expected);
|
|
11257
|
+
} catch (e) {
|
|
11258
|
+
return {
|
|
11259
|
+
ok: false,
|
|
11260
|
+
status: "failed",
|
|
11261
|
+
cacheDir,
|
|
11262
|
+
message: `codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
|
|
11263
|
+
`
|
|
11264
|
+
};
|
|
11265
|
+
}
|
|
11266
|
+
let tmp;
|
|
11267
|
+
try {
|
|
11268
|
+
mkdirSync(dirname2(cacheDir), { recursive: true });
|
|
11269
|
+
tmp = mkdtempSync(join3(dirname2(cacheDir), ".grammars-tmp-"));
|
|
11270
|
+
extractGrammarsTarball(bytes, tmp);
|
|
11271
|
+
if (!existsSync2(join3(tmp, "web-tree-sitter.wasm"))) {
|
|
11272
|
+
throw new Error("archive is missing web-tree-sitter.wasm");
|
|
11273
|
+
}
|
|
11274
|
+
if (existsSync2(cacheDir)) rmSync(cacheDir, { recursive: true, force: true });
|
|
11275
|
+
renameSync(tmp, cacheDir);
|
|
11276
|
+
tmp = void 0;
|
|
11277
|
+
if (expected) writeFileSync(markerPath, expected + "\n");
|
|
11278
|
+
} catch (e) {
|
|
11279
|
+
if (tmp) {
|
|
11280
|
+
try {
|
|
11281
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
11282
|
+
} catch {
|
|
11283
|
+
}
|
|
11284
|
+
}
|
|
11285
|
+
return {
|
|
11286
|
+
ok: false,
|
|
11287
|
+
status: "failed",
|
|
11288
|
+
cacheDir,
|
|
11289
|
+
message: `codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
|
|
11290
|
+
`
|
|
11291
|
+
};
|
|
11292
|
+
}
|
|
11293
|
+
return { ok: true, status: "pulled", cacheDir, message: `codeindex: grammars extracted \u2192 ${cacheDir}
|
|
11294
|
+
` };
|
|
11295
|
+
}
|
|
11296
|
+
|
|
11297
|
+
// src/ast/warm.ts
|
|
11298
|
+
init_loader();
|
|
11299
|
+
async function warmGrammars(opts = {}) {
|
|
11300
|
+
const label = opts.label ?? "codeindex";
|
|
11301
|
+
const notes = [];
|
|
11302
|
+
const note = (msg) => {
|
|
11303
|
+
notes.push(msg);
|
|
11304
|
+
if (opts.onNote) opts.onNote(msg);
|
|
11305
|
+
else process.stderr.write(msg);
|
|
11306
|
+
};
|
|
11307
|
+
const noPull = process.env.CODEINDEX_NO_GRAMMARS_PULL;
|
|
11308
|
+
const mayPull = (opts.pull ?? true) && !(noPull && noPull.trim() && noPull !== "0");
|
|
11309
|
+
const keys = [...opts.keys ?? allGrammarKeys()];
|
|
11310
|
+
let pulled = false;
|
|
11311
|
+
if (resolveGrammarsTier().tier === "none" && mayPull) {
|
|
11312
|
+
note(`${label}: tree-sitter grammars not found locally \u2014 pulling them into the shared cache (once per machine)\u2026
|
|
11313
|
+
`);
|
|
11314
|
+
const res = await pullGrammars(sharedGrammarsCacheDir(), { onNote: note });
|
|
11315
|
+
note(res.message);
|
|
11316
|
+
pulled = res.ok && res.status === "pulled";
|
|
11317
|
+
}
|
|
11318
|
+
await ensureGrammars(keys);
|
|
11319
|
+
const tier = resolveGrammarsTier().tier;
|
|
11320
|
+
const ready = keys.some((k) => grammarReady(k));
|
|
11321
|
+
if (!ready) {
|
|
11322
|
+
note(
|
|
11323
|
+
`${label}: no tree-sitter grammars available (offline?) \u2014 extracting with the regex tier, so symbols and call sites are less precise. Run \`codeindex grammars pull\` once online to enable AST precision.
|
|
11324
|
+
`
|
|
11325
|
+
);
|
|
11326
|
+
}
|
|
11327
|
+
return { tier, ready, pulled, notes };
|
|
11328
|
+
}
|
|
11170
11329
|
|
|
11171
11330
|
// src/engine.ts
|
|
11172
11331
|
init_resolve();
|
|
@@ -11450,8 +11609,8 @@ init_util();
|
|
|
11450
11609
|
init_types();
|
|
11451
11610
|
init_types();
|
|
11452
11611
|
init_loader();
|
|
11453
|
-
import { existsSync as
|
|
11454
|
-
import {
|
|
11612
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
11613
|
+
import { join as join15, resolve as resolve2 } from "path";
|
|
11455
11614
|
init_pipeline();
|
|
11456
11615
|
init_hash();
|
|
11457
11616
|
init_graph_json();
|
|
@@ -11631,7 +11790,7 @@ async function runCli(argv) {
|
|
|
11631
11790
|
return;
|
|
11632
11791
|
}
|
|
11633
11792
|
const flags2 = parseFlags(rest);
|
|
11634
|
-
if (!
|
|
11793
|
+
if (!existsSync5(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
|
|
11635
11794
|
const scans = !SCANLESS_COMMANDS.has(cmd) && !(cmd === "embed" && flags2.positional !== "build");
|
|
11636
11795
|
let precomputedWalk;
|
|
11637
11796
|
if (scans && !flags2.noAst) {
|
|
@@ -11651,7 +11810,7 @@ async function runCli(argv) {
|
|
|
11651
11810
|
let cache;
|
|
11652
11811
|
let meta = {};
|
|
11653
11812
|
try {
|
|
11654
|
-
const parsed = JSON.parse(
|
|
11813
|
+
const parsed = JSON.parse(readFileSync8(cachePath, "utf8"));
|
|
11655
11814
|
if (parsed.schemaVersion === SCHEMA_VERSION && parsed.extractorVersion === EXTRACTOR_VERSION) {
|
|
11656
11815
|
cache = new Map(Object.entries(parsed.files));
|
|
11657
11816
|
meta = {
|
|
@@ -11672,7 +11831,7 @@ async function runCli(argv) {
|
|
|
11672
11831
|
const embedPath = join15(outDir, "embeddings.bin");
|
|
11673
11832
|
const artifactSha = (path) => {
|
|
11674
11833
|
try {
|
|
11675
|
-
return sha1(
|
|
11834
|
+
return sha1(readFileSync8(path));
|
|
11676
11835
|
} catch {
|
|
11677
11836
|
return void 0;
|
|
11678
11837
|
}
|
|
@@ -11892,7 +12051,7 @@ async function runCli(argv) {
|
|
|
11892
12051
|
const cacheDir = sharedGrammarsCacheDir();
|
|
11893
12052
|
if (sub === "status") {
|
|
11894
12053
|
const info2 = resolveGrammarsTier();
|
|
11895
|
-
const runtimePresent = info2.dir ?
|
|
12054
|
+
const runtimePresent = info2.dir ? existsSync5(join15(info2.dir, "web-tree-sitter.wasm")) : false;
|
|
11896
12055
|
const target = resolveGrammarsPullTarget();
|
|
11897
12056
|
const status = {
|
|
11898
12057
|
engineVersion: ENGINE_VERSION,
|
|
@@ -11905,77 +12064,15 @@ async function runCli(argv) {
|
|
|
11905
12064
|
};
|
|
11906
12065
|
emit(JSON.stringify(status, null, 2) + "\n", flags2.out);
|
|
11907
12066
|
} else if (sub === "pull") {
|
|
11908
|
-
const
|
|
11909
|
-
|
|
11910
|
-
if (
|
|
11911
|
-
try {
|
|
11912
|
-
expected = await fetchExpectedSha256(target.sha256Url);
|
|
11913
|
-
} catch (e) {
|
|
11914
|
-
process.stderr.write(
|
|
11915
|
-
`codeindex: could not fetch checksum (${e instanceof Error ? e.message : String(e)}) \u2014 proceeding unverified
|
|
11916
|
-
`
|
|
11917
|
-
);
|
|
11918
|
-
}
|
|
11919
|
-
}
|
|
11920
|
-
const runtime = join15(cacheDir, "web-tree-sitter.wasm");
|
|
11921
|
-
const markerPath = join15(dirname4(cacheDir), `${ENGINE_VERSION}.sha256`);
|
|
11922
|
-
if (existsSync4(runtime) && expected && existsSync4(markerPath)) {
|
|
11923
|
-
let marker = "";
|
|
11924
|
-
try {
|
|
11925
|
-
marker = readFileSync6(markerPath, "utf8").trim();
|
|
11926
|
-
} catch {
|
|
11927
|
-
}
|
|
11928
|
-
if (marker === expected) {
|
|
11929
|
-
process.stderr.write(`codeindex: grammars already present at ${cacheDir} (up to date)
|
|
11930
|
-
`);
|
|
11931
|
-
return;
|
|
11932
|
-
}
|
|
11933
|
-
}
|
|
11934
|
-
process.stderr.write(`codeindex: fetching grammars from ${target.url} \u2192 ${cacheDir}
|
|
11935
|
-
`);
|
|
11936
|
-
let bytes;
|
|
11937
|
-
try {
|
|
11938
|
-
bytes = await fetchGrammarsTarball(target.url, expected);
|
|
11939
|
-
} catch (e) {
|
|
11940
|
-
process.stderr.write(`codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
|
|
11941
|
-
`);
|
|
11942
|
-
process.exitCode = 1;
|
|
11943
|
-
return;
|
|
11944
|
-
}
|
|
11945
|
-
let tmp;
|
|
11946
|
-
try {
|
|
11947
|
-
mkdirSync3(dirname4(cacheDir), { recursive: true });
|
|
11948
|
-
tmp = mkdtempSync(join15(dirname4(cacheDir), ".grammars-tmp-"));
|
|
11949
|
-
extractGrammarsTarball(bytes, tmp);
|
|
11950
|
-
if (!existsSync4(join15(tmp, "web-tree-sitter.wasm"))) {
|
|
11951
|
-
throw new Error("archive is missing web-tree-sitter.wasm");
|
|
11952
|
-
}
|
|
11953
|
-
if (existsSync4(cacheDir)) rmSync2(cacheDir, { recursive: true, force: true });
|
|
11954
|
-
renameSync(tmp, cacheDir);
|
|
11955
|
-
tmp = void 0;
|
|
11956
|
-
if (expected) writeFileSync4(markerPath, expected + "\n");
|
|
11957
|
-
} catch (e) {
|
|
11958
|
-
if (tmp) {
|
|
11959
|
-
try {
|
|
11960
|
-
rmSync2(tmp, { recursive: true, force: true });
|
|
11961
|
-
} catch {
|
|
11962
|
-
}
|
|
11963
|
-
}
|
|
11964
|
-
process.stderr.write(
|
|
11965
|
-
`codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
|
|
11966
|
-
`
|
|
11967
|
-
);
|
|
11968
|
-
process.exitCode = 1;
|
|
11969
|
-
return;
|
|
11970
|
-
}
|
|
11971
|
-
process.stderr.write(`codeindex: grammars extracted \u2192 ${cacheDir}
|
|
11972
|
-
`);
|
|
12067
|
+
const res = await pullGrammars(cacheDir, { onNote: (m) => process.stderr.write(m) });
|
|
12068
|
+
process.stderr.write(res.message);
|
|
12069
|
+
if (!res.ok) process.exitCode = 1;
|
|
11973
12070
|
} else {
|
|
11974
12071
|
throw new Error("grammars needs a subcommand: status | pull");
|
|
11975
12072
|
}
|
|
11976
12073
|
} else if (cmd === "rules") {
|
|
11977
12074
|
if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
|
|
11978
|
-
const rules = parseRules(JSON.parse(
|
|
12075
|
+
const rules = parseRules(JSON.parse(readFileSync8(flags2.config, "utf8")));
|
|
11979
12076
|
const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
|
|
11980
12077
|
const violations = checkRules(graph, rules);
|
|
11981
12078
|
const errors = violations.filter((v) => v.severity === "error").length;
|
|
@@ -12126,6 +12223,7 @@ export {
|
|
|
12126
12223
|
parseGitignore,
|
|
12127
12224
|
parseRules,
|
|
12128
12225
|
probeEndpoint,
|
|
12226
|
+
pullGrammars,
|
|
12129
12227
|
quantize,
|
|
12130
12228
|
rankHotspots,
|
|
12131
12229
|
rankedKeywords,
|
|
@@ -12172,6 +12270,7 @@ export {
|
|
|
12172
12270
|
untestedModules,
|
|
12173
12271
|
untrackedFiles,
|
|
12174
12272
|
walk,
|
|
12273
|
+
warmGrammars,
|
|
12175
12274
|
wordpiece,
|
|
12176
12275
|
writeMemory
|
|
12177
12276
|
};
|
package/src/ast/grammars-pull.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve, sep } from "node:path";
|
|
4
4
|
import { gunzipSync } from "node:zlib";
|
|
5
5
|
import { ENGINE_VERSION } from "../types.js";
|
|
@@ -176,3 +176,97 @@ export function extractGrammarsTarball(bytes: Uint8Array, destDir: string): stri
|
|
|
176
176
|
const raw = b.length >= 2 && b[0] === 0x1f && b[1] === 0x8b ? gunzipSync(b) : b;
|
|
177
177
|
return extractTarInto(raw, destDir);
|
|
178
178
|
}
|
|
179
|
+
|
|
180
|
+
// What a pull did. `status` distinguishes the three terminal states so a caller
|
|
181
|
+
// can react without parsing prose: nothing to do, wasms installed, or failed
|
|
182
|
+
// (and on failure NOTHING was written — the cache is exactly as it was).
|
|
183
|
+
export interface GrammarsPullResult {
|
|
184
|
+
ok: boolean;
|
|
185
|
+
status: "up-to-date" | "pulled" | "failed";
|
|
186
|
+
cacheDir: string;
|
|
187
|
+
/** One human-readable line; the CLI writes it to stderr, a library caller may log or drop it. */
|
|
188
|
+
message: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// The whole `grammars pull` mechanic — resolve target, fetch the sha256 sidecar,
|
|
192
|
+
// skip when the cache already holds that exact digest, download, then install
|
|
193
|
+
// ATOMICALLY (extract into a tmp sibling, verify the runtime wasm landed, swap
|
|
194
|
+
// by rename). Extracted from the CLI so `warmGrammars` and `codeindex grammars
|
|
195
|
+
// pull` share ONE implementation: the CLI maps the result onto stderr +
|
|
196
|
+
// process.exitCode, the warm path folds it into its own report.
|
|
197
|
+
//
|
|
198
|
+
// NEVER throws: every failure becomes `{ ok: false, status: "failed" }` with a
|
|
199
|
+
// message, because both callers must survive an offline machine — the engine
|
|
200
|
+
// simply stays on the regex tier. `onNote` receives progress lines (checksum
|
|
201
|
+
// degradation, download start) that are not the terminal message.
|
|
202
|
+
export async function pullGrammars(
|
|
203
|
+
cacheDir: string,
|
|
204
|
+
opts: { onNote?: (msg: string) => void } = {},
|
|
205
|
+
): Promise<GrammarsPullResult> {
|
|
206
|
+
const note = opts.onNote ?? (() => {});
|
|
207
|
+
const target = resolveGrammarsPullTarget();
|
|
208
|
+
let expected: string | undefined;
|
|
209
|
+
if (target.sha256Url) {
|
|
210
|
+
try {
|
|
211
|
+
expected = await fetchExpectedSha256(target.sha256Url);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
note(`codeindex: could not fetch checksum (${e instanceof Error ? e.message : String(e)}) — proceeding unverified\n`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Idempotent: the marker sibling records the digest of the tarball that
|
|
217
|
+
// populated cacheDir. Runtime wasm present AND marker === freshly-fetched
|
|
218
|
+
// digest ⇒ already up to date, skip the ~22 MB download.
|
|
219
|
+
const runtime = join(cacheDir, "web-tree-sitter.wasm");
|
|
220
|
+
const markerPath = join(dirname(cacheDir), `${ENGINE_VERSION}.sha256`);
|
|
221
|
+
if (existsSync(runtime) && expected && existsSync(markerPath)) {
|
|
222
|
+
let marker = "";
|
|
223
|
+
try {
|
|
224
|
+
marker = readFileSync(markerPath, "utf8").trim();
|
|
225
|
+
} catch {
|
|
226
|
+
// unreadable marker → fall through and re-pull
|
|
227
|
+
}
|
|
228
|
+
if (marker === expected) {
|
|
229
|
+
return { ok: true, status: "up-to-date", cacheDir, message: `codeindex: grammars already present at ${cacheDir} (up to date)\n` };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
note(`codeindex: fetching grammars from ${target.url} → ${cacheDir}\n`);
|
|
233
|
+
let bytes: Uint8Array;
|
|
234
|
+
try {
|
|
235
|
+
bytes = await fetchGrammarsTarball(target.url, expected);
|
|
236
|
+
} catch (e) {
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
status: "failed",
|
|
240
|
+
cacheDir,
|
|
241
|
+
message: `codeindex: pull failed — ${e instanceof Error ? e.message : String(e)} (nothing written)\n`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
let tmp: string | undefined;
|
|
245
|
+
try {
|
|
246
|
+
mkdirSync(dirname(cacheDir), { recursive: true });
|
|
247
|
+
tmp = mkdtempSync(join(dirname(cacheDir), ".grammars-tmp-"));
|
|
248
|
+
extractGrammarsTarball(bytes, tmp);
|
|
249
|
+
if (!existsSync(join(tmp, "web-tree-sitter.wasm"))) {
|
|
250
|
+
throw new Error("archive is missing web-tree-sitter.wasm");
|
|
251
|
+
}
|
|
252
|
+
if (existsSync(cacheDir)) rmSync(cacheDir, { recursive: true, force: true });
|
|
253
|
+
renameSync(tmp, cacheDir);
|
|
254
|
+
tmp = undefined;
|
|
255
|
+
if (expected) writeFileSync(markerPath, expected + "\n");
|
|
256
|
+
} catch (e) {
|
|
257
|
+
if (tmp) {
|
|
258
|
+
try {
|
|
259
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
260
|
+
} catch {
|
|
261
|
+
// best-effort cleanup
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
status: "failed",
|
|
267
|
+
cacheDir,
|
|
268
|
+
message: `codeindex: pull failed — ${e instanceof Error ? e.message : String(e)} (nothing written)\n`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return { ok: true, status: "pulled", cacheDir, message: `codeindex: grammars extracted → ${cacheDir}\n` };
|
|
272
|
+
}
|
package/src/ast/warm.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { allGrammarKeys, ensureGrammars, grammarReady, resolveGrammarsTier, sharedGrammarsCacheDir } from "./loader.js";
|
|
2
|
+
import type { GrammarsTierName } from "./loader.js";
|
|
3
|
+
import { pullGrammars } from "./grammars-pull.js";
|
|
4
|
+
|
|
5
|
+
// The one-call AST warm-up every consumer needs, and which every consumer but
|
|
6
|
+
// ultraindex was missing.
|
|
7
|
+
//
|
|
8
|
+
// WHY THIS EXISTS. Extraction is AST-preferred with a regex fallback
|
|
9
|
+
// (src/extract/code.ts), but `extractAst` returns undefined unless
|
|
10
|
+
// `grammarReady(key)` — and that is only true after an `await ensureGrammars()`.
|
|
11
|
+
// `scanRepo` is deliberately synchronous, so it CANNOT warm anything itself.
|
|
12
|
+
// A consumer that never awaits the warm-up therefore runs on the regex tier
|
|
13
|
+
// forever, on every machine, no matter what the grammar cache holds — silently,
|
|
14
|
+
// since the fallback is by design invisible. That is a capability quietly left
|
|
15
|
+
// on the table, not a crash, which is exactly why it survived unnoticed.
|
|
16
|
+
//
|
|
17
|
+
// So: call this ONCE at your CLI entry, before the synchronous pipeline. It is
|
|
18
|
+
// idempotent, offline-safe, and never throws.
|
|
19
|
+
export interface WarmGrammarsResult {
|
|
20
|
+
/** Tier AFTER the warm-up (a successful pull moves "none" → "cache"). */
|
|
21
|
+
tier: GrammarsTierName;
|
|
22
|
+
/** True when at least one requested grammar is loaded ⇒ the AST tier is live. */
|
|
23
|
+
ready: boolean;
|
|
24
|
+
/** True when this call populated the shared cache over the network. */
|
|
25
|
+
pulled: boolean;
|
|
26
|
+
/** Everything written to `onNote`, in order — so a caller can persist the trail in its run artifacts. */
|
|
27
|
+
notes: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface WarmGrammarsOptions {
|
|
31
|
+
/** Grammars to load. Default: every shipped grammar. Narrow it with `grammarKeysForExts` when the repo's languages are known. */
|
|
32
|
+
keys?: Iterable<string>;
|
|
33
|
+
/** Fetch the wasms into the shared cache when nothing is resolvable. Default true; `CODEINDEX_NO_GRAMMARS_PULL=1` forces false. */
|
|
34
|
+
pull?: boolean;
|
|
35
|
+
/** Prefix for the diagnostics ("ultrasec: …"). Default "codeindex". */
|
|
36
|
+
label?: string;
|
|
37
|
+
/** Where diagnostics go. Default: process.stderr. Pass a sink to keep stdout/stderr clean. */
|
|
38
|
+
onNote?: (msg: string) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function warmGrammars(opts: WarmGrammarsOptions = {}): Promise<WarmGrammarsResult> {
|
|
42
|
+
const label = opts.label ?? "codeindex";
|
|
43
|
+
const notes: string[] = [];
|
|
44
|
+
const note = (msg: string): void => {
|
|
45
|
+
notes.push(msg);
|
|
46
|
+
if (opts.onNote) opts.onNote(msg);
|
|
47
|
+
else process.stderr.write(msg);
|
|
48
|
+
};
|
|
49
|
+
const noPull = process.env.CODEINDEX_NO_GRAMMARS_PULL;
|
|
50
|
+
const mayPull = (opts.pull ?? true) && !(noPull && noPull.trim() && noPull !== "0");
|
|
51
|
+
const keys = [...(opts.keys ?? allGrammarKeys())];
|
|
52
|
+
|
|
53
|
+
let pulled = false;
|
|
54
|
+
if (resolveGrammarsTier().tier === "none" && mayPull) {
|
|
55
|
+
note(`${label}: tree-sitter grammars not found locally — pulling them into the shared cache (once per machine)…\n`);
|
|
56
|
+
const res = await pullGrammars(sharedGrammarsCacheDir(), { onNote: note });
|
|
57
|
+
note(res.message);
|
|
58
|
+
pulled = res.ok && res.status === "pulled";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await ensureGrammars(keys);
|
|
62
|
+
|
|
63
|
+
const tier = resolveGrammarsTier().tier;
|
|
64
|
+
const ready = keys.some((k) => grammarReady(k));
|
|
65
|
+
if (!ready) {
|
|
66
|
+
// Never silent: a degraded run must say so, and stay a SUCCESSFUL run —
|
|
67
|
+
// the regex tier still produces a complete, searchable result.
|
|
68
|
+
note(
|
|
69
|
+
`${label}: no tree-sitter grammars available (offline?) — extracting with the regex tier, so symbols and call sites are less precise. ` +
|
|
70
|
+
"Run `codeindex grammars pull` once online to enable AST precision.\n",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return { tier, ready, pulled, notes };
|
|
74
|
+
}
|
package/src/engine-cli.ts
CHANGED
|
@@ -3,12 +3,7 @@ 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
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";
|
|
6
|
+
import { resolveGrammarsPullTarget, pullGrammars } from "./ast/grammars-pull.js";
|
|
12
7
|
import { buildIndexArtifacts, buildArtifactsFromScan, type BuildIndexOptions } from "./pipeline.js";
|
|
13
8
|
import { sha1 } from "./hash.js";
|
|
14
9
|
import { renderGraphJson } from "./render/graph-json.js";
|
|
@@ -593,81 +588,14 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
593
588
|
};
|
|
594
589
|
emit(JSON.stringify(status, null, 2) + "\n", flags.out);
|
|
595
590
|
} else if (sub === "pull") {
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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`);
|
|
591
|
+
// The mechanic itself (sidecar checksum, idempotent skip, atomic install)
|
|
592
|
+
// lives in pullGrammars — shared verbatim with warmGrammars, so the CLI
|
|
593
|
+
// and the library warm-up can never drift apart. Here we only map its
|
|
594
|
+
// result onto the CLI contract: progress notes and the terminal message to
|
|
595
|
+
// stderr, non-zero exit on failure (which wrote nothing).
|
|
596
|
+
const res = await pullGrammars(cacheDir, { onNote: (m) => process.stderr.write(m) });
|
|
597
|
+
process.stderr.write(res.message);
|
|
598
|
+
if (!res.ok) process.exitCode = 1;
|
|
671
599
|
} else {
|
|
672
600
|
throw new Error("grammars needs a subcommand: status | pull");
|
|
673
601
|
}
|
package/src/engine.ts
CHANGED
|
@@ -60,8 +60,14 @@ export {
|
|
|
60
60
|
fetchExpectedSha256,
|
|
61
61
|
extractTarInto,
|
|
62
62
|
extractGrammarsTarball,
|
|
63
|
+
pullGrammars,
|
|
63
64
|
} from "./ast/grammars-pull.js";
|
|
64
|
-
export type { GrammarsPullTarget } from "./ast/grammars-pull.js";
|
|
65
|
+
export type { GrammarsPullTarget, GrammarsPullResult } from "./ast/grammars-pull.js";
|
|
66
|
+
// The one-call AST warm-up for consumers. `scanRepo` is synchronous and cannot
|
|
67
|
+
// warm the grammars itself, so a consumer that never awaits this stays on the
|
|
68
|
+
// regex tier forever — silently. Call it once at the CLI entry.
|
|
69
|
+
export { warmGrammars } from "./ast/warm.js";
|
|
70
|
+
export type { WarmGrammarsResult, WarmGrammarsOptions } from "./ast/warm.js";
|
|
65
71
|
|
|
66
72
|
// Resolution + modules + graph tier.
|
|
67
73
|
export { buildResolveContext, resolveImport, resolveDocLink } from "./resolve.js";
|
package/src/mcp.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
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";
|
|
8
|
+
import { readFileSync, statSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { createInterface } from "node:readline";
|
|
11
|
-
import { ENGINE_VERSION, type FileRecord } from "./types.js";
|
|
11
|
+
import { ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION, type FileRecord, type Graph, type SymbolIndex } from "./types.js";
|
|
12
12
|
import { ensureGrammars, grammarKeysForExts } from "./ast/loader.js";
|
|
13
13
|
import { buildArtifactsFromScan, type IndexArtifacts } from "./pipeline.js";
|
|
14
14
|
import { renderGraphJson } from "./render/graph-json.js";
|
|
@@ -419,7 +419,8 @@ export function memoizedEmbedModel(modelDir: string): StaticEmbedModel | undefin
|
|
|
419
419
|
// and a caller-supplied stale walk would desynchronize the freshness oracle.
|
|
420
420
|
export type SessionScanOptions = Omit<ScanOptions, "cache" | "precomputedWalk">;
|
|
421
421
|
|
|
422
|
-
type
|
|
422
|
+
type SessionCacheEntry = { hash: string; record: FileRecord; size?: number; mtimeMs?: number };
|
|
423
|
+
type SessionCacheMap = Map<string, SessionCacheEntry>;
|
|
423
424
|
|
|
424
425
|
// A SINGLE entry — never an unbounded map — holding the most recent scan.
|
|
425
426
|
let sessionCache:
|
|
@@ -457,6 +458,128 @@ export function toCacheMap(scan: RepoScan): SessionCacheMap {
|
|
|
457
458
|
return m;
|
|
458
459
|
}
|
|
459
460
|
|
|
461
|
+
// --- persisted-index preload -------------------------------------------------
|
|
462
|
+
// On the FIRST tool call for a repo, a committed .codeindex/ index (written by
|
|
463
|
+
// `codeindex index`) lets the session skip work TWO ways. cache.json seeds the
|
|
464
|
+
// session scan, so every unchanged file takes scan.ts's stat fastpath instead
|
|
465
|
+
// of a read + hash + extraction; and — only when the T4 freshness guard holds —
|
|
466
|
+
// the persisted graph.json/symbols.json are deserialized straight into the
|
|
467
|
+
// session, so the first graph/symbols/mermaid/repo_map/check_rules call skips
|
|
468
|
+
// the whole downstream pipeline (buildArtifactsFromScan). Both are pure
|
|
469
|
+
// optimizations: the seeded scan's reused records are value-identical to a cold
|
|
470
|
+
// scan's (T3/T4 determinism), and the guard is the SAME oracle the CLI's index
|
|
471
|
+
// fastpath uses to prove the on-disk artifacts equal a fresh build here. Absent,
|
|
472
|
+
// stale, corrupt, or any version/commit/sha mismatch → every step falls back to
|
|
473
|
+
// today's cold path EXACTLY (a fresh scanRepo / buildArtifactsFromScan), never a
|
|
474
|
+
// throw.
|
|
475
|
+
|
|
476
|
+
// ADDITIVE cache.json meta describing the artifacts a prior `index` run wrote
|
|
477
|
+
// (see engine-cli.ts's CacheMeta). Old caches lacking these keys simply never
|
|
478
|
+
// pass the guard below — their per-file records are still reused to seed the
|
|
479
|
+
// scan. Only the graph/symbols shas matter here; the embed sidecar has its own
|
|
480
|
+
// memoization path.
|
|
481
|
+
interface PersistedMeta {
|
|
482
|
+
engineVersion?: string;
|
|
483
|
+
commit?: string;
|
|
484
|
+
graphSha1?: string;
|
|
485
|
+
symbolsSha1?: string;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Read <repo>/.codeindex/cache.json into the (cacheMap, meta) the preload needs.
|
|
489
|
+
// Per-file records are reusable ONLY when (schemaVersion, extractorVersion)
|
|
490
|
+
// match this engine — the exact gate the CLI applies before trusting a cache —
|
|
491
|
+
// otherwise the whole cache is discarded (cold scan). Any read/parse failure (no
|
|
492
|
+
// index yet, unreadable, malformed) returns undefined: the cold path.
|
|
493
|
+
function readPersistedIndex(repo: string): { cacheMap: SessionCacheMap; meta: PersistedMeta } | undefined {
|
|
494
|
+
let parsed:
|
|
495
|
+
| ({ schemaVersion?: number; extractorVersion?: number; files?: Record<string, SessionCacheEntry> } & PersistedMeta)
|
|
496
|
+
| undefined;
|
|
497
|
+
try {
|
|
498
|
+
parsed = JSON.parse(readFileSync(join(repo, ".codeindex", "cache.json"), "utf8")) as typeof parsed;
|
|
499
|
+
} catch {
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
if (!parsed || parsed.schemaVersion !== SCHEMA_VERSION || parsed.extractorVersion !== EXTRACTOR_VERSION || !parsed.files) {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
const cacheMap: SessionCacheMap = new Map(Object.entries(parsed.files));
|
|
506
|
+
const meta: PersistedMeta = {
|
|
507
|
+
engineVersion: parsed.engineVersion,
|
|
508
|
+
commit: parsed.commit,
|
|
509
|
+
graphSha1: parsed.graphSha1,
|
|
510
|
+
symbolsSha1: parsed.symbolsSha1,
|
|
511
|
+
};
|
|
512
|
+
return { cacheMap, meta };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// The T4 freshness guard, applied to a session scan seeded from cache.json:
|
|
516
|
+
// contentUnchanged proves this scan's records are the ones that built the
|
|
517
|
+
// on-disk artifacts; engineVersion pins the version stamp graph.json embeds and
|
|
518
|
+
// commit the HEAD it embeds; the sha checks prove the on-disk bytes ARE that
|
|
519
|
+
// build's output. All true ⇒ graph.json/symbols.json are byte-equal to
|
|
520
|
+
// buildArtifactsFromScan(scan) run here, so deserialize them instead of
|
|
521
|
+
// rebuilding. Graph/SymbolIndex are pure JSON POJOs (no Map/Set/typed fields),
|
|
522
|
+
// so JSON.parse is a lossless round-trip — a schemaVersion assert is the only
|
|
523
|
+
// reconstruction needed (see the round-trip test). ANY failure — a stale scan,
|
|
524
|
+
// a version/commit/sha mismatch, a missing/corrupt/partial artifact, an
|
|
525
|
+
// unexpected schemaVersion — returns undefined so the caller rebuilds. NEVER
|
|
526
|
+
// throws (a corrupt artifact must degrade, not crash the session).
|
|
527
|
+
function preloadArtifacts(repo: string, scan: RepoScan, meta: PersistedMeta): IndexArtifacts | undefined {
|
|
528
|
+
if (
|
|
529
|
+
!scan.contentUnchanged ||
|
|
530
|
+
meta.engineVersion !== ENGINE_VERSION ||
|
|
531
|
+
meta.commit !== scan.commit ||
|
|
532
|
+
meta.graphSha1 === undefined ||
|
|
533
|
+
meta.symbolsSha1 === undefined
|
|
534
|
+
) {
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
const dir = join(repo, ".codeindex");
|
|
538
|
+
let graphBytes: Buffer;
|
|
539
|
+
let symbolsBytes: Buffer;
|
|
540
|
+
try {
|
|
541
|
+
graphBytes = readFileSync(join(dir, "graph.json"));
|
|
542
|
+
symbolsBytes = readFileSync(join(dir, "symbols.json"));
|
|
543
|
+
} catch {
|
|
544
|
+
return undefined; // a sha'd artifact went missing since cache.json — rebuild
|
|
545
|
+
}
|
|
546
|
+
// sha over the raw bytes; sha1(string) hashes the same UTF-8 bytes writeFileSync
|
|
547
|
+
// put on disk, so this equals the meta sha the CLI computed over the render.
|
|
548
|
+
if (sha1(graphBytes) !== meta.graphSha1 || sha1(symbolsBytes) !== meta.symbolsSha1) {
|
|
549
|
+
return undefined; // tampered / partial / corrupt on-disk bytes — rebuild
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const graph = JSON.parse(graphBytes.toString("utf8")) as Graph;
|
|
553
|
+
const symbols = JSON.parse(symbolsBytes.toString("utf8")) as SymbolIndex;
|
|
554
|
+
if (graph.schemaVersion !== SCHEMA_VERSION || symbols.schemaVersion !== SCHEMA_VERSION) return undefined;
|
|
555
|
+
return { scan, graph, symbols };
|
|
556
|
+
} catch {
|
|
557
|
+
// Unreachable once the shas matched (the bytes are valid JSON this engine
|
|
558
|
+
// wrote), but the contract is "never throw" — degrade to a rebuild.
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// First-touch preload: seed the session scan from cache.json and, when the guard
|
|
564
|
+
// holds, the artifacts from graph.json/symbols.json. undefined ⇒ no persisted
|
|
565
|
+
// index ⇒ the caller takes the cold scanRepo path unchanged.
|
|
566
|
+
function preloadSession(
|
|
567
|
+
repo: string,
|
|
568
|
+
opts: SessionScanOptions,
|
|
569
|
+
): { scan: RepoScan; cacheMap: SessionCacheMap; arts?: IndexArtifacts } | undefined {
|
|
570
|
+
const persisted = readPersistedIndex(repo);
|
|
571
|
+
if (!persisted) return undefined;
|
|
572
|
+
// Seed the scan from the persisted records — scan.ts's stat fastpath + exact
|
|
573
|
+
// content-hash reuse make this value-identical to a cold scan (T3/T4), only
|
|
574
|
+
// cheaper, and it computes the contentUnchanged the artifact guard reads. When
|
|
575
|
+
// the on-disk content drifted from cache.json, changed files are re-read/
|
|
576
|
+
// extracted here exactly as a cold scan would, so the scan stays correct and
|
|
577
|
+
// the guard simply fails (arts undefined → rebuild on demand).
|
|
578
|
+
const scan = scanRepo(repo, { ...opts, cache: persisted.cacheMap });
|
|
579
|
+
const arts = preloadArtifacts(repo, scan, persisted.meta);
|
|
580
|
+
return { scan, cacheMap: toCacheMap(scan), arts };
|
|
581
|
+
}
|
|
582
|
+
|
|
460
583
|
// The memoizing replacement for scanRepo inside callTool. Exported for tests.
|
|
461
584
|
export function getScan(repo: string, opts: SessionScanOptions = {}): RepoScan {
|
|
462
585
|
const key = sessionKey(repo, opts);
|
|
@@ -486,6 +609,15 @@ export function getScan(repo: string, opts: SessionScanOptions = {}): RepoScan {
|
|
|
486
609
|
sessionCache = { key, scan: fresh, cacheMap: toCacheMap(fresh) };
|
|
487
610
|
return fresh;
|
|
488
611
|
}
|
|
612
|
+
// First touch of this (repo, opts): try the persisted-index preload before a
|
|
613
|
+
// cold scan. A present, version-compatible .codeindex/cache.json seeds the
|
|
614
|
+
// scan (and, when the guard holds, the artifacts); absent it, fall through to
|
|
615
|
+
// the cold path EXACTLY as before.
|
|
616
|
+
const preloaded = preloadSession(repo, opts);
|
|
617
|
+
if (preloaded) {
|
|
618
|
+
sessionCache = { key, scan: preloaded.scan, cacheMap: preloaded.cacheMap, arts: preloaded.arts };
|
|
619
|
+
return preloaded.scan;
|
|
620
|
+
}
|
|
489
621
|
const scan = scanRepo(repo, opts);
|
|
490
622
|
sessionCache = { key, scan, cacheMap: toCacheMap(scan) };
|
|
491
623
|
return scan;
|
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.16.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
|