perf: reduce lookup allocations and stream identity checks (#3805)

Reduce lexical lookup allocations and stream persisted node identities during incremental verification. Preserve lookup snapshots, connection ownership, cursor cleanup, and both fail-closed publication checks.

Include fresh and persistent benchmark coverage, differential comparisons, and memory scaling reproduction.

Validation: 4,019 local tests, 72,000 differential comparisons, build and typechecks, and all 12 required CI checks passed.
This commit is contained in:
Gergő Magyar 2026-11-04 10:15:24 +00:00 • committed by GitHub
parent e3e50be954
commit 450702e10c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 9744 additions and 99 deletions

View file

@ -7,6 +7,93 @@ permissions:
contents: read
jobs:
lexical-lookup-benchmark:
name: lexical lookup / separate and repeated processes
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
env:
GITNEXUS_LEXICAL_BENCH_WORKERS: '2'
GITNEXUS_LEXICAL_BENCH_HEAP_MIB: '4096'
GITNEXUS_LEXICAL_BENCH_SCREEN: '0'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 2
persist-credentials: false
- name: Pin comparison revision
id: baseline
shell: bash
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if [[ -n "$PR_BASE_SHA" ]]; then
comparison_sha="$PR_BASE_SHA"
else
comparison_sha="$(git rev-parse --verify HEAD^1)"
fi
printf 'sha=%s\n' "$comparison_sha" >> "$GITHUB_OUTPUT"
- uses: ./.github/actions/setup-gitnexus
with:
build: 'true'
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ steps.baseline.outputs.sha }}
path: baseline
persist-credentials: false
- name: Build baseline runtime and install search extensions
working-directory: baseline/gitnexus
run: |
npm ci
npm run build
npx tsx scripts/ensure-fts.ts
- name: Install candidate search extensions
working-directory: gitnexus
run: npx tsx scripts/ensure-fts.ts
- name: Prepare identical source corpora
shell: bash
env:
BASE_SHA: ${{ steps.baseline.outputs.sha }}
run: |
mkdir "$RUNNER_TEMP/lexical-lookup"
for variant in baseline candidate; do
corpus="$RUNNER_TEMP/lexical-lookup/corpus-$variant"
git clone --quiet --no-checkout --no-hardlinks "$GITHUB_WORKSPACE/baseline" "$corpus"
git -C "$corpus" sparse-checkout set gitnexus/src/core/ingestion gitnexus-shared/src/scope-resolution
git -C "$corpus" checkout --quiet --detach "$BASE_SHA"
done
- name: Compare lexical lookup semantics
run: node gitnexus/bench/lexical-lookup/differential.mjs "$GITHUB_WORKSPACE/baseline" "$GITHUB_WORKSPACE" | tee "$RUNNER_TEMP/lexical-lookup/differential.log"
shell: bash
- name: Measure separate processes
shell: bash
run: >-
node gitnexus/bench/lexical-lookup/measure.mjs
"$GITHUB_WORKSPACE/baseline" "$GITHUB_WORKSPACE"
"$RUNNER_TEMP/lexical-lookup/corpus-baseline" "$RUNNER_TEMP/lexical-lookup/corpus-candidate"
"$RUNNER_TEMP/lexical-lookup/fresh" fresh
2>&1 | tee "$RUNNER_TEMP/lexical-lookup/fresh.log"
- name: Measure repeated processes
if: ${{ !cancelled() }}
shell: bash
run: >-
node gitnexus/bench/lexical-lookup/measure.mjs
"$GITHUB_WORKSPACE/baseline" "$GITHUB_WORKSPACE"
"$RUNNER_TEMP/lexical-lookup/corpus-baseline" "$RUNNER_TEMP/lexical-lookup/corpus-candidate"
"$RUNNER_TEMP/lexical-lookup/persistent" persistent
2>&1 | tee "$RUNNER_TEMP/lexical-lookup/persistent.log"
- name: Retain lexical lookup measurements
if: always()
uses: ./.github/actions/upload-test-artifact
with:
name: lexical-lookup-performance
path: |
${{ runner.temp }}/lexical-lookup/fresh/results.json
${{ runner.temp }}/lexical-lookup/persistent/results.json
${{ runner.temp }}/lexical-lookup/*.log
retention-days: 30
mcp-query-benchmark:
name: MCP query performance
runs-on: ubuntu-latest

View file

@ -29,11 +29,15 @@ export interface NameClaimSources {
readonly bindingsAt?: (scope: Scope, name: string) => readonly BindingRef[];
readonly importsAt?: (scope: Scope) => readonly ImportEdge[];
}
const NO_BINDINGS: readonly BindingRef[] = Object.freeze([]);
const NO_CLAIMS: readonly NameClaim[] = Object.freeze([]);
const NO_IMPORTS: readonly ImportEdge[] = Object.freeze([]);
const NO_SELECTION: ScopeClaimSelection = Object.freeze({ status: 'absent', claims: NO_CLAIMS });
const ABSENT: NameClaimResult = Object.freeze({
status: 'absent',
bindings: [],
imports: [],
claims: [],
bindings: NO_BINDINGS,
imports: NO_IMPORTS,
claims: NO_CLAIMS,
});
// Published scope arrays are readonly. Cache by the array, not the scope ID:
// finalization and cache hydration can replace a scope's import/claim snapshot.
@ -42,8 +46,6 @@ const importsByName = new WeakMap<
readonly ImportEdge[],
ReadonlyMap<string, readonly ImportEdge[]>
>();
const NO_CLAIMS: readonly NameClaim[] = Object.freeze([]);
const NO_IMPORTS: readonly ImportEdge[] = Object.freeze([]);
export function nameClaimsFor(scope: Scope, name: string): readonly NameClaim[] {
const claims = scope.nameClaims;
@ -93,11 +95,13 @@ export function selectClaimsAtScope(
name: string,
options: NameLookupOptions = {},
): ScopeClaimSelection {
const candidates = nameClaimsFor(scope, name);
if (candidates.length === 0) return NO_SELECTION;
const purpose = options.purpose ?? 'value';
const claims = nameClaimsFor(scope, name).filter(
const claims = candidates.filter(
(c) => c.purpose === undefined || c.purpose === 'both' || c.purpose === purpose,
);
if (claims.length === 0) return { status: 'absent', claims };
if (claims.length === 0) return NO_SELECTION;
const active = claims.filter(
(c) =>
c.availableFrom === undefined ||
@ -192,6 +196,9 @@ export function lookupLexicalName(
): NameClaimResult {
let id: ScopeId | null = startScope;
let position = options.position;
// Position is cleared when crossing deferred/redirected ownership. Reuse
// the caller's readonly options until that happens, then copy only once.
let selectionOptions = options;
let forcedClaims: readonly NameClaim[] | undefined;
let carriedConstructor: TypeRef | undefined;
const visited = new Set<ScopeId>();
@ -209,9 +216,10 @@ export function lookupLexicalName(
id = scope.lookupPolicy?.parentScope ?? scope.parent;
continue;
}
if (selectionOptions.position !== position) selectionOptions = { ...options, position };
const selected: ScopeClaimSelection =
forcedClaims === undefined
? selectClaimsAtScope(scope, name, { ...options, position })
? selectClaimsAtScope(scope, name, selectionOptions)
: { status: 'selected', claims: forcedClaims };
forcedClaims = undefined;
const redirected = selected.claims.find((c) => c.redirect !== undefined);
@ -245,38 +253,47 @@ export function lookupLexicalName(
return { ...ABSENT, status: 'blocked', scope, claims: selected.claims };
const nameClaims = nameClaimsFor(scope, name);
const hasClaimsForName = nameClaims.length > 0;
const allBindings = sources.bindingsAt?.(scope, name) ?? scope.bindings.get(name) ?? [];
const allBindings =
sources.bindingsAt?.(scope, name) ?? scope.bindings.get(name) ?? NO_BINDINGS;
const allImports = importsFor(sources.importsAt?.(scope) ?? scope.imports ?? NO_IMPORTS, name);
let bindings = allBindings.filter(
(b) =>
b.availableFrom === undefined ||
position === undefined ||
compare(b.availableFrom, position) <= 0,
);
let bindings =
allBindings.length === 0
? NO_BINDINGS
: allBindings.filter(
(b) =>
b.availableFrom === undefined ||
position === undefined ||
compare(b.availableFrom, position) <= 0,
);
let imports = allImports;
if (selected.status === 'selected') {
bindings = bindings.filter((b) =>
selected.claims.some((c) =>
c.kind === 'import'
? b.via !== undefined && matchesClaim(b.via.atRange, c)
: c.kind === 'binding' &&
(b.origin === 'local' || b.declarationRange !== undefined) &&
matchesBindingClaim(b.declarationRange, c),
),
);
imports = allImports.filter((e) =>
selected.claims.some((c) => c.kind === 'import' && matchesClaim(e.atRange, c)),
);
if (bindings.length > 0)
bindings = bindings.filter((b) =>
selected.claims.some((c) =>
c.kind === 'import'
? b.via !== undefined && matchesClaim(b.via.atRange, c)
: c.kind === 'binding' &&
(b.origin === 'local' || b.declarationRange !== undefined) &&
matchesBindingClaim(b.declarationRange, c),
),
);
imports =
allImports.length === 0
? NO_IMPORTS
: allImports.filter((e) =>
selected.claims.some((c) => c.kind === 'import' && matchesClaim(e.atRange, c)),
);
} else if (hasClaimsForName) {
// A declaration in another namespace, or a not-yet-visible declaration,
// must not sneak back through the unfiltered legacy binding channels.
bindings = bindings.filter(
(b) =>
b.availableFrom !== undefined &&
b.declarationRange !== undefined &&
!nameClaims.some((c) => rangesOverlap(c.range, b.declarationRange!)),
);
imports = [];
if (bindings.length > 0)
bindings = bindings.filter(
(b) =>
b.availableFrom !== undefined &&
b.declarationRange !== undefined &&
!nameClaims.some((c) => rangesOverlap(c.range, b.declarationRange!)),
);
imports = NO_IMPORTS;
}
const ownType = scope.typeBindings.get(name);
const typeFactOwnsName =
@ -328,9 +345,12 @@ export function lookupLexicalName(
(typeBinding === undefined || isInferredFact(typeBinding.source))
)
typeBinding = carriedConstructor;
const usable = bindings.filter(
(b) => (options.purpose ?? 'value') !== 'value' || b.via?.typeOnly !== true,
);
const usable =
bindings.length === 0
? NO_BINDINGS
: bindings.filter(
(b) => (options.purpose ?? 'value') !== 'value' || b.via?.typeOnly !== true,
);
return {
status:
usable.length > 0 ||
@ -358,6 +378,9 @@ export function lookupLexicalName(
}
// A write to an undeclared name declares nothing, but it still carries the
// value's type to reads inside the writing scope.
const carried = carriedConstructor === undefined ? {} : { typeBinding: carriedConstructor };
return id === null ? { ...ABSENT, ...carried } : { ...ABSENT, ...carried, status: 'blocked' };
if (carriedConstructor === undefined)
return id === null ? ABSENT : { ...ABSENT, status: 'blocked' };
return id === null
? { ...ABSENT, typeBinding: carriedConstructor }
: { ...ABSENT, typeBinding: carriedConstructor, status: 'blocked' };
}

View file

@ -1,7 +1,6 @@
# Analyze phase breakdown — where the time actually goes
Updated 2026-11-01 with scope-emission measurements after #3796 merged, plus
production-hardening measurements on LadybugDB **0.22.0** for unchanged dirty checkouts.
Updated 2026-11-02 with the lexical-lookup allocation experiment below.
Records what the `analyze`
pipeline costs per phase and the results of optimization experiments.
The September measurements remain below as historical
@ -14,6 +13,193 @@ dispatch path is `bench/parse-dispatch-rounds/`.
---
## Lexical-lookup allocation experiment — 2026-11-02
**Decision: keep experimental; the performance requirement for both workflows
has not passed.** Fresh-process edits improved in all three pairs (8.95% lower
median), but the first repeated-process pair took 12.68% longer and triggered
the harness's early rejection gate. Its host load rose sharply. That does not
prove a patch-induced regression, and it also does not establish a warm-process
speedup. Do not promote this as a production optimization on this evidence.
The patch reduces temporary allocation in `lookupNameClaim`,
`selectClaimsAtScope` and `lookupLexicalName`: reuse frozen empty results, skip
filters over empty inputs, avoid concatenating empty binding channels, and copy
lookup options only when the effective position changes. Nonempty binding
results retain snapshot semantics. Provider hooks, declaration ownership,
shadowing, redirects and position checks still run. No semantic-result cache,
new invalidation mechanism or native dependency is introduced.
### Separate-process edit measurements
| Measurement | Baseline | Candidate | Less elapsed time |
| --- | ---: | ---: | ---: |
| Edit 1: `save` → `load` | 122.065s | 111.135s | 8.95% |
| Edit 2: `load` → `save` | 114.006s | 101.730s | 10.77% |
| Edit 3: `save` → `load` | 131.285s | 111.303s | 15.22% |
| Median edit | 122.065s | 111.135s | 8.95% |
| All three edits | 367.356s | 324.168s | 11.76% |
| Median scope-resolution phase | 68.197s | 62.344s | 8.58% |
| Highest process peak RSS across edits | 4.29 GiB | 4.25 GiB | — |
All three scope-resolution phases improve, but not all wall-time differences
belong to that phase. For example, the first pair's process-detection interval
falls from 5.889s to 1.475s, and the third pair's post-`done` work falls from
16.551s to 7.974s. The patch does not directly change either stage. Treat the
observed end-to-end reductions as a small shared-host screen, not a universal
speedup or a precise attribution of saved time. One-minute host load ranges
from 7.79 to 11.43 at the start of these measured edits.
### Repeated-process screen
| Measurement | Baseline | Candidate | Less elapsed time |
| --- | ---: | ---: | ---: |
| First edit: `save` → `load` | 109.483s | 123.364s | −12.68% |
| Scope-resolution phase | 67.013s | 65.739s | 1.90% |
| Ladybug write intervals | 8.814s | 12.902s | — |
| FTS phase | 10.600s | 14.766s | — |
| Work after `done` progress | 8.947s | 14.858s | — |
| Process peak RSS, including setup | 5.49 GiB | 5.62 GiB | — |
These intervals come from progress callbacks; they include all work between
adjacent callbacks rather than instrumenting native operations individually.
Baseline one-minute load fell **6.35 → 3.81**; candidate load rose
**3.81 → 12.23**. Scope resolution improved slightly while several other
stages slowed. Only this first pair was run, as specified by the screening
gate; there is no three-pair warm-process median. Both graphs and persisted
call targets matched. Persistent mode retains one process per variant and
models repeated analysis, not watch-event scheduling or debounce latency.
Separate excluded cold setup runs took 218.219s/187.148s in fresh mode and
158.148s/153.951s in persistent mode (baseline/candidate). Each lifecycle uses
independent index directories. Setup order is fixed and host load changes,
so these are not controlled cold-index speedup claims. The
[measurement receipt](lexical-lookup/measurements.json) includes every sample,
phase duration, peak memory, graph hash, load average and runtime checksum.
### Method and correctness boundaries
Baseline runtime and both disposable corpus checkouts are pinned to
`347cc99b4a237f9a71c0084167bcbcbd9d06e610`; the candidate runtime adds only the
lookup patch. Each corpus has 3,384 indexed files / 3,120 parseable files,
including the same control file. Node **v22.23.3**, LadybugDB **0.22.0**,
Linux x64, Intel i9-12900K (24 logical CPUs), 16 parse workers, 8 GiB V8 heap.
Samples run serially; paired order is baseline/candidate, candidate/baseline,
baseline/candidate after excluded initialization runs. Runtime builds stay
fixed throughout each comparison, with no tests or other benchmarks launched
by this experiment concurrently. The machine is shared with other workloads.
The [harness](lexical-lookup/measure.mjs) times `runFullAnalysis`, including
incremental persistence and FTS. Process startup, graph hashing and subsequent
persisted-target verification are outside the timer. CPU profiling and heap
probes are off; the existing scope phase logger remains on for both variants.
Each measured edit changes one file, reparses 22 files, hits the parser cache
for 3,098 files, uses incremental writes and has no rebuild reasons. FTS must
remain available. This is a real edit benchmark, not an up-to-date fast return.
Every pair compares canonical hashes of **114,413 source/semantic nodes and
318,798 relationships**. Derived Folder/Community/Process nodes and
CONTAINS/MEMBER_OF/STEP_IN_PROCESS edges are excluded. Hashes cover the graph
returned after persistence; cleared source content is not independently
verified by these hashes. A separate LadybugDB query after each sample verifies
that the control CALLS edge follows `save`/`load`. Matching graphs preserve the
baseline's limits; they do not establish complete resolution of the corpus.
Targeted tests cover mutation after a missing lookup, result snapshots, reused
frozen options and deferred activation. The initial validation passed 137 tests
across lexical ownership, TypeScript type ownership, block shadowing and
TypeScript/Python/C++ native rename suites. A seeded differential screen also
matched the original implementation in 72,000 generated lexical lookups.
The broader scope-resolution suite then passed **3,467 tests / 175 files**
(overlaps the initial targeted tests). The parameterized differential harness
also passed when rerun against the same two builds.
Build, test typecheck and ESLint passed (existing non-null assertion warnings
remain). These were the checks completed alongside the benchmark; PR validation is recorded separately.
To reproduce, build the baseline and candidate runtimes first, prepare two
clean disposable corpus checkouts at the same commit, then run from
`gitnexus/` (the runtime arguments name repository roots):
```sh
node bench/lexical-lookup/differential.mjs BASE_RUNTIME CANDIDATE_RUNTIME
node bench/lexical-lookup/measure.mjs BASE_RUNTIME CANDIDATE_RUNTIME BASE_CORPUS CANDIDATE_CORPUS OUTPUT_FRESH fresh
node bench/lexical-lookup/measure.mjs BASE_RUNTIME CANDIDATE_RUNTIME BASE_CORPUS CANDIDATE_CORPUS OUTPUT_PERSISTENT persistent
```
By default, the timing harness stops after the first edit pair if the candidate
is not faster. To establish a baseline, set `GITNEXUS_LEXICAL_BENCH_SCREEN=0`
to collect all three pairs regardless of their timings. Repeat each lifecycle
in independent sessions, setting `GITNEXUS_LEXICAL_BENCH_BASE_FIRST=0` in
alternating sessions to reverse seed and edit ordering. Both settings accept
only `0` or `1` and are recorded in `results.json`. Report every sample and
the variation between sessions; a screening result alone is not a baseline.
The harness waits for native workers to exit normally and removes only its
control source file. Keep builds fixed and do not run tests concurrently.
The receipt's harness checksums identify the versions used for measurement.
The checked-in scripts were subsequently formatted, and cleanup now attempts
both worker shutdowns and removes the control files even if a worker fails.
These changes are outside the timed analysis interval.
### CI execution
`.github/workflows/ci-tests.yml` runs the differential check and both process
lifecycles in a dedicated job. It builds the candidate and the pull request's
event baseline SHA, falling back to a verified first parent when the event has
no baseline. It uses two clean sparse corpus checkouts at that baseline.
The CI corpus contains the ingestion and shared
scope-resolution source trees, plus Git's cone-mode parent files. It is smaller
than the full-repository corpus measured above; its timings are a separate series.
CI sets `GITNEXUS_LEXICAL_BENCH_WORKERS=2` and
`GITNEXUS_LEXICAL_BENCH_HEAP_MIB=4096`; the local defaults remain 16 workers and
8192 MiB. Each lifecycle requires a new output directory and retains its raw
results and logs in the `lexical-lookup-performance` artifact. Graph and persisted
call mismatches fail the job. CI sets `GITNEXUS_LEXICAL_BENCH_SCREEN=0` so all
three paired edits and their correctness checks run regardless of timing noise.
### Repeated baseline, 2026-11-03 (CI corpus)
The [baseline receipt](lexical-lookup/baseline-20261103.json) records three
independent sessions per lifecycle, each with three paired edits. Every pair
was retained, including slower candidates. Runtime order was reversed in the
middle session. The corpus is the sparse CI source tree at
`4916e0d5e1f4d6d70d41c1f39cd244bca2c7b390`; baseline and candidate runtimes are
`4916e0d5e1f4d6d70d41c1f39cd244bca2c7b390` and
`6b487a529e14d59e51a9df9a44524e98705a6b71`. Both use Node 22.23.3,
LadybugDB 0.22.0, two workers and a 4 GiB heap on the same shared host.
These are medians of the three session medians; cold seeds are excluded:
| Lifecycle | Baseline | PR candidate | Change |
| --- | ---: | ---: | ---: |
| Separate processes | 28.494s | 27.552s | 3.31% less time |
| Repeated processes | 30.276s | 29.814s | 1.53% less time |
The candidate's session medians have coefficients of variation of **5.02%**
and **17.01%**, respectively. These small aggregate gains are inconclusive on
this host. The receipt also retains every paired percentage change, graph hash,
persisted call target and host-load reading. All 18 edit pairs matched 23,641
source/semantic nodes and 64,292 relationships, and the seeded differential
check passed 72,000 comparisons per session. Selection and reporting share the
same corpus; no holdout was used. This does not establish a full-repository or
watch-mode speedup.
During measurement, the PR advanced to
`6aeeace8e96603fb062dcc709e834042d5a6df74` by merging a Kotlin grammar update.
The lookup implementations and lockfile are unchanged, but the newer native
grammar and revision were not measured in that baseline. This receipt remains
pinned to the earlier builds.
The subsequent [optimization review in PR #3805](https://github.com/abhigyanpatwari/GitNexus/pull/3805)
and [experiment receipt](lexical-lookup/optimization-20261103.json) compare two
additional candidates against unchanged `6aeeace8` controls. Neither met the
required repeatable 5% warm-process improvement with at most 3% fresh-process
regression. Neither additional lexical candidate was retained. The harness
makes complete, opposite-order baseline collection reproducible.
---
## Current measurements — 2026-10-31
### Environment and timing boundaries

View file

@ -84,3 +84,46 @@ An in-place verification failure keeps a non-FTS dirty phase: an FTS-only
repair cannot clear the marker and recertify the inconsistent graph.
The same protection applies when FTS returns but analyzer finalization fails
before the final identity scan: recovery still rebuilds the graph.
## Memory scaling
`memory.mjs` measures the compiled production reader in a fresh process, without
forcing GC between reads. It records full identity verification, elapsed time,
sampled heap/RSS, the process RSS high-water mark, and initial/final retained heap.
The native pool is 512 MiB for measurements and 1 GiB for fixture creation.
Build the runtime with `npm run build`. Save this configuration outside the repo:
```json
{
"runtime": "/absolute/path/to/GitNexus",
"storage": "/tmp/gnx-identity-memory",
"output": "/tmp/gnx-identity-memory/seed.json",
"count": 1048576,
"width": 0,
"mode": "seed",
"reader": "stream",
"iterations": 40
}
```
Run from `gitnexus/`:
```sh
node --expose-gc --max-old-space-size=4096 bench/incremental-write-integrity/memory.mjs /tmp/config.json
```
Then change `mode` to `measure` and choose a new output file. Run each measurement
in a new process and use one process per database at a time. To compare the
original array-based implementation, set `runtime` to its separately built
checkout and `reader` to `baseline`; keep the same fixture and memory limits.
Vary `count` with separate fixtures to measure node scaling. `width` adds that
many ASCII bytes to each of ID, name and path, so it measures identity size
independently of node count. Repeat each point and hold the largest size out of
the fitted curve. A finite RSS fit is evidence over that range, not a guarantee
about every native allocator or the rest of the indexing pipeline.
The streaming verifier retains expected-node maps and one decoded row rather
than a full table of JavaScript rows. The expected maps grow linearly with graph
size; the native query result and the original graph still consume memory.

View file

@ -66,9 +66,9 @@ const source = (file) => import(pathToFileURL(join(ROOT, file)).href);
await adapter.tryFlushWAL();
const samples = [];
let calls = 0;
const query = async (sql) => {
const query = async (sql, onRow) => {
calls++;
return adapter.executeQuery(sql);
await adapter.forEachQueryRow(sql, onRow);
};
for (let run = 0; run < 9; run++) {
calls = 0;
@ -80,17 +80,18 @@ const source = (file) => import(pathToFileURL(join(ROOT, file)).href);
if (run >= 2) samples.push(elapsed);
}
const first = graph.iterNodes().next().value;
const alteredQuery = async (sql) => {
const rows = await adapter.executeQuery(sql);
if (sql.includes('(n:`Function`)')) rows.find((r) => r.id === first.id).startLine++;
return rows;
const alteredQuery = async (sql, onRow) => {
await adapter.forEachQueryRow(sql, (row) => {
if (sql.includes('(n:`Function`)') && row.id === first.id) row.startLine++;
onRow(row);
});
};
await assert.rejects(
reconcileGraphNodeIdentities(graph, alteredQuery, 'negative-control'),
/startLine/,
);
await assert.rejects(
reconcileGraphNodeIdentities(graph, async () => [], 'negative-control'),
reconcileGraphNodeIdentities(graph, async () => {}, 'negative-control'),
/missing ID/,
);
@ -124,12 +125,12 @@ const source = (file) => import(pathToFileURL(join(ROOT, file)).href);
rows.length !== size.nodes || mismatched > 0 || actual.size !== wanted.size;
if (rejected) {
await assert.rejects(
reconcileGraphNodeIdentities(graph, adapter.executeQuery, phase),
reconcileGraphNodeIdentities(graph, adapter.forEachQueryRow, phase),
/Graph identity reconciliation/,
);
} else {
assert.equal(
(await reconcileGraphNodeIdentities(graph, adapter.executeQuery, phase)).nodes,
(await reconcileGraphNodeIdentities(graph, adapter.forEachQueryRow, phase)).nodes,
size.nodes,
);
}

View file

@ -0,0 +1,143 @@
// Build the runtime, then: node --expose-gc --max-old-space-size=4096 memory.mjs CONFIG.json
// Config: { runtime, storage, output, count, width, mode: "seed"|"measure",
// reader: "baseline"|"stream", iterations }. Each measurement is a fresh process.
// No forced GC between reads. Initial/final GC measure retained graph memory only.
import fs from 'node:fs';
import path from 'node:path';
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { pathToFileURL } from 'node:url';
const config = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const { runtime, storage, output, count, width = 0, mode, reader, iterations } = config;
assert(Number.isSafeInteger(count) && count >= 0);
assert(Number.isSafeInteger(width) && width >= 0);
assert(Number.isSafeInteger(iterations) && iterations > 0);
assert(['seed', 'measure'].includes(mode));
assert(['baseline', 'stream'].includes(reader));
assert(global.gc, 'Run with --expose-gc');
const bufferPoolBytes = (mode === 'seed' ? 1024 : 512) * 1048576;
process.env.GITNEXUS_LBUG_BUFFER_POOL_SIZE = String(bufferPoolBytes);
process.env.GITNEXUS_SHARED_STORE = 'off';
process.env.GITNEXUS_HOME = path.join(path.dirname(output), 'home');
const source = (file) => import(pathToFileURL(path.join(runtime, 'gitnexus/dist', file)).href);
const { createKnowledgeGraph } = await source('core/graph/graph.js');
const { reconcileGraphNodeIdentities } = await source('core/incremental/write-reconciliation.js');
const adapter = await source('core/lbug/lbug-adapter.js');
const graph = createKnowledgeGraph();
const padding = 'x'.repeat(width);
const fingerprint = createHash('sha256');
let identityBytes = 0;
for (let i = 0; i < count; i++) {
const index = String(i).padStart(7, '0');
const id = `Function:node${index}${padding}`;
const name = `fn${index}${padding}`;
const filePath = `src/file${index}.ts${padding}`;
const startLine = (i % 32) * 4;
const endLine = startLine + 2;
identityBytes += Buffer.byteLength(id) + Buffer.byteLength(name) + Buffer.byteLength(filePath);
fingerprint.update(JSON.stringify([id, name, filePath, startLine, endLine]) + '\n');
graph.addNode({ id, label: 'Function', properties: { name, filePath, startLine, endLine } });
}
const result = {
config,
node: process.version,
driver: JSON.parse(
fs.readFileSync(path.join(runtime, 'gitnexus/node_modules/@ladybugdb/core/package.json')),
).version,
bufferPoolBytes,
identityBytes,
identityFingerprint: fingerprint.digest('hex'),
iterations: [],
completed: false,
};
let peakRss = 0,
peakHeap = 0,
currentPeakRss = 0,
currentPeakHeap = 0;
function sample() {
const memory = process.memoryUsage();
peakRss = Math.max(peakRss, memory.rss);
peakHeap = Math.max(peakHeap, memory.heapUsed);
currentPeakRss = Math.max(currentPeakRss, memory.rss);
currentPeakHeap = Math.max(currentPeakHeap, memory.heapUsed);
return memory;
}
function save() {
result.peakRssBytes = Math.max(peakRss, process.resourceUsage().maxRSS * 1024);
result.sampledPeakHeapBytes = peakHeap;
fs.writeFileSync(output, JSON.stringify(result, null, 2) + '\n');
}
let timer;
try {
await adapter.initLbug(path.join(storage, 'lbug'), { skipFts: true });
if (mode === 'seed') {
await adapter.loadGraphToLbug(graph, storage, storage, undefined, undefined, undefined, 'none');
await adapter.tryFlushWAL();
} else {
global.gc();
result.initial = sample();
timer = setInterval(sample, 20);
timer.unref();
for (let i = 0; i < iterations; i++) {
currentPeakRss = 0;
currentPeakHeap = 0;
const before = sample();
let queries = 0;
const readRows =
reader === 'baseline'
? async (query) => {
queries++;
sample();
const rows = await adapter.executeQuery(query);
sample();
return rows;
}
: async (query, visit) => {
queries++;
sample();
await adapter.forEachQueryRow(query, visit);
sample();
};
const start = performance.now();
const receipt = await reconcileGraphNodeIdentities(graph, readRows, 'memory-scaling');
const ms = performance.now() - start;
assert.equal(receipt.nodes, count);
assert.equal(queries, receipt.tables);
const after = sample();
result.iterations.push({
i,
ms,
queries,
verifiedNodes: receipt.nodes,
before,
after,
peakRssBytes: currentPeakRss,
sampledPeakHeapBytes: currentPeakHeap,
maxRssKiB: process.resourceUsage().maxRSS,
});
save();
}
clearInterval(timer);
global.gc();
result.finalAfterGc = sample();
}
result.completed = true;
} catch (error) {
result.error = String(error);
process.exitCode = 1;
} finally {
clearInterval(timer);
await adapter.closeLbug();
save();
}
console.log(
JSON.stringify({
count,
width,
reader,
completed: result.completed,
iterations: result.iterations.length,
peakRssMiB: result.peakRssBytes / 1048576,
}),
);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,109 @@
import assert from 'node:assert/strict';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
const [baseline, candidate] = process.argv.slice(2);
assert(baseline && candidate, 'Expected BASE_RUNTIME CANDIDATE_RUNTIME');
const moduleAt = (root) =>
pathToFileURL(path.resolve(root, 'gitnexus/dist/_shared/scope-resolution/name-claims.js')).href;
const { lookupLexicalName: before } = await import(moduleAt(baseline));
const { lookupLexicalName: after } = await import(moduleAt(candidate));
let seed = 48161;
const random = (n) => {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
return seed % n;
};
const pos = (n) => ({ startLine: n, startCol: 0 });
const range = (n) => ({ ...pos(n), endLine: n, endCol: 20 });
const names = ['a', 'b', 'missing'];
let comparisons = 0;
for (let trial = 0; trial < 3000; trial++) {
const scopes = new Map();
for (let i = 0; i < 5; i++) {
const claims = [];
const bindings = new Map();
const types = new Map();
const imports = [];
for (const name of names.slice(0, 2)) {
if (random(3) === 0)
claims.push({
name,
kind: ['binding', 'import', 'blocked'][random(3)],
range: range(2 + random(8)),
purpose: ['value', 'type', 'both', undefined][random(4)],
availableFrom: random(2) ? pos(2 + random(8)) : undefined,
inactive: ['outer', 'blocked', 'module'][random(3)],
redirect: random(8) === 0 ? 'module' : undefined,
hoisted: random(2) === 0,
merge: random(3) === 0,
});
if (random(2))
bindings.set(name, [
{
def: { nodeId: `${i}:${name}`, type: 'Function', filePath: 'a.ts' },
origin: random(2) ? 'local' : 'import',
availableFrom: random(2) ? pos(5) : undefined,
declarationRange: range(5),
},
]);
if (random(3) === 0)
imports.push({
kind: 'namespace',
localName: name,
targetFile: random(2) ? 'b.ts' : null,
targetExportedName: '',
atRange: range(4),
linkStatus: random(2) ? 'resolved' : 'unresolved',
});
if (random(3) === 0)
types.set(name, {
rawName: 'Value',
source: [
'annotation',
'constructor-inferred',
'assignment-inferred',
'receiver-propagated',
][random(4)],
declaredAtScope: String(i),
bindingRange: random(2) ? range(5) : undefined,
});
}
scopes.set(String(i), {
id: String(i),
parent: i === 0 ? null : String(i - 1),
kind: i === 0 ? 'Module' : ['Function', 'Block', 'Class', 'Object'][random(4)],
filePath: 'a.ts',
range: range(i),
bindings,
imports,
ownedDefs: [],
typeBindings: types,
nameClaims: claims,
lexicalNames: random(3) === 0 ? new Set(['a']) : undefined,
ownsReceivers: random(3) === 0 ? new Set(['b']) : undefined,
lookupPolicy: {
nameClaimsComplete: random(2) === 0,
deferParentActivation: random(2) === 0,
skipFromChildren: random(4) === 0,
},
});
}
if (trial % 20 === 0) scopes.get('0').parent = '4';
const sources = { scopes: { getScope: (id) => scopes.get(id) } };
for (const name of names)
for (const purpose of ['value', 'type'])
for (const position of [undefined, pos(1), pos(6), pos(12)]) {
const options = Object.freeze({
purpose,
position,
skipEnclosingClasses: trial % 2 === 0,
preservePosition: trial % 3 === 0,
});
assert.deepEqual(
after('4', name, sources, options),
before('4', name, sources, options),
`Trial ${trial}, ${name}, ${purpose}, line ${position?.startLine}`,
);
comparisons++;
}
}
console.log({ comparisons, status: 'passed' });

View file

@ -0,0 +1,306 @@
// Build both runtimes, then run with Node:
// measure.mjs BASE_RUNTIME CANDIDATE_RUNTIME BASE_CORPUS CANDIDATE_CORPUS OUTPUT [fresh|persistent]
// Corpus checkouts must be clean/disposable and at the same commit. OUTPUT must not exist.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { createHash } from 'node:crypto';
import { execFileSync, fork } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const args = process.argv.slice(2);
assert(
args.length >= 5 && args.slice(0, 5).every((value) => value !== ''),
'Expected BASE_RUNTIME CANDIDATE_RUNTIME BASE_CORPUS CANDIDATE_CORPUS OUTPUT [fresh|persistent]',
);
const [baselineRuntime, candidateRuntime, baseline, candidate, output] = args
.slice(0, 5)
.map((value) => path.resolve(value));
const lifecycle = args[5] ?? 'fresh';
const workers = Number(process.env.GITNEXUS_LEXICAL_BENCH_WORKERS ?? 16);
const heapMiB = Number(process.env.GITNEXUS_LEXICAL_BENCH_HEAP_MIB ?? 8192);
const screenSetting = process.env.GITNEXUS_LEXICAL_BENCH_SCREEN ?? '1';
const baseFirstSetting = process.env.GITNEXUS_LEXICAL_BENCH_BASE_FIRST ?? '1';
assert(Number.isSafeInteger(workers) && workers > 0, 'Workers must be a positive integer');
assert(Number.isSafeInteger(heapMiB) && heapMiB > 0, 'Heap MiB must be a positive integer');
assert(['0', '1'].includes(screenSetting), 'Screen must be 0 or 1');
assert(['0', '1'].includes(baseFirstSetting), 'Base first must be 0 or 1');
const screen = screenSetting === '1';
const baseFirst = baseFirstSetting === '1';
assert(
['fresh', 'persistent'].includes(lifecycle),
`Lifecycle must be fresh or persistent; received ${JSON.stringify(lifecycle)}`,
);
assert.notEqual(baseline, candidate, 'BASE_CORPUS and CANDIDATE_CORPUS must differ');
const isWorker = process.env.GITNEXUS_LEXICAL_BENCH_WORKER === '1';
let runFullAnalysis, initLbug, executeQuery, closeLbug;
if (!isWorker) assert(!fs.existsSync(output), 'Output already exists');
const control = 'gitnexus/src/semantic-bench-control.ts';
const source =
'export class SemanticBenchStore { save(): void {} load(): void {} }\nexport function semanticBenchUse(s: SemanticBenchStore) { s.save(); }\n';
const head = (repo) =>
execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim();
assert.equal(head(baseline), head(candidate));
for (const repo of isWorker ? [] : [baseline, candidate]) {
assert.equal(
execFileSync('git', ['status', '--porcelain'], { cwd: repo, encoding: 'utf8' }),
'',
'Checkout must be clean',
);
assert(!fs.existsSync(path.join(repo, control)));
}
fs.mkdirSync(output, { recursive: true });
process.env.GITNEXUS_HOME = path.join(output, 'home');
process.env.GITNEXUS_STORAGE_ROOT = path.join(output, 'indexes');
delete process.env.GITNEXUS_STORAGE_PATH;
process.env.GITNEXUS_SHARED_STORE = 'off';
process.env.PROF_SCOPE_RESOLUTION = '1';
const records = [];
if (isWorker) {
const runtime = process.env.GITNEXUS_BENCH_RUNTIME;
({ runFullAnalysis } = await import(path.join(runtime, 'gitnexus/dist/core/run-analyze.js')));
({ initLbug, executeQuery, closeLbug } = await import(
path.join(runtime, 'gitnexus/dist/core/lbug/lbug-adapter.js')
));
const { configureLoggerSink } = await import(path.join(runtime, 'gitnexus/dist/core/logger.js'));
configureLoggerSink({
destination: {
write(line) {
try {
records.push(JSON.parse(line));
} catch {
/* non-JSON diagnostic */
}
process.stderr.write(line);
},
},
});
}
const results = [];
const canonical = (value) =>
Array.isArray(value)
? value.map(canonical)
: value && typeof value === 'object'
? Object.fromEntries(
Object.keys(value)
.sort()
.map((key) => [key, canonical(value[key])]),
)
: value;
const digest = (rows) => {
const hash = createHash('sha256');
for (const row of rows.sort()) hash.update(row).update('\n');
return { count: rows.length, sha256: hash.digest('hex') };
};
async function sample(repo, isCandidate, name) {
process.env.GITNEXUS_EXPERIMENTAL_SEMANTIC_CACHE = '0';
global.gc?.();
records.length = 0;
const logs = [];
const progress = [];
const loadBefore = os.loadavg();
const started = performance.now();
const result = await runFullAnalysis(
repo,
{
skipAgentsMd: true,
skipSkills: true,
workerPoolSize: workers,
verbose: true,
},
{
onProgress(phase, percent, message) {
if (progress.at(-1)?.phase !== phase)
progress.push({ ms: performance.now() - started, phase, percent, message });
},
onLog(message) {
logs.push(message);
},
},
);
const ms = performance.now() - started;
assert(!result.alreadyUpToDate, 'A no-op is not an edit benchmark');
assert(!result.graphWriteCollapsed);
assert(!result.ftsSkipped, 'This benchmark includes FTS');
const graph = result.pipelineResult.graph;
const nodes = [],
edges = [];
for (const node of graph.iterNodes()) {
// Derived clustering is stochastic; compare all source/semantic properties.
if (['Community', 'Process', 'Folder'].includes(node.label)) continue;
nodes.push(JSON.stringify(canonical(node)));
}
let controlCalls = 0;
for (const edge of graph.iterRelationships()) {
if (['MEMBER_OF', 'STEP_IN_PROCESS', 'CONTAINS'].includes(edge.type)) continue;
edges.push(JSON.stringify(canonical(edge)));
if (edge.type === 'CALLS' && edge.sourceId.includes('semanticBenchUse')) controlCalls++;
}
assert(controlCalls > 0, 'The changed call must resolve');
const expectedTarget = fs.readFileSync(path.join(repo, control), 'utf8').includes('s.load()')
? 'load'
: 'save';
let persistedTargets;
try {
await initLbug(path.join(result.storagePath, 'lbug'), { skipFts: true });
persistedTargets = await executeQuery(
"MATCH (a:Function)-[r:CodeRelation]->(b:Method) WHERE a.name = 'semanticBenchUse' AND r.type = 'CALLS' RETURN b.name AS target",
);
assert.deepEqual(
persistedTargets.map((row) => row.target),
[expectedTarget],
'Persisted call must follow the edit',
);
} finally {
await closeLbug();
}
const report = {
name,
variant: isCandidate ? 'candidate' : 'baseline',
ms,
loadBefore,
loadAfter: os.loadavg(),
maxRssKiB: process.resourceUsage().maxRSS,
stats: result.stats,
incremental: result.incrementalStats,
rebuildReasons: result.rebuildReasons,
reparsedFiles: result.pipelineResult.reparsedFileCount,
cachedFiles: result.pipelineResult.parseCacheHitFileCount,
graph: { nodes: digest(nodes), edges: digest(edges) },
logs,
progress,
persistedTargets,
records: [...records],
};
console.log(
'LEXICAL_BENCH ' +
JSON.stringify({ name, ms, incremental: report.incremental, graph: report.graph }),
);
return report;
}
// Separate runtimes isolate implementation and JIT state. Fresh mode exits
// after every sample; persistent mode retains a process across real edits.
function worker(repo, isCandidate) {
let child, pending, exited;
const start = () => {
child = fork(fileURLToPath(import.meta.url), process.argv.slice(2), {
execArgv: [`--max-old-space-size=${heapMiB}`, '--expose-gc'],
env: {
...process.env,
GITNEXUS_LEXICAL_BENCH_WORKER: '1',
GITNEXUS_BENCH_RUNTIME: isCandidate ? candidateRuntime : baselineRuntime,
},
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
});
exited = new Promise((resolve) => child.once('exit', resolve));
child.on('message', (message) => {
const current = pending;
pending = undefined;
if (message.error) current?.reject(new Error(message.error));
else current?.resolve(message.report);
});
child.on('error', (error) => pending?.reject(error));
child.on('exit', (code) => pending?.reject(new Error(`Benchmark worker exited: ${code}`)));
};
const close = async () => {
if (!child) return;
if (child.connected) child.disconnect();
const code = await exited;
child = undefined;
assert.equal(code, 0, 'Worker must exit normally after analysis');
};
return {
async run(name) {
assert(!pending, 'Samples must run serially');
if (!child) start();
const report = await new Promise((resolve, reject) => {
pending = { resolve, reject };
child.send({ repo, reuse: isCandidate, name });
});
results.push(report);
fs.writeFileSync(
path.join(output, 'results.json'),
JSON.stringify(
{
head: head(repo),
node: process.version,
lifecycle,
workers,
heapMiB,
screen,
baseFirst,
baselineRuntime,
candidateRuntime,
results,
},
null,
2,
),
);
if (lifecycle === 'fresh') await close();
return report;
},
close,
};
}
if (isWorker) {
process.on('message', async ({ repo, reuse: isCandidate, name }) => {
try {
process.send({ report: await sample(repo, isCandidate, name) });
} catch (error) {
process.send({ error: error.stack ?? String(error) });
}
});
} else {
const beforeWorker = worker(baseline, false);
const afterWorker = worker(candidate, true);
try {
for (const repo of [baseline, candidate]) {
fs.mkdirSync(path.dirname(path.join(repo, control)), { recursive: true });
fs.writeFileSync(path.join(repo, control), source);
}
let beforeSeed, afterSeed;
if (baseFirst) {
beforeSeed = await beforeWorker.run('baseline-seed');
afterSeed = await afterWorker.run('candidate-seed');
} else {
afterSeed = await afterWorker.run('candidate-seed');
beforeSeed = await beforeWorker.run('baseline-seed');
}
assert.deepEqual(afterSeed.graph, beforeSeed.graph, 'Seed semantic graphs differ');
for (let pair = 1; pair <= 3; pair++) {
const text = pair % 2 ? source.replace('s.save()', 's.load()') : source;
for (const repo of [baseline, candidate]) fs.writeFileSync(path.join(repo, control), text);
// Alternate ordering to reduce warm-up/load bias. Both indexes see a real edit.
let before, after;
if (Boolean(pair % 2) === baseFirst) {
before = await beforeWorker.run(`baseline-${pair}`);
after = await afterWorker.run(`candidate-${pair}`);
} else {
after = await afterWorker.run(`candidate-${pair}`);
before = await beforeWorker.run(`baseline-${pair}`);
}
assert.deepEqual(
after.graph,
before.graph,
'Semantic graph differs from a full semantic pass',
);
for (const entry of [before, after]) {
assert.equal(entry.incremental?.writeMode, 'incremental');
assert.equal(entry.incremental?.changedFiles, 1);
assert.deepEqual(entry.rebuildReasons, []);
}
if (screen && pair === 1 && after.ms >= before.ms) {
console.log('SCREEN REJECTED: no end-to-end improvement; stopping after one pair.');
break;
}
}
} finally {
const closed = await Promise.allSettled([beforeWorker.close(), afterWorker.close()]);
for (const repo of [baseline, candidate]) fs.rmSync(path.join(repo, control), { force: true });
for (const result of closed) if (result.status === 'rejected') throw result.reason;
}
}

View file

@ -0,0 +1,735 @@
{
"date": "2026-11-02",
"baseCommit": "347cc99b4a237f9a71c0084167bcbcbd9d06e610",
"node": "v22.23.3",
"ladybug": "0.22.0",
"hardware": {
"cpu": "12th Gen Intel(R) Core(TM) i9-12900K",
"logicalCpus": 24,
"platform": "linux",
"arch": "x64"
},
"settings": {
"workers": 16,
"heapMiB": 8192,
"sharedHost": true,
"scopePhaseLogging": true,
"cpuProfile": false,
"independentColdSeeds": true
},
"measurement": {
"timed": "runFullAnalysis including graph persistence and FTS",
"excluded": ["process startup", "graph hashing", "post-analysis persisted target query"],
"editOrder": [
"baseline-1",
"candidate-1",
"candidate-2",
"baseline-2",
"baseline-3",
"candidate-3"
],
"graphExclusions": [
"Folder",
"Community",
"Process",
"CONTAINS",
"MEMBER_OF",
"STEP_IN_PROCESS"
],
"limitations": [
"three fresh-process pairs and one repeated-process pair on a shared host",
"graph hashes are post-persistence; source content may have been cleared",
"persistent process exercises repeated analysis, not watch event scheduling",
"baseline resolution limits also apply to candidate",
"process maxRSS includes earlier samples in persistent mode"
]
},
"pin": {
"patchSha256": "a61b37f5ab42a95a62ae96510a5e285cb9b835a3f983ce4a96ecd0413504ea43",
"runtimes": {
"baseline": {
"gitnexus/dist/_shared/scope-resolution/name-claims.js": "157fd1ff2870468d335f97fff41d0b002f4cad749db64634aa48469053d7cea3",
"gitnexus/dist/core/ingestion/scope-resolution/scope/walkers.js": "15dba69d6ec871e43f4d0d1aadbad93e1645c13a9ad2e664e9f9bd679f73e55c"
},
"candidate": {
"gitnexus/dist/_shared/scope-resolution/name-claims.js": "6f998066bcb3f17d69f2dbae7dba1a1d5af865bc8798d4b137aaed9387a81284",
"gitnexus/dist/core/ingestion/scope-resolution/scope/walkers.js": "9400ed640a22343f6cc54e3515944af664022ac7f4a8c4306da0cc95df37f9e1"
}
}
},
"harnessSha256": "dbd428561cd614b8f0fb2dda48ad5445e214bea02a9c39f201cc2488bd7a81c8",
"harnessProvenance": "Checksums identify the scripts used for measurement, before repository formatting, preflight/cleanup fixes and configurable CI worker/heap limits. Local resource defaults are unchanged.",
"differential": {
"seed": 48161,
"comparisons": 72000,
"status": "passed",
"harnessSha256": "8a34834c153531daa9f6000e178fc87458e48d3507c8c6e20e2738f739413e88"
},
"runs": {
"fresh": {
"rawResultSha256": "3da8095b17ecbb5099164220152f8c9582268868db6a7d23b2565d987f2a7d6d",
"samples": [
{
"name": "baseline-seed",
"variant": "baseline",
"ms": 218218.857255,
"loadBefore": [19.08, 16.18, 14.03],
"loadAfter": [13.58, 16.53, 14.82],
"maxRssKiB": 5929416,
"rebuildReasons": [],
"reparsedFiles": 3120,
"cachedFiles": 0,
"phaseMs": {
"extracting": 307.5897699999996,
"structure": 59.0564260000001,
"parsing": 77980.037363,
"scopeResolution": 80390.527192,
"enriching": 36.09870200001751,
"communities": 2342.1585379999888,
"processes": 659.8627029999916,
"complete": 0.06783200000063516,
"lbug": 17391.16107600002,
"fts": 29286.008015999978,
"done": 6083.309720000019,
"beforeFirstProgress": 3682.979917
},
"scopeLogs": [
"[scope-resolution prof] extract=21ms finalize=71ms propagate=78ms rangeBind=0.0ms resolve=175ms emit=1547ms total=1891ms (110 files)",
"[scope-resolution prof] extract=2649ms finalize=1451ms propagate=1426ms rangeBind=0.0ms resolve=7468ms emit=41291ms total=54285ms (2893 files)",
"[scope-resolution prof] extract=270ms finalize=65ms propagate=61ms rangeBind=0.0ms resolve=444ms emit=1835ms total=2675ms (113 files)"
],
"graph": {
"nodes": {
"count": 114413,
"sha256": "d2e2ee693005fee4770a654b1391f5348d7b03316d57bf0e7bceb4cf6787f9ad"
},
"edges": {
"count": 318798,
"sha256": "595afccf9f3fa2725727f3439cb5fd7b1aadd18ae6aca4eaa478ef00b793fe21"
}
},
"persistedTargets": [
{
"target": "save"
}
]
},
{
"name": "candidate-seed",
"variant": "candidate",
"ms": 187147.92687899998,
"loadBefore": [13.13, 16.39, 14.78],
"loadAfter": [11.64, 15.07, 14.65],
"maxRssKiB": 5912680,
"rebuildReasons": [],
"reparsedFiles": 3120,
"cachedFiles": 0,
"phaseMs": {
"extracting": 248.5604829999993,
"structure": 55.20889800000077,
"parsing": 62435.270292,
"scopeResolution": 61285.705398999984,
"enriching": 31.93969999998808,
"communities": 2074.6883160000143,
"processes": 544.6516690000135,
"complete": 0.05536599998595193,
"lbug": 16684.85912899999,
"fts": 29567.160881000018,
"done": 5673.711943999981,
"beforeFirstProgress": 8546.114802
},
"scopeLogs": [
"[scope-resolution prof] extract=18ms finalize=60ms propagate=52ms rangeBind=0.0ms resolve=118ms emit=1176ms total=1425ms (110 files)",
"[scope-resolution prof] extract=2048ms finalize=1153ms propagate=1045ms rangeBind=0.0ms resolve=5119ms emit=31736ms total=41101ms (2893 files)",
"[scope-resolution prof] extract=201ms finalize=62ms propagate=64ms rangeBind=0.0ms resolve=282ms emit=1469ms total=2078ms (113 files)"
],
"graph": {
"nodes": {
"count": 114413,
"sha256": "d2e2ee693005fee4770a654b1391f5348d7b03316d57bf0e7bceb4cf6787f9ad"
},
"edges": {
"count": 318798,
"sha256": "595afccf9f3fa2725727f3439cb5fd7b1aadd18ae6aca4eaa478ef00b793fe21"
}
},
"persistedTargets": [
{
"target": "save"
}
]
},
{
"name": "baseline-1",
"variant": "baseline",
"ms": 122065.065072,
"loadBefore": [11.43, 14.97, 14.62],
"loadAfter": [8.47, 12.61, 13.79],
"maxRssKiB": 4474272,
"incremental": {
"changedFiles": 1,
"reparsedFiles": 22,
"affectedDependents": 0,
"deletedFiles": 0,
"writeMode": "incremental"
},
"rebuildReasons": [],
"reparsedFiles": 22,
"cachedFiles": 3098,
"phaseMs": {
"extracting": 221.9082469999994,
"structure": 52.67533299999923,
"parsing": 8765.232970000001,
"scopeResolution": 65945.25450899999,
"enriching": 121.54619799999637,
"complete": 0.25131200000760145,
"lbug": 10923.801725000012,
"communities": 2019.391571,
"processes": 5888.645534999989,
"fts": 13240.190296,
"done": 8270.738006,
"beforeFirstProgress": 6615.429370000001
},
"scopeLogs": [
"[scope-resolution prof] extract=19ms finalize=60ms propagate=58ms rangeBind=0.0ms resolve=148ms emit=1294ms total=1578ms (110 files)",
"[scope-resolution prof] extract=2031ms finalize=1178ms propagate=1108ms rangeBind=0.0ms resolve=6717ms emit=35251ms total=46284ms (2893 files)",
"[scope-resolution prof] extract=232ms finalize=61ms propagate=59ms rangeBind=0.0ms resolve=379ms emit=1552ms total=2283ms (113 files)"
],
"graph": {
"nodes": {
"count": 114413,
"sha256": "d2e2ee693005fee4770a654b1391f5348d7b03316d57bf0e7bceb4cf6787f9ad"
},
"edges": {
"count": 318798,
"sha256": "4ece58529c66b824a32ee5b1c3f642193279f98e39d95eea53bf2f5ec1700ae8"
}
},
"persistedTargets": [
{
"target": "load"
}
]
},
{
"name": "candidate-1",
"variant": "candidate",
"ms": 111134.852017,
"loadBefore": [8.47, 12.61, 13.79],
"loadAfter": [7.79, 11.1, 13.1],
"maxRssKiB": 4406608,
"incremental": {
"changedFiles": 1,
"reparsedFiles": 22,
"affectedDependents": 0,
"deletedFiles": 0,
"writeMode": "incremental"
},
"rebuildReasons": [],
"reparsedFiles": 22,
"cachedFiles": 3098,
"phaseMs": {
"extracting": 228.39688900000056,
"structure": 58.946551,
"parsing": 7914.542683,
"scopeResolution": 62759.170073999994,
"enriching": 126.18967100000009,
"complete": 0.17001499999605585,
"lbug": 9233.545855000018,
"communities": 1969.2113839999947,
"processes": 1475.3109669999976,
"fts": 12857.877876999992,
"done": 8347.120962,
"beforeFirstProgress": 6164.369089
},
"scopeLogs": [
"[scope-resolution prof] extract=15ms finalize=57ms propagate=51ms rangeBind=0.0ms resolve=115ms emit=1221ms total=1459ms (110 files)",
"[scope-resolution prof] extract=2192ms finalize=1268ms propagate=1122ms rangeBind=0.0ms resolve=5371ms emit=33985ms total=43937ms (2893 files)",
"[scope-resolution prof] extract=176ms finalize=75ms propagate=55ms rangeBind=0.0ms resolve=253ms emit=1333ms total=1892ms (113 files)"
],
"graph": {
"nodes": {
"count": 114413,
"sha256": "d2e2ee693005fee4770a654b1391f5348d7b03316d57bf0e7bceb4cf6787f9ad"
},
"edges": {
"count": 318798,
"sha256": "4ece58529c66b824a32ee5b1c3f642193279f98e39d95eea53bf2f5ec1700ae8"
}
},
"persistedTargets": [
{
"target": "load"
}
]
},
{
"name": "candidate-2",
"variant": "candidate",
"ms": 101730.29475,
"loadBefore": [7.79, 11.1, 13.1],
"loadAfter": [8.7, 10.56, 12.7],
"maxRssKiB": 4447528,
"incremental": {
"changedFiles": 1,
"reparsedFiles": 22,
"affectedDependents": 0,
"deletedFiles": 0,
"writeMode": "incremental"
},
"rebuildReasons": [],
"reparsedFiles": 22,
"cachedFiles": 3098,
"phaseMs": {
"extracting": 227.87885000000006,
"structure": 50.07236400000011,
"parsing": 6870.926647,
"scopeResolution": 58457.57910199999,
"enriching": 123.74990100000286,
"complete": 0.12248400000680704,
"lbug": 9597.999660000001,
"communities": 1855.2971499999985,
"processes": 1319.511560999992,
"fts": 12526.206298000005,
"done": 8028.759919000004,
"beforeFirstProgress": 2672.190814
},
"scopeLogs": [
"[scope-resolution prof] extract=14ms finalize=61ms propagate=47ms rangeBind=0.0ms resolve=103ms emit=1077ms total=1302ms (110 files)",
"[scope-resolution prof] extract=1860ms finalize=1052ms propagate=1064ms rangeBind=0.0ms resolve=4989ms emit=31810ms total=40774ms (2893 files)",
"[scope-resolution prof] extract=189ms finalize=62ms propagate=52ms rangeBind=0.0ms resolve=258ms emit=1383ms total=1943ms (113 files)"
],
"graph": {
"nodes": {
"count": 114413,
"sha256": "d2e2ee693005fee4770a654b1391f5348d7b03316d57bf0e7bceb4cf6787f9ad"
},
"edges": {
"count": 318798,
"sha256": "595afccf9f3fa2725727f3439cb5fd7b1aadd18ae6aca4eaa478ef00b793fe21"
}
},
"persistedTargets": [
{
"target": "save"
}
]
},
{
"name": "baseline-2",
"variant": "baseline",
"ms": 114005.953714,
"loadBefore": [8.7, 10.56, 12.7],
"loadAfter": [9.5, 10.18, 12.3],
"maxRssKiB": 4501784,
"incremental": {
"changedFiles": 1,
"reparsedFiles": 22,
"affectedDependents": 0,
"deletedFiles": 0,
"writeMode": "incremental"
},
"rebuildReasons": [],
"reparsedFiles": 22,
"cachedFiles": 3098,
"phaseMs": {
"extracting": 220.18439399999988,
"structure": 56.05602999999974,
"parsing": 6693.682359,
"scopeResolution": 68196.749623,
"enriching": 136.36504800000694,
"complete": 0.24019299999054056,
"lbug": 10199.119456000015,
"communities": 1921.6023549999954,
"processes": 1269.8549959999946,
"fts": 12961.042190000007,
"done": 8423.348173999999,
"beforeFirstProgress": 3927.708896
},
"scopeLogs": [
"[scope-resolution prof] extract=15ms finalize=63ms propagate=59ms rangeBind=0.0ms resolve=129ms emit=1239ms total=1506ms (110 files)",
"[scope-resolution prof] extract=2317ms finalize=1312ms propagate=1295ms rangeBind=0.0ms resolve=7098ms emit=36604ms total=48627ms (2893 files)",
"[scope-resolution prof] extract=225ms finalize=58ms propagate=59ms rangeBind=0.0ms resolve=366ms emit=1623ms total=2331ms (113 files)"
],
"graph": {
"nodes": {
"count": 114413,
"sha256": "d2e2ee693005fee4770a654b1391f5348d7b03316d57bf0e7bceb4cf6787f9ad"
},
"edges": {
"count": 318798,
"sha256": "595afccf9f3fa2725727f3439cb5fd7b1aadd18ae6aca4eaa478ef00b793fe21"
}
},
"persistedTargets": [
{
"target": "save"
}
]
},
{
"name": "baseline-3",
"variant": "baseline",
"ms": 131285.117027,
"loadBefore": [9.94, 10.26, 12.32],
"loadAfter": [11.35, 10.88, 12.28],
"maxRssKiB": 4423784,
"incremental": {
"changedFiles": 1,
"reparsedFiles": 22,
"affectedDependents": 0,
"deletedFiles": 0,
"writeMode": "incremental"
},
"rebuildReasons": [],
"reparsedFiles": 22,
"cachedFiles": 3098,
"phaseMs": {
"extracting": 246.84667200000013,
"structure": 55.95557000000008,
"parsing": 7195.49225,
"scopeResolution": 72694.88498100001,
"enriching": 136.15589299998828,
"complete": 0.1802390000084415,
"lbug": 13022.899677999987,
"communities": 2414.3188599999994,
"processes": 1639.1324740000127,
"fts": 14523.853445,
"done": 16550.80450099999,
"beforeFirstProgress": 2804.592464
},
"scopeLogs": [
"[scope-resolution prof] extract=17ms finalize=67ms propagate=66ms rangeBind=0.0ms resolve=138ms emit=1370ms total=1658ms (110 files)",
"[scope-resolution prof] extract=2113ms finalize=1144ms propagate=1129ms rangeBind=0.0ms resolve=7406ms emit=40471ms total=52262ms (2893 files)",
"[scope-resolution prof] extract=255ms finalize=74ms propagate=68ms rangeBind=0.0ms resolve=393ms emit=1821ms total=2611ms (113 files)"
],
"graph": {
"nodes": {
"count": 114413,
"sha256": "d2e2ee693005fee4770a654b1391f5348d7b03316d57bf0e7bceb4cf6787f9ad"
},
"edges": {
"count": 318798,
"sha256": "4ece58529c66b824a32ee5b1c3f642193279f98e39d95eea53bf2f5ec1700ae8"
}
},
"persistedTargets": [
{
"target": "load"
}
]
},
{
"name": "candidate-3",
"variant": "candidate",
"ms": 111303.319634,
"loadBefore": [11, 10.82, 12.25],
"loadAfter": [8.98, 10.27, 11.9],
"maxRssKiB": 4458828,
"incremental": {
"changedFiles": 1,
"reparsedFiles": 22,
"affectedDependents": 0,
"deletedFiles": 0,
"writeMode": "incremental"
},
"rebuildReasons": [],
"reparsedFiles": 22,
"cachedFiles": 3098,
"phaseMs": {
"extracting": 224.80108500000006,
"structure": 54.754461999999876,
"parsing": 9906.336681,
"scopeResolution": 62344.37333,
"enriching": 117.25856000000203,
"complete": 0.1237049999908777,
"lbug": 9196.625813000006,
"communities": 2015.0293479999964,
"processes": 3180.0593300000037,
"fts": 12870.898965,
"done": 7974.200893999994,
"beforeFirstProgress": 3418.8574609999996
},
"scopeLogs": [
"[scope-resolution prof] extract=22ms finalize=74ms propagate=61ms rangeBind=0.0ms resolve=115ms emit=1269ms total=1540ms (110 files)",
"[scope-resolution prof] extract=2053ms finalize=1125ms propagate=1129ms rangeBind=0.0ms resolve=5562ms emit=32778ms total=42647ms (2893 files)",
"[scope-resolution prof] extract=215ms finalize=61ms propagate=54ms rangeBind=0.0ms resolve=267ms emit=1445ms total=2041ms (113 files)"
],
"graph": {
"nodes": {
"count": 114413,
"sha256": "d2e2ee693005fee4770a654b1391f5348d7b03316d57bf0e7bceb4cf6787f9ad"
},
"edges": {
"count": 318798,
"sha256": "4ece58529c66b824a32ee5b1c3f642193279f98e39d95eea53bf2f5ec1700ae8"
}
},
"persistedTargets": [
{
"target": "load"
}
]
}
],
"summary": {
"baseline": {
"editCount": 3,
"medianMs": 122065.065072,
"sumMs": 367356.13581300003,
"medianScopeMs": 68196.749623,
"maxRssKiB": 4501784
},
"candidate": {
"editCount": 3,
"medianMs": 111134.852017,
"sumMs": 324168.466401,
"medianScopeMs": 62344.37333,
"maxRssKiB": 4458828
},
"medianReductionPercent": 8.954415457488041,
"aggregateReductionPercent": 11.756349003514243,
"scopeMedianReductionPercent": 8.581605905490585
}
},
"persistent": {
"rawResultSha256": "b09e4f487c1933281a4f875620b5ac5a23794e56246d6c317dd6d3196762293e",
"samples": [
{
"name": "baseline-seed",
"variant": "baseline",
"ms": 158147.924033,
"loadBefore": [3.28, 6.72, 10.14],
"loadAfter": [5.3, 7.19, 9.87],
"maxRssKiB": 5757604,
"rebuildReasons": [],
"reparsedFiles": 3120,
"cachedFiles": 0,
"phaseMs": {
"extracting": 205.49587699999938,
"structure": 49.78513100000055,
"parsing": 58223.494267999995,
"scopeResolution": 62109.850408000006,
"enriching": 31.760779000003822,
"communities": 1872.6300760000013,
"processes": 506.77787499999977,
"complete": 0.04477599999518134,
"lbug": 10664.06607499998,
"fts": 18818.957672999997,
"done": 2247.716622000007,
"beforeFirstProgress": 3417.344473
},
"scopeLogs": [
"[scope-resolution prof] extract=16ms finalize=59ms propagate=66ms rangeBind=0.0ms resolve=132ms emit=1181ms total=1454ms (110 files)",
"[scope-resolution prof] extract=1988ms finalize=1096ms propagate=1099ms rangeBind=0.0ms resolve=6393ms emit=33758ms total=44335ms (2893 files)",
"[scope-resolution prof] extract=168ms finalize=64ms propagate=50ms rangeBind=0.0ms resolve=322ms emit=1375ms total=1978ms (113 files)"
],
"graph": {
"nodes": {
"count": 114413,
"sha256": "d2e2ee693005fee4770a654b1391f5348d7b03316d57bf0e7bceb4cf6787f9ad"
},
"edges": {
"count": 318798,
"sha256": "595afccf9f3fa2725727f3439cb5fd7b1aadd18ae6aca4eaa478ef00b793fe21"
}
},
"persistedTargets": [
{
"target": "save"
}
]
},
{
"name": "candidate-seed",
"variant": "candidate",
"ms": 153951.283825,
"loadBefore": [5.3, 7.19, 9.87],
"loadAfter": [6.35, 7.83, 9.77],
"maxRssKiB": 5734340,
"rebuildReasons": [],
"reparsedFiles": 3120,
"cachedFiles": 0,
"phaseMs": {
"extracting": 226.53108299999963,
"structure": 46.397985999999946,
"parsing": 58254.662231,
"scopeResolution": 61421.754931999996,
"enriching": 35.70321300000069,
"communities": 1928.893007000006,
"processes": 529.0402800000011,
"complete": 0.05887399999483023,
"lbug": 10686.215123000002,
"fts": 16689.450012999994,
"done": 2236.283251999994,
"beforeFirstProgress": 1896.2938310000002
},
"scopeLogs": [
"[scope-resolution prof] extract=16ms finalize=54ms propagate=49ms rangeBind=0.0ms resolve=108ms emit=1134ms total=1361ms (110 files)",
"[scope-resolution prof] extract=2107ms finalize=1253ms propagate=1084ms rangeBind=0.0ms resolve=5107ms emit=32228ms total=41779ms (2893 files)",
"[scope-resolution prof] extract=172ms finalize=65ms propagate=52ms rangeBind=0.0ms resolve=270ms emit=1373ms total=1932ms (113 files)"
],
"graph": {
"nodes": {
"count": 114413,
"sha256": "d2e2ee693005fee4770a654b1391f5348d7b03316d57bf0e7bceb4cf6787f9ad"
},
"edges": {
"count": 318798,
"sha256": "595afccf9f3fa2725727f3439cb5fd7b1aadd18ae6aca4eaa478ef00b793fe21"
}
},
"persistedTargets": [
{
"target": "save"
}
]
},
{
"name": "baseline-1",
"variant": "baseline",
"ms": 109482.844232,
"loadBefore": [6.35, 7.83, 9.77],
"loadAfter": [3.81, 6.46, 9.05],
"maxRssKiB": 5757604,
"incremental": {
"changedFiles": 1,
"reparsedFiles": 22,
"affectedDependents": 0,
"deletedFiles": 0,
"writeMode": "incremental"
},
"rebuildReasons": [],
"reparsedFiles": 22,
"cachedFiles": 3098,
"phaseMs": {
"extracting": 211.5071590000298,
"structure": 41.07579499995336,
"parsing": 7984.799295000033,
"scopeResolution": 67012.942599,
"enriching": 124.94524899998214,
"complete": 0.18859700002940372,
"lbug": 8813.617234999954,
"communities": 1921.8943519999739,
"processes": 1619.4587620000239,
"fts": 10599.878043000004,
"done": 8947.475924000028,
"beforeFirstProgress": 2205.0612219999894
},
"scopeLogs": [
"[scope-resolution prof] extract=12ms finalize=49ms propagate=55ms rangeBind=0.0ms resolve=133ms emit=1216ms total=1467ms (110 files)",
"[scope-resolution prof] extract=2000ms finalize=1107ms propagate=1133ms rangeBind=0.0ms resolve=7186ms emit=37166ms total=48593ms (2893 files)",
"[scope-resolution prof] extract=164ms finalize=57ms propagate=58ms rangeBind=0.0ms resolve=346ms emit=1455ms total=2081ms (113 files)"
],
"graph": {
"nodes": {
"count": 114413,
"sha256": "d2e2ee693005fee4770a654b1391f5348d7b03316d57bf0e7bceb4cf6787f9ad"
},
"edges": {
"count": 318798,
"sha256": "4ece58529c66b824a32ee5b1c3f642193279f98e39d95eea53bf2f5ec1700ae8"
}
},
"persistedTargets": [
{
"target": "load"
}
]
},
{
"name": "candidate-1",
"variant": "candidate",
"ms": 123364.35062099999,
"loadBefore": [3.81, 6.46, 9.05],
"loadAfter": [12.23, 8.31, 9.37],
"maxRssKiB": 5896624,
"incremental": {
"changedFiles": 1,
"reparsedFiles": 22,
"affectedDependents": 0,
"deletedFiles": 0,
"writeMode": "incremental"
},
"rebuildReasons": [],
"reparsedFiles": 22,
"cachedFiles": 3098,
"phaseMs": {
"extracting": 221.45905800000764,
"structure": 46.565340999979526,
"parsing": 7283.348788000003,
"scopeResolution": 65739.48159400001,
"enriching": 129.2759209999931,
"complete": 0.21377500001108274,
"lbug": 12901.81731199997,
"communities": 2457.193612000032,
"processes": 2556.050026999961,
"fts": 14765.58123000001,
"done": 14857.72775999998,
"beforeFirstProgress": 2405.636203000031
},
"scopeLogs": [
"[scope-resolution prof] extract=11ms finalize=47ms propagate=51ms rangeBind=0.0ms resolve=107ms emit=1124ms total=1340ms (110 files)",
"[scope-resolution prof] extract=2046ms finalize=1134ms propagate=1135ms rangeBind=0.0ms resolve=5161ms emit=34994ms total=44470ms (2893 files)",
"[scope-resolution prof] extract=407ms finalize=69ms propagate=71ms rangeBind=0.0ms resolve=369ms emit=2216ms total=3132ms (113 files)"
],
"graph": {
"nodes": {
"count": 114413,
"sha256": "d2e2ee693005fee4770a654b1391f5348d7b03316d57bf0e7bceb4cf6787f9ad"
},
"edges": {
"count": 318798,
"sha256": "4ece58529c66b824a32ee5b1c3f642193279f98e39d95eea53bf2f5ec1700ae8"
}
},
"persistedTargets": [
{
"target": "load"
}
]
}
],
"summary": {
"baseline": {
"editCount": 1,
"medianMs": 109482.844232,
"sumMs": 109482.844232,
"medianScopeMs": 67012.942599,
"maxRssKiB": 5757604
},
"candidate": {
"editCount": 1,
"medianMs": 123364.35062099999,
"sumMs": 123364.35062099999,
"medianScopeMs": 65739.48159400001,
"maxRssKiB": 5896624
},
"medianReductionPercent": -12.679161275335815,
"aggregateReductionPercent": -12.679161275335815,
"scopeMedianReductionPercent": 1.9003209762333162
}
}
},
"decision": "Experimental: fresh-process screen positive; persistent screen stopped after first non-improving pair; benefit for both workflows unproven",
"validation": {
"runtimePinsVerifiedAfterRuns": true,
"focusedTests": {
"files": 6,
"tests": 137,
"status": "passed"
},
"scopeTests": {
"files": 175,
"tests": 3467,
"status": "passed",
"overlapWithFocused": true
},
"differentialComparisons": 72000,
"parameterizedDifferentialRerun": "passed",
"build": "passed for both runtimes",
"testTypecheck": "passed",
"eslint": "passed; existing warnings",
"fullSuite": "not run"
}
}

File diff suppressed because it is too large Load diff

View file

@ -34,14 +34,15 @@ const storedIdentityField = (node: GraphNode, field: string): unknown => {
* retained rows. A write-set-only probe misses corruption of an unchanged row.
* Scan one label at a time, without path/range predicates: a corrupt field must
* not be able to hide its own row. No multi-label scans (#3139), N+1 ID lookups,
* source-content copies, or whole-graph row materialization.
* source-content copies, or JS row-array materialization. The reader calls the
* synchronous visitor for each row and releases its cursor before resolving.
*
* This certifies node identity, not relationships or native storage internals.
* Call after COPY/checkpoint and after FTS/embedding/checkpoint, before metadata.
*/
export async function reconcileGraphNodeIdentities(
graph: KnowledgeGraph,
query: (cypher: string) => Promise<IdentityRow[]>,
readRows: (cypher: string, onRow: (row: IdentityRow) => undefined) => Promise<void>,
phase: string,
): Promise<{ nodes: number; tables: number }> {
const expected = new Map<NodeTableName, Map<string, GraphNode>>();
@ -71,10 +72,25 @@ export async function reconcileGraphNodeIdentities(
{ cause },
);
};
let rows: IdentityRow[];
try {
rows = await query(
await readRows(
`MATCH (n:\`${table}\`) RETURN ${fields.map((f) => `n.${f} AS ${f}`).join(', ')}`,
(row) => {
const node = remaining.get(row.id);
if (!node) throw new Error(`unexpected or duplicate ID ${JSON.stringify(row.id)}`);
for (const field of identityFields) {
const wanted = storedIdentityField(node, field);
const actual =
field === 'name' || field === 'filePath' ? (row[field] ?? '') : row[field];
if (actual !== wanted) {
throw new Error(
`${row.id}.${field}: expected ${JSON.stringify(wanted)}, read ${JSON.stringify(actual)}`,
);
}
}
remaining.delete(row.id);
nodes++;
},
);
} catch (error) {
fail(
@ -82,21 +98,6 @@ export async function reconcileGraphNodeIdentities(
error,
);
}
for (const row of rows) {
const node = remaining.get(row.id);
if (!node) fail(`unexpected or duplicate ID ${JSON.stringify(row.id)}`);
for (const field of identityFields) {
const wanted = storedIdentityField(node, field);
const actual = field === 'name' || field === 'filePath' ? (row[field] ?? '') : row[field];
if (actual !== wanted) {
fail(
`${row.id}.${field}: expected ${JSON.stringify(wanted)}, read ${JSON.stringify(actual)}`,
);
}
}
remaining.delete(row.id);
nodes++;
}
if (remaining.size) {
fail(
`${remaining.size} missing ID(s), including ${JSON.stringify(remaining.keys().next().value)}`,

View file

@ -99,6 +99,7 @@ export function lookupNameClaim(
local,
scopes.bindings.get(scope.id)?.get(spelling) ?? EMPTY_BINDINGS,
) ?? EMPTY_BINDINGS;
if (additional.length === 0 && augmented.length === 0) return local;
return [
...local,
...compatibleClassBindings(additional, options.classCandidateFilter),
@ -108,24 +109,23 @@ export function lookupNameClaim(
),
];
}
return [
...local,
...compatibleClassBindings(
scopes.bindings.get(scope.id)?.get(spelling) ?? EMPTY_BINDINGS,
options.classCandidateFilter,
),
...augmented,
...(collectNamespaceFqnBindings(
scope.id,
spelling,
scopes,
options.classCandidateFilter,
) ?? []),
...compatibleClassBindings(
scopes.workspaceFqnBindings?.get(spelling) ?? EMPTY_BINDINGS,
options.classCandidateFilter,
),
];
const indexed = compatibleClassBindings(
scopes.bindings.get(scope.id)?.get(spelling) ?? EMPTY_BINDINGS,
options.classCandidateFilter,
);
const namespace =
collectNamespaceFqnBindings(scope.id, spelling, scopes, options.classCandidateFilter) ??
EMPTY_BINDINGS;
const workspace = compatibleClassBindings(
scopes.workspaceFqnBindings?.get(spelling) ?? EMPTY_BINDINGS,
options.classCandidateFilter,
);
if (
local.length + indexed.length + augmented.length + namespace.length + workspace.length ===
0
)
return EMPTY_BINDINGS;
return [...local, ...indexed, ...augmented, ...namespace, ...workspace];
},
},
options,

View file

@ -3,6 +3,7 @@ import { createReadStream, createWriteStream, existsSync, constants as fsConstan
import { createInterface } from 'readline';
import { once } from 'events';
import { finished } from 'stream/promises';
import { setImmediate as yieldQueryRows } from 'node:timers/promises';
import path from 'path';
import lbug from '@ladybugdb/core';
import { closeQueryResults } from './query-result-utils.js';
@ -1956,6 +1957,57 @@ export const executeQuery = async (cypher: string): Promise<any[]> => {
return await executePrepared(cypher, {});
};
/**
* Visit a single trusted, parameter-free query without retaining a JS row array.
* The visitor must be synchronous and must not issue another connection query.
* Keep the connection locked through yields and cursor close so a checkpoint
* cannot interleave with reading. Use direct execution: repeated prepared reads
* exhausted the native pool in the normal-GC identity scaling probe.
*/
export const forEachQueryRow = async (
cypher: string,
onRow: (row: any) => undefined,
): Promise<void> => {
warnIfQueryTextUnbounded(cypher, 'forEachQueryRow', (message) => logger.warn(message));
const c = conn;
if (!c) throw new Error('LadybugDB not initialized. Call initLbug first.');
await withConnLock(async () => {
const queryResult = await c.query(cypher);
const results = Array.isArray(queryResult) ? queryResult : [queryResult];
let failed = false;
let failure: unknown;
try {
if (results.length !== 1 || !results[0]) {
throw new Error('forEachQueryRow requires exactly one query result');
}
const result = results[0];
let rows = 0;
while (result.hasNext()) {
const row = result.getNextSync();
if (row === null) throw new Error('Query cursor ended before its advertised next row');
onRow(row);
if (++rows % 1024 === 0) await yieldQueryRows();
}
} catch (error) {
failed = true;
failure = error;
} finally {
for (const result of results) {
try {
result?.close();
} catch (error) {
if (!failed) {
failed = true;
failure = error;
}
}
}
}
if (failed) throw failure;
});
};
export const streamQuery = async (
cypher: string,
onRow: (row: any) => void | Promise<void>,

View file

@ -73,6 +73,7 @@ import {
loadGraphToLbug,
getLbugStats,
executeQuery,
forEachQueryRow,
executeWithReusedStatement,
closeLbug,
closeLbugBeforeExit,
@ -4419,7 +4420,7 @@ async function runFullAnalysisInner(
await markIncrementalGraphVerification();
await reconcileGraphNodeIdentities(
pipelineResult.graph,
executeQuery,
forEachQueryRow,
'post-COPY/checkpoint',
);
}
@ -5573,7 +5574,7 @@ async function runFullAnalysisInner(
}
await reconcileGraphNodeIdentities(
pipelineResult.graph,
executeQuery,
forEachQueryRow,
'pre-publish/checkpoint',
);
}

View file

@ -11,7 +11,8 @@
* use an empty (initialized) DB — they prove the lock is taken regardless of
* whether any rows match.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import lbug from '@ladybugdb/core';
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
import { NODE_TABLES } from '../../src/core/lbug/schema.js';
@ -40,6 +41,113 @@ import { closeQueryResults } from '../../src/core/lbug/query-result-utils.js';
const closeSpy = vi.mocked(closeQueryResults);
withTestLbugDB('conn-serialization', () => {
describe('incremental identity cursor lifetime', () => {
afterEach(() => vi.restoreAllMocks());
it('reads real rows without getAll and releases the cursor after a visitor failure', async () => {
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
const getAll = vi.spyOn(lbug.QueryResult.prototype, 'getAll');
const close = vi.spyOn(lbug.QueryResult.prototype, 'close');
const seen: number[] = [];
await adapter.forEachQueryRow('UNWIND [1, 2, 3] AS n RETURN n', (row) => {
seen.push(row.n);
});
expect(seen).toEqual([1, 2, 3]);
expect(getAll).not.toHaveBeenCalled();
expect(close).toHaveBeenCalledTimes(1);
const error = new Error('bad identity');
await expect(
adapter.forEachQueryRow('RETURN 1 AS n', () => {
throw error;
}),
).rejects.toBe(error);
expect(close).toHaveBeenCalledTimes(2);
await expect(adapter.executeQuery('RETURN 2 AS n')).resolves.toEqual([{ n: 2 }]);
});
it('yields while retaining the lock through the last row and cursor close', async () => {
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
let read = 0;
let visited = 0;
let yieldedAt = 0;
const close = vi.fn();
const cursor = {
hasNext: () => read < 2050,
getNextSync: () => {
expect(visited).toBe(read);
return { n: ++read };
},
close,
};
const query = vi.spyOn(lbug.Connection.prototype, 'query').mockImplementation(async (sql) => {
expect(sql).toBe('scan');
return cursor as any;
});
let checkpoint: Promise<unknown> | undefined;
// Schedule outside the reader's lock context, like the WAL driver timer.
setImmediate(() => {
yieldedAt = visited;
checkpoint = adapter.executeQuery('CHECKPOINT');
});
const prepare = vi.spyOn(lbug.Connection.prototype, 'prepare');
prepare.mockImplementation(async () => {
expect(visited).toBe(2050);
expect(close).toHaveBeenCalledTimes(1);
return { isSuccess: () => true } as any;
});
vi.spyOn(lbug.Connection.prototype, 'execute').mockResolvedValue({
getAll: async () => [],
close: () => {},
} as any);
await adapter.forEachQueryRow('scan', () => {
visited++;
});
await checkpoint;
expect(yieldedAt).toBeGreaterThan(0);
expect(yieldedAt).toBeLessThan(2050);
expect(query).toHaveBeenCalledTimes(1);
expect(prepare).toHaveBeenCalledTimes(1);
});
it.each(['none', 'multiple'] as const)(
'rejects %s result cursors and closes all returned cursors',
async (kind) => {
const { forEachQueryRow } = await import('../../src/core/lbug/lbug-adapter.js');
const close = vi.fn();
const cursors = kind === 'none' ? [] : [{ close }, { close }];
vi.spyOn(lbug.Connection.prototype, 'query').mockResolvedValue(cursors as any);
const visit = vi.fn();
await expect(forEachQueryRow('scan', visit)).rejects.toThrow('exactly one query result');
expect(visit).not.toHaveBeenCalled();
expect(close).toHaveBeenCalledTimes(cursors.length);
},
);
it.each(['read', 'null', 'close', 'read-and-close'] as const)(
'fails closed on %s failure',
async (stage) => {
const { forEachQueryRow } = await import('../../src/core/lbug/lbug-adapter.js');
const readError = new Error('native read failure');
const closeError = new Error('native close failure');
const close = vi.fn(() => {
if (stage.includes('close')) throw closeError;
});
vi.spyOn(lbug.Connection.prototype, 'query').mockResolvedValue({
hasNext: () => stage !== 'close',
getNextSync: () => {
if (stage === 'null') return null;
throw readError;
},
close,
} as any);
const outcome = forEachQueryRow('scan', vi.fn());
if (stage === 'null') await expect(outcome).rejects.toThrow('advertised next row');
else await expect(outcome).rejects.toBe(stage === 'close' ? closeError : readError);
expect(close).toHaveBeenCalledTimes(1);
},
);
});
describe('singleton-conn helpers acquire withConnLock (P1 #2264)', () => {
beforeEach(() => {
// Setup (clear/seed/flush) already exercised the lock; reset so each

View file

@ -0,0 +1,16 @@
import { describe, expectTypeOf, it } from 'vitest';
import type { forEachQueryRow } from '../../src/core/lbug/lbug-adapter.js';
import type { reconcileGraphNodeIdentities } from '../../src/core/incremental/write-reconciliation.js';
type CursorVisitor = Parameters<typeof forEachQueryRow>[1];
type IdentityReader = Parameters<typeof reconcileGraphNodeIdentities>[1];
type IdentityVisitor = Parameters<IdentityReader>[1];
describe('identity reader synchronous visitor contract', () => {
it('accepts synchronous visitors and rejects async visitors at both boundaries', () => {
expectTypeOf<() => undefined>().toExtend<CursorVisitor>();
expectTypeOf<() => undefined>().toExtend<IdentityVisitor>();
expectTypeOf<() => Promise<void>>().not.toExtend<CursorVisitor>();
expectTypeOf<() => Promise<void>>().not.toExtend<IdentityVisitor>();
});
});

View file

@ -52,7 +52,12 @@ it.each([
nativePhases,
size: { nodes: 1 },
reconcileGraphNodeIdentities,
adapter: { executeQuery: async (query: string) => (query.includes('Function') ? rows : []) },
adapter: {
executeQuery: async (query: string) => (query.includes('Function') ? rows : []),
forEachQueryRow: async (query: string, onRow: (row: (typeof rows)[number]) => void) => {
if (query.includes('Function')) rows.forEach(onRow);
},
},
}) as (phase: string) => Promise<void>;
await audit('controlled-scan');
expect(nativePhases).toEqual([expect.objectContaining({ verdict, rows: rows.length })]);

View file

@ -138,7 +138,7 @@ describe('incremental graph identity before publication', () => {
);
await adapter.tryFlushWAL();
await expect(
reconcileGraphNodeIdentities(graph, adapter.executeQuery, 'native CSV parity'),
reconcileGraphNodeIdentities(graph, adapter.forEachQueryRow, 'native CSV parity'),
).resolves.toMatchObject({ nodes: 5 });
} finally {
await adapter.closeLbug();
@ -264,7 +264,7 @@ describe('incremental graph identity before publication', () => {
await expect(
reconcileGraphNodeIdentities(
recovered.pipelineResult.graph,
adapter.executeQuery,
adapter.forEachQueryRow,
'recovered/reopened',
),
).resolves.toMatchObject({ nodes: expect.any(Number) });

View file

@ -0,0 +1,204 @@
import { execFileSync, spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, describe, expect, it } from 'vitest';
const harness = fileURLToPath(new URL('../../bench/lexical-lookup/measure.mjs', import.meta.url));
const scratch: string[] = [];
afterEach(() => {
for (const root of scratch.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});
describe('lexical benchmark preflight', () => {
it.each(['missing', 'empty-runtime', 'empty-output', 'lifecycle'])(
'rejects %s arguments with a diagnostic before filesystem work',
(kind) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lexical-args-'));
scratch.push(root);
const args = [
root,
root,
path.join(root, 'before'),
path.join(root, 'after'),
path.join(root, 'output'),
];
if (kind === 'missing') args.pop();
if (kind === 'empty-runtime') args[0] = '';
if (kind === 'empty-output') args[4] = '';
if (kind === 'lifecycle') args.push('warm');
const result = spawnSync(process.execPath, [harness, ...args], { encoding: 'utf8' });
expect(result.status).toBe(1);
expect(result.stderr).toContain(
kind === 'lifecycle'
? 'Lifecycle must be fresh or persistent; received "warm"'
: 'Expected BASE_RUNTIME CANDIDATE_RUNTIME BASE_CORPUS CANDIDATE_CORPUS OUTPUT',
);
expect(fs.readdirSync(root)).toEqual([]);
},
);
it.each([
['GITNEXUS_LEXICAL_BENCH_SCREEN', 'Screen must be 0 or 1'],
['GITNEXUS_LEXICAL_BENCH_BASE_FIRST', 'Base first must be 0 or 1'],
])('rejects invalid %s before touching corpora', (setting, message) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lexical-option-'));
scratch.push(root);
const output = path.join(root, 'output');
const result = spawnSync(process.execPath, [harness, root, root, root, root, output], {
encoding: 'utf8',
env: { ...process.env, [setting]: 'false' },
});
expect(result.status).toBe(1);
expect(result.stderr).toContain(message);
expect(fs.existsSync(output)).toBe(false);
});
it.each(['empty', 'indexes', 'results.json'])(
'rejects existing %s output before touching corpora',
(contents) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lexical-preflight-'));
scratch.push(root);
const output = path.join(root, 'output');
fs.mkdirSync(output);
if (contents === 'indexes') fs.mkdirSync(path.join(output, 'indexes'));
if (contents === 'results.json')
fs.writeFileSync(path.join(output, 'results.json'), 'prior result');
const before = path.join(root, 'before');
const after = path.join(root, 'after');
const result = spawnSync(process.execPath, [harness, root, root, before, after, output], {
encoding: 'utf8',
env: {
...process.env,
GITNEXUS_LEXICAL_BENCH_WORKER: '0',
GITNEXUS_LEXICAL_BENCH_WORKERS: '2',
GITNEXUS_LEXICAL_BENCH_HEAP_MIB: '4096',
},
});
expect(result.status).toBe(1);
expect(result.stderr).toContain('Output already exists');
expect(fs.existsSync(before)).toBe(false);
expect(fs.existsSync(after)).toBe(false);
expect(fs.readdirSync(output)).toEqual(contents === 'empty' ? [] : [contents]);
if (contents === 'results.json')
expect(fs.readFileSync(path.join(output, contents), 'utf8')).toBe('prior result');
},
);
});
describe('lexical benchmark sample collection', () => {
it.each(['fresh', 'persistent'])(
'collects a complete reversed-order baseline with a slower candidate (%s)',
(lifecycle) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lexical-samples-'));
scratch.push(root);
const corpora = ['before', 'after'].map((name) => {
const repo = path.join(root, name);
fs.mkdirSync(repo);
execFileSync('git', ['init', '--quiet'], { cwd: repo });
execFileSync(
'git',
[
'-c',
'user.name=Benchmark Test',
'-c',
'user.email=test@example.test',
'-c',
'commit.gpgsign=false',
'commit',
'--quiet',
'--allow-empty',
'-m',
'fixture',
],
{
cwd: repo,
env: {
...process.env,
GIT_AUTHOR_DATE: '2020-01-01T00:00:00Z',
GIT_COMMITTER_DATE: '2020-01-01T00:00:00Z',
},
},
);
return repo;
});
const runtimes = [1, 100].map((duration) => {
const runtime = path.join(root, `runtime-${duration}`);
const core = path.join(runtime, 'gitnexus/dist/core');
fs.mkdirSync(path.join(core, 'lbug'), { recursive: true });
fs.writeFileSync(path.join(runtime, 'package.json'), '{"type":"module"}');
// A deterministic clock isolates collection/order behavior from machine load.
fs.writeFileSync(
path.join(core, 'run-analyze.js'),
`
import fs from 'node:fs';
import path from 'node:path';
let clock = 0;
Object.defineProperty(globalThis, 'performance', {value: {now: () => clock}});
export async function runFullAnalysis(repo) {
clock += ${duration};
const target = fs.readFileSync(path.join(repo, 'gitnexus/src/semantic-bench-control.ts'), 'utf8').includes('s.load()') ? 'load' : 'save';
return {
storagePath: repo, stats: {}, rebuildReasons: [],
incrementalStats: {writeMode: 'incremental', changedFiles: 1},
pipelineResult: {graph: {
*iterNodes() { yield {id: 'semanticBenchUse', label: 'Function'}; },
*iterRelationships() { yield {sourceId: 'semanticBenchUse', targetId: target, type: 'CALLS'}; }
}}
};
}
`,
);
fs.writeFileSync(path.join(core, 'logger.js'), 'export function configureLoggerSink() {}');
fs.writeFileSync(
path.join(core, 'lbug/lbug-adapter.js'),
`
import fs from 'node:fs';
import path from 'node:path';
let repo;
export async function initLbug(db) { repo = path.dirname(db); }
export async function executeQuery() {
return [{target: fs.readFileSync(path.join(repo, 'gitnexus/src/semantic-bench-control.ts'), 'utf8').includes('s.load()') ? 'load' : 'save'}];
}
export async function closeLbug() {}
`,
);
return runtime;
});
for (const screen of ['0', '1']) {
const output = path.join(root, `output-${screen}`);
const result = spawnSync(
process.execPath,
[harness, ...runtimes, ...corpora, output, lifecycle],
{
encoding: 'utf8',
env: {
...process.env,
GITNEXUS_LEXICAL_BENCH_WORKER: '0',
GITNEXUS_LEXICAL_BENCH_WORKERS: '2',
GITNEXUS_LEXICAL_BENCH_HEAP_MIB: '256',
GITNEXUS_LEXICAL_BENCH_SCREEN: screen,
GITNEXUS_LEXICAL_BENCH_BASE_FIRST: '0',
},
},
);
expect(result.status, result.stderr).toBe(0);
const report = JSON.parse(fs.readFileSync(path.join(output, 'results.json'), 'utf8'));
expect(report.screen).toBe(screen === '1');
expect(report.baseFirst).toBe(false);
expect(report.results.map((sample: { name: string }) => sample.name)).toEqual([
'candidate-seed',
'baseline-seed',
'candidate-1',
'baseline-1',
...(screen === '0' ? ['baseline-2', 'candidate-2', 'candidate-3', 'baseline-3'] : []),
]);
for (const repo of corpora)
expect(fs.existsSync(path.join(repo, 'gitnexus/src/semantic-bench-control.ts'))).toBe(
false,
);
}
},
);
});

View file

@ -120,13 +120,14 @@ const mockLbugAdapter = async () => {
initLbug: vi.fn(async () => undefined),
loadGraphToLbug: vi.fn(async () => undefined),
getLbugStats: vi.fn(async () => ({ nodes: 1, edges: 0, communities: 0, processes: 0 })),
executeQuery: vi.fn(async () => []),
// The policy fixture has one stored File row. Publication now reconciles
// that identity instead of trusting the synthetic node count alone.
executeQuery: vi.fn(async (query: string) =>
query.startsWith('MATCH (n:`File`) RETURN n.id AS id')
? [{ id: 'file:src/a.ts', name: REL_FILE, filePath: REL_FILE }]
: [],
),
forEachQueryRow: vi.fn<typeof actual.forEachQueryRow>(async (query, onRow) => {
if (query.startsWith('MATCH (n:`File`) RETURN n.id AS id')) {
onRow({ id: 'file:src/a.ts', name: REL_FILE, filePath: REL_FILE });
}
}),
executeWithReusedStatement: vi.fn(async () => []),
closeLbug: vi.fn(async () => undefined),
wipeLbugDbFiles: vi.fn(async () => undefined),

View file

@ -14,6 +14,7 @@ import {
findClassBindingInScope,
findReceiverTypeBinding,
isNamespaceNameShadowed,
lookupNameClaim,
} from '../../../src/core/ingestion/scope-resolution/scope/walkers.js';
import { findCallableBindingsAndAdlBlocker } from '../../../src/core/ingestion/languages/cpp/callable-bindings.js';
import { followChainPostFinalize } from '../../../src/core/ingestion/scope-resolution/passes/imported-return-types.js';
@ -60,6 +61,40 @@ function indexes(
}
describe('lexical name ownership', () => {
it.each(['Function', 'Class'] as const)(
'keeps %s lookup snapshots after source bindings change',
(kind) => {
const bindings = new Map<string, BindingRef[]>();
const inner = scope('fn', 'module', { bindings, kind });
const outer = scope('module', null);
const state = { ...indexes(inner, outer), bindings: new Map([[inner.id, bindings]]) };
const missing = lookupNameClaim(inner.id, 'helper', state);
expect(missing.status).toBe('absent');
const local: BindingRef[] = [binding];
bindings.set('helper', local);
const found = lookupNameClaim(inner.id, 'helper', state);
expect(found.bindings).toEqual([binding]);
local.push({ def: { ...target, nodeId: 'second:helper' }, origin: 'local' });
expect(found.bindings).toEqual([binding]);
expect(missing.bindings).toEqual([]);
expect(lookupNameClaim(inner.id, 'helper', state).bindings).toHaveLength(2);
},
);
it('clears deferred activation without mutating reused caller options', () => {
const late: BindingRef = { ...binding, availableFrom: { startLine: 8, startCol: 0 } };
const outer = scope('module', null, { bindings: new Map([['helper', [late]]]) });
const inner = scope('fn', outer.id, { lookupPolicy: { deferParentActivation: true } });
const options = Object.freeze({ position: Object.freeze({ startLine: 1, startCol: 0 }) });
const state = indexes(inner, outer);
expect(lookupNameClaim(inner.id, 'helper', state, options).bindings).toEqual([late]);
expect(lookupNameClaim(outer.id, 'helper', state, options).status).toBe('absent');
expect(
lookupNameClaim(inner.id, 'helper', state, { ...options, preservePosition: true }).status,
).toBe('absent');
expect(options.position).toEqual({ startLine: 1, startCol: 0 });
});
it('keeps touching half-open import ranges separate', () => {
const earlier = { startLine: 1, startCol: 0, endLine: 1, endCol: 10 };
const later = { startLine: 1, startCol: 10, endLine: 1, endCol: 20 };

View file

@ -41,7 +41,9 @@ const rows = () => [
},
];
const queryFor = (functions: ReturnType<typeof rows>) =>
vi.fn(async (query: string) => (query.includes('(n:`Function`)') ? functions : []));
vi.fn(async (query: string, onRow: (row: ReturnType<typeof rows>[number]) => void) => {
if (query.includes('(n:`Function`)')) functions.forEach(onRow);
});
describe('incremental identity reconciliation', () => {
it('certifies every identity using one unfiltered query per label', async () => {