Commit graph

1062 commits

Author SHA1 Message Date
Parafee41
795cf0e151
fix(swift): resolve inherited protocol extension calls (#3309)
* fix(swift): resolve inherited protocol extension calls

* fix(scope): gate inherited implicit receiver lookup

* fix(swift): resolve call result types by exact callee

* test(swift): align cache and local call expectations

* fix(scope): reconcile replay diagnostics

* fix(swift): preserve exact callable return types

* fix(swift): capture throwing async call results

* test(swift): refresh capture golden

* test(swift): refresh scope capture baseline

* fix(swift): require explicit callable returns

* fix(scope): preserve duplicate return metadata

* chore(scope): align index documentation

* Address PR review feedback (#3309)

Stamp only protocol/class-extension members (nested QN + SPM buckets),
arity-narrow implicit-this across MRO, and keep Swift type peeling out
of shared workspace-index via stripTypePreservingDecoration.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3309)

Stamp extension members even when the extension declares a nested type, keep inherited class members ahead of protocol-extension defaults, and report replay-only interface-dispatch fan-out drops.

Note: pre-existing failure in gitnexus tsc against an older gitnexus-shared dist not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3309)

Walk inherited implicit-this owners nearest-first so a nearer override wins, and tighten Swift owner-stamp tests.

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>
2026-09-19 07:44:35 +01:00
Gergő Magyar
ba39d5c009
feat(analyze): expose process-detection budget overrides (#3324)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
* 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>
2026-09-18 13:37:15 +01:00
Gergő Magyar
9d95af9fc3
refactor(analyze): move detected-branch sanitization off CLI config (#3325)
* 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>
2026-09-18 11:03:24 +01:00
Gergő Magyar
a2e1710ac0
fix(embeddings): stop Caching embeddings OOM on large incremental analyze (#3310)
* 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>
2026-09-17 16:18:29 +01:00
Matt
8b21ce3b95
feat(auto-sync): preserve PDG indexes across updates (#3290)
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Skill copy sync / shipped skills drift guard (push) Has been cancelled
* 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>
2026-09-16 20:12:45 +01:00
Abhinav Pandey
0ede6ae501
fix(rust): respect Cargo target boundaries in name fallback (#3294)
* fix(rust): respect Cargo target boundaries in name fallback

* fix(rust): read Cargo sources through validated file descriptors

* fix(rust): require matching imports for crate-root guesses

* fix(rust): require Cargo root identity for cross-target imports

* fix(rust): enforce Cargo identity across fallback imports

* fix(rust): keep Cargo membership on typical derive/macros (#3294)

Abort only genuine unknown expansion, ignore Cargo artifact layouts instead of every `target` path segment, and require a covering import for unanswered crate-root name fallback so #3253 still holds on ordinary Rust sources.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(rust): gate Cargo target membership in CI (#3253)

Add a parse-dispatch-rounds-style bench so derive/macro abort, target-path globs, and superlinear membership walks fail in CI instead of staying graph-invisible.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(rust): verify Cargo macro and re-export evidence

* style(bench): format Cargo membership benchmark

* fix(rust): require imports for cross-file root guesses

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 11:36:07 +01:00
Gergő Magyar
ebbcd5b0b4
fix(mcp): name the indexed ref on hot read tool staleness (#3291) (#3293) 2026-09-16 07:29:04 +00:00
Gergő Magyar
ac9a4e9abd
fix(embeddings): keep ONNX off the install and analyze critical path (#3287)
Some checks are pending
Gitleaks / gitleaks (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* 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>
2026-09-15 12:11:47 +01:00
Gergő Magyar
21a52af1d4
fix(lbug): ship FTS per-platform and recover in-place native aborts (#3274)
* 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>
2026-09-14 08:52:24 +01:00
mengkaka
79543c8f83
feat(storage): add configurable index storage and content retention tiers (#3060)
* 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>
2026-09-12 20:31:55 +00:00
pcaceres-hazloagil
a8736a07d0
fix(lbug): checkpoint race in pool-adapter.ts + pin @ladybugdb/core to 0.18.3 (#3189)
* 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>
2026-09-12 14:49:21 +01:00
Gergő Magyar
ceaff27c1e
fix(parse-cache): retire a chunk whose durable generation could not be reset (#3271)
* 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>
2026-09-12 12:16:45 +01:00
azizur100389
3f5ca8cdb7
fix(storage): guard stale file-lock reclamation (#3234)
* 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>
2026-09-12 11:47:12 +01:00
Gergő Magyar
1f64becb30
fix(mcp): reject unknown tool arguments and honor depth (#3267) 2026-09-12 06:55:14 +00:00
Parafee41
68eca0ced8
fix: map deleted files to indexed symbol ranges (#3269)
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-12 06:14:13 +00:00
Sumiteshwark
68c42009df
fix(dart): anchor @name so a constructor initializer stops minting a … (#3224)
Some checks are pending
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Skill copy sync / shipped skills drift guard (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
* 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>
2026-09-11 21:05:09 +01:00
Gergő Magyar
af2d9aec15
refactor(fts): share skip-FTS helpers and capability defaults (#3263)
* 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>
2026-09-11 20:17:18 +01:00
azizur100389
f8036ac349
feat(analyze): add explicit FTS opt-out (#3205)
* 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>
2026-09-11 14:05:28 +00:00
Aakash Sharma
d1a3edd333
perf(mcp): avoid O(n) git spawns on tools/list with many repos (#3259)
* 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>
2026-09-11 13:18:28 +01:00
Gergő Magyar
7bbaf6b73b
fix(review): fail closed on foreign embedding identity and vector-width drift (#3260)
Applied from a ce-code-review pass over this branch (run 20260911-053832-b49f88d6).

- `embeddings sync` now gates EVERY checkpoint kind on embedding identity.
  `unverified-count` was exempted, but `decideEmbeddingResume` abandons that
  kind before it compares identity, so the exemption was the only thing
  keeping a foreign model from filling holes beside the old model's vectors —
  two vector spaces in one CodeEmbedding table, reported healthy. The test
  that asserted this run succeeds now asserts the refusal.

- `embeddings sync` refuses when the index's recorded vector width differs
  from this run's. The column is FLOAT[N] fixed at build time and the pipeline
  deletes each batch's stale rows immediately before inserting, so a width
  change deleted rows it could not re-insert. `analyze` already forces a
  rebuild on the same mismatch; only a rebuild can retype the column.

- `GITNEXUS_EMBEDDING_RETRY_TIMEOUTS` parses with the repo's truthy convention
  (`1`/`true`/`yes`). The integer parser threw on `true`, so the conventional
  spelling hard-failed every embedding call rather than enabling the flag or
  leaving it off. README names the accepted spellings.

- One `persistMeta` write path replaces three inlined metadata
  read-modify-writes; #2790 traced two production drifts to hand-copied
  writers of these exact fields.

- Tests: the pipeline mock now invokes `onCheckpointWindowStart`/`onCheckpoint`,
  so the resume contract actually executes under test. Added coverage for the
  incomplete-index refusal, the partial-checkpoint branch, and the body-read
  timeout retry site that the existing opt-in test never reached.

Verified: tsc --noEmit clean; 99 tests pass across the three affected suites;
prettier clean.

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 07:37:08 +00:00
Ankit Verma
8bd71c8335
feat(staleness): report diverged and unknown index state instead of fresh (#3257)
* feat(staleness): report diverged and unknown index state instead of fresh

A staleness check collapsed every git failure into { isStale: false,
commitsBehind: 0 }. On a branch-pinned serve clone, a failed re-index
leaves the recorded commit orphaned by the --depth 1 fetch; once the
reflog expires and gc prunes it, rev-list fails and the index silently
reads as fresh while still behind.

checkStaleness / checkStalenessAsync now return an additive status:
current, behind, diverged (rev-list failed but HEAD resolved and differs
from lastCommit) or unknown (HEAD unresolvable, no lastCommit, timeout).
isStale and commitsBehind keep their values in every case.

One payload builder (core/staleness-status.ts) feeds MCP list_repos, the
hot read tools and /api/repos + /api/repo. diverged carries a hint and no
invented count; unknown appears on listings only. The helpers live in a
pure module so existing vi.mock stubs of git-staleness stay valid.

gitnexus group status no longer prints "-1 commits behind".

Fixes #3256

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Address PR review feedback (#3257)

- Document the `current` arm `fromHead` can return after a failed `rev-list`.
  The `staleness-status.ts` status list, the `fromHead` docstring and the
  `stalenessForTool` comment each named only `diverged`/`unknown`, so all three
  described a contract the helpers do not have.
- Document both `unknown` rows on the group status type: no recorded commit
  (`indexStale: true`, `commitsBehind: -1`) and a git probe that could not
  answer (`indexStale: false`, `commitsBehind: 0`), which do not agree on
  either field.
- Update both shipped `gitnexus-guide` copies to the new wire shape
  (`{ status, commitsBehind?, hint? }`), add a `diverged` example carrying no
  count, and state `unknown` is reported only by the `list_repos` listing.
  Presence now means "not `current`", not "behind N". Pinned in
  shipped-skills-sync.
- Lock the timed-out `rev-list` short-circuit with staleness-timeout.test.ts:
  it asserts `unknown` from exactly one git spawn, so removing the `killed`
  guard (which would probe HEAD again and double the #3232 bound) now fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Claim only the uncountable gap in the diverged hint (#3257)

`fromHead` reaches the `diverged` branch on any non-timeout `rev-list` failure
where `rev-parse HEAD` resolves to a different SHA. It compares the two SHAs and
runs no reachability check, so a transient object-read failure lands there while
`lastCommit` is still an ancestor of HEAD — and the hint told the operator the
commit was gone from history. `staleness-status.ts` already documents `diverged`
as only "provably not at HEAD; the count is unknown", so the string was also the
one place contradicting its own contract.

The hint now states what the check established and hedges the usual cause:
"Index is not at HEAD and the commit gap could not be counted — the recorded
commit may no longer be in this clone's history."

Updated with it: the `staleness.test.ts` diverged assertion, and the `diverged`
example plus its lead-in sentence in both shipped `gitnexus-guide` copies (still
byte-identical). `mcp/resources.ts` needs no change — it interpolates
`staleness.hint`, holding no copy of the text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(staleness): cover the remaining #3256 review test gaps

The tri-review's lower-priority gaps, each now locked:

- staleness-fallback.test.ts: after a failed (not timed-out) rev-list,
  both helpers report `current` when HEAD alone still resolves to the
  indexed commit, `diverged` when it resolves elsewhere, and `unknown`
  when it cannot be read, each from exactly rev-list + rev-parse. This
  complements staleness-timeout.test.ts: a timeout spawns once, any other
  failure probes HEAD.
- list_repos carries the listing-only `unknown` and a count-free
  `diverged`; `current` carries nothing.
- group status: a resolvable repo with no recorded commit composes to
  indexStale: true, commitsBehind: -1, status: unknown and renders as
  "STALE (? commits behind)", through the real groupStatus path rather
  than a hand-built row.

Each test was checked against a mutation of the code it guards (the
fromHead current arm, the group no-commit literal, list_repos
includeUnknown); every mutation fails its test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: tech-admin3 <tech-admin@kodnest.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-09-11 08:07:37 +01:00
Parafee41
c9b391357e
fix(group): detect function-local Python imports (#3254)
* fix(group): detect function-local Python imports

* fix(group): parse Python imports structurally

* fix(group): use safe Python parser wrapper

* fix(group): isolate Python parse timeouts

* fix(group): skip recovered indented Python from-imports

Error-recovered trees can promote unclosed-docstring lookalikes into
real import_from_statement nodes. Drop those indented imports, bound
file reads, log parse timeouts, and add fingerprint floors for the
tree-sitter scan.

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>
2026-09-11 06:23:52 +00:00
panbergco
32a5fc3c4f
feat: Make long HTTP embedding jobs resumable (#3065)
Some checks are pending
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
* Make long HTTP embedding jobs resumable

Retry endpoint timeouts when explicitly configured, creating a fresh abort signal for every attempt. Add gitnexus embeddings to fill an existing index in place: every successful batch is durable, and rerunning skips vectors whose content hash still matches instead of rebuilding the structural graph.

* Address PR review feedback (#3065)

Hold the per-index lock around embeddings sync, fail closed on a foreign-identity partial checkpoint, and refuse to create an empty DB from leftover metadata. Use the canonical embedding count, keep closeLbug from masking the real error, load hashes instead of full vectors, and document GITNEXUS_EMBEDDING_RETRY_TIMEOUTS.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Split embeddings sync off the install module so install no longer loads Ladybug.

Share the opt-in TimeoutError wrap between fetch-throw and .json(), and cover a non-file LadybugDB path.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Describe embeddings sync checkpoints as periodic and report cached node count.

Pipeline checkpoints every 5,000 nodes, and fetchExistingEmbeddingHashes is keyed by nodeId, not vector rows.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Leave an unverified-count checkpoint when embeddings sync cannot count rows.

A finished run must not keep an interrupted window marker; the next sync should re-derive the count instead of hitting the identity gate.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

---------

Co-authored-by: panbergco <panbergco@users.noreply.github.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>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-10 19:48:50 +01:00
auyua9
a4769439dd
fix(parse): retain metadata-only diff files (#3251)
* fix(parse): retain metadata-only diff files

* Address PR review feedback (#3251)

Keep detect_changes honest on C-quoted, TAB-terminated, and ambiguous
diff --git dests, and fail closed when a header cannot be parsed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify parseDiffHunks dest-prefix helper (#3251)

Drop the unused a/ prefix arm and the redundant empty-parse unparsed-header check.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(parse): ignore +++ inside hunks and accept mixed-quoted git headers

Stop treating hunk-body lines as file headers, and parse independently
C-quoted src/dest tokens on diff --git.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: ming <silverchris@foxmail.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 18:27:00 +01:00
Yahoo
0edf9ce0ff
fix(ruby): guard gem requires with dependency metadata (#3096)
* docs(plans): add ruby gem require boundary plan

* fix(ruby): guard gem requires with dependency metadata

* fix(ruby): scope gem sources by manifest

* test(ruby): model resolved lockfile specs

* fix(ruby): stop local gem suffix fallthrough

* docs: remove Ruby resolution plan

* test(ruby): gate gem resolution correctness and scaling

* test(ruby): align gem benchmark baseline with ratio gates

* Address PR review feedback (#3096)

- Strip a trailing .rb so local and external gem prefix matching follows Ruby's optional-suffix require.

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>
2026-09-10 16:04:11 +01:00
JaysonAlbert
e7141ab0c1
fix(schema): persist Spring constructor-to-bean injection edges (#3239)
Co-authored-by: Jayson Albert <momeijw@gamil.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-10 16:03:53 +01:00
Kevin Rajan
3236e2fbcd
fix(cobol): prefer copybook dirs so COPY EXTERNAL does not hit vendor decoys (#3240)
* fix(cobol): prefer copybook dirs so COPY EXTERNAL does not hit vendor decoys

COPY of an out-of-repo member first-won any same-named .cpy, so
vendor/EXTERNAL.cpy became a live cobol-copy IMPORTS edge. Share one
resolver between census and the regex processor: prefer copybooks/cpy/copy
plus the importer dir when present, else fail-open. Drop COBOL KNOWN_GAPS.

Fixes #2967

* bench(cobol): update depth budget for copybook-dir preference (#2967)

COBOL resolver now prefers well-known copybook directories (copybooks/,
cpy/, copy/, plus importer dir) over vendor paths when resolving COPY
statements. This intentional behavior change moves the resolver from
depth-free (prior measured ~0.885) to depth-sensitive (measured 1.751
on CI run 34394116972), because the new preferredCopybookDirs check
walks path components.

- Raise depth_budget from 1.6 to 2.4 (~1.37x the measured ratio)
- Update _measured.depth_ratio from 0.885 to 1.751
- Add _cobol_copybook_dir_preference_2967 note documenting the change

The COBOL fingerprints already reflect the new target set behavior
(vendor/EXTERNAL.cpy correctly returns null when a copybook dir is
present) per commit fc9e8270.

Co-authored-by: Kevin Rajan <kvnloo@users.noreply.github.com>

* bench(cobol): update baselines for copybook-dir preference (#2967)

COBOL preferred-dir filtering now affects resolution outcomes. The
unique-arm layouts (mixed copybooks/src dirs) drop from 1153 to 442
resolved as files outside preferred directories are correctly filtered.
The collide arm (all files in svc${d}/copybooks) keeps 1153 resolved
because ALL files remain in the preferred class.

- Update small/deep resolved: 1153 → 442
- Update small/large/deep fingerprints for new target set
- Raise heap_bound_bytes.cobol: 3500000 → 6200000 (1.5x measured 4112464 B)
- Add measure.mjs exception: collide legitimately differs from small
- Update _heap_bound_note with new cobol measurement context
- Expand _cobol_copybook_dir_preference_2967 note to explain collide delta

The collide arm now measures collision behavior within the preferred
class rather than across mixed layouts — an intentional outcome of the
preferred-dir semantics rather than a corpus defect.

Refs #2967

Co-authored-by: Kevin Rajan <kvnloo@users.noreply.github.com>

* fix(cobol): P1-A stem key with uppercase extensions, P1-B polyglot preferred-class latch

P1-A: Use raw extension for path.basename so CUSTREC.CPY keys as CUSTREC
  - Before: path.basename('CUSTREC.CPY', '.cpy') -> 'CUSTREC.CPY' (no strip)
  - After: path.basename('CUSTREC.CPY', '.CPY') -> 'CUSTREC' (stripped)
  - Processor used raw extension; census now matches

P1-B: Latch preferred-class only from copybook-tier paths
  - Before: docs/copy/README.md triggered hasPreferredDir = true
  - After: check preferred-dir only after extension filter
  - Processor receives polyglot allPathSet; test added

Test coverage:
  - cobol-copy-external-imports.test.ts: processor pins for both fixes
  - cobol-import-target-parity.test.ts: resolver parity updated to new behavior
  - All existing COBOL tests pass

Co-authored-by: Kevin Rajan <kvnloo@users.noreply.github.com>

* test(cobol): fix Mixed.CPY integration test for P1-A behavior

The integration test in cobol-import-index-reuse.test.ts had outdated
expectations from before P1-A. With P1-A, path.basename uses the raw
extension, so Mixed.CPY is now keyed as MIXED (extension stripped).

Before (pre-P1-A):
- Mixed.CPY → keyed as MIXED.CPY (uppercase ext not stripped)
- COPY MIXED.CPY → found, COPY MIXED → null

After (P1-A):
- Mixed.CPY → keyed as MIXED (raw ext stripped)
- COPY MIXED → found, COPY MIXED.CPY → null

Updated test expectations to match P1-A behavior. All three validation
tests now pass:
- cobol-import-index-reuse.test.ts (integration, index reuse)
- cobol-copy-external-imports.test.ts (processor P1-A/P1-B pins)
- cobol-import-target-parity.test.ts (resolver parity)

Fixes PER-994 CI failure on abhigyanpatwari/GitNexus#3240.

Co-authored-by: Kevin Rajan <kvnloo@users.noreply.github.com>

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kevin Rajan <kvnloo@users.noreply.github.com>
2026-09-10 15:42:59 +01:00
Abhinav Pandey
16307a11be
fix(flows): exclude guessed call edges from derived graph flows (#3193) 2026-09-10 13:15:30 +01:00
Abhinav Pandey
79f210c5b5
fix(go, workspace): resolve test siblings and tighten package discovery (#3191)
* fix(go): resolve test helpers through package sibling tables

* fix(workspace): discover source entries from scoped static configuration

* Address PR review feedback (#3191)

- Align sibling comments with the no-bare-name partition and drop the stale same-dir fallback claim.
- Pin `_test.go` dot-import wildcard augmentation so a revert to nonTestFiles cannot stay green.

Note: pre-existing failure in worker-pool startup crashes in the full vitest suite not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: tighten workspace discovery and index Go sibling bindings

Skip leftover test/ workspace roots and extra Vite configs. Publish
same-package Go names from per-package indexes instead of pairing every file.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 12:22:12 +01:00
Abhinav Pandey
2220f4d851
fix(resolution): label fallback guesses and preserve export visibility (#3190)
* fix(resolution): distinguish name guesses and preserve export visibility

* test(go): keep method enrichment fixture in one package

* fix(resolution): address split review edge cases and evidence reporting

* fix(exports): recognize imported and expression-local receivers

* fix(resolution): align export and target evidence with language scope

* fix(ingestion): preserve lexical import provenance through resolution

* test(ci): rebalance Windows shards from measured slow suites

* Address PR review feedback (#3190)

- Label constructor unique-name guesses as global-name-fallback and run language vetoes
- Tighten Go qualified, Rust crate::, and Ruby class-reopen fallback guards
- Ignore for-loop shadowed CommonJS receivers and exclude guesses from the resolved-call census
- Refresh FinalizeOutput and hook docs for lexical binding scopes

Co-authored-by: Cursor <cursoragent@cursor.com>

* Tighten review-feedback leftovers on fallback visibility.

Qualified Go calls still respect export and test-package rules, nested Rust src/ stays a module segment, and top-level conditional this.x is treated as CommonJS.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

* Address PR review feedback (#3190)

- Distinguish Swift package prefixes when comparing target modules
- Document lexical import binding and handledSites refusal marking
- Drop the stale ci-scope-parity workflow claim and prototype-safe export verdicts

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3190)

Supply caller source on Ruby visibility cases so they exercise the named allow branches instead of the missing-text bypass.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(resolution): label unique constructor types as name guesses

A workspace-unique class hit in findClassBindingInScope was treated as
an in-scope bind, so Go/JS constructor-form sites skipped the guess
label and the Go unexported veto.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(resolution): keep qualified constructors precise after unique-name split

Bare constructor unique-name hits stay guesses so Go can veto an
unexported type. A written qualifier is now carried as rawQualifiedName
so `new pkg.Foo()` and `models.Box[T]{}` can still recover the unique
class without that veto.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(bench): rebaseline Go/Java scope-capture fingerprints for constructor qualifiers

Generic Go composite literals and qualified Java `new pkg.Foo()` now
carry @reference.qualified-name on existing constructor matches.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(resolution): treat import-reached unique constructors as precise

C++ #include and Rust re-exports do not mint a lexical class binding.
A unique type in an imported file (or imported directory) is therefore
a real bind, not a name guess, so those CALLS edges stay import-resolved.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(resolution): require named or resolved imports for constructor precision

Bare Go Box[T]{} is not package-qualified, and a sibling-file import of a different name is not constructor visibility.

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>
2026-09-10 11:38:19 +01:00
ClayLeee
ab25b86807
fix(typescript): resolve tsconfig paths aliases on Windows (#3203)
* fix(typescript): resolve tsconfig `paths` aliases on Windows

`rebaseTarget` turns a `paths` target that `path.resolve` produced back into
a repo-relative one, and it recognised the wildcard suffix only as `/*`. On
Windows the resolved target is `C:\repo\src\*`, so the check never matched,
the bare-`*` branch stripped the star, `path.relative` ate the trailing
backslash, and every alias target came back as `src*`. `substituteStar` then
built `srclib/date` from `@/lib/date`, nothing resolved, and every alias
import was dropped as external — no IMPORTS/CALLS edge for anything reached
through `@/`, so `impact` answered UNKNOWN for a repo's whole shared layer.

Normalise the separator before looking at the suffix. `tsconfig-index.test.ts`
already asserts the `src/*` shape and fails 4 of 13 cases on Windows without
this; it only ever ran on Ubuntu, so add it to the cross-platform lane.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(typescript): make `paths` rebasing assertable from any runner

`rebaseTarget` reads the platform separator, but the only suite covering it
builds real directories — so on Ubuntu it sees `/` whatever the code does with
`\`, and only the windows-latest lane can fail it. That is how an alias bug
affecting every Windows install reached a green CI in the first place.

Thread an injectable `pathApi` through `rebaseTarget` and `repoRelative` — the
seam `isInside` and the `\\?\` prefix guard already use — and add a
fixture-free suite that pins the win32 and POSIX branches explicitly. Deleting
the separator normalisation now fails on every runner rather than only on
windows-latest.

Registered beside its fixture sibling in the cross-platform lane so the two
halves of the same rule stay discoverable as one group.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(typescript): emit a bare `*` for a repo-root `paths` target

A target naming the repo ROOT — `"*": ["./*"]` under `"baseUrl": "."` — rebases
to an EMPTY repo-relative prefix, so only the suffix survived and the encoding
came out as `/*`. `substituteStar` turns that into `/lib/date`, and
`resolveFile` matches repo-relative keys without stripping a leading slash, so
the indexed `lib/date.ts` misses and the alias is dropped as external.

Emit the bare `*` for that case instead, which substitutes to `lib/date` and
resolves. This is the shipped POSIX encoding as much as the Windows one, and it
is what the Windows path emitted by accident before the separator was
normalised — so the normalisation does not narrow what already resolved there.
The common `"@/*": ["./src/*"]` is untouched: it has a real prefix and stays
`src/*`.

Pinned at all three levels the encoding passes through: the rebaser, on both
path flavours; the loader, through a real fixture; and resolution, where `*`
finds the file and `/*` does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(test): say why the root-wildcard fixture declares no `baseUrl`

The comment introduced the fixture as `"baseUrl": "."`, which the loader
encodes as `''`, while the fixture passes `null` — a different scope, since
`null` means the config declares no baseUrl at all and takes the paths arm
alone.

`null` is the right fixture and the comment was the wrong half: with `''` the
baseUrl arm resolves `packages/utils/src/index` by itself, so the negative case
would return null for a reason unrelated to the target encoding. Verified by
substitution — `''` makes that assertion report the file instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-10 07:14:53 +01:00
Navid EMAD
506432017f
fix(zig): model callable-value references, and stop reporting their absence as exact (#3219)
Some checks are pending
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Scorecard / Scorecard analysis (push) Waiting to run
* fix(impact): stop reporting 'exact' over unmodelled callable-value references

A function named in VALUE position — `bridge.accessor(Element.getNamespaceUri,
null, .{})`, `{ onClick: handler }`, a comparator handed to a sort — is
registered somewhere rather than called. The registration is modelled (a
`value-ref` site becomes a USES edge; Kythe `ref` vs `ref/call`, Joern
METHOD_REF), but the invocation THROUGH the stored value is not: it happens
later via a struct field, a registry lookup or comptime reflection.

impact()/context() nonetheless reported such a target as `epistemic: 'exact'`.
For lightpanda-io/browser that meant the DOM `Element.namespaceURI` accessor
came back with two internal callers, LOW risk and a claim of completeness —
worse than no answer, because 'exact' tells the reader not to look further.
tools.ts defines 'lower-bound' as "the walk provably missed callers", which is
precisely this case.

computeEpistemicBoundary now probes for inbound USES edges stamped with the
value-ref reason and hedges when it finds any, contributing a boundary note and
a new `causes.callableValueReferences` (unit: distinct referrer symbols).

Read from the graph, not from index metadata: unlike a dropped receiver — which
leaves no edge to find and therefore needs a persisted summary — a value
reference IS in the graph. So the signal needs no re-index and no analyzer
change, and it works on indexes written before this commit.

The cause gets its own slot rather than joining `dispatchBoundary`: a value
referrer is neither an implementation nor an interface-level consumer, and the
two differ in what the reader should do about them — a dispatch boundary is
irreducible, a callable value usually becomes traceable once the provider
models the store/load that carries it.

Language-neutral: every provider that emits a `value-ref` capture participates.
The writer and the reader now share VALUE_REF_EDGE_REASON, because drift
between them would fail silently — the probe would match nothing and every
answer would go back to claiming certainty.

* fix(zig): emit `value-ref` for a callable named in value position

`mapReferenceKindToEdgeType` has handled the registration-vs-invocation case
since #2437, and TypeScript, JavaScript and C++ all emit `value-ref`. Zig
emitted zero — so a Zig function handed somewhere as a VALUE was absent from
the graph entirely.

That is not a corner: Zig's JS bridge is built out of this one shape,

    pub const namespaceURI = bridge.accessor(Element.getNamespaceUri, null, .{});

and `bridge.{accessor,function,indexed,…}` appears 2,047 times across 257 files
in lightpanda-io/browser — the project's whole JS<->Zig surface, none of it
reaching the graph. `impact` on `Element.getNamespaceUri`, which IS the DOM
`Element.namespaceURI` accessor, answered with its two in-file callers.

Three query rules, tagging `@reference.value-ref` on: a bare identifier in
argument position (`bridge.accessor(_tagName, …)`), a qualified one
(`bridge.accessor(Element.getNamespaceUri, …)`), and a const binding initialiser
(`pub const defaultHandler = onReset;`). Everything downstream already existed —
no new edge type, no schema change, no capture-machinery change, no baseline
edited.

One grammar detail is load-bearing: in tree-sitter-zig, call arguments are
DIRECT children of `call_expression` — there is no `arguments` node, only
builtins have one — so the callee has to be consumed explicitly by `function:`.
Without that binding the same rule also matches the callee of `foo(bar)` and
mints a USES edge shadowing the call's own CALLS edge. There is a test for it.

The rules are deliberately broad — `js.Bridge(Element)` and `register(count)`
match too — because the callable gate in the property-dispatch pass
(Function/Method/Constructor only) is the filter, the same design that keeps
TypeScript's `{ port: DEFAULT_PORT }` from registering anything. Measured on the
real corpus: all 3,169 emitted edges land on a Method (3,146) or Function (23).

Deliberately NOT modelled: the terminal invoke. The value reaches
`Accessor.init` -> a struct field -> `Factory.zig` reflection (`inline for`,
`@typeInfo`) -> `Caller.zig`'s `@call(.auto, func, args)` over `func: anytype`.
Resolving that needs comptime evaluation. The point is to stop dropping the
reference; the shortfall is now reported as `epistemic: "lower-bound"` by the
preceding commit instead of being papered over.

Measured on lightpanda-io/browser (698 .zig files), wiped index both times:
30,222 nodes / 71,070 edges -> 30,222 nodes / 74,229 edges. The delta is
entirely `USES` (3,169 after vs 10 before, and every one is a value-ref); CALLS,
ACCESSES, IMPORTS, HAS_METHOD, DEFINES and the rest are unchanged — purely
additive, no node invented.

The fixture joins the existing `zig-idioms` corpus rather than adding a new
one, because `bench/receiver-resolution` uses `test/fixtures/lang-resolution` as
its `--check` corpus. That gate, `scope-capture` (15 languages),
`zig-cross-file-resolution` and every other bench `--check` pass unchanged.

* fix(review): resolve qualified value references through their owner, and stop
hedging on registrations the analyzer already followed

Five findings from the review bot on #3219; four valid, all addressed.

1. QUALIFIED VALUE REFERENCES RESOLVED BY TAIL NAME (the serious one).
   `bridge.accessor(Element.getNamespaceUri, …)` was resolved with
   `findCallableBindingInScope(site.inScope, site.name, …)`, which never sees
   the receiver and gives LOCAL bindings precedence. Reproduced:

       const Element = @This();
       pub fn getThing(...)                 // main.getThing
       pub const JsApi = struct {
           fn getThing(...)                 // JsApi.getThing
           pub const thing = bridge.accessor(Element.getThing, null, .{});
       };

   emitted `USES JsApi → JsApi.getThing` — a WRONG edge, which is worse than
   the missing edge this PR set out to fix.

   `resolveValueRefTarget` now resolves a site carrying an explicit receiver
   through `findClassBindingInScope` → `findOwnedMember` (the machinery
   `receiver-bound-calls` already uses), gated on `CALL_TARGET_TYPES` because
   `findOwnedMember` also answers with fields. A bare site keeps the lexical
   walk, which is what an unqualified name means. When the owner cannot be
   resolved the site is DECLINED rather than falling back: declining costs a
   reference, falling back mints a confident edge to the wrong function, and
   the missing reference is now reported as `lower-bound` anyway.

   Cost on lightpanda-io/browser: value-ref edges 3,169 → 2,799 (−12%). Those
   370 were tail-name coincidences, not registrations — all 94 `Element.zig`
   `JsApi` entries survive, the cross-container case resolves and is correctly
   attributed (`IntersectionObserverEntry.JsApi` → `IntersectionObserverEntry.
   getTarget#0`, not the enclosing observer), and both acceptance probes are
   unchanged: `getNamespaceUri` 7/3/LOW/lower-bound, `getTagNameLower`
   31/10/HIGH/exact.

2. A FAILED PROBE READ AS "NO BOUNDARY". `.catch(() => [])` turned an
   unanswerable query into count 0 and no note, so `exact` could be published
   on the strength of a question that was never asked. It now returns `null`
   and emits a boundary note. This file's own `loadMeta` comment states the
   rule: a probe failing must never read as certainty.

3. NOT EVERY VALUE REFERENCE IS AN UNMODELLED INVOCATION. Where
   `emitPropertyDispatchCalls` sweep 2 synthesized the dispatch (reason
   `property-dispatch`), the walk did NOT provably miss the caller, so hedging
   was noise over an answer that was computed — and a signal that fires on
   every JS/TS hook table stops carrying information. A second probe excludes
   those targets. Zig never sets a property key, so the motivating case is
   untouched. The exclusion is symbol-level, not per-edge, because the graph
   does not record which registration produced which synthesized call; that
   residual is documented at the call site.

4. `LIMIT 50` SILENTLY UNDERSTATED THE COUNT. `rows.length` over a capped row
   set published a ceiling as the documented symbol count. Replaced with
   `COUNT(DISTINCT other.id)` — bounded work without a bounded answer, the
   shape `countByType` in the same file already uses.

5. THE TEST DID NOT PIN THE CALLER COUNT its own comment promised. Added
   `expect(result.impactedCount).toBe(2)`.

New regression tests: qualified references bind the written owner and not the
nearer lexical match; an unresolvable receiver emits nothing rather than a
wrong edge; a dispatch-modelled registration stays `exact`; a probe that cannot
run hedges instead of claiming certainty.

Known limitation, pinned by a test rather than left implicit: when a `@This()`
alias's NAME differs from its container's (`const Element = @This();` inside
`main.zig`), the receiver does not resolve and the reference is declined. The
provider has `rewriteZigThisAlias` for this, but it is applied to type nodes
and extending it to reference receivers would change existing CALL-site
behaviour. Lightpanda's `const Foo = @This();` in `Foo.zig` convention makes
the names coincide, which is why the corpus is unaffected.

* fix(review): bump the parse-cache schema and resolve module-owned value references

Review round 2 on #3219. Four inline items, all reproduced against the
worktree before deciding.

P1 — the new captures could stay INERT on a warm parse cache. Adding
`@reference.value-ref` rules to `ZIG_SCOPE_QUERY` changes
`ParsedFile.referenceSites`, which is a PARSE-TIME fact, but `SCHEMA_BUMP`
stayed at 93. A repo indexed before this branch and re-analyzed after it
replays the old, empty site list for every unchanged `.zig` file — `--force`
included, since shards are content-addressed — so no USES edge is emitted, the
boundary probe measures a real zero, and `impact` on a registered accessor goes
back to `epistemic: "exact"`. #3399 un-fixed on the incremental path most users
are on, with every cold-run test still green. DECISIONS D1-1's "zero changes to
… the schema" conflated the graph schema with the cache schema.

Bumped 93 -> 98, not 94: #3190 claims 94 and #3179 claims 94 through 97 in one
PR. `incremental-parse-cache.test.ts` re-pinned, with 93-97 added to the taken
list. Re-check against origin/main and open PRs immediately before merging.

P2 — a qualified value reference through a MODULE was declined with no hedge.
R1-2 resolves a written receiver with `findClassBindingInScope`, which requires
`isClassLike`; a namespace-only `@import` handle is not class-like, so

    const dom_utils = @import("dom_utils.zig");   // no `@This()` in that file
    pub const comparator = bridge.accessor(dom_utils.compare, null, .{});

resolved to nothing. That is not the conservative half of R1-2's trade-off: a
declined site emits NO edge, so there is nothing for the probe to read and
`impact` on `compare` reports `exact`. Silence, not a hedge — and the pass
comment claiming otherwise was wrong on this path.

`resolveValueRefTarget` now tries the second kind of owner a qualified name can
have. `findNamespaceValueRefTarget` reads the file's `namespace` import edges
for the handle and the target module's own `origin: 'local'` module-scope
bindings for the member — the same channel `receiver-bound-calls` Case 1
already trusts for `dom_utils.compare()`, with Case 1's three guards for Case
1's reasons: `isNamespaceNameShadowed`, local-origin bindings only, and two
distinct defs under one name resolve nothing. `CALL_TARGET_TYPES` gates module
owners exactly as it gates container owners. Language-neutral: it reads generic
namespace import edges, names no language.

Still declined, deliberately: a receiver this index knows under no name at all
(the `@This()`-alias case, owners outside the workspace). There the alternative
is a confident edge to a lexically-nearer function the source did not name.

P2 — `impact-callable-value-references.test.ts` was in neither vitest list.
It opens a real engine via `withTestLbugDB(poolAdapter: true)`, so TESTING.md
puts it in the `lbug-db` include list and the `default` exclude list; it was in
neither, so `default` also collected it into the parallel pool. Added next to
its `impact-epistemic-lower-bound` sibling in both arrays.

P3 — DECISIONS.md was stale against HEAD and embedded host paths. D2-3 still
described the `LIMIT 50` that R1-5 removed and D1-3 still described the
receiver-blind resolution that R1-2 replaced; both now carry explicit
"superseded by" pointers. The `~/code/...` and mise-node paths are replaced
with placeholders.

Two smaller corrections the new cause made necessary:

- `formatImpactResult`'s `lower-bound` header hard-coded "callers binding via
  DI / dynamic dispatch", which now contradicts the value-reference bullet
  printed directly under it. The bullets carry the cause; the header only
  states that the count is a floor.
- `tools.ts` said a `causes.callableValueReferences` of 0 means "nothing was
  missed". The dispatch exclusion is symbol-level, not edge-level, so a target
  with both a followed registration and an unfollowed escape also reads 0. The
  docs now say a 0 means "no unfollowed registration was proven".

Fixture: `src/webapi/dom_utils.zig` (namespace-only) plus three cases in
`Element.zig` — the module-qualified registration, a non-callable module member,
and a `u8` parameter shadowing the handle. The shadow case was verified to FAIL
with the guard disabled, so it is not passing for an unrelated reason.

Gates: `tsc --noEmit` clean, `npm run build` clean, prettier clean.
`resolvers/` 3,630 passed / 3 skipped; `unit/scope-resolution` 2,010 passed;
`impact-callable-value-references` 7 passed under `lbug-db`;
`incremental-parse-cache` 40 passed; eval formatters 104 passed. Bench --check
all PASS with no baseline edited: receiver-resolution, zig-cross-file-resolution,
scope-emission, callable-value-flow, scope-capture (15 languages), python-scope.

* fix(review): guard the class receiver against a shadowing binding, and pin the dispatchability partition

Review round 3 (`gitnexus-check` bot on `cf53bbaa`). Three findings, each
reproduced against the code before deciding.

R3-1 (Error, valid, REPRODUCED) — a CLASS receiver could resolve through a
shadowing value binding. `findClassBindingInScope` is a class-only walk: it
filters the scope chain by `isClassLike`, so it steps over a nearer binding that
is a value and keeps climbing — and past the chain entirely, into a
qualified-name fallback that answers with the unique workspace definition of the
name. A `u8` parameter named `Ticker`, in a file that neither declares nor
imports the `Ticker` container another file defines, therefore emitted
`register(Ticker.fire)` as a confident USES edge to that container's method.
That is exactly the wrong-edge failure R1-2 exists to prevent, arriving through
the class channel instead of the lexical one.

Fixed with `isOwnerNameShadowedBySomethingElse` — a sibling of
`isNamespaceNameShadowed` with one extra clause. The plain namespace guard could
NOT be reused: a container is often its own local declaration
(`fn make() { const Local = struct {…}; register(Local.go); }`), and reading that
binding as its own shadow suppresses precisely the resolutions this path exists
to make — the #2723 mistake, one channel over. So a scope that binds the name
answers immediately, and the answer is "not shadowed" only when one of that
scope's own bindings IS the def just resolved.

Both halves are pinned and both were verified to fail when the guard is
weakened: the parameter case fails with no guard at all, the local-container
case fails with the plain `isNamespaceNameShadowed`.

R3-2 (Warning; mechanism correct, unreachable today; fragility fixed instead) —
the dispatch exclusion could suppress an unfollowed registration. The bot is
right about the code: sweep 2 synthesizes CALLS only for a registration whose
site carried a `propertyKey`, while the exclusion zeroes the note on ANY inbound
`property-dispatch` CALLS edge. It is not reachable in the current rule set, and
the reason is measured rather than assumed: `@reference.value-ref` is emitted by
exactly three languages — JavaScript (2 rules), TypeScript (2), Zig (3) — every
JS/TS rule also captures `@reference.property-key` (both are object-literal
shapes) and no Zig rule does. A dispatchable registration is therefore always a
JS/TS one, an undispatchable one always a Zig one, and they cannot meet on one
symbol.

Rejected: splitting the edge `reason` into dispatchable / undispatchable. It is
the precise fix, but it is a graph-content change that churns whichever side
keeps the old literal — the Zig, TypeScript and probe suites all pin
'scope-resolution: value-ref' by hand as a drift canary — and it buys nothing
against a case no rule can produce.

What was actually wrong is that the exclusion's soundness rested on a
coincidence recorded nowhere, in files nobody reading `local-backend.ts` would
open. Fixed at both ends: the exclusion site now states the invariant, the three
facts it rests on and the two options for when it breaks; and
`value-ref-dispatchability.test.ts` fails the day it does — a JS/TS rule for a
bare callback argument, a Zig rule that grows a key, or a fourth language
emitting `value-ref` at all. Verified to fire (adding a property key to a Zig
value-ref rule fails the Zig case), and its rule splitter has its own guard test
so the suite cannot pass vacuously. `ZIG_SCOPE_QUERY` is exported for that test
only.

R3-3 (Nit, valid) — a test comment claimed the wrong epistemic result. The
`declines a qualified reference whose receiver cannot be resolved` case said the
shortfall shows up as `lower-bound`. It does not: with no edge there is no
evidence and the target stays `exact`. The pass docstring was corrected in round
2 and this comment was missed. It now says the decline costs the reference AND
the hedge, and why that is still the right trade.

Gates: tsc --noEmit clean, npm run build clean, prettier clean.
`test/integration/resolvers` 3,632 passed / 3 skipped (70 files);
`test/unit/scope-resolution` 2,015 passed (120 files);
`impact-callable-value-references` 7 passed under `lbug-db`. Bench --check:
receiver-resolution, zig-cross-file-resolution, scope-capture (15 languages),
scope-emission PASS with no baseline edited; callable-value-flow failed once on
its TIMING budget (2.006 > 1.9) with a byte-identical fingerprint, then passed
twice at 1.788 / 1.813 — machine load, not a regression.

* fix(review): let a file's own `@import` outrank the workspace-wide class fallback

Found by running the local preflight harness before pushing rather than after.
One of its five findings is a real hole in R2-2; the rest are documentation that
overclaimed.

R4-1 (valid, REPRODUCED) — the container channel preempted the file's own
`@import`. R2-2 added the module channel as a FALLBACK after
`findClassBindingInScope`, and that order is wrong: `findClassBindingInScope`
does not stop at the scope chain. When its `isClassLike` walk misses — and a
namespace handle binds a Module, so it always misses — it falls back to
`scopes.qualifiedNames`, a workspace-wide index, and answers with the unique def
of that name anywhere in the repo. A container named `dom_utils` in a file
`Element.zig` never imports therefore captured
`bridge.accessor(dom_utils.compare, …)`, binding
`Method:src/webapi/decoy.zig:dom_utils.compare#2` while the module channel that
would have answered correctly was never reached. R3-1's shadow guard cannot
catch it: the import binds at MODULE scope, which that guard treats as the floor.

Fixed by trying the module channel FIRST. An `@import` written in this file is
the strongest available statement about what the name means here and outranks a
global uniqueness guess; when the handle is not an import of this file the
channel answers nothing and the container path runs exactly as before. Pinned by
`decoy.zig` and a strengthened assertion on the existing module-owner test,
which fails on the old order.

R4-2 — the cause documentation named shapes nothing captures. `tools.ts`
illustrated `callableValueReferences` with "a callback argument", "a stored
function pointer" and `qsort(xs, n, sz, compareItems)`. Only Zig captures a call
argument or a const initialiser; JS/TS capture only object-literal property
values, and C has no value-ref rule, so the `qsort` example is counted in no
language. Both cause blocks now name the captured shapes and say that a bare
JS/TS callback argument is not among them, so a 0 does not rule it out.

R4-3 — the same block exempted itself from the re-index caveat this PR proves it
needs. "Read from the graph, so it needs no index-time metadata" is true of the
probe and false of the edges: an index built before a language emitted these
captures has none and reports 0 — the warm-cache failure R2-1 bumped
SCHEMA_BUMP for. It now says to re-analyze before reading a 0 as measured.

R4-4 — the dispatchability canary was narrower than its own promise. Its header
claimed it fails on "a fourth language emitting value-ref at all"; it reads
`languages/<dir>/query.ts`, so Vue — which owns no query and borrows
`emitTsScopeCaptures` / `emitJsScopeCaptures` — emits value-refs while the
assertion lists three languages, and a capture synthesized in code is invisible
to it. The case now asserts on query OWNERS, which is what it checks and is
sound because a delegating language inherits the rules it borrows; the header
states the synthesized-capture gap instead of letting a green tick imply it away.

R4-5 — the BARE docstring described a lexical walk that is not one.
`findCallableBindingInScope` applies the callable predicate WHILE walking, so a
nearer parameter or local is stepped over: the defect R3-1 fixed on the container
channel, unguarded here, pre-existing since #2437 and reachable in JS/TS. Out of
scope for #3399, so behaviour is unchanged and the sentence now says what the
walk does rather than implying a guarantee it does not give.

Gates: tsc --noEmit clean, npm run build clean, prettier clean.
`test/integration/resolvers` 3,632 passed / 3 skipped (70 files);
`test/unit/scope-resolution` 2,015 passed (120 files);
`impact-callable-value-references` 7 passed under `lbug-db`. Bench --check:
receiver-resolution, zig-cross-file-resolution, scope-capture (15 languages),
scope-emission, callable-value-flow all PASS with no baseline edited.

* fix(review): honour the hub opt-in for value references, so `hub.fn` means one thing

Review round 5 (`gitnexus-check` bot on `1c7c05ff`). One finding, valid and
reproduced before fixing.

`findNamespaceValueRefTarget` accepted only `ref.origin === 'local'`. R2-2
recorded that as deliberate — "the `namespaceExportsIncludeImportedNames` hub
opt-in is a provider decision this language-neutral pass does not make" — and
that reasoning was wrong twice over. Zig sets the flag
(`languages/zig/scope-resolver.ts:41`, measured on ghostty and tigerbeetle before
it landed), and the pass does have the provider in scope: `runScopeResolution`
takes one and already forwards several of its hooks to other passes.

The result was the exact asymmetry R2-2 argued against in its own first
paragraph. A Zig hub declares nothing — every name it publishes it imported — so
requiring a local declaration declines every member reached through one:

    // hub.zig
    pub const scale = @import("dom_utils.zig").scale;

    // Element.zig
    const hub = @import("hub.zig");
    pub fn callsThroughTheHub(v: u8) u8 { return hub.scale(v); }   // resolved
    pub const scaled = bridge.accessor(hub.scale, null, .{});      // declined

One name meaning two different things depending on whether a `(` follows it.

Fixed by forwarding `provider.namespaceExportsIncludeImportedNames` into the pass
and consulting the published channel when it is set — the same question
`receiver-bound-calls` Case 1 asks, through the same `lookupBindingsAt` read
`findExportedDefIncludingImportedNames` performs for the CALL form. The pass
still names no language; it asks the provider, which is the sanctioned hook.

Precedence is unchanged where it mattered: a locally declared member still wins
over a republished one, two distinct defs under one name still resolve nothing,
and `CALL_TARGET_TYPES` still gates the answer — pinned by `hub.DEFAULT_NS`, a
re-exported CONSTANT, which stays unregistered. Languages that do not opt in are
unaffected: the parameter defaults to `false`, and passing `false` was verified
to fail the new hub test, so it is load-bearing. The fixture republishes a member
(`scale`) that nothing else uses, so the hub assertion cannot be satisfied by an
edge another case emitted.

Gates: tsc --noEmit clean, npm run build clean, prettier clean.
`test/integration/resolvers` 3,634 passed / 3 skipped (70 files);
`test/unit/scope-resolution` 2,015 passed (120 files);
`impact-callable-value-references` 7 passed under `lbug-db`. Bench --check:
receiver-resolution, zig-cross-file-resolution, scope-capture (15 languages),
scope-emission, callable-value-flow all PASS with no baseline edited.

Also recorded in DECISIONS.md: `/autofix` returned "No successful autofix run"
because the `PR Autofix` run on `1c7c05ff` failed at `actions/upload-artifact`
with a 403 from GitHub's artifact storage, not because of anything in this
branch. This push triggers a fresh run.

* fix(review): inspect the module scope in the owner-shadow guard, and check the TSX suffix

The `gitnexus-check` pass on `bd6e577e` carried three findings of its own, and I
answered the later `1c7c05ff` pass without noticing them. Recording that as a
process failure too: bot reviews are per-head, and a later pass does not
necessarily repeat an earlier one's findings.

R6-1 (Error, valid, REPRODUCED) — the shadow guard stopped one rung short.
`isOwnerNameShadowedBySomethingElse` returned `false` on reaching the module
scope, justified as "a container declared there IS the binding, and the caller
already resolved it". That holds when the owner came from the scope chain and
fails when it came from the workspace-wide qualified-name fallback:

    // Gauge.zig — never imported by Element.zig
    const Gauge = @This();
    pub fn read(self: *Gauge) u8 { … }

    // Element.zig
    const Gauge = @import("dom_utils.zig").DEFAULT_NS;          // NOT a container
    pub const level = bridge.accessor(Gauge.read, null, .{});   // → Gauge.zig's read

`findClassBindingInScope` steps over the module-scope binding because it is not
class-like, answers from `scopes.qualifiedNames`, and the guard waved it through.

Worth recording: the first fixture attempt did NOT reproduce. A local
`const Gauge: u8 = 3;` also claims the workspace qualified name `Gauge`, leaving
two candidates, and the fallback refuses to guess between two — so the shape
defeated itself. Binding the name by IMPORT claims no qualified name, the
fallback stays unique, and it fires. A negative result on the first shape was not
evidence the finding was wrong.

Fixed by inspecting the module scope as the last rung instead of skipping it.
The identity exemption is what makes that safe where `isNamespaceNameShadowed`
cannot do it (#2723: a namespace import writes its own name into the module scope
and would read as its own shadow) — the binding that IS the owner exempts itself,
and only a binding to something else answers `true`. `lookupBindingsAt` is
consulted at that scope and only there, because an imported alias lives in the
finalized channel rather than in `scope.bindings`.

R6-2 — the hub re-export finding, already fixed in `5d8fe9d8`; the same defect
restated on the later head.

R6-3 (valid, fixed) — the dispatchability canary omitted the TSX suffix.
`getTsScopeQuery` analyzes a `.tsx` file with `TYPESCRIPT_SCOPE_QUERY +
TSX_JSX_QUERY_SUFFIX`, and the test read only the base, so a `value-ref` rule
added to the suffix would be emitted in TSX analysis with the canary green. The
suffix is now exported and concatenated into the check; verified load-bearing by
adding an unkeyed `jsx_expression` value-ref rule to it, which fails the
TypeScript case.

Gates: tsc --noEmit clean, npm run build clean, prettier clean.
`test/integration/resolvers` 3,635 passed / 3 skipped (70 files);
`test/unit/scope-resolution` 2,015 passed (120 files);
`impact-callable-value-references` 7 passed under `lbug-db`. Bench --check:
receiver-resolution, zig-cross-file-resolution, scope-capture (15 languages),
scope-emission, callable-value-flow all PASS with no baseline edited.

* perf(bench): gate callable-value reference resolution on linear scaling

`resolveValueRefTarget` (#3399) replaced one lexical walk with four
channels, and the last of them — a qualified receiver resolved through
`findClassBindingInScope` — falls back to `scopes.qualifiedNames`, a
WORKSPACE-WIDE index consulted once per site. Keyed that is O(1); scanned
it is O(files) per site, and a registration table that costs O(sites)
today costs O(sites x files) tomorrow. Nothing in the suite can see that:
the fixtures are single-file, and the corpus it actually matters on is
lightpanda-io/browser, where `bridge.{accessor,function,…}` appears 2,047
times across 257 files.

`bench/value-ref-resolution/measure.mjs` builds two synthetic Zig corpora
of identical shape 4x apart in file count, and times ONLY the per-site
resolution loop — extraction, `reconcileOwnership` and finalize are setup.
`linear_factor` is `(t_large/t_small)/(N_large/N_small)`: measured
1.00-1.09 across runs, with `us_per_site` flat at 1.4-1.5 between the arms.

Correctness comes first, because a timing gate alone is satisfied by a fast
wrong answer: exact site/resolved/declined counts per arm plus an
order-independent sha256 over every (site -> resolved target) pair. The
corpus exercises all four channels — CONTAINER, NAMESPACE, HUB and BARE —
and carries two DECLINE controls per module (a non-callable namespace
member, a non-callable bare argument), so a widened callable gate moves
`declined` instead of hiding inside the timing.

Verified load-bearing rather than assumed: making the qualified lookup scan
a workspace-sized collection leaves the fingerprint IDENTICAL and takes
`linear_factor` to 3.752 against a slack of 1.375 — the regression class
this exists for is exactly the one no correctness gate can see.

Zig is the corpus because it is the only language whose provider sets
`namespaceExportsIncludeImportedNames`, so it is the only one that can
exercise the hub channel at all; the pass itself names no language.

`resolveValueRefTarget` is exported for the bench. Timing
`emitPropertyDispatchCalls` instead would fold the signal into edge
emission, and re-implementing the channel order in the bench would pin the
bench's idea of the function rather than the function.

* feat(zig): index every build package in the repo, not only the root one

Zig already had workspace setup — `loadZigBuildConfig` has parsed
`build.zig.zon` `.path` deps, the root `build.zig`'s named modules and each
build module's own `addImport` table since #1432, threaded through
`ScopeResolver.loadResolutionConfig` exactly as tsconfig is for TypeScript.
What it did not have is the part `tsconfigFor` supplies: per-package scope.

`loadZigBuildConfig` reads `<repoRoot>/build.zig{,.zon}` and nothing else, so
a repo laying its packages out as `packages/<name>/build.zig` — no root build
files at all — got `null`, and EVERY bare `@import("<module>")` in it went
unresolved. Cross-file resolution silently degraded to relative imports.
Measured on a two-package probe before this change: `config = null`,
`@import("core")` from `packages/app/src/main.zig` → `null`.

`loadZigWorkspaceIndex` discovers packages the way `findTsconfigFiles`
discovers configs — one bounded breadth-first walk that skips the hardcoded
ignore set — and `zigPackageFor` is the `tsconfigFor` analogue: the nearest
enclosing package governs a file, deepest-first, with no fall-through to an
outer package. Fall-through is what makes a vendored dependency's
`@import("config")` resolve to the outer repo's `config` module, the same
failure `loadTsconfigIndex` documents for a package declaring no `baseUrl`.

`resolveZigImportInternal` is UNCHANGED — impact analysis puts it at HIGH risk
with 6 dependents, and it does not need to move: it is handed one package's
config, and which config it receives is the only difference. Its 48 existing
tests pass untouched. A nested package's paths are rebased to repo-relative at
load time (`.path = "../core"` → `packages/core`, which the root-relative
reading rejects outright as an escape); the ROOT package keeps its raw
spelling, which is what `parseZigBuildZon` promises and its tests pin, so a
single-package repo is byte-identical.

`loadImportConfigs` — which runs unconditionally for every repo, Zig or not —
keeps calling the root-only loader, so no non-Zig repo pays for the walk. That
is the split TypeScript already has between the cheap `loadTsconfigPaths` and
the repo-walking `loadTsconfigIndex`, which `loadResolutionConfig` reaches only
during a language pass.

The `zig-monorepo` fixture carries the discriminating case rather than only the
happy path: `tool` binds the alias `core` to its OWN `src/core.zig`, so a
repo-wide flattened module map — the shape a workspace index invites — would
point `measure` at `packages/core`, a confident edge into a package `tool` does
not depend on. That is worse than the unresolved import this fixes, and only
per-package scoping keeps them apart.

Tests: 7 unit cases pinning the index (including `loadZigBuildConfig` answering
`null` on the same fixture, side by side) and 3 integration cases pinning the
EDGES — cross-package CALLS from the module root AND from a non-root file, and
`tool` not crossing over. Verified load-bearing: restricting the index to the
root package fails all three.

Second consumer caught by the integration test and fixed with it:
`populateZigWorkspaceStaticGating` reads the same `resolutionConfig`, per file
because two files of that pass can belong to different packages.

Gates: build clean, `tsc --noEmit` clean, prettier clean, eslint 0 errors.
`test/integration/resolvers` 3,768 passed / 4 skipped, `test/unit/scope-resolution`
2,015 passed. Bench `--check` with NO baseline edited: `receiver-resolution`
(whose corpus is `test/fixtures/lang-resolution`, where the fixture lands),
`zig-cross-file-resolution`, `value-ref-resolution`, `scope-capture` (15
languages), `import-target`, `callable-value-flow`, `scope-emission`.

* perf(zig): dequeue the package walk by head index, not `shift()`

`findZigPackageDirs` pushes children while it drains the queue, which keeps
the array in a mode where `Array.prototype.shift()` memmoves the whole
remainder rather than taking V8's left-trimming fast path — so the walk is
quadratic in the frontier, bounded only by `ZIG_SCAN_MAX_DIRS` (20,000).

Measured at that bound rather than estimated from the move count: 53 ms at
fan-out 4 and 81 ms at fan-out 20, against 0.8 ms with a head index — 66-106x,
and paid before a single config is read.

FIFO order is unchanged, and the package ordering never depended on the walk
anyway: `loadZigWorkspaceIndex` sorts by (dir length desc, localeCompare).
Memory is unchanged too — the entries were already retained by the pushes;
`shift()` only dropped the head.

`findTsconfigFiles` and `loadNodeWorkspacePackages` carry the same walk with
the same bound and are equally affected. Deliberately NOT fixed here: they are
pre-existing, outside this PR's diff, and reach TypeScript/Node import
resolution, which nothing in this change set covers. Filed separately.

Reported by gitnexus-check on aea0ab06.

* chore: drop DECISIONS.md from the branch

Review feedback: the working log does not belong in the repository. The
reasoning it carried that is still load-bearing lives in the code comments
and in the PR description.

* perf(bench): gate value-ref resolution on scaling alone, not wall clock

A millisecond ceiling measures the runner. This repo has been bitten by that
twice already — bench/callable-value-flow's widening_overhead failed at 2.07
and 1.975 against a 1.9 budget on a shared runner while the code was correct,
both times on a sub-11ms measurement — so the arm is dropped rather than
loosened. `ms_budget` is gone from both arms and from `--check`; `min_ms`
and `us_per_site` are still reported, and nothing compares them to anything.

The remaining timing gate is the ratio, reshaped after
bench/parse-dispatch-rounds/baselines.json: `_what` / `_triage` notes, a
`_measured` block recording samples for context, and min-of-15 reps instead
of 7 (bench/import-target measured N=5 tripping its own budget about one run
in twenty, N=15 holding).

Budget 1.6 — 1.40x the measured maximum over 12 runs (0.907 .. 1.146), the
~1.5x headroom its siblings use on ratios.

Re-verified rather than re-quoted, and the previous note was wrong: replacing
QualifiedNameIndex.get with a full scan moves linear_factor from ~1.0 to 2.07,
not the ~3.7 recorded. Only 1 of the 17 value-ref sites per module reaches
that workspace-wide fallback. 1.6 sits clear of both ends. The note also
records the trap the first attempt fell into: patching gitnexus-shared/src
changes nothing, because the bench resolves the built package.

* fix(review): settle namespace precedence on the name, before the type gate

findNamespaceValueRefTarget's local lookup applied CALL_TARGET_TYPES while
selecting, so a target module declaring a NON-callable under the name answered
nothing and fell through to the published channel — binding a re-exported
callable under a name the module's own declaration owns. findExportedDef does
not do that: it returns any local def and lets its caller's type gate reject
it, so findExportedDefIncludingImportedNames never reaches the imported names
for a name the file declares. `x.f` and `x.f()` must not disagree about which
module owns f.

Not reachable through valid Zig today (a container cannot declare a name
twice, and Zig is the only provider setting namespaceExportsIncludeImportedNames),
which is why the regression test builds the indexes directly instead of adding
a fixture — there is no valid source to write. Its second case fails with the
guard removed.

Three smaller corrections from the same review:

- ZigBuildZonConfig.pathDeps promised the raw `.path` string; a nested package
  stores the normalized repo-relative value. The interface now documents both
  spellings and why either is safe to hand to normalizeZigDepPath. Comment
  only — no behaviour change.
- The 'single-package repo' compatibility test ran against zig-idioms, which
  declares libs/geo as a path dep and that directory has its own build.zig, so
  the walk finds two packages and the name was a claim rather than a check. It
  now runs against libs/geo itself and asserts the package list is exactly
  ['']; a second test pins the multi-package case, including that a file inside
  libs/geo is governed by that package and not by the root.
- The lower-bound header asserted 'some callers are not traced'.
  callableValueReferenceBoundaries hedges when its probe could not RUN and says
  whether the symbol is registered is unknown, so the header claimed an
  omission nothing established. It now says the count may be incomplete and
  names no cause; the per-cause bullets underneath carry that.

* feat(zig): bind a `@This()` alias to the container it names

`@This()` IS the enclosing container, and `const Self = @This();` is how most
Zig files say so. The container is minted under the FILE STEM and the alias
bound nothing class-like: a file-level alias mints no Const at all
(isZigFileThisAlias suppresses it so it cannot shadow the type for `w: *Widget`),
a container-level one mints a Variable every isClassLike walk steps over. So
`Self.member` resolved to nothing — not a wrong edge, no edge, and a caller
list missing it is the false confidence #3399 is about. This was the PR's
declared known limitation.

bindZigThisAliases binds the alias name to its container definition in
indexes.bindingAugmentations — the sanctioned post-finalize channel (I8),
consulted only AFTER a scope's own bindings, so it can never outrank a real
local declaration and can only answer where nothing answered before. Nothing
is replaced or removed. No query, capture or SCHEMA_BUMP change.

It runs inside populateZigRangeBindings, sharing that pass's parsed tree: a
pass of its own would re-parse every Zig file on a cold tree cache. Only
aliases declared directly in a container body or at file level are bound — the
same set collectZigThisAliases recognizes — because a function-local alias
belongs in that function's scope.

Measured cold-index before/after, same command, same build:

  tigerbeetle (246 .zig)  CALLS 17,066 -> 17,140 (+74), USES 437 -> 443,
                          MEMBER_OF 5,131 -> 5,143; every other edge type
                          unchanged, all 24 node-label counts identical
  mach (132 .zig)         CALLS 7,600 -> 7,615 (+15), MEMBER_OF +1, USES flat

The +74 matches a source census of 72 `Alias.member(` call sites in
tigerbeetle files whose alias differs from the stem. Spot-checked end to end:
src/aof.zig:636 writes `try AOF.init(io, output_path)` inside AOFType, and the
edge AOFType.merge -> AOFType.init#2 is present after and absent before. Node
totals move only through the derived layers (Community 524->527, Process
774->763) — no source symbol added or removed.

Scale of what was dropped: 73 of ghostty's 185 `@This()` files, 93 of
tigerbeetle's 94 and 8 of mach's 42 spell the alias differently from the stem,
carrying 302 `Alias.member` references between them, 96 of those calls.

Fixture zig-idioms/src/webapi/Widget.zig exercises both paths — a file-level
`Self` and a container-level `Me` in Metrics — through a call and a
registration. Five cases; three fail with the binding disabled, and the two
that do not are the controls: Element.zig's stem-spelled alias must keep
resolving byte-identically, and the alias name must not become resolvable from
another file.

* docs(review): stop claiming what the alias pass does not do

Two comments asserted things the adjacent code does not support.

The alias pass's call site said it ran before the payload walk "so a subject
spelled through the alias resolves here too". It does not: the payload walk
types subjects through findReceiverTypeBinding, which reads typeBindings plus
the namespace/workspace type channels and never bindingAugmentations, where
bindZigThisAliases writes. Measured on `for (Self.items) |it|` — `it` is bound
neither before nor after. The pass is in that loop for the tree and nothing
else, which is now what the comment says.

Nor is that a gap to close by also writing a typeBinding: no container name has
one, the file stem included, so a payload subject written `Type.member`
resolves for no spelling at all. Giving the alias an entry would make it behave
unlike the container it names. Recorded at the call site so the next reader
does not re-derive it.

The module-shadow test said Element.zig declares `const Gauge: u8 = 3;`. It
binds `Gauge` by IMPORT, and the distinction is the point of the fixture: a
local declaration would also claim the workspace qualified name `Gauge`,
leaving two candidates, and the fallback refuses to guess between two — so the
case the test exists for would never be reached. The comment now says which
binding it is and why the other shape would be self-defeating.

Comment-only: node and edge counts are byte-identical across a full re-index
(52,083 / 165,261 both sides).

* docs(zig): stop describing loadZigBuildConfig as root-only in the present tense

It takes a `packageDir` since this branch and reads
`path.join(repoRoot, packageDir, name)`; `loadZigWorkspaceIndex` is what
supplies it one, per package. Two comments still described the historical
root-only invocation as the function's current behaviour:

- the monorepo integration-test header, flagged by review;
- loadZigWorkspaceIndex's own docstring — the same sentence, in the function
  that calls it WITH a packageDir a few lines below, so fixing only the test
  copy would have left the worse of the two.

Both now attribute root-only reading to the CALL (no `packageDir`), which is
what the argument actually rests on: a monorepo has no root build files, so
that call answers null and every bare @import goes unresolved. The trailing
note about `loadImportConfigs` is reworded the same way — it calls the loader
for the root package alone; the loader is not root-bound.

Comment-only: node and edge counts byte-identical across a full re-index
(52,083 / 165,261 both sides), and detect-changes reports the two hunks
overlap no indexed symbol.

* fix(zig): a written namespace handle owns its own decline

`findNamespaceValueRefTarget` returning `undefined` conflated two different
answers: "no namespace import named this receiver" and "the module this file
named does not expose that member as a callable". Only the first should fall
through to the container channel. The second did too, and
`findClassBindingInScope`'s miss path answers from the WORKSPACE-wide
qualified-name index — so a same-named container in a file this one never
imported supplied the member the written module does not have.

The owner-shadow guard does not stop it, which is the part that is not obvious:
a plain `const utils = @import("utils.zig");` records a namespace IMPORT EDGE,
not a module-scope binding, so the guard finds nothing bound under the name and
reads the container as unshadowed. It catches `const Gauge = @import(x).MEMBER`
(a real binding) and misses the handle form.

Reproduced before fixing, not argued: `decoy.zig`'s `dom_utils` struct gains
`onlyOnDecoy`, a callable `dom_utils.zig` does not have, and `Element.zig`
registers `dom_utils.onlyOnDecoy`. That minted `JsApi -> onlyOnDecoy` — a
confident USES edge into a file `Element.zig` never imports, the wrong-edge
failure this PR exists to avoid, arriving through the container channel after
the namespace channel said no.

The channel now returns 'owned' for every outcome reached once the receiver is
established as this file's unshadowed namespace handle, and the caller declines
on it. A locally shadowed handle still falls through, because there the name
does not mean the import at that site and the container channel's guard is the
right decider.

Bench fingerprint and counts unchanged; no baseline edited.

* fix(zig): reject an absolute nested-package `.path` before rebasing it

The nested-package branch prefixes the package directory and THEN normalizes,
so an absolute `.path` stops looking absolute on the way: `packages/app/` plus
`/src` is `packages/app//src`, which is relative by inspection.
`normalizeZigDepPath` drops the empty segment and the dep lands on
`packages/app/src` — a directory that really exists — so a dependency pointing
outside the repository is fabricated into an in-repo resolution. The root
package was never affected: its prefix is empty, so the value reached the check
as written.

`isAbsoluteZigDepPath` is now asked of the value AS WRITTEN, before any
prefixing, and `normalizeZigDepPath` asks the same helper so the two cannot
drift. `..` is deliberately not handled there: `../core` escapes the package
but not the repo, and rebasing it is what the branch exists to do —
`normalizeZigDepPath` still rejects what escapes the ROOT afterwards.

Note for the reviewer: `path.posix.join(pkg, depPath)` does NOT fix this.
`join('packages/app/', '/dep')` is `packages/app/dep` — it strips the leading
slash too, producing the same fabricated path without rejecting anything.
Measured before writing the fix.

Regression test pins it through the real loader: `packages/app` declares
`.escapes = .{ .path = "/src" }`, and the test fails without the guard.

`impact normalizeZigDepPath` is HIGH (14 impacted, 4 direct, exact). The edit
is behaviour-preserving for that function — the same two conditions moved into
a named helper it calls — and its existing absolute-path suite, POSIX, Windows
drive and UNC spellings included, passes unchanged.

* docs(mcp): stop defining lower-bound as proof that callers were missed

The CLI header was corrected in round 8; the MCP tool contract still made the
assertion the CLI stopped making. `context` said lower-bound "means callers
exist that this view provably does not list" and `impact` said "the walk
provably missed callers" — but `callableValueReferenceBoundaries` also
publishes lower-bound when its probe could not RUN, and says in its own note
that whether the symbol is registered is unknown. A client following the
contract would read an unanswered question as evidence of an omission.

Both now define it as a FLOOR with two possible causes — the walk provably
missed callers, or a probe that would have established completeness could not
run — and point at `boundaries` for which. That keeps the common case exactly
as strong as it was; it only stops the contract asserting the one case it
cannot support. The `causes.callableValueReferences` bullet already documented
the probe-failure branch, so the headline was contradicting the body.

`local-backend.ts` quotes that definition to justify hedging; the quote is
updated to name which half it relies on.

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-09 20:12:17 +00:00
Gergő Magyar
b60c21d05d
fix(group): extract NestJS GraphQL contracts on real indexes (#3201) (#3227)
* fix(group): extract NestJS GraphQL contracts against real 0-based indexes (#3201)

Provider lookup used 1-based startLine while the graph stores tree-sitter rows, so every resolver missed. Also try PascalCased Document names and inline sibling FragmentDoc interpolations from graphql-codegen output.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(group): bind arrow-field providers and fail closed on interpolations

Match Method startLine to the public_field_definition wrapper, decode template escape sequences, and reject FragmentDoc names that mix static and dynamic declarators.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(group): only inline interpolated templates under a gql tag

Cooked reconstruction is not the runtime value for String.raw or unknown tags, so those interpolations stay fail-closed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(group): tighten gql-tag trust and PascalCase Document lookup

Only the identifier `gql` is a trusted interpolating tag. Underscored operation names now try the full pascal-case Document candidate graphql-codegen emits.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(group): memoize GraphQL interpolation source resolution

Avoid exponential re-walks when the same fragment name is declared twice at each layer of a ${FragmentDoc} chain.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(group): fail closed on invalid tagged-template escapes

Treat line continuations as empty cooked text and reject \8/\9 plus legacy octals so reconstructed gql source matches runtime.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 17:30:00 +01:00
Parafee41
20b13b3ed6
perf(resolution): avoid quadratic config walk queues (#3237) 2026-09-09 17:29:28 +01:00
Ankit Verma
a839d029ac
feat(api): report branch and index freshness on the serve repo routes (#3232)
* feat(api): report branch and index freshness on the serve repo routes

`GET /api/repos` and `GET /api/repo` now return the branch an index was
built from, its `lastCommit`, and — where it can be computed — how far
behind the working tree it is. All of it already existed: the fields are
on `RegistryEntry`, and `checkStalenessAsync` is the helper MCP
`list_repos` and `gitnexus status` already use. Only HTTP never asked.

#3199 made this pointed. A branch-pinned analyze registers its own entry,
so one repository yields two rows in /api/repos, and telling them apart
over HTTP meant pattern-matching the clone-directory suffix — a layout
detail that is trimmed for long refs and absent for path-registered repos.

Staleness is reported in the shape `list_repos` already returns: present
only when the index is behind, carrying commitsBehind and hint. It is
meaningful for path-registered repos; a url-registered repo is cloned
--depth 1, so its recorded commit is HEAD and a diverged history cannot be
walked by rev-list anyway. Documented in repo-projection.ts rather than
left to be discovered.

The projections live in their own module so the field list is assertable.
Route-inline, they were reachable only by booting a server, which is how
`branch` stayed unexposed while `gitnexus list` printed it.

/api/repos also gains the rate limiter it lacked: this change makes one
unauthenticated GET cost a `git rev-list` per registered repo, the shape
this codebase already limits elsewhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(api): bound the staleness probe and keep liveness off the fan-out route

Addresses the tri-review on #3232.

P1. `checkStalenessAsync` caught every git ERROR but not a HANG — a working
tree on a disconnected mount or behind a stuck lock never settles, and
/api/repos fans that out once per registered repo. Worse, the web liveness
probe used /api/repos on a 2s budget and re-polls on failure, so each failed
probe stacked another N children on a server that was actually healthy.

Two independent fixes, because either alone still leaves a sharp edge:
- `execFileAsync` now carries a 5s timeout, so a hang is killed and routed
  into the same fail-closed "not stale" answer the existing catch already
  gives a bad SHA. This also protects MCP `list_repos`, which shares the
  helper.
- `probeBackendStatus` (and `isDatabaseReady` through it) asks /api/health
  instead. Liveness should not cost one subprocess per indexed repo. Health
  sits behind the same /api/* edge gate, so the 401 "gated vs absent"
  distinction is preserved.

P2. rate-limit.test.ts pins which routes carry a limiter so a dropped one
fails a test before CodeQL has to catch it; /api/repos was newly limited but
not pinned. Added, and verified it fails when the limiter is removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-09 17:28:45 +01:00
EVA
25974caf05
fix(doctor): distinguish vector capability from repository index state (#3228)
Co-authored-by: Eva <eva@100yen.org>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-09 13:06:41 +01:00
mengkaka
4154b63131
feat(indexing): add Objective-C semantic indexing support (#3179)
* docs: add Objective-C fork provider notes

* feat(objective-c): add deterministic provider and grammar

* feat(objective-c): finalize provider MVP

* fix(objective-c): harden provider integration

* fix(objective-c): normalize bare macro markers

* docs(objective-c): integrate provider documentation

* fix(objective-c): harden resolution and header classification

* fix(objective-c): complete provider follow-ups

* fix: address Objective-C review follow-ups

* chore: format Objective-C grammar sources

* fix(objective-c): harden review follow-ups

* Address PR review feedback (#3179)

Keep Objective-C chunking and macro recovery aligned with the grammar, and stop Community MEMBER_OF edges from leaking into symbol context.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address follow-up review on ObjC chunking and language fallback.

Keep preprocessor directive text from changing file-scope brace depth, group real ivar nodes, skip header modifiers, and restore Rakefile/Gemfile detection through getLanguageFromFilename.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Parse Objective-C headers with the objc grammar in embeddings.

ensureAndParse and structural extraction now use the same content classifier as ingest, including method snippets from .h files, so Protocol/Category/Class chunks are not re-parsed as C++.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Keep file-scope macro elision off C line splices and @interface/@protocol/@implementation bodies, and attach ivar attributes to the following instance variable when chunking.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(bench): rebaseline Objective-C CSV emit

* feat(objective-c): add workspace resolution and linear emit benches

Plain .h files are classified as C++, so the ObjC pass could not
resolve #import of those headers. Load a C/C#-style workspace once
per pass, and keep protocol-candidate USES linear.

Refs #3179

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

- Compare LadybugDB labels() as a scalar when excluding Community MEMBER_OF edges.
- Walk superclass members, skip file-static C sibling defs, and ignore comments in ObjC header/macro scans.

Note: pre-existing failure in objective-c-provider integration (worker-pool ready timeout) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Emit Objective-C declaration captures so compilation-unit siblings can share
header/implementation bindings, and keep class vs protocol visibility groups
distinct.

Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Emit every comma-separated property/ivar declarator, and count @interface
after a multiline block comment closes so in-declaration macros stay intact.

Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: ximengkai <ximengkai@soyoung.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 09:40:21 +00:00
Gergő Magyar
376ed3bb4a
perf(lock): probe this process's own start time once (#3222)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* perf(lock): probe this process's own start time once

`acquireFileLock` stamps the owner file with the acquiring process's start
time so a later reclaimer can tell a live owner from pid reuse. That value
cannot change while we are running, but it was re-probed on every
acquisition — and on Windows the probe is a `powershell.exe` spawn plus a
`Get-CimInstance Win32_Process` WMI query, which is the single most
expensive step in taking an uncontended lock.

Add `readProcessStartTimeCached` and make it the default reader in
`acquireFileLock` and `resolveWatchDeps`. Only this process's own pid is
cached:

- A foreign pid is always re-probed. That process can exit and its pid be
  reused, which is precisely what the stamp exists to detect.
- A failed probe is not cached. `acquireFileLock` throws when the start
  time is empty, so caching one transient failure would leave the process
  unable to take a lock for the rest of its life.

`readProcessStartTime` itself is unchanged and still probes every call, so
the existing timezone-pinning regression test keeps exercising the real
`ps` invocation instead of passing off a cached value.

Behavior is otherwise identical: same probe, same string, same stamp.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-08 19:06:03 +01:00
Ankit Verma
8ddab9aed5
feat(api): honor branch on POST /api/analyze (#3199)
* feat(api): honor branch on POST /api/analyze

The serve route accepted a `branch` field in the request body, returned
202 and reported the job `complete` — while indexing the remote's default
branch. Express drops unknown body fields, so the caller got no error and
no warning; the only way to notice was to inspect the checked-out clone.

Both ends of the plumbing already existed: CloneOrPullOptions.branch is
honored by cloneOrPull, and AnalyzeOptions.branch already drives
resolveBranchPlacement. Only the HTTP layer was missing, so this wires
`branch` from the route through cloneOrPull and LaunchOptions into the
worker's AnalyzeOptions. StartMessage.options is already typed as
AnalyzeOptions, so the IPC protocol is unchanged.

Validation reuses validateBranchName — the same function backing the
CLI's `--branch` — so both entry points accept exactly the same refs and
a malformed value is rejected with 400 before it can reach git.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(api): make branch part of job identity and complete ref validation

Addresses the review on #3199.

1. Job dedup ignored `branch`, so a request for branch B while branch A was
   in flight was answered with A's job and a 202. The caller would read that
   as "B is indexed" — the same silent wrong-branch outcome honoring `branch`
   was meant to remove. Branch is now part of the dedup identity; a
   different-branch request falls through to the single-slot guard and gets a
   truthful 409 instead.

2. validateBranchName implemented only a subset of git's ref rules, so
   `feature.lock`, `/feature`, `feature/`, `feature//next`, `@`, `@{` and
   dot-prefixed components passed validation and failed later in the git
   subprocess — a 202 plus a background failure rather than the advertised
   400. The remaining `git check-ref-format` rules are now enforced at the
   same chokepoint, which fixes the CLI and `.gitnexusrc` paths too. No
   branch git can create is affected.

3. The LaunchOptions doc claimed an explicit branch always pins
   `branches/<slug>/`. resolveBranchPlacement keeps the run on the flat slot
   when that slot has no owner, or when its owner is already this label.
   Comment and CHANGELOG corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(server): describe cloneOrPull's branch path in its contract comment

The header comment predated `options.branch` and still said an existing
clone is only ever `git pull --ff-only`. The implementation has a second
path: with a branch it fetches that ref and runs
`checkout -B <branch> origin/<branch>`, so the requested branch — not the
one already checked out — ends up in the working tree.

The stale comment is actively misleading: a reviewer reading it concludes
that requesting a branch on an existing clone silently analyzes the
default branch, which is not what happens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(server): scope the origin check to the existing-clone path

My previous comment said remote.origin is verified "in both cases", which
is wrong: assertRemoteMatchesRequestedUrl runs inside `if (exists)`, so a
fresh clone has no origin to check. Restructured around whether targetDir
exists, which is what actually selects the behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(server): cover branch job identity in JobManager's own suite

The branch dedup tests were sitting in analyze-api.test.ts, but they
exercise JobManager directly, so they belong beside the existing
"returns existing job for same repoUrl when active" case in
analyze-job.test.ts. Moved, and extended to cover the callers that omit
branch entirely — the upload route, the embed manager and the existing
tests — which compare undefined === undefined and are unaffected.

Also pins that branch survives the whole clone -> analyze -> terminal
update sequence, since it is now part of dedup identity and must not
drift mid-flight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(api): give a pinned branch its own clone and settle the slot it wrote

Addresses the review on #3199.

Per-branch clone directories (review option 2). One checkout per repo made
`branch` one-shot: after any analyze the tree is dirty with generated
AGENTS.md / CLAUDE.md / .claude/, so a pinned request 202'd and then died on
cloneOrPull's porcelain refusal. Worse, a later request that OMITTED `branch`
pulled whatever branch the last pin left checked out and indexed it as the
default — silent wrong content, the same class as #3198 one request later.
A pinned run now clones into `<repo>__<branchSlug>`, so the two requests no
longer share a tree. The pinned clone registers under its directory name,
because both dirs share an origin and the inferred name would otherwise
collide; that name re-derives through getCloneDir, so DELETE still finds it.

Finalization gate. registerRepo always records the flat `.gitnexus`, but a
pinned run whose label differs from the flat slot's owner writes
`branches/<slug>/`. The gate probed the flat path regardless, so it never
settled, and the worker's normal exit 0 — sent ~500ms after `complete`, while
the job is deliberately still non-terminal — was classified as a crash and a
successful analysis was retried three times and failed. The gate now follows
the placement the worker reports (isPrimaryBranch, added to the IPC allowlist
under the rule that module already documents), and an exit after a terminal
IPC counts as winding down, not dying. Reported by the maintainer and
reproduced independently by @azizur100389.

analyzeCloneOptions extracted so the token/branch combination is asserted.
Inline, the branch-only case — a public URL with no token — was untested, and
dropping it there would silently reindex the default branch while every other
test stayed green.

CHANGELOG: the previous entry claimed the newly-400'd payloads "would have
failed at git", which is true of CLI --branch but wrong for HTTP, where they
succeeded on the default branch. Documented as an explicit behavior change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(server): bound the branch clone-dir name to one path component

`validateBranchName` allows a 255-character ref and `branchSlug` appends a
dash plus 8 hash characters, so `<repo>__<slug>` reached 267 — past the
255-byte component limit on ext4/APFS/NTFS. The clone would then fail to
create its target directory, which the character-only regex could not catch.

Only the readable half is trimmed. The hash is a digest of the full ref and
is always kept, so two long branches sharing a prefix still resolve to
different directories rather than silently sharing an index. `branchSlug`
itself is untouched: the per-branch index slots already use those names on
disk, and shortening them there would orphan existing indexes.

Also corrects two comments: the forwarding test claimed a branch selector
always pins `branches/<slug>/` (it keeps the flat slot when that slot has no
owner or already owns the label), and the web client's `branch` doc said
omitting it means the remote default — true for a `url` request, but a `path`
request is never cloned and indexes whatever that tree has checked out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(server): bound the branch clone-dir name and unblock same-branch re-index

Two defects found by testing this branch end to end, plus the review nits.

1. Path length. `validateBranchName` allows a 255-character ref and
   `branchSlug` appends a dash plus 8 hash characters, so `<repo>__<slug>`
   reached 267 — past the 255-byte component limit on ext4/APFS/NTFS, and the
   clone could not create its directory. Only the readable half is trimmed;
   the hash is a digest of the full ref and is always kept, so two long
   branches sharing a prefix still get separate directories. `branchSlug`
   itself is untouched — the per-branch index slots already use those names on
   disk and shortening them there would orphan existing indexes.

2. Same-branch re-index. Per-branch clone dirs stopped branches from
   contaminating each other, but a REPEAT pin still failed: analyze writes
   AGENTS.md / CLAUDE.md / .claude/ into the clone, so the second pinned run
   met its own dirt at the porcelain check and asked for
   `overwrite_local_changes`. When HEAD already matches the requested branch
   there is nothing to switch, so the run now takes the same `pull --ff-only`
   path an unpinned request takes — review option (1), alongside (2). The
   refusal is untouched where it matters: a real switch, or a detached HEAD,
   still goes through the checkout path and can still refuse.

Also: repositions getCloneDir's JSDoc, which an inserted constant had
orphaned; gates the pinned `registryName` on the same condition as the clone,
so supplying both `url` and `path` no longer renames the operator's local
repo; corrects a test comment that claimed a branch selector always pins
`branches/<slug>/`; and corrects the web client's `branch` doc, which said
omitting it means the remote default — true for `url`, but a `path` request
is never cloned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: drop the CHANGELOG edits from this PR

Requested in review. Checking the history, no feat/fix PR here touches
gitnexus/CHANGELOG.md — the only recent commit on it is `chore: release
v1.6.11`, and CONTRIBUTING says release notes are generated from the merged
PR title via .github/release.yml. Hand-editing an [Unreleased] section from a
feature branch was my mistake, not the project's convention.

The behavior change it documented (branch: null / "" / non-string now 400
where they were previously dropped and the default branch indexed) is stated
in the PR description instead, which is what feeds the release notes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(server): align the clone/pull contract with the same-branch fast path

Two comments I wrote went stale against my own later change.

`cloneOrPull`'s header still said an existing clone with `options.branch`
always fetches and runs `checkout -B`. Since the same-branch fast path landed
that is only true when the branch actually differs; when it is already checked
out the run takes `pull --ff-only` and no dirty-tree check applies. The header
now splits on whether the branch differs, which is what the code branches on.

The web client's `branch` doc said omitting it on a `url` request clones the
remote default. That holds only when there is no clone yet — an existing
unpinned clone is pulled on whatever branch it already has checked out.

Comments only; no behavior change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Address PR review feedback (#3199)

Pin a same-branch re-index to `git pull --ff-only origin <branch>` so the job cannot follow an unverified `branch.<name>.merge` while still skipping the dirty-tree refuse.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(server): close the #3199 holes on pinned analyze re-index

Same-ref updates were a raw pull dest (force-fetch via +) or a shallow ff-merge that could not move, and a tag pin compared the tag-object SHA so re-index refused a dirty tree. Fetch the mapped remote-tracking ref, stay put on a peeled SHA match, restore only GitNexus overlays, skip the 60s settle on alreadyUpToDate, and keep branch validation in core so the HTTP route does not import the CLI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

---------

Co-authored-by: Claude Opus 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>
2026-09-08 15:15:45 +01:00
Gergő Magyar
96132bd13a
perf(scope-resolution): stop re-scanning the ParsedFile store once per language (#3211)
* perf(scope-resolution): stop re-scanning the ParsedFile store once per language

Scope resolution calls `loadParsedFilesForPaths` once per language, and every
call walks every shard in the store. The skip decision needs the envelope's
path listing, and that listing is only trustworthy after the payload digest
has been checked -- so a pass that wants 50 Python files still opens and
SHA-256s all 413 shards / 301MB of a TypeScript-dominated store to prove it
can skip them. A pass wanting a SINGLE file costs 335ms. The cost scales with
language count, not with the files that language has, so a polyglot repo pays
it worst.

`tryLoadV8Cache` now returns the listing it already parsed for that skip
decision, and the store memoizes it per run. Later passes skip on the
memoized listing without reopening the file.

Measured on a 2234-file, 3-language repo, min-of-5:

  before   python 411ms   typescript 2872ms   javascript 507ms  = 3834ms
  after    python 415ms   typescript 2805ms   javascript 248ms  = 3484ms

-350ms here, roughly -250ms per additional language elsewhere. The first pass
is unchanged by construction -- it is what populates the memo. End-to-end the
graph is byte-identical: 51,288 nodes / 163,094 edges / 2106 clusters /
759 flows on a true incremental run.

Keyed on size+mtime as well as name. Shard names are content-addressed, so a
name collision across different content should be impossible, but that
invariant lives in the parse-cache keying rather than here and one stat per
shard is a few ms against the hundreds this saves. The memo holds one store
directory at a time, so a new repo in a long-lived MCP process drops the
previous set instead of accumulating.

The failure mode a listing memo introduces is a FALSE SKIP: a pass concludes a
shard holds nothing it wants and those files silently never reach the graph --
an exit-0 wrong answer, not a crash. The new test walks four passes with
disjoint wants over one store, plus a shard written after the memo is warm;
it fails when the skip is forced.

Also records the full scopeResolution breakdown in bench/. The headline is
that `emit` is 7161ms of the 14.7s phase and ~21% of the edit loop, spread
across a fan of passes with no hot inner loop -- so the win there is not
running them for unchanged files, which is a design rather than a patch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Address PR review feedback (#3211)

Strengthen the shard-listing memo test so a later miss asserts fs.open and
v8.deserialize never run for the skipped shard. Key-set checks alone still
passed if the memo never skipped.

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: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-08 13:48:30 +01:00
Gergő Magyar
0d1aed942f
docs(bench): close FTS as an optimization target with measured evidence (#3209)
PR #3208 landed two claims that further measurement disproved.

The narrowing is not inconsistent. A true incremental leaf edit rebuilds
exactly 8 of the 20 configured indexes -- the tables the writeback DMLs --
and the 20-index run I compared it against was a forced full rebuild
(runner-identity trap). Per-index costs for those 8 sum to 7769ms against
the 7525ms measured in-analyze. There is nothing to fix in `touchedFts`.

The 845ms was not `import('./platform/capabilities.js')`. The CLI already
imports that module statically; a cached dynamic import measures 0.035ms.
The `await` is the first yield after the native FTS build and absorbs the
libuv work still queued behind it. Recorded as a fourth measurement trap,
since it invalidates any mark placed on an await that follows native work.

What replaces them is a floor, established by probing a copy of the corpus
index directly:

  - narrowing further: nothing left, the 8 tables are exactly the DML'd set
  - concurrent builds: hard error, one write transaction at a time
  - connection thread count: flat at 4/8/16/24 (7298/7133/7109/7345ms
    min-of-3), though the default burns ~60% more CPU for it
  - dropping `content` from File: 3541ms -> 241ms, but that deletes
    full-file keyword search (#2317/#2323); capping is a bad trade because
    the size distribution is flat

The one lever left is overlap: the build runs on a libuv thread and hides
behind main-thread JS (3337ms for the index plus a 3000ms JS burn, against
~6859ms serial). File rows are `{ name, filePath }` from `processStructure`
with content lazy-read at CSV time, so they are known before parsing. What
blocks it is that the DB is closed for the whole pipeline and that an early
write moves `liveIndexMutationStarted` ahead of it.

The `ponytail:` comment in `createSearchFTSIndexes` invited exactly the fix
that cannot work -- every caller has already dropped the indexes it passes,
so a presence gate would never fire. Replaced with the measured reason.

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 19:22:24 +01:00
Gergő Magyar
f48bf81256
perf(parse): tighten the dispatch-round memory bound and unclamp the worker-pool override (#3200)
* docs(parse): record why dispatchGroups is a required interface member

Review finding #10 argued dispatchGroups should be optional to match
`getQuarantinedPaths?` / `getStats?`. Those are compatibility accommodation
for WorkerPool shapes that predate them, not a convention for new members;
optional here would force a `?.` plus an unreachable fallback at the single
production call site. Documenting the decision so the next reader does not
re-litigate it from the neighbouring optional markers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit addaab647377f3c4553f752fa3ca1388bcb9ca81)

* refactor(parse): simplify round accounting and dispatch setup

Simplification pass over the dispatch-rounds change. Behavior preserved:
identical graph on a full analyze (51,286 nodes / 163,092 edges).

- Drop `roundMissBytes`. `roundBufferedBytes` counts the same bytes plus the
  cache hits, so it is always the greater of the two and the first disjunct of
  the close condition could never fire on its own. One counter, one reset, one
  check.
- Measure round bytes with `Buffer.byteLength(content, 'utf8')` instead of
  `String.length`. UTF-16 code units undercount non-ASCII source by up to 3x,
  so the cap meant to bound main-thread retention was letting a CJK-heavy repo
  hold well past its nominal budget. Matches `estimateItemBytes` in the pool.
- Reset the durable ParsedFile directories for a round's chunks concurrently.
  Each targets its own chunk-hash directory, and running them serially put N
  round trips of fs work on the critical path the round exists to shorten.
  The try/catch stays inside the mapped callback, so one failure still
  degrades that chunk alone.
- Skip the quarantine filter entirely when nothing is quarantined, which is
  every run without a worker death. It was an identity copy of every group.
- `dispatchChunkParseRound` takes `DispatchGroup<...>` rather than re-declaring
  that shape inline; the type was already imported and used in its body.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 527d5b6e0ca8ae7bbc6a414c5ac7e27fd85e9995)

* refactor(parse): count round misses with the same idiom startRound uses

`drainRound` hand-rolled a reduce to count 'miss' entries while `startRound`,
one function above, filters the same predicate over the same union. Same
integer, one idiom.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7eaa193b0cb5fa515844f36ae1401d6fb2fed7b8)

* fix(parse): honor GITNEXUS_WORKER_POOL_SIZE above the auto sizing cap

The auto pool size is bounded by source bytes so a tiny repo does not spawn a
full idle pool. That bound was also clamping the operator's env override,
because the env value is read inside `resolveAutoPoolSize()` and the result
went through `Math.min(..., workProportionalCap)`.

`DEFAULT_POOL_SIZE_CAP`'s own comment offers `GITNEXUS_WORKER_POOL_SIZE` and
`--workers <N>` as equivalent escape hatches for operators on bigger machines.
They were not. Measured on a 30MB corpus, where the byte-derived cap is 16:

  --workers 24                  -> pool: 24/24 active
  GITNEXUS_WORKER_POOL_SIZE=24  -> pool: 16/16 active   (silently ignored)

Both are deliberate operator input, so both now bypass the work-proportional
cap, which goes back to bounding only the auto default. After the fix, on the
same corpus, with identical graph output (51,286 nodes / 163,092 edges):

  GITNEXUS_WORKER_POOL_SIZE=24  -> pool: 24/24 active
  GITNEXUS_WORKER_POOL_SIZE=4   -> pool: 4/4 active
  unset                         -> pool: 16/16 active

Verified by hand against the pool's own throughput log; not covered by an
automated regression test, since the pool size is only observable through
that log line and not through the progress stream a test can read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 17ed08608c878079b2927da25cfd39c1608a02a2)

* fix(parse): bound the durable-reset fan-out and pin the pool-size override

Review follow-ups on #3200.

The round's durable ParsedFile directory resets went out as one unbounded
`Promise.all` — one recursive rm + mkdir per miss chunk, all at once. A round
can hold hundreds of small packs, and those resets compete for descriptors with
the chunk prefetch this loop already has in flight. `readFileContents` degrades
a losing read SILENTLY by documented contract, so a dropped file would vanish
from the chunk, from the graph, and from the chunk hash — shipping a narrowed
index with exit 0. Now routed through `mapConcurrent` at the same width the file
reads use, which keeps the pipelining win and caps in-flight descriptors.

An operator's pool size is now also bounded by the number of files there are to
parse, so `GITNEXUS_WORKER_POOL_SIZE=100000` on a five-file repo cannot become
the literal thread count. This applies to `--workers` and the env var alike, so
the parity the previous commit established is intact. It does NOT shrink an
incremental re-analyze: `totalParseable` counts every parseable file in the
scan, not the changed ones.

Adds the regression test a reviewer asked for. The existing coverage
(`worker-pool-resilience` calling `resolveAutoPoolSize` directly,
`analyze-worker-pool-size` mocking `runFullAnalysis`) never reaches
`runChunkedParseAndResolve`'s `effectivePoolSize`, so both stayed green through
a revert of the fix. The new test drives the real parse phase with a worker
double that writes a per-`threadId` marker, and counts them: verified it fails
on the reverted line with `expected [ 'worker-1' ] to have a length of 3 but
got 1`, and passes on HEAD.

Also corrects the `GITNEXUS_PARSE_ROUND_BYTES` docstring, which still described
the cache-miss counter deleted two commits ago.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(parse): skip caching a chunk with a stale durable generation; warn on over-subscription

Closes the two findings left open by the review of #3200.

When `prepareDurableParsedFileChunk` fails, the previous generation's shards
are still on disk, so a later warm hit would union them with the new ones. The
chunk is now recorded and its parse-cache write skipped -- the same posture
`finalizeWorkerChunk` already takes for a quarantined chunk, and for the same
reason: do not cache what we cannot vouch for. The next run re-dispatches into
a directory it can actually clear. Bounding the reset fan-out removed the
correlated trigger; this closes the individual case.

Pool size over-subscription now warns rather than caps. Silently capping is
precisely what the override exists to prevent, so an operator's number is still
honored -- but an exported GITNEXUS_WORKER_POOL_SIZE applies to every analyze
in a long-lived caller (watch auto-sync, the MCP server), including small
incremental ones, and that is easy to set once and forget. The warning names
the host's usable core count, so it is a hardware fact rather than an invented
threshold. `resolveHostParallelism` is extracted from `resolveAutoPoolSize`
rather than re-deriving the cgroup-aware fallback at the new call site.

Tests: the stale-generation skip is pinned by a new case asserting nothing is
written under any key; verified it fails without the guard with
`expected 1 to be +0`. 60 unit and 49 integration tests pass across the
affected suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(parse): guard dispatch-round cadence with a bench, not a wall-clock budget

Round boundaries are deliberately invisible to graph output — batching that
changed output would be a bug — so nothing in the repo could see the #3196 win
regress. It would have come back as a silent ~1.5x on every cold analyze. Two
earlier attempts to pin it as a unit test failed for that exact reason: one
scraped a logger line the progress stream does not carry, the other asserted
graph content that is identical either way.

Extracts the round-close fold into `createRoundBudget`, so the decision is a
shared unit the bench measures rather than a copy that drifts. The parse loop
is streaming and cannot know chunk sizes up front, so an accumulator is the
honest shape — not a planner.

Four deterministic arms, one ratio, no millisecond gate:
- layout_fingerprint — pack membership. Every cache key derives from it, so
  drift needs a SCHEMA_BUMP, never a lone re-baseline.
- packs / single_file_packs — the FLOOR. `rounds` only asserts something while
  the corpus over-splits (774 packs where the byte budget needs 5). This is
  bench/import-target's lesson, where four heap arms read 0 B and passed every
  ceiling: a ceiling says "not too big", nothing said "still measuring".
- rounds — the regression signal, both directions.
- cjk_rounds vs ascii_rounds — pins UTF-8 byte accounting. The two corpora
  share a UTF-16 length and differ only in encoded size, so String.length
  collapses them to equal. This is the arm no unit test could be.
- pack_scaling_ratio — (t_4n/t_n)/4, min-of-15. A ratio because wall-clock is
  runner-speed-dependent and this repo has the scar: callable-value-flow's ms
  gate failed twice at 2.07 and 1.975 against 1.9 with correct code, on a
  sub-11ms measurement.

Every arm verified to fail before being recorded: close-every-chunk reads 774
rounds, disabling the close reads 1, reverting roundFileBytes to String.length
takes cjk_rounds 8 -> 3, and shrinking the corpus trips the shape floor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(bench): record the analyze phase breakdown and the rejected optimizations

Where analyze time actually goes, measured while landing #3194/#3196/#3200,
plus the two optimizations that looked compelling and were measured away.

The headline is that the parse work is done: a one-file-edit re-analyze is
36.5s, of which parse is 2.8s (8%). scopeResolution is 40% and the unlogged
graph emit + FTS rebuild is 49% — neither is incremental, and the ~18s sits
outside the phase runner so every phase log is blind to it.

Also records the trap that invalidated an earlier measurement: a non-git
corpus never records a schema fingerprint, so every run is a forced rebuild
and any "warm" number taken that way is fiction.

Rejected, with numbers: more workers (16/20/24 land inside run-to-run spread)
and bundling the worker entry (~250ms on a normal filesystem; the 8.6s that
motivated it was a 9p-mount artifact).

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>
2026-09-07 13:53:31 +01:00
Parafee41
1c1cbf111e
fix(zig): resolve cross-file static gates (#3185)
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Gitleaks / gitleaks (push) Has been cancelled
Publish / Classify release event (push) Has been cancelled
Scorecard / Scorecard analysis (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-cli) (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-web) (push) Has been cancelled
Publish / Build & Push RC Docker images (push) Has been cancelled
Publish / RC guard (marker + release-PR skip) (push) Has been cancelled
Publish / ci (push) Has been cancelled
Publish / Publish to npm (push) Has been cancelled
* fix(zig): resolve cross-file static gates

* Fix Zig workspace import alias enrichment

* Handle extensionless Zig workspace imports

* Reuse Zig import resolution for static gates

* Document Zig workspace static gating

* Harden Zig workspace reference enrichment

* Benchmark Zig cross-file static gating

* Enforce linear Zig benchmark scaling

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-07 07:19:30 +01:00
Gergő Magyar
8f006bd759
perf(parse): batch cache packs into one dispatch round (#3196)
* perf(parse): batch cache packs into one dispatch round

`WorkerPool.dispatch` is a barrier, so dispatching one parse-cache pack at a
time leaves most slots idle for every round-trip. Packs are keyed by
`(language, hash(path) % 128)`, so the byte budget rarely binds: this repo
produces 1285 packs where the budget alone needs 16, and 549 of those hold a
single file. In a real analyze, 76 of 221 dispatched chunks carried one file
and cost 15.3s — 20% of the parse phase for 3.4% of the files.

Chunks now accumulate into a round bounded by `GITNEXUS_PARSE_ROUND_BYTES` of
cache-missing source (default: the chunk byte budget) and go out through a new
`WorkerPool.dispatchGroups`. Jobs are still cut at pack boundaries, so each job
carries exactly one `chunkHash` and every result stays attributable to the pack
whose cache key owns it. Cache hits ride along as round entries, and rounds
drain in `chunkIdx` order, so deferred aggregation stays deterministic.

Cold `analyze --index-only` on this repo (2234 parseable files, 16 workers):
110.3s -> 70.5s total, parse phase 74.0s -> 40.5s, 221 dispatches -> 15.
Graph output is unchanged: 51,286 nodes / 163,092 edges / 2106 clusters /
759 flows in both arms. Peak main-thread RSS 3372MB -> 3487MB (+3.4%).

`dispatchGroups` also claims the pool synchronously and rejects a concurrent
call. Two overlapping dispatches hand the same slots out twice and both stall;
the first version of this change did exactly that, and the only symptom was
every worker idle-timing out ~10s later with no indication of the cause.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(parse): bound what an open round holds, not just what it dispatches

Follow-up to the review of #3196. Three reviewers independently found the
same defect: `roundMissBytes` was the only in-loop close condition, but cache
HITS were queued into the same round without contributing to it. A warm
re-analyze misses nothing, so no round ever closed and every chunk's source
plus its cached worker output stayed resident until the tail drain — the
#2649 heap failure shape on a large repo.

- Hit entries now carry a file COUNT, not the file array, so a replayed chunk
  never pins its source text. `applyChunkResults` only ever read `.length`.
- Track `roundBufferedBytes` across hits and misses and close on either cap.
  Verified on a warm run: with the cap, draining starts as soon as 2MB is
  buffered; without it all 221 merges land in the final 10% of the phase.
- Warm progress no longer freezes at the phase floor. `filesParsedSoFar` only
  advances at drain, so a new `queuedFilesSoFar` feeds the progress events
  while `filesParsedSoFar` stays the merge-accurate throughput number.
- A throw from `drainRound` used to unwind straight to `terminate()` while the
  next round's workers were still busy — the #2432 mid-N-API abort hazard.
  Settle the in-flight round first, then propagate.
- `dispatchGroups` returns one array per group; assert that length instead of
  `?? []`, which turned a contract break into a silently empty chunk.
- Collapse `PendingWorkerChunk` into the `miss` RoundEntry it duplicated.
- Repair two stale doc comments: `dispatch`'s JSDoc had been orphaned onto
  `dispatchGroups`, and `dispatchChunkParse` still described chunk overlap
  that now lives in parse-impl's round machinery.
- New test: a round mixing a cache hit and a cache miss. `drainRound` walks
  entries in chunkIdx order but pulls results on a separate cursor, and no
  existing test put both kinds in one round with content assertions.

Cold analyze unchanged: 71.3s, 15 rounds, 51,286 nodes / 163,092 edges.

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>
2026-09-06 18:27:09 +01:00
Gergő Magyar
780cac7885
fix(parse): keep stable cache packs parallel (#3194)
Stable cache packs introduced by 9718e1247 often fit one worker job,
leaving most workers idle. Size jobs against live pool capacity and
reuse extraction queries per native grammar instead of recompiling
on every pack. Preserve recovery readiness across dispatches and
bound failed-thread termination acknowledgment.

Bisect confirms the scheduling regression. Controlled parsing of 951
TypeScript files falls from 56.39s to 26.66s with identical graph output;
peak RSS increases roughly 10%. Add scheduling and query reuse coverage
and repair recovery fixtures that assumed single-job dispatch.

Validation: build, typecheck, formatting and 229 focused tests pass.
Full suite was interrupted during lengthy native DB testing; full
release CI remains outstanding.

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-09-06 10:37:05 +01:00
Gergő Magyar
a957c5d757
Merge branch 'main' into fix/skill-evolution-gate 2026-09-05 11:11:45 +01:00
Gergő Magyar
c5c4fbe43c
fix(zig): vendor tree-sitter-zig so npm i -g no longer warns on peers (#3180)
* fix(zig): vendor tree-sitter-zig so npm i -g no longer warns on peers

Published overrides do not apply to dependents, so the Zig optionalDependency
kept warning that tree-sitter@0.21.1 does not satisfy peerOptional ^0.22.1.
Load it from vendor/ like Dart/Kotlin/Swift instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ci): add zig parse snippet to prebuild validate

The six zig prebuild jobs failed at "Validate the .node loads and parses"
because snippets[GRAMMAR] was undefined and tree-sitter threw
"Input must be a function".

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: drop Unreleased changelog note from the Zig vendor PR

CHANGELOG.md is owned by the release process, not individual PRs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33949409205

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33949616521

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33949829377

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33950025077

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33950220655

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33950400275

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33950607933

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33950882912

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33951170483

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33951386477

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33951624305

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33951813452

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33951998309

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33952225172

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33952524393

* fix(ci): stop native prebuild rebuild loops

PR path filters and source checks see the cumulative diff, so generated binaries kept rebuilding the original source change. Skip output-only synchronize events using their exact before/head range, failing closed when Git cannot compare it. Exercise the workflow against real commit histories, including multi-commit source pushes and merge-ref drift.

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33953226606

* fix: address Zig PR review feedback (#3180)

Check for the vendored package without loading its native binding so a
broken installed Zig grammar fails the parsing test instead of skipping.
Match the optional child descriptor in the Zig metadata declaration and
include Zig in the two optional/vendored grammar comments.

Validation: 159 targeted tests, TypeScript, metadata type fixture, and
formatting passed. Injected native-load failure now fails instead of
skipping; explicit Zig opt-out still skips.

* chore(vendor): rebuild native prebuilds (tree-sitter-zig)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33956342308

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: gitnexus-release-bot[bot] <gitnexus-release-bot[bot]@users.noreply.github.com>
2026-09-05 10:35:33 +01:00
Gergő Magyar
8f08261d05
Merge branch 'main' into fix/skill-evolution-gate 2026-09-04 20:02:51 +01:00
Gergő Magyar
c0c3fa18a9
chore: release v1.6.11 (#3177)
* chore: release v1.6.11

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(test): read /api/info version from package.json

server-info unit tests hardcoded 1.6.10 while buildServerInfo reads
package.json, so the 1.6.11 bump failed ubuntu coverage 3/3.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: read the published version from one helper

Release bumps kept breaking tests that each re-required package.json.
packageVersion() is now the single read for CLI, MCP, serve, and those tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 20:02:37 +01:00
Gergo Magyar
7c68905aac Merge branch 'main' into pr-2785-feedback 2026-09-04 18:38:07 +00:00