* fix(deps): pin tree-sitter-c/cpp to fix Windows segfault (#1242)
`tree-sitter-c@0.23.2` ships native prebuilds compiled against tree-sitter
ABI 14 (tree-sitter-cli >=0.24), while GitNexus is pinned to the
tree-sitter@0.21.1 JS runtime. On Windows the JS runtime hits
`Cannot read properties of undefined (reading '161')` inside
`unmarshalNode` and a native segfault in the parse-worker pipeline on
real C codebases (e.g. STM32 headers from the issue reporter).
Two coordinated registry pins fix the root cause without any override
gymnastics or vendoring:
- `tree-sitter-c` -> `0.21.4` (last release built against the
tree-sitter@0.21 ABI; declared peer `^0.21.0`).
- `tree-sitter-cpp` -> `0.23.2` (last 0.23.x release before
tree-sitter-cpp added a runtime dep on the broken-ABI
`tree-sitter-c@^0.23.1`; pinning here lets us drop the previous
global override entirely).
`npm ls tree-sitter-c` is now clean: single deduped 0.21.4, no
`overridden` annotations, no nested copy.
Parser loader collapsed to one declarative table:
- One `SOURCES` map with `{ load, unavailableNote, optional? }` rows
for every grammar including TSX. Adding/removing a grammar is one
entry; `unavailableNote` is mandatory and the type checker enforces
it, so failures are never silent and never generic.
- Single `loadGrammar(key)` does lazy require + cache + per-failure
classification. Required failures `console.error` the note and
rethrow the original (preserves stack); optional failures
`console.warn` and report the language as Unsupported. One
warn-once `Set` deduplicates per language key.
- The previous bespoke `warnCUnavailable` + `cWarningEmitted` state
and 4 conditional spreads in the language map are gone.
Per-grammar `unavailableNote` strings name the package, list the most
likely failure mode for that grammar, and link the relevant tracking
issue (#1013, #1125, #1130, #1242) where applicable.
Tests: new `C parser ABI compatibility (#1242)` block under
parser-loader.test.ts exercises the actual failure paths
(non-trivial parse + tree walk + Query.captures + TreeCursor
descent). The original report's `unmarshalNode` crash sits on
exactly the traversal hot path these tests now cover.
Validation:
- npx tsc --noEmit: clean
- npx vitest run test/unit: 4808 passed, 10 skipped
- npx vitest run test/integration/resolvers/cpp.test.ts: 133/133
- minimal C parse + walk + query + cursor verified manually under
tree-sitter@0.21.1 + tree-sitter-c@0.21.4 on Win11 x64 / Node 22
Closes#1242. Does not unblock the broader tree-sitter@0.25 upgrade
tracked in #858.
Made-with: Cursor
* chore(ci): redesign tree-sitter upgrade-readiness report (#858)
The daily script that owns the body of #858 used to dump one giant
matrix and leave a human to figure out which grammars are actually
ready to bump. After pinning `tree-sitter-c@0.21.4` and
`tree-sitter-cpp@0.23.2` for #1242, several rows in that matrix now
look like regressions when in fact they are deliberate. The report
now classifies each grammar instead of just listing them.
What changed in `check-tree-sitter-upgrade-readiness.py`:
- New `INTENTIONAL_PINS` table documents grammars deliberately held
below `npm latest`, with a one-line rationale and a tracking issue
per row (#1242 for C and C++, #1013 for C#). The script reads pins
straight from `gitnexus/package.json` so a future bump cannot
drift away from this report.
- New `_classify_grammar(...)` produces one primary disposition per
grammar: Ready for 0.25 / Intentionally pinned / Waiting on
upstream npm release / Blocked on upstream / Could not check.
The dispositions drive the report layout.
- New `vendored_drift_summary(...)` covers all three vendored
parsers (`tree-sitter-proto`, `tree-sitter-dart`,
`tree-sitter-swift`) uniformly: ABI from `parser.c` when present,
upstream npm + GitHub status, and the rationale extracted from
each vendor's `_vendoredBy` field. Prebuilt-only vendors
(Swift today) report `ABI 'prebuilt'` instead of `None`.
- Report layout: top-of-page TL;DR + counts, an actionable
"What you can do today" section, then one section per
disposition bucket, then a dedicated "Vendored parsers"
section. The original raw matrix is preserved inside a
collapsible `<details>` block so the row-diff bot that watches
this issue still has stable input.
- `sys.stdout.reconfigure(encoding="utf-8")` so the workflow no
longer crashes on Windows when the report contains arrows or
em-dashes.
No workflow / cron changes; the daily job posts the new body the
next time it runs. #858 itself was updated by hand in the meantime
to keep the tracker readable.
Made-with: Cursor
* fix(parser-loader): log C grammar load failures at error severity (#1242)
Addresses review feedback on #1243.
`tree-sitter-c` is in `dependencies` (not `optionalDependencies`) so a
load failure on a supported platform always indicates a real install
problem the user needs to see — corrupted node_modules, unsupported
Node version, or an ABI mismatch with the bundled runtime. Previously
the optional-grammar machinery downgraded that to `console.warn`,
which can be missed in long log streams and silently drops C analysis
for an entire repo.
Decouples log severity from throw behavior:
- `GrammarSource.severity?: 'warn' | 'error'` is a new optional field
that overrides the default log level for a load failure. Default is
`error` for required grammars and `warn` for optional ones, matching
the prior behavior for every existing row.
- `LoadResult` carries the resolved severity through `loadGrammar` so
`logFailure` no longer derives it from `fatal`.
- `tree-sitter-c` row sets `optional: true, severity: 'error'`. The
pipeline still degrades gracefully (callers see Unsupported instead
of a thrown error), but the diagnostic is loud and the
`unavailableNote` now spells out what to try first
(`npm rebuild tree-sitter-c`, reinstall) and links the tracker.
No test changes needed: `parser-loader.test.ts` exercises behavior on
the success path and on optional-failure dispatch; severity is a
display-only concern routed through `console.error` vs `console.warn`,
which the existing tests don't assert on.
Made-with: Cursor
* fix(ci): treat intentional pins as 0.25 blockers in readiness report
Addresses review feedback on #1243.
`_classify_grammar` returned bucket `intentional` before checking
`target_compat`, and the per-grammar status loop only added a row to
`blockers` when npm-latest was incompatible with the target runtime.
The combination meant: if every other grammar resolved tomorrow but we
were still holding `tree-sitter-c@0.21.4` and `tree-sitter-cpp@0.23.2`
(both incompatible with `tree-sitter@0.25.x`), the script would emit
"**Ready** — all grammars are 0.25-compatible" and mislead maintainers
into thinking the runtime upgrade was unblocked.
Fix:
- The status loop now adds an entry to `blockers` whenever a grammar
is in `INTENTIONAL_PINS`, regardless of npm-latest's peer dep. The
blocker message names the pinned spec, embeds the rationale from
`INTENTIONAL_PINS`, and tells the reader the pin must be lifted
before the target runtime upgrade. When the pin is removed (entry
deleted from `INTENTIONAL_PINS`), the grammar resumes standard
classification on the next run.
- `bump_now` now excludes intentional pins so they never show up in
the "What you can do today" section. Bumping an intentional pin
requires a deliberate edit to both `INTENTIONAL_PINS` and
`package.json`, not a one-line dependency bump.
Verified locally: TL;DR now reports 8 blockers (6 upstream + 2
intentional) where it previously reported 6, and the verdict
correctly remains **Blocked** even in the hypothetical future where
all upstream blockers clear.
Made-with: Cursor
* fix(cli): surface silent finalize-skips so analyze cannot exit 0 without persisting (#1169)
Closes#1169.
On Windows, `gitnexus analyze .` was observed to exit with code 0 after
printing only the "GitNexus Analyzer" banner. `.gitnexus/lbug.wal` was
written but `meta.json` was never persisted and the repo was not added
to `~/.gitnexus/registry.json`, so `gitnexus list` / `status` reported
no indexed repository. The reporter confirmed the same shape on both
LadybugDB (1.6.x) and the pre-LadybugDB KuzuDB build (1.4.1), so the
silent finalize-skip is upstream of the DB engine and indistinguishable
from a healthy index from the user's perspective.
This change makes that state a hard, actionable failure regardless of
the upstream root cause.
Behaviour change
- New `assertAnalysisFinalized()` invariant in `repo-manager.ts` checks
that meta.json exists at `<repo>/.gitnexus/meta.json` AND that the
global registry has a canonical-path-matching entry. Throws
`AnalysisNotFinalizedError` (kind: "AnalysisNotFinalizedError") with a
diagnostic that names the missing artifact and the storage path the
user should inspect.
- `analyzeCommand` invokes the invariant on the rebuild path (skipped
on `alreadyUpToDate`), so a future silent finalize-skip surfaces with
exit code 1 and a recoverable error instead of a silent exit 0.
- `analyzeCommand` installs idempotent `unhandledRejection` and
`uncaughtException` handlers that bypass the progress bar's console
redirection by writing to a stderr handle captured at module load.
This addresses the secondary symptom where the `barLog` redirection
visually erased stack traces with `\x1b[2K\r` and stripped them via
`String(err)`.
- The catch block also writes the failing error's full stack via the
captured stderr, so failure diagnostics survive any downstream
monkey-patching of `process.stdout`/`stderr`.
Tests
- `test/unit/repo-manager-finalize-invariant.test.ts` (4 tests): cover
both `missing="meta"` and `missing="registry-entry"`, the happy path,
and Windows case-insensitive registry path matching.
- `test/integration/cli-e2e.test.ts` adds a regression test that runs
the real CLI on a fresh repo copy, asserts exit 0, AND verifies
`meta.json` plus the matching registry entry are both written —
catches any future regression of the wiring.
Validation
- `npx tsc --noEmit` passes.
- `npx vitest run --project default` passes for all my touched files
(89 tests across 4 files). The full default suite reports 7188 pass
with the known native LadybugDB Windows-worker flake unrelated to
this change.
- `npx prettier --check` clean on the diff.
- `npx eslint` reports only pre-existing `any` warnings on the file;
no new warnings introduced.
- Live repro on the issue's two-file Python fixture reproduces a
successful index after the change: meta.json present (742 B), exit 0,
`gitnexus list` shows the repo.
Rollback
Strictly additive — the success path is unchanged when `meta.json` is
written and the registry is updated. Reverting the four-file diff is
safe; the previous silent-finalize behaviour returns. No persisted
schema or registry shape changes.
DoD
- [x] Runtime wiring is complete on the affected CLI path.
- [x] Requested behavior is correct and existing contracts are preserved.
- [x] Smallest correct solution — one invariant, one helper, two
handlers; no speculative abstraction.
- [x] Tests prove the changed behavior at unit AND integration level.
- [x] Required validation for `gitnexus/` was run.
- [x] Repo boundaries respected; no language-specific code, no shared
ingestion changes, no new injection surfaces.
- [x] Diff contains only the intended change — no unrelated churn.
Made-with: Cursor
* fix(cli): enforce analyze finalization on fast path (#1169)
Address PR review feedback by checking finalization even when analyze reports already up to date, and by making the #1169 E2E guard fail on timeout instead of passing silently.
Made-with: Cursor
* test(cli): fix#1169 regression coverage on CI
Normalize macOS temp paths in the registry assertion and update the analyze worker timeout test mock for the new finalization invariant exports.
Made-with: Cursor
createFTSIndex now short-circuits on the in-process cache before issuing
the native CALL CREATE_FTS_INDEX, so a prior writable session cannot
trigger the macOS WAL/checkpoint duplicate-create path observed on main.
The cache is also primed on the "already exists" recovery and cleared on
re-init/close/drop, keeping ensureFTSIndex semantics identical for
read-only fallbacks.
The lbug-core-adapter close+reopen test moves to the end of the suite so
its native handle churn cannot corrupt later assertions in the same
fixture.
skills-e2e moves into its own sequential vitest project so the heavy
spawnSync-driven CLI fixtures stop competing with the parallel default
project on Windows runners, fixing the C-fixture beforeAll timeout.
Made-with: Cursor
* fix(ingestion): index Python repos with empty __init__.py and >32 KB files
Two defensive fixes that let `gitnexus analyze` complete on Python
codebases that previously failed.
scope-extractor: synthesize an empty Module scope when the provider
emits zero captures. Previously threw "no Module scope found", which
fired for any 0-byte `__init__.py` package marker if the bridge's
empty-source guard was bypassed.
python/captures: wrap the parser.parse() and getPythonScopeQuery()
.matches() calls in try/catch. node-tree-sitter throws "Invalid
argument" for sources that overrun internal buffers (observed at the
~32 KB threshold on Windows). Degrade gracefully with a clear
"skipping scope extraction for this file" warning instead of the
opaque "Invalid argument" surfacing through the bridge.
Verified by indexing whittlem/pycryptobot (which has 7 empty
__init__.py and 11 Python files between 34 KB and 158 KB):
2,367 nodes / 4,973 edges, no segfault, queries resolve symbols
inside the 158 KB controllers/PyCryptoBot.py.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ingestion): harden Python scope extraction fallbacks
Keep failed Python scope extraction on the bridge skip path and build synthetic module scopes before extractor indexes are derived.
Made-with: Cursor
---------
Co-authored-by: Vijay Gali <vgali@vexcelco.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
Let release-candidate.yml be the single main-push entry point that reuses CI before publishing, while keeping CI as the direct pull-request gate.
Made-with: Cursor
* fix(hook): resolve canonical repo root + guard read-only FTS ensure (#1224)
Two bugs in the Claude Code hook + query layer integration:
1. `findGitNexusDir` (in `gitnexus/hooks/claude/gitnexus-hook.cjs` and
`gitnexus-claude-plugin/hooks/gitnexus-hook.js`) walked upward from
cwd looking for a non-registry `.gitnexus/`. In linked git worktrees
created via `git worktree add`, the canonical repo's `.gitnexus/`
never sits above the worktree path, so the walk silently fails and
neither augmentation nor staleness notifications fire.
Fix: keep the cwd-walk as the fast path, then fall back to
`git rev-parse --git-common-dir` to resolve the shared `.git/`
directory (which lives inside the canonical repo across all linked
worktrees) and walk up from its parent. Returns null cleanly when
`git` isn't on PATH or cwd isn't inside any working tree.
2. `ensureFTSIndex` in the LadybugDB adapter rethrew when the active
connection is read-only (e.g. the MCP query pool, which opens DBs
read-only by design). Defensive callers used to surface five
"Cannot execute write operations in a read-only database" warnings
per query.
Fix: extract `isReadOnlyDbError` (mirroring the existing
`isDbBusyError` discriminator) and have `ensureFTSIndex` catch the
read-only error, cache the key, and return silently. Index creation
is owned by `gitnexus analyze` on a writable connection — the
ensure call is safely a no-op on the read pool. Lock / busy /
"already exists" / schema errors continue to propagate.
Tests:
- `test/unit/hooks.test.ts`: new "Linked git worktree resolution"
block exercises both hooks against a real linked worktree to confirm
PostToolUse stale notifications fire, plus a negative case when the
canonical repo has no `.gitnexus/`.
- `test/unit/lbug-readonly-error.test.ts`: new file unit-tests the
`isReadOnlyDbError` discriminator (positive matches, case
insensitivity, non-Error inputs, and unrelated errors that must
still surface — lock contention, "already exists", schema misses).
- `test/integration/lbug-core-adapter.test.ts`: extends the existing
FTS coverage with an idempotency assertion for `ensureFTSIndex` to
pin the read-only guard's success-path contract.
Verified with `npx tsc --noEmit` and `vitest run` on the affected
files (hooks + readonly + lbug-core-adapter + bm25-search +
lbug-extension-loader + lbug-embedding-hashes — 136 tests pass).
Build: `npm run build` succeeds.
Closes#1224
* fix(local-backend): cover supported vector path
Add the supported-platform regression assertion for QUERY_VECTOR_INDEX and align the unsupported VECTOR diagnostic wording with platform policy.
Made-with: Cursor
---------
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* fix(deps): upgrade @ladybugdb/core to 0.16.0 to resolve native segfaults
Resolves the SIGSEGV / access-violation (0xC0000005) / exit-139 crashes that
have been reported widely since 1.6.3. The native crashes originate in
@ladybugdb/core 0.15.x — primarily during FTS index creation, VECTOR
extension load, and concurrent query teardown — and are reproducible on
Linux, macOS and Windows. The maintainer-confirmed fix is to bump the
runtime to 0.16.0, which ships nodejs async + memory-management fixes,
extension ABI bump, and macOS Intel binaries.
Adopting 0.16.0 cleanly required three supporting changes; without them
the upgrade itself regresses other paths:
1. maxDBSize must be passed explicitly. 0.16.0 keeps the upstream JSDoc
note that the default 0 is "introduced temporarily for now to get
around with the default 8 TB mmap address space limit some
environment". Constrained CI runners and laptops cannot reserve 8 TB
and crash with "Buffer manager exception: Mmap for size
8796093022208 failed." A new gitnexus/src/core/lbug/lbug-config.ts
centralises a 16 GiB default (overridable via
GITNEXUS_LBUG_MAX_DB_SIZE) and every Database() construction site
now passes it.
2. enableCompression default flipped from false to true in 0.16.0. Every
Database() call site is updated to pass false explicitly so existing
GitNexus indexes keep the same wire format.
3. Bridge DB sidecar files (.wal, .shadow). 0.16.0 enforces a database-id
check on .wal / .shadow sidecars and rejects opens whose sidecars
belong to a different base name. writeBridge now (a) cleans the full
sidecar set when removing the tmp slot, (b) renames .wal / .shadow
alongside the main file during the atomic .tmp -> .lbug swap, and
(c) wraps openBridgeDbReadOnly in a bounded retry on transient
Win32-Error-33 lock errors. Eager db.init() / conn.init() forces the
lazy native handle to surface lock contention at the retry site.
Known limitation (not a regression): on Windows the 0.16.0 native binary
does not release the OS file lock until the process exits, so the
close-then-reopen-same-process pattern raises Error 33 after the first
close. Production paths (analyze / serve / mcp each open the DB exactly
once per process) are unaffected, but eight tests that exercise the
pattern are guarded with a process.platform === 'win32' skip; CI's
Linux + macOS shards exercise them as before. Tracking upstream:
kuzudb/kuzu#3872 / #3883 / #4730.
Closes#1136#1154#1160#1162#1178#1195#1196#1199#1204#1206
Refs #1209 (supersedes — Dependabot bump without the supporting fixes)
Made-with: Cursor
* fix(test): isolate LadybugDB native test state
Use per-suite LadybugDB databases in integration helpers so test forks do not reopen a database created by Vitest global setup, and centralize Windows-tolerant native temp cleanup for bridge tests.
* fix(lbug): avoid bridge existence reopen
Reuse the built LadybugDB config in the extension installer and avoid native close/reopen cycles when checking bridge existence on Windows.
Made-with: Cursor
* chore(docs): exclude local lbug plan
Keep the refactor planning note out of the PR while leaving the ignored local copy on disk.
Made-with: Cursor
* refactor(lbug): centralize database construction
Route LadybugDB opens through shared helpers so native constructor defaults stay consistent across core, pool, bridge, and extension install paths.
Made-with: Cursor
---------
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Addresses the medium-severity finding in @abhigyanpatwari's review of #1175:
the four `pair`-with-arrow patterns in `query.ts` anchored
`@declaration.function` on the outer `pair` node instead of the inner
`arrow_function` / `function_expression`. For multi-action object literals
like Zustand's
persist((set) => ({
addItem: (item) => doA(item),
removeItem: (item) => doB(item),
fetchData: () => doC(),
}))
`pass2AttachDeclarations.atPosition(pair.startLine, pair.startCol)`
resolved to the *parent* `(set) => ({...})` callback's scope (because the
pair node starts at the property-key token, before the inner arrow's
`@scope.function` range). All three pair-function defs landed in the
same parent's `ownedDefs`, and `resolveCallerGraphId.ownedDefs.find(...)`
returned the FIRST one — `addItem` — for every walk-up. Calls inside
`removeItem` and `fetchData` mis-attributed to `addItem`; those two
functions had zero outgoing CALLS edges in the registry-primary path.
Single-pair fixtures (`bump` in `store.ts`, `queryFn` in `query-hook.ts`)
masked the defect because there is no ambiguity when only one
Function-like def lives in the parent's `ownedDefs` — `find()` is
deterministic over a single-element set.
Fix: move the `@declaration.function` anchor from the outer `pair` to
the inner `arrow_function` / `function_expression`, mirroring the
`lexical_declaration` patterns above (`const fn = () => {}`). The def
then lands in the arrow's own scope's `ownedDefs`, the
`rangesEqual(anchor.range, innermost.range)` auto-hoist promotes the
binding to the parent scope (so importers + lookups still find the name
in the surrounding scope), and each pair-arrow becomes an independent
caller anchor in the walk.
Tests:
* Updated `useFeature → fetchData` expectation to `queryFn → fetchData`
in `typescript-hof-callbacks.test.ts`. The new attribution is
structurally correct: `fetchData()` is called from inside the named
pair-arrow `queryFn: () => fetchData()`. The pre-fix expectation
only worked because the pair-pattern bug rerouted the walk past the
syntactic owner.
* Added `multi-action-store.ts` fixture with three pair-arrows
(`addItem` / `removeItem` / `fetchData`) plus three top-level call
targets (`doA` / `doB` / `doC`). Four new tests pin per-action
attribution: positive (each action calls its own target), negative
(no sibling leakage), exact-set (the full pair set is what we
expect), and the regression fingerprint (`addItem → doB` MUST be
empty).
Validation:
* `REGISTRY_PRIMARY_TYPESCRIPT=1 vitest run` on
typescript-hof-callbacks (12 tests, +4 new), typescript-jsx-as-call
(7), typescript (236), typescript-finalize, typescript-cross-file-imports,
call-attribution-issue-1166 (18), all scope-resolution unit suites:
886/886 pass on registry-primary AND legacy DAG paths.
* Legacy DAG attribution was already correct via @abhigyanpatwari's
`tsExtractFunctionName` pair-parent handling (#1179, merged into this
PR earlier); this fix brings the registry-primary path to the same
behavior, restoring parity for multi-action objects.
* `npx prettier --check .`, `tsc --noEmit`, and `eslint` clean on the
three modified/added files.
Made-with: Cursor
Single line-length fix in `gitnexus/src/core/ingestion/languages/typescript.ts`
flagged by `quality / format` CI on commit ef96603f. The unformatted block came
from the merge of upstream PR #1179 (`fix/issue-1166-calls-edges`) where the
`pair`-with-arrow / `pair`-with-string-key handling was added; prettier wanted
the `.find` callback inlined onto a single line.
No behavior change. Pre-commit hook would have caught this locally if the
husky postinstall step had been able to write `.git/config` on this dev machine.
Made-with: Cursor
Introduced new scripts in package.json for GitNexus analysis:
- `gitnexus:refresh`: analyzes with embeddings and skills.
- `gitnexus:full`: forces analysis with embeddings and skills.
No production behavior changes. This enhances the development workflow for GitNexus users.
Addresses the automated review findings on PR #1175:
- prettier --write the 3 files flagged by `quality / format` CI check
(query.ts, typescript-hof-callbacks.test.ts, typescript-jsx-as-call.test.ts).
- [medium] typescript-jsx-as-call.test.ts: tighten the combined HOF+JSX
assertion from `toBeGreaterThan(0)` to `toHaveLength(1)`. A single
`<Foo />` is one logical invocation; the bounds-only assertion would
have masked a duplicate-CALLS-edge regression (e.g. if both
`jsx_self_closing_element` and a generic call pattern matched the
same site).
- [medium] typescript-hof-callbacks.test.ts: replace the vacuously-true
`for (c of calls) expect(...)` Zustand assertion with a structural
one. Old form passed unconditionally when `calls` was empty (any
change that silenced ALL CALLS edges from store.ts would have
slipped through). New form asserts both: (a) at least one File-rooted
edge exists (proving the `isCallerAnchorLabel` fallback fires), and
(b) no edge sources from anything else (proving the fallback fires
exclusively).
- [low] finalize-algorithm.ts (`findExportByName`): rephrase the
comment to make the language-agnostic nature of the tie-break rule
explicit. The implementation was already correct for all migrated
languages; only the comment overplayed the TypeScript specificity.
- [low] captures.ts (arity synthesis): add a comment explaining why
JSX call anchors (`jsx_self_closing_element` / `jsx_opening_element`)
intentionally don't synthesize `@reference.arity`. Name-only
resolution is correct for React (components aren't overloaded in the
current graph model); a JSX-aware synthesizer counting jsx_attribute
children would be needed if that ever changes.
No production behavior change. All 8/8 HOF + 7/7 JSX + 236/236
typescript + 11/11 api-deep-flow integration tests still pass.
gitnexus and gitnexus-shared typechecks clean.
Made-with: Cursor
Two roots in `findEnclosingFunctionId` (parse-worker) and the parallel
`findEnclosingFunction` (call-processor):
A. `genericFuncName` scanned `arrow_function` / `function_expression`
children for the first identifier and returned it. For unparenthesized
arrows like `file => processFile(file)` the first identifier is the
parameter `file`, so calls inside got attributed to a phantom
`Function file` ID and emitted dangling CALLS edges that never showed
up in `(:Function)-[:CALLS]->()` queries.
B. `tsExtractFunctionName` only named arrows whose parent was
`variable_declarator`. Object-property arrows like
`addItem: (item) => set(...)` (Zustand stores, TanStack queryFn,
React Context providers, config objects) live under a `pair`, so they
were treated as anonymous. With no named ancestor up to the file,
every call inside fell back to the File and became invisible to
`context()` / `impact()`.
Fix:
- `genericFuncName` returns null for anonymous JS/TS function-likes —
the language hook is authoritative.
- `tsExtractFunctionName` resolves names from `pair` parents
(property_identifier / string keys; computed keys stay anonymous).
- Mirror the new shape in `TYPESCRIPT_QUERIES` / `JAVASCRIPT_QUERIES` /
the scope-resolution query so pair-with-arrow becomes a Function
declaration node — call sourceIds resolve to a real graph node.
Adds 18 unit tests pinning attribution and definition behaviour for
plain helpers, `arr.map(x => fn(x))`, Promise constructor callbacks,
Zustand-style nested HOFs, TanStack query factories, string-keyed
pairs, and computed-key anonymity.
Fixes#1166
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two distinct gaps in the TypeScript scope-resolution path were silently
dropping call edges in real-world React + TanStack + Zustand codebases.
On the bug reporter's repo (Sourcerer-fe, 1185 src/ functions), 504
missing Function->Function CALLS edges are now captured (+61.6%) and
the no-outgoing-CALLS orphan rate drops from 73.2% to 60.3%.
HOF / arrow-callback caller-attribution (3 cooperating fixes):
- typescript/query.ts: @declaration.function anchor moved from the
wrapping lexical_declaration to the inner arrow_function /
function_expression, so anchor.range aligns with @scope.function and
pass2AttachDeclarations lands the def on the arrow's own scope.
- finalize-algorithm.ts: findExportByName prefers callable / class-
like defs over Variable when localDefs contains both for the same
name (TS emits two defs per `const fn = () => {}`).
- graph-bridge/ids.ts: resolveCallerGraphId's walk-up class-fallback
now uses isCallerAnchorLabel restricted to Function / Method /
Constructor / Class / Interface / Struct / Enum, so module-level
calls fall through to the File node instead of mis-attributing to
sibling Variable defs (the Zustand `create()(devtools(...))`
phantom-self-loop regression).
JSX as a CALLS edge (2 cooperating fixes):
- typescript/query.ts: new TSX_JSX_QUERY_SUFFIX (TSX-grammar only)
captures jsx_self_closing_element / jsx_opening_element as
@reference.call.free / @reference.call.member. PascalCase predicate
filters native HTML elements (<div>, <span>) so they don't emit
edges to nonexistent targets.
- typescript/captures.ts: shouldEmitReadMember extended with
jsx_self_closing_element / jsx_opening_element parent cases to
suppress phantom ACCESSES edges on member-form JSX names.
Tests: 8 HOF assertions + 7 JSX assertions across two new integration
test files plus 13 minimal fixtures. typescript.test.ts (236),
api-deep-flow.test.ts (11), and scope-resolution / scope-extractor unit
tests (613) pass with no regressions.
Made-with: Cursor
The "Web UI (browser-based)" section described an old client-side
architecture. Today gitnexus.vercel.app is a thin frontend that
auto-connects to a local `gitnexus serve` backend — there is no
ZIP drag-and-drop and no fully self-contained mode.
- Drop "No server, no install" claim
- Replace "drag & drop a ZIP" tagline with the actual onboarding step
- Add the missing `gitnexus serve` step to the local-dev block
Closes#1110
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add platform-aware semantic fallback
Make VECTOR an optional capability so Windows analysis remains stable while semantic embeddings can fall back to exact scan when native vector indexing is unavailable.
Made-with: Cursor
* fix: remove stale vector pool import
Keep the merge with main lint-clean after VECTOR loading moved out of the read pool.
* fix(swift): use official prebuilt parser runtime
Vendor the official tree-sitter-swift 0.7.1 runtime package so Swift parsing works without source-building, while keeping the repo on the current tree-sitter runtime until the broader upgrade is ready. Also preserves Swift resolver correctness for overloaded owned functions and extension-backed type duplicates now that Swift is available by default.
Made-with: Cursor
* fix(swift): move duplicate type ordering into provider
Keep Swift extension candidate ordering behind the LanguageProvider contract and cover the Swift 0.7 init scanner path so parser runtime changes do not leak language-specific logic into shared resolution.
Made-with: Cursor
* fix(swift): address parser runtime review
Add explicit Swift prebuild checks and vendor guidance so parser runtime packaging remains observable and maintainable.
* fix(hooks): ignore global registry during staleness checks
* test(hooks): cover indexed repos under global registry
---------
Co-authored-by: laplace young <yangqk12@whu.edu.cn>
* fix(group): add configurable cross-link path exclusions to reduce false positives
Add matching.exclude_links_paths and matching.exclude_links_param_only_paths
to group.yaml config. These filter out noisy HTTP contracts (health checks,
param-only catch-all routes) from cross-link matching while preserving them
in the contract registry for documentation purposes.
Defaults are empty/false for backward compatibility — no behavior change
unless the operator explicitly configures exclusions.
* fix(group): address review findings — filter unmatched, normalize trailing slash, add tests
- Excluded contracts no longer inflate SyncResult.unmatched (isNoisy guard)
- pathPart in buildNoisyContractFilter strips trailing slashes before comparison
- 8 new unit tests for buildNoisyContractFilter covering all code paths
- Config-parser test asserts defaults for new matching fields
* fix(group): normalize configured exclusion paths and add root-path test
- Strip trailing slashes from configured exclude_links_paths at Set-build
time so root path '/' (which normalizes to '') matches correctly
- Add test: exclude_links_paths: ['/'] suppresses http::GET::/ contracts
- Add new matching fields as commented examples in fixture group.yaml (DoD §2.4)
* docs(group): document exclude_links_paths and exclude_links_param_only_paths config fields
Add JSDoc to MatchingConfig interface, update the microservices guide
YAML example and field notes, and scaffold the new fields (commented out)
in the group create template.
* fix(lbug): bound DuckDB extension install via ExtensionManager (closes#1128)
`gitnexus analyze` could hang indefinitely (60% / 85% on Windows) when
DuckDB's `INSTALL fts` or `INSTALL VECTOR` was unable to reach
`extensions.duckdb.org`. The DuckDB driver's INSTALL is a synchronous
network call, so any blocked egress would block the Node event loop
forever.
Replace the ad-hoc, in-process INSTALL/LOAD scattered across
`lbug-adapter.ts` and `pool-adapter.ts` with a single
`ExtensionManager` that owns the lifecycle of optional DuckDB
extensions:
* `LOAD` is always tried first — per-connection, idempotent, no network.
* If `LOAD` fails and policy permits, INSTALL runs in a short-lived
child Node process bounded by `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS`
(default 15s). The parent loop keeps spinning; on timeout the child is
killed with SIGKILL and the capability is flagged unavailable.
* Capabilities and install attempts are cached per process, so a single
bounded install per extension covers every subsequent call.
Install policy is now an explicit, per-context decision:
* `auto` (default for analyze) — try LOAD, fall back to bounded INSTALL.
* `load-only` — used by `pool-adapter` (serve / MCP read paths) so user
queries never block on a network install.
* `never` — operator escape hatch for offline / airgapped environments.
`createFTSIndex` and `createVectorIndex` now check the boolean return
value before issuing the index DDL, so missing extensions degrade BM25
and semantic search gracefully without ever throwing during analyze.
Tests:
- New unit suite for `ExtensionManager` covering LOAD-first behavior,
all three policies, install caching, observability, and warn dedup.
- Existing vector-extension integration tests pass against the new
boolean return type.
- Existing embedding-pipeline mocks updated to return `true`.
Docs: `gitnexus/README.md` documents `GITNEXUS_LBUG_EXTENSION_INSTALL`
and `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS` with examples for
offline and slow-network environments.
Made-with: Cursor
* fix(lbug): move DuckDB extension install child into script
Keep the bounded out-of-process INSTALL behavior, but replace the inline child code with a stable packaged ESM script. This makes the child process directly runnable and gives debuggable stack traces without source-vs-dist branching or a runtime transpiler.
Made-with: Cursor
Avoid remote git/SSH downloads for the Dart grammar during Docker and npm installs by resolving tree-sitter-dart from vendored source and building it during postinstall.
Made-with: Cursor
installOpenCodeSkills() was writing to ~/.config/opencode/skill/gitnexus/
but OpenCode only discovers skills from ~/.config/opencode/skills/*/SKILL.md.
Skills installed by `gitnexus setup` were silently ignored by OpenCode.
- Line 590: path.join(opencodeDir, 'skill') → 'skills'
- Line 587: updated JSDoc comment to match
* fix(serve): serve web UI at root path instead of 404
gitnexus serve returned Cannot GET / because no route handler existed
for the root path. Now serves the built gitnexus-web dist at / with
SPA fallback for client-side routing. Falls back to a helpful landing
page with API links when the web UI hasn't been built yet.
Also updates the build script to build and copy gitnexus-web into
gitnexus/web/ for the published npm package.
* fix(serve): address Copilot review feedback
- Use regex SPA fallback that excludes /api paths (avoids serving
index.html for unknown API routes)
- Add rel="noopener noreferrer" to external link (reverse-tabnabbing)
- Move build "done" log after web UI step
* fix(build): use npm run build for web UI, add npm install guard
The build script ran `npx tsc -b && npx vite build` in gitnexus-web/,
but CI only installs node_modules for gitnexus/ — not gitnexus-web/.
npx then resolved the wrong `tsc` package (a trojan on npm), causing
all CI jobs to fail.
Fix: add an npm install guard when node_modules is missing, and use
`npm run build` (which runs the local typescript) instead of npx.
* feat(serve): styled fallback page, asset 404s, build script safety
- Add landingPageHtml() with gitnexus-web design tokens (void bg,
surface cards, accent color, terminal-style build command block).
- Add resolveWebDistDir() helper with non-ENOENT error logging.
- Register express.static with Cache-Control headers (no-cache HTML,
immutable assets) and SPA fallback route.
- Replace wildcard SPA fallback with regex that excludes /api/* AND
asset-like file extensions (.js, .css, .ico, .woff2, .map, etc.).
- Add ordering comment warning about SPA fallback route placement.
scripts/build.js:
- Change npm install to npm ci.
- Add timeout: 120_000 to all execSync calls.
Test coverage:
- 26 new unit tests for design tokens, terminal block, external links,
SPA regex acceptance/exclusion, cache headers, and fs.access edge
cases.
Closes#1048 (review feedback)
* fix: format, lint, and add GITNEXUS_WEB_DIST env var
- Remove unused fsType import from web-ui-serving.test.ts (lint error)
- Run prettier on fallback-page-screenshot.html and test file
- Add GITNEXUS_WEB_DIST env var as primary override in resolveWebDistDir
- Add tests for env var: prefer when set, fallback when dir missing
* fix: use cross-platform path matching in env var tests
Path.includes('/env/dist') fails on Windows where path.join
produces backslashed paths. Normalize via path.sep replacement
before matching.
* fix(serve): address PR #1048 review findings
- Add uncaughtException/unhandledRejection crash guards to HTTP serve path
- Export SPA_FALLBACK_REGEX so tests use the production constant (no drift)
- Export staticCacheControlSetHeaders so tests verify the real production function
- Add real Express dispatch tests for API 404 and asset 404 isolation
- Delete committed debug artifact fallback-page-screenshot.html
* fix(scope-resolution): allow same-range Module-as-parent for top-level scopes (closes#1086)
When a C# file consists of a single top-level `namespace_declaration` that
ends exactly at EOF (no trailing newline, no leading content outside the
namespace's `{}` body), tree-sitter-c-sharp 0.23.1 reports identical byte
ranges for `compilation_unit` and `namespace_declaration`. Pre-fix the
scope-extractor parent-finder relied on strict containment, so the Module
was popped off the stack and the Namespace ended up with `parent === null`
→ `ScopeTreeInvariantError: non-module-requires-parent` →
`extractParsedFile` swallowed the throw and the whole file was dropped
from the registry-primary path. Cross-file IMPORTS / CALLS edges
originating in or terminating at that file vanished.
Hit on three real-world `*.Designer.cs` files in PersistentWindows
(`HotKeyWindow.Designer.cs`, `LaunchProcess.Designer.cs`,
`DbKeySelect.Designer.cs`) — all have the byte signature
`<BOM><CRLF>namespace ... { ... }<EOF>` (last hex = `... 7D 0D 0A 7D`).
The fix is a single carve-out in the parent-validity contract: a `Module`
may parent a same-range non-`Module` child. The relationship stays
acyclic because the carve-out is direction-asymmetric — only Module-as-
outer parents a same-range non-Module, never the reverse.
Two coordinated changes:
* `gitnexus/src/core/ingestion/scope-extractor.ts` — `pass1BuildScopes`
now consults a new `canParentScope` helper instead of
`rangeStrictlyContains` directly. Sort tie-breaker added so a same-
range Module always sorts before a non-Module candidate, ensuring the
Module lands on the parent-stack first regardless of tree-sitter
capture iteration order.
* `gitnexus-shared/src/scope-resolution/scope-tree.ts` — `buildScopeTree`'s
`parent-must-contain-child` check now uses the same `canParentScope`
carve-out so the validator agrees with the extractor on what a
well-formed parent edge looks like. Error message updated to spell
out the new contract.
`rangeStrictlyContains` keeps its strict semantics in both files —
position-index lookups, hook-side range comparisons, and other call
sites are unchanged.
* `gitnexus/test/fixtures/lang-resolution/csharp-namespace-as-root-no-trailing-newline/`
— minimal regression fixture mirroring the PersistentWindows shape:
both `Models/User.cs` and `App/Program.cs` end exactly on the closing
`}` of their namespace with no trailing newline. The trigger is shape-
driven, not size-driven, so the fixture stays small (~250 bytes total).
* New `csharp.test.ts` describe block: scope extraction completes for
both files, and the cross-file `IMPORTS` edge resolves through the
scope-resolution path with `reason: 'csharp-scope: using'`.
* `scope-tree.test.ts`: replaced the prior "rejects child ranges
identical to the parent" case with three new ones — non-Module parent
still rejected at equal range; Module-as-parent of a same-range non-
Module accepted (the #1086 carve-out); Module-as-parent of another
Module still rejected (the asymmetry guard).
* `npx vitest run test/unit/scope-resolution test/integration/resolvers`
→ 2514 passed / 77 skipped / 0 failed (52 test files).
* `npx tsc --noEmit` clean in both `gitnexus/` and `gitnexus-shared/`.
* End-to-end on PersistentWindows (after rebuilding the Docker image
with this branch): 3 prior `scope extraction failed for *.Designer.cs`
warnings → 0. Pre-fix index numbers will be re-checked here once the
branch is built and indexed; the existing post-#1082 baseline is
1113 nodes / 2987 edges / 39 clusters / 97 flows.
`canParentScope` is language-agnostic. Other languages whose query emits
`(compilation_unit) @scope.module` plus a single same-range top-level
scope can naturally hit the same byte shape on minimal files; this fix
applies to all of them uniformly.
Refs: #1086 (issue with full root-cause analysis + 4-case empirical
repro through `extractParsedFile`).
* refactor(scope-resolution): export canParentScope from gitnexus-shared
Addresses #1087 review (medium): the helper was previously duplicated
byte-for-byte in `scope-extractor.ts` and `scope-tree.ts`. Per DoD
"single source of truth in shared", the contract piece belongs in
gitnexus-shared (Ring 2 SHARED #912) and the consuming layer should
import it. Eliminates the silent-drift surface where a future edit
to one copy would produce extractor/validator disagreement on what
a well-formed parent edge looks like.
Changes:
- gitnexus-shared/src/scope-resolution/scope-tree.ts: add `export`
to `canParentScope`.
- gitnexus-shared/src/index.ts: re-export `canParentScope`.
- gitnexus/src/core/ingestion/scope-extractor.ts: remove the local
`canParentScope` definition (and its now-unused local copy of
`rangeStrictlyContains`), import from `gitnexus-shared`. The local
`rangesEqual` stays — it's still used in capture-anchor logic at
two unrelated sites.
Validation (per DoD §4.4 — both CLI and web consumers verified):
- npx tsc --noEmit clean in gitnexus/ and gitnexus-shared/
- cd gitnexus-web && npx tsc -b --noEmit clean
- gitnexus-shared `npm run build` clean
- Targeted: vitest run test/unit/scope-resolution test/integration/resolvers
→ 2522 passed / 0 failed / 77 skipped (54 files)
- Full suite: vitest run → 7238 passed / 1 failed / 97 skipped.
The single failure is `test/unit/ignore-service.test.ts > warns
on EACCES but does not throw`, which cannot run when uid=0 (root
bypasses POSIX permission checks). Pre-existing on this branch
before the refactor; unrelated to scope-resolution.
The `loadIgnoreRules — error handling > warns on EACCES but does not
throw` test relies on `chmod 000` denying read access to a temporary
.gitignore file. On Linux, root bypasses POSIX read-permission checks,
so chmod 000 does NOT trigger EACCES under uid=0 — fs.readFile reads
the file anyway and loadIgnoreRules returns parsed rules instead of
the `null` the test expects.
Symptom under root: assertion fails with `Ignore { _rules: [...] }
to be null`, surfaced as a single test failure in any privileged
test environment (rootful Docker container, CI runners configured to
run tests as root, etc.).
Fix: extend the existing `skipIf(process.platform === 'win32')` guard
with `process.getuid?.() === 0`. The non-root code path still
exercises the real EACCES branch — root just can't reproduce the
failure mode the test asserts on, so skipping there is the correct
posture (matches the win32 skip's reasoning: the OS-level mechanism
the test depends on isn't available there).
Optional chaining (`getuid?.()`) keeps Windows compatibility — Node
on Windows doesn't expose `process.getuid` at all.