@maxgfr/codeindex 2.14.0 → 2.15.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/docs/MIGRATION.md +52 -0
- package/package.json +1 -1
- package/scripts/engine.d.mts +2 -2
- package/scripts/engine.mjs +63 -7
- package/src/mcp.ts +135 -3
- package/src/types.ts +1 -1
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.15.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.15.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.15.0/grammars-2.15.0.tar.gz";
|
|
259
259
|
interface GrammarsPullTarget {
|
|
260
260
|
url: string;
|
|
261
261
|
sha256Url?: string;
|
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.15.0";
|
|
18
18
|
SCHEMA_VERSION = 4;
|
|
19
19
|
EXTRACTOR_VERSION = 10;
|
|
20
20
|
}
|
|
@@ -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 readFileSync6, 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(readFileSync6(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 = readFileSync6(join14(dir, "graph.json"));
|
|
10388
|
+
symbolsBytes = readFileSync6(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;
|
|
@@ -11450,7 +11506,7 @@ init_util();
|
|
|
11450
11506
|
init_types();
|
|
11451
11507
|
init_types();
|
|
11452
11508
|
init_loader();
|
|
11453
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as
|
|
11509
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync7, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
11454
11510
|
import { dirname as dirname4, join as join15, resolve as resolve2 } from "path";
|
|
11455
11511
|
init_pipeline();
|
|
11456
11512
|
init_hash();
|
|
@@ -11651,7 +11707,7 @@ async function runCli(argv) {
|
|
|
11651
11707
|
let cache;
|
|
11652
11708
|
let meta = {};
|
|
11653
11709
|
try {
|
|
11654
|
-
const parsed = JSON.parse(
|
|
11710
|
+
const parsed = JSON.parse(readFileSync7(cachePath, "utf8"));
|
|
11655
11711
|
if (parsed.schemaVersion === SCHEMA_VERSION && parsed.extractorVersion === EXTRACTOR_VERSION) {
|
|
11656
11712
|
cache = new Map(Object.entries(parsed.files));
|
|
11657
11713
|
meta = {
|
|
@@ -11672,7 +11728,7 @@ async function runCli(argv) {
|
|
|
11672
11728
|
const embedPath = join15(outDir, "embeddings.bin");
|
|
11673
11729
|
const artifactSha = (path) => {
|
|
11674
11730
|
try {
|
|
11675
|
-
return sha1(
|
|
11731
|
+
return sha1(readFileSync7(path));
|
|
11676
11732
|
} catch {
|
|
11677
11733
|
return void 0;
|
|
11678
11734
|
}
|
|
@@ -11922,7 +11978,7 @@ async function runCli(argv) {
|
|
|
11922
11978
|
if (existsSync4(runtime) && expected && existsSync4(markerPath)) {
|
|
11923
11979
|
let marker = "";
|
|
11924
11980
|
try {
|
|
11925
|
-
marker =
|
|
11981
|
+
marker = readFileSync7(markerPath, "utf8").trim();
|
|
11926
11982
|
} catch {
|
|
11927
11983
|
}
|
|
11928
11984
|
if (marker === expected) {
|
|
@@ -11975,7 +12031,7 @@ async function runCli(argv) {
|
|
|
11975
12031
|
}
|
|
11976
12032
|
} else if (cmd === "rules") {
|
|
11977
12033
|
if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
|
|
11978
|
-
const rules = parseRules(JSON.parse(
|
|
12034
|
+
const rules = parseRules(JSON.parse(readFileSync7(flags2.config, "utf8")));
|
|
11979
12035
|
const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
|
|
11980
12036
|
const violations = checkRules(graph, rules);
|
|
11981
12037
|
const errors = violations.filter((v) => v.severity === "error").length;
|
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.15.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
|