fix(lbug): upgrade core and bundled extensions to 0.22.0 (#3578)

* fix(lbug): bump @ladybugdb/core to 0.21.2 to stop doubled rows after delete and re-copy

LadybugDB 0.21.0 and 0.21.1 return every row of a table that spans more
than one native vector twice once the incremental writeback has DETACH
DELETEd it and a COPY into an earlier table has 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. The identity guard from #3442 stops
the run with "unexpected or duplicate ID", so a Swift edit that pulls every
importer into the write set failed on repos with more than 2048 structs.
Only unreleased main was affected: 1.6.12 ships 0.18.3.

Pin 0.21.2 in package.json, update the six @ladybugdb/core entries in the
lockfile, and move coreVersion in the vendored FTS manifest. The FTS
extension stays at 0.21.0 because no v0.21.1 or v0.21.2 artifact is
published; the pairing assertion and its unit tests pass.

Add a regression to the delete-nodes-for-files suite: 2049 structs with
CONTAINS edges are loaded, every file is deleted, the graph is loaded
again, and each id and edge must appear exactly once.

Verification (Linux, Node 22.18):
- lbug-delete-nodes-for-files: 11/11 on 0.21.2. With the 0.21.1 native
  packages swapped in, the new test fails with 4098 rows instead of 2049.
- Reporter recipe end to end on the built CLI: incremental analyze exits 0
  with no duplicate ids on a Swift-heavy fixture and on a 70-file Python
  fixture, with and without FTS. On 0.21.1 the Swift fixture exits 1.
- A 0.21.1-written database opens and reads under 0.21.2.
- prettier, eslint and typecheck:tests are clean on the changed files.
- The full lbug-db project had not finished when this was committed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(lbug): pair core and extensions at 0.22.0

Include the duplicate-row scan and relationship page-padding fixes.
Use matching official FTS and VECTOR artifacts on all five platforms.

Add a page-padding regression and document the storage 47 to 49 upgrade,
including rollback and cached-embedding recovery limits.

* fix(lbug): recognize pending checkpoint recovery

* fix(bench): isolate indexes by runtime

* fix(query): reject mixed symbol paths

Printable paths could disagree with canonical symbol UIDs and pass
NUL-only validation. Reject those tuples at response boundaries.
Add Python/Swift MCP fault cases and a linked-worktree reproducer.

Refs #3527

* fix(query): preserve provider semantic IDs

* fix(lbug): clear analysis memory budget

Restore the normal reader budget after indexing. The leaked adaptive hint exhausted the 0.22.0 pool during query and rename operations. Assert the budget is restored after real analyses, and give integrity cases unique CI audit identities.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Gergő Magyar 2026-10-31 15:29:45 +00:00 • committed by GitHub
parent d32ed0bf06
commit 347165dd59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 706 additions and 103 deletions

View file

@ -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

View file

@ -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).

View file

@ -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 {

View file

@ -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 |
| --- | ---: | ---: | --- | --- |

View file

@ -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"
],

View file

@ -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",

View file

@ -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',

View file

@ -0,0 +1,264 @@
/**
* Generated, non-confidential #3527 probe. Keeps its indexes and raw responses.
* Usage: node scripts/repro-3527.mjs <built-gitnexus-package> <new-output-dir>
* 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;
}

View file

@ -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<string> = 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,

View file

@ -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. */

View file

@ -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();

View file

@ -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<string, unknown>;
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();

View file

@ -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<string>();
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<string[]> =>
((await executeQuery('MATCH (n:Struct) RETURN n.id AS id')) as Array<{ id: string }>).map(
(r) => String(r.id),
);
const containsEdges = async (): Promise<number> => {
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);
});
});

View file

@ -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

View file

@ -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<Array<Record<string, unknown>>> => {
const raw = await conn.query(cypher);
const rows: Array<Record<string, unknown>> = [];
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);

View file

@ -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<string[]> {
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;
}

View file

@ -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(

View file

@ -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));

View file

@ -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();
});
});

View file

@ -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" },

View file

@ -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

View file

@ -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

View file

@ -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"
}
]
}

View file

@ -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',