* feat(web): drop a folder onto the analyzer to upload it
The Local Folder panel of RepoAnalyzer is now a drop target. A dropped
folder is walked with the File and Directory Entries API
(DataTransferItem.webkitGetAsEntry), directories on the shared exclusion
list are pruned before they are read, and the resulting File objects are
handed to the existing filterRepoFiles -> uploadFolder -> trackJob path
with webkitRelativePath set to <folder>/<rest>, so the server receives the
same manifest shape the webkitdirectory picker produces.
Compared with the picker, the walk never enumerates node_modules or .git,
stops at the server's 20000 file cap and 64 path segments, skips
unreadable entries instead of failing, and reports progress while it runs.
Loose files and several folders at once are refused with a message (the
server accepts one top-level folder). The walk runs under the request
controller, so a mode switch or unmount aborts it; Analyze and the picker
are blocked while it runs. The panel-wide target also stops the browser
from navigating to a file dropped a few pixels off the button.
New strings in en and zh-CN; browsers without webkitGetAsEntry keep the
picker button and get an explanation.
* Address PR review feedback (#3315)
Clear readingCount only when this drop still owns the request controller, and skip oversized files before they count toward the 20k drop cap.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3315)
Count oversized files the drop walk skips in the summary droppedCount, and correct the 250-file batch-read comment.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(swift): match repeated SPM target prefixes
* test(swift): cover repeated SPM target prefixes
* test(swift): cover valid prefix before later partial match
* docs(swift): clarify target grouping parity scope
* docs(swift): clarify target grouping parity scope
* docs(swift): clarify target grouping parity scope
* fix(swift): resolve imports from Package.swift targets, not path segments
Stop fabricating IMPORTS from import Foundation onto a same-named folder.
Declare modules from Package.swift when the manifest is usable; keep
Sources/* for grouping and fail-open folder resolve minus SDK names.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(swift): keep empty Package.swift declarations and nested .target() deps external
An inferred Sources/* folder is grouping-only. A dependency .target(name:) is not a module. Treat both as unresolved so import Foundation cannot bind to a decoy folder.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(swift): keep implicit IMPORTS intra-group and gate linear Package.swift resolve
@_exported must not paint sibling files as implicit imports. A dedicated
bench pins declaration-only resolve and (t_4n/t_n)/4 linearity.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Honor member-only @_exported imports, skip comments while scanning
Package.swift factories, fail-open mixed helper-built target lists, and
block CoreData/CoreGraphics decoy folders on the inferred path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Match path: "." as the package root, skip block-commented Package.swift
factories, read import kind from the clause only, and skip capture tests
when the optional Swift grammar is missing.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): scan Swift import-kind without nested regex backtracking
CodeQL js/redos flagged the comment-skipping IMPORT_KIND_RE; a linear walk keeps the same kind tokens.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): linear Package.swift factory scan and gate Swift context
parseSwiftPackageManifest re-walked every prefix for comments (O(n²) in factory count). Resume the scan and cover nested factories in one pass. Wire Swift into the import-target context arm now that resolveImportTarget is 5-arg.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Tighten Package.swift and import-text scanners: skip comments/strings, reject escapes, treat ident + [ as incomplete, and drop the unused factory-comment wrapper.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): prettier the @_exported availability fixture
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Judge Package.swift completeness from Package(...)'s own targets: argument instead of raw-text regexes, nest block comments when reading an import kind, and strip leading ./ from declared target paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Collect Package.swift factories only from Package(targets: [...]), fail-open on computed array elements, and treat // after a label colon as a comment.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Ignore stray factories when Package() exists but omits targets:.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Require the Package-scan seen box so the always-true undefined guard goes away.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(swift): resolve nested constructors in extensions
* fix(swift): preserve qualified extension owners
* Address PR review feedback (#3308)
Recover qualified Swift extension owners through public / attribute prefixes, and stop last-dot-guessing when source text is present.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(swift): keep attribute text from stealing extension owners
Bound header recovery so @available messages cannot rekey a fragment, and still inject nested types when the Class scope has no bindings.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(swift): nest comments and keep the public extension fixture valid
Review follow-up: skip nested /* */ in the header scan, put the Inner.Entry decoy in a parsed file, and mark Outer/Container/Entry public so the live fixture is valid Swift.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(swift): recover Unicode identifiers as extension owners
The header regex was ASCII-only, so extension Café.Container keyed as Caf and dropped nested-type siblings. Match ID_Start/ID_Continue segments instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(swift): skip raw strings and decode UTF-8 scope columns
Header recovery treated #"..."# as an ordinary quote and sliced Tree-sitter byte columns as JS offsets, so a same-line Café prefix or a raw attribute message could steal or drop the extension owner.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(analyze): expose process-detection budget overrides (#3313)
Operators can raise or lower process count, branching, trace depth, and the entry-point candidate pool via CLI, .gitnexusrc, or GITNEXUS_* without changing shipped defaults. A budget-only change re-detects flows on the next analyze without --force.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(review): say invalid budget flags still honor env
A rejected --max-processes value was described as falling back to the built-in default even when GITNEXUS_MAX_* still won the next precedence tier.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(analyze): share process-detection defaults and skip unused walks
Keep DEFAULT_CONFIG aligned with the budget resolver and count symbols only when maxProcesses is still dynamic.
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(analyze): wrap process-detection budget files for prettier
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(analyze): name the real process-detection default formula
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(analyze): stop calling maxProcesses*2 a hard trace quota
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(analyze): say invalid env budget tokens fall back to defaults
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(analyze): recertify process-detection after in-place FTS abort (#3324)
Persist processDetection.uncertified on the in-place FTS dirty stamp when
the budget mismatched so a flagless retry cannot keep rewritten flows.
Qualify .gitnexusrc fail-fast copy and tighten related tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(analyze): skip live dirty stamp on atomic incremental (#3324)
POSIX atomic incremental mutates a staging copy, so stamping live incrementalInProgress before swap made a crash force-rebuild a healthy index. Align analyze --help with CLI > .gitnexusrc > env > default.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(changelog): drop the atomic-incremental dirty-stamp note
The code fix stays; Unreleased no longer lists that recovery change.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(cli): survive FTS SIGSEGV in --limit e2e
CREATE_FTS_INDEX can kill the setup analyze on some WSL hosts
(status null). Rebuild with --skip-fts and skip BM25-only
query --limit cases unless GITNEXUS_REQUIRE_FTS=1.
Refs #3324
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(cli): mark update-check child at import
Writing refresh-started from fetch() raced a 30s poll against
cold tsx boot on a loaded default-project worker.
Refs #3324
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3324)
Isolate default-budget FTS crash-marker tests from GITNEXUS_MAX_* env, assert uncertify-before-FTS order and deferred flow detection on park recovery, drop the dangling "then" from entry-point help, and correct stale streamGraphEmit docs without skipping the process-detection stamp.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(changelog): drop Unreleased process-detection notes
Keep the #3313 / #3322 code; Unreleased changelog matches main until release.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(analyze): load detected-branch sanitization from core git-ref
Keep the never-throw helper next to validateBranchName so run-analyze no longer imports CLI config parsing.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(analyze): warn once when a checkout name cannot label the index
After the write lock settles, emit a single onLog warning and keep writing the workspace slot. Pin that run-analyze does not import CLI analyze-config.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(analyze): escape hidden checkout names in the detect-reject warning
Keep the rejected ref visible in onLog without replaying bidi or quote characters, and document that sanitizeDetectedBranch rethrows unexpected errors.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(analyze): keep detect-reject warnings on one line (#3325)
Git-legal U+2028/U+2029 checkout names were rejected as whitespace but left raw in the new onLog warning, so the message split across two lines. Escape those code points in the formatter without changing validateBranchName.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* fix(analyze): keep C1 and Unicode spaces in detect-reject warnings
Escape NEL and remaining whitespace as \uXXXX so stripControlCharacters cannot drop or disguise the rejected checkout name.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(web): drop TypeScript 7-incompatible tsconfig paths
Remove baseUrl and the dead ../shared include so web project references typecheck under TypeScript 7.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(cli): parse TypeScript with a TypeScript 6 API package
Keep AST guards working after the named typescript package becomes 7, which no longer ships the Compiler API.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(lint): pin root TypeScript to the 6 API package
Give typescript-eslint a TypeScript 6 peer so syntax-only lint still installs after CLI and web move to TypeScript 7.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(deps): compile first-party packages with TypeScript 7.0.2
Unify CLI and web on the same native compiler line as gitnexus-shared so typecheck and emit no longer split 5.x versus 7.x.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(ci): describe parent TypeScript 7 as the shared compiler
Stop saying web compiles shared with TypeScript 5 now that the parent lockfile is 7.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): compile shared from parent TypeScript on Vercel and skill-evolution
Stop isolated npm installs in gitnexus-shared so those paths do not pull a second TypeScript 7 optional-platform tree.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs: record TypeScript 7 typecheck and Dependabot major-split policy
Keep contributor typecheck commands, and stop Dependabot from bumping shared onto a different TypeScript major than CLI and web.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lint): pin root TypeScript to 5.9 so npm ci satisfies eslint peers
typescript-eslint 8 peers typescript below 6.0.0, so the typescript6 alias made quality lint npm ci fail with ERESOLVE.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(cli): drop the TypeScript 6 Compiler API package
TypeScript 7.0 has no classic createProgram surface, so parse-only
guards now use Babel and Mode 4 uses the TypeScript 7 Checker.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs: align contributor setup with parent TypeScript 7 compile
Stop telling clones to npm-install gitnexus-shared; CI and Vercel already emit that package from a parent lib/tsc.js shim.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): typecheck React JSX on TypeScript 7 with explicit DOM libs
TypeScript 7 no longer implies DOM or auto-includes @types, so the web app must declare React/JSX settings while Vite keeps plugin-react.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: pin Vercel --include=dev and share parse-only string helpers
Production npm ci omits the web TypeScript unless --include=dev is on that install. Move staticStringValue next to the other Babel walk helpers so CLI help and contract tests share one source.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(embeddings): spill cached vectors to a Float32 temp file
Keep restore metadata in RAM and write embeddings once the in-memory
row limit is exceeded so incremental analyze can survive large caches
without a full-table number[] heap (#3306).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): stream CodeEmbedding cache under the connection lock
Spill vectors once the in-memory limit is crossed and fail the load
instead of adopting an empty snapshot, so incremental analyze cannot
OOM or quietly drop the restore cache (#3306).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(analyze): restore cached embeddings from a streamed spill snapshot
Hold row metadata across wipe, materialize 200-row batches, and treat
cache-load failures as warn-and-continue so incremental analyze can
preserve vectors without a full-table heap (#3306).
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3310)
Loop spill writes until the full vector lands, keep materialize failures out of the insert catch and the Phase 4 hash skip-set, and assert spilled restore subsets by node id instead of scan order.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3310)
Discard only this analyze run's embedding spills so a concurrent analyze on another index keeps its restore file, and isolate the default in-memory limit test from inherited env.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3310)
Mark a node stale when any restore batch fails so leftover chunks are deleted and rembedded, and exercise a full-length bad-magic spill header.
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(auto-sync): preserve PDG indexes across updates
* docs(auto-sync): document durable PDG synchronization
* Address PR review feedback (#3290)
- Correct requestedPdg state docs for threshold-skipped syncs
- Defer coalesced follow-up and skip failure-threshold counts for leftover-worker / retryable lock waits
- Document the pdg tri-state and caveat the 30m/5m example
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): raise Windows Ladybug #605 hang budget off the CI tail
Windows 3/3 typically finishes this native race in ~15s but has a 56s tail; 60s false-positives as deadlock. Keep the POSIX 60s detector and the completion/.shadow/row-count contract.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(embeddings): isolate local ONNX inference in a child_process sidecar
The analyze parent must not load onnxruntime-node. Fork a sidecar for
vectors only and reap it on worker exit; keep Ladybug writes in-process.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(embeddings): share the sidecar client across MCP, serve, and sync
Query hosts now use the core façade instead of a second in-process ONNX
embedder. Search skips an empty table, sync reaps beside closeLbug, and
ready means the stack is resolvable rather than a warm singleton.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(embeddings): refuse Intel Mac and unloadable prefix before npm heal
Analyze, sync, install, and the sidecar client now consult the platform
blocker before forking or downloading the optional stack. HTTP stays the
escape hatch; wasm is not treated as a rescue.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(embeddings): take the ONNX stack off default npm install
Pins live in gitnexusEmbeddingStack. embeddings install writes prefix
overrides before npm spawn. Leftover 1.6.12 package-first trees are residual.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(publish): drop grammar source from the published tarball
Every vendored grammar has 6/6 prebuilds, so files ships those plus
Leiden and FTS instead of parser.c. First ship stays above 80 MiB.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(embeddings): match MCP missing-stack warn to the R20 copy
Default install no longer calls the stack optional, so the once-per-backend
stderr assertion must look for the new lead line.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(review): bound sidecar death, cancel writes, and publish-file guards
Init-time native crashes no longer respawn a child on every query. Local
embedBatch honors AbortSignal after sidecar return, MCP query() surfaces
vector-lane degradation, disconnect always reaps, and the grammar prepack
guard checks files globs instead of on-disk prebuilds.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(embeddings): share runtime preflight and sidecar reap helpers
Analyze and embeddings-sync used the same blocker/prefix/install gate
with different error routing. One assessment keeps those paths aligned
without changing CLI vs thrown-error behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3287)
Keep a reaped sidecar from resetting its replacement, wait for dispose,
tighten the publish-files guard, and stop assuming a leftover ONNX tree in CI.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address remaining PR review feedback (#3287)
Clear the sidecar reap timeout, add init IPC slack, and isolate embeddings-sync tests from HTTP-mode env.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(embeddings): unstub globals after sidecar HTTP-mode tests
Keep a leaked fetch stub from failing assertions out of later tests in the same file.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(embeddings): pin sidecar success cases off darwin/x64
The runtime blocker reads the real process platform before the fork mock, so local-success tests must not inherit an Intel Mac host.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address remaining PR review feedback (#3287)
Keep vector degradation per query, treat leftover Intel-Mac stacks as not ready, and document that the CLI image no longer ships ONNX.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify embedding sidecar shutdown and search hot paths
Drop redundant sidecar reaps and unused child helpers, and run FTS alongside semantic search.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address remaining PR review feedback (#3287)
Share HF attempt parsing with the sidecar init deadline, abort embed waits without killing the child, and restore last init options on recreate.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address remaining PR review feedback (#3287)
Treat sub-1 HF attempt env values as invalid, and drop leaked sidecar waiters when IPC send throws.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address remaining PR review feedback (#3287)
Keep sidecar init on a shared chain; each waiter can abort only its own wait.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): declare embedding-table existence probe as unordered LIMIT
The empty-table skip in semanticSearch is existence-only; declare it so the #2787 determinism guard stops failing coverage shard 3/3.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): pin Ladybug core so Dependabot cannot ship a skewed FTS artifact
The extension version is a separate upstream constant. Ignore daily core bumps and fail the pairing gate when the committed manifest does not name the installed core.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): make doctor and CI FTS gates resolve the packaged artifact
Doctor and the REQUIRE_FTS file gates still treated an empty ~/.lbdb as
unavailable, which would turn three CI jobs red once analyze stops
installing into that tree.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): name native-abort and tuple-missing so analyze cannot mis-advise
The CLI summary's trailing else treated every unknown skip reason as a
missing extension. New crash and platform causes must get their own
remedies, not a network-install hint.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): delete the dead read-path FTS index create
ensureFTSIndex had no production callers and swallowed read-only
CREATE_FTS_INDEX failures, which hid the only signal that a reader
tried to write.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): vendor per-platform FTS artifacts so analyze needs no host install
Keyword search depended on a CDN fetch into ~/.lbdb. Shipping the five
published tuples inside the package makes air-gapped and ignore-scripts
installs load the same artifact the publish gate checksums.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): load the packaged FTS artifact before any network install
Analyze still required a CDN fetch into ~/.lbdb even when the package
already shipped the file. FTS now path-loads the vendored tuple first
and records source labels so a later truncated home copy cannot steal
the diagnosis.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): diagnose a core/extension version skew instead of a missing runtime
A structurally valid FTS artifact whose path version disagrees with the
packaged pin must name both versions, not prescribe VC++ or OpenSSL.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): stamp an FTS phase so repair stays usable after an in-place abort
A native CREATE_FTS_INDEX abort leaves no skip reason; the next run infers
it from the dirty flag, and --repair-fts must not treat that phase as a
half-written graph.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): park an in-place FTS crash WAL without wiping the graph
An FTS abort after a successful checkpoint must reopen the live index on
macOS, Windows, and Linux. Staging never parks the live WAL; readers keep
today's large-WAL refusal.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): refuse read-only opens of an FTS-poisoned WAL
MCP and serve cannot repair a leftover in-place abort. Fail before the
native open and name --repair-fts, on macOS, Windows, and Linux.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): name a vendor-neutral Windows OpenSSL prerequisite
OQ1 is unanswered here so GitNexus does not ship OpenSSL DLLs. Windows
FTS now asks for a system OpenSSL 3 runtime instead of Git Bash PATH.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(lbug): inject the FTS vendor root and redact it on HTTP and MCP
Path-loaded artifacts no longer vary with HOME. Tests pass an injected
vendor tree and assert search warnings never leak a filesystem path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(lbug): document load-only as the global FTS install default
Analyze still overrides to auto. Packaged per-platform artifacts load
before any network install on macOS, Windows, and Linux.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(lbug): format the FTS install-policy README table
Prettier does not run on Markdown in pre-commit, so the U10 table wrap
needs its own formatting commit.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): skip FTS CREATE after a persisted native abort
A recovered analyze run was retrying CREATE_FTS_INDEX from skipReason
alone. Keep that skip until --repair-fts, fail closed on unsupported
tuples, and honor the checkpoint warrant for park/repair.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): honor checkpoint flushed warrant and align FTS tests with packaged vendor
A no-op CHECKPOINT must not satisfy the FTS park warrant, and CI still asserted HOME-only FTS isolation after analyze started path-LOADing the packaged artifact.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(lbug): accept a nonempty incremental write set in the #2790 recovery check
FTS-phase recovery can incremental-add files (changed=0, added=1). That is not the #2790 empty-diff wipe skip.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): compare FTS home versions to the core pin and tighten the publish filename gate
Ladybug's ~/.lbdb/extension directory is the runtime/core version; treating it as the artifact version false-diagnosed skew. The publish guard now rejects a path-escaping filename the same way the fetch script does.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(lbug): seed FTS e2e fixtures from the packaged vendor artifact
A machine with no ~/.lbdb copy should still run the vendor-survivorship cases; the seed no longer depends on HOME or a network install.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3274)
Keep in-place FTS abort evidence after persist so a second CREATE abort
cannot fail-open readers, and close the CLI, loader, embed, and e2e gaps
the review called out.
Note: full npm test hit Ladybug worker-pool startup failures under memory
pressure; tsc and 180 targeted unit tests passed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3274)
Run the vendored-path symlink guard on the OS matrix, put e2e HOME
fixtures on Ladybug's real extension layout, pin the embed crash-WAL
gate before the writable open, and let analyze writers park through
missing-shadow recovery.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): keep --repair-fts CI green after vendored-first FTS
Never-installed warning fixtures must not inspect a packaged vendor binary, and a failed dirty restamp must not abort an otherwise successful --repair-fts run.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(cli): give the #1169 analyze e2e the same 90s Windows budget as its sibling
The first #1169 persist-meta case was still on a 60s spawn/it budget and was killed banner-only on windows-latest after the FTS warning fixture no longer failed the shard first.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(ci): reweight Windows shards after the FTS e2e grew
Vendored-first HOME fixtures pushed fts-extension-e2e to ~6 minutes on windows-latest, so the old 146s weight packed it with skills-e2e and blew the 20-minute watchdog.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(storage): add configurable index storage and content retention tiers
Rebase #3060 onto current origin/main. Keep GITNEXUS_STORAGE_PATH,
GITNEXUS_STORAGE_ROOT, and GITNEXUS_CONTENT_RETENTION, and fold in
main's FTS skip, embed-session, and help-text updates.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3060)
Keep legacy registry rows on the local storage fallback, resolve
symlinks before the destructive-path guard, and align hook lookup
with CLI branch slugs, branch-slot metadata, and longest-path match.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3060)
Only list swept upload directories after a successful removal so
callers cannot treat a permission or transient rm failure as gone.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3060)
Document that getStoragePath may consult registered storage while
this module still does not mutate the global registry.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(storage): close review findings for external indexes and retention
Re-inspect ownership under the analyze lock, fail-closed when the
registry file is missing, and keep skip-git hook discovery plus
retention fields on HTTP/MCP list surfaces. /api/file stays 410
unless contentRetention is full.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* Address PR review feedback (#3060)
Treat lock-only index dirs as empty, honor HTTP --force storage policy, and prefer registered plus branch-aware slots in hooks and augment.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3060)
Keep hook fallbacks inside the current worktree, compare foreign-local slots canonically, and make storage fixtures survive ownership validation.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix macOS hook test expecting realpath'd registry paths.
resolveHookRepo returns the written registry path, not a filesystem realpath, so the assertion must match that.
* Address gitnexus-check warnings on hook install docs and slot tests.
The Cursor troubleshooting list omitted registry-query.cjs, and the writable-slot test only checked that isDirectory exists instead of that the path is a directory.
* Align the HTTP catalog source-scan with skippable resolveRepo validation.
resolveRepo lists fresh repos with validate: options.validateStorage !== false so DELETE can skip prune; the test still required a literal validate: true.
* Harden storage path sinks so CodeQL path-injection and ReDoS alerts clear.
Contain every filesystem probe inside the resolved storage slot with the inline path.relative idiom, reject filesystem-root slots, and trim slot basenames in linear time.
* Settle bridge stamps before writing so CI size/mtime matches stay stable.
LadybugDB can still flush into bridge.lbug after close+rename; persist whole-millisecond mtimes and wait for consecutive stats to agree so a freshly written pair matches.
* Type the settled bridge stat as fs.Stats so tsc does not see bigint.
Awaited<ReturnType<typeof fsp.stat>> collapsed the bigint overload and broke prepare/typecheck on CI.
* Keep the bridge mtime stamp exact so same-size swaps still fail the pair check.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Wrap the bridge stamp predicate so prettier --check stays green.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Require a quiet interval before stamping a settled bridge file.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Reuse shared storage and settle helpers instead of local copies.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(lbug): await evict-then-reopen so it can't race the checkpoint
closeOne() closed the evicted repo's shared Database with a
fire-and-forget `db.close().catch(() => {})` (no await). Both call
sites that evict-then-reopen — evictLRU() right before doInitLbug
opens the new connection, and the "idle & changed" path in initLbug —
proceeded to open the next repo's connection immediately after,
without waiting for the evicted repo's close (and the checkpoint it
triggers) to finish. On the real engine the new open can then collide
with that still-in-flight checkpoint, surfacing on any read as:
Runtime exception: Cannot open database in read-only mode while
checkpoint is in progress. Please retry later.
This reproduces reliably once more than MAX_POOL_SIZE (5) distinct
repos are queried within a short window (self-hosted deployments with
more than a handful of active repos hit it routinely), and gets worse
under genuinely concurrent requests for different repos, since nothing
serialized pool mutations across callers either.
Fix:
- closeOne / evictLRU are now async and await their internal work
(closeOne's own close() call; evictLRU's call to closeOne), closing
the race within a single initLbug call.
- The exported initLbug is wrapped in a small async mutex
(initLbugInner does the real work) so concurrent initLbug calls for
different repos serialize instead of each racing their own
evict-then-reopen against the others.
- closeLbug's two closeOne() calls are now awaited too — closeOne
becoming async meant closeLbug could resolve before pool.delete()
had actually run, which a repo-pinning test caught (isLbugReady()
briefly still true right after a resolved closeLbug()).
- closeOne now deletes the pool entry (and clears its pin, and
notifies pool-close listeners) BEFORE the awaited db.close(), not
after. Review caught that the previous order left a "zombie" entry
reachable via pool.get(repoId) — closed=true, available emptied, but
still present — for the duration of that await; a same-repo
query/init landing in that window would see isLbugReady() as true
and hit a "Connection pool integrity error" in checkout() instead of
just reopening. Deleting first removes the entry entirely, so a
concurrent caller takes the normal fresh-open path instead.
Verified two ways:
- Against the compiled bundle (`ghcr.io/abhigyanpatwari/gitnexus`,
1.6.10/1.6.11 — pool-adapter.js is byte-identical between them): an
A/B docker build with 7 tiny local repos and genuinely concurrent
(parallel, not sequential) /api/graph requests goes from 7/7 failing
to 7/7 succeeding on a freshly-analyzed pool.
- Unit tests here (mocks @ladybugdb/core the same way as
lbug-pool-pinning.test.ts): one asserts the evicted repo's close()
completes before the initLbug call that triggered the eviction
settles; another asserts closeLbug's own promise doesn't resolve
before the underlying close() does. Both gate their mock's close()
on a real short delay and were confirmed to fail against code that
drops the corresponding await.
Note: a second, deeper issue was also observed in the docker A/B
setup — repeated rounds of concurrent access show a repo that has
gone through one evict+reopen cycle can become permanently unable to
reopen for reads, identically with and without this fix. That did not
reproduce with mocks and isn't understood yet; filed separately as
#3186, which stays open and untouched by this PR — this fix closes a
real, root-caused bug on its own but does not resolve#3186 by itself.
Second review round caught a follow-up: the idle-timeout sweep calls
closeOne(repoId) directly, outside of initLbug's poolLock. Now that
closeOne deletes the pool entry before its awaited close(), an
unsynchronized idle close racing a same-repo initLbug could let that
initLbug treat the repo as absent while the idle close (and its
checkpoint) is still in flight — reopening the same class of race this
PR exists to close, just via the idle path instead of LRU eviction.
Routed the idle sweep's closeOne call through withPoolLock too, so it
serializes against initLbug the same way evictLRU already does.
(Tried to add a mocked regression test for this specific interleaving;
dropped it — the mock's dbCache-reuse path masks the difference
regardless of the fix, so it could not be made to discriminate
reliably. Fixed by direct code review instead, same as the note below
already does for the native-engine-specific checkpoint collision.)
Also removed the initLbugInner per-repoId initPromises dedup map: with
every initLbug call now serialized through poolLock, a second call for
a repoId already being initialized cannot observe a pending promise in
initPromises (the first call always fully completes, including its
finally-block cleanup, before the lock releases) — the branch was dead
code the bot correctly flagged twice.
Third review round caught two more follow-ups on the same theme (both
introduced by making the idle sweep route through poolLock):
- closeLbug()'s no-arg ("close everything") branch still calls closeOne
directly in a loop over a snapshotted pool.keys(), without the lock —
an initLbug racing that loop could register a fresh entry the
snapshot never saw, leaving it resident after a call meant to empty
the pool. Wrapped the snapshot+loop in withPoolLock.
- The idle timer callback can now sit queued behind an in-progress
initLbug before its turn arrives, and that init (or a concurrent
touchRepo()) can refresh lastUsed in the meantime — so the pre-lock
idleness check taken when the timer fired can be stale by the time
it actually runs. Re-check lastUsed/checkedOut again inside the lock,
right before closing, instead of trusting the outer snapshot.
* fix(deps): pin @ladybugdb/core back to 0.18.3
Bisected the "checkpoint is in progress" symptom (root cause #2, not
touched by the pool-adapter.ts fix in the previous commit) down to a
single dependency-version-bump commit with zero application code
changes: e91ea0ca, "chore(deps): bump @ladybugdb/core in /gitnexus",
0.18.3 -> 0.19.0.
Confirmed both ends independently, on the actual official build
(Dockerfile.cli), no engine-swapping involved:
- v1.6.9 (native 0.18.3, as released): 7 tiny repos, 5 rounds of
genuinely concurrent /api/graph requests each — 0/35 failures.
- v1.6.10/v1.6.11 (native 0.19.1, as released): same repro — fails
every round from round 2 onward.
- v1.6.11 completely unmodified (not even this repo's own fix) with
ONLY @ladybugdb/core downgraded to 0.18.3 (real `npm install
@ladybugdb/core@0.18.3 --save-exact`, full rebuild, no application
code touched): 0/56 failures across 8 rounds.
That last point isolates this fully: none of GitNexus's own JS changes
between 1.6.9 and 1.6.11 (including the dbIdentity/rebuild-detection
logic added in #2614, or anything in sidecar-recovery.ts) are
load-bearing for this symptom — the regression lives entirely in the
native engine, introduced somewhere between 0.18.3 and 0.19.0.
Full unit suite green with this pin (14818 passed, same 2
environment-specific flakes present on main regardless of this change
— macOS realpath symlink resolution in analyzer-identity.test.ts and a
subprocess retry-count assertion in review-agent-workflow.test.ts,
neither touches lbug/ladybugdb).
This is a pragmatic pin, not a long-term fix: 0.19.0+ presumably ships
fixes of its own that 0.18.3 lacks, and the actual regression should
still be root-caused and fixed upstream (tracked at
LadybugDB/ladybug#919, which a maintainer is already engaging with).
Recommend re-evaluating this pin once that's resolved.
* fix(lbug): serialize closeLbug's single-repo branch with the pool lock
Review on PR #3189 caught the same class of gap as three earlier
rounds on the previous PR: closeOne now deletes the pool entry before
its awaited db.close() finishes, so an initLbug(repoId, ...) racing
this branch could acquire the lock right after the delete, see no
cached entry, and start opening a fresh connection while this close's
checkpoint is still in flight — reopening the exact race withPoolLock
exists to close. The no-arg ("close everything") branch already went
through the lock; this makes the single-repoId branch consistent with
it.
npx tsc --noEmit clean, full lbug/pool unit suite (19 files, 257
tests) green.
* fix(lbug): address azizur100389's review findings on PR #3189
- pool-adapter.ts: the idle timer's in-lock recheck already re-verified
lastUsed/checkedOut against a fresh snapshot, but not pinnedRepos —
pinRepo() can run while the timer callback is queued behind an
in-progress initLbug, and the timer would then still close a repo the
caller just pinned, dropping that lease entirely (LOW finding).
- Storage-version mismatches (opening an index written by a different
@ladybugdb/core build, e.g. after downgrading the pinned dependency)
surfaced as GitNexus's generic "unavailable, retry later" and were
retried LOCK_RETRY_ATTEMPTS times for nothing, since the file's
on-disk version never changes between retries (HIGH/blocking
finding). Added isStorageVersionMismatchError() to lbug-config.ts and
wired it into both places that actually open a LadybugDB connection:
pool-adapter.ts's doInitLbug (used by MCP tools/wiki/group-sync) and
lbug-adapter.ts's doInitLbug (the separate single-connection path
/api/graph and /api/query use via withLbugDb). Both now fail fast
with an actionable "run `gitnexus analyze --force`" message instead.
In lbug-adapter.ts the check wraps both openLbugConnection and
ensureReadOnlyConnectionUsable: the native engine's storage-version
check isn't necessarily enforced until the first real query runs
(ensureReadOnlyConnectionUsable's own probe), so openLbugConnection
alone can succeed on a mismatched file.
Verified end-to-end against the real native engine: registered a repo
under the pinned 0.18.3 engine, swapped its .gitnexus/lbug file for one
written by an unmodified v1.6.11 (0.19.1) image, restarted the server
to bypass in-memory connection caching, and queried /api/graph — got
the actionable message instead of the generic retry-later error.
`npx tsc --noEmit` clean; full lbug/pool unit suite (36 tests, 14
files) green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(test): export isStorageVersionMismatchError from wholesale lbug-config mocks
doInitLbug's catch block (both pool-adapter.ts and lbug-adapter.ts) now
calls isStorageVersionMismatchError() unconditionally on every open
failure, but 5 test files wholesale-mock lbug-config.js without that
export — Vitest rejects access to an undeclared mocked export, so any
test driving an error through that catch block (e.g. the WAL-recovery
and evict-reopen-race suites) breaks (bot finding on PR #3189).
Added isStorageVersionMismatchError (stubbed to always return false —
none of these suites exercise the storage-version path) and
STORAGE_VERSION_MISMATCH_SUGGESTION to each mock, matching the existing
isWalCorruptionError/WAL_RECOVERY_SUGGESTION pattern already there.
analyze-pagesize-error.test.ts was not affected: it mocks lbug-config.js
via importOriginal, so it already re-exports the real function.
Verified: the 6 affected files (48 tests) pass; npx tsc --noEmit clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(test): export isStorageVersionMismatchError from remaining lbug-config mocks
The previous commit fixed the 5 test files that wholesale-mock
lbug-config.js via vi.mock(), but missed 4 more that mock it via
vi.doMock() instead (a different Vitest API my earlier grep for
vi.mock(...) didn't match): lbug-adapter-wal-schema.test.ts (8
call sites), lbug-checkpoint-lifecycle.test.ts (12 call sites), and
basicblock-callee-ids-schema.test.ts / convex-metadata-persistence-
contract.test.ts (1 shared mock factory each). All of these exercise
lbug-adapter.ts's doInitLbug, which now also calls
isStorageVersionMismatchError() unconditionally on every open failure
— caught by actually running the full suite rather than trusting the
`-t lbug` name filter, which doesn't match these files' test names.
Verified: all 10 affected files (97 tests) pass; npx tsc --noEmit
clean. Full suite rerun in progress to confirm no other gaps remain.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(lbug): fail-fast storage-version mismatch and unlock pool lock-retry
Incremental analyze was warning through a version mismatch, and lock-retry
sleep held the pool mutex so one analyze-locked repo blocked every other
init. Fail immediately with the rebuild hint on both adapters, and sleep
outside withPoolLock so other repos can open during backoff.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* Address PR review feedback (#3189)
- Serialize initLbugWithDb on withPoolLock so it cannot attach to a Database
closeOne is still checkpointing.
- Delete pin leases again after the awaited close so a pin acquired during
teardown cannot survive onto the next init.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(parse-cache): retire a chunk whose durable generation could not be reset
#3200 skipped the parse-cache write when `prepareDurableParsedFileChunk`
failed, but the chunk hash was already in `usedKeys` from the lookup. When a
previous generation existed on disk — reachable because the coherence gate
re-dispatches a chunk whose `.v8` shard is live but whose durable shards are
unreadable — `saveParseCache` copied that old shard forward, and the durable
prune, which keeps exactly the saved keys, retained the mixed directory. The
next run then served a warm hit out of a directory the previous run had
already decided it could not account for.
Retire the hash instead of only skipping the write:
- `ParseCache.staleKeys` is a transient set that `saveParseCache` filters out
of its key list. Filtering at save is what makes it survive the post-parse
key merges in run-analyze (#2106 sibling fold, unreadable-meta retention),
and it reaches both stores at once because the durable prune keeps exactly
the keys `saveParseCache` returns.
- The hash is retired at the reset-failure site, which runs unconditionally.
The parse-cache write branch sits behind `rawResults.length > 0`, so a chunk
whose worker round returns nothing would never have been retired there.
- Worker-quarantined chunks get the same treatment for the same reason: they
also reach the save with no in-memory entry, which is what triggers the
copy-forward. That branch was previously unreachable when the worker died on
the chunk and returned no results.
- Guard the durable prune's non-survivor `fs.rm`. The causes that break the
reset break that delete too, and it sat outside the validation try — one
undeletable directory aborted the loop and cost every remaining chunk its
index entry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(review): apply review findings
Retire a chunk only when a generation nobody cleared is still on disk.
`prepareDurableParsedFileChunk` is rm-then-mkdir, and the catch could not tell
the two apart: an rm that succeeded before a failing mkdir leaves NO directory,
so the workers recreate it and write a clean generation. Retiring there
discarded a good `.v8` for no safety gain — and under a correlated failure
(an empty durable index turns every chunk into a re-dispatched miss, then a
descriptor burst rejects the resets en masse) it would have wiped both shared
stores for every branch, where the pre-#3204 posture cost only the writes.
`durableChunkHasStaleShards` is the discriminator.
Finish the delete guard on the path that runs before it. The staged→live
overlay in `mergeStagedDurableParsedFileStore` awaited `replaceDurableChunkDir`
unguarded, so on the cold-rebuild path one undeletable directory threw out of
the merge before the prune ever ran — the durable index was never rewritten and
a retired chunk kept its directory. Same log-and-continue treatment, plus
best-effort handling of the two `.replacing` backup removals.
Aggregate the prune's delete-failure warning: a store-wide cause hits every
non-survivor, and one line per directory buries the message that matters.
Tests:
- Guard the chmod-based prune test with the repo's `skipIf` for root/Windows
and assert the directory survived, so it cannot pass vacuously where the
delete succeeds.
- Add a two-chunk control: one chunk's reset fails, and the sibling must stay
warm through the next run. One chunk plus a global spawn marker could not
tell "retires the failing chunk" from "retires everything".
- Add the rm-succeeded/mkdir-failed case, which must NOT retire.
- Model both post-parse merges in the R4 test (the sibling fold re-adds the
key, the unreadable-meta fallback unions `entries`), and move the in-memory
`entries` assertion to a direct helper test — the sharded path never
populates `entries`, so the old assertion proved nothing.
- Type the cache factory as `ParseCache`; the `staleKeys` assertions were
TS2339 and `?? false` read as a pass regardless.
- Register the store test in the cross-platform filesystem list.
Correct two comments that still described a quarantined chunk by the premise
this fix disproves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(review): stop swallowing the backup removal in replaceDurableChunkDir
Swallowing that `fs.rm` manufactured the very hazard this PR removes. When a
non-empty `${to}.replacing` survives, the following `fs.rename(to, backup)`
cannot overwrite it and is suppressed as "dest was missing", so `backedUp`
stays false and the `fs.cp(from, to)` fallback merges the staged generation
INTO the live directory — old shards alongside new, which the prune then
indexes as one valid survivor. Let it throw; the per-entry guard added to
`mergeStagedDurableParsedFileStore` already stops one such chunk from costing
the others their prune. The post-publish backup cleanup stays best-effort,
where an undeletable leftover really is litter.
Also:
- Make the sibling-isolation test perform the run its title claims. It asserted
index membership and stopped; an index entry does not exercise the warm-hit
path, so it would have passed even if the sibling re-dispatched. Each chunk
now runs alone so the single spawn marker names which one re-parsed.
- Correct two comments that outran the implementation: retirement is gated on
shards actually surviving, and an undeletable directory is dropped from the
index rather than removed from disk.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(storage): guard stale file-lock reclamation
* fix(storage): close lock recovery failure paths
* Address PR review feedback (#3234)
- Flush the lock-child stderr diagnostic before process.exit
- Treat explicit NaN timeouts as the default ceiling
- Document non-retryable guard timeouts on the worker IPC contract
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(storage): stop mislabeling live lock waits as orphan recovery
A brief peer inspect must not attach guardPath or send operators to
RUNBOOK delete steps. Refuse lock-free embeddings sync, stop --watch
only on a true guard timeout, and drop an unreadable self-created
guard before failing closed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* Address PR review feedback (#3234)
- Verify lock/guard absence before degrading a denied main-lock create
- Launch the third contender from unlinkSync, not the dead rename path
- Document group-lock timeouts for unrecoverable guard leftovers
Co-authored-by: Cursor <cursoragent@cursor.com>
* style: prettier index-lock reclaim guard tests
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(storage): treat O_EXCL as the lock-file presence check
CodeQL flagged existsSync-then-wx on analyze.lock. Create with wx first and
only reclaim unreadable leftovers after grace, so a successor is never
unlinked from a lost race.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(dart): anchor @name so a constructor initializer stops minting a phantom
A Dart declaration whose value is a constructor call parses the callee as a
SECOND (identifier) sibling of the declared name:
final TextEditingController _title = TextEditingController();
-> initialized_identifier[ identifier "_title",
identifier "TextEditingController", selector ]
The five graph-node rules that capture fields and top-level variables matched
`(identifier) @name` without the first-child anchor, so @name bound to both
siblings and the query minted a phantom Property/Variable named after the TYPE
alongside the real declaration. On dart-flutter-conduit that produced a
`Property TextEditingController` next to the genuine `_title` / `_body` in
editor_screen.dart and login_screen.dart.
static_final_declaration has the same shape, so class statics and top-level
final/const were affected too, as were top-level `var`/`final` variables. All
five rules now anchor @name with `.`, matching the mirror rules in
languages/dart/query.ts which already anchored.
Verified against the vendored grammar: the phantoms disappear and every real
declaration is still captured (_title, _body, nullable field, static final,
uninitialized field, top-level final, top-level var). End to end on
dart-flutter-conduit: Property nodes 108 -> 106, type-shaped names 2 -> 0,
real fields unchanged.
The new test loads the grammar via createParserForLanguage rather than
loadLanguage: loadLanguage resolves to void, so the surrounding
`if (!(await loadDartOrSkip())) return;` idiom is always falsy and skips the
body. Confirmed as a negative control -- reverting only the query change makes
the new test fail on the exact phantom.
* chore(autofix): apply prettier + eslint fixes via /autofix command
* Address PR review feedback (#3224)
Correct the RHS_ONLY_TYPES comments so they state the capture invariant
instead of claiming those names appear only as constructor callees.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3224)
Build the Dart query with Parser.Query and parser.getLanguage() so the
test no longer casts the tree (or Query/captures) through any.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(fts): share skip-FTS helpers and capability defaults
Keep analyze, repair, and HTTP open paths on one option/capability object so the same stamp cannot drift.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lbug): keep initLbug options as skipFts-only
Restore the public initLbug and ensureFtsRowDmlSafe option shapes so one-arg callers stay on the pre-#3263 contract.
Co-authored-by: Cursor <cursoragent@cursor.com>
* revert(lbug): drop LbugInitOptions alias that tripped contract-drift
Keep the session option objects as inline types so initLbug's public signature matches main byte-for-byte.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(analyze): add explicit FTS opt-out
* chore(autofix): apply prettier + eslint fixes via /autofix command
* fix(analyze): treat flag and env FTS opt-out as one mode
Avoid a same-commit rebuild when only the skipReason discriminator
changes, and advertise disablement on dirty meta before leftover
indexes are wiped.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* perf(mcp): avoid O(n) git spawns on tools/list with many repos
toolSchemaRepoRequirements called listAllowedRepos -> listRepos -> checkStalenessAsync for every registered repo. With ~200 repos, this spawned 200 parallel git rev-list processes on every tools/list discovery call, causing a ~30s delay.
Replaced with a lightweight countRepos() method that reads the registry file once without spawning git processes, preserving full staleness checks for list_repos.
* Address PR review feedback (#3259)
Count the validated registry in countRepos so tools/list cannot advertise a multi-repo schema for ENOENT ghosts, and update the unrestricted listTools mocks to that contract.
Note: pre-existing failure in update-notice.test.ts (missing dist/cli/mcp.js) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add a 200-repo tools/list bench for the countRepos path (#3259)
Pin the #1363 comparison (listRepos git fan-out vs validated countRepos / listTools) in-tree so the latency claim can be re-run. Also drop the change-history comments on the unrestricted schema arm.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Gate the tools/list bench with baselines and CI --check (#3259)
Exact registry/schema floors plus ratio timing, no millisecond ceiling, so restoring listRepos() on tools/list fails CI.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3259)
Align unrestricted tools/list schema flags with the refreshed registry snapshot, and make the bench reject a non-positive BENCH_REPS and isolate fixtures by N.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* Address PR review feedback (#3259)
Isolate the tools/list bench from GITNEXUS_MCP_READ_ONLY and create the default fixture under mkdtempSync so CodeQL is not looking at a predictable /tmp path.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>