IgnoreService only read per-repo .gitignore/.gitnexusignore, so an
exclusion meant to apply across every indexed repo had to be repeated
per repo or hand-patched into node_modules (wiped on every upgrade).
loadIgnoreRules now also reads a global ignore file at
$GITNEXUS_HOME/ignore (default ~/.gitnexus/ignore), reusing the
existing global directory that already holds registry.json and
config.json. It is added first, so per-repo .gitignore/.gitnexusignore
rules can still negate it, mirroring the .gitignore -> .gitnexusignore
precedence already in place. GITNEXUS_NO_GLOBAL_IGNORE (or
noGlobalIgnore) skips it, mirroring GITNEXUS_NO_GITIGNORE.
GitNexus review-agent finding: stripDynBound's documented Box<dyn Trait>,
Rc/Arc<dyn Trait>, and auto-trait/lifetime bound-list (dyn Trait + Send)
shapes had no test anywhere — only the bare &dyn Trait parameter case was
exercised end-to-end. Add direct unit coverage on normalizeRustTypeName and
(via interpretRustTypeBinding) normalizeRustReturnType for these shapes.
call-summary-schema-version.test.ts pins INCREMENTAL_SCHEMA_VERSION as a
literal per bump, documenting the reuse-gate boundary for each version.
Update the "current" expectation to 11 and add the v10 pre-current case,
matching the v7/v8/v9/v10 precedent already in the file.
Addresses gitnexus-review-agent findings on PR #2608:
- MED: on a partial apply (a file's write throws), drop that file's edits
from total_edits/graph_edits/text_search_edits/changes so the reported
result describes what actually reached disk, not what was attempted. The
comprehensive enumeration otherwise let a failing file contribute its
entire line count as phantom 'applied' edits. failed_files still names
every dropped file. Counts are now derived once from the reported set.
- MED: hoist the word-boundary regexes out of the per-line loop (one compile
each instead of one per line), reused by the apply loop.
- LOW: apply loop reuses escapedOldName instead of recomputing the escape
formula inline (removes a preview/apply drift risk).
- Soften the in-code comment: enumeration gives per-call preview/apply
consistency; the pre-existing two-read TOCTOU (external write between
preview and apply) is out of scope and noted, not newly introduced.
Tests: add a mixed graph-ref + text_search multi-file case (asserts per-file
confidence and the never-downgrade guard, via a stubbed rg), and a
partial-write-failure case (asserts only landed files are reported). Assert
concrete graph_edits/text_search_edits splits, not just their sum.
Expected drift from the query.ts change: abstract trait methods now emit a
scope + declaration capture, shifting captureGroups/digest for every rust-*
fixture containing a trait with a required (bodyless) method.
New minimal fixture (single trait + impl + &dyn Trait call site, no other
same-named callers) proves the dyn-dispatch CALLS edge discriminates: fails
against the pre-fix source (0 edges) and passes against the two preceding
commits' fix (exactly 1 edge, verified via the CLI analyze pipeline against
a standalone repo).
The existing rust-abstract-dispatch fixture was NOT extended for this,
deliberately: it already has other callers referencing the same method
names (process()'s repo.find()/save()/count()), and an existing resolution
fallback picks those up via simple-name matching regardless of receiver
type — masking this specific defect in the in-process test-pipeline path.
A dedicated, single-caller fixture keeps the regression test load-bearing.
rename() reported total_edits from a partial enumeration (definition line
only, one-edit-per-graph-file then break, and text search that skipped any
file already covered by the graph) while the apply step does a whole-file
\boldName\b global replace on every touched file. When a private symbol's
definition and all its call sites live in one file, only the definition line
was reported (total_edits: 1) even though apply rewrote every occurrence, in
both dry-run and apply.
Rebuild changes/total_edits/graph_edits/text_search_edits from one file set:
classify each file to rewrite (definition + graph refs = graph confidence;
rg-only files = text_search, never downgrading a graph file), then enumerate
every matching line per file with apply's exact escaped global regex. The
reported edit list now equals what apply writes. Apply behavior is unchanged.
Adds a regression test reproducing the issue's single-file Rust case (def +
3 same-file call sites, empty graph): total_edits is 4 in both dry-run and
apply, and equals the replacements that land on disk.
Two gitnexus-review-agent findings on PR #2602:
- MEDIUM: the bodied-constant MRO-to-host-enum path (a qualified call to an
inherited, non-overridden enum method) was claimed in a comment but never
tested. Add EnumConst.A.log() -> EnumConst.log#0, exercising E$N's
@reference.inherits MRO arm end to end.
- LOW: `bodiedName ?? hostEnum` conflated "body-less" with "name synthesis
failed on a bodied constant" (reachable only on malformed/error-recovery
trees), silently binding an overriding constant's receiver to the host
enum — a wrong edge instead of no edge. Switch to `isBodied ? bodiedName :
hostEnum` so a bodied constant binds ONLY to its E$N class, mirroring the
object_creation_expression branch's skip-on-synthesis-failure. Verified
output-neutral on the well-formed bench corpus.
Rebaseline the java scope-capture fingerprint (a822cef9 -> d04298a9): the
bench corpus IS test/fixtures/lang-resolution, so the new dispatchInherited
fixture method shifts it (+6 capture groups); the logic change contributes
nothing (confirmed by isolating the fixture-only fingerprint). java.test.ts
242 passed; measure.mjs --check PASS (14 languages); tsc/prettier/eslint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Calling a method on an enum-constant receiver (E.CONST.method()) emitted
no CALLS edge. The receiver "E.CONST" is a two-segment compound receiver;
resolveCompoundReceiverClass walks each dotted segment via the owning
class scope's typeBindings map, but enum constants had no typeBinding, so
the constant segment dead-ended and no target was ever resolved.
#2555/#2558 gave bodied constants a first-class synthesized E$N class with
an MRO that includes the host enum; this is the receiver-side follow-up.
synthesizeJavaAnonymousClassDeclarations now emits a class-scope
typeBinding for every enum constant's simple name -> its E$N class (bodied)
or the host enum itself (body-less), reusing the exact mechanism a field
declaration uses. The generic compound-receiver chain walk then resolves
E.CONST.method() with no change to any shared scope-resolution code.
Bodied dispatch (EnumConst.A.hook() -> EnumConst$1.hook#0) and body-less
inherited dispatch (Plain.A.m() -> Plain.m#0) are covered by new tests in
the existing java-enum-constant-body fixture; both were verified to fail
against the pre-fix tree.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The module.registerHooks compat seam and the onnxruntime resolvers cited
the old '>=22.0.0' floor as the reason their sub-22.15 fallback was
reachable. With the floor now ^22.18.0 || >=24.11.0 (all >=22.15), every
supported runtime exposes the API; the fallback stays as defensive
handling for below-floor runtimes (engines is advisory, not
engine-strict). Comments only - no behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the supported minimum raised to Node 22.18, retarget every lane and
pinned runtime that sat at a lower version so nothing builds or runs the
package on an unsupported (EBADENGINE-warning) Node:
- ci-tests.yml: node-floor-compat 22.14 -> 22.18.0 (name, comment, pin,
version assertion) so the floor gate guards the new minimum; its #2372
registerHooks failure mode cannot recur above 22.15. Containment-canary
pin 22.16.0 -> 22.18.0.
- gitnexus-review-agent.yml + the pinned review/canary runtime: the
reproducible runtime is version-locked in lockstep across
.github/{gitnexus-review-runtime,claude-canary-runtime}/package.json and
their lockfiles (engines), the workflow's node-version, its two
'node --version = v22.18.0' assertions, the lockfile-engines guard, and
NODE_VERSION. Moved all of them 22.16.0 -> 22.18.0.
- gitnexus-skill-evolution.yml: pinned runtime 22.16.0 -> 22.18.0.
- CONTRIBUTING.md prerequisite floor updated.
- review-agent-workflow.test.ts, which enforces the runtime lock, updated
to expect 22.18.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Anchor isBenignDropFtsIndexError to the START of the message
(startsWith, not includes) so a future genuine failure that merely
mentions "Binder exception" or "Catalog exception" mid-message can't
be misclassified as benign. New test proves the old substring match
would have swallowed such a message.
- incremental-fts-drop-ordering.test.ts: probe FTS availability once in
beforeAll and skip VISIBLY via ctx.skip() in beforeEach (matching the
withTestLbugDB/lbug-vector-extension convention) instead of a silent
console.warn+return inside the test body, which reported a false pass
with zero coverage of the ordering invariant when FTS was unavailable.
The post-first-run FTS-index-built check is now a hard assertion
instead of a second soft skip, since the beforeEach gate already
proved the extension loads.
Fixes#2589: incremental analyze intermittently crashed with "FTS
index 'file_fts' is inconsistent: term is missing during delete" after
markdown-only commits, and --repair-fts also failed in that state.
deleteNodesForFiles' batched DETACH DELETE ran against tables that
still carried the FTS index built at the end of the PREVIOUS analyze
run -- createSearchFTSIndexes only drops+rebuilds every index in Phase
3, well after that delete already ran. LadybugDB's FTS extension is
not proven to survive DML against an indexed table (its own docs never
demonstrate the sequence). Call the new dropSearchFTSIndexes() up front
in the non-escalated incremental branch, before deleteNodesForFiles --
Phase 3 still rebuilds every index from the final row set regardless.
New end-to-end test drives a real runFullAnalysis full+incremental
cycle and confirms it fails without this change (file_fts and 50
sibling indexes still present at delete time) and passes with it.
dropFTSIndex previously caught and discarded every DROP_FTS_INDEX error
unconditionally. Extract isBenignDropFtsIndexError, a pure classifier
for the two legitimate "nothing to drop" cases (Binder/Catalog
exceptions: index never created, or the FTS function isn't registered)
verified end-to-end against @ladybugdb/core 0.18.x's real conn.query()
error text. Anything else -- e.g. the Runtime exception "FTS index is
inconsistent" class from #2589 -- now rethrows instead of being masked,
so a corrupted index can no longer persist across analyze runs
undetected.
Review finding: the record_declaration container-node fix (894110bf)
makes previously-uncaptured Record nodes and HAS_METHOD edges appear
for the first time, but the incremental write set only covers changed
files. Without this bump, an existing index would silently keep
omitting the Record node and its HAS_METHOD edges for unchanged
record files after an ordinary incremental analyze.
Same contract as v7 (#2437/#2522) and the two closest precedents, v8
(#2550) and v9 (#2555), which bumped this constant for the identical
"model X as first-class node" class of change.
new Local().inner() bound the whole object_creation_expression as
@reference.receiver, so its raw source text ("new Local()") became the
receiver name. That text can never match a scope binding, so the call
silently fell through to name-only fallback resolution and could
resolve to an unrelated same-named method on a collision.
Normalize the receiver to the constructed type's simple name (reusing
javaBaseSimpleNameOf, already used for the anonymous-class inheritance
edge) so Case 2 (class-name / static receiver) in
receiver-bound-calls.ts resolves it via its normal MRO walk. Mirrors
the existing normalizePhpReceiver precedent in php/captures.ts - a
language-local capture rewrite, no shared-pipeline change.
JAVA_QUERIES had no @definition.record capture, unlike its
class_declaration/interface_declaration/enum_declaration siblings and
unlike CSHARP_QUERIES' own record_declaration pattern. A Java record's
container node was never created, so its HAS_METHOD edges were dropped
at persistence even though ownership resolution computed a valid
ownerId for its methods.
Downstream label mapping, the class-extractor config, the dispatch
table, and ownership reconciliation already treated 'Record' correctly
- this was purely a missing structure-phase capture.
Wrap the version-directory scan in try/catch so a transient FS error
(permission denial, an AV file lock on Windows, a directory vanishing
mid-scan) fails closed to null instead of throwing — matching the
original callers' contract, and safer across the Windows/macOS/Linux
CI matrix where these error modes differ.
Also drop the redundant USERPROFILE/HOME manual chain in
extension-binary-real.test.ts in favor of the repo's established
os.homedir() convention (already used ~15 other places here), which
Node resolves correctly per-OS and already honors env overrides.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
resolveInstalledFtsExtension (extension-binary-real.test.ts) and
resolveSeedExtension (fts-extension-e2e.test.ts) both hardcoded the
on-disk FTS extension path as .lbdb/extension/<lbug.VERSION>/..., but
LadybugDB's native INSTALL/LOAD resolves its own extension-ABI version
directory, which does not always track the npm package version. Bumping
@ladybugdb/core from 0.18.1 to 0.18.2 in this PR still installs into a
0.18.1 directory, so both hardcoded lookups came up empty and failed
hard under GITNEXUS_REQUIRE_FTS=1 in CI (all platforms, shard 3).
Add findInstalledFtsExtension() to discover the real installed file by
scanning every version subdirectory, and use it from both test files.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: remove hardcoded 300-flows cap for large repositories
The dynamicMaxProcesses was capped at 300 via Math.min(300, ...),
causing large repositories (280K+ nodes) to lose execution flows.
Change: Remove the Math.min(300, ...) cap, keep dynamic calculation.
Effect: 280K-node repo: 300 → 1617 flows.
* test: add regression for dynamic maxProcesses sizing (#2198)
Verify that processProcesses honours maxProcesses > 300 without truncation.
Addresses the optional follow-up suggested by @koriyoshi2041.
* test: exercise computeDynamicMaxProcesses at the phase layer (#2198)
Extract from the inline
expression in so the regression test can exercise the
function that actually contained the removed cap.
The previous test called directly with
, which passes regardless of whether the phase-level
cap is present — never had the cap.
The new test suite covers:
- floor (20) for tiny repos
- linear scaling in the 0–3000 range
- growth past 300 for large repos (the actual regression)
- explicit assertion that reintroducing Math.min(300, …) would fail
Addresses review feedback from @azizur100389.
---------
Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
* feat(embeddings): support GITNEXUS_EMBEDDING_REQUEST_DIMS=omit
What: Honor GITNEXUS_EMBEDDING_REQUEST_DIMS=omit by suppressing the request-body
`dimensions` field sent to HTTP embedding backends.
Why: Strict OpenAI-compatible backends return vectors in the model's native size
but reject an unfamiliar `dimensions` field, breaking `analyze --embeddings`
against them. The var was parsed but never propagated, so `omit` was a no-op.
How: Add `requestDimensions` to HttpConfig, return it from readConfig, and forward
`config.requestDimensions` (not the validation-only `config.dimensions`) to
`httpEmbedBatch`. Local dimension checks still use `config.dimensions`.
Details: Coexists with the retry/pacing fields introduced upstream; both feature
sets are preserved. Default behavior unchanged when REQUEST_DIMS is unset.
Impact: gitnexus/src/core/embeddings/http-client.ts; README; unit tests.
* fix(embeddings): name GITNEXUS_EMBEDDING_REQUEST_DIMS in its own config error
Address the review findings on #2574.
What:
- A malformed GITNEXUS_EMBEDDING_REQUEST_DIMS now throws an error naming
GITNEXUS_EMBEDDING_REQUEST_DIMS, not the sibling GITNEXUS_EMBEDDING_DIMS.
- isHttpEmbeddingDimsError recognizes both leads, so the CLI still classifies
the REQUEST_DIMS config mistake as a clean config error, not a stack dump.
- Tests: numeric-override decoupling (DIMS=1024 validates the response while
REQUEST_DIMS=512 is sent in the body), the omit aliases (none/off/false/0),
and the malformed-value error path (which also pins the naming fix).
- README documents the full accepted values: omit-aliases and integer override.
Why: readConfig reused the DIMS error lead for the REQUEST_DIMS branch, so
REQUEST_DIMS=garbage misdirected the operator to edit the wrong variable. The
feature's actual decoupling and its non-omit inputs had no test coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: wangxc <wangxc_a_bj@si-tech.com.cn>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Idempotency beforeAll runs runSkillsCli (analyze --skills) twice, each
capped at 45s, under a 90s hook budget — exactly 2x the per-call timeout,
with no headroom for fixture creation and git init. On slow Windows CI
runners the two analyzes plus setup exceed 90s and the hook times out
('Hook timed out in 90000ms'), failing the shard before the test's own
status===null timeout tolerance can apply. Every other describe hook in
this file already uses 120s; align this one.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The 64 MiB floor was too small: LadybugDB's bulk COPY needs working
buffer-pool memory that scales with the repo, so a 64 MiB pool fails with
"buffer pool is full and no memory could be freed" on any non-trivial
repo (empirically: the 6-file skills-e2e idempotency fixture needs
>=128 MiB; the ~1800-file GitNexus checkout needs >=256 MiB). Introduce a
distinct ADAPTIVE_POOL_FLOOR (256 MiB) for the hint clamp, kept separate
from BUFFER_POOL_FLOOR (64 MiB), which still guards defaultBufferPoolSize
on tiny-RAM machines; the hint is still clamped up to the machine default
so it can never over-commit.
This keeps the change as what it actually is — a large-repo optimization:
GitNexus full analyze is 51s (2 GiB) -> 35s (adaptive ~414 MiB). Small
repos now open COPY-safely at 256 MiB instead of the 2 GiB default (same
wall time on Linux, where commit is lazy; the eager commit is far cheaper
than 2 GiB on Windows). The reframed comments drop the earlier
unrepresentative "3-file repo / 64 MiB fast" claim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the sizing lever without changing behavior yet: a module-scoped
buffer-pool size hint plus estimateBufferPool(graphElementCount), read by
resolveBufferManagerSize with precedence env-override > clamp(hint, 64MiB,
default) > default. The hint can only shrink the pool from the default
(clamped to [floor, default]), so the 2GiB/80%-RAM cap and the
GITNEXUS_LBUG_BUFFER_POOL_SIZE escape hatch (incl. 0) are preserved. With
no hint set, resolveBufferManagerSize returns exactly what it did before.
Motivation: LadybugDB eagerly commits the buffer pool at DB open, so the
fixed min(2GiB,80%RAM) pool adds a measured ~2.8s to every analyze even on
a 3-file repo (dominant on Windows). Sizing the pool to the graph lets
small repos use the fast 64MiB floor while large repos keep the cap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ci): install root and shared node_modules for the evolution benchmark
The first real workflow_dispatch of the skill-evolution loop failed at task
binding: capture_task_dependency_binding aborted with
SandboxError: sandbox_copy path is unavailable: node_modules: No such
file or directory
The benchmark tasks sandbox-copy node_modules from three locations
(tasks.scenarios.yaml) — the monorepo root, gitnexus-shared, and gitnexus —
mirroring a full dev checkout. The install step only ran `npm ci` in
gitnexus/, so the root and gitnexus-shared node_modules never existed and
the loop died before any agent ran. Install all three (root, then build
gitnexus-shared, then build gitnexus), matching the per-package install in
ci-tests.yml plus the root deps the tasks require.
A new contract test pins all three installs so this fails in CI rather than
on the next real run — the same guard the workflow's other two P1 fixes got.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5uu9Ar3e45QZ5xFsG4AZ
* fix(ci): only add the missing root install; the subpackage steps already exist
The initial fix redundantly rebuilt gitnexus-shared and gitnexus inside the
gitnexus step — but the workflow already builds both in their own dedicated
steps. Only the monorepo root node_modules was missing. Add a single
"Install monorepo root dependencies" step and leave the two subpackage
build steps untouched, so the benchmark's root sandbox_copy resolves without
double-building.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5uu9Ar3e45QZ5xFsG4AZ
---------
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>