diff --git a/MIGRATION.md b/MIGRATION.md index 22ef9d6c2..36a899840 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,5 +1,23 @@ # Migration Guide +## LadybugDB 0.22.0 storage and extensions + +LadybugDB 0.22.0 reads indexes written by 0.21.1, but a write/checkpoint upgrades +their storage format from version 47 to 49. No manual data conversion is needed. +Use the packaged FTS and VECTOR binaries shipped with the new core; older +extension binaries can load successfully and then crash when building an index. + +Before upgrading, stop index writers and readers and retain a backup of the +complete index storage directory if rollback is required. Include shared stores +when worktrees or sibling clones share an index. Keep clients sharing a writable +index on compatible engine versions. + +After an index is written by 0.22.0, downgrading the dependency alone is not +enough: 0.21.1 refuses storage version 49. Restore the backup, or rebuild with the +older engine using `gitnexus analyze --force`. A rebuild under an engine that +cannot read the upgraded index cannot recover its cached embeddings; use the +backup to preserve them, or regenerate them with `--embeddings`. + ## Automatic parser cache identities Parser caches now use artifact-derived base and PDG identities instead of diff --git a/gitnexus/bench/mcp-query/README.md b/gitnexus/bench/mcp-query/README.md index 81c5df126..51558f711 100644 --- a/gitnexus/bench/mcp-query/README.md +++ b/gitnexus/bench/mcp-query/README.md @@ -29,10 +29,13 @@ candidate. Without `--check`, `--sessions 1 --rounds 1` is a smoke run and canno establish a performance verdict. For an identical-code control, point both roots at the same build. Do not discard slow samples or rerun until a check turns green. -The candidate indexes one fixed generated repository containing 32 short and 32 -oversized TypeScript functions. Both servers read that same index. Index creation -is outside the timing window. Schemas must be compatible; an incompatible base -fails explicitly. Corpus generation and dependency installation use no model. +Both runtimes index the same fixed generated repository containing 32 short and +32 oversized TypeScript functions. Each server uses its own index and registry, +so engine upgrades can compare different native storage formats. Index creation +is outside the timing window. Both indexes must be complete and their responses +must pass the parity checks below. The report retains both indexes' metadata in +`indexes`; `index` remains the candidate metadata for existing report consumers. +Corpus generation and dependency installation use no model. Eight fresh server pairs each run a warmup cycle and five measured rounds. Request order alternates between baseline and candidate. Workloads cover small @@ -44,7 +47,9 @@ first query are recorded separately. Filesystem caches are not flushed. - Response identities, order and diagnostics must agree; timing, guidance and source representation are excluded from query equality. Indexed context source - must agree. At least ten known matches must survive each nonempty query. + must agree. Each staleness receipt timestamp must match its own index metadata; + only that timestamp is excluded from cross-runtime staleness equality. At least + ten known matches must survive each nonempty query. - Candidate query source cannot exceed 16,000 Unicode characters, cannot disappear entirely, and large previews must announce truncation. Context must retain the original indexed snippet (the indexer separately caps long function snippets). diff --git a/gitnexus/bench/mcp-query/measure.mjs b/gitnexus/bench/mcp-query/measure.mjs index e490650e0..29006d702 100644 --- a/gitnexus/bench/mcp-query/measure.mjs +++ b/gitnexus/bench/mcp-query/measure.mjs @@ -60,15 +60,12 @@ const runtimes = Object.fromEntries( }), ); const repo = path.join(out, 'repo'); -const index = path.join(out, 'index'); const env = { PATH: process.env.PATH, HOME: process.env.HOME, LANG: 'C.UTF-8', TZ: 'UTC', ...(process.env.LD_LIBRARY_PATH ? { LD_LIBRARY_PATH: process.env.LD_LIBRARY_PATH } : {}), - GITNEXUS_HOME: path.join(out, 'home'), - GITNEXUS_STORAGE_PATH: index, GITNEXUS_SHARED_STORE: 'off', GITNEXUS_MEMORY: 'off', GITNEXUS_CONTENT_RETENTION: 'full', @@ -76,6 +73,16 @@ const env = { GITNEXUS_MCP_READ_ONLY: '1', GITNEXUS_LBUG_EXTENSION_INSTALL: 'load-only', }; +const runtimeEnvs = Object.fromEntries( + ['baseline', 'candidate'].map((variant) => [ + variant, + { + ...env, + GITNEXUS_HOME: path.join(out, 'home', variant), + GITNEXUS_STORAGE_PATH: path.join(out, 'index', variant), + }, + ]), +); const queries = [ { name: 'small_source', symbol: 'lookupSmallSource', source: true }, { name: 'small_metadata', symbol: 'lookupSmallSource', source: false }, @@ -109,7 +116,8 @@ function canonical(value) { } function inspect(response, entry, variant) { - assert(!response.isError, `MCP error in ${entry.name}`); + if (response.isError) + assert.fail(`MCP error in ${variant}/${entry.name}: ${JSON.stringify(response)}`); if (entry.tool === 'tools/list') { for (const name of ['query', 'context']) assert( @@ -128,6 +136,12 @@ function inspect(response, entry, variant) { !body.error && !body.partial, `Incomplete ${variant}/${entry.name} response: ${JSON.stringify({ error: body.error, partial: body.partial, warning: body.warning })}`, ); + // Separate builds have different receipt timestamps. Validate each against its + // own index before excluding that timestamp from cross-runtime parity. + if (body.staleness) { + assert.equal(body.staleness.indexedAt, report.indexes[variant].indexedAt); + delete body.staleness.indexedAt; + } const rows = entry.tool === 'query' ? [...body.process_symbols, ...body.definitions] : []; if (entry.tool === 'query') { if (entry.name === 'empty') assert.equal(rows.length, 0); @@ -185,7 +199,7 @@ async function open(variant, clients) { command: process.execPath, args: [runtimes[variant].cli, 'mcp'], cwd: repo, - env, + env: runtimeEnvs[variant], stderr: 'pipe', }); const stderr = fs.createWriteStream(path.join(out, `${variant}.log`), { flags: 'a' }); @@ -288,22 +302,33 @@ try { execFileSync('git', args, { cwd: repo, env: gitEnv, stdio: 'pipe' }); } report.fixtureSha256 = hash(JSON.stringify(source)); + report.indexes = {}; const log = fs.openSync(path.join(out, 'index.log'), 'wx'); try { - execFileSync(process.execPath, [runtimes.candidate.cli, 'analyze', repo, '--index-only'], { - cwd: repo, - env, - stdio: ['ignore', log, log], - }); + // Native storage formats can differ across engine upgrades. Each runtime + // builds its own index from the same fixture; response parity is checked below. + for (const variant of ['baseline', 'candidate']) { + const runtimeEnv = runtimeEnvs[variant]; + fs.writeSync(log, `\n${variant} index (${runtimes[variant].sha})\n`); + execFileSync(process.execPath, [runtimes[variant].cli, 'analyze', repo, '--index-only'], { + cwd: repo, + env: runtimeEnv, + stdio: ['ignore', log, log], + }); + const metadata = JSON.parse( + fs.readFileSync(path.join(runtimeEnv.GITNEXUS_STORAGE_PATH, 'gitnexus.json'), 'utf8'), + ); + report.indexes[variant] = metadata; + assert( + metadata.stats.nodes >= 128 && metadata.stats.files === 64, + `Incomplete ${variant} fixture index`, + ); + assert.equal(metadata.stats.embeddings ?? 0, 0, 'Benchmark must not generate embeddings'); + } } finally { fs.closeSync(log); } - report.index = JSON.parse(fs.readFileSync(path.join(index, 'gitnexus.json'), 'utf8')); - assert( - report.index.stats.nodes >= 128 && report.index.stats.files === 64, - 'Incomplete fixture index', - ); - assert.equal(report.index.stats.embeddings ?? 0, 0, 'Benchmark must not generate embeddings'); + report.index = report.indexes.candidate; // Preserve the existing report field. for (let session = 0; session < sessions; session++) { const clients = {}; try { diff --git a/gitnexus/bench/mcp-query/statistics.mjs b/gitnexus/bench/mcp-query/statistics.mjs index 5abe46b83..eef6ffbb0 100644 --- a/gitnexus/bench/mcp-query/statistics.mjs +++ b/gitnexus/bench/mcp-query/statistics.mjs @@ -172,7 +172,7 @@ export function formatReport(report) { Base \`${report.runtimes.baseline.sha}\`; candidate \`${report.runtimes.candidate.sha}\`. -${report.sessions} fresh process pairs × ${report.rounds} rounds. Real MCP stdio, one shared keyword-search index, alternating requests. No model calls. +${report.sessions} fresh process pairs × ${report.rounds} rounds. Real MCP stdio, the same source fixture indexed by each runtime, alternating requests. No model calls. | Workload | Median ms, base → candidate | p95 ms, base → candidate | Paired change (95% interval) | Assessment | | --- | ---: | ---: | --- | --- | diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index ad1b66655..a71deaeca 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -23,7 +23,7 @@ "hasInstallScript": true, "license": "PolyForm-Noncommercial-1.0.0", "dependencies": { - "@ladybugdb/core": "0.21.1", + "@ladybugdb/core": "0.22.0", "@modelcontextprotocol/sdk": "^1.0.0", "@scarf/scarf": "^1.4.0", "busboy": "^1.6.0", @@ -1765,9 +1765,9 @@ } }, "node_modules/@ladybugdb/core": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@ladybugdb/core/-/core-0.21.1.tgz", - "integrity": "sha512-G0LWyn/dIqX9yHlyjpR12Rvou2o1dvCB9EkuzGa5ykCYbzur8EQhRUbi4fw3FlbckWvR6rK/tVbnFpBcRLwiHg==", + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@ladybugdb/core/-/core-0.22.0.tgz", + "integrity": "sha512-kEFtewqyRUk4rQNq2DA5xPgIyC2cOI8PhTixqz0sOhG6/BZ3hhoAP9RkYm0NkcAfCSH3SXs1varNGuccqPcapA==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1776,17 +1776,17 @@ "node-addon-api": "^6.0.0" }, "optionalDependencies": { - "@ladybugdb/core-darwin-arm64": "0.21.1", - "@ladybugdb/core-darwin-x64": "0.21.1", - "@ladybugdb/core-linux-arm64": "0.21.1", - "@ladybugdb/core-linux-x64": "0.21.1", - "@ladybugdb/core-win32-x64": "0.21.1" + "@ladybugdb/core-darwin-arm64": "0.22.0", + "@ladybugdb/core-darwin-x64": "0.22.0", + "@ladybugdb/core-linux-arm64": "0.22.0", + "@ladybugdb/core-linux-x64": "0.22.0", + "@ladybugdb/core-win32-x64": "0.22.0" } }, "node_modules/@ladybugdb/core-darwin-arm64": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-arm64/-/core-darwin-arm64-0.21.1.tgz", - "integrity": "sha512-TEFYNqBdbIojf1t29p3esgRhMDm56lD5eK91dwWFZJTVWAZoGXn+yLKs0NUQgj3lW2ZquuRTLZGglPhFEsDL1Q==", + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-arm64/-/core-darwin-arm64-0.22.0.tgz", + "integrity": "sha512-UE7UnM/Stb/wDyk/1Jl6BgZOl9KmyBr2EVfiASeLmRka09RKlSI6t5/cVu5j7v0qdJzvi/Csg0CP+lhWNNMurg==", "cpu": [ "arm64" ], @@ -1797,9 +1797,9 @@ ] }, "node_modules/@ladybugdb/core-darwin-x64": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-x64/-/core-darwin-x64-0.21.1.tgz", - "integrity": "sha512-w9p3oKrqeMOeWpX3xdCc/hQJHfrZtpB9We+Ir+0qyls68dSQoeXXR1GZYzzTElOtxr4OM5USwYbVyxADx+CMZg==", + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-x64/-/core-darwin-x64-0.22.0.tgz", + "integrity": "sha512-VM4yiEVTNQel4Dz5cO9Rmc/NBiBWIsBxwMnD3MfSl9gZGGSoLTHa9JDD1RcDJoQ2tZvvXtQMjw5q6E7tnYYyAw==", "cpu": [ "x64" ], @@ -1810,9 +1810,9 @@ ] }, "node_modules/@ladybugdb/core-linux-arm64": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-arm64/-/core-linux-arm64-0.21.1.tgz", - "integrity": "sha512-TodWmzOHxmmKvXXo9buBXGeyolxf/NqBUbo/LcR/tuMViKhGH8TBzIXi8DfVVwmsJ+CQCU065TvXgv2JoHAlZg==", + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-arm64/-/core-linux-arm64-0.22.0.tgz", + "integrity": "sha512-54K+vxXxQkpxnY6v6nBTPdSY4g9OEoKjXt2PxzRLnoNyZ1OMERxWiwaFLr4jAc5HHgpA3jHLIAvkaY2RSnDaWA==", "cpu": [ "arm64" ], @@ -1823,9 +1823,9 @@ ] }, "node_modules/@ladybugdb/core-linux-x64": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-x64/-/core-linux-x64-0.21.1.tgz", - "integrity": "sha512-hCcLYv8ds0x4fkTaRkuc4f75KLdH9e0EV51ne6uBNdQSY7kyc0RgSoUG3utUGfFDMMkfgIJAeIxQY4ck9jQiXg==", + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-x64/-/core-linux-x64-0.22.0.tgz", + "integrity": "sha512-ZUbkNmRTjMb16XBPJSCkdNCW4up30jHpDzn9fn6qifg50XnkUQ3eje200EyrdIjd/LLYi9d/VG72mMRW7+cQTA==", "cpu": [ "x64" ], @@ -1836,9 +1836,9 @@ ] }, "node_modules/@ladybugdb/core-win32-x64": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@ladybugdb/core-win32-x64/-/core-win32-x64-0.21.1.tgz", - "integrity": "sha512-nBS1XSRJfpexdlhkezmEiRtkUt2qEDp4zBjIhH+oxgRdD7UsRz1Jyit8foJuZAmUsoQGukN5LO+YeyDU2WZTyA==", + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@ladybugdb/core-win32-x64/-/core-win32-x64-0.22.0.tgz", + "integrity": "sha512-A7XW4cQYXQjfizF+yook55fSppdF07vUzwhi9d3ER1Z1CnkrzmECAF5DIwaDQ+jFAgwUCCEcJ7kQ9HrnYtZ6Zg==", "cpu": [ "x64" ], diff --git a/gitnexus/package.json b/gitnexus/package.json index babdca4d8..28f278431 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -81,7 +81,7 @@ "version": "node scripts/sync-plugin-manifests.mjs" }, "dependencies": { - "@ladybugdb/core": "0.21.1", + "@ladybugdb/core": "0.22.0", "@modelcontextprotocol/sdk": "^1.0.0", "@scarf/scarf": "^1.4.0", "busboy": "^1.6.0", diff --git a/gitnexus/scripts/cross-platform-tests.ts b/gitnexus/scripts/cross-platform-tests.ts index 4b71be9b9..90d68b674 100644 --- a/gitnexus/scripts/cross-platform-tests.ts +++ b/gitnexus/scripts/cross-platform-tests.ts @@ -209,6 +209,7 @@ const LBUG_NATIVE = [ 'test/integration/lbug-orphan-sidecar-recovery.test.ts', 'test/integration/lbug-interrupted-checkpoint-recovery.test.ts', 'test/integration/lbug-checkpoint-accounting.test.ts', + 'test/integration/lbug-rel-copy-page-padding.test.ts', 'test/integration/lbug-incremental-search.test.ts', // Retained identities and reader metadata must agree on every native platform. 'test/integration/incremental-method-integrity.test.ts', diff --git a/gitnexus/scripts/repro-3527.mjs b/gitnexus/scripts/repro-3527.mjs new file mode 100644 index 000000000..39b547bf5 --- /dev/null +++ b/gitnexus/scripts/repro-3527.mjs @@ -0,0 +1,264 @@ +/** + * Generated, non-confidential #3527 probe. Keeps its indexes and raw responses. + * Usage: node scripts/repro-3527.mjs + * Exit 1 means an integrity/coverage failure; exit 2 means the probe could not run. + * This does not claim to reproduce the original private macOS checkout. + */ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; + +const [runtimeArg, outputArg] = process.argv.slice(2); +assert(runtimeArg && outputArg, 'Supply a built GitNexus package and a NEW output directory'); +const runtime = fs.realpathSync(runtimeArg); +const output = path.resolve(outputArg); +fs.mkdirSync(output); +const primary = path.join(output, 'primary'); +const repo = path.join(output, 'worktree'); +fs.mkdirSync(primary); +const requireRuntime = createRequire(path.join(runtime, 'package.json')); +const lbug = requireRuntime('@ladybugdb/core'); +const env = { + PATH: process.env.PATH, + HOME: process.env.HOME, + LANG: 'C.UTF-8', + TZ: 'UTC', + GIT_CONFIG_GLOBAL: '/dev/null', + GIT_CONFIG_NOSYSTEM: '1', + GIT_AUTHOR_NAME: 'fixture', + GIT_AUTHOR_EMAIL: 'fixture@example.com', + GIT_COMMITTER_NAME: 'fixture', + GIT_COMMITTER_EMAIL: 'fixture@example.com', + GITNEXUS_HOME: path.join(output, 'home'), + GITNEXUS_MEMORY: 'off', + NODE_OPTIONS: '--max-old-space-size=2048', + GITNEXUS_LBUG_BUFFER_POOL_SIZE: String(512 * 1024 ** 2), + GITNEXUS_WORKER_POOL_SIZE: '1', + GITNEXUS_PARSE_CHUNK_CONCURRENCY: '1', + GITNEXUS_LBUG_EXTENSION_INSTALL: 'load-only', +}; +const expected = {}; +const report = { + runtime, + node: process.version, + engine: lbug.VERSION, + platform: `${process.platform}-${process.arch}`, + expectedMethods: 4480, + expectedStructs: 4480, + queries: [], + native: [], + crossLanguageMethodPaths: 0, + nulStructNames: 0, + witnesses: [], +}; +const save = (name, value) => + fs.writeFileSync(path.join(output, name), JSON.stringify(value, null, 2) + '\n'); +const git = (args, cwd = primary) => execFileSync('git', args, { cwd, env, stdio: 'pipe' }); +function cli(args, name) { + const stderr = fs.openSync(path.join(output, name + '.stderr.txt'), 'w'); + try { + const text = execFileSync( + process.execPath, + [path.join(runtime, 'dist/cli/index.js'), ...args], + { + cwd: repo, + env, + encoding: 'utf8', + stdio: ['ignore', 'pipe', stderr], + maxBuffer: 32 * 1024 ** 2, + timeout: 300_000, + }, + ); + fs.writeFileSync(path.join(output, name + '.txt'), text); + return text; + } finally { + fs.closeSync(stderr); + } +} +function source(kind, batch, edit = false) { + const python = kind === 'python'; + const file = python + ? `checks/test_integrity_batch_${batch}.py` + : `Sources/Features/Component${batch}/IntegritySentinels_${batch}.swift`; + const lines = python ? [`class IntegrityBatch${batch}:`] : []; + for (let i = 0; i < 64; i++) { + const name = python + ? `test_mixed_integrity_${batch}_${i}` + : `MixedIntegritySentinel${batch}_${i}`; + const startLine = lines.length + 1; + const marker = `mixed integrity sentinel ${batch}_${i}${edit && i === 0 ? ' changed' : ''}`; + const block = python + ? [` def ${name}(self):`, ` return "${marker}"`] + : [`struct ${name} {`, ` let marker = "${marker}"`, '}']; + const id = python + ? `Method:${file}:IntegrityBatch${batch}.${name}#0` + : `Struct:${file}:${name}`; + expected[id] = { id, name, filePath: file, startLine, endLine: startLine + block.length - 1 }; + lines.push(...block, ''); + } + const root = fs.existsSync(repo) ? repo : primary; + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), lines.join('\n')); + expected['File:' + file] = { id: 'File:' + file, name: path.basename(file), filePath: file }; +} +function mismatches(rows) { + return rows.filter( + (row) => + !expected[row.id] || + Object.keys(expected[row.id]).some((k) => row[k] !== expected[row.id][k]), + ); +} +async function nativeChecks() { + const entry = JSON.parse(fs.readFileSync(path.join(output, 'home/registry.json'))).find( + (entry) => fs.realpathSync(entry.path) === fs.realpathSync(repo), + ); + assert(entry, 'The exact worktree must be registered'); + const meta = JSON.parse(fs.readFileSync(path.join(entry.storagePath, 'gitnexus.json'))); + const dbPath = meta.graphPath || path.join(entry.storagePath, 'lbug'); + report.dbPath = dbPath; + report.lastCommit = meta.lastCommit; + assert.equal(meta.lastCommit, git(['rev-parse', 'HEAD'], repo).toString().trim()); + const db = new lbug.Database(dbPath, 512 * 1024 ** 2, false, true); + const conn = new lbug.Connection(db); + const fields = + 'n.id AS id,n.name AS name,n.filePath AS filePath,n.startLine+1 AS startLine,n.endLine+1 AS endLine'; + async function read(query, params) { + const result = params + ? await conn.execute(await conn.prepare(query), params) + : await conn.query(query); + try { + return await result.getAll(); + } finally { + await result.close(); + } + } + async function check(rows, ids, stage) { + const bad = mismatches(rows); + const found = new Set(rows.map((row) => row.id)); + const missing = ids.filter((id) => !found.has(id)); + const duplicates = rows.length - found.size; + report.crossLanguageMethodPaths += bad.filter( + (row) => row.id?.startsWith('Method:') && row.filePath?.includes('.swift'), + ).length; + report.nulStructNames += bad.filter( + (row) => row.id?.startsWith('Struct:') && row.name?.includes('\0'), + ).length; + report.native.push({ + stage, + rows: rows.length, + bad: bad.length, + missing: missing.length, + duplicates, + }); + if ((bad.length || missing.length || duplicates) && report.witnesses.length < 8) { + const uid = missing[0] || bad.find((row) => expected[row.id])?.id; + const primaryKey = uid + ? await read(`MATCH(n:${uid.split(':')[0]} {id:$uid}) RETURN ${fields}`, { uid }) + : []; + report.witnesses.push({ + stage, + bad: bad.slice(0, 3), + missing: missing.slice(0, 3), + uid, + primaryKey, + }); + } + } + try { + const methods = Object.keys(expected).filter((id) => id.startsWith('Method:')); + const structs = Object.keys(expected).filter((id) => id.startsWith('Struct:')); + for (const [table, ids] of [ + ['Method', methods], + ['Struct', structs], + ]) { + await check(await read(`MATCH(n:${table}) RETURN ${fields}`), ids, table + '-all'); + } + const statement = await conn.prepare( + `MATCH(n) WHERE n.id IN $ids RETURN ${fields} ORDER BY startLine,id`, + ); + for (let i = 0; i < 150; i++) { + const ids = [methods[(i * 29) % methods.length], structs[(i * 67) % structs.length]]; + if (i % 3) ids.splice(i % 2, 1); + const result = await conn.execute(statement, { ids }); + let rows; + try { + rows = await result.getAll(); + } finally { + await result.close(); + } + await check(rows, ids, 'mixed-' + i); + } + } finally { + await conn.close(); + await db.close(); + } +} +try { + for (let i = 0; i < 70; i++) for (const kind of ['python', 'swift']) source(kind, i); + fs.writeFileSync(path.join(primary, '.gitignore'), '.gitnexus/\n'); + git(['init', '-b', 'main']); + git(['add', '.']); + git(['-c', 'commit.gpgsign=false', 'commit', '-m', 'mixed language fixture']); + git(['worktree', 'add', '-b', 'verification', repo]); + cli(['analyze', '--index-only', '.'], 'baseline-analyze'); + for (let round = 0; round < 3; round++) { + source('python', round, true); // Leave every Swift file untouched. + git(['add', '.'], repo); + git(['-c', 'commit.gpgsign=false', 'commit', '-m', `incremental edit ${round}`], repo); + const name = `round-${round}-analyze`; + const analyze = cli(['analyze', '--index-only', '.'], name); + assert( + !/switching to a full DB write|escalated/i.test( + analyze + fs.readFileSync(path.join(output, name + '.stderr.txt')), + ), + 'Probe requires selective writeback', + ); + cli(['status'], `round-${round}-status`); + const terms = [ + 'mixed integrity sentinel', + 'test_mixed_integrity', + 'MixedIntegritySentinel', + 'integrity batch', + ]; + for (const [i, term] of terms.entries()) { + const text = cli(['query', term, '--repo', repo], `round-${round}-query-${i}`); + const result = JSON.parse(text.slice(text.indexOf('{'))); + const rows = [...(result.process_symbols ?? []), ...(result.definitions ?? [])]; + const relevant = rows.filter((row) => /^(Method|Struct|File):/.test(row.id)); + report.queries.push({ + round, + term, + error: result.error, + partial: result.partial, + rows: rows.length, + methods: rows.filter((r) => r.id?.startsWith('Method:')).length, + structs: rows.filter((r) => r.id?.startsWith('Struct:')).length, + bad: mismatches(relevant), + }); + } + save('report.json', report); + } + save('expected.json', expected); + await nativeChecks(); + report.passed = + report.queries.every((q) => !q.error && !q.partial && q.rows > 0 && !q.bad.length) && + report.queries.some((q) => q.methods > 0) && + report.queries.some((q) => q.structs > 0) && + report.native.every((q) => !q.bad && !q.missing && !q.duplicates); + save('report.json', report); + console.log( + JSON.stringify({ + engine: report.engine, + passed: report.passed, + report: path.join(output, 'report.json'), + }), + ); + process.exitCode = report.passed ? 0 : 1; +} catch (error) { + report.probeError = String(error); + save('report.json', report); + console.error(error); + process.exitCode = 2; +} diff --git a/gitnexus/src/core/lbug/query-result-integrity.ts b/gitnexus/src/core/lbug/query-result-integrity.ts index e54e3c293..aa9019647 100644 --- a/gitnexus/src/core/lbug/query-result-integrity.ts +++ b/gitnexus/src/core/lbug/query-result-integrity.ts @@ -1,3 +1,6 @@ +import { getLanguageFromFilename } from 'gitnexus-shared'; +import { SYMBOL_NODE_LABELS } from '../ingestion/utils/symbol-labels.js'; + /** Shared integrity boundary for identities returned by graph and search queries. */ export const SYMBOL_IDENTITY_RECOVERY_SUGGESTION = 'Run gitnexus analyze --force from the affected repository root to rebuild the index.'; @@ -41,6 +44,28 @@ export function assertIdentityFields(...values: unknown[]): void { } } +/** Reject mixed rows even when both the UID and the unrelated path are printable. */ +export function assertSymbolFilePath(id: unknown, filePath: unknown): void { + // Older indexes may omit paths or use opaque IDs without a source-path component. + if (typeof id !== 'string' || typeof filePath !== 'string' || !filePath) return; + const separator = id.indexOf(':'); + if (separator < 0) return; + const label = id.slice(0, separator); + const sourceLabels: ReadonlySet = SYMBOL_NODE_LABELS; + if (label === 'File' || label === 'Folder') { + if (id !== `${label}:${filePath}`) throw new SymbolIdentityError(); + } else if (sourceLabels.has(label) && id.indexOf(':', separator + 1) >= 0) { + // A node label does not define its ID format. Providers also emit semantic + // IDs (objc:class:..., spring-config:..., etc.) without a leading file path. + // Only compare paths when that leading component identifies a source file; + // opaque IDs and ambiguous colon-containing paths cannot be decoded here. + const leadingComponent = id.slice(separator + 1, id.indexOf(':', separator + 1)); + if (!getLanguageFromFilename(leadingComponent)) return; + // Match against the supplied path rather than splitting it: filenames can contain colons. + if (!id.startsWith(`${label}:${filePath}:`)) throw new SymbolIdentityError(); + } +} + export function assertQueryIdentity( row: unknown, idKey: string, diff --git a/gitnexus/src/core/lbug/sidecar-recovery.ts b/gitnexus/src/core/lbug/sidecar-recovery.ts index 84b8f684c..ab29ae9a8 100644 --- a/gitnexus/src/core/lbug/sidecar-recovery.ts +++ b/gitnexus/src/core/lbug/sidecar-recovery.ts @@ -241,11 +241,13 @@ export const isReadOnlyShadowReplayError = (err: unknown): boolean => { // SIGKILLing a writer mid-CHECKPOINT (homelab 2026-09-19, GitNexus image // 1.6.10-20260917, built on 0.19.x). The 0.18.3 binary does NOT contain it // (checked via `strings`), and 0.18.3 tolerates the same on-disk state, so on -// 0.18.x this classifier simply never fires. If the engine pin ever moves -// back to ^0.19, the read-path self-heal is already in place. +// 0.18.x this classifier simply never fires. In 0.22.0 the same refusal is +// reported as "checkpoint recovery is pending". export const isReadOnlyCheckpointInProgressError = (err: unknown): boolean => { const msg = err instanceof Error ? err.message : String(err); - return /cannot open database in read-only mode while checkpoint is in progress/i.test(msg); + return /cannot open database in read-only mode while checkpoint (?:is in progress|recovery is pending)/i.test( + msg, + ); }; /** Prefix of the interrupted-checkpoint wrap — must not rematch the native classifier. */ diff --git a/gitnexus/src/core/run-analyze.ts b/gitnexus/src/core/run-analyze.ts index 4e6dd278f..bec58c4b6 100644 --- a/gitnexus/src/core/run-analyze.ts +++ b/gitnexus/src/core/run-analyze.ts @@ -1556,6 +1556,8 @@ export async function runFullAnalysis( else if (options.shareWith) await setShareOptOut(repoPath, false); return result; } finally { + // Readers opened after analyze must not inherit its smaller write budget. + setBufferPoolSizeHint(undefined); await removeColdParseRebuildDir(parseRun.storagePath, true); discardScopedEmbeddingSpills(); lock.release(); diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 56a732438..6b8d1d48e 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -38,6 +38,7 @@ import { SYMBOL_IDENTITY_RECOVERY_SUGGESTION, SymbolIdentityError, assertSymbolIdentity, + assertSymbolFilePath, queryRowValue, assertIdentityFields, assertQueryIdentity, @@ -395,6 +396,8 @@ function assertResponseIdentities(value: unknown): void { if (Array.isArray(value)) { for (const item of value) assertResponseIdentities(item); } else if (value !== null && typeof value === 'object') { + const row = value as Record; + assertSymbolFilePath(row.id ?? row.uid, row.filePath); for (const [key, field] of Object.entries(value)) { if (key === 'seedBlocks' || key === 'reachableBlocks' || key === 'intraReachableBlocks') { if (!Array.isArray(field)) throw new SymbolIdentityError(); diff --git a/gitnexus/test/integration/lbug-delete-nodes-for-files.test.ts b/gitnexus/test/integration/lbug-delete-nodes-for-files.test.ts index 5686ace62..dffed4eda 100644 --- a/gitnexus/test/integration/lbug-delete-nodes-for-files.test.ts +++ b/gitnexus/test/integration/lbug-delete-nodes-for-files.test.ts @@ -566,3 +566,82 @@ withTestLbugDB('embedding-row-dml-vector-gate', (handle) => { }, 120_000); }); }); + +/** + * Delete-all-then-recopy of a table that spans more than one native vector. + * + * @ladybugdb/core 0.21.0 and 0.21.1 returned every row of such a table twice + * once the incremental writeback had DETACH DELETEd it and a COPY into an + * earlier table had run: `NodeGroup::scan` let a morsel that ended before the + * group's first live row wrap an unsigned subtraction and rescan to the end of + * the group (LadybugDB #1085, fixed in 0.21.2). No primary-key error fires, so + * the only symptom was `Graph identity reconciliation failed … duplicate ID` + * on repos with more than 2048 symbols of one label (Swift structs in the + * report that found it), and 2×-inflated counts for anything that scanned. + * + * The statement order below is the one `runFullAnalysisInner` issues: delete the + * write set's rows, then COPY File before the symbol tables. + */ +withTestLbugDB('delete-recopy-multi-vector-table', (handle) => { + describe('deleteNodesForFiles then loadGraphToLbug on a multi-vector table', () => { + // One native vector holds 2048 rows; 2049 is the smallest table that spans two. + const STRUCT_COUNT = 2049; + const STRUCTS_PER_FILE = 64; + const fileOf = (i: number): string => `Sources/S${Math.floor(i / STRUCTS_PER_FILE)}.swift`; + + const buildStructGraph = () => { + const nodes: TestNodeInput[] = []; + const rels: TestRelInput[] = []; + const files = new Set(); + for (let i = 0; i < STRUCT_COUNT; i++) { + const fp = fileOf(i); + if (!files.has(fp)) { + files.add(fp); + nodes.push({ id: `File:${fp}`, label: 'File', name: path.basename(fp), filePath: fp }); + } + const id = `Struct:${fp}:S${i}`; + nodes.push({ + id, + label: 'Struct', + name: `S${i}`, + filePath: fp, + startLine: 1, + endLine: 3, + }); + rels.push({ sourceId: `File:${fp}`, targetId: id, type: 'CONTAINS' }); + } + return { graph: buildTestGraph(nodes, rels), filePaths: [...files] }; + }; + + it('returns each struct and its edge exactly once after the rows were deleted and re-copied', async () => { + const { loadGraphToLbug, deleteNodesForFiles, executeQuery } = + await import('../../src/core/lbug/lbug-adapter.js'); + const storagePath = path.dirname(handle.dbPath); + const structIds = async (): Promise => + ((await executeQuery('MATCH (n:Struct) RETURN n.id AS id')) as Array<{ id: string }>).map( + (r) => String(r.id), + ); + const containsEdges = async (): Promise => { + const rows = (await executeQuery( + `MATCH (:File)-[r:CodeRelation]->(:Struct) WHERE r.type = 'CONTAINS' RETURN count(r) AS c`, + )) as Array<{ c: number | bigint }>; + return Number(rows[0]?.c ?? 0); + }; + + const { graph, filePaths } = buildStructGraph(); + await loadGraphToLbug(graph, '/tmp/repo', storagePath); + expect(new Set(await structIds()).size).toBe(STRUCT_COUNT); + + // Every file is in the write set, as when a Swift edit pulls in all importers. + await deleteNodesForFiles(filePaths); + expect(await structIds()).toEqual([]); + + await loadGraphToLbug(graph, '/tmp/repo', storagePath); + + const ids = await structIds(); + expect(ids).toHaveLength(STRUCT_COUNT); + expect(new Set(ids).size).toBe(STRUCT_COUNT); + expect(await containsEdges()).toBe(STRUCT_COUNT); + }, 120_000); + }); +}); diff --git a/gitnexus/test/integration/lbug-interrupted-checkpoint-recovery.test.ts b/gitnexus/test/integration/lbug-interrupted-checkpoint-recovery.test.ts index 287f2333b..faefec26b 100644 --- a/gitnexus/test/integration/lbug-interrupted-checkpoint-recovery.test.ts +++ b/gitnexus/test/integration/lbug-interrupted-checkpoint-recovery.test.ts @@ -202,7 +202,7 @@ describe('interrupted-checkpoint recovery (pooled read path self-heal)', () => { await probe.close().catch(() => {}); } })(), - ).rejects.toThrow(/checkpoint is in progress/i); + ).rejects.toThrow(/checkpoint (?:is in progress|recovery is pending)/i); // The wiki path: pooled READ-ONLY open. Before the fix this refused with // "Cannot open database in read-only mode while checkpoint is in diff --git a/gitnexus/test/integration/lbug-rel-copy-page-padding.test.ts b/gitnexus/test/integration/lbug-rel-copy-page-padding.test.ts new file mode 100644 index 000000000..1aeea7684 --- /dev/null +++ b/gitnexus/test/integration/lbug-rel-copy-page-padding.test.ts @@ -0,0 +1,73 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import lbug from '@ladybugdb/core'; +import { expect, it } from 'vitest'; +import { createTempDir } from '../helpers/test-db.js'; + +// LadybugDB #1059/#1091: uncompressed relationship COPY allocated whole pages +// but wrote only the payload. On 0.21.1 this fixture leaves the file 3,936 bytes +// short; an immediate scan can still pass while the pages remain buffered. +it('pads uncheckpointed relationship COPY pages and preserves their rows', async () => { + const temp = await createTempDir(); + const dbPath = path.join(temp.dbPath, 'lbug'); + const db = new lbug.Database(dbPath, 64 * 1024 * 1024, false, false, 1024 ** 3, false); + const conn = new lbug.Connection(db); + const query = async (cypher: string): Promise>> => { + const raw = await conn.query(cypher); + const rows: Array> = []; + for (const result of Array.isArray(raw) ? raw : [raw]) { + try { + rows.push(...(await result.getAll())); + } finally { + result.close(); + } + } + return rows; + }; + + try { + await query('CALL force_checkpoint_on_close=false'); + await query('CREATE NODE TABLE N(id INT64 PRIMARY KEY)'); + await query('CREATE REL TABLE R(FROM N TO N, weight INT64)'); + await query('UNWIND range(0, 999) AS i CREATE (:N {id: i})'); + await query('CHECKPOINT'); + + const expected = Array.from({ length: 1000 }, (_, i) => ({ + source: i, + target: (i + 1) % 1000, + weight: i, + })); + const csvPath = path.join(temp.dbPath, 'rels.csv'); + await fs.writeFile( + csvPath, + expected.map(({ source, target, weight }) => `${source},${target},${weight}\n`).join(''), + ); + const csvLiteral = csvPath.replace(/\\/g, '/').replace(/'/g, "\\'"); + await query(`COPY R FROM '${csvLiteral}' (HEADER=false, PARALLEL=false)`); + + // Check before a scan or checkpoint can conceal the short native flush. + expect((await fs.stat(dbPath)).size % 4096).toBe(0); + const scan = `MATCH (a:N)-[r:R]->(b:N) + RETURN a.id AS source, b.id AS target, r.weight AS weight ORDER BY weight`; + expect(await query(scan)).toEqual(expected); + + await query( + 'MATCH (a:N), (b:N) WHERE a.id = 0 AND b.id = 500 CREATE (a)-[:R {weight: 1000}]->(b)', + ); + expected.push({ source: 0, target: 500, weight: 1000 }); + expect(await query(scan)).toEqual(expected); + await query('CHECKPOINT'); + expect((await fs.stat(dbPath)).size % 4096).toBe(0); + expect(await query(scan)).toEqual(expected); + } finally { + try { + await conn.close(); + } finally { + try { + await db.close(); + } finally { + await temp.cleanup(); + } + } + } +}, 120_000); diff --git a/gitnexus/test/integration/objective-c-provider.test.ts b/gitnexus/test/integration/objective-c-provider.test.ts index b060636fb..cde7a3f95 100644 --- a/gitnexus/test/integration/objective-c-provider.test.ts +++ b/gitnexus/test/integration/objective-c-provider.test.ts @@ -7,6 +7,7 @@ import { fileURLToPath } from 'url'; import type { GraphNode, RelationshipType } from 'gitnexus-shared'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; import { runFullAnalysis } from '../../src/core/run-analyze.js'; +import { getEffectiveBufferPoolSize } from '../../src/core/lbug/lbug-config.js'; import { LocalBackend } from '../../src/mcp/local/local-backend.js'; import { classifyObjectiveCFileContent } from '../../src/core/ingestion/languages/objective-c.js'; import type { PipelineResult } from '../../src/types/pipeline.js'; @@ -213,6 +214,7 @@ async function analyzeObjectiveCRepo( options: { force?: boolean } = {}, ): Promise { const logs: string[] = []; + const readerBudget = getEffectiveBufferPoolSize(); await runFullAnalysis( repoRoot, { @@ -226,6 +228,7 @@ async function analyzeObjectiveCRepo( onLog: (message) => logs.push(message), }, ); + expect(getEffectiveBufferPoolSize()).toBe(readerBudget); return logs; } diff --git a/gitnexus/test/integration/query-symbol-integrity.test.ts b/gitnexus/test/integration/query-symbol-integrity.test.ts index d22f07c5a..03e7285c1 100644 --- a/gitnexus/test/integration/query-symbol-integrity.test.ts +++ b/gitnexus/test/integration/query-symbol-integrity.test.ts @@ -62,6 +62,14 @@ describe('query identity boundary through the real MCP server', () => { await fs.mkdir(path.join(repo, 'src'), { recursive: true }); await fs.writeFile(path.join(repo, '.gitignore'), '.gitnexus/\n'); await fs.writeFile(path.join(repo, 'src/fixture.ts'), SOURCE); + await fs.writeFile( + path.join(repo, 'src/fixture.py'), + 'class Fixture:\n def integrityNeedle(self):\n return 42\n', + ); + await fs.writeFile( + path.join(repo, 'src/fixture.swift'), + 'struct IntegrityNeedle {\n let value = 42\n}\n', + ); initGitRepo(repo); execFileSync('git', ['config', 'commit.gpgsign', 'false'], { cwd: repo }); commitAll(repo, 'source-derived query fixture'); @@ -148,7 +156,7 @@ describe('query identity boundary through the real MCP server', () => { 'bounds source previews in native queries without losing matches (%#)', async (source) => { const setSource = async () => { - for (const label of ['Function', 'File']) { + for (const label of ['Function', 'Class', 'Method', 'Struct', 'File']) { await adapter.executePrepared(`MATCH (n:${label}) SET n.content = $source`, { source }); } }; @@ -188,6 +196,11 @@ describe('query identity boundary through the real MCP server', () => { ); it.each([ + [ + 'path inconsistent with the symbol UID', + 'MATCH (n:Function {id: $uid}) SET n.filePath = $value', + 'Sources/Unrelated.swift', + ], ['NUL symbol name', 'MATCH (n:Function {id: $uid}) SET n.name = $value', 'invalid\0name'], ['NUL symbol path', 'MATCH (n:Function {id: $uid}) SET n.filePath = $value', 'invalid\0path'], ['NUL process label', 'MATCH (p:Process) SET p.label = $value', 'invalid\0process'], @@ -218,6 +231,35 @@ describe('query identity boundary through the real MCP server', () => { expect(result).not.toHaveProperty('definitions'); }); + it.each([ + { + table: 'Method', + uid: 'Method:src/fixture.py:Fixture.integrityNeedle#0', + field: 'filePath', + value: 'src/fixture.swift', + }, + { + table: 'Struct', + uid: 'Struct:src/fixture.swift:IntegrityNeedle', + field: 'name', + value: '\0\0\0\0', + }, + ])('rejects the injected #3527 $table/$field shape', async ({ table, uid, field, value }) => { + const result = await query(async () => { + // Prove the source-derived fixture contains the target before injecting the fault. + expect( + await adapter.executePrepared(`MATCH (n:${table} {id: $uid}) RETURN n.id AS id`, { uid }), + ).toEqual([{ id: uid }]); + await adapter.executePrepared(`MATCH (n:${table} {id: $uid}) SET n.${field} = $value`, { + uid, + value, + }); + }); + expect(result.error).toContain('invalid symbol identity'); + expect(result.recoverySuggestion).toBe(SYMBOL_IDENTITY_RECOVERY_SUGGESTION); + expect(result).not.toHaveProperty('definitions'); + }); + it('accepts legacy missing process entry points and empty optional labels', async () => { const result = await query(async () => { await adapter.executeQuery( diff --git a/gitnexus/test/unit/sidecar-recovery.test.ts b/gitnexus/test/unit/sidecar-recovery.test.ts index 3853ffaf0..c70aaa493 100644 --- a/gitnexus/test/unit/sidecar-recovery.test.ts +++ b/gitnexus/test/unit/sidecar-recovery.test.ts @@ -33,6 +33,10 @@ import { } from '../../src/core/lbug/sidecar-recovery.js'; const logger = () => ({ warn: vi.fn(), info: vi.fn(), debug: vi.fn() }); +const CHECKPOINT_RECOVERY_PENDING = + 'Runtime exception: Cannot open database in read-only mode while checkpoint recovery is pending. ' + + 'If another process is checkpointing, retry after it finishes; otherwise reopen in read-write mode ' + + 'to complete recovery before reopening read-only.'; describe('LadybugDB sidecar recovery', () => { let dir: string; @@ -255,6 +259,13 @@ describe('LadybugDB sidecar recovery', () => { expect(isReadOnlyCheckpointInProgressError(new Error(CANONICAL))).toBe(true); }); + it('matches the 0.22 checkpoint-recovery refusal', () => { + expect(isReadOnlyCheckpointInProgressError(new Error(CHECKPOINT_RECOVERY_PENDING))).toBe( + true, + ); + expect(isReadOnlyCheckpointInProgressError(CHECKPOINT_RECOVERY_PENDING)).toBe(true); + }); + it('matches prefix-wrapped and case-variant forms', () => { expect( isReadOnlyCheckpointInProgressError( @@ -328,16 +339,19 @@ describe('LadybugDB sidecar recovery', () => { const SHADOW = "Runtime exception: Couldn't replay shadow pages under read-only mode. Please re-open the database with read-write mode to replay shadow pages."; - it('wraps checkpoint refusal without rematching the native classifier', () => { - const wrapped = readOnlyRecoveryFailureMessage(dbPath, new Error(CANONICAL)); - expect(wrapped.startsWith(INTERRUPTED_CHECKPOINT_RECOVERY_PREFIX)).toBe(true); - expect(wrapped).toMatch(/gitnexus analyze/); - expect(wrapped).not.toMatch(/sidecar is missing/i); - expect(wrapped).not.toMatch(/--force/); - expect(wrapped).not.toContain(CANONICAL); - expect(isReadOnlyCheckpointInProgressError(new Error(wrapped))).toBe(false); - expect(isReadOnlyRecoveryFailure(new Error(wrapped))).toBe(true); - }); + it.each([CANONICAL, CHECKPOINT_RECOVERY_PENDING])( + 'wraps checkpoint refusal without rematching the native classifier (%#)', + (refusal) => { + const wrapped = readOnlyRecoveryFailureMessage(dbPath, new Error(refusal)); + expect(wrapped.startsWith(INTERRUPTED_CHECKPOINT_RECOVERY_PREFIX)).toBe(true); + expect(wrapped).toMatch(/gitnexus analyze/); + expect(wrapped).not.toMatch(/sidecar is missing/i); + expect(wrapped).not.toMatch(/--force/); + expect(wrapped).not.toContain(refusal); + expect(isReadOnlyCheckpointInProgressError(new Error(wrapped))).toBe(false); + expect(isReadOnlyRecoveryFailure(new Error(wrapped))).toBe(true); + }, + ); it('wraps shadow-replay refusal without rematching the native classifier', () => { const wrapped = readOnlyRecoveryFailureMessage(dbPath, new Error(SHADOW)); diff --git a/gitnexus/test/unit/symbol-filepath-integrity.test.ts b/gitnexus/test/unit/symbol-filepath-integrity.test.ts new file mode 100644 index 000000000..ca75730eb --- /dev/null +++ b/gitnexus/test/unit/symbol-filepath-integrity.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { + assertSymbolFilePath, + SymbolIdentityError, +} from '../../src/core/lbug/query-result-integrity.js'; + +describe('source UID/path consistency (#3527)', () => { + it.each([ + ['Method:checks/test_batch.py:Batch.test_run#0', 'Sources/Unrelated.swift'], + ['Struct:Sources/Widget.swift:Widget', 'Sources/Other.swift'], + ['File:src/actual.ts', 'src/unrelated.ts'], + ['Folder:src/actual', 'src/unrelated'], + ['Function:src/actual.ts:run', 'src/actual'], + ])('rejects a printable mixed tuple: %s', (id, filePath) => { + expect(() => assertSymbolFilePath(id, filePath)).toThrow(SymbolIdentityError); + }); + + it.each([ + ['Method:checks/test_batch.py:Batch.test_run#0', 'checks/test_batch.py'], + ['Struct:Sources/Widget.swift:Widget', 'Sources/Widget.swift'], + ['File:src/part:one.ts', 'src/part:one.ts'], + ['Folder:src/part:one', 'src/part:one'], + ['Function:src/part:one.ts:run', 'src/part:one.ts'], + ['Method:src/a.cpp:Owner::run#0', 'src/a.cpp'], + ['Function:src/space .ts:run', 'src/space .ts'], + ['func:alpha ', 'src/legacy.ts'], + ['Function:legacy', 'src/legacy.ts'], + ['Function:legacy:run', 'src/legacy.ts'], + ['Class:vendor.v2:Widget', 'src/Widget.ts'], + ['Class:objc:class:Widget', 'Sources/Widget.h'], + ['Method:objc:method:Widget:-:load:', 'Sources/Widget.m'], + ['Function:objc:function:helper', 'Sources/Widget.m'], + ['Property:spring-config:application.properties:payment.timeout', 'application.properties'], + ['CodeElement:spring-bean:Method:src/Config.java:Config.bean#0', 'src/Config.java'], + ['CodeElement:spring-aop:Class:src/Service.java:Service:transactional', 'src/Service.java'], + ['Route:GET:/api/items', 'src/routes.ts'], + ['Tool:server:search', 'src/tools.ts'], + ['Method:checks/test_batch.py:Batch.test_run#0', undefined], + ['Method:checks/test_batch.py:Batch.test_run#0', null], + ['Method:checks/test_batch.py:Batch.test_run#0', ''], + ])('preserves valid paths and legacy/synthetic identities: %s / %j', (id, filePath) => { + expect(() => assertSymbolFilePath(id, filePath)).not.toThrow(); + }); +}); diff --git a/gitnexus/vendor/lbug-fts/manifest.json b/gitnexus/vendor/lbug-fts/manifest.json index 62c4da0c7..74d698acc 100644 --- a/gitnexus/vendor/lbug-fts/manifest.json +++ b/gitnexus/vendor/lbug-fts/manifest.json @@ -1,10 +1,11 @@ { - "coreVersion": "0.21.1", - "extensionVersion": "0.21.0", + "coreVersion": "0.22.0", + "extensionVersion": "0.22.0", "filename": "libfts.lbug_extension", "officialRepo": "https://extension.ladybugdb.com/", "urlTemplate": "{officialRepo}v{extensionVersion}/{upstreamPlatform}/fts/{filename}", "upstreamDigestPublished": false, + "workflowRun": "https://github.com/LadybugDB/ladybug/actions/runs/40157072171", "tuples": [ { "tuple": "linux-x64", "upstreamPlatform": "linux_amd64" }, { "tuple": "linux-arm64", "upstreamPlatform": "linux_arm64" }, diff --git a/gitnexus/vendor/lbug-fts/prebuilds/SHA256SUMS b/gitnexus/vendor/lbug-fts/prebuilds/SHA256SUMS index 9683a3704..c56deed46 100644 --- a/gitnexus/vendor/lbug-fts/prebuilds/SHA256SUMS +++ b/gitnexus/vendor/lbug-fts/prebuilds/SHA256SUMS @@ -1,5 +1,5 @@ -248332de781a7781f612a55b5397e88f58f1f1ba2a973836ada199eab0f6bf10 ./darwin-arm64/libfts.lbug_extension -742f2756c2f80bcd1886b6ee0446483038cff0ecf6cf47f698bc6fe855cbaef6 ./linux-x64/libfts.lbug_extension -7b6132ae6a554e3eeb3c4b0da7e2eb5c483b258b31e848a0e3d6000ac29885ee ./win32-x64/libfts.lbug_extension -b8675b086664e3d6742330a517c5b619f0ac10dd094d250f12bef9822a8d5750 ./linux-arm64/libfts.lbug_extension -c86f427503555a3ccdc55eb66a8d82793a206aac599a6092f7d1d4eb50c21403 ./darwin-x64/libfts.lbug_extension +6fdf9749f47ca2e35c82302055ff441061a68158dfb2c4a61c396ccd6dabe289 ./darwin-arm64/libfts.lbug_extension +872b46f46468cf27c7f1d26bb0b98ccdc4d17056d73ecd45c6047352e2cda5c9 ./linux-arm64/libfts.lbug_extension +8fcc16c1f26ddea668ca76bc18f6f83a1588238a4baa08511106baa51e5f4430 ./linux-x64/libfts.lbug_extension +a016f7d56611a7218d6ac2f4cace083ec414e39ea9ceb601e44233c1f9fa7b4a ./darwin-x64/libfts.lbug_extension +dc70aaf9940c204158b8932c67b08c459cd70718748997bfb5b2fba8386cffce ./win32-x64/libfts.lbug_extension diff --git a/gitnexus/vendor/lbug-fts/prebuilds/darwin-arm64/libfts.lbug_extension b/gitnexus/vendor/lbug-fts/prebuilds/darwin-arm64/libfts.lbug_extension index 53bcff048..724869ee5 100644 Binary files a/gitnexus/vendor/lbug-fts/prebuilds/darwin-arm64/libfts.lbug_extension and b/gitnexus/vendor/lbug-fts/prebuilds/darwin-arm64/libfts.lbug_extension differ diff --git a/gitnexus/vendor/lbug-fts/prebuilds/darwin-x64/libfts.lbug_extension b/gitnexus/vendor/lbug-fts/prebuilds/darwin-x64/libfts.lbug_extension index 6fa05c0da..ac52125af 100644 Binary files a/gitnexus/vendor/lbug-fts/prebuilds/darwin-x64/libfts.lbug_extension and b/gitnexus/vendor/lbug-fts/prebuilds/darwin-x64/libfts.lbug_extension differ diff --git a/gitnexus/vendor/lbug-fts/prebuilds/linux-arm64/libfts.lbug_extension b/gitnexus/vendor/lbug-fts/prebuilds/linux-arm64/libfts.lbug_extension index bf4c01920..4e198707f 100644 Binary files a/gitnexus/vendor/lbug-fts/prebuilds/linux-arm64/libfts.lbug_extension and b/gitnexus/vendor/lbug-fts/prebuilds/linux-arm64/libfts.lbug_extension differ diff --git a/gitnexus/vendor/lbug-fts/prebuilds/linux-x64/libfts.lbug_extension b/gitnexus/vendor/lbug-fts/prebuilds/linux-x64/libfts.lbug_extension index f7ab6c269..262f3a947 100644 Binary files a/gitnexus/vendor/lbug-fts/prebuilds/linux-x64/libfts.lbug_extension and b/gitnexus/vendor/lbug-fts/prebuilds/linux-x64/libfts.lbug_extension differ diff --git a/gitnexus/vendor/lbug-fts/prebuilds/win32-x64/libfts.lbug_extension b/gitnexus/vendor/lbug-fts/prebuilds/win32-x64/libfts.lbug_extension index dec711432..5760f1385 100644 Binary files a/gitnexus/vendor/lbug-fts/prebuilds/win32-x64/libfts.lbug_extension and b/gitnexus/vendor/lbug-fts/prebuilds/win32-x64/libfts.lbug_extension differ diff --git a/gitnexus/vendor/lbug-vector/README.md b/gitnexus/vendor/lbug-vector/README.md index ea5e9e862..52ede38d4 100644 --- a/gitnexus/vendor/lbug-vector/README.md +++ b/gitnexus/vendor/lbug-vector/README.md @@ -1,16 +1,16 @@ # LadybugDB VECTOR artifacts These unmodified binaries come from the official LadybugDB -[v0.21.1 build](https://github.com/LadybugDB/ladybug/actions/runs/36647903773), -at core commit `17a70e51c31b50d30231da32154d9ff4bef0d95f` and extensions -commit `bf74ab654af6bd40a0e3208ac4e8bcf5cb45a7a0`. +[v0.22.0 build](https://github.com/LadybugDB/ladybug/actions/runs/40157072171), +at core commit `4ca22377b85a33a10247410fe3944a5224b1169d` and extensions +commit `119912e604ebbaad64072a701698d6fcf4140051`. `manifest.json` records each GitHub artifact ID, the archive digest supplied by GitHub, and the extracted binary's SHA-256. Every archive was checked against that digest before extracting `vector/libvector.lbug_extension`. -The core's `0.21.0` CDN compatibility path can serve a VECTOR binary built -against incompatible core symbols. Load the matching packaged artifact first +The CDN compatibility path can serve a VECTOR binary built against +incompatible core symbols. Load the matching packaged artifact first so vector indexes work without a network install or a mutable home-cache copy. The existing `never` policy still disables the extension; missing artifacts retain the normal named-load and install fallback. @@ -23,7 +23,7 @@ native tests before changing the pin. Do not substitute a nightly binary. For example, the Linux x64 archive is available with: ```sh -gh api repos/LadybugDB/ladybug/actions/artifacts/11069747578/zip > vector-linux.zip +gh api repos/LadybugDB/ladybug/actions/artifacts/12584987551/zip > vector-linux.zip ``` GitHub Actions artifacts expire; the committed binaries and checksums preserve diff --git a/gitnexus/vendor/lbug-vector/manifest.json b/gitnexus/vendor/lbug-vector/manifest.json index a0a75ee0c..939bfbee1 100644 --- a/gitnexus/vendor/lbug-vector/manifest.json +++ b/gitnexus/vendor/lbug-vector/manifest.json @@ -1,46 +1,46 @@ { - "coreVersion": "0.21.1", + "coreVersion": "0.22.0", "filename": "libvector.lbug_extension", "upstreamRepository": "https://github.com/LadybugDB/ladybug", - "upstreamTag": "v0.21.1", - "upstreamCommit": "17a70e51c31b50d30231da32154d9ff4bef0d95f", - "extensionsCommit": "bf74ab654af6bd40a0e3208ac4e8bcf5cb45a7a0", - "workflowRun": "https://github.com/LadybugDB/ladybug/actions/runs/36647903773", + "upstreamTag": "v0.22.0", + "upstreamCommit": "4ca22377b85a33a10247410fe3944a5224b1169d", + "extensionsCommit": "119912e604ebbaad64072a701698d6fcf4140051", + "workflowRun": "https://github.com/LadybugDB/ladybug/actions/runs/40157072171", "artifacts": [ { - "tuple": "linux-x64", - "artifactId": 11069747578, - "artifactName": "lbug-extensions-linux-x86_64", - "archiveSha256": "377be87f6cf0e56a38230a536e768a0914f273163f112b8101f69894a088777b", - "sha256": "571726cc2ea0d202df1909ae5a3b0528c9dea3ea8d40be6c16b310ec14aa8821" - }, - { - "tuple": "linux-arm64", - "artifactId": 11069462823, - "artifactName": "lbug-extensions-linux-aarch64", - "archiveSha256": "bec0b8c5eb7f75b1f448b1a10360dbbd82c4553e3514418e90a37065468cb4cd", - "sha256": "583101f5bf2d0e5d40de2ee8966018a3bdf0fb8ad8951ef51ef46cd59169445f" + "tuple": "darwin-arm64", + "artifactId": 12585252054, + "artifactName": "lbug-extensions-osx-arm64", + "archiveSha256": "204276e018cd78b8c92e269347dd5cd42b830ec5960c81b33560e25193d645a8", + "sha256": "7a8232ac89814b9edd0e8eb13f75438646b1b1d2a951b916546d4626c0da6ae5" }, { "tuple": "darwin-x64", - "artifactId": 11069679640, + "artifactId": 12585423270, "artifactName": "lbug-extensions-osx-x86_64", - "archiveSha256": "2ddd47cd3afb5a2bcdae37e928125a81a1aa73775bb2e5e9448033f51eb02c18", - "sha256": "7c0cea65d68c341453ce567e0d12897d0691b5f9ff01cff0fdf0db5756ab7851" + "archiveSha256": "1ccb3a8a2dc016da8bc4a807e6cd79313b91e2fcda46c88ee663d17fc486024a", + "sha256": "dbfd24e42528945121d7ff435b2814ac1bc724e26a59e0ddec1dafddb4eb0a4a" }, { - "tuple": "darwin-arm64", - "artifactId": 11069049174, - "artifactName": "lbug-extensions-osx-arm64", - "archiveSha256": "5d004e4bb9e3dd6f6da7248135eba64998adca283390c88e95f17d46224bbf5c", - "sha256": "6f878f3c0a2320a34967ff568fcf6bbe9065666e95035f0eddfe28bbc73b53f4" + "tuple": "linux-arm64", + "artifactId": 12585442882, + "artifactName": "lbug-extensions-linux-aarch64", + "archiveSha256": "0351794dabb4cc1f1c7a5750f7fd59c24f9880aea89ada5787f140de0fdc78cf", + "sha256": "75a193552363511f12adc4166427774e81c307f22f39d5d814158ba988bfc0dd" + }, + { + "tuple": "linux-x64", + "artifactId": 12584987551, + "artifactName": "lbug-extensions-linux-x86_64", + "archiveSha256": "0ffe6dc0a3fd61b814edb6cf5aa6e5b738ed8f554f86ece1b406a5d93a8c3e05", + "sha256": "23da57e805fed208f1eacb4b011cb0c2e917ae098fa6853763e44221211e71b0" }, { "tuple": "win32-x64", - "artifactId": 11070343650, + "artifactId": 12586676467, "artifactName": "lbug-extensions-win-x86_64", - "archiveSha256": "cef691c45f28be517b703bfdfafdd66a7d30b7da236f96c4e57297f5e67cf9f2", - "sha256": "6a8878eb2c93400222187460f8fc20ffc9cda4fc2c382fcbdf88f72a350d5746" + "archiveSha256": "57c66ce9facff3dd3e9ede7c2dd41f7fcd1fc3583f260a6c95829424811bdf7e", + "sha256": "d8712afafb30b5c0a3efe623f910e434dae5132b5970518649686944bc529748" } ] } diff --git a/gitnexus/vendor/lbug-vector/prebuilds/darwin-arm64/libvector.lbug_extension b/gitnexus/vendor/lbug-vector/prebuilds/darwin-arm64/libvector.lbug_extension index 09df253e5..22dcbe12f 100644 Binary files a/gitnexus/vendor/lbug-vector/prebuilds/darwin-arm64/libvector.lbug_extension and b/gitnexus/vendor/lbug-vector/prebuilds/darwin-arm64/libvector.lbug_extension differ diff --git a/gitnexus/vendor/lbug-vector/prebuilds/darwin-x64/libvector.lbug_extension b/gitnexus/vendor/lbug-vector/prebuilds/darwin-x64/libvector.lbug_extension index 37c59b812..bf8db3155 100644 Binary files a/gitnexus/vendor/lbug-vector/prebuilds/darwin-x64/libvector.lbug_extension and b/gitnexus/vendor/lbug-vector/prebuilds/darwin-x64/libvector.lbug_extension differ diff --git a/gitnexus/vendor/lbug-vector/prebuilds/linux-arm64/libvector.lbug_extension b/gitnexus/vendor/lbug-vector/prebuilds/linux-arm64/libvector.lbug_extension index 940de9cfa..19aef42bb 100644 Binary files a/gitnexus/vendor/lbug-vector/prebuilds/linux-arm64/libvector.lbug_extension and b/gitnexus/vendor/lbug-vector/prebuilds/linux-arm64/libvector.lbug_extension differ diff --git a/gitnexus/vendor/lbug-vector/prebuilds/linux-x64/libvector.lbug_extension b/gitnexus/vendor/lbug-vector/prebuilds/linux-x64/libvector.lbug_extension index 8f627b503..ed75b080b 100644 Binary files a/gitnexus/vendor/lbug-vector/prebuilds/linux-x64/libvector.lbug_extension and b/gitnexus/vendor/lbug-vector/prebuilds/linux-x64/libvector.lbug_extension differ diff --git a/gitnexus/vendor/lbug-vector/prebuilds/win32-x64/libvector.lbug_extension b/gitnexus/vendor/lbug-vector/prebuilds/win32-x64/libvector.lbug_extension index 1fa3e01b9..daed1907e 100644 Binary files a/gitnexus/vendor/lbug-vector/prebuilds/win32-x64/libvector.lbug_extension and b/gitnexus/vendor/lbug-vector/prebuilds/win32-x64/libvector.lbug_extension differ diff --git a/gitnexus/vitest.config.ts b/gitnexus/vitest.config.ts index 2c011325e..8315e6e6f 100644 --- a/gitnexus/vitest.config.ts +++ b/gitnexus/vitest.config.ts @@ -119,6 +119,7 @@ export default defineConfig({ 'test/integration/lbug-orphan-sidecar-recovery.test.ts', 'test/integration/lbug-interrupted-checkpoint-recovery.test.ts', 'test/integration/lbug-checkpoint-accounting.test.ts', + 'test/integration/lbug-rel-copy-page-padding.test.ts', 'test/integration/lbug-incremental-search.test.ts', 'test/integration/lbug-readonly-init.test.ts', // Shared sibling store (#3352): each file runs real analyses and opens @@ -230,6 +231,7 @@ export default defineConfig({ 'test/integration/lbug-orphan-sidecar-recovery.test.ts', 'test/integration/lbug-interrupted-checkpoint-recovery.test.ts', 'test/integration/lbug-checkpoint-accounting.test.ts', + 'test/integration/lbug-rel-copy-page-padding.test.ts', 'test/integration/lbug-incremental-search.test.ts', 'test/integration/lbug-readonly-init.test.ts', 'test/integration/analyze-wal-checkpoint-failure.test.ts',