mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-02 02:11:29 +00:00
* perf(store): add best-effort V8 sidecars beside canonical JSON caches Warm ParsedFile and parse-cache loads skip JSON.parse when a sidecar is present. JSON remains authoritative: envelope validation plus v8.deserialize decide the hit, and any failure falls back without reparsing. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(store): require generation bind or sidecar drop before cache overwrite A same-length JSON rewrite could accept a leftover V8 sidecar if both generation rotation and unlink failed. Refuse the new generation unless at least one of those invalidations succeeds; skip publishing a sidecar when only the drop succeeded. detect_changes --scope all: 7 files, risk low, no affected processes. tsc --noEmit clean; 115/115 relevant unit tests; cache-related integration tests pass. parse-impl-env-reads worker-ready timeout is pre-existing (same 5 failures with this change set stashed). ESLint 0 errors; remaining warnings are pre-existing and not on changed lines. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(autofix): apply prettier + eslint fixes via /autofix command * refactor(store): share V8 overwrite invalidation across persist paths The bind-or-drop gate lived in five writers. One helper keeps the protocol in a single place and lets bind/drop run together on the async path. detect_changes --scope all: 3 files, risk low, no affected processes. Co-authored-by: Cursor <cursoragent@cursor.com> * perf(store): hardlink durable ParsedFile shards into the run store Warm restore of parsedfile-cache into parsedfile-store now publishes all four shard files via fs.link, falling back to copy-into-tmp + rename so a leftover dest hardlink can never be written through. JSON remains the canonical cache; V8 sidecars ride the same path. Co-authored-by: Cursor <cursoragent@cursor.com> * perf(store): load immutable V8 shards in place, drop JSON fallback Warm analyze was still paying JSON.parse plus a restore copy. One .v8 envelope per shard and SCHEMA_BUMP 81 make a miss re-extract instead of serving a stale JSON twin. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(store): validate durable V8 warm-cache restores Reject incomplete or corrupt durable generations and snapshot valid shards before skipping parse workers, preserving ParsedFiles when persistence fails. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(store): drop unused durable load path Load ParsedFiles only from the run-store snapshot and share one checksummed payload reader so inspect and deserialize stay consistent. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
116 lines
4.6 KiB
TypeScript
116 lines
4.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import {
|
|
getDurableParsedFileDir,
|
|
persistDurableParsedFileShardSync,
|
|
durableChunkHasShards,
|
|
loadParsedFilesForPaths,
|
|
} from '../../../src/storage/parsedfile-store.js';
|
|
import type { ParsedFile } from 'gitnexus-shared';
|
|
import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js';
|
|
|
|
// #2082 M2 U5 — the warm/mixed cache seam for statement facts. On a warm (or
|
|
// mixed) run the unchanged chunk's ParsedFiles are loaded from the
|
|
// durable store instead of re-parsed (#2038); if that load (or intern)
|
|
// dropped or aliased the new `bindings`/`statements`
|
|
// fields, reaching-defs would silently degrade to `no-facts` for every cached
|
|
// file — exactly the field-loss class the #2038 mergeChunkResults lesson
|
|
// warns about. This pins the persist → load round-trip at the exact
|
|
// seam scope-resolution consumes.
|
|
|
|
const factCfg: FunctionCfg = {
|
|
filePath: 'src/a.ts',
|
|
functionStartLine: 1,
|
|
functionEndLine: 5,
|
|
functionStartColumn: 0,
|
|
entryIndex: 0,
|
|
exitIndex: 1,
|
|
blocks: [
|
|
{ index: 0, startLine: 1, endLine: 1, text: '', kind: 'entry', statements: [] },
|
|
{
|
|
index: 1,
|
|
startLine: 5,
|
|
endLine: 5,
|
|
text: '',
|
|
kind: 'exit',
|
|
statements: [
|
|
{ line: 2, defs: [0], uses: [] },
|
|
{ line: 3, defs: [1], uses: [0] },
|
|
],
|
|
},
|
|
],
|
|
edges: [{ from: 0, to: 1, kind: 'seq' }],
|
|
bindings: [
|
|
{ name: 'x', declLine: 2, declColumn: 6, kind: 'let' },
|
|
{ name: 'y', declLine: 3, declColumn: 6, kind: 'const' },
|
|
],
|
|
};
|
|
|
|
const mkParsedFile = (filePath: string): ParsedFile =>
|
|
({
|
|
filePath,
|
|
moduleScope: '',
|
|
scopes: [],
|
|
parsedImports: [],
|
|
localDefs: [],
|
|
referenceSites: [],
|
|
cfgSideChannel: [factCfg],
|
|
}) as unknown as ParsedFile;
|
|
|
|
describe('durable ParsedFile store carries M2 statement facts (#2082 U5)', () => {
|
|
let tempDir = '';
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'm2-facts-store-'));
|
|
});
|
|
afterEach(() => {
|
|
if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('persist → loadParsedFilesForPaths preserves bindings + statements deep-equal', async () => {
|
|
const durableDir = getDurableParsedFileDir(tempDir);
|
|
const chunkHash = 'c'.repeat(64);
|
|
const files = ['src/a.ts', 'src/b.ts'];
|
|
|
|
// What a worker writes at flush on a cache MISS (the cold half of a
|
|
// mixed-mode run)…
|
|
persistDurableParsedFileShardSync(durableDir, chunkHash, 7, 0, files.map(mkParsedFile));
|
|
const wanted = new Set(files);
|
|
expect(await durableChunkHasShards(tempDir, chunkHash, wanted)).toBe(true);
|
|
const loaded = await loadParsedFilesForPaths(tempDir, wanted);
|
|
expect(loaded.size).toBe(2);
|
|
for (const filePath of files) {
|
|
const pf = loaded.get(filePath);
|
|
expect(pf).toBeDefined();
|
|
const channel = (pf as { cfgSideChannel?: unknown }).cfgSideChannel;
|
|
expect(Array.isArray(channel)).toBe(true);
|
|
const cfg = (channel as FunctionCfg[])[0];
|
|
// deep-equal: the interning reviver may dedup strings/objects but the
|
|
// VALUES must be intact — and no aliasing may merge the two files'
|
|
// distinct fact arrays into wrong shapes.
|
|
expect(cfg.bindings).toEqual(factCfg.bindings);
|
|
expect(cfg.blocks.map((b) => b.statements)).toEqual(factCfg.blocks.map((b) => b.statements));
|
|
}
|
|
});
|
|
|
|
it('facts survive even when two files share identical binding tables (reviver dedup safety)', async () => {
|
|
// The store reviver interns strings and dedups objects keyed on `nodeId`
|
|
// presence — BindingEntry/StatementFacts deliberately carry no such field,
|
|
// so dedup must never alias-then-mutate across files. Two files with
|
|
// byte-identical channels is the worst case.
|
|
const durableDir = getDurableParsedFileDir(tempDir);
|
|
const chunkHash = 'd'.repeat(64);
|
|
persistDurableParsedFileShardSync(durableDir, chunkHash, 7, 0, [
|
|
mkParsedFile('src/same1.ts'),
|
|
mkParsedFile('src/same2.ts'),
|
|
]);
|
|
const wanted = new Set(['src/same1.ts', 'src/same2.ts']);
|
|
expect(await durableChunkHasShards(tempDir, chunkHash, wanted)).toBe(true);
|
|
const loaded = await loadParsedFilesForPaths(tempDir, wanted);
|
|
const c1 = (loaded.get('src/same1.ts') as { cfgSideChannel?: FunctionCfg[] }).cfgSideChannel;
|
|
const c2 = (loaded.get('src/same2.ts') as { cfgSideChannel?: FunctionCfg[] }).cfgSideChannel;
|
|
expect(c1?.[0].bindings).toEqual(factCfg.bindings);
|
|
expect(c2?.[0].bindings).toEqual(factCfg.bindings);
|
|
});
|
|
});
|