* fix: devendor tree-sitter-proto install lifecycle to fix ENOTEMPTY on global upgrade
PR #843's preinstall cleanup hook cannot address the reported bug because
it runs on the NEW package's staging tree, not the OLD install being
removed. Issue #836 still reproduces on 1.6.2-rc.8.
Root cause: vendor/tree-sitter-proto was declared as `file:` dep with its
own `dependencies` and `install` script, so npm created
`vendor/tree-sitter-proto/node_modules/node-addon-api/` at install time,
which blocked npm's rmdir on global upgrade.
Changes:
- Strip `dependencies` and `install` script from the vendored sub-package's
package.json so npm no longer creates a nested node_modules or runs a
lifecycle script under vendor/.
- Hoist `node-addon-api` and `node-gyp-build` into gitnexus
optionalDependencies; npm resolves them at the consumer's top level.
- Add scripts/build-tree-sitter-proto.cjs modeled on patch-tree-sitter-swift.cjs.
Runs at gitnexus postinstall, best-effort: skips cleanly on missing
toolchain or --ignore-scripts so non-proto functionality keeps working.
- Remove scripts/preinstall-cleanup.cjs — dead code; cannot run against
the old install being removed.
- Keep .npmignore entries from PR #843 (tarball hygiene, still correct).
- Add explicit .gitignore rules for gitnexus/vendor/**/build and
gitnexus/vendor/**/node_modules (closes the repo-side hygiene gap).
- Add .github/workflows/ci-global-upgrade.yml: matrix smoke test that
installs the previously-published rc globally, upgrades to the packed
current branch, and verifies no vendor install-time artifacts survive.
Runs on macOS (reporter's platform), Linux, and Windows. Also includes
an --ignore-scripts degraded-mode lane. Wired into ci.yml gate.
Plan: docs/plans/2026-04-15-002-fix-tree-sitter-proto-vendor-deps-plan.md
Phase 1 (this commit) addresses the reported `node_modules/node-addon-api`
hazard. Phase 2 (follow-up) will migrate to prebuildify + prebuilt .node
binaries in the tarball — the 2026 canonical shape for tree-sitter
grammars, which eliminates the postinstall compile path entirely.
Refs #836
* fix(ci): ci-global-upgrade should be reusable-only and use setup-gitnexus
Three issues caught by CI on PR #846:
1. Concurrency linter rejected the `CIGU-` prefix (allowlist is
`${{ github.workflow }}` or substring `CI-`). The literal-prefix
guidance in ci.yml is specifically about disambiguating when
reusable workflows run in nested contexts, and ci-global-upgrade
doesn't need its own concurrency block at all — the caller
(ci.yml) already governs concurrency for nested invocations.
2. `npm install` in gitnexus/ runs `prepare: node scripts/build.js`,
which depends on gitnexus-shared/dist being built first. Other CI
jobs handle this via the setup-gitnexus composite action. Use it
here too (with build: 'false' — we only need the dep graph, then
npm pack runs prepack which builds gitnexus itself).
3. Removed `pull_request` and `workflow_dispatch` triggers. The
workflow is now pure `workflow_call` — invoked once from ci.yml
via `uses:`. This avoids the duplicate-run problem where both the
top-level pull_request trigger AND the nested workflow_call would
fire on every PR.
* fix(ci): relax vendor build/ guard and use bash shell on Windows
Two fixes for ci-global-upgrade failures on PR #846:
1. The guard after the upgrade step was rejecting vendor/tree-sitter-proto/build/
in the global install. That was too strict. The original #836 bug was
about vendor/tree-sitter-proto/node_modules/ specifically, not build/.
The build/ directory appears because node-gyp-build compiles through the
symlink npm creates at node_modules/gitnexus/node_modules/tree-sitter-proto,
and its contents are plain .node, .obj, .lib files that rmdir handles
without trouble. We know this empirically because the test got past the
upgrade step in the run where the old vendor/node_modules was present.
The guard now only flags nested node_modules, which is what the fix
actually removes.
2. The Windows --ignore-scripts lane failed with ENOENT when npm tried to
open the tarball. The path was computed in a bash step using $(pwd),
which on Windows returns /d/a/... form, but npm install ran in the
default cmd shell and received a mangled Windows path. Adding
shell: bash to the install steps keeps path handling consistent.
When @huggingface/transformers is installed globally (e.g. via npm install -g),
it defaults its cache directory to ./node_modules/.cache inside its own install
dir, which is unwritable by non-root users.
This causes EACCES errors on first use when the model is downloaded:
EACCES: permission denied, mkdir '/usr/lib/node_modules/gitnexus/node_modules/@huggingface/transformers/.cache'
Set env.cacheDir before pipeline() is called in both embedders (CLI and MCP).
Respects HF_HOME env var if set, falls back to ~/.cache/huggingface.
Co-authored-by: Sisyphus <sisyphus@gitnexus.dev>
* ci: standardize workflow concurrency and automate release-note labeling
Concurrency — prevent racing CI jobs
- Every top-level workflow now declares an explicit concurrency block.
- PR runs cancel-in-progress on supersede; main/push/workflow_call/publish
runs queue instead of cancelling so every commit and every release is
validated end-to-end.
- ci.yml uses a literal `CI-` prefix (not `${{ github.workflow }}`) and a
per-run nested group for workflow_call invocations, avoiding a potential
deadlock with publish.yml and release-candidate.yml callers whose own
concurrency groups could otherwise collide with the called workflow.
- ci-report.yml falls back to `<head-repo>/<head-branch>` for fork PRs
(stable across reruns) instead of the per-run-unique workflow_run.id
which did not actually serialize anything.
- ci-quality.yml enforces the convention: fails CI if any non-reusable
workflow lacks a concurrency block or a reusable workflow declares one.
Release-note automation
- New pr-labeler.yml: amannn/action-semantic-pull-request enforces
conventional-commit PR titles on pull_request (fork-safe, read-only);
release-drafter/release-drafter with disable-releaser: true applies the
matching label under pull_request_target (write-scoped). sync-labels in
.github/release-drafter.yml removes managed autolabels that no longer
match (e.g. when `!` or `BREAKING CHANGE:` is dropped from a PR).
- .github/release.yml (unchanged) continues to map labels to categorized
release-notes sections.
- dependabot.yml added for the github-actions ecosystem so pinned SHAs
auto-refresh on a weekly cadence.
Docs
- CONTRIBUTING.md documents the concurrency convention, the
conventional-commit PR-title rules, and the reusable-workflow exception.
Follow-up to verify before relying on the labeler in anger
- gh api repos/amannn/action-semantic-pull-request/git/refs/tags/v5.5.3
- gh api repos/release-drafter/release-drafter/git/refs/tags/v6.0.0
- Confirm release-drafter reads its config from the base ref (not fork
head) when invoked via pull_request_target.
* ci: address PR review feedback on concurrency and labeler workflows
Two blocking fixes
- pr-labeler.yml: separate concurrency slots for pull_request and
pull_request_target. Previously both triggers shared a single group
with cancel-in-progress: true, so the privileged autolabel run could
cancel the title-validation check mid-run and leave a required status
in a permanent cancelled state.
- pr-labeler.yml autolabel job: add contents: read. release-drafter's
context.config() reads .github/release-drafter.yml from the default
branch via the repo-contents API and 403s without the scope. Job-level
permissions nullify all unlisted scopes so an explicit grant is needed.
Two non-blocking improvements
- Replace the hardcoded reusable-workflow allowlist in ci-quality.yml
with dynamic on:-block parsing. New workflow_call-only workflows no
longer produce false-positive convention failures.
- Implement actual group-key validation. The check now also asserts that
every concurrency.group expression references either ${{ github.workflow }}
or the literal CI- prefix (the documented ci.yml exception).
- Script extracted to .github/scripts/check-workflow-concurrency.py so
it is runnable locally and independently testable.
* Initial plan
* fix: stale vectors preserved on content edits and vector index missing after zero-node run
Issue 1: Add contentHash to EMBEDDING_SCHEMA and embedding pipeline.
- contentHash column persisted per CodeEmbedding row
- POST /api/embed queries nodeId+contentHash, compares per-node hash
- Stale rows (hash mismatch) are DELETE'd before re-embedding
- Legacy DBs without contentHash treated as stale (full re-embed)
- loadCachedEmbeddings and run-analyze cache restore include contentHash
Issue 2: createVectorIndex called unconditionally before zero-node early return.
Regression tests:
- contentHashForNode determinism and content-change detection
- EMBEDDING_SCHEMA includes contentHash STRING column
- Pipeline exports verified
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/1581c0c0-f359-4376-b47e-62d24a28fd2d
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix: use parameterized query for stale embedding DELETE, revert package-lock.json
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/1581c0c0-f359-4376-b47e-62d24a28fd2d
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix: address review feedback — config consistency, narrow catches, extract DB logic
Bug #1: Use finalConfig consistently in contentHashForNode (line 224 was
using raw `config` while line 307 used `finalConfig`). Cache precomputed
hashes in filter phase to avoid double computation (Perf #5).
Bug #2: Narrow catch in loadCachedEmbeddings to only fall back on
column/table-missing errors. Rethrow transient/connection errors.
Bug #3: Log non-trivial DELETE failures instead of silently swallowing.
Arch Violation #3: Extract fetchExistingEmbeddingHashes from api.ts into
lbug-adapter.ts. Server layer now calls a single adapter function instead
of re-implementing the DB query logic with nested try-catch.
Tests: Add config consistency test, note that fetchExistingEmbeddingHashes
tests require native module (run in CI).
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b8c4f6b0-4095-4507-a15d-d8469793efac
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix: narrow Column error match to 'contentHash' in lbug-adapter fallback checks
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b8c4f6b0-4095-4507-a15d-d8469793efac
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix: address production-readiness review — eliminate competing state, use schema constants, hard-fail on stale DELETE, add incremental filter tests
Gap A / Arch Violation 1: Remove duplicate vectorExtensionLoaded flag from
embedding-pipeline.ts — delegate to lbug-adapter's loadVectorExtension()
which owns the VECTOR extension lifecycle and resets on DB reconnect.
Arch Violation 2: Replace all hardcoded 'CodeEmbedding' and
'code_embedding_idx' strings in embedding-pipeline.ts and run-analyze.ts
with EMBEDDING_TABLE_NAME, EMBEDDING_INDEX_NAME, and CREATE_VECTOR_INDEX_QUERY
imported from schema.ts. Add EMBEDDING_INDEX_NAME export to schema.ts.
Gap B: Make DELETE failure for stale vectors a hard throw (not just a
warning). Continuing after failed DELETE risks Kuzu vector-index corruption
since the constraint requires DELETE-before-INSERT for vector-indexed
properties. "not found" / "does not exist" errors are still safe to ignore.
STALE_HASH_SENTINEL: Define a named constant in embedding types.ts for the
empty-string sentinel convention. Used consistently in lbug-adapter.ts and
run-analyze.ts so the invariant is self-documenting.
Tests: Add comprehensive unit tests for the incremental filter logic with
mocked embedder:
- New node → embedded
- Unchanged node (hash matches) → skipped
- Stale node (hash mismatch) → DELETE + re-embed
- STALE_HASH_SENTINEL → treated as stale
- Zero nodes after filter → createVectorIndex still called
- DELETE failure with non-trivial error → throws
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b21edee7-c9c5-4742-947b-d0def4fb26aa
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix: tighten error classification — extract isMissingColumnOrTableError helper, remove broad pattern matching
- Extract isMissingColumnOrTableError() helper in lbug-adapter for
consistent schema-error detection (replaces duplicate inline checks)
- Tighten 'contentHash' match: now requires 'property' AND 'contentHash'
(Kuzu-specific pattern) instead of broad 'contentHash' substring
- Tighten DELETE error check: only ignore 'does not exist' (Kuzu's actual
message), not broad 'not found' which could mask connection errors
- Fix test node ID/name/filePath consistency
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b21edee7-c9c5-4742-947b-d0def4fb26aa
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix: CI failures and final review — move STALE_HASH_SENTINEL to schema, tighten error matching, fix test mocking, format
- Move STALE_HASH_SENTINEL from embeddings/types.ts to lbug/schema.ts
(fixes inverted layer dependency: lbug should not import from embeddings)
- Tighten isMissingColumnOrTableError: replace broad msg.includes('not found')
with /(table|column|property).*not found/i regex to avoid matching transient errors
- Add vi.resetModules() in test beforeEach for explicit module isolation
(fixes vi.doMock not intercepting loadVectorExtension in CI)
- Skip precomputedHashes.set() on unchanged (return false) path
- Run prettier on all 5 files flagged by CI format check
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e20311fd-4361-47b4-a137-9adc3e533b35
* fix: address remaining review nits — rename precomputedHashes, generalize error matcher, revert package-lock
- Rename precomputedHashes → computedStaleHashes (hashes are computed
on-demand during filter, only cached for stale nodes being re-embedded)
- Remove contentHash-specific clause from isMissingColumnOrTableError —
the regex /(table|column|property).*not found/i already covers it
- Revert package-lock.json ssh→https protocol change
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e20311fd-4361-47b4-a137-9adc3e533b35
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix(group/sync): wire ManifestExtractor into syncGroup pipeline
ManifestExtractor was fully implemented in extractors/manifest-extractor.ts
but never imported or called in sync.ts. As a result, any links declared in
group.yaml were parsed and validated by config-parser.ts but silently dropped
— config.links was always an empty dead-end as far as syncGroup was concerned.
Changes:
- Import ManifestExtractor in sync.ts
- Call extractFromManifest(config.links, dbExecutors) inside the outer try
block, after all repos are processed but before the finally closes the DB
pools (symbol resolution via resolveSymbol requires open executors)
- Collect the resulting contracts into autoContracts and the cross-links into
a separate manifestCrossLinks array
- Merge manifestCrossLinks into the final crossLinks alongside runExactMatch
results
Without this fix, users who declare explicit service dependencies in
group.yaml links (the documented workaround for HTTP clients that use absolute
URLs and are invisible to the auto-extractors) get 0 cross-links regardless of
what they configure.
* test(group/sync): cover manifest links producing cross-links
Add a unit test that asserts config.links entries produce contract pairs
and a manifest cross-link (matchType: 'manifest') via syncGroup.
Also refactors the manifest extraction call to sit outside the else/try
block so it runs regardless of extractorOverride arity — makes the code
testable without mocked DB pools and ensures links work when callers supply
a zero-arity override (e.g. in tests or programmatic usage).
* style: prettier format sync.ts and sync.test.ts
Also removes the stray empty line in the finally block (noted in review).
* fix(group/sync): dedupe cross-links and warn on dangling manifest repos
Addresses review feedback on PR #827:
1. Dedupe cross-links. Manifest contracts participate in runExactMatch, so a
manifest-declared link also emitted a duplicate matchType:'exact' CrossLink
for the same endpoint pair. Dedupe by (from, to, type, contractId) and
prefer manifest (operator-declared intent).
2. Warn on dangling repos. When a manifest link references a repo not in
config.repos, log a warning. Synthetic UIDs keep the cross-link
deterministic, but the operator probably meant something else.
3. Tests:
- Assert no duplicate 'exact' CrossLink is emitted alongside the manifest one.
- Assert synthetic UID format when no DB executors are available.
- New test: dangling manifest repo still produces a cross-link + logs a warning.
* perf(group/manifest): parallelize and memoize symbol resolution
Previous implementation ran 2N sequential Cypher round-trips per
manifest (one for provider side, one for consumer, awaited in-order
per link). For manifests with tens of links this dominated syncGroup
latency in groups with many declared cross-repo contracts.
Changes:
- Resolve provider + consumer in parallel per link (Promise.all).
- Resolve all links in parallel (outer Promise.all over links.map).
Each repo's executor pool is independent, so cross-repo fan-out
scales with the number of distinct repos in the manifest.
- Memoize by (repo, type, contract). Manifests frequently declare
the same contract from both directions or across sibling groups,
so duplicate triples now hit the DB once instead of 2× per link.
Correctness:
- resolveSymbol is a pure LIMIT 1 read, so caching + concurrent
invocation is safe.
- Iteration order over links is preserved in the final
contracts / crossLinks arrays — result shape is identical.
Test:
- New test asserts that two links sharing (repo, type, contract)
produce exactly one DB call per distinct repo-tuple.
---------
Co-authored-by: jonasvanderhaegen-xve <>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* fix(lbug): wait for read stream close in splitRelCsvByLabelPair (Windows ENOTEMPTY)
The windows-latest CI job intermittently failed:
FAIL test/unit/rel-csv-split.test.ts > splitRelCsvByLabelPair > handles empty CSV (header only) without errors
Error: ENOTEMPTY: directory not empty, rmdir 'C:\Users\RUNNER~1\AppData\Local\Temp\rel-csv-test-XW5KOu'
Cause: splitRelCsvByLabelPair resolved its Promise on readline's 'close'
event, but the underlying fs.ReadStream's file descriptor is released
asynchronously after that — especially on Windows. For the empty-CSV
test the function returns so quickly that afterEach fires rmSync while
the relations.csv fd is still held, so Windows reports ENOTEMPTY on
the directory.
Fixes:
- Production: after readline 'close', wait for inputStream 'close' (or
resolve immediately if already closed/destroyed). Call inputStream
.destroy() defensively so we never hang if the fd never emits 'close'.
- Test: afterEach now retries rmSync up to 5 times on ENOTEMPTY/EBUSY/
EPERM with a brief back-off — defense-in-depth so the test doesn't
flake on slow CI runners independent of the production change.
The production fix benefits every caller, not just the test: any code
that deletes the CSV's parent directory right after the Promise
resolves previously hit the same race on Windows.
* refactor(lbug): replace custom stream state machines with stdlib primitives
Full audit of splitRelCsvByLabelPair's stream usage after the original
ENOTEMPTY fix. Replaced three hand-rolled mechanisms with their
standard-library equivalents — 147 -> 71 lines in the function, and
the caller's WriteStream closure dropped from 13 lines to 5.
- readline: 'on(line)' + pause/resume/waitingForDrain state machine
-> 'for await (const line of rl)'. Async-iterator delivery naturally
serializes line processing with our awaits, so at most one ws is in
backpressure at a time. We just 'await once(ws, "drain")' when
'write()' returns false — the custom Set, the settled flag and the
'only resume when all streams have drained' logic all go away.
- Multi-stream error coordination: hand-rolled cleanup() that had to
be entered exactly once and had to destroy the inputStream and every
pair ws -> single AbortController shared across every 'once(ws,
'drain', { signal })'. Any stream error aborts every pending wait.
- 'stream/promises.finished(inputStream)' in the 'finally' block
replaces the manual 'rl.on('close', () => inputStream.once('close',
...))' dance, and covers both the success and error paths with the
same primitive. This closes the Windows ENOTEMPTY race root cause —
we never return while the fd might still be in flight.
- Caller closure: 'new Promise((res, rej) => ws.end(cb) + remove
listener on error)' -> 'ws.end(); await finished(ws)'.
- Test 'afterEach': custom retry loop -> 'fs.rmSync(..., { maxRetries:
5, retryDelay: 50 })' (Node added these options specifically for
cross-platform tmpdir cleanup).
- Test 'destroys all streams when one errors': old code leaked
backpressure and created multiple pair streams before the first
blocked; new strict serial backpressure doesn't, so the test now
unblocks the first stream once to advance the loop and create the
second stream before triggering the error.
* fix(csv-generator): deduplicate all node types, not just File nodes
The pipeline can produce duplicate node IDs across all symbol types
(Class, Method, Function, etc.). Only File nodes were guarded by a
seenFileIds Set, leaving every other type unprotected. When the CSV
was COPY'd into LadybugDB, duplicate PKs caused mass "Batch execution
error: Found duplicated primary key value" warnings on gitnexus serve.
Replace the per-type seenFileIds with a single seenNodeIds Set checked
at the top of the iteration loop, before the switch, so every label is
covered by the same O(1) deduplication guard.
Fixes: #822
* fix(embeddings): use MERGE instead of CREATE for CodeEmbedding inserts
CREATE fails with duplicate PK when a CodeEmbedding node already exists,
which happens when:
- A PostToolUse hook triggers a concurrent gitnexus analyze during an
active analyze run (git commits fire the hook)
- A partial prior run left some embeddings in the DB before a crash
Switching to MERGE makes the insert idempotent: existing embeddings are
updated in place, new ones are created, no PK violations.
Fixes: #822
* fix(server): skip already-embedded nodes in POST /api/embed to avoid vector-index SET error
Kuzu/LadybugDB forbids SET on a property that is part of a vector index.
The /api/embed endpoint was calling runEmbeddingPipeline without skipNodeIds,
causing it to attempt MERGE+SET on every node including those already embedded.
Fix: query existing CodeEmbedding nodeIds before running the pipeline and pass
them as skipNodeIds so only new (unembedded) nodes are processed.
* fix(server): narrow catch to table-not-exist errors only in POST /api/embed
Bare catch{} would silently swallow connection errors and proceed to
re-embed all nodes, hiding infrastructure issues. Now only swallows
errors where the CodeEmbedding table does not yet exist.
* style: prettier format gitnexus/src/server/api.ts
* fix(server): log skip-embedding count and table-not-found swallow path
Addresses review feedback on PR #823:
- Log count of already-embedded nodes when skipNodeIds is populated
(aids debugging if Kuzu driver row shape changes).
- Log when the 'table does not exist' swallow path fires so ops can
catch it if Kuzu ever changes error wording.
- Document the {} config positional argument with an inline comment
referencing the runEmbeddingPipeline signature.
---------
Co-authored-by: jonasvanderhaegen-xve <>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* feat(ci): add release-candidate publish pipeline
Auto-publishes gitnexus@rc on every merge to main. Version scheme is
canonical semver X.Y.Z-rc.N where the base is the current npm 'latest'
bumped by the 'bump' input (default patch) and N auto-increments by
querying existing rc versions on the registry. First rc for a new base
is rc.1; the counter resets naturally when the base advances after a
stable release.
- Reuses ci.yml via workflow_call so tests must pass before publish
- SHA-pinned actions, per-job permission scoping, provenance enabled
- Guard job dedupes duplicate dispatches against HEAD via v*-rc.* tags
- Docs-only pushes skipped via paths-ignore
- workflow_dispatch inputs: bump (patch/minor/major), force (override guard)
- Publishes under the 'rc' dist-tag so 'latest' is never moved
- Tags commits as v<rc-version> and creates GitHub prereleases
* fix(ci): address release-candidate review feedback
- Sort rc tags by creatordate (handles out-of-order pushes correctly)
- Fail fast on npm registry errors; only fall back to package.json on E404
- Drop unused pull-requests: write permission on the reused CI job
- Add secrets: inherit so any future CI secrets are available to sub-jobs
- Remove unused reltag step output
* fix(ci): address Copilot review comments
- Correct concurrency comment (runs serialize on same ref, not overlap)
- Apply E404-only fallback to 'npm view versions' query, matching the
pattern used for the 'npm view version' query
- README: clarify that docs-only merges don't trigger rc publish
- CONTRIBUTING: drop 'from main' claim for publish.yml; the tag-push
trigger does not enforce branch reachability
* fix(ci): address adversarial review — idempotency, cycle continuity, tag integrity
Codex adversarial review flagged three release-safety issues in the rc
pipeline. Fixes:
1. Cycle continuity (H). Non-patch rc trains no longer collapse back to
patch on the next push. 'bump' input accepts a new 'auto' value
(default) that infers the active rc base from the registry: if any
X.Y.Z-rc.* exists with X.Y.Z > latest, continue that base; otherwise
patch-bump. Explicit patch/minor/major still forces a cycle reset and
now also bypasses the dedup guard so an explicit dispatch on a
tagged HEAD is honored.
2. Idempotency across post-publish failures (H). The guard marker
('rc/<HEAD_SHA>' lightweight tag) and the release tag ('v<RC>'
annotated) are now pushed atomically *before* 'npm publish'. A
publish failure leaves the marker in place and the guard refuses to
re-publish. Added a defensive 'npm view <pkg>@<rc> version' check
before publish to catch registry-level races. Recovery path
documented in CONTRIBUTING.md.
3. Tag ↔ package integrity (M). 'v<RC>' now points at a detached
release commit whose tree contains the rewritten package.json, so
the tag's source archive matches the npm tarball exactly. 'main'
stays pristine; the release commit is reachable only via the tag.
* fix(ci): surface registry errors on defensive version check; drop actions: read
- npm view <pkg>@<rc> version now distinguishes E404 (safe) from network
failures (abort) via the same mktemp+grep pattern used for the other
two npm view calls
- Dropped actions: read on the ci workflow_call — no sub-workflow uses
the Actions API
* fix: add setMaxListeners(50) to relationship pair WriteStreams
Dynamically-created per-pair WriteStreams for relationship CSV splitting
default to Node.js's maxListeners limit of 10. On large repositories with
many relationship types, readline backpressure causes repeated
ws.once('drain', ...) calls that exceed this limit, flooding stderr with
MaxListenersExceededWarning messages.
This matches the existing pattern in csv-generator.ts where
BufferedCSVWriter already calls this.ws.setMaxListeners(50).
* fix: address all 3 stream bugs in relationship CSV splitting
Addresses review feedback from @magyargergo and Claude CI analysis:
Bug 1 (High): Add error handlers to per-pair WriteStreams.
Previously, if a WriteStream errored (disk full, EMFILE) while rl was
paused waiting for drain, the drain callback never fired, rl.resume()
was never called, and the outer Promise hung forever — leaking all
open file descriptors until process kill.
Now each WriteStream gets an error handler that destroys all streams,
closes the readline interface + its input ReadStream, and rejects the
Promise.
Bug 2 (Medium): Add waitingForDrain Set to prevent drain listener
accumulation. rl.pause() is not synchronous — buffered line events
continue firing after pause(), and multiple lines targeting the same
pairKey each added another ws.once('drain', ...) listener. This was the
root cause of MaxListenersExceededWarning.
Now a Set<string> tracks which streams are already waiting for drain.
Only the first backpressure event registers the listener; subsequent
lines for the same stream are silently skipped (they're already written
to the stream buffer). This eliminates listener accumulation entirely
and makes setMaxListeners(50) a safety net rather than a band-aid.
Bug 3 (Low): Close readline and destroy input ReadStream in error
handler. Previously only the WriteStreams were destroyed on error,
leaving the ReadStream FD to linger until GC.
* fix: address review feedback — remove setMaxListeners, harden cleanup
- Remove setMaxListeners(50) entirely. The waitingForDrain guard
guarantees at most 1 drain listener per stream at any time. Tested
with 200 pairs x 500 lines (100k total) — max listeners was always 1,
zero warnings. No hard-coded limit needed.
- Wrap destroy() calls in cleanup() with try/catch so already-destroyed
streams don't throw synchronously (addresses @xkonjin review point 1).
- Add ws.once('error', reject) to the ws.end() phase so flush errors
during stream close properly reject instead of hanging Promise.all
(addresses Claude CI Bug 3b finding).
* test: add 8 regression tests for relationship CSV stream fixes
Covers all bugs fixed in this PR:
- Bug 1: WriteStream error rejects Promise and destroys all streams
- Bug 2: waitingForDrain guard keeps drain listeners at max 1 per stream
- Bug 3: cleanup() handles already-destroyed streams safely
Tests use a MockWriteStream with controllable backpressure and error
injection to verify the exact patterns in loadGraphToLbug() without
needing a real LadybugDB instance.
* style: run prettier on changed files
* fix(test): use backpressure to keep promise pending during error tests
The error tests were racing — readline finished reading the tiny CSV
and resolved the Promise before setTimeout fired the error. Now the
mock streams use blocked=true to trigger backpressure, keeping the
Promise pending so the error fires while the split is still in progress.
* fix: use named error handler in ws.end() to prevent listener leak
ws.once() wraps the callback, so removeListener with the original
function reference won't match. Switch to ws.on() with a named
onError function so removeListener correctly detaches it after
successful close.
* refactor: extract splitRelCsvByLabelPair, fix multi-stream drain
1. Extract splitRelCsvByLabelPair as an exported function with optional
wsFactory parameter for dependency injection. loadGraphToLbug now
delegates to it. Tests import and call the real function instead of
a local reimplementation.
2. Fix multi-stream drain coordination: rl.resume() is now guarded by
waitingForDrain.size === 0, so readline only resumes when ALL
backpressured streams have drained. Previously, any single stream
draining would resume readline while other streams were still full,
allowing unbounded buffer growth.
3. Export WriteStreamFactory type and RelCsvSplitResult interface for
test consumption.
* fix: resolve C/C++ cross-file calls through transitive #include chains
In C/C++, #include is transitive: if a.c includes b.h and b.h includes
c.h, then a.c can call any function declared in c.h. The wildcard import
synthesis only walked direct imports (1 hop), missing symbols reachable
through transitive header chains.
This is the dominant pattern in large C codebases — Redis's db.c includes
server.h which includes dict.h, so db.c should resolve calls to dictFind()
declared in dict.h and defined in dict.c. Before this fix, those cross-file
call edges were missing entirely.
The fix expands the import closure transitively for C/C++ files before
synthesizing wildcard bindings. A BFS walks ctx.importMap and graphImports
to collect all transitively reachable headers, then passes the full closure
to synthesizeForFile.
Tested on Redis (github.com/redis/redis):
- Before: dictFetchValue had 0 cross-file callers, processCommand had 0
- After: dictFetchValue has 9 callers, processCommand has 1, +1946 edges total
Fixes#813
* refactor(ingestion): dispatch wildcard synthesis by import-semantics strategy
Generalize PR #816's C/C++ transitive #include fix into a language-agnostic
strategy pattern. The `wildcard-synthesis.ts` pipeline phase no longer
references `SupportedLanguages.C` / `SupportedLanguages.CPlusPlus` — it
dispatches on `provider.importSemantics` via an exhaustive `switch`.
Also fixes a correctness bug the original BFS introduced: `queue.pop()`
(LIFO/DFS) reversed the iteration order of `#include` directives, which —
combined with first-seen-wins dedup in `synthesizeForFile` — silently
bound overloaded symbols to the wrong header. For the `cpp-calls`
fixture, `write_audit("hello")` was being resolved to `zero.h`'s arity-0
overload instead of `one.h`'s arity-1 overload, breaking arity
narrowing. Switched to FIFO (`queue.shift()`) with direct imports seeded
in declaration order.
Taxonomy (researched across 20+ languages + stack-graphs / SCIP prior art):
| Tag | Traversal | Languages |
|---------------------|-----------------|------------------------------------|
| named | none | TS, JS, Java, C#, Rust, PHP, Kotlin|
| wildcard-transitive | BFS closure | C, C++ |
| wildcard-leaf | single hop | Go, Ruby, Swift, Dart |
| namespace | none at import | Python |
| explicit-reexport | topological DAG | (scaffold; TS `export *` future) |
Changes:
- Widen `ImportSemantics` union from 3 to 5 tags with full taxonomy JSDoc
- Retag 5 providers: c-cpp (x2) → wildcard-transitive; dart, go, ruby,
swift → wildcard-leaf
- Move BFS closure into `wildcard-synthesis.ts` as `expandTransitiveIncludeClosure`
(pipeline-owned; providers stay pure declarations)
- Replace `if (lang === C || CPP)` with `dispatchSynthesis` helper called
by both Loop 1 (ctx.importMap) and Loop 2 (graphImports) so a future
transitive language whose edges arrive via graphImports gets closure
expansion consistently
- `never`-assertion default arm forces compile-time exhaustiveness
- `explicit-reexport` arm falls through to leaf behavior (scaffold;
TODO: implement re-export DAG walk for TS `export *` / Rust `pub use`)
- New unit tests covering circular includes, deep chains, diamond dedup,
graphImports-only paths, and order-preservation (the regression fix)
Verification:
- All existing C/C++ transitive tests pass unchanged
- Previously failing `cpp.test.ts > resolves run → write_audit to one.h
via arity narrowing` now passes
- `tsc --noEmit` clean
- 225/225 tests pass across wildcard-synthesis, cross-file-binding,
cpp resolver, and new closure unit tests
* fix(ingestion): bound closure size, O(1) dequeue, track Strategy 4 (#816 review)
Address @xkonjin's review feedback on the import-resolution strategy refactor:
1. **DoS guard**: cap transitive closures at 5,000 files via
`MAX_TRANSITIVE_CLOSURE_SIZE`. Pathological codebases (boost-style headers,
monoheader kernels) could previously produce closures with tens of thousands
of entries per translation unit. BFS now stops early and returns a partial
closure rather than risking OOM. The closest-headers-first BFS ordering
means the partial closure still contains the files overload resolution
cares about.
2. **Perf**: replace `Array.prototype.shift()` (O(n)) with a head-index queue
(O(1) dequeue). Deep chains previously had quadratic BFS behavior; now
linear in closure size.
3. **Strategy 4 tracking**: change TODO in `dispatchSynthesis` to
`TODO(#821)` referencing the filed issue for TS `export *` / Rust
`pub use` DAG-walk implementation, and clarify that today's leaf
fallthrough preserves correctness for direct imports — only the extra
re-export traversal is missing.
4. **Test**: new unit test exercising the 5,000-file cap on a 10k-file
synthetic chain, verifying partial-closure invariants (starts from
importer side, bounded, deep nodes excluded).
Not addressed in this commit (followups):
- Review point 3 (graphImports-only deep-chain *integration* fixture):
unit tests already exercise the `graphImports` traversal path directly
in isolation and combined with `importMap`. A fixture that stresses
graphImports-only transitive resolution is valuable but requires
understanding when the pipeline populates graphImports distinctly from
ctx.importMap — tracking as a followup rather than blocking this PR.
---------
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* fix(extractors): resolve 3 silent contract mis-resolution bugs (#793)
Addresses Codex adversarial review findings for extractor contract
resolution on the new group extractor surface.
F1 (manifest-extractor): resolveSymbol passed the full "METHOD::path"
contract string through normalizeRoutePath, producing "/GET::/api/orders"
which never matches Route.name. Adds parseHttpContract() helper that
strips the METHOD:: prefix before path normalization. Contract ID
construction (buildContractId) is unchanged.
F2 (http-route-extractor): graph-assisted backfill used path-only
detections.find(), so multi-verb same-URL files attached the wrong
verb/handler to provider rows and inferred the wrong verb on FETCHES
consumer edges. Now requires path+method match when method is known,
and skips backfill when method is unknown and multiple detections tie
on path.
F3 (grpc-extractor): resolveProtoConflict seeded bestScore=-1 and only
replaced on strict >, so all-zero-score ties silently selected
candidates[0]. Now computes all scores, counts ties at the top score,
and returns null on ambiguity (caller skips contract emission and
warns with service name + candidate paths).
All three fixes are test-first; 73 tests pass across the three suites.
No schema changes, no new dependencies, contract ID wire format
(http::METHOD::path, grpc::pkg.Service/Method, http::*::path) preserved.
* fix(extractors): address PR #817 review — ambiguous symbol pick + contract id casing
Copilot + Claude review on PR #817 flagged two follow-up bugs on top of
the F1/F2/F3 fixes:
1. http-route-extractor: ambiguous multi-verb case left handlerName null
but still ran the CONTAINS DB query. pickSymbolUid(syms, null) then
silently picked pool[0] — reintroducing handler mis-attribution via
a different route than the .find() bug F2 fixed. Now gates symbol
enrichment on an ambiguousCandidates flag so the file-basename
fallback wins instead.
2. manifest-extractor: buildContractId passed raw user casing through
for the explicit-method form, so get::/api/orders and
GET::/api/orders produced different contract ids even though
parseHttpContract upper-cases during lookup. Now reuses
parseHttpContract + normalizeRoutePath to canonicalize both method
and path, so logically equivalent manifest inputs share a contract
id (and share a manifestSymbolUid fallback).
Adds one regression test per bug: lowercase vs uppercase manifest
contract ids must match, and ambiguous multi-verb with CONTAINS rows
must not silently attach a real handler or call the CONTAINS query
at all. 75 tests pass across the three extractor suites.
* chore: prettier formatting
* Initial plan
* refactor: move language-specific container node logic into LanguageProvider
- Add resolveEnclosingOwner hook to LanguageProviderConfig
- Add staticOwnerTypes to MethodExtractionConfig
- Implement Ruby resolveEnclosingOwner (singleton_class → class/module)
- Replace hardcoded STATIC_OWNER_TYPES with config.staticOwnerTypes
- Move Ruby static types to rubyMethodConfig
- Move Kotlin static types to kotlinMethodConfig
- Remove Ruby singleton_class branch from findEnclosingClassInfo
- Collapse seqFindEnclosingClassNode/seqFindRawEnclosingContainerNode
into single provider-aware seqFindEnclosingOwnerNode
- Update worker path to pass provider.resolveEnclosingOwner
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/bc9f9d4d-f749-4872-9ff2-17fc86e08787
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* test: add regression tests for config-driven staticOwnerTypes and resolveEnclosingOwner hook
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/bc9f9d4d-f749-4872-9ff2-17fc86e08787
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* refactor: implement DAG-based pipeline architecture with phase extraction
Restructure the ingestion pipeline from a ~1800-line monolithic orchestrator
into a DAG (Directed Acyclic Graph) of named phases with explicit dependencies.
New files under pipeline-phases/:
- types.ts: PipelinePhase, PipelineContext, PhaseResult contracts
- runner.ts: DAG runner with topological sort validation
- scan.ts, structure.ts, markdown.ts, cobol.ts: early phases
- parse.ts + parse-impl.ts: chunked parse + resolve (the core)
- routes.ts, tools.ts, orm.ts: post-parse enrichment phases
- cross-file.ts + cross-file-impl.ts: cross-file binding propagation
- mro.ts, communities.ts, processes.ts: graph analysis phases
- index.ts: barrel export
pipeline.ts reduced from ~1960 lines to ~184 lines:
- DAG phase array declaration
- runPipelineFromRepo as thin orchestrator
- topologicalLevelSort retained for backward compat
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/136bf9c3-2f4f-449b-9fff-001332c8371c
* test: add DAG runner unit tests, update ARCHITECTURE.md with phase DAG docs
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/136bf9c3-2f4f-449b-9fff-001332c8371c
* fix: address code review - pass resolutionContext through parse output, fix worker URL path
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/136bf9c3-2f4f-449b-9fff-001332c8371c
* fix: declare transitive parse dependency explicitly in mro/communities/processes phases
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/136bf9c3-2f4f-449b-9fff-001332c8371c
* refactor: improve pipeline-phases clean code and folder structure
- Extract synthesizeWildcardImportBindings to wildcard-synthesis.ts
- Extract extractORMQueriesInline to orm-extraction.ts
- Create shared constants.ts for AST_CACHE_CAP
- Fix inline type import in orm.ts (use proper top-level import)
- Add comprehensive JSDoc to getPhaseOutput explaining type safety
- Move isDev to module level in cross-file.ts (consistency)
- Improve module-level documentation across files
- Organize barrel exports in index.ts with section comments
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/2bd6d4aa-6271-4009-8dd2-332ea8ec73ab
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* address review feedback: fix circular dep, allFetchCalls mutation, progress bugs, remove DAG naming, extract isDev, fix _item naming, fix O(n²) line calc
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/6cf53c9b-d55d-4c6f-bf3d-7bfb82d512b6
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* improve JSDoc on lineNumberAtOffset binary search
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/6cf53c9b-d55d-4c6f-bf3d-7bfb82d512b6
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* address review: filter deps in runner, move totalFiles to ctx, fix cycle JSDoc, centralize isDev, remove DAG naming
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b388424f-b939-4a94-97de-3855f9465564
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix doc consistency in graph-sort.ts module-level and function-level JSDoc
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b388424f-b939-4a94-97de-3855f9465564
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix(pipeline): wrap phase errors with phase name and emit terminal error progress event
Restores phase diagnostics at CLI/MCP boundary. runPipeline now wraps
phase.execute() in try/catch and rethrows with 'Phase <name> failed: ...'
preserving the original via { cause }. Also emits a terminal
{ phase: 'error' } progress event so subscribers see the failure before
the rejection propagates. Handler errors during error reporting are
swallowed to keep the original cause authoritative.
Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U1)
* fix(pipeline): move bindingAccumulator dispose into crossFile try/finally; make single-use
crossFile.execute() now wraps its body in try/finally so the accumulator
is released on both the happy path and when runCrossFileBindingPropagation
throws. Dev-mode telemetry stays inside the try block before dispose (all
three counters return 0 after dispose clears internal maps).
BindingAccumulator becomes single-use: appendFile after dispose now throws
'BindingAccumulator: use after dispose' instead of silently re-animating
via the old _disposed auto-clear. Docs updated; the only production
construction site (parse-impl) always creates a fresh instance per run,
so no caller relied on the re-use contract.
Residual risk documented in crossFile module JSDoc: a future phase
inserted between parse and crossFile that throws would still leak the
accumulator. Any such phase must manage accumulator lifetime explicitly.
Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U2)
* docs(pipeline): explain why importCtx teardown is safe before crossFile
Investigation (plan U3) confirms: `importCtx` (ImportResolutionContext)
is a scratch workspace with no downstream consumer after parse.
`resolutionContext` (returned to crossFile) is a distinct object that
owns importMap / namedImportMap / packageMap / moduleAliasMap / model,
and never closes over importCtx. cross-file-impl consumes only that
ctx via processCalls. The two confusingly-similar "context" names
were the root of the adversarial reviewer's concern — comment locks
in the invariant so the next reader sees it.
No behavioral change.
Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U3)
* refactor(pipeline): remove ctx.totalFiles side-channel; promote to ParseOutput
totalFiles was a hidden mutable field on PipelineContext written by
parse and read by mro/communities/processes — five reviewers flagged
this as a violation of the immutable-context invariant. Removed from
PipelineContext, which is now fully readonly, and made the implicit
temporal dep explicit: mro/communities/processes now declare 'parse'
as a dep and read totalFiles via getPhaseOutput<ParseOutput>(...).
No behavior change. Topo-sort unchanged because parse was already a
transitive dep through crossFile.
Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U4)
* feat(method-extractor): runtime staticOwnerTypes guard at factory chokepoint
createMethodExtractor now rejects MethodExtractionConfigs that list
companion_object / singleton_class / object_declaration in
typeDeclarationNodes but omit the matching entry from staticOwnerTypes.
Fails loudly at provider construction time instead of producing
silent isStatic=false on the 50000th file analyzed.
Opt-out convention preserved: an explicit `new Set()` (empty Set)
signals intentional exclusion and passes the guard (memory obs #30588).
All 13 existing language configs pass the guard; the new negative test
fails without it. Test-first.
Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U5)
* fix(pipeline): wrap sequential-fallback in try/finally so cleanup survives throws
The sequential-fallback block in runChunkedParseAndResolve now runs
inside a try/finally that guarantees astCache.clear(), accumulator
finalize, and enrichExportedTypeMap execute even if readFileContents
or processCalls throws mid-fallback. Cleanup failures are caught
inside the finally so they can't mask the original error.
Accumulator disposal ownership remains with crossFile (U2) — U6 only
adds astCache cleanup and preserves finalize ordering on the error
path.
Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U6)
* test(pipeline): direct unit coverage for wildcard-synthesis and cross-file-impl
Both modules previously had zero direct unit coverage — branches were
exercised only through integration tests' happy paths.
wildcard-synthesis.test.ts covers: Go graph-IMPORTS fallback, Python
moduleAliasMap build, MAX_SYNTHETIC_BINDINGS_PER_FILE cap, dedup
against existing namedImportMap entries, and empty-exportedSymbols
early return.
cross-file-impl.test.ts covers: gapRatio below threshold no-op,
MAX_CROSS_FILE_REPROCESS cap, graph-only exportedTypeMap fallback,
and empty namedImportMap short-circuit.
Tests assert current behavior — any future regression flips them.
Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U7)
* test(pipeline): golden-file graph-parity regression guard on mini-repo fixture
Pins the current post-P1/P2 graph output (57 symbols, 92 relationships,
4 processes, deterministic edge digest) so future silent refactors
cannot drift behavior unnoticed. If any count changes or any edge
rewires, the test fails with a readable diff listing what changed
and a copy-pasteable UPDATE_GOLDEN=1 regen command.
Edge digest keyed by symbolic (label, name, filePath) triples rather
than raw generateId output — stays meaningful across id-encoding
refactors while still catching real semantic rewiring.
Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U8)
* fix(pipeline): minimal cycle reporting + resolveEnclosingOwner loop safeguards
U9: runner cycle detection now reports only the SCC members via DFS
back-edge trace ('Cycle detected: A -> B -> C -> A') rather than
everything with inDegree > 0 (which mixed cycle members with blocked
dependents). Also emits the 'error' progress event for graph-
validation failures, symmetric with U1's runtime-error path.
U16: findEnclosingClassInfo now defends against language-provider
hooks that return non-container nodes — visitedContainers Set breaks
repeat-visit loops, MAX_ENCLOSING_WALK_ITERATIONS is belt-and-braces.
Documented the hook contract invariant so future provider authors
know the walk-continues-upward expectation.
Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U9, U16)
* refactor(pipeline): type hygiene, dead code cleanup, shared allPathSet, graph-sort naming
Bundles plan units U10, U11, U12, U14, U15:
U10 — Type hygiene: readonly ParseOutput arrays (allExtractedRoutes,
allDecoratorRoutes, allToolDefs, allORMQueries, allPaths); removed
redundant 'as string[] | undefined' cast in routes.ts and 'as URL' in
parse-impl.ts; WorkerPool is now 'import type'. Readonly contract
propagated into processORMQueries (only iterates).
U11 — Dead code & shims: deleted constants.ts shim (AST_CACHE_CAP
inlined into its sole real consumer cross-file-impl.ts; isDev
consumers now import directly from ../utils/env.js). Removed internal
utility re-exports from pipeline-phases/index.ts (no external
consumers). Removed topologicalLevelSort re-export from pipeline.ts;
updated topological-sort.test.ts to import from the canonical
utils/graph-sort.js. Stripped 'Phase 3+4:' stale JSDoc from
parse-impl.ts.
U12 — Perf: StructureOutput now carries allPathSet (ReadonlySet<string>)
built once; cobol, markdown, and cross-file-impl consume the shared
set instead of allocating their own. Parse forwards it via
ParseOutput.allPathSet; processCobol/processMarkdown widened to
ReadonlySet<string>.
U14 — graph-sort.ts: renamed local 'inDegree' to
'pendingImportsPerFile' with expanded JSDoc explaining the reverse-
graph Kahn's formulation and warning future maintainers not to
'correct' it to standard in-degree semantics. Added self-edge test.
U15 — Unconditional worker-fallback logging: removed isDev guard on
the worker-pool-creation-failure console.warn so operators can
diagnose perf degradations in production.
No behavior change. U8 golden-file test confirms pipeline output is
byte-identical.
Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U10, U11, U12, U14, U15)
* docs: fix ARCHITECTURE.md table integrity; bump AGENTS.md/CLAUDE.md to 1.3.0
U13 — documentation fixes:
ARCHITECTURE.md: the prior insertion of the 'Pipeline Phase DAG'
section orphaned 7 rows from the 'Where to change what' header.
Moved those 7 rows back up under their header so the table reads
contiguously; DAG section now follows the completed table.
AGENTS.md + CLAUDE.md: bumped version 1.2.0 -> 1.3.0, updated Last
reviewed to 2026-04-13, added matching Changelog row documenting
the GitNexus index stats refresh after the DAG refactor. Stat
bumps (symbols/relationships/execution flows) that were sitting
uncommitted in the working tree are now landed under a proper
changelog entry per each file's own documented schema.
Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U13)
* refactor(pipeline): drop spurious parse deps, true-readonly ParseOutput.exportedTypeMap, skip redundant wildcard synth
- mro/communities/processes: switch redundant `parse` dep to `structure` —
totalFiles originates in structure, so depending on parse for it was a
spurious data dep that obscured the real DAG.
- ParseOutput.exportedTypeMap: typed as truly ReadonlyMap<...,ReadonlyMap>>;
graph→exports enrichment moved into parse-impl so the snapshot is
fully populated at parse return. crossFile builds its own local mutable
working copy for per-file re-resolution writes — no cast at the boundary.
- parse-impl: hasSynthesized flag guards the unconditional final
synthesizeWildcardImportBindings call when per-chunk/fallback synthesis
already ran (graph-global + idempotent across chunks).
- cross-file-impl: documented the intentional `phase: 'parsing'` progress
label so telemetry bucketing stays consistent with the parse phase.
- cross-file-impl test: replaced the now-moved fallback-enrichment
assertion with a stronger one — crossFile must not mutate the
parse-supplied map.
Addresses PR #809 review pass 5 carry-overs.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* feat(group): extractor expansion + manifest extractor
Part 2 of 4 in the split of #606 (ticket: #792). Follows #795
(bridge.lbug storage foundation, already merged), but this PR has no
code-level dependency on #795 — it only imports types and the
ContractExtractor interface that existed on upstream main before
either PR. It could have been reviewed in parallel with #795.
## What changed
Expands the 3 existing contract extractors with substantially more
language/framework coverage, and adds a new `manifest-extractor`
that resolves `group.yaml`-declared cross-links against the per-repo
graph via exact-name lookups.
### New file (228 LOC)
- `gitnexus/src/core/group/extractors/manifest-extractor.ts` —
exact graph lookup for `group.yaml`-declared cross-links. HTTP
paths are canonicalized before Route.name matching; gRPC is
resolved by service/method name (NO `.proto`-filename fallback);
topic and lib use exact-name match. Falls back to a synthetic
`manifest::<repo>::<contractId>` uid when the graph has no
matching symbol, so cross-impact traversal still has a stable
anchor for the contract.
### Modified extractors (+958 LOC prod)
- `extractors/grpc-extractor.ts` (+522) — `.proto` parser with
comment and string-literal sanitization (braces inside strings no
longer truncate service bodies); package/service/method canonical
IDs; server/client detection across Go (`grpc.NewServer`,
`RegisterXxxServer`, `XxxGrpc.XxxImplBase`), Java (`@GrpcService`,
`BlockingStub`), Python (`servicer_to_server`, `XxxStub`), and
TypeScript/Node (`@GrpcMethod`, `ClientGrpc`, `loadPackageDefinition`).
- `extractors/http-route-extractor.ts` (+174) — Go gin/echo/stdlib
`HandleFunc`, NestJS `@Controller`+`@Get`/etc, Python FastAPI
decorators, Java Spring `@RequestMapping`/`@GetMapping`,
restTemplate / WebClient / OkHttp consumers.
- `extractors/topic-extractor.ts` (+98) — sarama `ProducerMessage{}`
struct literal detection (replaces a constructor-anchored regex
that missed topics inside producer loops), kafka-go Writer/Reader,
Python NATS (`await nc.subscribe`/`await nc.publish`), JetStream
helpers.
### Modified and new tests (+1264 LOC)
- `grpc-extractor.test.ts` (+539) — full coverage of the new proto
parser (strings-with-braces regression, comments-with-braces
regression), per-language server/client detection
- `http-route-extractor.test.ts` (+240) — per-framework route
extraction + normalization edge cases
- `topic-extractor.test.ts` (+177) — the sarama in-loop regression,
JetStream, Python NATS, kafka-go Writer/Reader
- `manifest-extractor.test.ts` (+308 NEW) — HTTP path normalization,
gRPC exact lookup with proto-fallback regression, lib and topic
exact matching, synthetic-uid fallback behavior
### Self-review fixes folded in
Carried forward from the #606 self-review (commit `d15b8cb`):
- **HIGH #1** — `manifest-extractor.resolveSymbol` was too fuzzy.
Previously used `CONTAINS` on route/name fields plus an
unconditional `filePath ENDS WITH '.proto'` fallback for gRPC.
Consequences: `/orders` matched `/suborders`, and any repo with
any `.proto` file returned a random proto symbol for a gRPC
manifest entry. Replaced with exact equality + deterministic
`ORDER BY` + synthetic-uid fallback for unresolved manifests.
Regression tests included.
- **MED #3** — gRPC proto parser brace-depth counting now sanitizes
strings and comments first (`stripProtoCommentsAndStrings`). A
valid proto with `option deprecated_reason = "use NewService {
instead"` used to have its service body closed early by the `"{"`
inside the literal, silently dropping methods after the offending
string. Regression tests for both string-with-brace and
comment-with-brace cases.
- **MED #4** — sarama Kafka regex changed from
`sarama.NewSyncProducer[\s\S]{0,300}?Topic:` (anchored on
constructor, caught only first topic in a loop) to
`sarama.ProducerMessage{...Topic:}` (matches every struct literal
directly). Regression test with a for-loop that constructs
multiple `ProducerMessage`s.
- **MED #7** — `manifest-extractor.resolveSymbol` no longer has a
silent `catch { /* fall through */ }`. Errors from the graph
executor are logged via `console.warn` with link type, contract
name, repo key, and error message before falling through to the
synthetic-uid path.
## Why
Reviewer focus here is pure regex / parser correctness — no
storage, no Cypher queries, no algorithmic changes to the cross-link
algorithm. Separating this from the bridge foundation PR (#795)
meant reviewers could stay in a single mental mode (parsing logic)
instead of context-switching between DDL, Cypher, and regex.
## How to verify
- `cd gitnexus && npx tsc --noEmit`
- `cd gitnexus && npx vitest run test/unit/group/grpc-extractor.test.ts --pool=forks`
- `cd gitnexus && npx vitest run test/unit/group/http-route-extractor.test.ts --pool=forks`
- `cd gitnexus && npx vitest run test/unit/group/topic-extractor.test.ts --pool=forks`
- `cd gitnexus && npx vitest run test/unit/group/manifest-extractor.test.ts --pool=forks`
Local pre-push: typecheck clean, all 99 extractor unit tests pass
(grpc 43, http 18, topic 30, manifest 8).
## Risk / rollback
**Low.** Extractors have no user-facing surface in this PR — they
produce `ExtractedContract[]` that is consumed by `sync.ts` in the
next split (#793). No existing behavior changes for users who don't
run a `group sync`. Rollback = `git revert` of the merge commit;
the modifications to `grpc-extractor.ts` / `http-route-extractor.ts`
/ `topic-extractor.ts` revert to the pre-PR versions that still
work (they're subsets of the new functionality).
## Scope discipline (per GUARDRAILS.md)
- Only the 8 files above are touched; no drive-by refactors
- No CI/release/security config changes
- No secrets or machine-specific paths
- Content lifted from #606 (CI 11/11 green on `d15b8cb`)
## Dependencies
- **Base:** `main` (upstream already includes #795 as `1ff324c`)
- **Blocks:** sync pipeline (#793) and the cross-impact feature (#794)
- **Tracker issue:** #792
- **Parent PR:** #606
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(group): migrate topic-extractor from regex to tree-sitter queries
Addresses @magyargergo's feedback on #796 that regex-based lookups
should use tree-sitter nodes instead, and that the top-level
extractors must NOT carry language dependencies. This is phase 1 of
a multi-step migration — topic-extractor first because its patterns
are the most uniform (16 "call/annotation with first-arg string
literal" variants), which makes it a clean proof of the approach
before grpc-extractor and http-route-extractor get the same treatment.
## Architecture: language-agnostic orchestrator + per-language plugins
The top-level extractor is a thin orchestrator that never imports a
tree-sitter grammar or a query string. Per-language knowledge lives
in a new `topic-patterns/` folder with one file per language plus a
registry that maps file extensions to compiled plugins:
```
src/core/group/extractors/
├── tree-sitter-scanner.ts # shared, language-agnostic scanning utilities
├── topic-extractor.ts # thin orchestrator (no grammar imports)
└── topic-patterns/
├── types.ts # TopicMeta, Broker
├── index.ts # registry: extension → compiled provider
├── java.ts # tree-sitter-java + JAVA_TOPIC_PROVIDER
├── go.ts # tree-sitter-go + GO_TOPIC_PROVIDER
├── python.ts # tree-sitter-python + PYTHON_TOPIC_PROVIDER
└── node.ts # tree-sitter-javascript + tree-sitter-typescript
# → JAVASCRIPT_/TYPESCRIPT_/TSX_TOPIC_PROVIDER
```
**Shared scanner (`tree-sitter-scanner.ts`)** — defines
`PatternSpec<TMeta>`, `LanguagePatterns<TMeta>`, `CompiledPatterns<TMeta>`
and the `scanFile(parser, plugin, content)` helper. Plugins compile their
queries eagerly at module load via `compilePatterns()`, so a broken
pattern fails loudly at import time instead of silently at scan time.
`unquoteLiteral()` handles single/double/template quotes, Python
triple-quoted strings, and Go raw backtick strings.
**Per-language plugins** own:
- the tree-sitter grammar import (this is the ONLY place in
`src/core/group/` where tree-sitter grammars are imported),
- the query S-expressions,
- the `TopicMeta` payload (role, broker, confidence, symbolName) that
the orchestrator receives back on every match.
Each plugin uses a `@value` capture name to bind the topic literal node.
The JavaScript and TypeScript grammars share AST node names for every
construct we query, so `node.ts` defines the pattern sources once and
compiles them against `JavaScript`, `TypeScript.typescript`, and
`TypeScript.tsx` — exporting three providers because `Parser.Query`
objects are NOT portable across grammar instances.
**Registry (`topic-patterns/index.ts`)** — maps `.java` → Java provider,
`.go` → Go, `.py` → Python, `.js`/`.jsx` → JS, `.ts` → TS, `.tsx` → TSX.
Also exports `TOPIC_SCAN_GLOB` so adding a new language is a single
file-level edit (drop `topic-patterns/<lang>.ts`, import + register it
here — zero edits required in `topic-extractor.ts`).
**Orchestrator (`topic-extractor.ts`)** — ~110 lines, no grammar or
query imports. Per file: `getProviderForFile(rel)` → `scanFile(parser,
provider, content)` → `unquoteLiteral(valueText)` → `makeContract(...)`.
Reuses one `Parser` instance across files; the scanner calls
`setLanguage` per plugin.
## Why this is better than regex
1. **Comments and strings are respected for free.** The old regex
would match `// kafkaTemplate.send("fake.topic")` as a real
producer; tree-sitter never visits comments or string literals as
code nodes, so false positives from commented-out code are
eliminated.
2. **Struct/object literal patterns are structural, not textual.**
`sarama.ProducerMessage{Topic: "..."}` no longer needs a 300-char
lookahead (which was a known cross-match bug partly mitigated by a
loop regression test in the self-review). The new query matches a
specific `composite_literal` with a specific `qualified_type` and
`keyed_element` — exactly one struct literal per match.
3. **No order-of-operations fragility.** Regex for
`channel.publish` vs `channel.consume` was independent and
file-wide; the AST scopes matches to the specific `call_expression`.
4. **Language-agnostic extension.** Adding Ruby, Rust, or C# topic
detection later means dropping one file in `topic-patterns/` — no
changes to shared scanner or orchestrator, and no tree-sitter
imports leak into top-level code.
## Per-file fault tolerance
- Malformed files that tree-sitter can't parse are silently skipped
(`parser.parse` is wrapped by `scanFile`). The ingestion pipeline
already logs unparseable files at index time.
- A syntactically invalid query is caught at `compilePatterns` time,
not scan time — broken plugins fail loudly at import.
- Per-pattern `matches()` failures are swallowed so one broken query
in a plugin doesn't block the rest.
## Tests
All 30 existing `topic-extractor.test.ts` tests pass **without any
changes to the test file** — they were written as input/output contract
tests (given this source file, expect these `ExtractedContract` objects)
and that contract is unchanged. Regression coverage includes:
- Kafka: Java `@KafkaListener` + `kafkaTemplate.send`; Node
`producer.send` + `consumer.subscribe`; Go sarama producer/consumer
(sync and async); kafka-go Writer/Reader; Python `KafkaConsumer` +
`producer.send/produce`
- RabbitMQ: Java `@RabbitListener` + `rabbitTemplate.convertAndSend`;
Node `channel.consume/publish/sendToQueue`; Python `basic_consume/
basic_publish` with keyword args
- NATS: Go and Node `nc.Subscribe/Publish`; Go and Node JetStream
`js.Subscribe/Publish`; Python `await nc.subscribe/publish`
Including the regression test for the sarama `ProducerMessage`
in-loop case — the AST-based query captures every literal in the
file independently, not just the first one after `NewSyncProducer`.
## Neighbor regression check
- `topic-extractor.test.ts` — 30/30 pass (rewritten extractor)
- `http-route-extractor.test.ts` — 18/18 pass (untouched)
- `grpc-extractor.test.ts` — 43/43 pass (untouched)
- `manifest-extractor.test.ts` — 8/8 pass (untouched)
- Full `npx tsc --noEmit` clean
## Scope discipline (per GUARDRAILS.md)
- Only files under `src/core/group/extractors/` are touched; no
changes to other extractors, tests, MCP surface, or pipeline.ts.
- No CI/release/security config changes, no secrets.
- New tree-sitter imports all reference grammars that are already
installed as dependencies (`tree-sitter`, `tree-sitter-javascript`,
`tree-sitter-typescript`, `tree-sitter-python`, `tree-sitter-java`,
`tree-sitter-go` — all in `package.json` for the existing pipeline).
## Phase 2 / phase 3 plan
- **Phase 2 (next commit):** rewrite `http-route-extractor.ts`
Strategy B (regex fallback) on the same plugin pattern. Graph-assisted
Strategy A stays as-is (already uses pipeline-built tree-sitter data
via `HANDLES_ROUTE` Cypher queries).
- **Phase 3 (commit after):** rewrite `grpc-extractor.ts` for Java /
Go / Python / TypeScript detection. `.proto` files are the one
outstanding question — there is no `tree-sitter-proto` grammar
installed; the in-tree string-sanitizing parser stays as a pragmatic
exception with a comment, alternative being to add
`tree-sitter-proto` as a dep (open for the maintainer).
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(group): migrate http-route-extractor Strategy B to tree-sitter plugins
Phase 2 of the extractor refactor requested by @magyargergo on #796.
Same architecture as the phase 1 topic-extractor rewrite: a thin,
language-agnostic orchestrator plus per-language plugins that own
tree-sitter grammars and query sources. The top-level extractor file
no longer imports any tree-sitter grammar or query string.
## Architecture
```
src/core/group/extractors/
├── tree-sitter-scanner.ts # shared, language-agnostic primitives
├── http-route-extractor.ts # thin orchestrator (no grammar imports)
└── http-patterns/
├── types.ts # HttpDetection, HttpLanguagePlugin, HttpRole
├── index.ts # registry: ext → plugin + HTTP_SCAN_GLOB
├── java.ts # tree-sitter-java: Spring + RestTemplate/WebClient/OkHttp
├── go.ts # tree-sitter-go: gin/echo/HandleFunc + http/resty consumers
├── python.ts # tree-sitter-python: FastAPI + requests
├── php.ts # tree-sitter-php: Laravel Route::get/...
└── node.ts # tree-sitter-javascript + tree-sitter-typescript:
# NestJS controllers, Express, fetch, axios
```
**Shared scanner (`tree-sitter-scanner.ts`)** — generalised from phase 1:
- `ScanMatch<TMeta>.captures` is now a full `CaptureMap` (every named
capture the query binds, not just a single `@value`). Topic extractor
updated to read `match.captures.value` accordingly.
- New `runCompiledPatterns(plugin, tree)` helper lets plugins run
multiple query bundles against the same pre-parsed tree. This is
needed for HTTP plugins that combine a class-prefix query with a
method-route query (Spring, NestJS).
- `scanFile` becomes a thin wrapper over `parser.parse + runCompiledPatterns`.
**HTTP plugin shape** — unlike topic plugins, HTTP plugins expose a
`scan(tree)` function rather than a flat pattern list. This reflects
HTTP's more complex extraction: each detection needs method + path +
handler name, and framework patterns like Spring `@RequestMapping` /
NestJS `@Controller` require cross-referencing a class-level prefix
with method-level annotations. Plugins internally use
`compilePatterns` + `runCompiledPatterns` and walk the AST to resolve
the class/method relationships.
**Per-framework coverage:**
- **Java (`java.ts`)**
- Spring: `@RequestMapping("/api/v2")` class prefix + `@(Get|Post|Put|
Delete|Patch)Mapping("/sub")` method routes, joined via the
enclosing `class_declaration` node id.
- `RestTemplate.getForObject/postForEntity/put/delete/patchForObject` →
method derived from API name.
- `WebClient.method(HttpMethod.X, "/path")` → method from
`HttpMethod.X` capture.
- `new Request.Builder().url("/path")` → OkHttp consumer.
- **Go (`go.ts`)**
- gin / echo / chi frameworks: `\w+.GET("/path", handler)` captures
upper-case verb + handler identifier.
- `net/http.HandleFunc("/path", handler)` → provider (default GET).
- `http.Get/Post/Head` consumer, `http.NewRequest("METHOD", ...)`,
resty `client.R().Get/Post/...`.
- **Python (`python.ts`)**
- `@app.get("/path")` FastAPI decorators.
- `requests.get/post/...` and `requests.request("METHOD", "url")`.
- **PHP (`php.ts`)**
- Laravel `Route::get/post/.../patch('/path', ...)` via
`scoped_call_expression`. Uses `PHP.php_only` to match the
existing ingestion pipeline's grammar selection.
- **Node (`node.ts`) — JS + TS + TSX**
- Pattern sources defined once, compiled against three grammar
variants (`JavaScript`, `TypeScript.typescript`, `TypeScript.tsx`)
because `Parser.Query` objects are not portable across grammars.
Exports three plugins sharing the same `scan` logic.
- NestJS: `@Controller('prefix')` decorators are siblings of the
class in `export_statement` / `program`; `@Get(':id')` decorators
are siblings of the method in `class_body`. The plugin walks
decorator → next named sibling to find the decorated class /
method, then combines the class prefix with the method path.
Only emits NestJS detections when the enclosing class has a real
`@Controller` decorator — prevents false positives from generic
classes that happen to use `@Get` from another library.
- Express: `(router|app).<verb>('/path', ...)`.
- `fetch(url)` (default GET) + `fetch(url, { method: 'X' })`
(uses two queries + a SyntaxNode-id dedupe set so URL literals
aren't double-emitted by the options variant).
- `axios.get/post/...`.
## Orchestrator changes
`http-route-extractor.ts` drops every `scanXxxProviders` / `scanXxxConsumers`
regex method and replaces them with a single source-scan loop that
delegates to `getPluginForFile(rel).scan(tree)`. The orchestrator
still owns:
- **Path normalization** (`normalizeHttpPath`, `normalizeConsumerPath`)
— language-agnostic string processing shared by both strategies.
- **Graph-assisted Strategy A** (`HANDLES_ROUTE` / `FETCHES` / `CONTAINS`
Cypher queries) — unchanged in spirit. The only regex helpers it
used (`inferMethodFromFileScan`, `pickJavaHandlerName`) are now
replaced by a lookup against the plugin's detections for the same
file: for each route row, find the detection whose normalized path
matches, and pull the HTTP method + handler name from it.
- **Per-file parse cache** — the orchestrator parses each relevant
file at most once per `extract()` call. Both the graph-assisted
enrichment loop and the source-scan fallback share the same
`cachedDetections` map, so we never run the plugin twice for the
same file.
## Why this is better than the regex version
1. **Comments and strings for free.** The old regex would match
`// router.get('/fake')` as a real Express route; tree-sitter
never visits string/comment nodes.
2. **Structural controller-prefix.** Spring and NestJS class-prefix
joining is now scoped to the enclosing class via `class_declaration`
node ids, eliminating file-wide state that broke when a file had
multiple controllers.
3. **Precise NestJS disambiguation.** The plugin only emits a NestJS
detection when the enclosing class has a real `@Controller`
decorator — the old regex would fire on any `@Get(...)` in the
file regardless of surrounding context.
4. **Language-agnostic extension.** Adding Ruby / Rust / Kotlin HTTP
detection later means dropping one file in `http-patterns/` — no
changes to the shared scanner, the orchestrator, or the Strategy A
Cypher queries.
## Tests
- `http-route-extractor.test.ts` — **18/18 pass** (tests unchanged;
they're contract-style input/output tests and the contract shape is
unchanged). Covers Spring class prefix, Express, gin/echo, stdlib
HandleFunc, NestJS, Laravel, FastAPI for providers and
fetch/axios/python-requests/rest-template/webClient/okhttp/go-stdlib/
resty for consumers, plus graph-first Strategy A for both.
- `topic-extractor.test.ts` — **30/30 pass** after the `captures.value`
API migration.
- `grpc-extractor.test.ts` — 43/43 pass (untouched; phase 3).
- `manifest-extractor.test.ts` — 8/8 pass (untouched).
- `service.test.ts`, `sync.test.ts`, `storage.test.ts` — 41/41 pass.
- `npx tsc -p tsconfig.json --noEmit` clean.
## Scope discipline (per GUARDRAILS.md)
- Only files under `src/core/group/extractors/` are touched.
- No changes to pipeline.ts, MCP surface, ingestion, or tests.
- No CI / release / security / secrets changes.
- Tree-sitter grammars imported by plugins (`tree-sitter-java`,
`tree-sitter-go`, `tree-sitter-python`, `tree-sitter-php`,
`tree-sitter-javascript`, `tree-sitter-typescript`) are all already
in `package.json` for the existing ingestion pipeline.
## Phase 3 plan
- **grpc-extractor** gets the same treatment: plugin-per-language under
`grpc-patterns/` for Java / Go / Python / TS detection. `.proto`
files remain an open question — no `tree-sitter-proto` grammar is
installed, so the in-tree string-sanitizing parser from PR #796's
self-review stays as a pragmatic exception unless the maintainer
wants us to add `tree-sitter-proto` as a new dep.
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(group): migrate grpc-extractor source scans to tree-sitter plugins
Phase 3 (final) of the extractor refactor requested by @magyargergo on
#796. Same architecture as phase 1 (topic) and phase 2 (http): thin
language-agnostic orchestrator + per-language plugins that own
tree-sitter grammars and query sources. With this commit the top-level
extractors under `src/core/group/extractors/` import ZERO tree-sitter
grammars and ZERO query strings — every grammar import lives in a
`*-patterns/<lang>.ts` plugin file, and the orchestrators go through
the registry indirection.
## Architecture
```
src/core/group/extractors/
├── tree-sitter-scanner.ts # shared primitives (unchanged)
├── grpc-extractor.ts # orchestrator (only `.proto` parser left)
└── grpc-patterns/
├── types.ts # GrpcDetection, GrpcLanguagePlugin, GrpcRole
├── index.ts # registry: ext → plugin + GRPC_SCAN_GLOB
├── go.ts # tree-sitter-go: RegisterXxxServer, Unimplemented, NewXxxClient
├── java.ts # tree-sitter-java: @GrpcService + XxxImplBase + newBlockingStub
├── python.ts # tree-sitter-python: add_XxxServicer_to_server + XxxStub
└── node.ts # tree-sitter-javascript + tree-sitter-typescript:
# @GrpcMethod, @GrpcClient field type,
# .getService<X>('Svc'), new XxxServiceClient,
# loadPackageDefinition dynamic constructors
```
## Per-language coverage
**Go (`go.ts`)**
- Provider: `\w+.RegisterXxxServer(...)` via `call_expression →
selector_expression → field_identifier` + JS regex filter
`^Register(\w+)Server$`.
- Provider: `pb.UnimplementedXxxServer` embedded in a struct via
`struct_type → field_declaration_list → field_declaration →
qualified_type → type_identifier` + JS filter.
- Consumer: `\w+.NewXxxClient(...)` via the same call_expression
query + JS filter `^New(\w+)Client$`.
**Java (`java.ts`)**
- Provider: `class X extends YyyGrpc.YyyImplBase` — two queries
handle the scoped and plain forms. `scoped_type_identifier`'s
children are positional (no `scope:`/`name:` fields), so the
query matches the two `type_identifier` children by position.
- `#match? @inner "ImplBase$"` restricts matches at query time.
- Whether the class has `@GrpcService` or not controls only the
`source` metadata label — the plugin walks the class_declaration's
`modifiers` child in JS to detect the marker_annotation.
- Consumer: `YyyGrpc.newStub(ch)` / `newBlockingStub(ch)` via a
`method_invocation` query with `#match? @method
"^new(Blocking)?Stub$"`, service name extracted via
`^(\w+)Grpc$` on the object identifier.
**Python (`python.ts`)**
- Single call-expression query covers both bare identifier and
`obj.method` attribute forms:
`(call function: [(identifier) @fn (attribute attribute: (identifier) @fn)])`.
- Plugin filters `@fn.text` against two JS regexes:
`^add_(\w+)Servicer_to_server$` (provider) and `^(\w+)Stub$`
(consumer), with a reserved-names ignore list for the Stub case
(Mock / Test / Fake / Stub).
**Node — JavaScript + TypeScript + TSX (`node.ts`)**
- Pattern sources defined once, compiled three times (one per grammar)
because `Parser.Query` objects are not portable across grammars.
Exports three `GrpcLanguagePlugin`s sharing the same `scan`.
- `@GrpcMethod('Service', 'Method')`: decorator query captures the
two string literals. Confidence is hard-coded 0.8 regardless of
proto map resolution (matches the original regex version's
behaviour).
- `@GrpcClient(...) field: XxxServiceClient`: decorator query
captures the decorator node, plugin walks up to find the enclosing
`public_field_definition` (decorators on fields are CHILDREN of
the field definition in tree-sitter-typescript, not siblings) and
reads its first `type_annotation → type_identifier`, then runs the
`^(\w+Service)Client$` JS filter.
- `client.getService<X>('AuthService')`: call-expression query on
`member_expression.property = "getService"` + string literal arg.
- `new XxxServiceClient(...)`: `new_expression` with a bare
identifier constructor, filtered by `^(\w+Service)Client$` so
generic `new AuthClient(...)` (missing the `Service` infix) does
NOT falsely register as a consumer. Preserves the regression test
`test_extract_ts_non_service_client_constructor_is_ignored`.
- `loadPackageDefinition` dynamic loader: gated on
`tree.rootNode.text.includes('loadPackageDefinition')`. When set,
`new foo.bar.Xxx(...)` qualified constructors with a capitalised
property name register as consumers.
## Orchestrator changes
`grpc-extractor.ts` loses every `scanGoProviders` / `scanJavaProviders`
/ ... helper and replaces them with a single source-scan loop that:
1. Parses each file with the plugin's grammar (one shared `Parser`
instance across all files, `setLanguage` called per plugin).
2. Calls `plugin.scan(tree)` to get `GrpcDetection[]`.
3. Converts each detection to an `ExtractedContract` via the private
`detectionToContract` helper, which:
- Looks the short service name up in the proto map (filled by
the `.proto` parser).
- Picks confidence = `confidenceWithProto` if resolved, else
`confidenceWithoutProto`.
- Builds a method-level contract id (`grpc::pkg.Svc/Method`) when
the detection carries a `methodName` (TS `@GrpcMethod` only),
otherwise a service-level id (`grpc::pkg.Svc/*`).
Everything else — the `.proto` parser, `buildProtoContext`,
`buildProtoMap`, `resolveProtoConflict`, `serviceContractId`,
`stripProtoCommentsAndStrings`, `extractServiceBlocks`, the dedupe
function — stays exactly as before. The `.proto` parser is kept as a
pragmatic exception to the "no regex in extractors" rule because no
`tree-sitter-proto` grammar is installed in the repo; a comment at the
top of the file explains this and flags the maintainer option of
adding `tree-sitter-proto` as a dependency.
## Why this is better than the regex version
1. **Comments and strings are respected for free.** Matched node types
are only code constructs, never text inside comments or string
literals.
2. **No false positives on partial names.** The old `(\w+?)Grpc`-style
regexes would cross-match unrelated identifiers; structural queries
restrict matches to the exact AST shape (`scoped_type_identifier →
type_identifier` pairs, `method_invocation → identifier` etc.).
3. **NestJS `@GrpcClient` is structural, not regex-based.** The old
regex required a specific textual layout
(`@GrpcClient(...) private readonly foo!: XxxServiceClient`); the
plugin now walks the AST, so modifier order / optional modifiers /
multi-line formatting don't break it.
4. **Language-agnostic extension.** Adding Kotlin / Rust / C# gRPC
detection later is a one-file edit in `grpc-patterns/index.ts` —
no touches to the shared scanner, the orchestrator, or the proto
parser.
## Tests
- `grpc-extractor.test.ts` — **43/43 pass** (tests unchanged; the
contract shape is identical). Covers .proto parsing (including the
brace-inside-string regression), Go provider/consumer,
Java @GrpcService / plain ImplBase provider + newBlockingStub
consumer, Python servicer + stub, TS @GrpcMethod + @GrpcClient +
.getService + new XxxServiceClient + loadPackageDefinition + the
`AuthClient` vs `AuthServiceClient` discrimination, dedupe across
multiple patterns in one file, proto-aware confidence, and the
inherited-package resolution for split proto definitions.
- `topic-extractor.test.ts` — 30/30 pass.
- `http-route-extractor.test.ts` — 18/18 pass.
- `manifest-extractor.test.ts` — 8/8 pass.
- `service.test.ts`, `sync.test.ts`, `storage.test.ts` — 41/41 pass.
- `npx tsc -p tsconfig.json --noEmit` clean.
## Scope discipline (per GUARDRAILS.md)
- Only files under `src/core/group/extractors/` are touched.
- No pipeline.ts, MCP surface, ingestion, CI / release / security, or
test changes.
- New tree-sitter grammar imports (`tree-sitter-go`, `tree-sitter-java`,
`tree-sitter-python`, `tree-sitter-javascript`, `tree-sitter-typescript`)
are all already installed for the ingestion pipeline.
## End of phase series
This commit completes the three-phase extractor refactor:
- **Phase 1** (`ea06d11`): topic-extractor → `topic-patterns/`
- **Phase 2** (`b6015f6`): http-route-extractor → `http-patterns/`
- **Phase 3** (this commit): grpc-extractor → `grpc-patterns/`
Every remaining regex-based extractor helper under the `src/core/group/
extractors/` directory is either (a) language-agnostic string
processing (path normalization, dedupe keys) or (b) the `.proto`
parser, which is documented as an explicit exception.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(group): add tree-sitter-proto for .proto file parsing
Addresses @magyargergo's suggestion on #796 to replace the manual
string-sanitizing .proto parser with a tree-sitter grammar.
- **Vendored `tree-sitter-proto`** in `vendor/tree-sitter-proto/`.
Grammar source from [coder3101/tree-sitter-proto](https://github.com/coder3101/tree-sitter-proto)
(latest `grammar.js`), parser.c regenerated with `tree-sitter-cli
0.24` to produce ABI version 14 — compatible with the project's
`tree-sitter 0.25` runtime (which supports ABI ≤ 14). Added as
`optionalDependency` with `file:./vendor/tree-sitter-proto`.
- **New `grpc-patterns/proto.ts` plugin** — uses the same
`compilePatterns` + `runCompiledPatterns` infrastructure as every
other plugin. Two queries:
- `(package (full_ident) @pkg)` — package declaration
- `(service (service_name) @service_name (rpc (rpc_name) @rpc_name))`
— one match per (service, rpc) pair
- **Graceful fallback** — `tree-sitter-proto` is an optional
dependency. If it fails to install (platform incompatibility) or
fails the runtime smoke-test (`setLanguage` + `parse` on a trivial
proto), `PROTO_GRPC_PLUGIN` stays `null` and the orchestrator
uses the existing manual parser. The smoke-test catches the
`SyntaxNode` TDZ error that occurs in vitest's fork-based test
runner.
- **Orchestrator updated** — when `hasProtoPlugin` is true, `.proto`
files are handled by the plugin loop (they're included in
`GRPC_SCAN_GLOB`), and the manual `parseProtoFile` loop is
skipped. `buildProtoContext` still runs to build the proto map
for cross-referencing source-file detections.
1. **No manual comment/string stripping.** The old parser needed
`stripProtoCommentsAndStrings` (110 lines) to avoid counting
braces inside comments and string literals. tree-sitter handles
this natively.
2. **No brace-depth tracking.** `extractServiceBlocks` used a manual
depth counter to find service boundaries. tree-sitter's AST gives
us `service` → `service_name` + `rpc` → `rpc_name` directly.
3. **Performance.** tree-sitter's C-based parser is faster than
character-by-character JS scanning + regex on large proto files.
- `grpc-extractor.test.ts` — **43/43 pass** (unchanged)
- All other extractor tests — 99/99 pass
- `npx tsc -p tsconfig.json --noEmit` clean
Co-authored-by: Claude <noreply@anthropic.com>
* chore: add .gitignore for vendored tree-sitter-proto build artifacts
https://claude.ai/code/session_01SFUCxgKMMQ8EgRHYw91xPU
* fix: correct .gitignore paths for vendored tree-sitter-proto
Patterns should be relative to the .gitignore file's directory.
https://claude.ai/code/session_01SFUCxgKMMQ8EgRHYw91xPU
* refactor(group): address Copilot review feedback on #796
Six fixes suggested by the Copilot AI review:
1. **`normalizeHttpPath` root-path edge case** — stripping trailing
slashes on the input `/` produced an empty string, yielding
malformed contract ids like `http::GET::`. Now preserves `/` for
the root handler/fetch case.
2. **Dedupe `scanFiles` call** — `extract()` was globbing the
source-scan file list twice (once for the provider fallback, once
for the consumer fallback). Moved to a single lazy call that
memoizes the result for the rest of the method.
3. **HTTP `scanFiles` now ignores `**/vendor/**`** — every other
extractor's glob already ignored vendored sources; the HTTP one
didn't. Fixed for consistency.
4. **`loadPackageDefinition` check is now structural** — was calling
`tree.rootNode.text.includes('loadPackageDefinition')` which forces
materialization of the entire file text from the parse tree
(expensive on large files). Replaced with a dedicated compiled
query on `(call_expression function: [(identifier) | (member_expression)])`
so the check stays in the AST domain.
5. **`grpc-extractor.ts` header docstring updated** — still claimed
".proto parsing is not tree-sitter-based because no grammar is
installed". Now describes the actual behaviour: tree-sitter when
`tree-sitter-proto` is available (optionalDependency), manual
fallback otherwise.
6. **Eliminated the double proto file parse on the fallback path** —
`buildProtoContext` already globs + parses every `.proto` file to
build `servicesByName`. On the `!hasProtoPlugin` branch the
extractor was globbing + parsing again via the now-removed
`parseProtoFile` helper. The fallback branch now iterates the map
that `buildProtoContext` already produced to emit provider
contracts directly — single pass per proto file.
## Tests
- `topic-extractor.test.ts` — 30/30 pass
- `http-route-extractor.test.ts` — 18/18 pass
- `grpc-extractor.test.ts` — 43/43 pass
- `manifest-extractor.test.ts` — 8/8 pass
- `npx tsc -p tsconfig.json --noEmit` clean
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(group): address Claude review feedback (bugs + dedup + hygiene) on #796
Follows up `2f28bfc` with the remaining items from the Claude AI review:
## Bugs
**Bug 2 — Label-unaware Cypher queries in `resolveSymbol`.**
The manifest-extractor's lookup queries were `MATCH (n) WHERE n.name = $x`
with no label filter, so a topic/service/package name could silently match
any node type (File, Variable, Import, Folder, …). Added label filters:
- `topic` → `(n:Function|Method|Class|Interface)` (topics are best-effort
symbol-name matches against listener/publisher symbols)
- `grpc` method → `(n:Function|Method)`
- `grpc` service → `(n:Class|Interface)`
- `lib` → `(n:Package|Module)`
All 8 manifest-extractor tests still pass (mock executor is
label-agnostic, but the production LadybugDB graph now gets correctly
scoped queries).
**Bug 8 — Tautological `!handlerName` condition.**
`http-route-extractor.ts:extractProvidersGraph` had
`let handlerName = null; if (!method || !handlerName) { ... }` — the
`!handlerName` clause was always true since there was no intervening
assignment. Simplified to always run the plugin-scan lookup (we need
the handler name even when `methodFromRouteReason` already resolved
the method).
## Clean code / dedup
**Design 7 — `readSafe` was copy-pasted in all three orchestrators.**
Extracted to `extractors/fs-utils.ts` as the single source of truth
for the path-traversal guard. Dropped the three local copies and the
now-unused `fs`/`path` imports from topic-extractor.
**Style 10 — Language-specific `_test.go` skip in the topic orchestrator.**
Was `if (rel.endsWith('_test.go')) continue;` inside the language-
agnostic extraction loop. Pushed into the glob's ignore list
(`'**/*_test.go'`) alongside the existing `node_modules`, `vendor`,
`dist`, `build` entries, with a comment explaining that other
languages' test file conventions either live in separate directories
(Python `tests/`, Java `src/test/`) or are already covered by the
existing ignores.
## Already addressed in `2f28bfc` (mentioned again in Claude review)
- Bug 3: `normalizeHttpPath('/')` returns `''` — fixed
- Bug 4: double glob + double parse of `.proto` — fixed
- Bug 5: `scanFiles` called twice in HTTP — fixed
- Bug 6: missing `**/vendor/**` in HTTP glob — fixed
- Design 9 partially: `tree.rootNode.text.includes('loadPackageDefinition')`
replaced with a dedicated structural query
## Deferred
- Bug 1 (`http::*::path` vs `http::GET::path` matching) — out of scope;
sync.ts matching logic lands in #793, manifest extractor already
emits correct synthetic uids for unresolved HTTP contracts.
- Design 9 full (change plugin `scan(tree)` → `scan(tree, source)`) —
the only real use case (`loadPackageDefinition` gate) is already
fixed via a structural query, so the interface change would be
cosmetic churn without a concrete consumer.
## Tests
- `topic-extractor.test.ts` — 30/30 pass
- `http-route-extractor.test.ts` — 18/18 pass
- `grpc-extractor.test.ts` — 43/43 pass
- `manifest-extractor.test.ts` — 8/8 pass
- `npx tsc -p tsconfig.json --noEmit` clean
Co-authored-by: Claude <noreply@anthropic.com>
* docs+fix(group): address remaining Claude review items + add pipeline flow chart
## Fixes
**Remaining 🔴 — HTTP contract id wildcard format.** Documented the
`http::*::<path>` format as an intentional wildcard for manifest links
that omit the HTTP method, alongside the explicit-method form
(`GET::/path` → `http::GET::/path`). The docblock on `buildContractId`
now states both forms, notes that wildcard-aware matching is the
responsibility of the sync / cross-impact layer (#793), and
recommends the explicit-method form whenever the author knows the
method (it round-trips through exact equality without needing
wildcard logic downstream). Tests unchanged — the wildcard format is
what they've always asserted.
**Minor 1 — stale comment at `manifest-extractor.ts:124-126`.** The
comment claimed "creates a contract with an empty symbolUid/ref" but
the code switched to `manifestSymbolUid(repo, contractId)` a few
commits back. Updated to describe the actual synthetic-uid fallback
semantics and the cross-impact path that relies on both sides of the
join deriving the same uid.
**Minor 2 — exhaustiveness guard on `buildContractId`.** The
`switch(type)` covered all five current `ContractType` variants but
silently returned `undefined` if a new variant was added. Added a
`default: const _exhaustive: never = type; throw new Error(...)`
clause so the build fails loudly on an unhandled variant.
**Minor 3 — `tree.rootNode.text` in `grpc-patterns/node.ts`.** Already
fixed in `2f28bfc` via a dedicated structural query
(`LOAD_PACKAGE_DEFINITION_SPEC`). No action needed.
## New: pipeline flow chart (per @magyargergo's request)
Added `src/core/group/PIPELINE.md` with four Mermaid diagrams:
1. **High-level overview** — `group.yaml` → extractors + manifest →
contract matching → `bridge.lbug` → `runGroupImpact`.
2. **Per-repo extractor two-strategy shape** — graph-assisted
Strategy A vs. source-scan Strategy B.
3. **Plugin architecture** — orchestrator → registry →
per-language `*-patterns/<lang>.ts` → `tree-sitter-scanner.ts` →
`ExtractedContract`.
4. **Manifest extraction** — label-scoped `resolveSymbol` with the
synthetic-uid fallback.
5. **Cross-impact query (#606)** — local impact → bridge join →
cross-repo fan-out.
Each diagram is annotated with which PRs own which stage (this PR:
extractors + manifest; #795: bridge storage; #606: cross-impact
runtime) and points at the concrete files/functions involved.
## Tests
- 99/99 extractor tests pass
- `npx tsc -p tsconfig.json --noEmit` clean
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Initial plan
* feat(SM-20): extract registries into model/ module with SemanticModel interface
- Create model/type-registry.ts — TypeRegistry interface + factory
- Create model/method-registry.ts — MethodRegistry interface + factory
- Create model/field-registry.ts — FieldRegistry interface + factory
- Create model/semantic-model.ts — SemanticModel interface + factory
- Create model/heritage-map.ts — re-export HeritageMap types
- Create model/binding-accumulator.ts — re-export BindingAccumulator types
- Create model/resolve.ts — move lookupMethodByOwnerWithMRO from call-processor
- Update symbol-table.ts — delegate to SemanticModel for registry ops
- Update call-processor.ts — re-export lookupMethodByOwnerWithMRO from model/resolve
No circular dependencies: model/resolve.ts does NOT import resolution-context.ts.
All 775 related unit tests pass with no regressions.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/27ad2975-1a31-4f50-815b-178ee8a95277
* fix: clarify re-export comment per code review feedback
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/27ad2975-1a31-4f50-815b-178ee8a95277
* refactor(SM-20): wire up SemanticModel as first-class resolution input
PR #786 extracted TypeRegistry/MethodRegistry/FieldRegistry into model/
behind SemanticModel, but consumers still routed through SymbolTable
delegates. This change completes Phase 6 of the fuzzy-lookup elimination
roadmap by making call-processor, resolution-context, type-env, and
heritage-map query the model directly via `table.model.{types,methods,fields}`.
Also absorbs the open PR #786 review findings so the branch lands clean:
- Removed duplicate JSDoc block on lookupMethodByOwner (symbol-table.ts)
- Added model/index.ts barrel for the public model/ surface
- Fixed O(n) buildParentMapFromHeritage BFS via head-pointer queue
- Clarified re-export facade framing on binding-accumulator.ts and
heritage-map.ts inside model/
- Refined @internal JSDoc on lookupMethodByOwnerWithMRO
Changes:
- symbol-table.ts: expose `readonly model: SemanticModel` on the
SymbolTable interface. SymbolTable delegate wrappers (lookupClassByName
etc.) stay as thin pass-throughs for backward compat; deletion is a
follow-up once all internal callers are migrated.
- model/resolve.ts: lookupMethodByOwnerWithMRO now takes SemanticModel
instead of SymbolTable, removing the last SymbolTable import from the
model/ module. Preserves circular-dependency firewall.
- call-processor.ts: 6 call sites in D0 member resolution, field
resolution, ctor override, and ctor disambiguation migrated to
model.types/methods/fields.
- resolution-context.ts: tier 3 class+impl lookup migrated.
- type-env.ts: 5 sites across lookupClassDefsByName, resolveFieldType,
and resolveMethodReturnType migrated.
- heritage-map.ts: parent/child class-name resolution migrated.
Tests:
- symbol-table.test.ts: +10 parity and feeding-audit tests covering
every model.{types,methods,fields} path (Class, Method, Property,
Impl, Function-with-ownerId, Property-without-ownerId skip, arity
filtering, clear cascade).
- call-processor.test.ts: classLookupSpy now targets
ctx.symbols.model.types since the wrapper is bypassed.
- type-env.test.ts: createMockSymbolTable and the destructured-call
makeSymbolTable helpers gained a model shim that forwards to the
(possibly overridden) top-level lookup stubs.
Validation: full suite 5603 passed / 159 skipped, resolver integration
suite (19 files, 1766 tests) clean, tsc --noEmit clean.
* refactor(SM-21): invert ownership — SemanticModel contains SymbolTable
Follow-up to SM-20. Previously SymbolTable owned a `model` subfield;
this commit turns the ownership direction around so the SemanticModel
is the top-level container and SymbolTable is nested as `.symbols`:
SemanticModel (top-level, passed everywhere)
├── types (TypeRegistry)
├── methods (MethodRegistry)
├── fields (FieldRegistry)
└── symbols (SymbolTable — file-indexed + callable-name index)
The owner-scoped registries live directly on the model; file and
callable-name lookups go through `.symbols`. Consumers receive a
`SemanticModel` and reach into the appropriate field — no more
`table.model.types.X` double-hop.
Core changes:
- symbol-table.ts: createSymbolTable now takes injected
TypeRegistry/MethodRegistry/FieldRegistry via a SymbolTableDeps
argument. When omitted (test fallback), it creates standalone
registries locally and clears them in clear() — production callers
always inject. The five registry convenience delegates
(lookupClassByName, lookupMethodByOwner, lookupFieldByOwner,
lookupClassByQualifiedName, lookupImplByName) remain as thin
forwards to the injected registries so standalone SymbolTable use
(chiefly tests) stays ergonomic.
- model/semantic-model.ts: createSemanticModel() now creates the
three registries AND a SymbolTable wired to them, exposing the
SymbolTable as `.symbols`. clear() cascades through all four.
- resolution-context.ts: `readonly symbols: SymbolTable` field is
replaced with `readonly model: SemanticModel`. Internal factory
builds a SemanticModel and keeps a local `symbols` alias for
backward-compatible inner body.
Consumer migrations (src/):
- call-processor.ts: ctx.symbols.add/.lookupExactAll/
.lookupCallableByName → ctx.model.symbols.*; ctx.symbols.model.X →
ctx.model.X. buildTypeEnv option key renamed symbolTable → model.
- type-env.ts: symbolTable parameter renamed model (type
SemanticModel), all internal call sites rewritten to use
model.types.*, model.methods.*, model.fields.*,
model.symbols.lookupExactAll / .lookupCallableByName.
- heritage-map.ts: 2 class-lookup sites migrated.
- pipeline.ts: ctx.symbols → ctx.model.symbols throughout.
Test migrations:
- symbol-table.test.ts: parity tests (which validated the old
table.model.X hop) replaced with direct SemanticModel coverage via
createSemanticModel(). New tests exercise types/methods/fields/
symbols feeding end-to-end.
- type-env.test.ts: createMockSymbolTable rebuilt as a
SemanticModel-shaped mock that still accepts the legacy flat
override bag for backward compat; inline `makeSymbolTable` helpers
for destructured-call and importedReturnTypes suites rewritten to
match the new shape; buildTypeEnv options `symbolTable: X` and
`{ symbolTable }` shorthand renamed to `model:`; one real
createSymbolTable-based test rewritten to use createSemanticModel.
- call-processor.test.ts, heritage-map.test.ts,
heritage-processor.test.ts, symbol-resolver.test.ts: bulk sed
`ctx.symbols.` → `ctx.model.symbols.`. call-processor.test.ts spy
updated to target `ctx.model.types.lookupClassByName`.
Validation: full test suite 5589 passed / 169 skipped / 0 failed;
tsc --noEmit clean; pre-commit eslint + prettier + typecheck all
green. CLAUDE.md / AGENTS.md stats bumped from an earlier `npx
gitnexus analyze` refresh (3965 symbols / 10012 edges / 243 flows).
* refactor(SM-22/SM-23): dispatch table + DAG rearchitecture
SM-22: Extract registration dispatch table into model/registration-table.ts.
Replaces the if/else ladder inside SymbolTable.add() with an O(1)
Map<NodeLabel, RoutingDecision> fan-out. SemanticModel wires the table
per-instance so hooks close over the correct registries.
SM-23: DAG rearchitecture. symbol-table.ts is now a pure 2-index leaf
(fileIndex + callableByName) with zero imports from model/. All
type/method/field routing lives in the model/ layer. Tests migrated to
createSemanticModel() + model.symbols access pattern.
Tests: 5632 passed, 0 failures.
* refactor: delete dead code (skipCallableIndex + model/ facades)
Removes the unused skipCallableIndex flag from the registration dispatch
table and deletes two facade files that had zero consumers.
skipCallableIndex was declared on RoutingDecision and populated for all
10 entries but never read at runtime — semantic-model.ts explicitly
documented that the flag was NOT consulted. The callable-index gate
lives inside SymbolTable.add() via CALLABLE_TYPES.has(type), which is
the single source of truth. Deleting the flag keeps SymbolTable as the
sole decision point and removes documentation-as-data.
model/binding-accumulator.ts and model/heritage-map.ts were facade
pass-throughs of their parent-directory counterparts. Grep confirms no
consumer imports either from the model/ path — all usage goes through
../binding-accumulator.js and ../heritage-map.js directly. model/index.ts
was the only "user" and re-exported them with a note about unifying the
import boundary, but that boundary has no actual consumers today.
Resolves review findings M-01 and M-03 from
.context/compound-engineering/ce-review/20260411-144641-59605d93/maintainability.json
Tests: 5631 passed, 0 failures (1 less than pre-Unit-1: the
skipCallableIndex-specific assertion was removed).
* refactor: remove lookupMethodByOwnerWithMRO backward-compat shim
call-processor.ts re-exported lookupMethodByOwnerWithMRO from
./model/resolve.js as a backward-compat shim for symbol-table.test.ts.
The function already lives in model/resolve.ts and is re-exported
properly from model/index.ts (the barrel) — the call-processor shim
was a duplicate export path with no durable reason to exist.
Migrated the test import from call-processor.js to model/index.js
(the canonical barrel). Deleted the re-export statement and the stale
"re-exported for backward compatibility" comment block. Hoisted the
remaining import to the top of the file with the other imports; the
bottom-of-file position was a relic of the shim pattern.
Resolves review finding M-02 from
.context/compound-engineering/ce-review/20260411-144641-59605d93/maintainability.json
Tests: 5631 passed, 0 failures.
* refactor: harden registration dispatch runtime safety
Two hardening changes in semantic-model.ts, both closing silent-failure
paths in the SM-series dispatcher-bypass failure mode.
1. model.symbols.clear() now cascades to the owner-scoped registries.
Previously, the SymbolTable facade exposed rawSymbols.clear directly,
which only emptied fileIndex + callableByName — the types/methods/
fields registries stayed populated. Any caller holding a SymbolTable
reference that invoked .clear() left the model in a split state where
subsequent .add() calls double-registered in the registries. No
current caller exercises this path, but it was a latent phantom-
resolution risk that didn't belong in a public API. Extracted the
cascade into a single cascadeClear closure wired into both
model.clear() and the facade's clear field.
2. runExhaustivenessGuard now throws instead of console.warn on drift.
The production short-circuit via NODE_ENV === 'production' is
preserved, so real users never see the throw — but CI and dev runs
now fail loudly if a NodeLabel is added to gitnexus-shared without
being placed in one of the three registration-table allowlists. The
previous warn-only behavior was silent in test output volume; SM-19
already documented dispatcher-bypass as the dominant silent-failure
mode in this codebase.
Test-first: added test/unit/model/semantic-model.test.ts covering
model.symbols.clear() cascade (4 registries × clear = 4 tests), the
existing model.clear() cascade (regression guard), and a happy-path
construction test that verifies the current allowlists have zero drift.
Resolves correctness P2 finding (symbols.clear() partial clear),
correctness P3 (exhaustiveness warn-only), and kieran-typescript KT-03
(same exhaustiveness finding, agreement boost).
Tests: 5638 passed (+7 new), 0 failures.
* docs: fix stale JSDoc references in resolveStaticCall
call-processor.ts:2215-2216 referenced SymbolTable.lookupClassByName
and SymbolTable.lookupMethodByOwner via {@link}. Both methods were
removed from SymbolTable during SM-20 — they now live on TypeRegistry
and MethodRegistry respectively, accessible via model.types and
model.methods.
Other SymbolTable.* references in the codebase (lookupExactFull, add,
lookupCallableByName in call-processor.ts:593, symbol-table.ts:86,
type-extractors/types.ts:57) target methods that are still on
SymbolTable and remain valid.
Resolves correctness P3 and kieran-typescript KT-02 (same finding,
agreement boost).
* refactor: deduplicate ALL_NODE_LABELS constant
ALL_NODE_LABELS was private in semantic-model.ts and duplicated
verbatim in registration-table.test.ts. Two hardcoded lists meant a
new NodeLabel added to gitnexus-shared could land in one copy but not
the other, silently drifting the exhaustiveness invariant.
Exported ALL_NODE_LABELS from semantic-model.ts, re-exported through
model/index.ts for barrel consistency, and switched the test to import
it instead of redeclaring. The explanatory comment now describes the
single-source-of-truth contract.
Resolves maintainability M-04.
Tests: 5638 passed, 0 failures.
* refactor: add compile-time NodeLabel exhaustiveness check
The runtime exhaustiveness guard in semantic-model.ts caught drift at
test time. Added a type-level check in registration-table.ts that
catches drift at BUILD time — if a new NodeLabel is added to
gitnexus-shared without being classified into one of the three
allowlists, TypeScript fails the _exhaustiveCheck assignment and
names the missing label.
The runtime guard stays as belt-and-suspenders: if a future contributor
bypasses the type check with @ts-ignore, the runtime guard still fires
in dev/test.
Implementation: converted the three allowlist Set<NodeLabel> initializers
to use `as const` tuples, then derived a union type from the tuples and
asserted `Exclude<NodeLabel, union> extends never`. Zero runtime impact
— the exported Sets are unchanged, Map.get hot-path performance is
unchanged, the test API is unchanged.
Resolves kieran-typescript KT-04.
Tests: 21/21 registration-table tests pass with zero modifications.
* refactor(test): restore type safety to createMockSymbolTable
createMockSymbolTable was widened to (overrides: any = {}): any with an
eslint-disable-next-line, and every buildTypeEnv call site passed the
mock as `model: mockSymbolTable as any`. The widening masked silent
false-green tests: buildTypeEnv accesses model.types/methods/fields,
and a flat any-typed override could silently return undefined from a
path that TypeScript should have caught at compile time.
Defined LegacyMockOverrides interface with typed stubs for each method
the mock can override (SymbolTable reads + TypeRegistry/MethodRegistry/
FieldRegistry lookups). Return type is now SemanticModel, so the mock
object is compile-checked against the real interface — a missing
registry method is a type error, not a silent runtime undefined.
Removed the eslint-disable and all 9 `as any` casts at call sites
(lines 1287, 1300, 1307, 2124, 2138, 5823, 5835, 5850, 5870). The
mock's return value now flows through buildTypeEnv's typed `model`
option without coercion.
Resolves kieran-typescript KT-01 and testing gap TG-02. This was the
highest-value cleanup in the plan — the only finding representing real
hidden test weakness.
Tests: 360 passed | 7 skipped (type-env.test.ts), typecheck clean.
* test: close coverage gaps in model/ registries
Added direct unit tests for the three owner-scoped registries that
previously had only transitive coverage via symbol-table.test.ts and
registration-table.test.ts. These new tests pin behaviors that were
flagged by the testing reviewer as untested or undertested.
method-registry.test.ts (14 tests):
- T-01: arity-fallback branch — when argCount matches no overload,
fall back to the full pool so fuzzy resolution still has candidates.
Previously untested and would have returned undefined instead of
a valid candidate if the branch regressed.
- T-02: requiredParameterCount range filtering — methods with default
parameters accept any argCount in [requiredParameterCount,
parameterCount]. Previously untested at the registry level.
- Variadic fallback (parameterCount=undefined is retained during arity
narrowing, bypassing range check).
- Return-type dedup paths: shared returnType → first wins, differing
returnTypes → undefined, firstReturnType=undefined → undefined,
single-overload skips dedup entirely.
type-registry.test.ts (9 tests):
- classByName homonym accumulation (two User classes in different
packages both returned).
- classByQualifiedName disambiguation — same simple name, different
FQNs resolve independently.
- Partial classes with identical simple + qualified name accumulate
in both indexes.
- registerImpl stores Rust impl blocks separately from classes.
- Multiple impl blocks per type accumulate.
field-registry.test.ts (6 tests):
- register/lookup round-trip, owner-scope isolation, last-wins on
duplicate key (flat map, not overload list).
- clear + re-register round-trip.
Extended symbol-table.test.ts cascade test (renamed from "both
registries" to "all three registries and the nested symbol table") to
also assert model.methods and model.fields are cleared — the test
name previously implied full coverage but only asserted types + symbols.
Resolves testing findings T-01, T-02, T-03, T-05.
Tests: 5667 passed (+29 new), 0 failures.
* refactor(test): replace brittle reference-equality tests + add intent comments
Two cleanups flagged as low-severity P3 by the testing reviewer:
1. registration-table.test.ts: Replaced three reference-equality tests
(hook identity via toBe) with behavioral tests that survive a future
refactor to per-label closures. The new "class-like behavior group"
describe iterates Class/Struct/Interface/Enum/Record/Trait and
verifies each one writes to types.registerClass. Same pattern for
Method/Constructor. A separate "behavior group isolation" describe
verifies class-like hooks don't leak into methods/fields and Impl
never pollutes registerClass. Strictly more coverage than the
reference-equality tests provided and implementation-independent.
2. symbol-resolver.test.ts: Added a comment above the lookupExactFull
and SM-16: getFiles() describes explaining why they intentionally
use createSymbolTable() directly instead of createSemanticModel().
The DAG leaf-only behaviors they test do not involve registries, so
testing the bare SymbolTable keeps the unit isolated. Prevents a
future reader from "fixing" the inconsistency.
3. qualified-class-lookups.test.ts: Added a comment above
`const symbolTable = model.symbols` explaining that processParsing
writes still reach the owner-scoped registries via SemanticModel's
fan-out — the alias is convenience, not a leaf in isolation.
Resolves testing T-04, kieran-typescript KT-05, kieran-typescript KT-06.
Tests: affected files all green (112 passed in registration-table +
symbol-resolver + qualified-class-lookups).
* refactor(model): collapse RoutingDecision wrapper and trim barrel surface
Two cleanups against the advanced-review findings on post-Unit-9 state:
S2 (cross-reviewer agreement — architecture-strategist + code-simplicity):
Delete the RoutingDecision single-field wrapper interface. Post-Unit-1
it held exactly one field (hook: RegistrationHook) and added pure
ceremony at every call site — `dispatchTable.get(key)!.hook(name, def)`
vs the now-direct `dispatchTable.get(key)!(name, def)`. Change the Map
type from Map<NodeLabel, RoutingDecision> to Map<NodeLabel,
RegistrationHook>, drop the interface, and update 17 test call sites.
A3 (architecture-strategist): Trim model/index.ts barrel surface.
createRegistrationTable, RegistrationHook, and RegistrationTableDeps
were re-exported from the barrel despite having zero legitimate
consumers outside model/ itself. The only callers (semantic-model.ts
and registration-table.test.ts) import directly from
./registration-table.js. Barrel exposure invited external callers to
construct orphan dispatch tables with independent registries,
weakening the SM-21 ownership inversion where SemanticModel is the
composition root. Kept CALLABLE_ONLY_LABELS, INERT_LABELS,
DISPATCH_LABELS exported since those remain useful for downstream
resolution logic and have no construction risk.
Resolves review findings:
- S2 (code-simplicity P3, 0.85) + architecture-strategist residual
- A3 (architecture-strategist P3, 0.82)
Tests: 5674 passed, 0 failures. Typecheck clean.
* refactor(model): replace runtime exhaustiveness guard with compile-time bijection
Replace the three-layer drift protection (hardcoded ALL_NODE_LABELS
array + 3 tuple consts + _ExhaustiveLabelCheck type + runExhaustivenessGuard
runtime + CI taxonomy test) with a single Record<NodeLabel, LabelBehavior>
map that structurally proves every invariant at compile time.
## Before
- ALL_NODE_LABELS hardcoded in semantic-model.ts (36 entries, could drift)
- DISPATCH_LABELS_TUPLE / CALLABLE_ONLY_LABELS_TUPLE / INERT_LABELS_TUPLE
private tuples (36 more entries total, could overlap or miss)
- _ClassifiedLabel / _UncoveredLabel type-level check (caught missing
labels but NOT duplicates across tuples)
- runExhaustivenessGuard runtime throw (only defense against duplicates)
- NodeLabel taxonomy coverage test in CI (same check as runtime guard)
Four defenses for invariants that the type system can express directly.
## After
```ts
type LabelBehavior = 'dispatch' | 'callable-only' | 'inert';
const LABEL_BEHAVIOR = {
Class: 'dispatch',
// ...36 entries...
Tool: 'inert',
} as const satisfies Record<NodeLabel, LabelBehavior>;
```
The `as const satisfies Record<NodeLabel, LabelBehavior>` combo enforces:
1. **Every NodeLabel must be a key** — Record requires all K keys.
Adding a NodeLabel to gitnexus-shared without classifying it here
fails with "Property 'X' is missing in type ..." naming the drifted label.
2. **No non-NodeLabel keys allowed** — `satisfies` with object literals
triggers excess-property checking. A typo'd key fails to compile.
3. **No duplicate classification** — impossible by construction; object
keys are unique at the source level.
4. **Valid category** — LabelBehavior is a narrow union, typos caught.
`ALL_NODE_LABELS`, `DISPATCH_LABELS`, `CALLABLE_ONLY_LABELS`, and
`INERT_LABELS` are now derived via `Object.keys(LABEL_BEHAVIOR)` and
`filter(l => LABEL_BEHAVIOR[l] === ...)` — single source of truth,
structurally impossible to drift.
## Deleted
- runExhaustivenessGuard() function in semantic-model.ts (~18 lines)
- ALL_NODE_LABELS hardcoded array in semantic-model.ts (~38 lines)
- DISPATCH_LABELS_TUPLE / CALLABLE_ONLY_LABELS_TUPLE / INERT_LABELS_TUPLE
private consts in registration-table.ts (~30 lines)
- _ClassifiedLabel / _UncoveredLabel / _exhaustiveCheck type machinery
(~20 lines)
## Kept named proofs: none
The `as const satisfies` on the object literal already catches all four
drift modes. Named type-level proofs (_MissingFromMap / _ExtraKeysInMap)
are pure duplication and were removed per review.
## Also in this commit
- S6: trim wrappedAdd narration comments in semantic-model.ts
(Step 1/2/3 block comments removed; kept the Function+ownerId WHY note)
- A3: tighten model/index.ts barrel — createRegistrationTable,
RegistrationHook, RegistrationTableDeps remain direct-imports only;
ALL_NODE_LABELS and LabelBehavior re-exported from the new home in
registration-table.ts
## Resolves
- Advanced-review S4 (runtime guard per-call cost) — guard no longer exists
- Advanced-review S1 (tuple three-defenses indirection) — single Record replaces all tuples
- Correctness P3 (exhaustiveness warns-only) — structurally impossible to drift
- Unit 6 type-level check — subsumed by the Record type
- Unit 3 runtime throw — no longer needed
Tests: 5674 passed, 0 failures. Typecheck clean.
* test(model): delete duplicate closure-isolation spy tests
S5 (code-simplicity P3): The 'closure isolation — each hook can only
write to its registry' describe block duplicated the 'behavior group
isolation' block's coverage via a different mechanism.
Behavioral tests (lines 151-174, kept):
table.get('Class')!('User', def);
expect(deps.methods.lookupMethodByOwner('unrelated', 'User')).toBeUndefined();
expect(deps.fields.lookupFieldByOwner('unrelated', 'User')).toBeUndefined();
Spy tests (deleted, ~55 lines):
vi.spyOn(deps.methods, 'register')
table.get('Class')!('User', def);
expect(methodsSpy).not.toHaveBeenCalled();
Both assert the same invariant — classHook does not touch the methods or
fields registries. The behavioral form observes the END STATE of the
registry (lookup returns undefined), which is the actual contract.
The spy form asserts the IMPLEMENTATION (a specific method was not
called), which couples to internal wiring — a refactor to a different
register function name would break the spy test while the behavioral
test would still pass.
Also dropped the now-unused `vi` import from vitest.
Tests: 24/24 registration-table.test.ts pass (-4 from spy deletion).
* refactor(model): compile-time cross-invariant between CLASS_TYPES and dispatch classHook
A1 (architecture-strategist P2, 0.90): CLASS_TYPES in symbol-table.ts
and the class-like entries of the dispatch table were two independent
hardcoded sets. Adding a new class-like label (e.g. Swift 'Extension')
to one but not the other would silently degrade qualifiedName
population — the symptom is subtle (partial qualified-name lookups)
and no test asserted the co-extensive invariant.
Fixed with a single source of truth and a two-layer compile-time
enforcement:
## symbol-table.ts
- Add `CLASS_TYPES_TUPLE` as `readonly [...] as const satisfies
readonly NodeLabel[]`. The `satisfies` forces every tuple entry to
be a valid NodeLabel at compile time.
- Export derived type `ClassLikeLabel = typeof CLASS_TYPES_TUPLE[number]`.
- Derive `CLASS_TYPES` Set from the tuple — same runtime shape as
before, now typed `ReadonlySet<NodeLabel>`.
## registration-table.ts
- Import `CLASS_TYPES_TUPLE` and `ClassLikeLabel` from symbol-table.ts.
- Narrow the `satisfies` on `LABEL_BEHAVIOR` via intersection:
Record<NodeLabel, LabelBehavior> & Record<ClassLikeLabel, 'dispatch'>
This forces every class-like label to have value 'dispatch' at
compile time. Adding a label to CLASS_TYPES_TUPLE without
classifying it as dispatch in LABEL_BEHAVIOR fails to compile with
a type error naming the drifted label.
- Build the class-like entries of the dispatch Map by iterating
`CLASS_TYPES_TUPLE` at factory time. Adding a label to the tuple
automatically wires it to classHook — no second place to update.
## What the design prevents
1. Drift scenario A (A1 original): 'Extension' added to CLASS_TYPES_TUPLE
but not to LABEL_BEHAVIOR → compile error on LABEL_BEHAVIOR's
satisfies.
2. Drift scenario B: 'Extension' added to CLASS_TYPES_TUPLE but not
wired to classHook → impossible because the Map is derived from the
tuple.
3. Drift scenario C: class-like label classified as something other
than 'dispatch' in LABEL_BEHAVIOR → compile error on the narrowed
intersection.
Runtime behavior unchanged: same 6 labels in CLASS_TYPES, same 6
class-like entries in the dispatch Map. Tests pin the behavior via
the existing behavior-group tests in registration-table.test.ts.
DAG unchanged: registration-table.ts already imported from symbol-table.ts
(the allowed upward direction). symbol-table.ts still imports nothing
from model/.
Tests: 5670 passed, 0 failures. Typecheck clean.
* test(field-extraction): use SemanticModel facade instead of raw SymbolTable
A6 (architecture-strategist P3, 0.85): field-extraction.test.ts created
its FieldExtractorContext fixture with `symbolTable: createSymbolTable()` —
a raw SymbolTable leaf, not the facade. In production, the context's
symbolTable field is always `model.symbols` (the SemanticModel-wrapped
facade where .add() dispatches through the owner-scoped registries).
The current field extractors don't call symbolTable.add() at all, so
this change is behavior-neutral today. The value is architectural
consistency — matching the test fixture to the production shape
prevents silent drift if a future field extractor starts registering
dynamically-discovered properties via the context. Without the fix,
such writes would hit the raw leaf and skip the fan-out, and tests
would pass even though the symptom (empty FieldRegistry) would
manifest in production.
Tests: 50/50 field-extraction.test.ts pass. Production tsc --noEmit
clean. Test-tsconfig error count unchanged (634 pre-existing errors
in unrelated test files, out of scope).
* refactor(A5): decouple model/resolve.ts from language registry
Move the MroStrategy type into gitnexus-shared and replace the
language: SupportedLanguages parameter on lookupMethodByOwnerWithMRO
with a direct mroStrategy: MroStrategy literal. Callers derive the
strategy from their language provider before invoking the resolver.
model/resolve.ts no longer imports from ../languages/index.js, so the
model/ layer is free of cross-layer coupling with the language
registry — this closes finding A5 from the SM-20/21/22/23 advanced
review (plan 006).
* feat(A4): add MethodRegistry.lookupMethodByName flat-by-name index
Add a secondary `methodsByName: Map<string, SymbolDefinition[]>` index
on MethodRegistry that returns every method with a given unqualified
name, accumulated across owners and overloads. The new index shares
SymbolDefinition references with methodByOwner — no duplication.
This is step 1 of the A4 double-index removal (plan 006). Tier 3
global resolution will switch to this index in Unit 3 so Method and
Constructor can be removed from CALLABLE_TYPES in Unit 4.
* refactor(A4): extend Tier 3 + memberCallByFile to consult method registry
Add model.methods.lookupMethodByName to Tier 3 global resolution in
resolution-context.ts and to the callable-pool build in
call-processor.ts (resolveMemberCallByFile + D2 widen path).
Intentionally behavior-preserving: Method and Constructor are still
in CALLABLE_TYPES so the new lookup returns identical candidates that
already reach Tier 3 through callableByName. Both paths dedup by
nodeId during this intermediate state — Unit 4 shrinks CALLABLE_TYPES
and the dedup is removed.
Part of plan 006 A4 step 2.
* refactor(A4): shrink CALLABLE_TYPES to free callables only
CALLABLE_TYPES = {Function, Macro, Delegate}. Method and Constructor
are no longer double-indexed in callableByName — they reach resolvers
through model.methods.lookupMethodByName instead.
Companion changes:
- Introduce CALL_TARGET_TYPES = CALLABLE_TYPES ∪ {Method, Constructor}
for the resolver's kind filter (filterCallableCandidates,
countCallableCandidates). Separates registration semantics (narrow)
from the resolver's acceptable-target set (wide).
- type-env.ts for-loop return-type inference consults both indexes,
treating the union as the authoritative call pool.
- resolveMemberCallByFile + D2 widen path keep the nodeId dedup in
place: Python/Rust/Kotlin class methods emitted as Function+ownerId
still land in both indexes until Unit 5 unblocks the normalization.
- Tier 3 global resolution (resolution-context.ts) keeps the same
dedup for the same reason.
Test updates reflect the new contract: Method/Constructor live in
methodsByName, not callableByName. Orphan Method-without-ownerId now
lives only in the file index (no registry coverage).
Part of plan 006 — closes A4 for strictly-labeled methods. Python/
Rust/Kotlin Function+ownerId normalization is tracked as Unit 5
(blocked).
* refactor: rename CALLABLE_TYPES → FREE_CALLABLE_TYPES
Pure rename. The constant's meaning changed in Unit 4 (free callables
only — no methods, no constructors) so the name now reflects that
scope: "callables that have no owner scope". Updates the constant
declaration and every consumer in src/ and test/.
Closes plan 006 Unit 6.
* refactor(A2): strict SymbolTableReader (pure reads) + SymbolTableWriter (+add)
Split the SymbolTable interface into three strictly layered surfaces:
- SymbolTableReader: lookups + iteration. NO add, NO clear. Holders
cannot mutate the table in any way.
- SymbolTableWriter extends Reader: + add. NO clear. Holders can
register new symbols but cannot trigger a leaf-index reset.
- InternalSymbolTable (private, not exported): + clear. The cascading
reset capability is reachable only through createSymbolTable's
return type, held exclusively by SemanticModel.rawSymbols.
SemanticModel.symbols is now typed as SymbolTableWriter — external
consumers (workers, processors, pipelines) can register symbols and
query them, but cannot reach .clear(). The A2 LSP fix holds: callers
holding any public reference cannot desync the leaf indexes from the
owner-scoped registries.
Delete the transitional `type SymbolTable = SymbolTableReader` alias
and migrate every consumer (src + test) to the explicit names:
- Field and parameter annotations use SymbolTableReader by default;
only code that calls .add() uses SymbolTableWriter.
- parsing-processor (workers + sequential paths) takes
SymbolTableWriter so it can register extracted symbols.
- field-types, call-processor, named-binding-processor,
workers/parse-worker: use SymbolTableReader (query-only).
- Tests: drop the stale `clear` fields from mock factories and
migrate the semantic-model cascade tests from the removed
model.symbols.clear() path to model.clear().
Closes plan 006 Unit 7. Industry sources: TypeScript compiler API
builder pattern, Salsa ParallelDatabase, .NET IReadOnlyList. See the
a2-lsp-clear-contract-research artifact for full citations.
* feat(A2): add SemanticModel.resetFileIndex() partial-reset entry point
Add a named method that clears only the leaf file and callable
indexes without cascading to the three owner-scoped registries
(types, methods, fields). Replaces the rare partial-reset use case
that was previously reachable via the now-removed symbols.clear()
path from A2 (plan 006 Unit 7).
JSDoc makes the semantic difference with model.clear() explicit so
future readers don't have to guess which method to call for a given
reingestion scenario.
Test-first: three scenarios cover the partial-vs-full semantics,
re-add after reset, and idempotency.
Closes plan 006 Unit 8.
* docs(S7): trim registration-table module JSDoc
Remove the ~24 lines of design-provenance citations from the module
JSDoc. The rust-analyzer, TypeScript-compiler, and Fowler references
are preserved in git history via the original SM-22 commits and in
plan 006 Unit 9.
Keep the ownership diagram, behavior-group table, and the
'How to add a new NodeLabel' checklist — those are load-bearing for
future contributors.
Closes plan 006 Unit 9 (S7 advanced-review finding).
* test(S3): migrate type-env.test.ts off LegacyMockOverrides
Replace the createMockSymbolTable bridge and LegacyMockOverrides
interface with real createSemanticModel() + add() calls across all
14 call sites. Where a test needs a specific registry lookup that
can't be pre-populated cleanly, use vi.spyOn on the real registry
instead.
Pattern breakdown:
- Pattern A (pre-populate via model.symbols.add): 13 sites
- Pattern B (vi.spyOn on registry lookup): 1 site
Deletes LegacyMockOverrides + createMockSymbolTable entirely. The
real MethodRegistry arity/returnType semantics match the hand-rolled
mock behavior in every migrated case, and no 'as any' casts remain
in the file.
Closes plan 006 Unit 10 (S3 advanced-review finding).
* refactor: remove unused MroStrategy type exports from language-provider and resolve modules
* refactor: relocate symbol-table, heritage-map, resolution-context into model/
Use git mv so blame and history follow each file:
- gitnexus/src/core/ingestion/symbol-table.ts → model/symbol-table.ts
- gitnexus/src/core/ingestion/heritage-map.ts → model/heritage-map.ts
- gitnexus/src/core/ingestion/resolution-context.ts → model/resolution-context.ts
These three files are part of the SemanticModel layer (file/callable
indexes, heritage parent map, tiered resolver) and now sit alongside
the registries they collaborate with. Updates every consumer import
path across src/ and test/ to the new locations.
* refactor(model): enforce pure-leaf DAG + delete legacy re-exports
model/ is now a pure leaf: zero upward imports and zero compat
shims in its parent processors. Completes the DAG cleanup started
in the previous commit.
1. walkBindingChain — moved into model/resolution-context.ts;
named-binding-processor.ts deleted.
2. NamedImportMap + NamedImportBinding + isFileInPackageDir —
moved into model/resolution-context.ts. Every consumer now
imports from the canonical location directly. Legacy re-exports
in import-processor.ts deleted.
3. c3Linearize + gatherAncestors — moved into model/resolve.ts.
mro-processor.ts imports them back for computeMRO. Legacy
c3Linearize re-export from mro-processor.ts deleted.
4. ExtractedHeritage type — moved into model/heritage-map.ts.
call-processor.ts, parsing-processor.ts, pipeline.ts,
heritage-processor.ts, and the test files now import it from
the canonical location. Legacy re-exports in parse-worker.ts
and heritage-processor.ts deleted.
5. resolveExtendsType — rewritten in model/heritage-map.ts to
take an explicit HeritageResolutionStrategy (A5-style DI).
buildHeritageMap accepts an optional getHeritageStrategy
callback; production uses getHeritageStrategyForLanguage from
heritage-processor.ts. Legacy resolveExtendsType re-export
from heritage-processor.ts deleted.
Verified:
- grep 'from "..' gitnexus/src/core/ingestion/model → empty
- grep 'Re-export for legacy' gitnexus/src/core/ingestion → empty
- npx tsc --noEmit → clean
- npx vitest run → 5686 passing
* docs(model): strip phase/plan references from module comments
Remove SM-20/21/22/23, A2/A4/A5, plan 006, Unit N labels and historical
phrasing ("previously", "legacy", "model-leaf DAG cleanup") from all 10
files in src/core/ingestion/model/. Preserve domain vocabulary (Tier
1/2/3), invariants, and caveats — only the plan archaeology is gone.
* refactor(model): tighten interface segregation + compile-time invariants
Apply four gated findings from branch-wide code review:
- SemanticModel.symbols now typed as SymbolTableReader; MutableSemanticModel
widens it back to SymbolTableWriter. ResolutionContext.model is typed as
MutableSemanticModel since it owns the lifecycle. Resolvers that only
query symbols can annotate their own fields as SemanticModel to drop
write access at the type level.
- Lookup methods (lookupExactAll, lookupCallableByName, lookupClassByName,
lookupClassByQualifiedName, lookupImplByName) now return
readonly SymbolDefinition[]. The returned arrays are live views into
the internal indexes; the readonly marker prevents accidental caller
mutation. walkBindingChain return type narrowed to match.
- FREE_CALLABLE_TUPLE + FreeCallableLabel exported from symbol-table.ts
as the single source of truth for free-callable labels. LABEL_BEHAVIOR
now satisfies Record<FreeCallableLabel, 'callable-only'> as a second
cross-invariant alongside Record<ClassLikeLabel, 'dispatch'>. Adding a
label to the tuple without classifying it as 'callable-only' fails at
build time. CALLABLE_ONLY_LABELS is now a re-export alias of
FREE_CALLABLE_TYPES so the two sets cannot drift.
- walkBindingChain fast-exits before allocating its cycle-detection Set
when the caller's file has no named bindings. Skips ~200k transient
Set allocations per large-repo resolution pass.
Also fixes five stale comments flagged by the review: duplicate JSDoc
block on RegistrationHook merged; resolve.ts "delegates to mro-processor"
direction corrected; RegistrationTableDeps JSDoc names
createRegistrationTable (not createSymbolTable); mro-processor.ts
"re-exported at top" stale comment removed; gatherAncestors export
comment matches reality.
tsc --noEmit clean, full test suite green (5786 tests).
* refactor(model): resolve four deferred P2 review findings
Address the four gated items from the branch-wide review that needed
design decisions before applying:
F#3 — Method/Constructor without ownerId fallback to callable index.
The dispatch hook silently skips owner-scoped labels that lack an owner
(an extractor contract violation — AST-degraded parse, or a buggy
language extractor). Pre-dispatch-table code let such defs fall through
to callableByName and stay reachable at Tier 3 global resolution. This
restores that fallback in SymbolTable.add so orphaned Methods and
Constructors don't silently vanish. Property deliberately does NOT
participate in the fallback to avoid polluting common names like
id / name / type.
F#4 — Delete MutableSemanticModel.resetFileIndex. The method had zero
production callers (only three tests), documented a "rare partial-
reingestion flow" that was never implemented, and contained the
adversarial-reviewer's double-populate trap: calling resetFileIndex
followed by re-adding the same class symbol would push a duplicate
SymbolDefinition into TypeRegistry.classByName without ever clearing
the first one. If incremental reingestion is ever needed, it can be
designed properly with per-file TypeRegistry invalidation. For now,
deleting the footgun is safer than documenting it.
F#5 — Compile-time dispatch-table completeness check. `LABEL_BEHAVIOR`
already enforces "every NodeLabel is classified" via
`Record<NodeLabel, LabelBehavior>`, but the dispatch-table factory
populated its Map with manual `table.set(...)` calls that TypeScript
could not correlate back to the `'dispatch'` classification. Add a
type-level `DispatchLabel` extracted from `LABEL_BEHAVIOR` via a
conditional mapped type, and build the table from an object literal
that satisfies `Record<DispatchLabel, RegistrationHook>`. Adding a new
dispatch-classified label without wiring it to a hook now fails the
build with a named-key error — no more silent no-op hooks.
F#7 — Tier 3 dedup fast-path via MethodRegistry.hasFunctionMethods.
The Set-based dedup between callableDefs and methodDefs is only needed
when a Python/Rust/Kotlin class method (emitted as Function+ownerId by
the worker) lands in both indexes. For TS/Java/C#/C++/Ruby-only repos
— where the two indexes are disjoint by construction — the dedup was
pure overhead on every global-tier hit. MethodRegistry now tracks
whether any Function-typed def was ever registered, and resolution-
context branches Tier 3 into a concat-only fast path when that flag
is false. Slow path with dedup survives unchanged for mixed-language
repos.
New tests pin the invariants: hasFunctionMethods flag transitions,
Method/Constructor orphan fallback, Property non-fallback, and the
MethodRegistry clear() reset. Full test suite green (5756 tests).
* refactor(model): close remaining P3 review findings + coverage gaps
Address the remaining review items in one batch.
Production refactors:
- Rename classHook → classLikeHook (M05). The hook handles Class /
Struct / Interface / Enum / Record / Trait; the vocabulary used in
surrounding docs and the behavior-group table is "class-like". The
rename makes the code match the taxonomy without forcing readers
through a mental glossary.
- Extract MAX_BINDING_CHAIN_DEPTH constant in resolution-context.ts
and document it as a known silent false-negative source (ADV-003).
Five hops cover the common TypeScript monorepo pattern; raising the
cap is a one-line change if a real repo exceeds it. walkBindingChain
consumes the constant so the 5 magic number no longer floats free.
- Replace defs.filter() allocation in MethodRegistry.lookupMethodByOwner
with a two-pass streaming count + conditional materialization
(PERF-04). Pure-match and pure-reject arity paths now skip the
filtered-array allocation entirely; only the discriminating case
(at least one match AND at least one rejection) pays it.
- Rewrite NOOP_SYMBOL_TABLE in parse-worker.ts and NOOP_SYMBOL_TABLE_SEQ
in parsing-processor.ts to implement all six SymbolTableReader
methods (ADV-005). The `as unknown as SymbolTableReader` cast is
removed in favor of a direct SymbolTableReader annotation, so future
additions to the interface surface as compile errors on the stubs
instead of silently falling through.
- type-env.ts getCallableUnionCount and getFirstCallable now take
`model: SemanticModel` as an explicit argument instead of reaching
into the enclosing `model!` non-null assertion (KT-003). Callers
enter via an `if (model)` guard and pass the narrowed reference, so
the non-null precondition is visible at the type level and the
closures cannot be accidentally extracted into a context without
the guard.
- Tier 3 dedup in resolution-context.ts now covers all four index reads
(classDefs, implDefs, callableDefs, methodDefs) via a pushUnique
helper (C-03). Previously classDefs and implDefs were spread directly
without dedup; any theoretical nodeId collision would have produced
duplicates in globalDefs.
Test infrastructure:
- Extract makeDef / makeMethod factory helpers into
test/unit/model/helpers.ts (T-07). The four registry/table test
files now import the shared helper and specialize with overrides,
removing ~25 lines of duplicated boilerplate and creating a single
point of maintenance.
New test coverage:
- T-01: c3 BFS fallback — cyclic Python hierarchy that fails c3
linearization and must fall back to heritageMap.getAncestors() BFS
order. Added to the lookupMethodByOwnerWithMRO describe block.
- T-02: Tier 2a-named precedence — verifies the binding chain walker
fires before Tier 2a import-scoped when an aliased import
`import { User as U } from B` competes with a raw same-name Tier 2a
hit. Also pins Tier 1 same-file precedence over Tier 2a-named.
- T-03: Tier 3 Function+ownerId dedup — end-to-end test that a Python
class method emitted as `Function + ownerId` yields exactly ONE Tier
3 candidate (not two). Companion test pins the fast-path branch for
hasFunctionMethods === false repos.
- T-06: walkBindingChain guards — circular re-export detection,
depth-cap exceeded drop, and boundary case at exactly
MAX_BINDING_CHAIN_DEPTH hops resolving successfully.
All tests added to a new test/unit/model/resolution-context.test.ts
dedicated to ResolutionContext.resolve() tier-precedence invariants.
Full suite: 5708 passing (minus the known Windows LBUG lock flake
that passes in isolation).
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* feat(group): bridge.lbug storage + contract matching expansion
Part 1 of 4 in the split of #606 (ticket: #791, closes#790 with a
revised plan per @magyargergo's request).
## What changed
Adds the LadybugDB-backed bridge storage infrastructure and extends
the contract matching algorithm with wildcard support. All changes are
additive: storage.ts, sync.ts, service.ts, cli/group.ts, mcp/tools.ts
are left on their upstream main versions and will migrate to the new
bridge in follow-up PRs (#792, #793, #794).
### Files
**New (844 LOC prod):**
- `gitnexus/src/core/group/bridge-db.ts` — atomic write-to-temp with
`retryRename` for Windows EBUSY/EPERM, per-item write tolerance via
`WriteBridgeReport`, `findContractNode` with three-tier symbol
lookup (uid → filePath+name → filePath)
- `gitnexus/src/core/group/bridge-schema.ts` — schema DDL
- `gitnexus/src/core/group/normalization.ts` — contract ID
canonicalization + `dedupeContracts` / `dedupeCrossLinks` helpers
used by both matching and bridge write
**Modified (+137 LOC prod):**
- `gitnexus/src/core/group/matching.ts` — adds `runWildcardMatch` for
`grpc::Service/*` wildcard consumers, `buildProviderIndex` helper,
and canonical gRPC ID handling in `normalizeContractId`
- `gitnexus/src/core/group/types.ts` — `MatchType` gains `'wildcard'`;
new `BridgeHandle` and `BridgeMeta` interfaces
**New tests (658 LOC):**
- `gitnexus/test/unit/group/bridge-db.test.ts` — core write/read round
trip, `WriteBridgeReport` shape, dropped-links counter, retryRename
behavior on EBUSY/ENOENT/EPERM/EACCES
- `gitnexus/test/unit/group/bridge-db-edge.test.ts` — edge cases
(malformed meta, missing contract nodes, concurrent access)
**Modified tests (+225 LOC):**
- `gitnexus/test/unit/group/matching.test.ts` — wildcard consumer
matching, gRPC canonical ID handling, same-service guard
### Self-review fixes folded in
Carried forward from the original #606 self-review:
- `writeBridge` try/finally handle lifecycle + `handleClosed` sentinel
- `openBridgeDbReadOnly` partial-handle cleanup
- `writeBridgeMeta` uses `retryRename` for Windows consistency
- `retryRename` unit tests (was zero coverage)
- Per-item try/catch around every CREATE loop so one malformed contract
doesn't abort the whole write
- Dropped cross-link counter (`linksDroppedMissingNode`)
### Why now
magyargergo asked for the #606 PR to be split so we can iterate with
confidence (https://github.com/abhigyanpatwari/GitNexus/pull/606#issuecomment-4229612271).
This is the foundational layer — pure infra, no user-facing surface,
no callers of the new APIs in this PR. Later PRs wire it in.
### How to verify
- `cd gitnexus && npx tsc --noEmit`
- `cd gitnexus && npx vitest run test/unit/group/bridge-db.test.ts --pool=forks`
- `cd gitnexus && npx vitest run test/unit/group/bridge-db-edge.test.ts --pool=forks`
- `cd gitnexus && npx vitest run test/unit/group/matching.test.ts --pool=forks`
- Pre-commit hook runs clean
### Risk / rollback
**Low.** All new code sits under `src/core/group/` in new files plus a
minimal `+16/-1` diff to `types.ts` and a `+136/-0` diff to `matching.ts`
(both purely additive). No existing callers reference the new APIs
(bridge-db, openBridgeOrFallback, runWildcardMatch) — the PRs that wire
them in come later in the split chain. Rollback = `git revert` of the
merge commit; no state introduced, no schema migration triggered.
### Scope discipline (per GUARDRAILS.md)
- Only the 8 files listed above are touched; no drive-by refactors
- No CI/release/security config changes
- No secrets, tokens, or machine-specific paths
- Content is lifted from the #606 branch which already passed CI 11/11
green on `d15b8cb` (before the split)
### Dependencies
- **Base:** `main` (no dependencies on other split PRs)
- **Blocks:** extractor expansion (#792), sync pipeline (#793),
cross-impact feature (#794)
- **Related ticket:** #791
Co-authored-by: Claude <noreply@anthropic.com>
* fix(group): address @claude review on #795
Addresses the findings from the automated review on PR #795
(https://github.com/abhigyanpatwari/GitNexus/pull/795#issuecomment-4229770000
— posted by @magyargergo / claude-code Action run).
### Medium severity (reviewer flagged as blockers)
- **bridge-db.ts `openBridgeDbReadOnly` bak recovery** — the `.bak`
recovery path used bare `fsp.rename(bakPath, dbPath)`, which is
exactly the scenario most likely to hit Windows EBUSY/EPERM (an
interrupted writer still holding the handle for a few ms). Switched
to `retryRename` for consistency with the rest of the file's
Windows-safe rename path.
- **bridge-db.ts `ensureBridgeSchema` error detection** — the inline
`msg.includes('already exists')` substring match has been lifted
into a named constant `LBUG_ALREADY_EXISTS_MSG` with a comment
documenting the coupling to LadybugDB's error message wording and
why we can't use `IF NOT EXISTS` (LadybugDB DDL doesn't support it)
or typed errors (LadybugDB's JS driver doesn't expose error codes).
Also tightened the `catch (err: any)` to `catch (err: unknown)`.
- **bridge-db.ts `findContractNode` — extracted out of writeBridge**
— the 35-line async closure living inside `writeBridge` has been
lifted to three module-level functions: `createContractLookupIndex`,
`indexContract`, and `findContractNode`. `findContractNode` is now
a pure synchronous function taking a prebuilt index instead of
doing its own DB queries. The `writeBridge` cross-link loop is now
~25 lines instead of ~100.
- **bridge-db.ts `findContractNode` — N+1 query elimination** — the
old inner-closure version issued up to 6 DB round-trips per
cross-link (2 endpoints × up to 3 tiers of fallback queries). For a
group with 1000 cross-links, that's up to 6000 DB queries just to
resolve endpoints. The new version consults an in-memory
`ContractLookupIndex` built incrementally as contracts are inserted
(`indexContract` called AFTER each successful insert so failed
inserts don't poison the index). Cross-link resolution is now
O(1) per link instead of O(3) DB queries per link, with zero DB
round-trips during the cross-link loop.
### Minor severity
- **bridge-db.ts `queryBridge` empty-array guard** — if LadybugDB
ever returns an empty `QueryResult[]` at the top level (shouldn't
happen with single-statement calls, but driver contract isn't
explicit), the old code would call `.getAll()` on `undefined` and
crash with a confusing stack. Added an `unwrapQueryResult` helper
that throws an explicit `'empty QueryResult array'` error instead,
making a potential driver regression visible immediately.
- **normalization.ts `contractRichness` weights** — added a
block-level comment documenting the weight ordering (+3 for
symbolUid, +2 for each symbol-identifying field, +1 for service
tag or non-manifest origin) and explicitly noting that the
absolute numbers don't matter, only the relative ordering. Matches
the "comment for contributors" suggestion in the review.
- **bridge-schema.ts `BRIDGE_SCHEMA_VERSION` migration comment** —
added a 4-point contract explaining what bumping the constant
means ("discard and re-sync" strategy for V1, no in-place
migration yet, new migration logic should live in a separate
`bridge-migrations.ts` module when it becomes necessary).
- **test/unit/group/fixtures.ts** — extracted the `makeContract`
helper previously copy-pasted between `bridge-db.test.ts` and
`bridge-db-edge.test.ts` into a shared fixtures module. Both test
files now import from `./fixtures.js`. Kept the scope minimal:
fixtures is NOT a general-purpose factory module, just the shared
baseline contract builder.
### New tests
Added 9 pure-function unit tests for the now-extracted
`findContractNode` in `bridge-db.test.ts`:
- returns null on empty index
- tier 1 (symbolUid) match, including repo-scope and role-scope
isolation
- tier 2 (filePath + symbolName) fallback when symbolUid is empty
or mismatches
- tier 3 (filePath only) when exactly one contract lives in the
file, and refusal when multiple do
- priority ordering when multiple tiers could resolve
These are fully isolated — no DB, no temp directories, no native
LadybugDB binding — so they run in <10ms total and are
immediately trustworthy as a regression safety net.
### Deliberately deferred (reviewer marked as "fine for now")
- `BridgeHandle._db` / `._conn` typing to `unknown` with casts in
`bridge-db.ts` — reviewer's note: "The typing is fine for now."
- Batch inserts via `UNWIND` — needs LadybugDB support confirmation,
tracked as a follow-up; the per-item pattern remains.
- `queryBridge` prepared-statement lifecycle — the current pattern
(prepare → execute → GC) relies on LadybugDB's internals, worth
verifying against their docs in a separate audit.
### Scope discipline (per `GUARDRAILS.md`)
- Only files touched by this PR (`bridge-db.ts`, `bridge-schema.ts`,
`normalization.ts`, both bridge test files, new `fixtures.ts`) —
no drive-by refactors
- No CI/release/security config changes
- No secrets
### Test + typecheck status
- `npx tsc --noEmit` clean
- `bridge-db.test.ts`: added 9 `findContractNode` tests, all pass in
isolation. The full-file run still hits the pre-existing native
LadybugDB cleanup segfault that flakes the reported count — same
as every prior commit on this branch, not a regression.
- `bridge-db-edge.test.ts`: 4/4 pass
- `matching.test.ts`: 28/28 pass
- `types.test.ts`: 5/5 pass
- `retryRename` tests (4/4) and `findContractNode` tests (9/9)
verified in isolation via `-t` filter
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: restore tree-sitter-swift postinstall patch for macOS ARM64
PR #516 (77dcb06) deleted `scripts/patch-tree-sitter-swift.cjs` and
the `postinstall` script entry when bumping to `tree-sitter-swift@0.7.1`,
since 0.7.1 ships prebuilt darwin-arm64 binaries and no longer needs the
patch. PR #538 (01ddc3e) then had to revert `tree-sitter-swift` back to
`^0.6.0` (and `tree-sitter` back to `^0.21.1`) because `npm overrides`
doesn't apply when gitnexus is installed via `npx -y` (gitnexus isn't the
root project, so overrides are silently ignored, producing ERESOLVE errors).
PR #538 reverted the grammar package changes but did not restore the patch
script, leaving `tree-sitter-swift@0.6.0` unable to build its native
binding on macOS ARM64. The symptom is `gitnexus analyze` printing
"Skipping swift" or "swift parser not available".
`Dockerfile.test` still references `node scripts/patch-tree-sitter-swift.cjs`
(added in the same PR #516), confirming the regression — the test image
build is also broken.
This commit restores the patch script from commit `0c8ec95` (the last
revision before it was deleted) and re-adds the `postinstall` entry to
`package.json`. No logic changes — it is an exact restoration.
The TODO comment in the script ("Remove this script when tree-sitter is
upgraded to ^0.22.x") still applies.
* style: run prettier on patch-tree-sitter-swift.cjs
The C# tree-sitter query set only matched `base_list` on
`class_declaration`, so interfaces extending other interfaces
(`interface IFoo : IBar`) were never captured as heritage edges.
This broke transitive interface implementation chains. For example,
given:
interface IBase { }
interface IFoo : IBase { }
class MyClass : IFoo { }
only `MyClass -> IFoo` was emitted, and the `IFoo -> IBase` edge was
silently dropped. Any analysis that relies on walking the full
interface inheritance chain (e.g. "which classes implement IBase?")
therefore returned incomplete results.
This patch adds two new query patterns mirroring the existing
class_declaration heritage patterns, but targeting
`interface_declaration`:
(interface_declaration name: (identifier) @heritage.class
(base_list (identifier) @heritage.extends)) @heritage
(interface_declaration name: (identifier) @heritage.class
(base_list (generic_name (identifier) @heritage.extends))) @heritage
The existing heritage-processor pipeline already handles these
captures correctly once the query emits them, so no changes are
needed outside of tree-sitter-queries.ts.
Testing:
- New fixture `csharp-interface-heritage/` covering:
* interface : interface (single base)
* interface : interface, interface (multiple bases)
* class : interface (where that interface derives from others)
- 6 new test cases in test/integration/resolvers/csharp.test.ts
asserting exactly 4 IMPLEMENTS edges and 0 EXTENDS edges for the
fixture.
- Full C# resolver suite: 175/175 passing, no regressions.
Co-authored-by: Prota100 <Prota100@users.noreply.github.com>
* Fix: replace recursive AST traversal with iterative stack to prevent stack overflow on large files
Fixes#752
Large PHP files (2000+ lines) with deeply nested AST structures (closures,
array literals, chained method calls) cause "Maximum call stack size exceeded"
during analysis. This converts three recursive tree traversal functions to
iterative loops using explicit stacks:
1. `walk()` in type-env.ts — the main AST walker that processes every node.
On a 2,462-line PHP controller, this recurses through 5,000-10,000+ nodes.
2. `findRelationCall()` in languages/php.ts — recursive search for Eloquent
relationship calls within method bodies.
3. `findDescendant()` in utils/ast-helpers.ts — generic recursive utility
used by PHP property extraction and other parsers.
All three now use a while loop with an array-based stack instead of function
call recursion, eliminating V8's ~10K frame call stack limit as a constraint.
Tested against a production Laravel codebase with 373 PHP files (87,723 lines
total, largest file 2,462 lines) — indexes successfully in 17.4s with zero
errors, where the recursive version would crash with stack overflow.
* Fix: reverse child push order in findRelationCall iterative traversal
The iterative stack-based traversal pushed children in forward order,
causing the last child to be processed first (LIFO). This reversed the
original recursive left-to-right DFS order. Push children in reverse
so the first child ends up on top of the stack.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix: move stack declaration before processNode, rename walk to processNode
Move the stack initialization above the function that pushes onto it,
making the data-flow order match the code order. Rename walk to
processNode since it now processes a single node rather than
recursively traversing the tree.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: load VECTOR extension during DB init for semantic search
The VECTOR extension was only loaded inside the embedding generation
pipeline (createVectorIndex). On a fresh gitnexus serve session,
semantic and hybrid search failed because QUERY_VECTOR_INDEX was
unknown.
Now loads the VECTOR extension alongside FTS during database
initialization in both the single-connection and pool-based paths.
Fixes#766
* fix: reset vectorExtensionLoaded on DB close and retry paths
The vectorExtensionLoaded flag was not being reset in closeLbug() or
the busy-retry cleanup path in withLbugDb(). This caused the VECTOR
extension to not be re-loaded after a close+re-init cycle, breaking
semantic search on reconnection.
Also resets shared.ftsLoaded and shared.vectorLoaded in the pool
adapter closeOne() for external DB entries, preventing stale extension
state when the pool is re-opened.
Adds integration tests covering vector extension loading, idempotency,
and state reset on both close and busy-retry paths.
* fix: set ftsLoaded flag in initLbugWithDb to avoid redundant extension reloads
* fix: set shared.vectorLoaded flag in initLbugWithDb to avoid redundant reloads
* fix: map diff hunks to symbol line ranges in detect_changes
The detect_changes tool previously used `git diff --name-only` and
picked the first 20 arbitrary symbols from each changed file. This
produced false positives (unchanged symbols reported as modified) and
false negatives (actually changed symbols dropped by the LIMIT).
Now uses `git diff -U0` to get unified diff with hunk headers, parses
the @@ line ranges, and queries for symbols whose [startLine, endLine]
range overlaps the diff hunks. Only truly touched symbols are reported.
Also fixed the CONTAINS path match to ENDS WITH to prevent cross-file
false positives from substring matching.
Fixes#758
* fix: address review feedback - variable shadowing, batch queries, tests
- Rename `params` to `queryParams` in detectChanges hunk-mapping loop
to avoid shadowing the outer method parameter
- Replace N+1 per-symbol process lookup with a single batched query
using WHERE n.id IN $ids (same pattern as impact BFS traversal)
- Add unit tests for parseDiffHunks covering single/multi file,
single/multi hunk, omitted count, pure-deletion, and empty input
* style: fix prettier formatting in parse-diff-hunks test
* Initial plan
* SM-19: Replace resolveCallTarget with thin dispatcher
Delete the monolithic resolveCallTarget function (~200 lines) and replace it
with a 15-line thin dispatcher that routes to resolveMemberCall,
resolveStaticCall, or resolveFreeCall. Extract module-alias resolution and
file-based member-call fallback into dedicated helper functions.
- resolveCallTarget body reduced from ~200 lines to ~15 lines
- Extract resolveModuleAliasedCall helper (Python/Ruby module imports)
- Extract resolveMemberCallByFile helper (trait dispatch, overload disambiguation)
- Extract singleCandidate helper (constructor alias fallback, name-based fallback)
- Update unit tests for new dispatcher semantics
- Update doc comments referencing deleted D0-D4 paths
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/469eac38-b0c0-4a26-a2ff-3eb06299730b
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* SM-19: Add singleCandidate tail fallback for member calls with unresolvable receiver type
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/469eac38-b0c0-4a26-a2ff-3eb06299730b
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix(SM-19): address all PR #770 review findings + fix CI
Fixes all 5 test failures (2 unit + 3 integration) and addresses 10
review findings from comment 4225312416.
Critical fix — singleCandidate null-route guard
The SM-19 dispatcher chained singleCandidate as an unconditional tail
fallback for member calls with receiverTypeName. This bypassed the
SM-10 R3 null-route contract: when the receiver type IS in the index
but file/owner filtering produced zero matches, the old code returned
null (genuine miss), but the new code fell through to singleCandidate
(false-positive CALLS edge).
Root cause: resolveMemberCallByFile returns null for two semantically
different reasons — (1) type not found in the index at all, and
(2) type found but no candidate matched after narrowing. The dispatcher
treated both as "try the next fallback." The old resolveCallTarget
exited the entire function on case 2.
Fix: after the scoped resolvers both return null, check whether the
receiver type resolves in the index. If it does (case 2), null-route
— the scoped resolvers made the right decision. If it doesn't (case 1,
e.g. PHP 'mixed', dynamic types), singleCandidate is the correct last
resort. ctx.resolve is cached so the check is free.
This fixes:
- Unit: no heritageMap null-route test (was getting 1 edge, expects 0)
- Integration: Rust c.trait_only() negative test
- Integration: 3 PHP heritage + alias tests (singleCandidate correctly
fires when the receiver type is not in the index)
Performance (findings #1, #2, #3)
- Thread pre-computed tiered result into resolveModuleAliasedCall via
new tieredOverride parameter — eliminates the duplicate ctx.resolve
call on every module-alias path.
- Add countCallableCandidates helper that short-circuits at threshold
without allocating an intermediate array — replaces the
filterCallableCandidates(...).length > 1 allocation in skipMember.
- resolveMemberCallByFile lookupCallableByName caching deferred to a
follow-up (finding #2) — the fix requires threading widenCache
through the file-scoped resolver which is a larger change.
Code quality (findings #4, #5)
- Remove dead code: redundant conditional in resolveMemberCallByFile
where both branches returned null.
- Move WidenCache type declaration from mid-file (between JSDoc blocks)
to adjacent to CONSTRUCTOR_TARGET_TYPES with other type declarations.
Formatting
- Applied prettier to call-processor.ts (CI format check was failing).
Verification
- tsc --noEmit clean
- 3188 unit tests pass (0 skipped real tests)
- 1766 resolver integration tests pass
- Zero regressions — all PHP, Rust, and no-heritageMap tests green
Review: https://github.com/abhigyanpatwari/GitNexus/pull/770#issuecomment-4225312416
* fix(SM-19): restore module-alias narrowing and constructor disambiguation
Codex adversarial review on PR #770 surfaced two silent regressions in the
SM-19 thin dispatcher:
Finding 1 [high] — Typed member calls bypassed module-alias narrowing.
When two homonym receiver types are both imported by the caller, the
import-scoped tier no longer narrows and the owner/file resolvers see
genuine ambiguity. The dispatcher null-routed silently, dropping valid
CALLS edges. Fix: consult `resolveModuleAliasedCall` at the top of the
typed-member branch so an active alias on `call.receiverName` picks the
aliased file before the generic resolvers run.
Finding 2 [medium] — Constructor dispatch lost overload disambiguation.
When `resolveStaticCall` bails (ambiguous or ownerless Constructor pool)
and the caller supplied `overloadHints` / `preComputedArgTypes`, the
branch fell straight through to `singleCandidate` — which also bails on
multiple same-arity survivors. Fix: between `resolveStaticCall` and
`singleCandidate`, run constructor-filtered overload disambiguation on
the tiered pool. Only engages when a narrowing signal is present;
preserves SM-10 R3 null-route for genuinely ambiguous cases.
Tests:
- call-processor.test.ts: 3 new dispatcher-level regression tests
covering real-homonym alias narrowing, constructor overload
disambiguation with `argTypes`, and null-route control
- symbol-table.test.ts: update `module alias homonyms` test which
previously codified the Finding 1 regression; now asserts resolution
to the aliased file's method
Verification: 3191 unit + 2398 integration tests pass; tsc --noEmit
clean; prettier clean.
* refactor(SM-19): address code review findings with clean-code pass
Code review on commit f424685e surfaced one P1 correctness regression and
two P2 maintainability concerns. This commit closes all ten findings:
P1 — Alias helper placement regression
- resolveModuleAliasedCall now runs as a FALLBACK in the typed-member
branch, after resolveMemberCall/resolveMemberCallByFile return null.
Previously it short-circuited BEFORE scoped resolvers, leaking unrelated
homonyms from the aliased file when a local var coincidentally matched
a module alias.
- Added type-file verification guard: alias narrowing only fires when the
alias target file is among the receiver type's defining files. Prevents
cross-type false positives and hardens SM-10 R3.
P2 — Thin-dispatcher drift (roadmap Phase 3)
- Extracted disambiguateByOverloadOrArgTypes shared helper. Centralizes
the overloadHints → preComputedArgTypes precedence rule used by both
member and constructor resolvers.
- Folded constructor overload disambiguation into resolveStaticCall as
step 4.5 (between the ambiguous-pool bail and the instantiable-class
fallback). resolveStaticCall now accepts optional overloadHints /
preComputedArgTypes symmetric with resolveMemberCallByFile.
- Dispatcher's constructor branch returns to a 2-line delegation.
- resolveMemberCallByFile now calls the shared helper instead of inlining
the ternary.
P2 — Missing test coverage
- owner-scoped wins over alias narrowing (alias with unrelated target
class must not override unique owner-scoped answer)
- alias narrowing rejects unrelated target type (type-file guard)
- alias fallthrough: receiverName not in alias map
- alias fallthrough: alias target file has no matching method
(overloadHints-for-constructor variant transitively covered via the
extracted helper's member-path tests; direct dispatcher test deferred
as it requires real OverloadHints fixture parsing)
P3 — Clarity and durability
- Stripped "Codex SM-19 Finding N" prefixes from comments. Replaced with
durable explanations of WHY each guarded branch exists.
- Added cross-reference comment at the tail-branch resolveModuleAliasedCall
call site pointing to the typed-member branch usage.
Verification: 3195 unit + 1766 resolver integration + 2398 full integration
tests pass. tsc --noEmit clean. prettier clean.
Plan: docs/plans/2026-04-11-002-fix-sm19-code-review-findings-plan.md
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* fix: resolve false 404s and stale repo context during multi-repo switching on Windows
* test(e2e): add repo-switching tests — hold-queue 503, ?project= URL, Windows path normalization
* test(e2e): fix repo-switching specs — use live backend with ?server= param
* Initial plan
* Update test files for SymbolTable interface changes
Remove lookupFuzzy, lookupFuzzyCallable, globalIndex, and callableIndex
references from all test files. Replace lookupFuzzyCallable with
lookupCallableByName. Update getStats assertions to only expect
{ fileCount }. Remove tests that exclusively tested removed methods.
Files updated:
- symbol-table.test.ts: Remove lookupFuzzy describe block and all
globalIndex/callableIndex tests, update callable method references
- symbol-resolver.test.ts: Remove SM-16 lookupFuzzy test block,
update Tier 3 describe title
- type-env.test.ts: Update all mock SymbolTable objects and spy
variable names
- call-form.test.ts: Update ownerId propagation test
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(SM-18): Remove lookupFuzzy, lookupFuzzyCallable, globalIndex, callableIndex
Remove from SymbolTable interface and implementation:
- lookupFuzzy method
- lookupFuzzyCallable method
- globalIndex Map
- callableIndex Map (renamed to callableByName, backing lookupCallableByName)
Add lookupCallableByName as the targeted replacement for fuzzy callable
lookups. Migrate all production callers:
- resolution-context.ts: lookupFuzzyCallable → lookupCallableByName
- type-env.ts: lookupFuzzyCallable → lookupCallableByName
- call-processor.ts: lookupFuzzy → lookupCallableByName (D2 widen paths)
Remove fuzzyCallCount/fuzzyCallableCallCount stats and globalSymbolCount
from getStats(). Update pipeline.ts logging accordingly.
Memory savings: globalIndex stored every non-Property symbol (typically
the largest index by entry count). Removing it eliminates one Map plus
all its per-name arrays — net savings proportional to unique symbol
count in the project.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/4a658c69-41a9-4d57-8527-50ca544ca967
* fix(SM-18): address all PR #769 review findings
1. type-env.test.ts mock: add missing lookupImplByName + getFiles methods.
2. Macro/Delegate tests: 2 new tests confirm C/C++ Macro and C# Delegate
are indexed in callableByName.
3. D2 widen path test: module-alias scenario verifying lookupCallableByName
resolves methods in aliased files that shadow same-file definitions.
4. CALLABLE_TYPES unified: exported from symbol-table.ts (single source of
truth), imported in call-processor.ts. Removed duplicate
CALLABLE_SYMBOL_TYPES constant.
5. getStats() observability restored: tier hit counters (tierSameFile,
tierImportScoped, tierGlobal, tierMiss) replace the removed
fuzzyCallCount diagnostic.
* chore(SM-18): remove unnecessary `as any` casts on valid NodeLabel types
Macro, Delegate, TypeAlias, Const, and Variable are all valid NodeLabel
values in gitnexus-shared. The casts suppressed type checking without
purpose and signaled false uncertainty.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* Initial plan
* chore: initial plan for SM-16 resolveUncached refactor
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/0f505332-25be-46a7-b78e-fde58c1fc6fd
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* feat(SM-16): restructure resolveUncached — replace lookupFuzzy with targeted index lookups
- Remove single lookupFuzzy call that fed all Tier 2a/2b/3 in resolveUncached
- Tier 2a: iterate importedFiles with lookupExactAll per file (O(imports) × O(1))
- Tier 2b: iterate symbols.getFiles() filtered by isFileInPackageDir + lookupExactAll
(O(files) × O(1), avoids global name scan)
- Tier 3: replace with lookupClassByName + lookupImplByName + lookupFuzzyCallable
(three O(1) index lookups covering class-like, Rust impl blocks, and callables)
- Add getFiles() to SymbolTable interface (exposes fileIndex.keys() for Tier 2b)
- Add lookupImplByName() to SymbolTable — dedicated Rust Impl index kept separate
from classByName to preserve correct heritage-map resolution
- Remove allDefs parameter from walkBindingChain; always use lookupExactAll directly
- Add 29 new unit tests covering SM-16 changes and per-language fixtures
- fuzzyCallCount in getStats() is now 0 for all resolve() calls (acceptance criterion)
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/0f505332-25be-46a7-b78e-fde58c1fc6fd
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix(SM-16): clean up — readable Tier 3 if-else, correct doc comment, remove unused import
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/0f505332-25be-46a7-b78e-fde58c1fc6fd
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix(SM-16): address all PR #764 review findings
1. Eager callableIndex — maintained on add() like classByName/implByName,
removing the O(globalIndex) lazy rebuild on the Tier 3 hot path.
2. Tier 2b inverted index — packageDirSuffix→Set<filePath> built lazily on
first Tier 2b hit. Changes O(allFiles×packages) per resolution to
O(packages×filesInPackage).
3. Tier 3 type exclusion documented — TypeAlias, Const, Variable are
intentionally not reachable at Tier 3. 4 negative/positive tests added.
4. Tier 3 allocation guard simplified — single spread replaces 4-way if-else.
5. getFiles() live iterator documented with safety contract.
6. Remaining lookupFuzzy callers in call-processor.ts documented in the
Tier 3 comment block.
7. Tier 2b language fixtures — added Rust, Kotlin, PHP tests (3 new).
Also merges origin/main (SM-15 accumulator fixes).
* fix(SM-16): address Codex adversarial review — Tier 2b cache lifecycle + Macro/Delegate at Tier 3
1. Tier 2b packageDirIndex now invalidated in clearCache() and clear(),
preventing stale snapshots when symbols/packages are added between
chunk processing phases.
2. Macro (C/C++) and Delegate (C#) added to CALLABLE_TYPES in the eager
callableIndex, restoring Tier 3 reachability for these call targets
that the old lookupFuzzy returned.
* fix(SM-16): address ce:review findings — Tier 2b cache lifecycle + Tier 3 perf + test gaps
1. packageDirIndex no longer invalidated in clearCache() — the index
persists across file boundaries since packageMap and symbols are
append-only during the calls phase. Only clear() (pipeline reset)
invalidates. Prevents O(files×dirs) rebuild per-file.
2. Tier 3 short-circuit: return null before spread when all three
indexes are empty, avoiding allocation on the common miss path.
3. Add Macro (C/C++) and Delegate (C#) Tier 3 regression tests —
the only newly-added CALLABLE_TYPES were completely untested.
4. Add packageDirIndex invalidation regression test — verifies clear()
resets the index and newly-added symbols are visible.
* fix(SM-16): address final review — deduplicate NamedImportMap + doc fixes
1. NamedImportMap: removed duplicate definition from resolution-context.ts,
now imported directly from import-processor.ts (no re-export needed —
no consumers imported it from resolution-context).
2. packageDirIndex build cost documented accurately in comment.
3. fuzzyCallCount scope documented in test comment.
4. Tier 2a test suite: added comment about Go/Kotlin/PHP coverage.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* Initial plan
* Initial setup - Phase 9 BindingAccumulator cross-file return type wiring
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/7cee6490-090d-4714-8cb5-a704168ff47a
* feat(SM-15): wire BindingAccumulator into processCallsFromExtracted for Phase 9 cross-file return type propagation
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/7cee6490-090d-4714-8cb5-a704168ff47a
* fix(SM-15): address all PR #763 review findings
Performance (R1)
- Changed _fileScopeByFile from Map<string, [string,string][]> to
Map<string, Map<string,string>>. fileScopeGet(filePath, name) is
now O(1) — replaces the O(n) linear scan + defensive-copy alloc
that ran once per ConstructorBinding entry. fileScopeEntries()
reconstructs tuples from Map.entries() for backward compat.
- Updated finalize() dev-mode invariant to compare deduplicated Map
size rather than raw array length (Map.set deduplicates same-name).
Lifecycle (R2)
- Documented that Phase 9 intentionally reads pre-finalize because
finalize() cannot move before both the worker consumer (line 984)
AND the sequential-path writer (line 1061). Pre-finalize reads are
safe because finalize() is write-lock-only with no side effects.
Replaced the ambiguous "populated but not yet finalized" comment
with the full lifecycle ordering explanation.
Sequential-path parity (R3)
- Wired bindingAccumulator into processCalls at line 797 (sequential
path) so verifyConstructorBindings gets the Phase 9 fallback.
- Added bindingAccumulator parameter to processAssignmentsFromExtracted
signature and wired it at the pipeline.ts call site (line 1026).
- Both paths now produce identical Phase 9 behavior for the same code.
Tracking comments (R4)
- Added "Overlapping mechanism (N of 3)" cross-references at:
1. buildImportedReturnTypes (~line 109)
2. collectExportedBindings (~line 168)
3. Phase 9 fallback in verifyConstructorBindings (~line 563)
Each links to the other two and notes future unification.
Language coverage (R5)
- Added 5 new Phase 9 integration test suites in cross-file-binding.test.ts:
JavaScript, C++, C#, PHP, Ruby. Each uses the existing fixture
directories and asserts getUser() → User → user.save() resolves.
Total cross-file binding tests: 52 (was 37).
Quality asymmetry (R6)
- Added inline comment at the Phase 9 fallback noting worker-path
entries are Tier 0/1 only and that binding accuracy is structurally
lower for large repos where the worker path dominates.
Tests (+21 new)
- 6 fileScopeGet unit tests (happy path, unknown file/name, mixed
scopes, post-dispose, duplicate varName last-write-wins)
- 15 integration tests across 5 new language suites
Verification
- tsc --noEmit clean
- 3147 unit tests pass (+6 new)
- 52 cross-file binding integration tests pass (+15 new)
- 1766 resolver integration tests pass
- Zero regressions
Plan: docs/plans/2026-04-10-001-fix-sm15-review-findings-plan.md
Review: https://github.com/abhigyanpatwari/GitNexus/pull/763#issuecomment-4220354242
* fix(SM-15): gate accumulator fallback on resolution tier and fix sequential file-order dependency
Two Codex adversarial reviews identified medium-severity bugs in the Phase 9
BindingAccumulator fallback:
1. Local-first violation: the fallback fired regardless of whether ctx.resolve()
found same-file candidates, letting an imported callee shadow a local one
and produce false CALLS edges. Fixed by gating on tiered.tier !== 'same-file'
and callableDefs.length <= 1.
2. Sequential file-order dependency: processCalls flushed and verified per-file,
so consumer files processed before their providers missed accumulator bindings.
Fixed by splitting into a flush pre-pass (all files) then a resolution loop,
mirroring the worker path's "all appends before any reads" pattern.
Also adds 11 consumer-before-provider integration test fixtures (one per
supported language) and 4 unit tests for tier gating edge cases.
* refactor(SM-15): eliminate duplicated prepare logic in processCalls two-pass split
Replace the duplicated pre-pass + legacy-path code (parse → query → heritage
→ TypeEnv → exports) with a single preparation loop followed by a resolution
loop. Both paths now share the same preparation code — the only conditional
is the accumulator flush.
Side benefit: globalParentMap is now fully populated before any resolution
runs, improving cross-file isSubclassOf accuracy regardless of file order.
Net -118 lines (226 removed, 108 added).
* fix(SM-15): address PR #763 third-pass review findings
1. Update stale dispose() JSDoc — remove forward-reference to Phase 9
wiring that is now complete; document actual consumers.
2. Add processAssignmentsFromExtracted Phase 9 unit test — verifies the
accumulator fallback produces ACCESSES write edges when the SymbolTable
has no returnType for the callee.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* Initial plan
* feat(SM-13): extract resolveFreeCall from resolveCallTarget
Extract the free-function call resolution path into a dedicated
`resolveFreeCall(calledName, filePath, ctx)` function that uses
`lookupExact` + import-scoped resolution via `ctx.resolve()`.
- Free function calls (foo()) now route through `resolveFreeCall`
- Swift/Kotlin implicit constructors (User()) delegate to
`resolveStaticCall` within `resolveFreeCall`
- `resolveCallTarget` dispatches `callForm === 'free'` early,
removing the inline freeFormHasClassTarget logic
- S0 block simplified to only handle `callForm === 'constructor'`
- Global (Tier 3) fallthrough preserved via ctx.resolve() until Phase 5
- 9 new unit tests for resolveFreeCall
- All 163 unit tests pass, all 1199 integration resolver tests pass
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c5f2e73a-259a-438c-b5c8-286b82e3c215
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* chore: revert unrelated package-lock.json change
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c5f2e73a-259a-438c-b5c8-286b82e3c215
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix(SM-13): address PR #756 review findings on resolveFreeCall
Addresses all 7 findings from the PR #756 review comment.
Code (R1, finding #1)
- Replace the literal `'Class' | 'Struct' | 'Record'` check in
`hasClassTarget` with `INSTANTIABLE_CLASS_TYPES.has(c.type)`. Converts
an invariant that was previously comment-enforced ("keep this list
aligned with INSTANTIABLE_CLASS_TYPES") into one enforced structurally.
Any future extension of the set propagates here automatically. The
narrower Swift extension dedup block below still uses literal
`'Class' | 'Struct'` by design — Swift extensions only produce Class
duplicates in practice, Record is deliberately excluded there, and
the inline comment now documents that asymmetry.
Tests (+12 regression scenarios)
Finding #2 — language coverage
- Go free function (doStuff())
- Python free function (def helper(): ... helper())
- Rust free function outside any impl block
- Java statically-imported function
- JavaScript module-level function
Each exercises `_resolveCallTargetForTesting` with `callForm='free'`
and the language-specific file extension. `resolveFreeCall` has no
file-extension branching, so these guard the dispatch chain per
language without assuming extractor-specific symbol shapes.
Finding #3 — argCount threading
- 2-arg overload selected when argCount=2
- 0-arg overload selected when argCount=0
Finding #5 — Tier 3 (global) resolution
- Function globally visible but not imported. Asserts exact
`TIER_CONFIDENCE.global === 0.5` and `reason === 'global'` to catch
silent drift if the tier table is ever refactored.
Finding #6 — preComputedArgTypes worker path
- String overload matched via preComputedArgTypes=['String']
- Int overload matched via preComputedArgTypes=['int'] (lowercase,
mirroring the parse-worker's inferred-literal shape; stored 'Int' is
normalized via normalizeJvmTypeName at comparison time)
Finding #7 — Enum null-route documentation
- Enum-only free call asserts `toBeNull()` with an explanatory comment
linking to the INSTANTIABLE_CLASS_TYPES rationale. NOT marked skipped
— current behavior is intentional, not broken.
Finding #4 — Swift extension dedup guard
- Two same-name Class entries at different path lengths; exercises the
full dispatch chain:
1. filterCallableCandidates with 'free' strips Class → length 0
2. hasClassTarget triggers resolveStaticCall
3. Homonym ambiguity null-routes per SM-12 round-1 contract
4. Constructor-form retry repopulates with both Classes
5. Dedup block sorts by filePath.length → shortest path wins
Verification
- `tsc --noEmit` clean
- 3064 unit tests pass (+12)
- 1766 integration tests pass
- Zero regressions
Plan: docs/plans/2026-04-09-003-fix-sm13-resolve-free-call-review-findings-plan.md
Review: https://github.com/abhigyanpatwari/GitNexus/pull/756#issuecomment-4213879002
* refactor(SM-13): extract dedupSwiftExtensionCandidates shared helper
Follow-up to the PR #756 review fix. SM-13 duplicated the Swift
extension same-name collision dedup block between `resolveCallTarget`
and `resolveFreeCall` — two copies of identical 15-line logic with the
same heuristic (`filePath.length` sort, Class/Struct-only, `length > 1`
guard). Extract a single shared helper so the two sites cannot drift.
Changes
- New `dedupSwiftExtensionCandidates(candidates, tier)` helper defined
alongside `tryOverloadDisambiguation`, with JSDoc documenting:
- The Swift extension scenario it addresses
- Why it is intentionally narrower than INSTANTIABLE_CLASS_TYPES
(Class/Struct only, not Record — C#/Kotlin records don't exhibit
the multi-file definition pattern, widening risks accidental
dedup of legitimately distinct record types)
- The return-null-on-no-match contract so callers can fall through
- `resolveCallTarget` tail dedup (was lines 1593-1610): replaced with
a single `dedupSwiftExtensionCandidates` call
- `resolveFreeCall` tail dedup (was lines 1994-2012): same replacement
- Net line count: -32 insertions, -9 deletions in the consumer sites,
+36 for the shared helper + JSDoc
Verification
- `tsc --noEmit` clean
- 3064 unit tests pass (including the R7 Swift dedup guard test added
in the previous commit that exercises the full free-form retry
chain through this helper)
- 1766 integration tests pass
- Zero regressions
Follows-up on: https://github.com/abhigyanpatwari/GitNexus/pull/756
* docs(SM-13): address PR #756 final review — comment cleanup only
Three documentation-only findings from the approval review. No
behavior change, no new tests, no code path modifications.
Finding #1 — stale line-number comment
- The comment inside `resolveFreeCall` at the `hasClassTarget` site
referenced "lines ~1994-2008" for the Swift extension dedup block.
Those lines were the inlined pre-SM-13 version; the block has since
been extracted to `dedupSwiftExtensionCandidates`. Replaced the line
reference with the helper name so future readers don't chase dead
line numbers.
Finding #2 — fuzzy-widening asymmetry undocumented
- `resolveFreeCall` intentionally has no `widenCache` parameter and no
D2 fuzzy-widening pass (unlike `resolveCallTarget`'s member-call
path). Added an explicit "Asymmetry vs `resolveCallTarget`" paragraph
to the JSDoc so a caller comparing the two signatures knows the
skipped pass is deliberate and tied to Phase 5.
Finding #3 — constructor-form retry reasons undocumented
- `resolveStaticCall` can return null for three distinct reasons
(empty instantiable pool, homonym ambiguity, ownerless Constructor
nodes). The retry below it unconditionally re-filters with
`'constructor'` form, which is correct for all three but not
obvious. Added a structured three-case comment enumerating each
reason and linking (a) to the SM-12 null-route contract, (b) to
the R7 dedup test, and (c) to the currently-uncovered ownerless-
Constructor path (noted as a future test candidate).
Verification
- `tsc --noEmit` clean
- 175 `resolveFreeCall` + `resolveStaticCall` + sibling tests pass
(sanity check — no behavior change expected)
- No regressions
Follows-up on: https://github.com/abhigyanpatwari/GitNexus/pull/756#issuecomment-4215739052
* test(SM-13): cover ownerless-Constructor retry + PHP free function
Two low-severity test gaps from PR #756 review comment 4215739052 —
previously addressed doc-only, now have concrete test coverage.
Finding #3 low — ownerless-Constructor retry path (previously comment-only)
- The retry after resolveStaticCall returns null handles three distinct
null-return reasons. Cases (a) and (b) were already tested (Interface/
Trait null-route from SM-12, Swift shadowing dedup from R7). Case (c) —
resolveStaticCall step-4 bailout when the tiered pool contains
ownerless Constructor nodes — was only covered by a comment.
- New test: Class + ownerless Constructor in tiered pool, callForm='free'.
Exercises the full chain:
1. resolveStaticCall step 3 walks classCandidates via
lookupMethodByOwner — ownerless Constructor not in methodByOwner,
nothing found.
2. Step 4 detects Constructor in tiered pool, bails with null.
3. resolveFreeCall retry re-runs filterCallableCandidates with
'constructor' form, which prefers Constructor over Class per
CONSTRUCTOR_TARGET_TYPES ordering.
4. Single survivor returned.
- Asserts the Constructor node (not the Class) is the resolved target.
Low — PHP free function coverage gap
- The language coverage table in the same review flagged PHP free
functions (top-level `function helper()` outside any class) as
uncovered. Added a test mirroring the existing Go/Python/Rust/Java/
JS language tests — exercises the `.php` dispatch path for free
calls. Ruby and C/C++ remain uncovered; deferred to a future round
since those languages also have other gaps in the broader test file.
Verification
- `tsc --noEmit` clean
- 3066 unit tests pass (+2 new regression tests)
- 1766 integration tests pass
- Zero regressions
Follows-up on: https://github.com/abhigyanpatwari/GitNexus/pull/756#issuecomment-4215739052
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* Initial plan
* feat(SM-12): extract resolveStaticCall from resolveCallTarget
- Add resolveStaticCall(className, methodName, currentFile, ctx, argCount?) using
lookupClassByName + lookupMethodByOwner for O(1) constructor/static resolution
- Add S0 fast path in resolveCallTarget for constructor/free-form class calls
- Export resolveStaticCall from call-processor.ts
- Add 11 unit tests covering constructor resolution, confidence tiers,
arity disambiguation, and resolveCallTarget delegation
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c9471ca9-57ff-4dae-956e-e7ffdc326bc4
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* chore: revert unrelated package-lock.json change
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c9471ca9-57ff-4dae-956e-e7ffdc326bc4
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* refactor: shorten verbose test name per code review feedback
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c9471ca9-57ff-4dae-956e-e7ffdc326bc4
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix(SM-12): address PR #754 review findings
Addresses Claude's review comments on PR #754:
Performance
- Pass pre-computed `tiered` result into `resolveStaticCall` as optional
`tieredOverride` parameter, eliminating the duplicate `ctx.resolve(className,
currentFile)` on every constructor call path.
- Cache `freeFormHasClassTarget` in `resolveCallTarget` so the S0 fast path
and the free-form constructor retry share a single `.some()` scan.
Architecture
- Reconcile `CLASS_LIKE_TYPES` (call-processor) with `CLASS_TYPES`
(symbol-table): `CLASS_LIKE_TYPES = [...CLASS_TYPES, 'Impl']`. This makes
the relationship explicit — the call resolver's set is a strict superset
of the heritage-index set, guaranteeing anything reachable via
`lookupClassByName` also passes the resolver filter. Trait is now included
(harmless: traits have no Constructor nodes, so step-3 returns undefined
and step-5 still returns the class-like node when unique). Documented
the Interface inclusion rationale (static methods + MRO walker).
- Collapse `resolveStaticCall`'s `methodName` parameter into `className` —
all call sites passed identical values. Named constructors (Dart
`User.fromJson()`) arrive as member calls and go through
`resolveMemberCall`. Documented the reserved path for when a language
surfaces a static-method-shaped call with a distinct member name.
- Document the known gap: `callForm === 'member'` constructor patterns
(e.g. Python `models.User()`) are handled by the tail fallback, not S0.
Tests
- Add tiered-override test asserting `ctx.resolve` is not re-invoked when
a pre-computed result is passed in.
- Add language-specific `_resolveCallTargetForTesting` integration tests
for Java (`new User()`), Python (`User()`), and Kotlin (`User()`).
Verification: 3031 unit + 1766 integration tests pass, zero regressions.
* fix(SM-12): restrict resolveStaticCall fallback to instantiable kinds
Addresses the high-severity finding from the Codex adversarial review of
PR #754: `resolveStaticCall`'s step-5 "return the class itself when no
Constructor node is found" fallback reused `CLASS_LIKE_TYPES`, which —
after SM-11 and PR #754's reconciliation — now includes `Interface`,
`Trait`, and `Impl`. That is the method-dispatch set, not the
instantiable set, so constructor-shaped calls could resolve to
non-instantiable nodes and emit false `CALLS` edges.
Concrete failure: Rust same-file `impl User { ... }` alongside
`struct User { ... }` — both land at same-file tier, the Impl is not
filtered out, and the step-5 fallback produces a `CALLS` edge to the
`Impl` block instead of the `Struct`. The same widening exposed
Interface / Trait targets in Java / C# / PHP / Scala.
Fix
- Introduce `INSTANTIABLE_CLASS_TYPES = {'Class', 'Struct', 'Record'}`
as a sibling to `CLASS_LIKE_TYPES`, documenting the contract
explicitly and cross-referencing `CONSTRUCTOR_TARGET_TYPES`.
- Update `CLASS_LIKE_TYPES` JSDoc to clarify it is the method-dispatch
set and add an anti-pattern warning against reusing it for
constructor-fallback filtering.
- Tighten `resolveStaticCall` step 5: filter `classCandidates` through
`INSTANTIABLE_CLASS_TYPES` before the `length === 1` check. This
strips `Impl` from the Rust shadowing scenario (leaving `Struct` as
the sole instantiable target) and null-routes Interface / Trait /
`Impl`-alone scenarios, matching the SM-10 R3 null-route precedent.
- Step 3 (explicit Constructor lookup via `lookupMethodByOwner`) is
intentionally unchanged — its `def.type === 'Constructor'` check is
the correct contract, and legitimate Constructor nodes attached to
`Impl` owners still resolve correctly.
Tests (+10 regression scenarios)
- Positive guards: Struct, Record fallback paths.
- Null-route: Interface (Java/C#/TS), PHP Trait, Rust Trait.
- Rust same-file shadowing: Struct wins over Impl.
- Rust Impl-alone: null-routes (no Struct present).
- Step-3 preservation: Constructor owned by Impl still resolves to the
Constructor node, proving step-5 tightening doesn't leak into step 3.
- Full cascade via `_resolveCallTargetForTesting` for Interface and
Trait — confirms no downstream path silently re-introduces the edge.
Verification
- `tsc --noEmit` clean
- 3041 unit tests pass (+10)
- 1766 integration tests pass
- Zero regressions
Plan: docs/plans/2026-04-09-002-fix-sm12-constructor-fallback-instantiable-only-plan.md
Codex review job: review-mnrao7fr-nv9y0e
* fix(SM-12): address PR #754 second review round
Addresses the 9 findings from the follow-up review on PR #754.
Performance
- Align `freeFormHasClassTarget` with `INSTANTIABLE_CLASS_TYPES`: drop
`Enum` (S0 would always return null for it — wasted lookup work) and
add `Record` (C# records and Kotlin data classes were bypassing S0
entirely). The trigger set and the fallback filter set now agree by
construction, documented inline.
Documentation
- Remove stale single-line JSDoc on `CLASS_LIKE_TYPES` (line 57) that
duplicated the full multi-line block immediately below it — tooling
picks up the first block so the old one-liner was shadowing the
current explanation.
- Rewrite the `resolveStaticCall` JSDoc step list to match the actual
step boundaries in the implementation (steps 3, 4, 5 were blurred in
the old description).
- Add inline comment on step 3 documenting the same-name lookup
assumption (`${candidate.nodeId}\0${className}`) and the symmetric
miss case for Python `__init__`-style constructors.
- Add inline comment on step 4 documenting that it also catches the
ambiguous-step-3 case, and warning against removing the check
without handling that path explicitly.
- Add inline comment on step 5 enumerating the three length outcomes
(0 / 1 / >1) so future readers see the dominant null-route case.
- Document Ruby `User.new` as a known gap alongside Python
`models.User()` in the S0 header comment.
Tests (+2 scenarios)
- Record free-form constructor call via `_resolveCallTargetForTesting`
exercises the aligned `freeFormHasClassTarget` trigger end-to-end,
closing the gap where the direct `resolveStaticCall` test passed
but the integration path was silently bypassing S0.
- Arity threading via `_resolveCallTargetForTesting` asserts that
`call.argCount` flows through resolveCallTarget → S0 →
resolveStaticCall → lookupMethodByOwner, catching any future
regression where the argCount is dropped at the S0 call site.
Verification
- `tsc --noEmit` clean
- 3043 unit tests pass (+2)
- 1766 integration tests pass
- Zero regressions
Plan: docs/plans/2026-04-09-002-fix-sm12-constructor-fallback-instantiable-only-plan.md
Review: https://github.com/abhigyanpatwari/GitNexus/pull/754#issuecomment-4213536094
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* Initial plan
* feat(SM-11): extract resolveMemberCall from resolveCallTarget
- Create resolveMemberCall(ownerType, methodName, currentFile, ctx, heritageMap?)
that uses owner-scoped + MRO resolution only (no fuzzy lookup)
- resolveCallTarget delegates member calls (D0 path) to resolveMemberCall
- walkMixedChain uses resolveMemberCall for owner-scoped member-call resolution
- Add 7 unit tests for resolveMemberCall covering direct, inherited, MRO,
null cases, and confidence tier assertions
- Export resolveMemberCall for external use
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3b7889a9-5f2f-4572-8904-45084210f10d
* fix(SM-11): address PR #744 review
Blocking fixes:
- B1: Revert unrelated package-lock.json gitnexus-shared addition
- B2: Document confidence-tier semantic change on resolveMemberCall
Performance / coupling fixes:
- S1: walkMixedChain now calls resolveMethodByOwner directly (hot path) to avoid throwaway ResolveResult allocation per chain step
- S2: Thread tier from resolveMethodByOwner via { def, tier } tuple; eliminates double ctx.resolve
Alignment with semantic-model plan (Phase 3 target):
- resolveMethodByOwner now iterates ALL class-like candidates from ctx.resolve, deduplicating matches by nodeId. Absorbs D4's ownerId-filtering into the owner-scoped path.
- Handles homonym classes (two Users in different files) without falling through to D1-D4 fuzzy widening
- Shared-ancestor MRO walks automatically dedup (both homonyms walk to same base method)
- Unified direct-vs-MRO lookup under a single canWalkMRO check
Tests added:
- T1: Three D0 skip-condition tests via new _resolveCallTargetForTesting internal export (overloadHints, preComputedArgTypes, hasActiveModuleAlias)
- T2: Rust qualified-syntax null test (trait-inherited method) + direct impl control
- T3: C++ leftmost-base diamond inheritance test
- B2 lock-in: cross-file class tier assertion
- Homonym disambiguation: only-one-owns-method, both-own-method ambiguity, shared-ancestor MRO convergence
Verification:
- tsc --noEmit: clean
- vitest run test/unit/: 3014 passed
- vitest run test/integration/resolvers/: 1746 passed
* test(SM-11): address second PR #744 review round + per-language integration tests
Review fixes (https://github.com/abhigyanpatwari/GitNexus/pull/744#issuecomment-4211877593):
P1 (Performance): Replace Map allocation in resolveMethodByOwner with a firstDef+ambiguous flag pattern. Zero allocation for the common single-candidate case on the hot path — the previous Map approach allocated on every member call regardless of whether deduplication was needed.
P2 (Test gap): Strengthen the module-alias D0 skip test with a homonym fixture (two Users in different files). Previously the test passed whether or not D0 was actually bypassed; the new version proves D0 must be skipped by showing that resolveMemberCall directly returns null (ambiguous) but D1-D4 with alias narrowing picks the right one. Also fixes the underlying D2-vs-alias widening interaction: when filteredCandidates was narrowed by module-alias disambiguation, D2 no longer widens back to the full fuzzy pool (introduces aliasNarrowed boolean flag).
L1 (Language coverage): Add C# and Kotlin implements-split tests at the resolveMemberCall layer.
L2 (Maintainability): Export OverloadHints as @internal so the test can use a direct cast instead of fragile Parameters<...> type inference.
Per-language integration tests:
- rust-child-extends-parent: Direct impl method resolution via D0 (with honest documentation of the trait-method-as-Function gap that is Phase 5 / SM-16 scope)
- java-interface-default-method: User implements Validator with default method resolved via implements-split MRO
- csharp-interface-default-method: Same pattern for C# 8.0+ default interface methods
- kotlin-interface-default-method: Same pattern for Kotlin interfaces with default implementations
- python-multi-level-mro: 3-level C3 linearization (Grandparent ← Parent ← Child)
- cpp-diamond-inheritance: Classic diamond (Base ← A, B ← Derived) via leftmost-base MRO
Verification:
- tsc --noEmit: clean
- vitest run test/unit/: 3015 passed
- vitest run test/integration/resolvers/: 1763 passed (+17 new per-language tests)
* fix(SM-11): Codex adversarial review corrections + deeper D0 fixes
Addresses the three high-severity findings from the Codex adversarial review of PR #744 (https://github.com/abhigyanpatwari/GitNexus/pull/744#issuecomment-4212075120), plus four deeper fixes discovered during regression triage. All discovered issues are now addressed end-to-end rather than papered over with tail-return fallbacks.
Codex review findings:
R1 (C++ diamond): The cpp-diamond-inheritance fixture used non-virtual inheritance, which is genuinely ambiguous in real C++ (two Base subobjects). Changed A and B to use 'virtual public Base' so there's a single shared Base subobject and d.method() is an unambiguous call that the leftmost-base MRO walk correctly resolves.
R2 (C# default-interface): The csharp-interface-default-method fixture called user.Validate() via a User-typed variable, but C# does not inherit default interface methods as callable class members — the call is only valid through an interface-typed variable. Changed App.cs to 'IValidator user = new User(...)' which is the idiomatic dispatch pattern.
R3 (resolveCallTarget tail-return): When D1-D4 receiver filtering produced zero file-matched and zero owner-matched candidates for a member call, the function fell through to the permissive single-candidate tail return — silently emitting CALLS edges for methods that don't belong to the receiver. Added an explicit null-route inside the D1-D4 block that fires only when both filters yielded 0.
R4 (Rust negative assertion): Added the c.trait_only() negative integration test in rust.test.ts demonstrating that direct member calls on Rust structs do not walk trait ancestry. The test now passes because of R3 (previously fell through to the tail return).
Regression triage discoveries:
1. D0 was dead code on the sequential pipeline. The sequential path sets overloadHints for every call regardless of whether the method is overloaded, and the original D0 skip condition '!overloadHints && !preComputedArgTypes' was therefore always false. The Java/C#/C++ SM-9/SM-10 inheritance tests were passing ONLY via the tail-return fallback. Fix: narrow the skip to 'overloadHints && filteredCandidates.length > 1' — skip D0 only when there are actually multiple candidates that need overload disambiguation.
2. lookupMethodByOwner couldn't disambiguate arity-differing overloads (e.g. C++ greet() vs greet(string)). With D0 now firing on the sequential path, same-name/different-arity overloads would collapse to an arbitrary first pick. Fix: added an optional argCount parameter to lookupMethodByOwner + lookupMethodByOwnerWithMRO that filters the overload set by parameterCount/requiredParameterCount before the returnType dedup.
3. Python and Rust class methods are captured as Function nodes (not Method) with ownerId set to the class. The methodByOwner index only accepted 'Method' and 'Constructor' types, so Python class methods and Rust trait methods were invisible to D0. Fix: extended the methodByOwner indexing condition to include 'Function' when ownerId is set. This also unlocks the Rust trait-method negative assertion by ensuring the qualified-syntax MRO strategy has something to return null for.
4. D0 was being skipped when a local variable shadowed an imported module name (Python 'from models.c import C; c = C()' creates both a module alias 'c → models/c.py' AND a typed local 'c'). Fix: the D0 skip now gates on 'aliasNarrowed' (a new boolean tracking whether the alias block actually narrowed filteredCandidates) instead of 'hasActiveModuleAlias'. If the method isn't in the aliased module, the receiver is a typed local variable and D0 should run.
5. PHP trait walk missed the HasTimestamps trait because lookupClassByName did not include 'Trait' type. buildHeritageMap uses lookupClassByName to resolve parent names, so 'BaseModel use HasTimestamps' was failing to register an ancestor edge for BaseModel → HasTimestamps. Fix: added 'Trait' to CLASS_TYPES. The trait is now a valid class-like type for heritage resolution (PHP use, Rust impl Trait for Struct, Scala traits).
Test updates:
- Updated the 'no heritageMap' unit test in call-processor.test.ts to assert the correct null-route behavior instead of the old tail-return fallback.
- Added a new unit test asserting Trait inclusion in the class set.
- Updated the 'does NOT include other type-like labels' test to remove Trait from its rejection set.
Verification:
- tsc --noEmit: clean
- vitest run test/unit/: 3016 passed (+1 new Trait inclusion test)
- vitest run test/integration/resolvers/: 1764 passed (+1 new Rust negative assertion)
- Zero regressions
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergo Magyar <magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* Initial plan
* Add MRO fast path before D2 fuzzy widening in resolveCallTarget
When receiverTypeName is known, try resolveMethodByOwner (owner-scoped
+ MRO lookup) before falling back to the expensive lookupFuzzy in D2.
This short-circuits cross-file member call resolution for the common
non-overloaded case.
The fast path is skipped when overload disambiguation hints are
available (overloadHints or preComputedArgTypes) to avoid picking the
wrong overload for same-return-type overloaded methods.
Passes heritageMap to resolveCallTarget from all 4 call sites:
- Language seed path (processCalls)
- Sequential path (processCalls)
- walkMixedChain fallback
- Worker path (processCallsFromExtracted)
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9e49521f-2472-47bc-96e9-be4a46b073f0
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix(SM-10): address PR #741 review
Correctness:
- Module-alias guard for D0. When call.receiverName matches an active
entry in ctx.moduleAliasMap for the current file, D0 is now skipped
and resolution falls through to D1-D4 which respects the
alias-narrowed candidate pool. Prevents a homonymous class in a
different file from being picked by ctx.resolve(receiverTypeName)
inside resolveMethodByOwner. New unit test pins the contract.
Unit tests (call-processor.test.ts — 3 new):
- D0 hit: child.parentMethod() resolves via MRO walk when
heritageMap is provided.
- D0 skipped: same scenario still resolves via D1-D4 when heritageMap
is undefined (backward-compat guard).
- Module-alias guard: two files both define class User with a save()
method; 'import auth_mod as auth' in app.py must resolve
auth.user.save() to auth_mod.py, not user_mod.py.
Integration language coverage (+3 fixtures/tests):
- swift-child-extends-parent — first-wins, gated on swiftAvailable.
- ruby-child-extends-parent — first-wins.
- php-child-extends-parent — first-wins (uses ParentClass since
'Parent' is a PHP reserved word).
* test(SM-10): address second PR #741 review round
Unit tests (call-processor.test.ts, +2 new):
- overloadHints guard: Java source with two same-return-type overloads
method(int) and method(String), int added first so lookupMethodByOwner
would return it. processCalls auto-generates overloadHints for Java,
forcing D0 to be skipped. o.method("hello") must resolve to
method(String) via literal-inferred disambiguation.
- preComputedArgTypes guard: worker-path equivalent via
processCallsFromExtracted with ExtractedCall.argTypes=['String'].
Same two overloads, same correctness guarantee.
Integration tests (+2 fixtures + test blocks):
- go-child-extends-parent — struct embedding, first-wins
(Go structs are labeled 'Struct' not 'Class' in GitNexus).
- dart-child-extends-parent — extends, first-wins, gated on
dartAvailable like other Dart tests.
Documentation:
- Expanded the fallthrough comment in resolveMethodByOwner to clarify
that unknown-extension paths land on plain lookupMethodByOwner
without an ancestor walk, and that D1-D4 still runs on D0 miss.
* test(SM-10): D0 miss with heritageMap present falls through to D1-D4
Closes the last remaining gap from PR #741 review round 3. The existing
'D0 skipped' test only covered the heritageMap=undefined case, leaving
the miss-with-heritageMap path implicitly covered by integration tests
only. This adds a focused unit test where:
- Class Obj has a method doWork findable via tiered resolution
(import-scoped) but intentionally NOT registered in methodByOwner
(no ownerId), so lookupMethodByOwner misses.
- heritageMap is provided but built from an empty heritage array, so
getAncestors(class:Obj) returns []. The MRO walk yields no parents.
- lookupMethodByOwnerWithMRO therefore returns undefined → D0 miss.
- D1 resolves the receiver type; D2 widens via lookupFuzzy;
D3 file-filter picks the single matching candidate.
- A CALLS edge must still be emitted — D0 miss must not swallow
the call.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* Initial plan
* feat(SM-9): add lookupMethodByOwnerWithMRO with HeritageMap parent chain walking
- Export c3Linearize from mro-processor.ts for reuse
- Add lookupMethodByOwnerWithMRO in call-processor.ts with MRO strategy support
- Update resolveMethodByOwner to fall back to MRO walk when HeritageMap available
- Thread heritageMap through walkMixedChain for chain resolution
- Add 10 unit tests covering all acceptance criteria
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/cc58249b-42f1-45a9-89fb-e3917e4d0171
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* feat(SM-9): add Java integration test with class Child extends Parent fixture
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/cc58249b-42f1-45a9-89fb-e3917e4d0171
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* docs: address code review comments on MRO strategy documentation
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/cc58249b-42f1-45a9-89fb-e3917e4d0171
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* perf(SM-9): address PR #740 review comments
- Eliminate double direct lookup in resolveMethodByOwner: delegate
straight to lookupMethodByOwnerWithMRO when a HeritageMap is
available (the MRO helper already does the direct lookup before
walking ancestors). Fallback path handles the no-HeritageMap case.
- Memoize C3 linearization per HeritageMap via a WeakMap keyed cache.
HeritageMap is immutable after build, so C3 results are stable for
its lifetime; WeakMap lets the cache auto-drain when the HeritageMap
is GC'd. Null sentinel caches linearization failures so cyclic
hierarchies are not reprocessed. Eliminates per-call buildParentMap +
c3Linearize on Python codebases.
- ancestors variable typed as readonly to accept the cached result
without copying.
- Add four missing MRO unit tests: Kotlin implements-split, C#
implements-split, JavaScript first-wins (separate provider from TS),
and C++ leftmost-base diamond (first diamond test for C++).
* fix(SM-9): CI prettier + address PR #740 follow-up review
- Fix CI prettier failure in test/integration/resolvers/java.test.ts
(auto-formatted — was introduced in 37563a31 before my first fix
commit but had not been caught locally).
- Pin caller on the SM-9 Java integration test (parentMethodCall.source
=== 'run') so a regression that misattributes the CALLS edge fails.
- Add two implements-split unit tests:
* Ambiguous default from two interfaces → BFS first-wins. Pins the
contract that lookupMethodByOwnerWithMRO returns a defined result
(full ambiguity detection is deferred to computeMRO graph pass).
* Class method precedence over interface default: Child extends Base
implements IFoo where both define handle() — documents that BFS
visits the extends edge first, matching Java's class-wins rule.
- Add @internal JSDoc on lookupMethodByOwnerWithMRO clarifying it is
exported only for testing; resolveMethodByOwner is the proper entry
point for callers.
* test(SM-9): per-language integration fixtures and tests for inherited method resolution
Extends the SM-9 integration coverage beyond Java with six new
child-extends-parent fixtures, one per MRO strategy:
- python-child-extends-parent → C3 strategy
- typescript-child-extends-parent → first-wins
- javascript-child-extends-parent → first-wins (separate provider)
- kotlin-child-extends-parent → implements-split
- csharp-child-extends-parent → implements-split
- cpp-child-extends-parent → leftmost-base
Each fixture follows the java-child-extends-parent pattern:
- Parent class with a single method
- Child class extending Parent, no override
- App class/function that instantiates Child and calls
the parent method — exercises the full ingestion pipeline,
HeritageMap construction, and lookupMethodByOwnerWithMRO walk.
For every fixture the matching integration test asserts:
- Parent and Child classes are detected
- Child → Parent EXTENDS edge is emitted
- The parent-method call resolves to the correct target file
- The caller is pinned (source === 'run' / 'Run') to catch
edge misattribution regressions
Rust is intentionally omitted — its qualified-syntax strategy
returns undefined from lookupMethodByOwnerWithMRO by design, so
there is no inherited-method resolution to assert against.
All 1739 integration resolver tests pass (+18 new SM-9 tests
across 6 languages).
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* Initial plan
* feat(SM-8): add HeritageMap with MRO-aware parent/ancestor lookup
- New heritage-map.ts: HeritageMap interface with getParents() and getAncestors()
- buildHeritageMap() consumes ExtractedHeritage[], resolves names via lookupClassByName
- Cycle protection and bounded depth (MAX_ANCESTOR_DEPTH=32) in getAncestors
- Worker path: HeritageMap built from deferredWorkerHeritage, threaded into processCallsFromExtracted
- Sequential path: Heritage accumulated across chunks, HeritageMap built after all chunks, passed to processCalls
- 18 unit tests covering parent lookup, multi-level, diamond, cycles, missing parent, bounded depth
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c413e0a3-5d63-4ddb-8ece-02fe6ed99efd
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* test: rename cycle test for clarity per code review
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c413e0a3-5d63-4ddb-8ece-02fe6ed99efd
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* refactor(SM-8): merge implementor map into heritage map
- Add `getImplementorFiles(interfaceName)` to HeritageMap interface
- Build implementor index (interface name → file paths) alongside parent
lookup in `buildHeritageMap`, using same `resolveExtendsType` logic
- Remove `ImplementorMap` type, `buildImplementorMap`, `mergeImplementorMaps`
from call-processor.ts
- Update `findInterfaceDispatchTargets`, `processCalls`, and
`processCallsFromExtracted` to use HeritageMap for both parent
lookup and implementor dispatch
- Pipeline: single `buildHeritageMap` call replaces separate
buildImplementorMap + buildHeritageMap for both worker and
sequential paths
- Migrate implementor tests from call-processor.test.ts to
heritage-map.test.ts (4 new getImplementorFiles tests)
- Update interface dispatch test to use buildHeritageMap instead
of hand-constructed ImplementorMap
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/085dffb4-b31e-4aa5-9aa3-4314bc0010e7
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* test: rename implementor test for clarity per code review
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/085dffb4-b31e-4aa5-9aa3-4314bc0010e7
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix(SM-8): address PR #739 review comments
- pipeline.ts: cache chunk file contents from Pass 1 to eliminate
double-read of sequential chunks in Pass 2. Peak memory drains
incrementally as Pass 2 processes each chunk.
- heritage-map.ts: document Rust trait-impl omission from implementor
index and the interface-name collision limitation.
- heritage-map.test.ts: add six tests covering the extends->IMPLEMENTS
path across C# (interfaceNamePattern), Swift (heritageDefaultEdge),
Java (symbol-table Interface lookup), Kotlin, PHP, and the Rust
trait-impl omission.
- pipeline.ts: comment why the heritage accumulation uses a manual
push loop instead of spread (ref #650).
* test(SM-8): address second PR #739 review pass
- Add TypeScript implements test to getImplementorFiles (closes
the .ts coverage gap flagged by the bot reviewer).
- Tighten deep-chain boundary assertion from toBeLessThanOrEqual(32)
to toBe(32) so a future regression returning fewer ancestors
fails loudly. Added an ancestors[31] === 'class:Level32' check
to pin the upper boundary.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* refactor(call-processor): use class lookup index in phase p
* test(call-processor): cover class lookup fallback
---------
Co-authored-by: 许恩宁 <xuenning@qiyi.com>
* refactor(type-env): use class lookup index for type resolution
* test(type-env): add lookupClassByName regression coverage
* test(type-env): expand class lookup regression coverage
---------
Co-authored-by: 许恩宁 <xuenning@qiyi.com>
Add eagerly-populated methodByOwner index to SymbolTable, keyed by
ownerNodeId\0methodName. Used by walkMixedChain as a fast path for
resolving intermediate method calls in cross-class chains like
user.getAddress().getCity().getZipCode(), avoiding expensive fuzzy
lookups when the owner type is already known.
Handles overloaded methods: returns the first match when all overloads
share the same returnType, undefined when return types differ (ambiguous).
- Add lookupMethodByOwner to SymbolTable interface + implementation
- Add resolveMethodByOwner helper in call-processor.ts
- Add fast path in walkMixedChain before resolveCallTarget fallback
- Add Java cross-class chain fixture + 6 integration tests
- Add 148 unit tests for methodByOwner index behavior
* fix(ignore): respect negation patterns in .gitnexusignore childrenIgnored
childrenIgnored checked `ig.ignores(rel) || ig.ignores(rel + '/')` which
short-circuited on the bare path — directory-only negation patterns like
`!iOS/` were missed because `ig.ignores('iOS')` treats the path as a file.
Now only checks with trailing slash since childrenIgnored is only called
for directories. Bare-name patterns (e.g. `local`) still match per gitignore spec.
Fixes#596
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(ignore): add edge-case for bare `!dir` negation pattern
Verifies that `!iOS` (without trailing slash) also un-ignores the iOS/
directory — confirms the `ignore` package normalizes both `!dir` and
`!dir/` forms consistently when tested with a trailing-slash path.
Addresses non-blocking review suggestion on #654.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(ignore): link ignore package docs for bare-name normalization
Adds references to the `ignore` package documentation in both the
childrenIgnored comment and the bare-negation test, explaining why
`!iOS` (without trailing slash) also re-includes the iOS/ directory.
Addresses non-blocking review suggestion on #654.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: replace Array.push(...spread) with loop to prevent stack overflow
On large codebases (78K+ C files), deferred arrays in
runChunkedParseAndResolve accumulate 100K+ entries. The spread
operator in push(...array) puts every element on the call stack
as a function argument, exceeding the maximum call stack size.
Replace all 11 occurrences of `arr.push(...other)` with
`for (const _item of other) arr.push(_item)` which uses
constant stack space regardless of array size.
Fixes#649
* fix: also replace push(...spread) in parsing-processor.ts
* style: format pipeline.ts to match prettier config
---------
Co-authored-by: tian kian tan <tan@example.com>
* feat: same-arity overload disambiguation via type-hash suffix (#651)
Add ~type1,type2 suffix to Method/Constructor node IDs when same-arity
overloads with different parameter types exist in the same class. Also add
$const suffix for C++ const-qualified method overloads via new isConst field.
Key changes:
- typeTagForId() detects same-arity collisions and appends ~typeTag
- constTagForId() detects const/non-const collisions and appends $const
- TS/JS excluded from type-hashing (overload signatures collapse to impl body)
- Sequential findEnclosingFunction fixed: falls through on ambiguous same-class
candidates instead of picking first; fallback path includes typeTag + constTag
- Per-call-site integration tests across Java, C#, Kotlin, C++, TypeScript
- Cross-file + chain resolution tests for all 5 languages
- C++ isConst extraction via tree-sitter type_qualifier in function_declarator
1710 integration + 18 unit tests pass.
* fix: preserve generic/template args in type-hash, perf + type safety fixes
- Add rawType field to ParameterInfo preserving full type text (vector<int>)
while type stays simplified (vector). typeTagForId uses rawType for tags.
- Populate rawType in all 11 language method extractors
- Add buildCollisionGroups() to pre-group methods by name#arity (O(N) once
per class instead of O(N) per method call)
- Cache method extraction in call-processor findEnclosingFunction fallback
- Fix null guards on getLanguageFromFilename in all findEnclosing paths
- Tighten SKIP_TYPE_HASH_LANGUAGES to ReadonlySet<SupportedLanguages>
- Document ID stability invariant on first overload introduction
- C++ integration tests: template overloads (vector<int> vs vector<string>),
cross-file template + chain resolution, out-of-class method definitions
1718 integration + 20 unit tests pass.
* fix: add rawType to method-extraction unit test assertions
All 26 parameter .toEqual() assertions in method-extraction.test.ts
needed the new rawType field added to match ParameterInfo schema change.
* perf: cache tempMap/groups per class, consolidate extractFromNode
- Cache derived method map + collision groups per classNode.id in
parsing-processor (avoids rebuild per method in same class)
- Replace per-call extractFromNode with cached class extraction +
funcName:line lookup in call-processor fallback (avoids AST walk
per call site)
- Remove dead clearEnclosingFunctionCache export, fix JSDoc
* test: add sequential-path integration test for same-arity overloads
Add skipWorkers option to PipelineOptions to force sequential parsing.
New test suite verifies type-hash disambiguation produces identical
results through the sequential path (parsing-processor + call-processor
findEnclosingFunction) as the worker path.
* feat: MethodExtractor configs for Python, PHP, Swift, Dart, Rust, Ruby with exhaustive integration tests
Add per-language MethodExtractionConfig for all remaining tree-sitter languages
(RFC #568 PR 2). Each config follows the established createMethodExtractor()
factory pattern — no new types, no parse-worker changes.
Configs:
- Python: @abstractmethod, @staticmethod/@classmethod, *args/**kwargs, type hints, _/__ visibility
- PHP: abstract/final/static keywords, PHP 8 #[] attributes, __construct/__destruct
- Swift: 5-level visibility, protocol-as-abstract, static/class methods, @ attributes
- Dart: _ convention visibility, abstract (no body), method_signature unwrapping
- Rust: pub visibility, &self receiver, trait_item + impl_item, #[] attributes
- Ruby: positional visibility via sibling-walk, singleton_method as static
Integration fixtures (18 directories) covering 3 resolution patterns:
- Method enrichment: parameterTypes, isAbstract, isFinal, annotations on graph nodes
- Overload dispatch: arity-based CALLS resolution via parameterTypes
- Abstract dispatch: abstract/concrete method distinction (Python, PHP, Rust, Swift)
Go deferred — requires factory changes for receiver-based method extraction.
Closes#571
* fix: address code review findings across 6 MethodExtractor configs
Fix all actionable items from the PR #624 deep-dive review:
Dart (critical — fixes 6 CI failures):
- isDartStatic: check children first, siblings as fallback
- isDartAbstract: handle declaration nodes for abstract methods
- extractSingleParam: detect required keyword as sibling token
- Add declaration to methodNodeTypes, mixin_declaration to typeDeclarationNodes
- Add member call query for variable assignments in tree-sitter-queries
Python:
- hasDecorator now matches dotted paths (e.g. @abc.abstractmethod)
- Fix version comment from ^0.23.6 to 0.23.4
PHP:
- Add enum_declaration to typeDeclarationNodes (PHP 8.1+)
- Add version comment for 0.23.12
Swift:
- Add isOverride using hasKeyword/hasModifier pattern
Rust:
- Fix version comment from ^0.23.2 to 0.23.1
Also: identifier fallback in generic.ts for mixin owner names,
Dart integration test label fix (Method vs Function), version
comment for tree-sitter-dart 1.0.0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: Dart extension_declaration and Ruby module_function support
Dart:
- Add extension_declaration to typeDeclarationNodes and extension_body
to bodyNodeTypes — extension methods are now extracted into the graph
- Add extension_declaration and mixin_declaration to CLASS_CONTAINER_TYPES
for HAS_METHOD edge resolution
Ruby:
- module_function now maps to visibility 'private' in extractRubyVisibility
- module_function methods marked isStatic via backward-walk in isStatic
- Override semantics: private/public after module_function resets isStatic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(go): Go MethodExtractor config with receiver-based extraction
Add Go as the 13th language with a per-language MethodExtractor config.
Go methods are top-level (not nested in struct bodies), so this adds
extractFromNode() to the MethodExtractor interface for direct method
node extraction without an enclosing class.
Config extracts:
- Name from field_identifier (methods) / identifier (functions)
- Return type including multi-return (first type from parameter_list)
- Parameters with variadic support
- Visibility via uppercase/lowercase convention
- Receiver type with pointer unwrapping (*User → User)
- isStatic for functions (no receiver)
Infrastructure:
- extractOwnerName optional hook on MethodExtractionConfig
- extractFromNode on MethodExtractor (factory auto-implements)
- Parse-worker uses extractFromNode when no enclosing class found
- method_declaration added to CLASS_CONTAINER_TYPES
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: method enrichment integration tests for 7 languages + TS abstract class fix
Add method-enrichment integration test fixtures and test blocks for
Go, C++, Java, Kotlin, TypeScript, JavaScript, and C#. Each fixture
tests: class detection, HAS_METHOD edges, EXTENDS edges, isAbstract,
isStatic, annotations, parameterTypes, and CALLS edge resolution.
Fixes found during testing:
- Remove method_declaration from CLASS_CONTAINER_TYPES (added for Go
but broke Java/C# HAS_METHOD edge resolution — method_declaration
is also Java's method node type)
- Add abstract_class_declaration query to TypeScript tree-sitter
queries (was missing, so abstract classes were invisible to pipeline)
1699 integration tests pass across 20 test files, 0 regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: format typeDeclarationNodes array for better readability in PHP config
* fix: Go interface methods + Rust impl-for-Struct owner resolution
Go:
- Add method_elem to methodNodeTypes so interface method signatures
are extractable as abstract methods
- Integration test: Animal interface detected, Speak isAbstract,
CALLS edges from app.go
Rust:
- Add extractOwnerName to resolve impl Trait for Struct to the
concrete Struct (not the Trait) — fixes method misattribution
- Fix findEnclosingClassId to generate Struct: label (not Impl:)
for impl blocks so HAS_METHOD edges resolve to struct nodes
- Tighten abstract-dispatch test: assert SqlRepo owns find/save
generic.ts:
- Fix extractOwnerName fallback: when hook returns a value, skip
both name-field and type_identifier scan (was overwriting result)
1703 integration tests pass, 0 regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: code review response — Rust impl label, Swift params, Dart async, sequential methodExtractor
Address code review findings from PR #624:
- ast-helpers: Rust `impl Trait for Struct` uses Struct label (matches existing
graph node), plain `impl Struct` uses Impl label (matches definition.impl)
- swift: fix parameter type extraction (user_type not type_annotation), detect
default values as function_declaration siblings, add version comment
- dart: isDartAsync now detects async*/sync* generators, add clarifying comment
for declaration nodes in extension bodies
- python: correct isFinal comment (PEP 591 @typing.final exists, just not modeled)
- parsing-processor: port methodExtractor enrichment to sequential path so
isAbstract/isStatic/visibility/annotations/isFinal populate on <15-file repos
- tests: remove silent `if (prop !== undefined)` guards, assert properties
directly, fix label queries (Dart Method vs Function, Swift Method for protocol
methods), add Rust HAS_METHOD sourceLabel tests, Swift parameterTypes tests,
and Dart async/sync* integration tests with fixture
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: Rust grammar gap + qualified method IDs to resolve same-file collisions
Phase 1 — Rust grammar:
- Add function_signature_item query to RUST_QUERIES so abstract trait methods
(fn speak(&self) -> String;) become graph nodes with isAbstract=true
Phase 2 — Qualified method IDs:
- findEnclosingClassInfo returns {classId, className} for AST-based class lookup
- Both parsing paths (sequential + worker) qualify method/property IDs with
enclosing class: Method:file:ClassName.method instead of Method:file:method
- extractFuncNameFromSourceId handles ClassName.method format
- Fixes silent data loss when same-name methods in different classes shared a
file (e.g., Animal.speak and Dog.speak both now exist as distinct graph nodes)
Test updates:
- Rust: abstract+concrete trait methods both verified, function count adjusted
- Python: static method disambiguation now emits 2 CALLS edges (correct — no
more ID collision masking the second call)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: owner-aware resolution for qualified method IDs
Address Codex adversarial review findings after qualified ID change:
- findEnclosingFunction: disambiguate candidates by ownerId when multiple
same-name methods exist in file; qualify fallback-generated IDs
- findEnclosingFunctionId (worker): qualify sourceIds with enclosing class
name so CALLS source attribution matches definition-phase node IDs
- buildExportedTypeMapFromGraph: use lookupExactAll + nodeId match instead
of lookupExactFull which returns first definition for bare name
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: methodExtractor variadic arity, return type preservation, PHP abstract dispatch
Three bugs in the methodExtractor enrichment path broke 17 integration tests:
1. Variadic parameterCount: buildMethodProps and parse-worker set
parameterCount = info.parameters.length even for variadic functions,
causing arity filtering to reject valid calls. Now checks isVariadic
and sets parameterCount = undefined (matching extractMethodSignature).
2. C++ bare `...` token: extractCppParameters only iterated named
children, missing the unnamed `...` token in C-style variadics like
log_entry(const char* fmt, ...). Added fallback scan of all children.
3. Return type stripping: All 11 language extractReturnType functions
used extractSimpleTypeName() which strips generic parameters
(List<User> → "List", Task<User> → "Task"). Changed to .text?.trim()
to preserve full generic types needed for for-loop iterable resolution,
async-await binding, and return-type inference.
Also fixes PHP abstract dispatch test that matched SqlRepository instead
of the interface due to ambiguous filePath.includes('Repository') filter,
and adds parent-walk fallback in PHP isAbstract for extractFromNode path.
* chore: remove plan and review artifacts from PR
* fix: address Round 4 review findings + infrastructure improvements
- Ruby: add singleton_class support for class << self methods (4 new tests)
- PHP: add enum_declaration to CLASS_CONTAINER_TYPES
- Dart: add mixin/extension labels to CONTAINER_TYPE_TO_LABEL
- Swift: add TODO for unverifiable struct/enum node types on Node 22
- C#: add grammar version comment (0.23.1)
- Ruby: fix version comment range to pin (0.23.1)
- Rust/ast-helpers: add cross-reference comments for impl_item duplication
- ast-helpers: document CLASS_CONTAINER_TYPES ↔ typeDeclarationNodes invariant
- generic.ts: replace Array.includes with Set for O(1) dedup in addNestedBodies
- Go/Python/Ruby: align isAbstract signature with 2-param interface contract
- CLAUDE.md: fix malformed backtick around gitnexus:start HTML comment
- parsing-processor: add per-class method extraction cache (eliminates O(N*M))
- ast-helpers: add scoped_type_identifier to impl_item resolution
- call-processor: add dev-mode warnings at silent candidates[0] fallbacks
- MCP context(): surface methodMetadata for Method/Function/Constructor nodes
- resources.ts: update schema to list all stored Method properties
* fix: singleton_class HAS_METHOD edge regression in findEnclosingClassInfo
singleton_class (class << self) was added to CLASS_CONTAINER_TYPES but
has no name field — its receiver `self` has node type 'self', not
'identifier'. findEnclosingClassInfo now walks up to the enclosing
class/module to inherit its name, matching ruby.ts:extractOwnerName.
Also fixes findEnclosingClassNode in parse-worker.ts to skip
singleton_class and return the actual class/module node.
Adds integration test assertions for from_habitat (class << self method):
HAS_METHOD edge from Animal, isStatic=true, parameterCount=1.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(vue): add Vue SFC (.vue) support for indexing
Vue Single File Components are now fully supported in the indexing pipeline.
The implementation extracts <script> / <script setup> blocks from .vue files
and parses them using the existing TypeScript tree-sitter grammar — no new
npm dependencies required.
Key changes:
- SFC script extractor: regex-based extraction of <script setup lang="ts">
blocks with correct line offset mapping back to the .vue file
- Vue language provider: reuses TypeScript queries, type config, field
extractors, and named binding extraction
- Import resolution: .vue added to EXTENSIONS so `import Foo from './Foo'`
resolves to Foo.vue; Vue import resolver delegates to TS resolver for
tsconfig path alias support
- Export detection: <script setup> top-level bindings are implicitly exported
- Template component detection: PascalCase tags in <template> emit CALLS edges
- Line offsets applied to all emitted positions (startLine, endLine, route
lineNumbers, decorator positions) in both worker and sequential paths
Validated on a 3,553-file Vue project:
Before: 24,693 nodes | 73,614 edges | 0 symbols from .vue
After: 30,495 nodes | 112,324 edges | 5,213 symbols from .vue
18,682 imports from .vue | 5,826 vue-to-vue imports
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(typescript): track destructured call results in TypeEnv
Extend `extractPendingAssignment` to handle object destructuring from
function calls and await expressions:
const { isMaker } = useUserRole()
const { data } = await fetchData()
const { name } = repo.getProfile()
Previously, only `const { x } = someVariable` (identifier RHS) produced
TypeEnv bindings. Call-expression RHS was silently skipped, leaving
destructured properties untracked.
The fix emits a synthetic `callResult` item plus N `fieldAccess` items
per destructured property, which the existing fixpoint resolver processes
in 2 iterations. No changes needed to type-env.ts, PendingAssignment
types, or call-processor — the existing infrastructure handles it.
Also extracts a `collectDestructuredFields` helper to share the
object_pattern property iteration logic between the identifier and
call-expression branches.
Note: Full property-type resolution requires the callee to have a
declared returnType in the SymbolTable. Arrow-function composables
without type annotations (common in Vue/React) won't resolve property
types until return-type inference is added in a future change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(vue): address PR review issues for Vue SFC support
- Extract duplicated isVueSetupTopLevel to vue-sfc-extractor.ts shared
utility, removing identical copies from parse-worker.ts and
parsing-processor.ts
- Fix VUE_BUILT_INS to be a superset of TS BUILT_INS by importing and
spreading the TypeScript set, preventing spurious unresolved calls for
standard built-ins (Symbol, BigInt, WeakMap, array methods, etc.)
- Add Vue template component CALLS edge resolution in both sequential
and worker paths (call-processor.ts), matching PascalCase template
tags against imported .vue file basenames via the import map
- Add integration test for template PascalCase CALLS edges
(App.vue → Button.vue)
- Add integration test for isExported: false on non-setup <script>
blocks (OldStyle.vue options API)
- Add comment explaining TEMPLATE_RE greedy regex behavior for nested
template tags
- Fix stale language count comment (14 → 15) and remove dead code
branch in test
Made-with: Cursor
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Spec covers 4 HIGH-priority issues from review: path traversal via
group name, gRPC proto regex nested braces, service boundary detector
directory exclusions, double-close of LadybugDB pools.
Plan: 6 tasks with TDD, ordered by complexity.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire extractors into the sync pipeline with service boundary detection.
GroupService provides high-level API for all group operations.
- Sync pipeline: orchestrates extraction (HTTP, gRPC, topics) with
service boundary assignment and exact matching
- GroupService: groupList, groupSync, groupContracts, groupQuery,
groupStatus (groupImpact deferred to cross-repo follow-up PR)
- CLI: group create/add/remove/list/sync/contracts/query/status
- MCP tools: group_list, group_sync, group_contracts, group_query,
group_status
- Monorepo fixture: 3 services (auth/orders/gateway) connected via
gRPC + Kafka + HTTP — all intra-repo cross-links discovered
- Documentation: CLI commands and MCP tools added to both READMEs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core foundation for repository group analysis:
- Type system: ContractType, ExtractedContract, StoredContract, CrossLink
with optional `service` field for intra-repo matching
- Config parser for group.yaml (repos, detection flags, matching thresholds)
- Contract registry storage with atomic writes
- Exact matching engine with per-type normalization (HTTP, gRPC, topic)
and intra-repo support (different services within same repo can match)
- Extract LadybugDB pool-adapter from MCP backend for reuse by sync pipeline
- Git staleness checker for group status reporting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cpp): C/C++ MethodExtractor config with pure virtual detection (#572)
- Pure virtual (= 0) detected as isAbstract via token scanning
- virtual/final/override via hasKeyword and virtual_specifier children
- Access specifier visibility via backward sibling walk (public:/private:/protected:)
- Pointer/reference parameter types extracted correctly
- Constructor and destructor support via declaration node type
- Static detection via storage_class_specifier
- 16 new tests covering all acceptance criteria
* fix(cpp): isVirtual infers from override/final + out-of-class resolution
- isVirtual returns true for override/final methods (C++ mandates these
are virtual)
- Add findClassNodeByQualifiedName to parse-worker: resolves Foo::bar()
back to the Foo class declaration for method extractor enrichment
- Handles pointer/ref return types, constructors, destructors
- Integration test for virtual/static/constructor inline methods
- 233 unit+integration tests pass, 97 C++ resolver tests pass
* fix(cpp): address review — deep pointers, templates, unions, trailing returns
- Fix extractParamName: recursive unwrap for int** ptr → "ptr" (not "**ptr")
- Fix findFunctionDeclarator: recursive unwrap for multi-level pointer chains
- Template methods: generic extractor unwraps template_declaration to inner node
- union_specifier: added to typeDeclarationNodes, visibility defaults to public
- Trailing return type: auto foo() -> T now extracts T instead of "auto"
- Fix version comment: ^0.22.4 → ^0.23.4 to match package.json
- 4 new tests: double pointer params, template methods, union methods, trailing returns
* fix(cpp): template method visibility + union isTypeDeclaration test
extractCppVisibility now walks from the template_declaration parent
when the node is wrapped by a template, restoring correct access-
specifier resolution for templated class methods.
Also adds missing isTypeDeclaration assertion for union_specifier and
expands the template method test with explicit visibility checks.
* fix(cpp): address deep gap analysis review findings
- findClassNodeByQualifiedName: recursive pointer/reference
declarator unwrap, fixing out-of-class linking for deep pointer
return types (e.g. int** Foo::bar())
- findClassNodeByQualifiedName: recurse into namespace_definition
blocks so namespace-wrapped classes resolve correctly
- Suppress = delete / = default special members from extraction
via delete_method_clause / default_method_clause node detection
- Update known-gaps: namespace-wrapped classes, const-overload collapse
- Add tree-sitter-c version comment for consistency
- toBeFalsy() → toBe(undefined) for precise isVirtual assertion
- Tests: = delete, = default, = 0 non-regression, operator overloads,
deep pointer return types, default visibility (class vs struct),
multiple access specifier sections
* fix(wiki): Azure OpenAI compat and HTML viewer script injection
- Use max_completion_tokens instead of deprecated max_tokens for all models
- Skip sending temperature for Azure provider (some models reject non-default values)
- Simplify Azure interactive setup: endpoint + deployment + key (3 prompts instead of 7)
- Escape </script> in embedded JSON to prevent premature script tag closure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(test): align wiki-llm-client test with max_completion_tokens change
The test expected max_tokens for non-reasoning models, but the source
now uses max_completion_tokens for all models since max_tokens is
deprecated by newer OpenAI models.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Abhigyan Patwari <abhigyan@Abhigyans-MacBook-Air.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(ts,js): MethodExtractor config for TypeScript and JavaScript (#570)
Add per-language method extraction config following the established
JVM and C# patterns. Shared config base mirrors the field extractor's
typescript-javascript.ts pattern — TS-only node types are harmless
no-ops for JS.
Key features:
- isAbstract for abstract class methods and interface methods
- Parameter extraction with isOptional (?:, defaults) and isVariadic (...)
- Decorator extraction from preceding body-level siblings
- isAsync and isOverride detection
- Visibility via accessibility_modifier two-pass pattern
- Return type extraction unwrapping type_annotation
* test(ts,js): add override, getter/setter, destructured param tests
Address code review findings:
- Add override method detection test
- Add getter/setter extraction test
- Add destructured parameter with type annotation test
- Tighten constructor and private method assertions
* refactor(ts,js): address code review findings
- Replace O(M*N) decorator index scan with previousNamedSibling walk
- Remove dead findVisibility 'modifiers' fallback (TS uses
accessibility_modifier, not a modifiers wrapper)
- Document call_signature/construct_signature as known gaps
- Document that TS constructors are method_definition nodes
- Remove unused findVisibility import
* fix(ts,js): type guard before cast, add generator/computed/overload tests
- Use type guard pattern (Set.has check before as-cast) in visibility
extraction to ensure string is validated before narrowing
- Add generator method test (*items()) — confirms extraction works
- Add computed property name test ([Symbol.iterator]) — documents
bracket-in-name behavior as intentional
- Add class-level method overload test — verifies overload signatures
+ implementation are all extracted
* fix(ts,js): detect #private methods as visibility 'private'
ES2022 private class methods (#name) use private_property_identifier
as their name node type. Detect this and return 'private' visibility
instead of the default 'public'.
* fix(ts,js): address review findings + close ingestion gaps
- hasKeyword/findVisibility: skip name field child to prevent false
positives on soft-keyword method names (e.g. `abstract()`, `static()`)
- extractTsJsParameters: filter TS `this` parameter (compile-time only)
- extractMethodSignature: mirror `this`-param skip in fallback path
- tree-sitter queries: capture abstract_method_signature,
method_signature, and private_property_identifier for TS; add
private_property_identifier for JS
- Remove dead childForFieldName('name') fallbacks and typeFromAnnotation
fallback
- Add 10+ unit tests, 4 integration tests through query pipeline
* test(ts): update HAS_METHOD count for interface method_signature capture
The new method_signature query now captures ILogger.log() as a Method
node with a HAS_METHOD edge, increasing the expected count from 4 to 5.
* fix(ts,js): address second review — async generator test, declare module gap
- Add async generator method test (async *values() → isAsync: true)
- Document declare module/global augmentation as known gap
npm runs `prepare` after `prepack` during publish, so the previous
`prepare: tsc` overwrote the rewritten imports before packing.
Both `prepare` and `prepack` now run the full build script so the
tarball always contains rewritten relative imports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): build gitnexus-shared before publish, use CHANGELOG for release notes
The publish workflow was missing the gitnexus-shared build step that
the setup-gitnexus composite action provides. Since PR #536 unified
the ingestion pipeline, gitnexus imports types from gitnexus-shared,
so it must be built first.
Also replaces generate_release_notes with CHANGELOG.md extraction so
GitHub Releases use the reviewed changelog entry instead of a flat
PR title list.
Made-with: Cursor
* fix: bundle gitnexus-shared into CLI dist to fix module resolution
gitnexus-shared was declared as a file: dependency but never published
to npm, causing ERR_MODULE_NOT_FOUND for users installing gitnexus
globally. The build script now copies gitnexus-shared/dist into
dist/_shared/ and rewrites bare specifiers to relative paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move gitnexus-shared to devDependencies, use tsc for prepare
gitnexus-shared must remain available for tsc to resolve imports during
development/CI, but is not needed at runtime since it's bundled into
dist/_shared/. Moving it to devDependencies keeps it out of production
installs while allowing compilation. The prepare script now runs plain
tsc (no shared bundling needed for local dev).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Abhigyan Patwari <abhigyan@Abhigyans-MacBook-Air.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The publish job failed because npm ci triggers the prepare script (tsc)
before gitnexus-shared types are available. Add the same build step
that the setup-gitnexus composite action uses in CI.
* feat(web): add repo landing screen with selectable repo cards
Instead of auto-loading the first indexed repo when the backend is
detected, show a landing screen that lets users choose which repo
to explore or analyze a new one. This addresses the UX gap where
users with multiple indexed repos had no way to pick—they were
always sent to the first one found.
- New RepoLanding component with clickable repo cards (name, stats,
indexed date) and an embedded RepoAnalyzer for new repos
- DropZone gains a 'landing' phase between server detection and
graph loading
- Shared connectToRepo handler replaces the old handleAnalyzeComplete
for both repo selection and post-analysis connection
Made-with: Cursor
* fix(web): update e2e flow for repo landing screen
The new landing screen intentionally stops auto-loading the first indexed
repo, so the existing Playwright tests were still waiting for the explorer
to appear automatically. Update the specs to select a repo from the landing
screen before asserting on the graph, and add a stable test id for repo cards.
Also format DropZone to satisfy the Prettier CI check.
Made-with: Cursor
* fix(e2e): use waitFor instead of instant isVisible for landing card
locator.isVisible() is a non-retrying instant check — the landing card
hadn't rendered yet when it was called, causing the click to be silently
skipped. Switch to waitFor which properly polls until the element appears.
Made-with: Cursor
---------
Co-authored-by: Abhigyan Patwari <abhigyan@Abhigyans-MacBook-Air.local>
* feat(csharp): add C# MethodExtractor config (#573)
Add C# method extraction config mirroring the JVM pattern from PR #576.
Wire csharpMethodConfig into the C# language provider and add 18 tests
covering classes, interfaces, abstract classes, structs, records,
constructors, params/out/ref/optional parameters, sealed methods,
attributes, and visibility modifiers.
* fix(csharp): add destructor, operator, conversion operator, and in-param support
- Add destructor_declaration, operator_declaration, and
conversion_operator_declaration to methodNodeTypes
- Custom extractName for operators (e.g., "operator +", "implicit operator double")
- Fix extractReturnType for operator declarations (use type field, not returns)
- Add in modifier to parameter extraction (alongside out/ref)
- Add 4 new tests: destructor, operator+, implicit conversion, in parameter
* fix(csharp): add ref param test and document compound visibility limitation
- Add test for ref parameter modifier (was only testing out)
- Document that protected internal / private protected resolve to first modifier
* feat(csharp): support compound visibilities (protected internal, private protected)
- Add 'protected internal' and 'private protected' to FieldVisibility union
- Detect compound modifiers in both C# method and field extractors via
collectModifierTexts helper scanning adjacent modifier nodes
- Add 2 tests for compound visibility detection
* feat(csharp): primary constructors, virtual/override/async, primary fields
Address all known limitations from review:
- Primary constructor support (C# 12): add extractPrimaryConstructor to
MethodExtractionConfig and extractPrimaryFields to FieldExtractionConfig.
Record params become public readonly properties; class params become
private captured fields.
- Add isVirtual, isOverride, isAsync optional fields to MethodInfo,
MethodExtractionConfig, NodeProperties, and parse-worker propagation.
- Detect virtual/override/async modifiers in C# method config.
- Move collectModifierTexts to shared helpers.ts (deduplicate).
- Fix destructor name to ~ClassName (disambiguates from constructor).
- Add expression-bodied method test.
- 118 tests total across method + field extraction suites, all passing.
* fix(csharp): review round 2 — annotations, record_struct, grammar pin
- Fix primary constructor annotations: use [] instead of extracting
class-level attributes (C# has no syntax for ctor-specific attributes)
- Add record_struct_declaration to typeDeclarationNodes in both method
and field extractors, CLASS_CONTAINER_TYPES, and isRecord visibility check
- Pin tree-sitter-c-sharp version (^0.23.1) in params comment
* fix(csharp): complete record_struct query + label mapping, sealed override test
- Add record_struct_declaration capture patterns to tree-sitter-queries.ts
(type definition + primary constructor)
- Add record_struct_declaration → 'Struct' in CONTAINER_TYPE_TO_LABEL
- Assert isOverride: true alongside isFinal in sealed override test
* fix(csharp): record_struct label mismatch, add record struct + documented limitation tests
- Fix record_struct_declaration query tag: @definition.struct (not @definition.record)
to match CONTAINER_TYPE_TO_LABEL and prevent broken HAS_METHOD edges
- Add 3 record struct tests: isTypeDeclaration, method extraction, primary constructor
- Add documented limitation tests: partial method (isAbstract: false), generic type
parameter stripping (name excludes <T>)
* fix(csharp): remove record_struct_declaration — not a real tree-sitter node type
tree-sitter-c-sharp 0.23.1 parses 'record struct' as record_declaration
(absorbs the 'struct' keyword as an unnamed child token). The non-existent
record_struct_declaration in queries caused TSQueryErrorNodeType, breaking
ALL C# file processing.
Remove from: tree-sitter-queries.ts, typeDeclarationNodes in both
extractors, CLASS_CONTAINER_TYPES, and CONTAINER_TYPE_TO_LABEL.
Record struct types are already handled via record_declaration.
* feat(csharp): add isPartial support, filter targeted attributes, static ctor test
- Add isPartial optional field to MethodInfo, MethodExtractionConfig,
NodeProperties, and parse-worker propagation pipeline
- Detect partial modifier in C# config — marks both declaration-only
and implemented partial methods
- Filter targeted attribute lists (e.g. [return: MarshalAs(...)]) in
extractCSharpAnnotations — only untargeted attributes collected
- Add static constructor test (isStatic: true, same name as class)
- Add 3 partial method tests: declaration-only, with body, coexisting pair
- Document record_struct/record_class as defensive dead code in
export-detection.ts (grammar absorbs keywords into record_declaration)
* fix(csharp): this param for extension methods, dedup visibility, test fixes
- Handle this modifier on extension method parameters (type prefixed
as 'this string', consistent with out/ref/in handling)
- Deduplicate visibility logic in extractPrimaryConstructor — reuse
csharpMethodConfig.extractVisibility instead of inline compound check
- Fix record struct test title to reflect actual grammar behavior
- Add conversion operator returnType assertion
- Add extension method this parameter test
* fix(csharp): primary constructor line points to param list, empty name guard
- Use paramList.startPosition instead of ownerNode.startPosition for
primary constructor line number (avoids methodInfoCache key collision)
- Guard against empty param names from tree-sitter error recovery nodes
gitnexus-web imports from gitnexus-shared, which requires npm run build
to generate dist/. Without this step, npm run dev fails with module
resolution errors.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(wiki): extend LLMConfig/CLIConfig with Azure and reasoning model fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wiki): restore cursor model resolution, fix LLMProvider type, clean up regex
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(wiki): remove stale LLMProvider type alias from repo-manager
* fix(wiki): fix Azure auth header, api-version param, reasoning model params, content_filter error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wiki): tighten Azure detection, reasoning model regex, content_filter gating
- isReasoningModel: new regex matches only o1/o3 bare + any oN-mini/oN-preview; bare o4/o5/etc now return false
- isAzureProvider: use URL hostname matching to block spoofed subdomain URLs
- callLLM: warn on Azure legacy /deployments/ URL without api-version
- callLLM: gate content_filter error to azure===true; also catch ResponsibleAIPolicyViolation
- tests: add afterEach stub cleanup, spoofed-URL, bare-o4, non-Azure content_filter, and URL-only Azure auto-detect tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wiki): detect content_filter finish_reason in SSE stream and throw clear error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wiki): skip delta accumulation after content_filter, use provider-neutral error message
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(wiki): add Azure OpenAI option to interactive setup wizard
Inserts Azure as option [3] in the provider menu (shifting Custom to [4]
and Cursor to [5]), adds guided Azure setup flow with resource/deployment
prompts, v1/legacy URL format selection, reasoning-model flag, and
content_filter error handling in the catch block.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wiki): store explicit false for non-reasoning Azure deployments, trim resource name inputs
- isReasoningModelDeployment now stores false (not undefined) when user says no
- Always include isReasoningModel in saved azureConfig (no conditional guard needed)
- Trim resourceName and deploymentName prompt inputs to avoid whitespace issues
- Improve reasoning model note to mention Azure requirement
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(wiki): add --api-version and --reasoning-model CLI flags for Azure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wiki): include apiVersion and reasoningModel in hasCLIOverrides guard
* fix(wiki): default provider to 'openai' in resolveLLMConfig when not configured
* style: apply prettier formatting
* fix(wiki): address PR review — remove unrelated files, harden inputs
- Remove evidence/, fix-adapter.js, and planning doc accidentally included
- URL-encode apiVersion in buildRequestUrl to prevent query string injection
- Add --no-reasoning-model flag to allow CLI override of saved config
- Simplify verbose ternary in Azure wizard prompt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(wiki): use execFileSync for EDITOR to prevent shell injection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: replace NodeProperties index signature any with unknown
Change [key: string]: any to [key: string]: unknown in NodeProperties.
Remove 19 redundant (node.properties as any) casts in csv-generator.ts
— all accessed properties are already declared on the type.
* refactor: replace any with SyntaxNode across ingestion layer
Mechanical substitution — all tree-sitter AST node parameters and
variables typed as any are now properly typed as SyntaxNode.
- ast-helpers.ts: 13 any → SyntaxNode
- parsing-processor.ts: 8 any → SyntaxNode
- parse-worker.ts: 40 any → SyntaxNode/TreeSitterLanguage/Parser.Query
- php.ts: 11 any → SyntaxNode
Also adds TreeSitterLanguage type alias for optional grammar loading.
* refactor: eliminate remaining any in ingestion layer
- call-processor, call-routing, c-cpp: SyntaxNode substitutions
- parse-worker: typed WorkerIncomingMessage discriminated union
- worker-pool: typed WorkerOutgoingMessage + Error handler
- ast-cache, import-processor: targeted cast for Tree.delete()
- community-processor: graphology AbstractGraph types, LeidenModule
interface for vendored leiden code
Ingestion layer: 130 → 7 any warnings remaining.
* feat(java): method references + worker overload disambiguation (TypeEnv + argTypes)
Fix two Java gaps: (1) method references (obj::method) via
tree-sitter @call + parseJavaMethodReference wired through extractLanguageCallSiteSeed for parse-worker and call-processor; (2) overloaded calls with typed
non-literal args by extending OverloadHints with TypeEnv for identifiers and adding ExtractedCall.argTypes from extractCallArgTypes on the worker path with
matchCandidatesByArgTypes (inferJvmLiteralType remains for literals).
* test(csharp): expect interface-dispatch edge for IRepository.Save in heritage fixture
* refactor(ingestion): move parseJavaMethodReference to call-sites/java.ts
* refactor(ingestion): defer worker call resolution until implementor map is complete
* style: prettier + remove unused import for CI quality checks
Made-with: Cursor
* fix(ingestion): implementor map for C# base_list + sequential pipeline path
- buildImplementorMap: treat extends rows as implements when resolveExtendsType
says IMPLEMENTS (worker heritage mirrors parse-worker, all base_list as extends)
- Worker path: pass ctx into buildImplementorMap(deferredWorkerHeritage, ctx)
- Sequential path: extract heritage before processCalls and pass implementor map
so small repos get interface-dispatch CALLS (fixes csharp-proj integration test)
Made-with: Cursor
* perf(pipeline): accumulate sequential implementor map without O(E) per chunk
- Merge buildImplementorMap(chunk heritage) into one map each sequential chunk so
work is O(heritage) per chunk and interface dispatch sees prior chunks (worker parity)
- Drop unused globalImplementorMap + redundant merge after worker pass
Made-with: Cursor
* feat: configure prettier with pre-commit hook integration
Add prettier, lint-staged, and prettier-plugin-tailwindcss at the repo
root with husky pre-commit hook integration. Moves husky from
gitnexus/ to root package.json for reliable hook installation.
- Root package.json with prepare/format/format:check scripts
- .prettierrc with endOfLine:lf and tailwindStylesheet for TW v4
- .prettierignore excluding fixtures, vendor, generated, *.d.ts, *.md
- .gitattributes enforcing LF line endings for Windows consistency
- Pre-commit hook uses direct node_modules/.bin/ paths (no npx)
* style: apply prettier formatting to entire codebase
One-time bulk format. No logic changes.
Use .git-blame-ignore-revs to skip this commit in git blame.
* chore: add .git-blame-ignore-revs for prettier format commit
* perf: pre-commit hook runs only tests related to staged files
Use vitest --related to scope test execution to tests that import
the changed files, instead of running the full suite on every commit.
* perf: remove vitest from pre-commit hook, keep in CI only
Pre-commit now runs lint-staged + tsc only. Tests run in CI
(ci-tests.yml) where they belong — keeps commits fast.
* ci: add prettier format check to quality workflow
PRs will now fail if code isn't formatted with prettier.
* feat: add server-side ingestion API (POST /api/analyze, SSE progress)
Extract core analysis orchestration from CLI into shared run-analyze.ts
module. Add server-side analyze endpoints so the web app can trigger
ingestion via HTTP instead of running the full pipeline in-browser.
New files:
- src/core/run-analyze.ts — shared runFullAnalysis() orchestrator
- src/server/analyze-job.ts — job manager (single-slot, dedup, SSE events)
- src/server/analyze-worker.ts — forked child process (8GB heap, IPC)
- src/server/git-clone.ts — shallow clone/pull with SSRF protection
API endpoints:
- POST /api/analyze — start analysis (returns 202 + jobId)
- GET /api/analyze/:jobId — poll job status
- GET /api/analyze/:jobId/progress — SSE progress stream
Security: URL validation blocks private IPs and non-HTTP schemes.
Path validation requires absolute paths. Git stderr not leaked to API.
* feat(web): add server-side analyze UI (Phase 2)
Add "Analyze on Server" flow to the web app's Server tab so users
can trigger server-side ingestion from the browser. On completion,
the graph is automatically loaded via the existing connectToServer flow.
New files:
- AnalyzeProgress.tsx — progress bar with phase label, elapsed time, cancel
Modified files:
- backend.ts — startAnalyze(), streamAnalyzeProgress() SSE client
- DropZone.tsx — analyze URL input + button below Connect section
- App.tsx — onServerAnalyze handler wires analyze -> connect flow
* feat: add job cancellation, timeout, and child process tracking (Phase 3)
- DELETE /api/analyze/:jobId — cancel running analysis (SIGTERM to worker)
- 30-minute timeout kills long-running workers automatically
- Child process refs tracked in JobManager for cleanup on shutdown
- dispose() kills all active children on SIGINT/SIGTERM
- Web cancel button now calls server DELETE endpoint
- cancelAnalyze() added to web backend client
* refactor(web): remove browser ingestion pipeline (Phase 4)
Delete 16 duplicated ingestion files, 2 unused service files
(git-clone, zip), and tree-sitter parser-loader from gitnexus-web.
All ingestion now runs server-side via POST /api/analyze.
Deleted (18 files, ~5,000 lines):
- core/ingestion/*.ts (16 pipeline processors)
- core/tree-sitter/parser-loader.ts (WASM tree-sitter loader)
- services/git-clone.ts (isomorphic-git client-side clone)
- services/zip.ts (JSZip extraction)
Simplified:
- DropZone.tsx — server-only (removed ZIP/GitHub tabs)
- ingestion.worker.ts — removed runPipeline/runPipelineFromFiles
- useAppState.tsx — removed pipeline callbacks
- App.tsx — removed handleFileSelect/handleGitClone
- main.tsx — removed Buffer polyfill for isomorphic-git
- types/pipeline.ts — removed PipelineResult/serialize helpers
Kept: cluster-enricher.ts (LLM enrichment, still used by worker)
Dependencies now removable: web-tree-sitter, isomorphic-git,
@isomorphic-git/lightning-fs, jszip (estimated 3-4MB bundle savings)
* refactor(web): sync graph schema from CLI + delete WASM grammars
Sync graph/types.ts and lbug/schema.ts from the CLI (source of truth)
to the web module so the browser LadybugDB can handle all node and
relationship types the server pipeline produces.
Synced types: Route, Tool, Section node labels; HANDLES_ROUTE, FETCHES,
HANDLES_TOOL, ENTRY_POINT_OF, WRAPS, QUERIES relationship types;
description fields on Function/Class/Interface/Method/CodeElement.
Deleted: public/wasm/ directory (14 tree-sitter WASM grammars + core).
Removed deps: web-tree-sitter, isomorphic-git, @isomorphic-git/lightning-fs,
jszip, buffer, @types/jszip (~3-4MB bundle savings).
* feat: create gitnexus-shared package for unified type definitions
Create a new gitnexus-shared package that is the single source of truth
for types shared between the CLI and web modules:
- SupportedLanguages enum (15 languages)
- Graph types: NodeLabel, NodeProperties, RelationshipType, GraphNode, GraphRelationship
- Schema constants: NODE_TABLES, REL_TYPES, REL_TABLE_NAME, EMBEDDING_TABLE_NAME
- Pipeline types: PipelinePhase, PipelineProgress
Both gitnexus (CLI) and gitnexus-web import from gitnexus-shared via
file: dependency. Each package re-exports and extends with platform-specific
additions (CLI: KnowledgeGraph with mutation methods; Web: simpler KnowledgeGraph).
This ensures types can never drift between packages — adding a new
language, node type, or relationship type in gitnexus-shared automatically
propagates to both consumers.
* refactor: import shared types directly from gitnexus-shared at call sites
Replace all re-export patterns with direct imports from gitnexus-shared.
72 files updated across CLI and web:
- SupportedLanguages: 49 CLI files now import from 'gitnexus-shared'
instead of '../config/supported-languages.js'
- GraphNode, GraphRelationship, NodeLabel: 22 CLI + 10 web files now
import from 'gitnexus-shared' instead of local re-export wrappers
- NODE_TABLES: api.ts imports from 'gitnexus-shared'
- PipelineProgress: useAppState.tsx imports from 'gitnexus-shared'
Local types.ts files now only define platform-specific KnowledgeGraph
(CLI has mutation methods, web has add-only). No more re-exports.
* fix: update lock files for gitnexus-shared, remove stale vite polyfills
Add gitnexus-shared@1.0.0 to lock files so npm ci succeeds in CI.
Remove buffer polyfill and global define from vite.config.ts (isomorphic-git was removed).
* fix(security): add write guard to HTTP /api/query, fix CORS proxy bypass
- Add isWriteQuery() check to POST /api/query handler — blocks CREATE,
DELETE, SET, MERGE, DROP, etc. via HTTP API (guard was only in MCP
pool adapter and browser-side, not the HTTP server path)
- Extend CYPHER_WRITE_RE with CALL, INSTALL, LOAD keywords
- Fix CORS proxy subdomain bypass: endsWith('github.com') allowed
'evil-github.com'. Now requires exact match or '.github.com' suffix
* feat(server): enhance /api/search with enrichment, add /api/grep, strip graph content
- POST /api/search: add mode param (hybrid|semantic|bm25), server-side
enrichment returns connections/cluster/processes per result in one call
(collapses 31 sequential HTTP calls to 1 for the agent search tool)
- GET /api/grep: regex search across indexed file contents, eliminates
need to transfer all file contents to browser
- GET /api/graph: strip content field by default (80-95% payload
reduction). Use ?includeContent=true for backward compat
- Add LRU cache invalidation hook point for future caching
* feat(server): add /api/embed endpoint for server-side embedding generation
- POST /api/embed: triggers embedding pipeline via onnxruntime-node
with JobManager for single-slot concurrency, timeout, and dedup
- GET /api/embed/:jobId: poll job status
- GET /api/embed/:jobId/progress: SSE stream with heartbeat, event IDs,
and X-Accel-Buffering:no header for proxy compatibility
- DELETE /api/embed/:jobId: cancel running embedding job
- Maps embedding pipeline phases (ready→complete, error→failed) to
JobManager status conventions
* feat(web): create consolidated BackendClient module
Single HTTP client replacing backend.ts, server-connection.ts, and
worker HTTP helpers. Includes:
- Typed methods: runQuery, search (enriched), grep, readFile, connect
- Generic streamSSE<T> utility extracted from analyze progress pattern
- BackendError with discriminated code field (network/server/client/timeout)
- Embed API: startEmbeddings, streamEmbeddingProgress, cancelEmbeddings
- Search with mode param (hybrid|semantic|bm25) and enrichment
* refactor(web): rewrite Graph RAG tools for backend-only HTTP queries
- Search tool: uses enriched /api/search (1 call replaces 31 sequential queries)
- Cypher tool: removes browser-side embedding; {{QUERY_VECTOR}} routes to
/api/search with mode:'semantic' instead of local transformers.js
- Grep tool: uses /api/grep instead of in-memory fileContents map
- Read tool: uses /api/file instead of fileContents map lookup
- Impact tool: getCallSiteSnippet now async via /api/file
- createGraphRAGTools now accepts GraphRAGBackend interface instead of
7 separate function params + fileContents map
- createGraphRAGAgent simplified to (config, backend, context?)
- Removed imports: embedder, lbug/schema (replaced with gitnexus-shared)
- Net: -205 lines
* refactor(web): delete WASM infrastructure, remove 7 packages (-5242 lines)
Delete browser-side LadybugDB, embeddings, search, and worker:
- gitnexus-web/src/core/lbug/ (adapter, csv-generator, schema, query-result)
- gitnexus-web/src/core/embeddings/ (embedder, pipeline, text-gen, types)
- gitnexus-web/src/core/search/ (bm25-index, hybrid-search)
- gitnexus-web/src/workers/ingestion.worker.ts (828 lines)
- gitnexus-web/src/services/server-connection.ts (merged into backend-client)
- gitnexus-web/src/types/lbug-wasm.d.ts
Remove packages: @ladybugdb/wasm-core, @huggingface/transformers,
comlink, minisearch, vite-plugin-wasm, vite-plugin-top-level-await,
vite-plugin-static-copy
Update vite.config.ts: remove WASM plugins, COOP/COEP headers,
worker config, optimizeDeps exclude
Update imports: App.tsx, DropZone, Header, AnalyzeProgress,
BackendRepoSelector, useBackend → backend-client
* refactor(web): replace Worker/Comlink with direct BackendClient calls
- useAppState: remove Worker instantiation, Comlink.wrap, apiRef.
All queries now go through BackendClient HTTP functions directly.
- Agent runs on main thread (I/O-bound LLM streaming, not CPU-bound)
- initializeAgent: creates GraphRAGAgent with GraphRAGBackend interface
bound to BackendClient methods (runQuery, search, grep, readFile)
- startEmbeddings: calls POST /api/embed + SSE progress instead of
running browser-side transformers.js pipeline
- switchRepo: no longer loads graph into WASM DB or extracts fileContents
- App.tsx: handleServerConnect simplified (no fileContents, no loadServerGraph)
- Delete old backend.ts (replaced by backend-client.ts)
- Net: -396 lines
* fix(web): fix await-in-map build error in agent streaming
Move dynamic import of AIMessage outside .map() callback to avoid
"await can only be used inside an async function" build error.
* fix(web): remove stale apiRef references that broke chat functionality
sendChatMessage referenced apiRef.current (deleted Worker ref) which
would throw TypeError. Replaced with agentRef.current guard since agent
now runs on main thread.
* fix(server): dispose embedJobManager on shutdown, fix job mutation
- Add embedJobManager.dispose() to shutdown handler (was missing,
causing cleanup timer to keep Node process alive)
- Replace direct job.repoName/status mutation with updateJob() to
ensure SSE event emission for initial status change
* fix(server): parameterize Cypher, harden grep, unify SSE endpoints
- Search enrichment: replace string interpolation with executePrepared()
using $nid parameter binding to prevent Cypher injection
- Add executePrepared() to core lbug-adapter (prepare/execute pattern)
- /api/grep: add 200-char pattern length limit (ReDoS protection),
search files on disk instead of loading entire corpus into memory
(constant memory usage regardless of repo size)
- Extract mountSSEProgress() shared helper for SSE streaming — both
analyze and embed endpoints now have consistent heartbeat (30s),
event IDs (reconnection support), and X-Accel-Buffering header
* refactor(web): remove dead code from Worker-era architecture
- Remove loadServerGraph no-op function, interface member, and all consumers
- Remove testArrayParams stub and interface member
- Remove fileContents state from GraphStateProvider (never populated in
server-side architecture)
- Remove forceDevice parameter from startEmbeddings (server-side, no device choice)
- Replace phantom EmbeddingProgress type with inline { phase, percent }
- Replace resolvePathFromContents (needed fileContents Map) with graph-based
file path resolution using filePathIndex built from graph nodes
- Fix: AI citation grounding ([[file.ts:10]]) now works via graph node lookup
instead of broken fileContents-based resolution
* fix(web): use streamAgentResponse for full tool_call/reasoning streaming
Replace naive agent.stream() loop that only handled content chunks with
streamAgentResponse() generator from agent.ts. This properly routes:
- reasoning tokens (before/between tool calls)
- tool_call events (name, args, status)
- tool_result events (completed tool output)
- content tokens (final answer after all tools done)
Previously the onChunk handler for tool_call/tool_result/reasoning was
dead code since the streaming loop only emitted content events.
* fix(web): resolve CI type errors from dead code removal
- Import GraphNode/GraphRelationship from gitnexus-shared in graph.ts
(not re-exported from local types.ts)
- Add Route, Tool entries to NODE_COLORS and NODE_SIZES constants
- Add PipelineResult type to web types/pipeline.ts
- Remove fileContents from CodeReferencesPanel and RightPanel
- Remove testArrayParams and forceDevice from EmbeddingStatus
- Remove forceDevice args from startEmbeddings() calls in App.tsx
- Fix embeddingProgress property accesses for simplified type
* fix(ci): add setup-gitnexus-web action, build shared once per job
- Remove prepare script from gitnexus-shared (tsc not available during
npm ci of consuming packages)
- Create .github/actions/setup-gitnexus-web composite action: builds
gitnexus-shared then runs npm ci for gitnexus-web
- setup-gitnexus action: already builds gitnexus-shared for CLI jobs
- ci-quality typecheck-web: uses setup-gitnexus-web (DRY)
- ci-e2e: uses setup-gitnexus-web (DRY)
- ci-tests: gitnexus-shared already built by setup-gitnexus, just
install web deps without rebuilding
* fix(ci): use prepare script so gitnexus-shared builds during npm ci
Move typescript from devDependencies to dependencies in gitnexus-shared
so the prepare script (tsc) works when npm resolves file: deps during
npm ci. No GHA modifications needed — npm handles the build lifecycle
automatically.
Remove manual gitnexus-shared build steps from setup-gitnexus and
setup-gitnexus-web actions.
* fix(ci): build gitnexus-shared explicitly in setup actions
The file: dependency protocol doesn't reliably run prepare scripts
because devDependencies aren't installed first. Instead of fragile
lifecycle hacks, build gitnexus-shared explicitly in both setup actions:
- setup-gitnexus: npm install && npm run build in gitnexus-shared/
- setup-gitnexus-web: same, before npm ci in gitnexus-web/
- ci-tests: shared already built by setup-gitnexus, web just npm ci
No prepare script, no dist in git, no typescript as a prod dependency.
* fix: remove CALL from CYPHER_WRITE_RE — breaks FTS and vector search
CALL is used by read-only procedures: CALL QUERY_FTS_INDEX(...) and
CALL QUERY_VECTOR_INDEX(...). Adding it to the write guard blocked all
FTS search, causing 3 test failures. The database is opened in read-only
mode as defense-in-depth against write procedures via CALL.
Keep INSTALL and LOAD in the blocklist (genuinely dangerous).
* fix(web): update vercel.json for gitnexus-shared, remove COOP/COEP
- Add installCommand that builds gitnexus-shared before installing
web deps (Vercel doesn't know about the monorepo file: dependency)
- Remove Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy
headers (no longer needed — WASM LadybugDB removed)
* fix(web): update tests for deleted modules
- Delete csv-generator.test.ts (tests deleted WASM-only csv-generator)
- Update security-guards.test.ts: import NODE_TABLES/REL_TYPES from
gitnexus-shared instead of deleted src/core/lbug/schema
- Update server-connection.test.ts: import normalizeServerUrl from
backend-client, remove extractFileContents tests (function deleted)
* fix(e2e): remove Server tab click — UI is now server-only
The DropZone no longer has ZIP/GitHub/Server tabs (browser ingestion
was removed). The server URL input is directly visible on the landing
page. Update e2e test to skip the tab click and go straight to input.
All 5 e2e tests pass locally.
* refactor: use gitnexus-shared for PipelinePhase/PipelineProgress types
CLI was duplicating PipelinePhase and PipelineProgress locally instead
of importing from gitnexus-shared. Updated all consumers to import
directly. Also removed dead code: SerializablePipelineResult,
serializePipelineResult(), deserializePipelineResult().
* fix(server): address PR #536 review — security, race conditions, dead code
- Fix path traversal in POST /api/analyze: split into isAbsolute + normalize check
- Add shared repo lock (activeRepoPaths) preventing concurrent analyze+embed on same repo
- Fix 202 response returning actual job.status instead of hardcoded 'queued'
- Add 30-minute timeout for embedding jobs (was missing unlike analyze jobs)
- Fix DropZone calling startAnalyze without setting backend URL first
- Add SSE reconnect with exponential backoff (3 retries) and Last-Event-ID
- Fix normalizeServerUrl to return base URL (no /api suffix) — clear contract
- Delete dead code: proxy.ts, server-graph-hydration.ts, pipeline.ts re-export barrel
- Update LoadingOverlay to import PipelineProgress directly from gitnexus-shared
* fix(server): fix repo lock key mismatch and embed cancel race
- Use getStoragePath(targetPath) as lock key in analyze handler to match
embed handler's entry.storagePath — keys now always align
- Guard embed completion: don't overwrite 'failed' with 'complete' when
job was cancelled while pipeline was still running
- Remove unused jobType parameter from acquireRepoLock
- Log backend.init() errors instead of silently swallowing
* fix: add gitnexus-shared as a local dependency in package-lock.json
* refactor: move language detection to gitnexus-shared, add syntax highlighting for all 15 languages
Move getLanguageFromFilename() from CLI to gitnexus-shared with COBOL
support added. Add getSyntaxLanguageFromFilename() for Prism-compatible
syntax highlighting covering all 15 code languages plus auxiliary
formats (json, yaml, markdown, html, css, bash, sql, xml).
Refactor CodeReferencesPanel to use shared function instead of a local
30-line switch. Delete dead gitnexus-web/src/config/supported-languages.ts
(web already imports SupportedLanguages from gitnexus-shared).
* feat(web): add first-time user onboarding with auto server detection
Replace the manual "Connect to Server" panel with an automatic onboarding
flow that guides first-time users through starting the GitNexus server.
Server detection:
- useBackend hook polls via setTimeout chain (3s, no overlap)
- Page Visibility API pauses polling when tab is hidden
- SSE heartbeat (/api/heartbeat) for instant disconnect detection
Onboarding UI (OnboardingGuide.tsx):
- Step-by-step flow: copy command → run → auto-connect
- Smart command: shows `gitnexus serve` in dev, `npx gitnexus@latest serve` in prod
- Node.js version auto-detected from package.json via Vite define
- Faux terminal windows with copy-to-clipboard, platform tabs, polling indicator
Transitions (DropZone.tsx):
- Crossfade wrapper with snapshot pattern for smooth phase transitions
- Three phases: onboarding → success (1.2s hold) → loading → graph
- Auto-recovery: falls back to onboarding if server dies or connect fails
Server changes:
- GET /api/heartbeat: SSE endpoint for liveness detection
- GET /api/info: version, launch context, Node.js version
- npm run serve script for local development
- app.disable('x-powered-by') hardening
* feat(web): add repo analysis UI, SSE heartbeat, and review fixes
Repo analysis:
- AnalyzeOnboarding: empty-state card when server has zero repos
- RepoAnalyzer: GitHub URL + Local Folder tabs with browse button
- Header repo dropdown: click project badge to switch repos or analyze new
- DropZone 'analyze' phase integrated into Crossfade transitions
Reliability fixes from 5-agent review:
- Polling: stop scheduling timers when tab hidden, restart on visibility return
- Heartbeat: exponential backoff (1s/2s/4s, 3 retries) prevents graph loss on blip
- RepoAnalyzer: completion timer tracked in ref, cleaned up on unmount
- DropZone: standardized card padding (p-7), heading sizes (text-lg)
Accessibility:
- prefers-reduced-motion global CSS rule (WCAG 2.3.3)
- focus-visible rings on CopyButton
- cursor-pointer on all Header buttons
- Consistent rounded-xl on all dropdowns
Cleanup:
- Deleted dead AnalyzeSheet.tsx (219 LOC) and BackendRepoSelector.tsx (89 LOC)
- Fixed AnalyzeProgress lucide import (lucide-react → @/lib/lucide-icons)
* fix(server): resolve analyze worker fork crash in dev mode
The forked analyze worker was crashing immediately with exit code 1
when running via `npm run serve` (tsx). Two issues:
1. Worker path resolved to `analyze-worker.js` but only `.ts` exists
in the source directory — the `.js` file is only in `dist/`.
2. On Windows, bare `--import tsx` in execArgv fails because Node's
ESM resolver for --import uses the child's CWD, not the parent's
node_modules. Windows also rejects raw paths as `d:` is not a
valid URL scheme.
Fix: detect dev vs prod via `import.meta.url` extension. In dev mode,
resolve `tsx/esm` to an absolute `file://` URL via `pathToFileURL()`
anchored to the parent's `createRequire` context. This works on all
platforms and doesn't depend on the child's CWD or PATH.
Also captures child stderr for better crash diagnostics.
Verified: `POST /api/analyze` with GitHub URL completes successfully
in dev mode (tsx) — status goes from cloning → analyzing → complete.
* fix(server): add worker auto-retry, error handling, and crash diagnostics
Worker resilience:
- Auto-retry up to 2 times with exponential backoff (1s, 2s) on crash
- SSE progress shows "Retrying after crash (1/2)..." during retry
- Captures child stderr for crash diagnostics in failure message
- AnalyzeJob tracks retryCount per job
Server error handling:
- app.listen wrapped in Promise so EADDRINUSE/EACCES propagate cleanly
- serve.ts catches startup errors with friendly messages and exit code 1
- EADDRINUSE gets actionable guidance (stop other process or --port flag)
- Global uncaughtException/unhandledRejection handlers prevent silent exits
- DEBUG=1 env var shows full stack traces
* feat: add e2e tests for onboarding flows, worker retry, and error handling
E2E tests (onboarding.spec.ts — 11 tests):
- Flow 1: OnboardingGuide shown when server unreachable (6 tests)
- Flow 2: Auto-connect with success card, analyze phase for zero repos
- Flow 3: Analyze form — GitHub URL validation, Local Folder tab, tab switching
- Flow 4: Repo dropdown in exploring view (skipped without live server)
Updated server-connect.spec.ts:
- Replaced manual Connect button flow with auto-connect waitForGraphLoaded
Server resilience:
- Worker auto-retry (2 attempts with exponential backoff) on crash
- Friendly error messages for serve startup failures (EADDRINUSE etc.)
- Global uncaughtException/unhandledRejection handlers prevent silent exits
- app.listen wrapped in Promise for proper error propagation
* refactor(shared): enforce exhaustive language coverage via Record types
Replace the if/else chain in getLanguageFromFilename with two exhaustive
Record<SupportedLanguages, ...> maps:
- EXTENSION_MAP: every language → its file extensions
- SYNTAX_MAP: every language → its Prism syntax identifier
Adding a new member to the SupportedLanguages enum without adding it to
both maps now produces a TypeScript compile error:
Property '[SupportedLanguages.NewLang]' is missing in type...
This matches the existing pattern in languages/index.ts (providers table)
which already uses `satisfies Record<SupportedLanguages, LanguageProvider>`.
Three compile-time enforcement points now exist:
1. EXTENSION_MAP in language-detection.ts (file extensions)
2. SYNTAX_MAP in language-detection.ts (Prism syntax identifiers)
3. providers in languages/index.ts (LanguageProvider instances)
* feat(web): load source code from server and scroll to selected line
CodeReferencesPanel now fetches file content via GET /api/file when a
node is selected, instead of showing "Code not available in memory".
- Fetches via readFile() from backend-client when selectedFilePath changes
- Shows loading spinner while fetching
- After content loads, auto-scrolls to the selected node's startLine
- Highlights the selected line range with a cyan left border
- Cancels in-flight fetch if selection changes before it completes
Also: refactored language-detection.ts to use exhaustive Record types
(EXTENSION_MAP and SYNTAX_MAP) so adding a new SupportedLanguages enum
member without implementing extensions/syntax is a compile error.
* feat: buffered file reading for Code Inspector
Server: GET /api/file now supports ?startLine=N&endLine=M for reading
a line range instead of the entire file. Returns { content, startLine,
endLine, totalLines }.
Client: readFile() returns ReadFileResult with metadata. When selecting
a symbol (function, class, method), fetches only ±50 lines around the
symbol's startLine/endLine instead of the full file. File nodes still
fetch the entire file.
SyntaxHighlighter startingLineNumber set from the buffer offset so line
numbers are correct even for partial reads.
* fix: adapt readFile callers to new ReadFileResult return type
tools.ts: readFile comes from GraphRAGBackend interface which returns
Promise<string> (the adapter in useAppState extracts .content), so
revert the { content } destructuring back to plain string assignment.
useAppState.tsx: wrap backendReadFile with { repo } options object
and extract .content to satisfy the GraphRAGBackend interface.
* fix(web): ensure new repos appear in list immediately after analysis
Two fixes:
1. DropZone: handleAnalyzeComplete now passes the repoName through to
connectToServer so the specific newly-analyzed repo loads — not the
server's default first repo.
2. App.tsx: fetchRepos() is now awaited BEFORE handleServerConnect in
both the DropZone and Header flows. This ensures the repo list is
populated before the exploring view renders, so the new repo appears
in the header dropdown immediately without a page reload.
* feat: delete repos, re-analyze with force, select after analysis
Server — DELETE /api/repo:
- Acquires repo lock first (409 if analyze/embed in flight)
- Closes LadybugDB, deletes index + clone dir, unregisters, re-inits
- Lock released in finally block
Server — analyze complete:
- backend.init() must succeed before SSE complete fires
- If backend.init() fails, job is marked failed (not complete)
Web — Header repo dropdown:
- Re-analyze: calls POST /api/analyze with force=true, shows spinning
icon + inline progress bar via SSE
- Delete: acquires lock, aborts any running re-analysis SSE for same
repo, refreshes list, switches to next repo
- After analysis completes: refreshes repo list, connects to the
specific repo by name, loads graph, shows in explorer
- Retry with 1.5s backoff on 404 (server may still be reinitializing)
Type safety:
- err: any → err: unknown + instanceof BackendError in retry loop
- Added missing BackendRepo + BackendError imports in App.tsx
Downgrade tree-sitter from ^0.25.0 to ^0.21.1 and align all parser versions to eliminate ERESOLVE peer dependency conflicts that break MCP server install via npx. Also corrects hallucinated tree-sitter-dart SHA. Fixes#537
* feat(phase8): add field type data structures and extractor interface
* feat(phase8): implement TypeScript field extractor
* feat-phase9-add-call-result-binding
* test-phase8-add-field-extraction-unit-tests
* docs: update documentation for Phase 8 and Phase 9
* feat(swift): Phase 8/9 integration tests for field-type and call-result binding
Add Swift field-type resolution and call-result binding integration tests
with fixtures, plus merge-conflict fixes for the FieldExtractor code.
**Swift integration tests:**
- `swift-field-types/` fixture (Models.swift + App.swift) — tests
HAS_PROPERTY edges, field-chain CALLS resolution (user.address.save()
→ Address#save), and ACCESSES edges for field reads.
- `swift-call-result-binding/` fixture — tests call-result binding
(let user = getUser(); user.save() → User#save).
- 2 new describe blocks in swift.test.ts with skipIf(!swiftAvailable).
**Swift arity fix:**
- extractMethodSignature fallback counts direct `parameter` children
when no wrapper list node exists (Swift's tree-sitter grammar places
parameters as direct children of function_declaration). Without this,
all Swift functions had parameterCount: 0 and the arity filter rejected
valid call targets.
**FieldExtractor merge-conflict fixes:**
- field-extractor.ts: update import from removed ./utils.js to
./utils/ast-helpers.js; use typeEnv.fileScope() instead of .get('').
- field-extractors/typescript.ts: same import fix.
- field-types.ts: alias TypeEnvironment as TypeEnv (renamed on main).
- field-extraction.test.ts: mock TypeEnvironment interface properly.
* feat(field-extractors): generic table-driven field extractors for all 14 languages, wired into pipeline
Implements field extractors for all supported languages and integrates
them into the ingestion pipeline as the single source of truth for
Property node metadata.
**Generic field extractor factory** — `field-extractors/generic.ts`
defines a `createFieldExtractor(config)` factory that generates
FieldExtractor instances from a per-language `FieldExtractionConfig`.
Each config specifies AST node types, name/type/visibility extraction
functions, and static/readonly detection — typically 20-40 lines per
language vs 300+ for a hand-written extractor.
**Per-language configs** — `field-extractors/configs/` has 11 config
files covering 13 languages (TS/JS share, Java/Kotlin share).
TypeScript keeps its hand-written extractor for richer handling.
**LanguageProvider integration** — New optional `fieldExtractor` property
on LanguageProviderConfig, set via defineLanguage() in each language
file. Follows the same strategy pattern as typeConfig, exportChecker,
and labelOverride. Removed the separate FieldExtractorRegistry class
and field-extractors/index.ts — extractors are accessed via
getProvider(lang).fieldExtractor.
**Pipeline wiring** — Both parse-worker.ts (worker pool) and
parsing-processor.ts (sequential fallback) now call the FieldExtractor
during Property node creation. Results are cached per class node.
Property nodes are enriched with: declaredType, visibility, isStatic,
isReadonly.
**extractPropertyDeclaredType removed** — The 100-line multi-strategy
function in type-extractors/shared.ts is replaced by the FieldExtractor.
All 14 languages register an extractor, eliminating the need for a
generic fallback. The Python config's extractType was fixed to handle
annotation-without-value patterns (address: Address).
**Integration tests** — Each language's resolver test file gains
pipeline-based assertions verifying visibility/isStatic/isReadonly on
Property nodes via getNodesByLabelFull. Tests run through
runPipelineFromRepo with real fixtures — no direct extractor calls.
* fix(type-env): thread enclosingFunctionFinder through scope resolution, unskip Dart ACCESSES test
The type-env's findEnclosingScopeKey had the same Dart sibling problem
as findEnclosingFunction — it walked parents but never found
function_signature because the call lives inside function_body (a
sibling). Instead of hardcoding a function_body check, thread the
provider's enclosingFunctionFinder hook through BuildTypeEnvOptions →
lookupInEnv → findEnclosingScopeKey. All three buildTypeEnv call sites
(call-processor, parsing-processor, parse-worker) now pass the hook.
This enables the type-env to resolve scoped parameter bindings for Dart
(e.g., `user: User` in processUser), which lets the chain-resolution
tier (Step 1c) walk `user.address` and emit ACCESSES edges.
Dart integration test unskipped — 10/10 passing including ACCESSES.
Reverted CHANGELOG.md to origin/main.
* fix: resolve all PR #494 review findings (10 items)
CRITICAL:
- parse-worker.ts: classNode: any → SyntaxNode on getFieldInfo
and findEnclosingClassNode; removed redundant as number casts
- parsing-processor.ts: classNode: any → SyntaxNode on seqGetFieldInfo
HIGH:
- ruby.ts: attr_accessor now extracts ALL symbol arguments via
extractNames hook in generic factory (was firstNamedChild only)
- typescript.ts: added JSDoc explaining why hand-written extractor
coexists with config-based typescript-javascript.ts
MEDIUM:
- field-types.ts: FieldVisibility union type replaces string
('public'|'private'|'protected'|'internal'|'package'|'fileprivate'|'open')
Propagated through field-extractor.ts, generic.ts, all 7 config files
- typescript.ts: extractFullType collapsed from 12 branches to 3 lines
- generic.ts: added extractNames? optional hook + buildField refactor
LOW:
- ruby.ts: extractVisibility(node) → extractVisibility(_node)
- python.ts: fixed misleading isStatic comment
TypeScript compiles cleanly.
* test: add 24 field extraction tests for generic factory + 5 languages
Generic factory (4 tests):
- createFieldExtractor with TypeScript config validates factory itself
- Body discovery for interfaces, static/readonly modifiers
- Non-type node rejection
Python (4 tests):
- Annotated class field extraction
- Underscore-based visibility: _name=protected, __name=private
Go (5 tests):
- isTypeDeclaration on type_declaration nodes
- Config functions: uppercase=public, lowercase=package visibility
- extractType, isStatic, isReadonly
C++ (5 tests):
- public/private/protected access specifier backward-sibling walk
- Default visibility: class=private, struct=public
- static/const modifier detection
Ruby (6 tests):
- attr_accessor multi-symbol: :name, :email, :age → 3 fields
- attr_reader=readonly, attr_writer=non-readonly
- Multiple attr_* calls in one class
Total: 46 tests passing
* chore: remove plan doc from PR
---------
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* feat: add COBOL language support with regex extraction pipeline
Standalone COBOL processor following the markdown-processor.ts pattern:
- No LanguageProvider modification — COBOL uses regex, not tree-sitter
- No SupportedLanguages enum change — standalone processor pattern
New files:
- cobol-processor.ts — orchestrator (processCobol, isCobolFile, isJclFile)
- cobol/cobol-preprocessor.ts — regex state machine extraction (~888 LOC)
- cobol/cobol-copy-expander.ts — COPY statement expansion with circular detection
- cobol/jcl-parser.ts — JCL job/step/DD extraction
- cobol/jcl-processor.ts — JCL graph node creation
Extraction produces:
- Module nodes (PROGRAM-ID)
- Function nodes (paragraphs)
- Namespace nodes (sections)
- Property nodes (data items)
- CALLS edges (PERFORM intra-file, CALL cross-program)
- IMPORTS edges (COPY statements)
- CONTAINS edges (section → paragraph hierarchy)
Pipeline integration: single processCobol() call in Phase 2.6
54 new tests (33 COBOL + 21 JCL), all 3889 tests pass.
* docs: document custom processor pattern in pipeline.ts
Add comment block at the custom processor integration point
documenting the pattern for future non-tree-sitter language additions.
* feat(cobol): enrich graph with EXEC SQL/CICS, ENTRY points, MOVE data flow, PERFORM THRU
Maps the remaining 60% of CobolRegexResults to the graph:
- EXEC SQL blocks → CodeElement nodes + ACCESSES edges to DB tables
- EXEC CICS LINK/XCTL → CodeElement nodes + cross-program CALLS edges
- ENTRY points → Constructor nodes (registered for cross-program resolution)
- MOVE statements → ACCESSES edges (read/write data flow tracking)
- PERFORM THRU → expanded CALLS edges for range targets
- File declarations → Record nodes with assignment metadata
- Cross-program CALL 2nd pass: resolves unresolved targets after all programs processed
* test(cobol): add 26 integration tests with exact assertions + fix CICS resolution bug
Integration tests (test/integration/resolvers/cobol.test.ts):
- 26 tests covering full COBOL system extraction
- ALL assertions use exact toBe(N) — zero fuzzy assertions
- Fixtures: CUSTUPDT.cbl, AUDITLOG.cbl, CUSTDAT.cpy, RPTGEN.cbl, RUNJOBS.jcl
Bug fix (cobol-processor.ts):
- CICS LINK/XCTL cross-program resolution was broken — edges were
created with "resolved" reason but pointing to <unresolved> targets
- Fix: use cics-link-unresolved / cics-xctl-unresolved suffix pattern
matching the existing cobol-call-unresolved pattern
- Second-pass resolver now patches both CALL and CICS unresolved edges
All 3915 tests pass, 0 failures.
* test(cobol): exhaustive 57-test suite with strict exact assertions
Complete rewrite of COBOL integration tests using ground-truth approach:
dump the full graph, then assert EVERY node and EVERY edge.
57 tests across 9 sections:
- Node completeness: Module(3), Function(13), Namespace(2), Property(21),
Record(1), CodeElement(8), Constructor(1) — exact sorted arrays
- Edge completeness: 22 tests covering every type+reason combination
with exact source→target pairs
- Cross-program resolution: 6 tests verifying CALL, CICS LINK/XCTL, JCL
- COPY expansion: copybook data items in RPTGEN
- Section hierarchy: exact paragraph membership per section
- Data item ownership: exact per-module breakdown
- MOVE data flow: exact read/write pairs
- JCL integration: job/step/dataset containment
- Grand totals: CALLS(22), CONTAINS(48), IMPORTS(1), ACCESSES(7)
Fixture enhancements:
- CUSTUPDT.cbl: added INIT-SECTION + PROCESSING-SECTION, PERFORM THRU
- AUDITLOG.cbl: added ENTRY "AUDITLOG-BATCH"
- RPTGEN.cbl: added EXEC CICS XCTL
Zero fuzzy assertions — every expect uses toBe(N) or toEqual([...sorted]).
* fix(cobol): add removeRelationship API + single-quote CALL/COPY/ENTRY, PERFORM keyword skip
Phase 0A: Add removeRelationship(id) to KnowledgeGraph interface and
implementation (trivial Map.delete wrapper). Required for orphan edge
cleanup in next commit.
Phase 1A (from PR #500 review, modified):
- RE_CALL and RE_COPY_QUOTED now match both "double" and 'single' quotes
- parseSingleCopyStatement in copy-expander updated for single quotes
- PERFORM_KEYWORD_SKIP set prevents UNTIL/VARYING/WITH/TEST/FOREVER
from being stored as false-positive perform targets
- Sequence number stripping uses /[^0-9 ]/ (preserves numeric seq numbers
unlike PR #500's /\S/ which stripped them)
- Normalized || to ?? for regex group extraction in copy-expander
5 new graph unit tests, all 57 COBOL integration tests pass.
* fix(cobol): RE_ENTRY single-quote + remove orphan unresolved CALLS edges
Phase 1B: RE_ENTRY regex now supports both "double" and 'single' quoted
ENTRY targets. Uses named intermediates (entryName, usingClause) with ??
operator. USING capture group shifted from [2] to [3].
Phase 1C: Second-pass resolution now collects resolved orphan edge IDs
during iteration and removes them after the loop completes, using the new
graph.removeRelationship() API. Graph no longer contains phantom
<unresolved>: edges alongside their resolved replacements. CALLS count
drops from 22 to 18 (4 orphan edges removed).
* fix(cobol): Property ID collisions + O(1) Map lookup for MOVE edges
Phase 1D+3C (atomic): Property node IDs now use composite key
filePath:section:level:name instead of filePath:name. This prevents
duplicate data item names in different sections (e.g., STATUS in both
WORKING-STORAGE and LINKAGE) from silently colliding.
New generatePropertyId() helper ensures both node creation and MOVE
edge lookup use the identical key formula. buildDataItemMap() replaces
the O(n) findDataItemNode linear scan with O(1) Map lookup, built once
per file before MOVE processing.
* feat(cobol): MOVE multi-target extraction with OF/IN qualifier filtering
MOVE X TO A B C now produces write edges for all targets, not just the
first. extractMoveTargets() helper handles OF/IN qualified names
(WS-NAME OF WS-RECORD -> target is WS-NAME), subscript stripping
(WS-TABLE(I) -> WS-TABLE), and MOVE_SKIP filtering on targets.
Data model: CobolRegexResults.moves.to:string -> targets:string[]
MOVE CORRESPONDING stays single-target per COBOL standard.
Processor MOVE loop now iterates move.targets.
* feat(cobol): COPY IN/OF library, pseudotext REPLACING, dynamic CALL, PERFORM TIMES, CICS MAP unquoted
Phase 2B: COPY ... IN/OF library-name now captured as metadata in
CopyResolution (IN and OF are synonyms per COBOL-85 standard).
Phase 2C: COPY REPLACING ==pseudotext== support. Tokenizer handles
==...== delimiters alongside "quoted" strings. Pseudotext forces EXACT
type. Two-pass applyReplacing: first pass handles space-containing/
non-identifier pseudotext via global string replace; second pass handles
identifier-level LEADING/TRAILING/EXACT. New test file
cobol-copy-expander.test.ts with 10 tests.
Phase 2E: PERFORM WS-COUNT TIMES no longer produces a false-positive
perform target (checks for TIMES keyword after captured identifier).
Phase 2F: Dynamic CALL via data item (CALL WS-PROG-NAME without quotes)
now emits a CodeElement annotation node with description 'dynamic-call'
instead of silently ignoring. Adds isQuoted:boolean to call results.
Phase 3A: CICS MAP(WS-MAP-NAME) unquoted identifiers now captured.
Phase 3B: Normalized || to ?? in copy-expander (done in Phase 1A).
* feat(cobol): nested program support — capture multiple PROGRAM-IDs per file
Phase 2D: The state machine now captures all PROGRAM-IDs, not just the
first. The primary program name stays in programName; additional nested
programs go into nestedPrograms[]. The processor creates separate Module
nodes for each nested program, contained by the outer module, and
registers them in moduleNodeIds for cross-program CALL resolution.
Paragraphs/data items are not yet scoped per-program (attributed to the
outer module) — full per-program scoping is a future enhancement that
requires END PROGRAM boundary tracking in the state machine.
* test(cobol): expand integration tests for all new language features
New fixtures:
- NESTED.cbl — two PROGRAM-IDs (OUTER-PROG, INNER-PROG) for nested
program support testing
- COPYLIB.cpy — copybook for pseudotext REPLACING test target
Modified fixtures:
- CUSTUPDT.cbl — single-quoted ENTRY 'ALTENTRY', multi-target MOVE
(WS-AMT TO FIELD-A FIELD-B), dynamic CALL WS-PROG-NAME, COPY COPYLIB
with pseudotext REPLACING, LINKAGE SECTION with LS-PARAM
- RPTGEN.cbl — PERFORM WS-COUNT TIMES (false-positive guard), unquoted
MAP(WS-MAP-NAME), additional data items WS-COUNT WS-MAP-NAME
Integration test rewritten with 62 exact assertions covering:
- 5 Module, 17 Function, 33 Property, 9 CodeElement, 2 Constructor nodes
- Nested program containment (OUTER-PROG -> INNER-PROG)
- Dynamic CALL annotation (CodeElement with cobol-dynamic-call)
- Multi-target MOVE (UPDATE-BALANCE: 2 reads, 3 writes)
- Single-quoted ENTRY (ALTENTRY under CUSTUPDT)
- PERFORM TIMES guard (WS-COUNT not in CALLS)
- Orphan unresolved edge removal (zero -unresolved edges)
- Grand totals: 21 CALLS, 68 CONTAINS, 2 IMPORTS, 10 ACCESSES
* fix(cobol): pseudotext REPLACING now applies correctly via isPseudotext flag
Root cause: ==PREFIX-== matched /^[A-Z][A-Z0-9-]*$/i (trailing hyphens
allowed), routing it to the second-pass EXACT identifier match where
PREFIX-RECORD !== PREFIX- failed silently.
Fix: Propagate isPseudotext from parseReplacingClause to CopyReplacing
interface, then use it in applyReplacing first-pass condition to force
global string replacement for all pseudotext entries regardless of
whether the content looks like an identifier.
Result: COPY COPYLIB REPLACING ==PREFIX-== BY ==WS-==. now correctly
transforms PREFIX-RECORD → WS-RECORD, PREFIX-CODE → WS-CODE, etc.
* refactor(cobol): per-program scoping via boundary tracking + line-range grouping
State machine changes (minimal, ~30 lines):
- Add RE_END_PROGRAM regex for END PROGRAM program-name. detection
- Replace nestedPrograms[] with programs[] containing startLine/endLine/
nestingDepth metadata for each PROGRAM-ID in the file
- Reset division/section/paragraph state on new PROGRAM-ID boundary
- EOF finalization flushes remaining stack entries (single-program files)
- Programs sorted by startLine (outer before inner)
Processor changes:
- Uses programs[] with line-range containment to find enclosing parent
Module for nested programs (replaces hardcoded nestedParent logic)
- programModuleIds Map tracks Module node IDs per program name
Fixture: NESTED.cbl now includes END PROGRAM lines for both programs.
Integration test: PREFIX-* Property nodes now correctly appear as WS-*
after the pseudotext REPLACING fix from the previous commit.
* feat(cobol): free-format COBOL support (>>source free)
Auto-detects >>SOURCE FREE directive in the first 500 chars and switches
to free-format line processing:
- No column-position rules (cols 1-6 are program text, not sequence area)
- Comments use *> prefix instead of col 7 indicator
- No continuation line indicator
- Strip inline *> comments
- Skip >>SOURCE directive lines
preprocessCobolSource() skips col-1-6 stripping for free-format files.
Paragraph/section regexes relaxed from fixed 7-space prefix to flexible
whitespace with case-insensitivity (/^\s*([A-Z][A-Z0-9-]+)\.\s*$/i).
EXCLUDED_PARA_NAMES expanded with COBOL verbs (GOBACK, END-READ, etc.)
to prevent false-positive paragraph detection in free-format.
Also fixes: entry-point-scoring.ts crash when language is 'cobol'
(MERGED_ENTRY_POINT_PATTERNS[language] was undefined → optional chaining).
Benchmark on ACAS 3.01 (268 GnuCOBOL free-format programs, 10MB):
- Before: 407 nodes, 393 edges (near-empty, only file nodes)
- After: 4,297 nodes, 3,612 edges, 542 clusters, 11 flows
* fix(cobol): relax data item regexes for free-format (^\s+ to ^\s*)
RE_FD, RE_DATA_ITEM, RE_ANONYMOUS_REDEFINES, and RE_88_LEVEL all used
^\s+ which requires at least 1 leading space. In free-format mode, lines
are trimmed before processing, so data items like "01 WS-FIELD PIC X."
have no leading whitespace after trimming.
Changed to ^\s* (zero or more spaces) which works for both fixed-format
(indented lines still have spaces) and free-format (trimmed lines).
ACAS benchmark (268 GnuCOBOL programs):
- Before: 4,297 nodes, 3,612 edges (paragraphs only)
- After: 13,832 nodes, 8,615 edges (+ data items, FDs, 88-levels)
* feat(cobol): 100% structural feature coverage — GO TO, SCREEN, SD/RD, SORT, SEARCH, CANCEL, Level 66
New extractions: GO TO (CALLS edges), SCREEN SECTION data items,
SD/RD alongside FD (Record nodes), SORT/MERGE USING/GIVING (ACCESSES),
SEARCH (ACCESSES), CANCEL (CALLS), Level 66 RENAMES (Property),
IS EXTERNAL/IS GLOBAL (Property description enrichment).
ACAS: 13,951 nodes | 13,193 edges | 685 clusters | 150 flows
(+53% edges from new GO TO/SORT/SEARCH/CANCEL extractions)
* feat(cobol): enriched CICS extraction — file I/O, dynamic PROGRAM, queues, HANDLE ABEND
EXEC CICS blocks now extract:
- FILE/DATASET clause: captures VSAM file name (literal or data item ref)
for READ/WRITE/REWRITE/DELETE/STARTBR/READNEXT/READPREV → ACCESSES edges
- PROGRAM clause: now handles unquoted variable references (dynamic CICS
program transfer) → CodeElement annotation with cics-dynamic-program reason
- QUEUE clause: captures TS/TD queue names from WRITEQ/READQ → ACCESSES edges
- LABEL clause: captures HANDLE ABEND error handler targets → CALLS edges
- TRANSID: now handles unquoted variable references
CodeElement descriptions enriched with all captured fields (map, program,
transid, file, queue, label).
CardDemo benchmark: +49 nodes, +33 edges from enriched CICS extraction.
* feat(cobol): complete CICS command extraction — all 7 expert recommendations
From COBOL expert agent analysis:
1. ENDBR added to isRead file command list
2. LOAD added to PROGRAM edge commands (alongside LINK/XCTL)
3. Two-word commands expanded: WRITEQ/READQ/DELETEQ TS/TD, HANDLE
ABEND/AID/CONDITION, START TRANSID
4. Queue reason differentiated: cics-queue-read/-write/-delete
5. RETURN/START TRANSID → CALLS edges to synthetic <transid> target
6. MAP → ACCESSES edges for screen traceability
7. INTO/FROM data fields extracted → ACCESSES edges to data items
Also: dataItemMap built before CICS block processing (was declared after),
CodeElement descriptions enriched with all captured CICS fields.
* test(cobol): strict exhaustive integration tests with exact edgeSet assertions
Every edge reason has exact sorted pair assertions via edgeSet(), not
just counts. Any change to extraction that adds, removes, or reorders
edges will produce a precise, descriptive failure.
Updated RPTGEN.cbl fixture with:
- GO TO EXIT-PARAGRAPH, SORT USING/GIVING, SEARCH table
- EXEC CICS READ FILE INTO, WRITEQ TS QUEUE FROM, SEND MAP FROM
- EXEC CICS HANDLE ABEND LABEL, RETURN TRANSID, XCTL PROGRAM(variable)
- ABEND-HANDLER and EXIT-PARAGRAPH paragraphs
46 tests covering 24 CALLS + 79 CONTAINS + 18 ACCESSES + 2 IMPORTS edges
across 15 distinct edge reason codes, all with exact sorted pair lists.
* fix(cobol): address 5 findings from second Claude review (compiler front-end perspective)
Finding #2: Numeric sequence numbers now stripped (changed /[^0-9 ]/ to
/\S/ in preprocessCobolSource). Lines like "000100 MAIN-PARAGRAPH." now
have cols 1-6 blanked so paragraph regex matches correctly.
Finding #11: JCL in-stream PROC ordering fixed — pre-register all PROCs
into moduleNames before step processing. Steps that EXEC a PROC defined
later in the same file now get CALLS edges.
Finding #A: PROCEDURE DIVISION USING no longer captures calling-convention
keywords (BY, VALUE, REFERENCE, CONTENT, ADDRESS, OF) as parameter names.
Finding #C: SORT/MERGE USING/GIVING now captures ALL file references
(multi-file), not just the first. Changed from single-match to section
extraction with split.
Finding #D: Section headers no longer set currentParagraph, preventing
PERFORM caller misattribution to Namespace instead of Function nodes.
* fix(cobol): address code review findings — ReDoS fix, perf, cleanup
P1 CRITICAL — ReDoS in SORT USING/GIVING:
Replaced nested-quantifier regex with safe indexOf+substring+split
approach. No backtracking possible on crafted input.
P2 — readCopy O(M) linear scan:
Added copybookByPath reverse Map for O(1) path-to-content lookup.
P3 — Dead code removal:
Deleted unused RE_SORT_USING and RE_SORT_GIVING constants.
P3 — EXCLUDED_PARA_NAMES simplification:
Replaced 20 END-* entries with startsWith('END-') prefix check.
Auto-covers future END-* verbs.
P3 — Misplaced JSDoc on removeRelationship:
Fixed comment that described removeNodesByFile instead.
Added missing JSDoc to removeNodesByFile.
Review agents: architecture-strategist, performance-oracle,
security-sentinel, code-simplicity-reviewer.
* refactor: add Cobol to SupportedLanguages with parseStrategy: standalone
New languages/cobol.ts — standalone regex processor provider with no-op
tree-sitter fields. Declares parseStrategy: 'standalone' to distinguish
from tree-sitter-based languages.
Added parseStrategy: 'tree-sitter' | 'standalone' to LanguageProviderConfig
for languages that use their own processor instead of tree-sitter.
Removed all 11 'cobol' as any casts — now uses SupportedLanguages.Cobol.
Added empty Cobol entries to entry-point-scoring and framework-detection.
* fix(cobol): 5 fixes from third Claude review + 3 regression tests
Fixes:
- Line numbers now 1-indexed in fixed-format (was 0-indexed, off-by-one
in jump-to-definition links)
- Copybook content preprocessed before COPY expansion (sequence numbers
and patch markers in copybooks no longer survive into expanded source)
- ENTRY USING filters calling-convention keywords (BY, VALUE, REFERENCE,
CONTENT, ADDRESS, OF) — same fix as PROCEDURE DIVISION USING
- SORT/MERGE trailing period stripped from USING/GIVING file tokens
- Paragraph exclusion uses exact match for SECTION/DIVISION (was substring
match that excluded valid names like CROSS-SECTION-ANALYSIS)
USING_KEYWORDS moved to module scope for reuse by both PROCEDURE DIVISION
USING and ENTRY USING handlers.
New unit tests:
- ENTRY USING BY VALUE filtering
- Paragraph names containing SECTION not excluded
- Numeric sequence numbers stripped enabling paragraph detection
* fix(cobol): address 6 findings from fourth Claude review + tests
Fourth review findings fixed:
- New #IV: PERFORM TIMES guard uses perfMatch.index instead of
line.indexOf (prevents wrong match when target appears earlier in line)
- New #V: 88-level condition values now handle single-quoted literals
('Y' no longer stored with embedded quotes)
- New #I: CANCEL edges use two-pass resolution like CALL (no longer
silently dropped when target indexed after source)
- New #3: Multi-line SORT/MERGE accumulation — sortAccum state variable
accumulates lines until period, then extracts USING/GIVING from full
statement (95% of production SORT statements span multiple lines)
- New #II: PROCEDURE DIVISION USING on split lines — pendingProcUsing
flag defers parameter capture to next line if USING not on same line
- New #6 (prior): EXCLUDED_PARA_NAMES exact match for SECTION/DIVISION
Updated fixture: RPTGEN.cbl SORT now uses multi-line format with GIVING
on separate line (period-terminated). New sort-giving integration test.
ACCESSES total: 18 → 19 (new sort-giving edge from multi-line capture).
* fix(cobol): address 4 findings from fifth Claude review
Finding #B (5 reviews old): Section/paragraph node IDs now include
enclosing program name to prevent collision when nested programs share
section/paragraph names. New findOwningProgramName() helper uses
programs[] line ranges to find the innermost enclosing program.
Finding #α: pendingProcUsing now reset in the if(procUsingMatch) branch
(was only set in else branch, could leak across nested programs).
Finding #β: RE_CALL_DYNAMIC uses negative lookbehind (?<![A-Z0-9-]) to
prevent false-positive on compound identifiers like WS-CALL OCCURS.
Finding #γ: sortAccum flushed at EOF (parallel to flushSelect and
pendingFdName EOF cleanup). Prevents silent loss of SORT USING/GIVING
relationships in truncated files.
* fix(cobol): address findings from reviews 5+6 with full test coverage
Review 5 fixes:
- #α: pendingProcUsing reset in if(procUsingMatch) branch
- #β: RE_CALL_DYNAMIC negative lookbehind prevents WS-CALL false positive
- #γ: sortAccum flushed at EOF for truncated files
- #B: Section/paragraph IDs include owning program name
Review 6 fixes:
- #P: sectionNodeIds/paraNodeIds maps use program-scoped keys
(PROGNAME:NAME). New scopedParaLookup/scopedCallerLookup helpers.
findContainingSection updated with programs parameter.
- #Q: RETURNING added to USING_KEYWORDS for COBOL 2002+
- #R: RE_PERFORM matches both THRU and THROUGH via alternation
New unit tests (6):
- PERFORM THROUGH captures thruTarget
- PROCEDURE DIVISION USING RETURNING filters keyword
- RE_CALL_DYNAMIC no false-match on WS-CALL compound identifier
- Multi-line SORT captures USING/GIVING from continuation lines
- PROCEDURE DIVISION USING on split line via pendingProcUsing
- Copybook preprocessing strips sequence numbers
* fix(cobol): address findings from seventh Claude review + 3 tests
Review 7 fixes:
- #i: findContainingSection only updates best when lookup succeeds
(prevents undefined overwriting valid parent section)
- #ii: RE_PROC_SECTION handles segment numbers (SECTION 30.)
- #III: procedureUsing now stored per-program on boundary stack
entries, propagated to programs[] output. Inner programs no longer
overwrite outer program's parameters.
- #δ: Dynamic CANCEL (CANCEL variable) now creates CodeElement
annotation node, matching dynamic CALL behavior. RE_CANCEL_DYNAMIC
with negative lookbehind. cancels[] gains isQuoted field.
- #Q: RETURNING added to USING_KEYWORDS (already in prev commit)
- #R: PERFORM THROUGH already fixed (THRU|THROUGH alternation)
New unit tests:
- Nested programs carry per-program procedureUsing
- SECTION with segment number detected
- Dynamic CANCEL via data item captured with isQuoted=false
* feat(cobol): link PROCEDURE DIVISION USING to LINKAGE data items + close 4 findings
Finding #10 FIXED: procedureUsing parameters now create ACCESSES edges
with reason 'cobol-procedure-using' from Module to matching LINKAGE
SECTION Property nodes. This exposes the program's parameter contract
in the graph (e.g., AUDITLOG → LS-CUST-ID, AUDITLOG → LS-AMOUNT).
Findings closed by expert agent consensus:
- #6 COPY IN library: WONTFIX — captured metadata, no universal
library-to-directory mapping exists. Field costs nothing and is useful
for library queries.
- #14 SQL DELETE: WONTFIX — DB2 requires FROM; existing FROM pattern
handles it. Bare DELETE would risk false positives.
- #E OCCURS DEPENDING ON: WONTFIX — runtime sizing concern, not
structural. The static occurs count is sufficient for indexing.
All 39 findings from 7 Claude reviews now resolved or closed.
* fix(cobol): resolve 48 review findings across 9 review cycles
Ninth deep review resolved all remaining COBOL parser gaps identified
by 5 specialist agents (COBOL expert, architecture strategist,
TypeScript reviewer, security sentinel, code simplicity reviewer).
Fixes (P1 — critical):
- SELECT OPTIONAL now correctly skips OPTIONAL keyword (C1)
- RETURNING params excluded from PROCEDURE DIVISION USING list (C7)
- SORT GIVING no longer captures clause keywords as file names (C5)
- Extract flushSort() helper eliminating 40-line duplication (S2)
- Flush unclosed EXEC blocks at EOF matching SORT/SELECT pattern (S3)
- Guard undefined map key in jcl-processor moduleNames (S1)
- Add MAX_TOTAL_EXPANSIONS=500 to prevent exponential COPY breadth (S4)
Fixes (P2 — important):
- Quote-aware stripInlineComment for | and *> in string literals (C2+C3)
- Fixed-format literal continuation now handles quoted strings (C6)
- PROGRAM-ID detected regardless of division state for siblings (C9)
Fixes (P3 — cleanup):
- EXEC SQL INTO restricted to INSERT INTO to avoid FETCH false-pos (C8)
- Copy expander line numbers fixed from 0-based to 1-based (C11)
- Remove dead code: inInStreamProc, fileIsLiteral, expansionDepth (S7-S10)
Also fixes 8th-review findings: nested program CONTAINS attribution,
multi-PERFORM on same line, INPUT/OUTPUT PROCEDURE IS in SORT,
GO TO DEPENDING ON multi-target, MOVE CORR abbreviation, per-program
procedureUsing ACCESSES edges.
Tests: 145 COBOL tests passing (59 integration + 86 unit)
Benchmarks: CardDemo 12,323 nodes/8,893 edges (7.4s)
ACAS 14,016 nodes/15,452 edges (9.3s, -9% faster)
* docs(cobol): update documentation for ninth review cycle fixes
Update all 4 COBOL documentation files to reflect the 16 fixes
from the ninth review cycle:
- regex-extraction.md: quote-aware comment stripping, SELECT OPTIONAL,
RETURNING exclusion, SORT_CLAUSE_NOISE filter, flushSort() helper,
GO TO multi-target, PROGRAM-ID division-independent detection
- copy-expansion.md: MAX_TOTAL_EXPANSIONS=500 breadth guard, 1-based
line numbers, removed expansionDepth/warnedCircular param
- deep-indexing.md: GO TO DEPENDING ON, INPUT/OUTPUT PROCEDURE IS,
MOVE CORR edge reasons, INSERT INTO restriction, literal continuation
- performance.md: updated benchmarks (CardDemo 12,323n/8,893e/7.4s,
ACAS 14,016n/15,452e/9.3s), COPY breadth guard
* fix(cobol): resolve 10th review findings — nested program edge attribution
Fix 6 findings from the 10th review (PR #498 comment #4132201110):
#A+#F: All CALL/CANCEL/CICS/ENTRY/SQL/SEARCH/file-declaration edges
now use owningModuleId() for nested program attribution instead of
the outer program's parentId. Added helper function owningModuleId()
to centralize the pattern.
#B: Added USING and GIVING to SORT_CLAUSE_NOISE set to prevent MERGE
USING + OUTPUT PROCEDURE from capturing clause keywords as file names.
#C: INPUT/OUTPUT PROCEDURE regex now captures optional THRU/THROUGH
range end paragraph, mirroring RE_PERFORM's THRU support.
#D: scopedCallerLookup fallback now uses programModuleIds.get(pgm)
instead of parentId, so PERFORM/MOVE/GOTO in nested programs with
unresolvable paragraphs fall back to the correct inner module.
#E: pendingProcUsing only set when PROCEDURE DIVISION line is NOT
period-terminated, preventing false USING expectation.
Tests: 145 passing | TypeScript clean
* fix(cobol): resolve 10th review findings — nested program edge attribution
Fix 6 findings from the 10th review (PR #498 comment #4132201110):
#A+#F: All CALL/CANCEL/CICS/ENTRY/SQL/SEARCH/file-declaration edges
now use owningModuleId() for nested program attribution instead of
the outer program's parentId. Added helper function owningModuleId()
to centralize the pattern.
#B: Added USING and GIVING to SORT_CLAUSE_NOISE set to prevent MERGE
USING + OUTPUT PROCEDURE from capturing clause keywords as file names.
#C: INPUT/OUTPUT PROCEDURE regex now captures optional THRU/THROUGH
range end paragraph, mirroring RE_PERFORM's THRU support.
#D: scopedCallerLookup fallback now uses programModuleIds.get(pgm)
instead of parentId, so PERFORM/MOVE/GOTO in nested programs with
unresolvable paragraphs fall back to the correct inner module.
#E: pendingProcUsing only set when PROCEDURE DIVISION line is NOT
period-terminated, preventing false USING expectation.
Tests: 145 passing | TypeScript clean
* fix(cobol): resolve 11th review findings — final nested program + multi-CALL gaps
#1: scopedCallerLookup(null) now uses owningModuleId(lineNum) instead
of parentId, fixing PERFORM/MOVE/GOTO before first paragraph in nested
programs.
#2+#3: CALL and CANCEL extraction now uses matchAll (global flag) to
capture multiple occurrences on the same line. Dynamic CALL/CANCEL
checked independently instead of in else branch.
#4: SORT/MERGE ACCESSES edge IDs now use owningModuleId(sort.line)
instead of parentId for nested program correctness.
#5: preprocessCobolSource free-format detection now uses first 10 lines
(consistent with extractCobolSymbolsWithRegex threshold).
#6: EXCLUDED_PARA_NAMES expanded with DISPLAY, ACCEPT, WRITE, READ,
REWRITE, DELETE, OPEN, CLOSE, RETURN, RELEASE, SORT, MERGE to prevent
false-positive paragraph detection on isolated verbs.
Also removed unused GraphNode import from cobol-processor.ts.
Tests: 145 passing | TypeScript clean
* docs(cobol): deepened full language coverage plan with research findings
3 research agents analyzed Phase 1-2 features and graph value ranking.
Key findings: cobol-call-using is #1 edge type (9.2/10); multi-line
accumulation is dominant challenge; DECLARATIVES is lowest-risk Phase 2
item; SET TO TRUE covers 80-90% of SET usage.
* feat(cobol): implement Phase 1 — high-value data flow edges
4 new extraction features that create new ACCESSES and IMPORTS edges:
1.1: EXEC SQL INCLUDE -> IMPORTS edges with reason 'sql-include'
Handles unquoted (SQLCA), quoted ('DBRMLIB.MEMBER'), and
underscored (CUST_TBL_DCL) member names.
1.2: CALL USING parameter extraction -> ACCESSES edges
Extracts parameters from CALL USING clause, filtering BY/REFERENCE/
CONTENT/VALUE/ADDRESS/OF/LENGTH/OMITTED keywords. Creates
'cobol-call-using' ACCESSES edges (graph value: 9.2/10).
1.4: OCCURS DEPENDING ON -> ACCESSES edges with reason 'cobol-depends-on'
Extended OCCURS regex captures DEPENDING ON field with subscript
stripping. Creates dependency edge from table to controlling field.
1.5: VALUE clause for standard data items
Extracts VALUE from data item clauses: quoted strings with type
prefix (X/N/G/B), ALL literals, numerics (incl negative/decimal),
and figurative constants. Populates Property node values.
Tests: 145 passing (+2 ACCESSES from CALL USING) | TypeScript clean
* feat(cobol): implement Phase 2 — DECLARATIVES, SET, INSPECT, EXEC DLI
4 new extraction features for error handling, data flow, and IMS/DB:
2.1: EXEC DLI (IMS/DB) -> CodeElement + ACCESSES edges
Accumulates EXEC DLI blocks like EXEC SQL. Parses DLI verbs
(GU, GN, ISRT, REPL, DLET, CHKP, SCHD, TERM). Extracts
SEGMENT, PCB, INTO/FROM, PSB. Creates dli-{verb} ACCESSES
edges to <ims>:segment Record nodes.
2.2: DECLARATIVES / USE AFTER EXCEPTION -> ACCESSES edges
Tracks inDeclaratives state. Detects USE AFTER STANDARD
EXCEPTION ON file-name. Creates cobol-error-handler ACCESSES
edge from handler section to file Record.
2.3: SET statement -> ACCESSES edges
Detects SET TO TRUE (80-90% of SET usage) and SET index
TO/UP BY/DOWN BY. Creates cobol-set-condition / cobol-set-index
write edges + cobol-set-read for identifier values.
2.4: INSPECT -> ACCESSES edges with multi-line accumulator
Accumulates INSPECT until period (like SORT). Extracts inspected
field + tally counters. Creates cobol-inspect-read/write/tally
edges. Form detection: tallying/replacing/converting/combined.
Preprocessor: 1398 -> 1597 LOC (+199). Tests: 145 passing.
* feat(cobol): implement Phase 3 — completeness fixes
6 partial features fixed to first-class support:
3.1: CALL RETURNING -> ACCESSES write edge (cobol-call-returning)
3.2: SELECT OPTIONAL flag preserved in FileDeclaration + Record node
3.3: ALTERNATE RECORD KEY extraction (matchAll for multiple keys)
3.4: COMMON attribute on nested programs (RE_PROGRAM_ID extended)
3.5: IS EXTERNAL / IS GLOBAL as first-class boolean properties
(removed usage string hack)
3.6: AUTHOR / DATE-WRITTEN mapped to Module node description
Tests: 145 passing | TypeScript clean
* feat(cobol): implement Phase 4 — INITIALIZE + metadata completeness
4.1: INITIALIZE statement -> ACCESSES write edge (cobol-initialize)
4.2: DATE-COMPILED and INSTALLATION paragraphs extracted and mapped
to Module node description alongside existing AUTHOR/DATE-WRITTEN
All 4 plan phases complete. Coverage: ~95% (up from 71.9%).
Tests: 145 passing | TypeScript clean
* test(cobol): add 24 unit tests for Phase 1-4 features
Coverage for all new extraction features:
Phase 1 (8 tests):
- EXEC SQL INCLUDE (unquoted, quoted, underscored)
- CALL USING (simple, mixed modes, ADDRESS OF, OMITTED)
- CALL RETURNING
- OCCURS DEPENDING ON
- VALUE clause (string, numeric, figurative constant)
Phase 2 (10 tests):
- EXEC DLI GU/ISRT/SCHD (verb, segment, PCB, INTO, FROM, PSB)
- DECLARATIVES USE AFTER EXCEPTION (single + multiple sections)
- SET TO TRUE, SET index UP BY
- INSPECT TALLYING, INSPECT REPLACING
Phase 3-4 (6 tests):
- SELECT OPTIONAL flag
- ALTERNATE RECORD KEY
- PROGRAM-ID IS COMMON
- IS EXTERNAL / IS GLOBAL booleans
- INITIALIZE extraction
- Full programMetadata (AUTHOR, DATE-WRITTEN, DATE-COMPILED, INSTALLATION)
Total: 168 tests passing (145 + 24 - 1 removed duplicate)
* fix(cobol): use /\r?\n/ split for Windows CRLF compatibility
All 4 COBOL source files now split on /\r?\n/ instead of '\n' to
handle CRLF line endings on Windows. Previously, trailing \r in
lines caused RE_GOTO's $ anchor to fail on multi-line GO TO
DEPENDING ON statements, producing only 1 goto edge instead of 4.
Files fixed: cobol-preprocessor.ts (2 sites), cobol-processor.ts,
jcl-parser.ts, cobol-copy-expander.ts
Tests: 168 passing | TypeScript clean
* fix(cobol): resolve 12th review — dynamic CALL/CANCEL dedup + trailing anchors
#1+#2: Removed incorrect hasQuotedCall/hasQuotedCancel deduplication
guards. RE_CALL_DYNAMIC and RE_CANCEL_DYNAMIC require [A-Z] after
CALL/CANCEL, so they CANNOT match quoted targets — the guards were
both unnecessary and actively harmful, suppressing dynamic CALL/CANCEL
in ON EXCEPTION patterns.
#3+#5: Changed RE_CALL_DYNAMIC and RE_CANCEL_DYNAMIC trailing anchor
from (?:\s|\.) to (?=\s|\.|$) (lookahead). The consuming anchor
failed when the identifier was the last token on a physical line.
Tests: 168 passing | TypeScript clean
* feat(cobol): add CALL accumulator + fix SORT double-statement (#4, #6)
Finding #4: Multi-line CALL USING accumulator
Added callAccum state variable that accumulates CALL statements
spanning multiple physical lines until period or END-CALL is found.
Uses flushCallAccum() to re-extract CALL target + USING parameters
from the full accumulated statement. This fixes the silent loss of
ACCESSES parameter edges when USING appears on lines after CALL.
Finding #6: SORT double-statement on same line
After flushSort(), the code now falls through to re-check the
current line for a new SORT/MERGE start (was previously blocked
by the sortAccum === null check evaluating before flushSort ran).
Also fixed: used non-global regex for CALL detection test to avoid
the classic global regex .test() lastIndex bug.
Tests: 168 passing (+1 ACCESSES from multi-line CALL USING)
* fix(cobol): resolve 13th review — CICS LOAD, USING extraction, file scoping
#1: CICS LOAD unresolved edge no longer silently deleted in second pass.
Changed narrow cics-link/cics-xctl check to catch-all pattern:
rel.reason?.startsWith('cics-') && rel.reason.endsWith('-unresolved')
#2: flushCallAccum USING extraction now stops before COBOL statement
verbs (INSPECT, SEARCH, SORT, MERGE, DISPLAY, ACCEPT, MOVE, PERFORM,
GO TO, CALL, IF, EVALUATE). Prevents absorbing adjacent statements
as false USING parameters in legacy pre-COBOL-85 code without END-CALL.
#3: CICS FILE Record nodes now globally-scoped (<cics-file>:FILENAME)
instead of per-file-scoped. Enables cross-program CICS file access
analysis, consistent with SQL table scoping (<db>:TABLE).
#4: callAccum pre-check regex now has (?<![A-Z0-9-]) lookbehind to
prevent false activation on compound identifiers like WS-CALL-FLAG.
Tests: 168 passing | TypeScript clean
* fix(cobol): resolve 14th review — callAccum false paragraph + Area A guard
#1: callAccum continuation lines now check for COBOL statement verb
starts (GO TO, PERFORM, MOVE, etc.) and paragraph/section headers.
If detected, the CALL is flushed as-is and the line processed
normally — prevents false paragraph detection and currentParagraph
corruption from lines like "WS-ADDR." being treated as paragraphs.
#4: callAccum pre-check now guarded by currentDivision === 'procedure'
to prevent unnecessary activations in DATA DIVISION.
#5: Fixed-format paragraph detection now rejects lines with >7 leading
spaces (Area B indentation) as paragraph candidates. Paragraph
names in fixed-format must start in Area A (col 8-11, max 7 spaces).
Free-format mode is unaffected.
Tests: 168 passing | TypeScript clean
* fix(cobol): resolve 15th review — callAccum Area A + verb boundary fixes
#A: Column-position-aware paragraph detection in callAccum flush.
#B: inspectAccum early-flush on paragraph/section/verb headers.
#C: Verb boundary \b → (?:\s|$) prevents MOVE-COUNT false flush.
* test(cobol): add 17 edge-case regression tests + fix USING verb boundary
17 new tests covering all recurring review patterns:
Multi-line CALL USING (7 tests):
- Parameters on separate continuation lines (IBM mainframe style)
- No absorption of INSPECT/GO TO/paragraphs following CALL
- END-CALL scope terminator
- Hyphenated identifiers (MOVE-COUNT) not triggering false flush
- Dual quoted+dynamic CALL on same line (ON EXCEPTION)
Nested program attribution (2 tests):
- CALL in inner program within inner line range
- PERFORM before first paragraph has null caller
CRLF compatibility (1 test):
- GO TO DEPENDING ON with \r\n line endings
Area A paragraph detection (2 tests):
- Area B (>7 spaces) rejected; Area A (7 spaces) accepted
SORT/MERGE (1 test): COLLATING SEQUENCE keywords not captured
PROCEDURE USING (2 tests): RETURNING excluded, period-terminated
Comment stripping (1 test): pipe in quoted string preserved
SELECT OPTIONAL (1 test): correct file name, not OPTIONAL keyword
Bug fix: USING extraction regex verb terminators changed from
\bVERB\b to \bVERB(?=\s|$) in flushCallAccum — prevents truncation
on hyphenated identifiers like MOVE-COUNT, PERFORM-LIMIT.
Total: 185 tests passing
* test(cobol): add 32 comprehensive edge-case regression tests
13 new describe blocks covering all extraction features:
- EXEC DLI: no-SEGMENT, multi-line accumulation (2 tests)
- SET: multiple targets, DOWN BY, TO numeric (3 tests)
- INSPECT: CONVERTING, multiple counters, tallying-replacing,
paragraph flush during accumulation (4 tests)
- DECLARATIVES: no-STANDARD keyword, I-O mode, post-END paragraphs (3)
- COPY REPLACING: pseudotext deletion ==OLD== BY ==== (1 test)
- VALUE: hex literal, negative numeric, ALL literal (3 tests)
- OCCURS: TO range, fixed-size without DEPENDING ON (2 tests)
- Dynamic CALL/CANCEL: end-of-line, multiple CANCELs (3 tests)
- EXEC SQL: INCLUDE skips tables, SELECT INTO host vars, host
variable extraction (3 tests)
- INITIALIZE: target and caller context (1 test)
- Nested programs: sibling scoping, PROGRAM-ID without ID DIV (2)
- EXEC EOF flush: unclosed EXEC SQL flushed (1 test)
- Multi-PERFORM: IF/ELSE dual PERFORM on single line (1 test)
- IS EXTERNAL: USAGE not polluted by external flag (1 test)
Total: 215 tests passing
* fix(cobol): resolve 16th review — CANCEL in CALL block + USING boundary
#1: flushCallAccum now extracts CANCEL statements from within CALL
ON EXCEPTION blocks. Adds RE_CANCEL + RE_CANCEL_DYNAMIC matchAll
passes alongside existing CALL extraction.
#2: Added \bCANCEL(?=\s|$) to USING lookahead regex to prevent CANCEL
keyword being captured as false USING parameter.
#3: Multi-line CALL start now returns immediately to prevent the CALL
start line from simultaneously feeding sortAccum/inspectAccum.
#6: Division transitions now flush all active accumulators (callAccum,
sortAccum, inspectAccum) to prevent state leakage across programs.
Also added CANCEL to callAccum flush trigger verb list.
Tests: 215 passing | TypeScript clean
* refactor(cobol): extract shared verb constants + resolve 17th review
Extract COBOL_STATEMENT_VERBS, RE_STATEMENT_VERB_START, and
RE_USING_PARAMS as shared constants — eliminates 4 duplicated
25-verb regex patterns.
17th review: #1 flushCallAccum before EXEC entry, #2 inspectAccum
verb parity via shared constant.
Tests: 215 passing | TypeScript clean
* test(cobol): replace all fuzzy assertions with exact toBe checks
Replaced 7 toBeGreaterThan/toBeLessThan/toBeGreaterThanOrEqual
assertions with exact toBe values:
- dataItems.length: >= 3 → toBe(3)
- calls.length: >= 1 → toBe(1)
- calls[0].line: range check → toBe(10)
- programs[].startLine/endLine: comparison → exact values
- innerA.endLine/innerB.startLine: comparison → exact values
Also added 11 new edge-case tests (accumulator flush on EXEC/division
transitions, free-format, CANCEL in CALL block, SORT THRU, verb
flush, integration).
226 tests passing — zero fuzzy assertions remain.
* fix(cobol): resolve 19th review + 15 accumulator flush tests
Fixes:
#1: END PROGRAM flushes callAccum/sortAccum/inspectAccum
#2: PROGRAM-ID sibling path flushes all accumulators
#3: Added COMPUTE/ADD/SUBTRACT/MULTIPLY/DIVIDE/STRING/UNSTRING
to COBOL_STATEMENT_VERBS (now 32 verbs)
Tests (15 new):
- END PROGRAM flush: single + nested programs (2)
- PROGRAM-ID sibling flush (1)
- Arithmetic verb flush: COMPUTE/ADD/SUBTRACT/MULTIPLY/DIVIDE (5)
- String verb flush: STRING/UNSTRING (2)
- Arithmetic not captured as false USING params (1)
- SORT flushed at END PROGRAM (1)
- INSPECT flushed at END PROGRAM (1)
- All with exact toBe assertions (2)
Total: 239 tests passing | Zero fuzzy assertions
* fix(cobol): resolve 20th review — INITIALIZE multi-target + 2 tests
Finding 1: INITIALIZE now captures multiple targets with REPLACING
clause keyword filtering. Regex changed to lazy match stopping at
REPLACING/WITH/period boundary. Targets split on whitespace and
filtered against INITIALIZE_CLAUSE_KEYWORDS set.
Tests (2 new):
- INITIALIZE multi-target: WS-CUSTOMER WS-ORDER WS-LINE-ITEM → 3
- INITIALIZE with REPLACING: only WS-RECORD captured, not keywords
Total: 241 tests passing | TypeScript clean
* fix: close remaining Dart language support gaps
Four issues that were not addressed in PR #204:
1. extractFunctionName: add function_signature/method_signature handlers
and add both to FUNCTION_NODE_TYPES. Without this, findEnclosingFunctionId
cannot resolve Dart function scopes — all calls inside Dart functions
have no sourceId, breaking CALLS edge attribution.
2. formal_parameter_list: add to paramListTypes in extractMethodSignature.
Dart's tree-sitter grammar uses this node type (not formal_parameters),
so parameter counting returns 0 for all Dart functions.
3. Write-access queries: add @assignment patterns for obj.field = value
and this.field = value. Without these, no ACCESSES write edges are
emitted for Dart code.
4. initialized_identifier guard in extractDartDeclaration: comma-separated
declarations (String a, b, c) produce initialized_identifier nodes
which are in DART_DECLARATION_NODE_TYPES but were unhandled — the type
lives on the parent node.
Also adds Dart column to the feature matrix in type-resolution-system.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(dart): field-type resolution, call attribution, import resolution, and integration tests
Fixes five Dart language support gaps with integration tests and
architectural alignment:
**Tree-sitter queries** — Add field declaration patterns for typed and
nullable class fields (`String name = ''`, `String? name`). Without
these, Dart class fields were invisible to the pipeline (zero Property
nodes, zero HAS_PROPERTY edges).
**Import resolution** — Dart relative imports (`import 'models.dart'`)
don't use a leading `./`. The standard resolver only recognises paths
starting with `.` as relative; bare paths fell through to a Java-style
dot-to-slash conversion that mangled `models.dart` into `models/dart`.
Fix: prepend `./` before calling resolveStandard.
**Call attribution** — Dart's tree-sitter grammar places `function_body`
as a sibling of `function_signature`, not as a child wrapping both. The
`findEnclosingFunction` parent-walk never found the function because the
call lives inside `function_body` which is a sibling of the signature.
Fix: add `enclosingFunctionFinder` hook to LanguageProvider interface
(following the same strategy pattern as `labelOverride`), with the
Dart-specific logic in `languages/dart.ts`. Both `parse-worker.ts` and
`call-processor.ts` consume the hook generically — no Dart-specific code
in the generic processors.
**Receiver chain extraction** — Add `unconditional_assignable_selector`
to `MEMBER_ACCESS_NODE_TYPES` so `inferCallForm` returns `'member'` for
Dart method calls. Add Dart-specific receiver extraction blocks in
`extractReceiverName`, `extractReceiverNode`, and a `selector` handler
in `extractMixedChain` for Dart's flat sibling-selector model (vs the
nested member-expression model used by all other languages).
**Integration tests** — New `dart.test.ts` with field-type resolution
and call-result-binding describe blocks. Fixtures: `dart-field-types/`
(models.dart + app.dart) and `dart-call-result-binding/` (models.dart +
app.dart). 9 passing tests, 1 skipped (ACCESSES edges for field reads
depend on type-env parameter binding propagation — tracked for follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
Dart was added as the 14th supported language in PR #204 but the README
was not updated. Adds Dart row to the supported languages table and
updates the language count from 13 to 14.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): enable Cypher queries when connected to backend server
Route queries through HTTP API in backend mode instead of checking local WASM database.
Made-with: Cursor
* chore: add Maven/Gradle wrapper files to default ignore list
Add build wrapper scripts and directories to hardcoded ignore lists:
- Directories: .mvn, .gradle, gradle
- Files: mvnw, mvnw.cmd, gradlew, gradlew.bat
These are build infrastructure files, not source code.
Made-with: Cursor
* ci: re-trigger CI (Windows flaky timeout)
Made-with: Cursor
* feat: add more node types in filter panel
* feat: add more node types in filter panel
* revert additional changes
* test(web): add unit tests for filter panel node types
- FILTERABLE_LABELS: verify new types (Enum, Type, Decorator, Variable)
have colors, sizes, and no duplicates
- Filter panel icons: verify every filterable label has an icon mapped
and all icons are exported from lucide-icons
- Color legend: verify new types are included, ordered correctly, and
are a subset of FILTERABLE_LABELS
Made-with: Cursor
The shape-check-regression test uses withTestLbugDB but was running in
the default vitest project with parallel forks, causing LadybugDB
file-lock conflicts on Windows CI. Move it to the lbug-db project
(sequential execution) and exclude from default.
Follows up on #501.
* feat: add PHP response shape extraction for json_encode patterns
Adds extractPHPResponseShapes() to detect response keys from PHP
json_encode() calls with associative array literals. Supports:
- Short array syntax: json_encode(['key' => value])
- Long array syntax: json_encode(array('key' => value))
- Error classification via http_response_code() and header() status
- exit;/die; boundary detection to prevent cross-block status leaking
- Nested array filtering (only top-level keys extracted)
Pipeline integration dispatches PHP files to the new extractor.
Verified on collector project: 10 PHP routes now show responseKeys.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review — exit boundary, die; offset, CGI Status header
- Replace lastIndexOf('exit;')/lastIndexOf('die;') with regex that
matches exit(N), exit(0), die('msg'), die($var) as boundaries
- Fixes die; off-by-one (was slicing at +5 for a 4-char keyword)
- Add header('Status: NNN') CGI/FastCGI format detection
- Add 3 regression tests for the fixed bugs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: extract shared helpers, remove duplicate test block
- Extract lastMatchGroup() and buildShapeResult() to eliminate repeated
patterns in both JS/TS and PHP extractors
- Simplify detectPHPStatusCode to use ?? chaining with lastMatchGroup
- Remove duplicate 9-test PHP describe block (kept the 12-test version)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add PHP response shape integration tests
Adds a PHP fixture (api/items.php, api/submit.php) with multiple
json_encode patterns and a pipeline integration test verifying:
- Route nodes created for PHP endpoints
- responseKeys/errorKeys correctly extracted and separated
- exit(N)/die() boundaries respected
- HANDLES_ROUTE edges point to correct PHP handler files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ci: E2E workflow, web typecheck job, pre-commit hook, test suite
CI:
- ci.yml consolidated to reference ci-tests.yml
- ci-quality.yml: add typecheck-web job for gitnexus-web/
- ci-e2e.yml: E2E workflow with dorny/paths-filter (web changes only)
- ci-report.yml: remove dead integration-reports references
- CI gate allows skipped E2E status
- .gitignore: playwright artifacts, eval test artifacts
Pre-commit hook:
- .githooks/pre-commit: typecheck + unit tests for both packages
- Activated via git config core.hooksPath in prepare script
Test infrastructure:
- Vitest + React Testing Library: 58 unit tests
(graph, server-connection, mermaid, settings, constants, utils, paths)
- Playwright E2E: 5 tests + manual recording harness
- vitest.config from vitest/config, engines.node >= 20
- Playwright artifacts retain-on-failure
- wait-on in devDependencies
- vitest/coverage-v8 aligned with vitest 4.x
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: update gitnexus-web package-lock.json
Reflects devDependency additions (vitest, playwright, wait-on,
@testing-library, etc.) from package.json changes in this PR.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(e2e): add missing process-list-loaded testid, increase CI timeouts
- Add data-testid="process-list-loaded" to ProcessesPanel (E2E tests
were waiting for an element that didn't exist)
- Increase server connect timeouts from 5s to 10s for slower CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): run gitnexus-web unit tests in CI, remove unused variable
- Add gitnexus-web npm ci + vitest run to ci-tests.yml so web unit
tests are gated by the CI status check (were only running locally)
- Remove unused IS_PLAYWRIGHT_AUTOMATION variable from E2E spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(e2e): add process-row testid, wait for networkidle on page load
- Add data-testid="process-row" to ProcessItem component (E2E tests
referenced it but it didn't exist in the source)
- Use waitUntil: 'networkidle' on page.goto to ensure Vite dev server
is fully ready before interacting (fixes first-test timeout in CI)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(e2e): add process-view-button and process-highlight-button testids
E2E tests referenced these data-testid attributes but they didn't
exist in ProcessItem. All 6 E2E testids now have matching source
elements: status-ready, process-list-loaded, process-row,
process-view-button, process-highlight-button, server-url-input.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(e2e): remove networkidle — Vite HMR WebSocket prevents it from resolving
networkidle waits for zero network activity for 500ms, but Vite's HMR
WebSocket stays open permanently, causing page.goto to timeout at 60s
on all tests after the first. The explicit toBeVisible waits on UI
elements are sufficient and deterministic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(e2e): wait for Server button visibility, add CI retry, all 5 tests pass locally
Root cause: test 1 clicked the Server button before React hydrated,
so the tab content never rendered and the input wasn't found.
Fixes:
- Wait for Server button toBeVisible before clicking
- Increase input wait to 15s
- Remove networkidle (Vite HMR WebSocket prevents it from resolving)
- Add retries: 1 in CI for transient cold-start flakiness
Verified locally: all 5 E2E tests pass, 198 unit tests pass, typecheck clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): tolerate LadybugDB native crash during analyze step
gitnexus analyze can crash with "double free or corruption" (known
issue #273) during the LadybugDB native addon shutdown. The index is
usually written successfully before the crash. The workflow now:
1. Allows analyze to exit non-zero with a warning
2. Verifies .gitnexus index was actually created
3. Only fails if no index exists (real failure)
All tests verified locally: 198 unit, 5 E2E pass, typecheck clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): fix shell quoting in analyze step, simplify to || true
The previous echo string had special characters that broke bash
quoting in GitHub Actions. Simplified to: analyze || true, then
check if .gitnexus exists.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add agent development framework, GitHub templates, eval refactor
Agent framework (layered docs for AI-assisted contributions):
- AGENTS.md: canonical instructions, impact analysis, MCP tools
- CLAUDE.md: Claude Code-specific deltas and hooks
- GUARDRAILS.md: safety boundaries, non-negotiables, escalation
- ARCHITECTURE.md: monorepo layout, data flow map
- TESTING.md: test structure, commands, categories
- RUNBOOK.md: copy-paste operations for dev/CI/MCP
- llms.txt: minimal LLM context pointer
Editor integration:
- .cursor/index.mdc + rules/100-monorepo.mdc
GitHub templates:
- PR template with areas-touched checkboxes
- Bug report + feature request issue forms
Eval harness:
- Refactored mcp_bridge, tool_registry, constants
- Error sanitization utilities
- Property-based tests via Hypothesis
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(eval): use format_exception instead of format_exc in sanitize_exception
format_exc() returns the currently handled exception traceback, which
may be unrelated if called outside an active except block. Using
format_exception(type(exc), exc, exc.__traceback__) reliably captures
the passed exception's traceback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: update CONTRIBUTING.md and TESTING.md for current CI/hook setup
- CONTRIBUTING.md: add gitnexus-web typecheck command, pre-commit hook
checklist item
- TESTING.md: add gitnexus-web typecheck command, pre-commit hook
section (husky), update CI integration to list actual workflow files
(ci-quality, ci-tests, ci-e2e)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: update testing docs to reflect CI/E2E changes from PR #486
- AGENTS.md: update test counts (CLI ~2000 unit, ~1850 integration),
add gitnexus-web testing section (198 unit, 5 E2E with commands)
- RUNBOOK.md: fix Node requirement to >=20, fix E2E local repro command
- TESTING.md: E2E uses data-testid selectors + real servers, not mocks
- .cursor/rules/100-monorepo.mdc: add web test/E2E commands
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: address context engineering review — deduplicate tokens, expand Cursor rules
- Remove ~100-line gitnexus:start block from CLAUDE.md (was duplicated from AGENTS.md)
- Fix gitnexus:start block inlined inside AGENTS.md Reference Docs bullet (doubled)
- Replace CLAUDE.md scope table with pointer to AGENTS.md (single source of truth)
- Expand .cursor/index.mdc with 5 non-negotiable safety rules for always-on context
- Add .cursor/rules/200-eval.mdc with Python/eval commands (glob-scoped to eval/**)
- Improve llms.txt with priority annotations and descriptions
- Bump version headers to 1.2.0, last-reviewed to 2026-03-24
Saves ~1,400 tokens/session with zero information loss.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
- Add process.stdin.isTTY guard before --review prompt to prevent CI hangs
- Rename misleading --verbose e2e test to reflect it checks help output
- Replace DEBUG with GITNEXUS_VERBOSE for error stack traces
Made-with: Cursor
Spawn actual CLI process to verify:
- wiki --help surfaces all new flags
- wiki on non-git directory exits with code 1
- wiki on non-indexed repo fails with "No GitNexus index"
- --provider cursor skips API key prompt in non-TTY mode
- --verbose is accepted as valid flag
Made-with: Cursor
- Cache detectCursorCLI() result to avoid spawning `agent --version`
on every LLM call
- Fix stale JSDoc in cursor-client.ts (no longer uses stdin or
stream-json)
- Remove unused WikiOptions fields (model, baseUrl, apiKey) that were
passed but never read by WikiGenerator
- Fix inconsistent progress callback phase tracking in --review
continuation path
- Add wiki CLI help test covering --provider, --review, --verbose flags
Made-with: Cursor
Add Cursor headless CLI as a 4th provider option for wiki generation,
allowing users to leverage their Cursor subscription for wiki pages.
- Add --provider cursor flag and cursor-client.ts
- Add --review flag for interactive module tree editing
- Add --verbose flag for debugging
- Improve module tree generation (flatten single-child, unique slugs)
- Prevent DB timeout during long LLM calls
Usage: gitnexus wiki --provider cursor --model claude-4.5-opus-high
Made-with: Cursor
* refactor: SICP-informed LanguageProvider architecture for ingestion pipeline
Consolidate 16 scattered dispatch surfaces into a single LanguageProvider
Strategy interface per language. Processors are now fully language-agnostic —
zero SupportedLanguages.X enum access, zero dispatch table imports.
Architecture (5-layer DAG, zero circular dependencies):
L0: Capability modules (dispatch tables, single source of truth)
L1: LanguageProvider interface + createLanguageProvider factory
L2: 13 per-language provider files (Strategy objects)
L3: Registry with satisfies Record<SL, LP> + pre-built lookup maps
L4: Processors (language-agnostic, all behavior via provider.*)
Key changes:
- Add LanguageProvider interface with 15 properties (6 required, 9 optional)
- Create 13 provider files in languages/ + php-helpers.ts
- Migrate all processors to getProvider(language) — cached once per scope
- Replace heritage if-checks with provider.interfaceNamePattern/heritageDefaultEdge
- Replace MRO switch(language) with switch(provider.mroStrategy)
- Replace isNodeExported with provider.exportChecker
- Move PHP description extraction behind provider.descriptionExtractor
- Move Swift implicit imports behind provider.implicitImportWirer
- Move PHP route detection behind provider.isRouteFile
- Move Kotlin wildcard append behind provider.importPathPreprocessor
- Remove deprecated TypeEnvironment.env, add fileScope()/allScopes()
- De-export TypeEnv type (module-private)
- Pre-build extensionMap, WILDCARD_LANGUAGES, SYNTHESIS_LANGUAGES at load
- Remove dead entryPointPatterns/frameworkPatterns from interface
- Derive createLanguageProvider config type via Pick/Partial/Omit
- Tighten callback types from any to SyntaxNode
- Migrate 270+ test call sites from .env to TypeEnvironment API
Adding a new language: 3 files (enum + provider + registry line).
No processor file touched. Ever.
* refactor: clean architecture for LanguageProvider with O(1) AST cache
Address all PR #488 review comments and achieve pristine SICP layer separation:
Interface redesign:
- Split LanguageProvider into Config (input) + Provider (runtime with defaults)
- Rename createLanguageProvider → defineLanguage with explicit DEFAULTS constant
- Add MroStrategy, ImportSemantics named type aliases for better IDE tooltips
- Tighten labelOverride signature: string|null → NodeLabel|null (compile-time safety)
- Tighten descriptionExtractor nodeLabel: string → NodeLabel
- Un-export LanguageProviderConfig (internal to defineLanguage)
CI fixes (all 4 failures resolved):
- isNodeExported: add null guard for unknown languages
- preprocessImportPath tests: pass getProvider() instead of raw enum
- MRO tests: update expected strings to match language-agnostic prefixes
Code deduplication:
- Extract findDescendant/extractStringContent to ast-helpers.ts (single source of truth)
- Unify Kotlin method detection: remove duplicate from extractFunctionName,
use provider.labelOverride as single source of truth via findEnclosingFunctionId
- extractFunctionName return type: string → NodeLabel
Performance (O(1) AST node access):
- Add per-file Map-based memoization in parse-worker for parent-chain walks
- Cache enclosingClassId, enclosingFunctionId, exportStatus per SyntaxNode
- Clear caches before each file parse (not after — handles parse failures)
Architecture (pristine languages/ folder):
- Move php-helpers.ts → helpers/php.ts (L0 capability, not L2 config)
- Create helpers/swift.ts from extracted Swift provider logic
- Extract cppLabelOverride AST walk → isCppInsideClassOrStruct in ast-helpers.ts
- Extract isPhpRouteFile → helpers/php.ts
- All 13 provider files are now pure configuration — zero implementation logic
- Ruby: remove no-op namedBindingExtractor assignment (undefined from dispatch table)
* refactor: eliminate LANGUAGE_QUERIES, typeConfigs, namedBindingExtractors dispatch tables
Phase 1 of L0 dispatch table elimination. Providers now import capabilities
directly instead of indexing into redundant Record<SL, T> dispatch tables:
- LANGUAGE_QUERIES: providers import named query constants directly
(TYPESCRIPT_QUERIES, PYTHON_QUERIES, etc.). Table kept in tree-sitter-queries.ts
for call-processor.ts dynamic lookup + test consumers.
- typeConfigs: providers import from individual type-extractor files
(typescriptConfig from typescript.ts, javaTypeConfig from jvm.ts, etc.).
Dispatch table fully removed from type-extractors/index.ts.
- namedBindingExtractors: providers import extractors directly from
named-binding-extraction.ts (extractTsNamedBindings, etc.).
Dispatch table fully removed from import-resolution.ts.
Net: -48 LOC of dispatch table indirection. L3 satisfies Record<SL, LP>
remains the single exhaustiveness check.
* refactor: eliminate exportCheckers, callRouters, importResolvers dispatch tables
Phase 2 of L0 dispatch table elimination. All 6 dispatch tables are now gone:
- exportCheckers: individual checkers exported directly (tsExportChecker,
pythonExportChecker, etc.). isNodeExported uses a local checkersByLanguage
map to avoid circular dependency with languages/index.ts.
- callRouters: table removed. Providers import noRouting or routeRubyCall
directly. noRouting now exported. Dead import removed from call-processor.ts.
- importResolvers: resolver functions exported with clean names
(resolveTypescriptImport, resolveJavaImport, etc.). Inline lambdas
extracted to named exports. Dispatch functions renamed from *Dispatch
suffix to clean resolve*Import pattern.
Combined with Phase 1, all 6 L0 dispatch tables have been eliminated.
L3 satisfies Record<SL, LanguageProvider> is the single exhaustiveness check.
Providers are now fully self-contained — each imports its capabilities directly.
* perf+refactor: type-env caching, sequential fallback caching, utils.ts split
Phase 3 — performance optimizations and barrel cleanup:
Type-env parent-walk caching:
- Memoize findEnclosingClassName and findEnclosingParentClassName with
per-file Map<SyntaxNode, string|undefined> caches
- Eliminates O(n*m) repeated child scanning in extractParentClassFromNode
- Caches cleared in buildTypeEnv before each file's walk phase
Sequential fallback caching:
- Add classIdCache + exportCache Maps to parsing-processor.ts
- Mirrors the O(1) memoization pattern from parse-worker.ts
- Both paths now have identical caching for parent-chain walks
Split utils.ts barrel into focused modules:
- noise-filter.ts: BUILT_IN_NAMES + isBuiltInOrNoise (167 LOC)
- language-detection.ts: getLanguageFromFilename (58 LOC)
- utils.ts slimmed to re-exports + yieldToEventLoop + isVerboseIngestionEnabled
- Backward compatible — existing imports from utils.ts still work
* refactor: rename resolvers/ → import-resolvers/, restructure tests per-concern
Directory renames (git mv — history preserved):
- src/core/ingestion/resolvers/ → import-resolvers/ (10 files)
- test/unit/call-routing.test.ts → call-routing/ruby.test.ts
- test/unit/named-binding-extraction.test.ts → named-bindings/csharp.test.ts
- test/unit/import-resolution.test.ts → import-resolution/preprocessing.test.ts
All 11 import paths updated to reference new import-resolvers/ location.
Test imports updated for new subdirectory depth.
Note: test/integration/resolvers/ NOT renamed — those tests cover the full
ingestion pipeline per-language, not just import resolution.
* refactor: eliminate utils.ts barrel — all 33 consumers now import directly
Migrated 65 import sites across 33 files to import from the focused source
module instead of the utils.ts barrel:
- ast-helpers.js: SyntaxNode, extractFunctionName, findEnclosingClassId, etc.
- call-analysis.js: inferCallForm, extractReceiverName, countCallArguments, etc.
- noise-filter.js: BUILT_IN_NAMES, isBuiltInOrNoise
- language-detection.js: getLanguageFromFilename
utils.ts reduced to 2 original functions only:
- yieldToEventLoop
- isVerboseIngestionEnabled
Zero re-exports remain. Every import is now direct to its source module.
* refactor: create utils/ folder, move all shared utilities, delete utils.ts barrel
Final phase of module structure migration:
- git mv ast-helpers.ts, call-analysis.ts, noise-filter.ts,
language-detection.ts → utils/ subdirectory (history preserved)
- Extract yieldToEventLoop → utils/event-loop.ts
- Extract isVerboseIngestionEnabled → utils/verbose.ts
- Delete utils.ts (zero re-exports, zero functions remain)
- Update 38 import paths across source and test files
The ingestion/ root is now clean — only processors, capability modules,
and the pipeline orchestrator live at the top level. All shared utilities
are in utils/, all language-specific helpers in helpers/, all import
resolvers in import-resolvers/.
* refactor: move findChild from import-resolvers/utils.ts to utils/ast-helpers.ts
findChild is a generic AST helper (find first named child by type) — it
belongs with the other AST traversal utilities, not in the import resolver
module. 4 consumers updated to import from utils/ast-helpers.js.
* refactor: split named-binding-extraction.ts into per-language files
Rename named-binding-extraction.ts → named-binding-processor.ts (git mv,
history preserved), keeping only walkBindingChain for re-export chain resolution.
7 per-language extractor functions moved to named-bindings/ subdirectory:
- named-bindings/typescript.ts (extractTsNamedBindings — TS + JS)
- named-bindings/python.ts (extractPythonNamedBindings)
- named-bindings/kotlin.ts (extractKotlinNamedBindings)
- named-bindings/rust.ts (extractRustNamedBindings + collectRustBindings)
- named-bindings/php.ts (extractPhpNamedBindings)
- named-bindings/csharp.ts (extractCsharpNamedBindings)
- named-bindings/java.ts (extractJavaNamedBindings)
Each provider now imports its binding extractor from the per-language file.
* refactor: eliminate import-resolution.ts — distribute to natural homes
Split per-language resolvers into import-resolvers/ per-language files and
eliminate the import-resolution.ts catch-all module entirely:
Per-language resolvers moved to import-resolvers/:
- standard.ts: resolveStandard, resolveJavascriptImport, resolveTypescriptImport,
resolveCImport, resolveCppImport
- jvm.ts: resolveJavaImport, resolveKotlinImport
- go.ts: resolveGoImport
- csharp.ts: resolveCSharpImport (helper renamed to Internal)
- php.ts, python.ts, ruby.ts, rust.ts: same pattern
- swift.ts: new file for resolveSwiftImport
Types distributed to their concern directories:
- import-resolvers/types.ts: ImportResult, ImportConfigs, ResolveCtx, ImportResolverFn
- named-bindings/types.ts: NamedBinding, NamedBindingExtractorFn
preprocessImportPath moved to import-processor.ts (its primary consumer).
import-resolution.ts deleted — zero catch-all modules remain.
* refactor: tighten SPR — eliminate re-exports, dead code, type holes, and redundant patterns
12 review findings resolved across the ingestion layer:
Type safety:
- CallRouter callNode: any → SyntaxNode (closes type hole)
- CaptureMap type alias replaces Record<string, any>
- providersWithImplicitWiring filter now type-narrowed (removes ! assertions)
- Ruby exportChecker: unnecessary as-cast removed, named export created
Architecture:
- Circular type dependency eliminated (ImportResolutionContext moved to types.ts)
- LANGUAGE_QUERIES residual dispatch replaced with provider.treeSitterQueries
- noRouting sentinel deleted — callRouter now properly optional on 12 providers
- All 6 re-exports from import-processor/pipeline/languages eliminated
Pattern cleanup:
- Dead checkersByLanguage table + isNodeExported removed from export-detection
- 4 duplicated config interfaces consolidated to language-config.ts
- extractCsharpNamedBindings → extractCSharpNamedBindings (casing consistency)
Simplification:
- import-resolvers/index.ts barrel deleted (dead re-exports)
- helpers/ inlined into languages/ (php.ts, swift.ts) — 1 directory removed
Verified: tsc --noEmit clean, 3837 tests pass, 0 failures.
* refactor: address review — remove LANGUAGE_QUERIES table, type-extractors barrel, fix Windows timeout
Review comment fixes (github.com/abhigyanpatwari/GitNexus/pull/488#issuecomment-4117817648):
1. LANGUAGE_QUERIES dispatch table removed from tree-sitter-queries.ts
— 5 test files migrated to getProvider(lang).treeSitterQueries
— eliminates last parallel dispatch surface
2. type-extractors/index.ts barrel deleted
— type-env.ts now imports TYPED_PARAMETER_TYPES from shared.js directly
3. Windows CI timeout fix: afterAll cleanup hook in test-indexed-db.ts
now passes explicit 120s timeout to prevent KuzuDB C++ destructor
hang from hitting vitest's default 30s testTimeout on Windows
Verified: tsc --noEmit clean, 3835 tests pass, 0 failures.
* refactor: eliminate chained getProvider property access — assign to variable first
All getProvider(lang).property calls now follow the pattern:
const provider = getProvider(language);
const x = provider.property;
5 source files + 4 test files updated (~35 occurrences).
This ensures consistent provider variable usage and avoids
repeated lookups in hot paths.
* refactor: remove last 4 re-exports from import-resolvers, fix stale CaptureMap comment
- Remove `export type { TsconfigPaths }` from standard.ts
- Remove `export type { GoModuleConfig }` from go.ts
- Remove `export type { ComposerConfig }` from php.ts
- Remove `export type { CSharpProjectConfig }` from csharp.ts
All 4 types are canonically defined in language-config.ts;
zero consumers imported via the resolver re-exports.
- Fix stale CaptureMap JSDoc: said "Uses any" but type is SyntaxNode | undefined
Add GLM support using OpenAI-compatible API via ChatOpenAI from LangChain.
Defaults to the Z.AI coding endpoint (https://api.z.ai/api/coding/paas/v4)
with configurable base URL. Supported models: GLM-5, GLM-5-Turbo, GLM-4.7, GLM-4.5.
- Add ADD_TAGS: ['foreignObject'] to all DOMPurify.sanitize calls —
Mermaid uses foreignObject for HTML text labels inside flowchart
nodes. The SVG profile was stripping them, causing empty boxes.
- Remove leftover sub-batch loop lines from prepared statement hoist
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- MarkdownRenderer: wrap handleLinkClick in useCallback, add to
markdownComponents useMemo deps (fixes stale closure)
- GraphCanvas: remove sigmaRef from useEffect deps (ref identity
never changes), extract handleToggleAIHighlights to useCallback
- CodeReferencesPanel: add nodeById Map for O(1) focus-in-graph
lookup (was O(N) graph.nodes.find on every click)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deep imports from lucide-react/dist/esm/icons/*.js are internal paths
that broke the Vercel production build. Replaced with standard named
re-exports from lucide-react — keeps the centralized module pattern
without relying on fragile internal paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hoists conn.prepare(cypher) outside the sub-batch loop so it's called
once per (fromLabel, toLabel) pair instead of ceil(N/4) times. The
statement is reused for all rows in the group, then closed in finally.
Yields to event loop every 500 relations instead of every sub-batch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- BackendRepoSelector, EmbeddingStatus, Header, MermaidDiagram,
QueryFAB, RightPanel, ToolCallCard, WebGPUFallbackDialog: switch
from lucide-react barrel imports to @/lib/lucide-icons deep imports
- MermaidDiagram: lazy-load ProcessFlowModal via React.lazy
- Extract ProviderConfigCard from SettingsPanel for cleaner separation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Performance:
- nodeById Map in GraphCanvas for O(1) click/hover lookups
- fileNodeByPath Map in useAppState for O(1) file path lookups
- Set.has() for HIGHLIGHT_NODES/IMPACT matching (was O(N²))
- useMemo for primaryLanguage in StatusBar
- useCallback on toggleLabelVisibility/toggleEdgeVisibility
- Recursive FileTreePanel search (full subtree, not 1 level)
React fixes:
- Remove stale queryResult dep from clearAICodeReferences
- Cancel RAF chains in CodeReferencesPanel on cleanup
- Clean up timeouts in SettingsPanel and MarkdownRenderer on unmount
- try/catch on localStorage in DropZone (private browsing)
- Await handleServerConnect in App.tsx auto-connect
- pendingToolCalls counter replaces allToolsDone boolean in agent.ts
- JSON.parse try/catch in agent streaming
- sessionStorage JSDoc fix in settings-service
Bundle:
- Centralized lucide icon deep imports
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mermaid uses foreignObject for HTML text labels inside flowchart
nodes. The SVG profile strips them by default, causing empty boxes.
ADD_TAGS: ['foreignObject'] preserves text while still sanitizing
against XSS.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add NODE_COLORS and NODE_SIZES entries for all 15 multi-language labels
(Struct, Trait, Impl, TypeAlias, Const, Static, Namespace, Union,
Typedef, Macro, Property, Record, Delegate, Annotation, Constructor,
Template) — fixes Record<NodeLabel, ...> completeness
- Escape table names in count/lookup queries with escapeTableName() to
prevent silent failures for backtick-required tables
- Add HAS_PROPERTY and ACCESSES to RelationshipType union
- Update initPromise after db recreation in loadGraphToLbug so subsequent
initLbug() calls return fresh db/conn refs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ProcessFlowModal: revert import from non-existent @/lib/lucide-icons back to lucide-react
- embedding-pipeline: remove extra argument in executeQuery call (signature only accepts 1 arg)
Both issues were introduced in PR #475 (web-security-hardening).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- empty graph produces header-only CSVs (no data rows)
- empty graph relCSV has only header
- double quotes in node names are RFC 4180 escaped
- file node without fileContents gets empty content (no crash)
- community with empty keywords array produces valid CSV
- unknown node labels are silently skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 3 new test fixtures: swift-if-let-guard-let, swift-await-try, swift-for-loop-inference
- Add integration tests for if let/guard let binding resolution (4 assertions)
- Add integration tests for await/try expression unwrapping (3 assertions)
- Add for-loop-inference fixture (documented as known gap — type-env infrastructure
is in place but call-processor re-parse path doesn't propagate the binding yet)
- Fix cross-chunk Swift implicit imports: standard processImports path now passes
allFileList instead of chunk-only files to addSwiftImplicitImports, matching
the fast-path behavior
- Add Swift type_annotation fallback in type-env declarationTypeNodes population
(handles [User] array sugar where childForFieldName('type') returns null)
- Handle Swift 'pattern' node in extractVarName fallback (pattern wraps simple_identifier)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
npm ci was failing with "Missing: hono@4.12.8" and
"Missing: graphology-types@0.24.8" because the lock file was
out of sync after rebase. Regenerated from clean state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pin exact versions (no ^) to prevent surprise upgrades:
- tree-sitter: "0.22.4" (was "^0.22.4")
- tree-sitter-swift: "0.7.1" (was "^0.7.1")
Add npm overrides to suppress peer dependency warnings from grammar
packages that declare ^0.21.x but work fine with 0.22.4.
Note: tree-sitter-swift 0.6.0 fails to build on current Node (needs
node-gyp + Swift toolchain). 0.7.1 with prebuilt binaries is required
for Swift support to work at all.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Export detection: exclude private(set)/fileprivate(set) from
unexported check. Only the setter is restricted — the symbol
itself is still readable cross-file.
2. For-loop binding: use extractVarName() instead of raw .text
to avoid polluting scopeEnv with non-identifier keys from
tuple destructuring patterns (e.g. `for (a, b) in ...`).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers all high and medium impact gaps from the Swift feature coverage
analysis:
1. if let / guard let bindings: add if_statement and guard_statement
to DECLARATION_NODE_TYPES, extract varName and value for Tier 2
return-type propagation (callResult, copy, fieldAccess, methodCallResult)
2. await / try expression unwrapping: add unwrapSwiftExpression() that
strips await_expression and try_expression wrappers before checking
for call_expression. Applied in extractPendingAssignment,
extractInitializer, and scanConstructorBinding.
3. for item in collection: add extractForLoopBinding for Swift with
extractSwiftElementTypeFromTypeNode that handles [User] array sugar
and Array<User> generic types. Registered in typeConfig.
4. Multiple inheritance specifiers: already working — tree-sitter
queries match all inheritance_specifier occurrences automatically.
Verified, no code changes needed.
5. Enum case extraction: add (enum_entry (simple_identifier) @name)
@definition.property query to SWIFT_QUERIES.
6. self/super resolution: unskipped both describe.skip test suites
(tree-sitter-swift 0.7.1 ships prebuilds, Node 22 build issue
resolved). Both pass — 5 previously-skipped tests now running.
7. Optional chaining obj?.method(): already working — tree-sitter-swift
parses the ? transparently. Verified, no code changes needed.
Tests: 3,603 → 3,608 (5 unskipped self/super tests)
Swift tests: 23 → 28 passing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses reviewer feedback: the new Swift behaviors (implicit imports,
constructor fallback, extension dedup, export detection) had no dedicated
integration tests. Adds 4 fixture directories and 11 new test assertions:
1. swift-implicit-imports: two files, no explicit import, cross-file
constructor + member call resolves via addSwiftImplicitImports
2. swift-extension-dedup: extension creates duplicate Class node,
constructor still resolves to primary definition
3. swift-constructor-fallback: ClassName() without `new` resolves as
constructor via free→constructor retry
4. swift-export-visibility: internal symbols visible cross-file,
public/open visible, private/fileprivate noted as Tier 3 limitation
All 3,603 tests pass (11 new, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Swift was missing the extractPendingAssignment extractor, which meant
return-type-based variable bindings like `let user = getUser()` couldn't
propagate the return type of `getUser()` to `user`. This broke member
call resolution: `user.save()` couldn't resolve to `User.save()` when
there were competing methods (both User and Repo have save()).
Handles four Swift patterns:
- let user = getUser() → callResult (Tier 2 propagation)
- let result = user.save() → methodCallResult
- let name = user.name → fieldAccess
- let copy = user → copy
All 3,592 tests pass — including the 2 previously-failing Swift
return-type inference tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix assignment query: tree-sitter-swift 0.7.1 uses named fields
(target:/result:/suffix:) instead of positional children
- Update export detection tests: Swift `internal` (default) is now
correctly treated as exported (module-scoped visibility)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When Swift extensions create multiple Class nodes with the same name
(e.g. Product.swift + ProductMatchableConformance.swift), the call
resolver gets multiple candidates and refuses to emit a CALLS edge.
Add dedup: when all candidates share the same type (Class/Struct) and
differ only by file, prefer the primary definition (shortest filepath).
Note: This fix is partial — some constructor calls inside function
bodies may still be consumed by the type-env constructor binding
scanner before reaching resolveCallTarget. Filed as known limitation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three changes that together enable cross-file call resolution for Swift:
1. export-detection.ts: Treat internal (default) Swift symbols as exported.
Swift's default access level is `internal` (module-scoped, visible to
all files in the same target). Only private/fileprivate are file-scoped.
Previously all non-public/open symbols were marked unexported.
2. import-processor.ts: Add implicit import edges between all Swift files
in the same module/target. Swift has no file-level imports — all files
see each other automatically. Without these edges, the tiered resolver
can't find cross-file symbols at Tier 2a (import-scoped).
Supports SPM targets via Package.swift; falls back to single-module
for Xcode projects without SPM.
3. call-processor.ts: Add constructor fallback for free-form calls.
Swift constructors look like free function calls (no `new` keyword):
`let ocr = OCRService()`. The call form is inferred as `free`, which
filters out Class/Struct targets. Now retries with `constructor` form
when free-form finds no callable but the name resolves to a type.
Tested on 61-file iOS 26 project (PricePal):
- Before: 0 cross-file CALLS edges
- After: full cross-file resolution (OCRService traced from ScanViewModel)
- 3,099 nodes, 10,449 edges, 246 clusters, 243 flows
Related: #406, #407
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The patch script fails to parse tree-sitter-swift@0.6.0's binding.gyp
because the file contains both Python-style # comments AND trailing
commas in JSON arrays. The existing regex strips # comments but leaves
trailing commas, causing JSON.parse() to fail with:
"Unexpected token ']'"
This silently prevents tree-sitter-swift from building, which means
Swift files are skipped entirely during analysis.
Fix: add a second regex pass to strip trailing commas before ] or }
after comment removal.
Fixes#386, #406
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Canonical agent instructions: **[AGENTS.md](../AGENTS.md)** (GitNexus MCP rules, monorepo commands, Cursor Cloud notes). **[CLAUDE.md](../CLAUDE.md)** adds Claude Code-specific notes and points back to AGENTS.md for GitNexus.
## Non-negotiables (always apply)
- NEVER edit a function/class/method without running `gitnexus_impact` first.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename`.
- NEVER commit without running `gitnexus_detect_changes()`.
- NEVER ignore HIGH/CRITICAL risk warnings from impact analysis.
- NEVER run `npx gitnexus analyze` without `--embeddings` if `.gitnexus/meta.json` shows stored embeddings.
Full rules: **[AGENTS.md](../AGENTS.md)** (`gitnexus:start` block, Cursor Cloud section).
**Rule architecture:** Prefer this file plus optional `.cursor/rules/*.mdc` globs (YAML `globs` in frontmatter). Legacy `.cursorrules` is deprecated; content lives here.
Metadata: version, last reviewed, scope, model policy, reference docs, changelog.
Last updated: 2026-03-22
-->
Last reviewed: 2026-04-13
**Project:** GitNexus · **Environment:** dev · **Maintainer:** repository maintainers (see GitHub)
This file uses a standard agent header (version, scope, model policy, reference docs, changelog), adapted for this **TypeScript/JavaScript monorepo**.
## Scope
| | |
|--|--|
| **Reads** | Repository tree as needed for the task: `gitnexus/`, `gitnexus-web/`, `eval/`, plugin packages, `.github/`, `.gitnexus/` when present, and docs. |
| **Writes** | Only paths required for the requested change; keep diffs minimal. Update lockfiles when dependencies change. |
| **Executes** | `npm`, `npx`, `node` under `gitnexus/` and `gitnexus-web/`; `uv run` for Python under `eval/` when applicable; shell utilities for documented CI/dev workflows. |
| **Off-limits** | User secrets (e.g. real `.env`), production deployment credentials, unrelated repositories, destructive git history operations without explicit human confirmation. |
## Model Configuration
- **Primary:** Pin in **Cursor** (Settings → model). Use a **named** model (e.g. GPT-5.2, Claude Sonnet 4.x). Avoid relying on **Auto** when reproducibility or audit trail matters.
- **Fallback:** As configured in Cursor or your organization (do not encode `latest` or wildcards in automation configs).
- **Notes:** The open-source GitNexus CLI indexer does not call an LLM. Optional Nexus AI in the web UI uses end-user provider keys and models.
## Execution Sequence (complex tasks)
Long sessions dilute instructions. For **multi-step** work, state up front:
1. Which rules in this file and **[GUARDRAILS.md](GUARDRAILS.md)** apply (and any relevant Signs).
2. Current **Scope** boundaries (Reads / Writes / Off-limits).
3. Which **validation commands** you will run (e.g. `cd gitnexus && npm test`, `npx tsc --noEmit`).
On very long threads, the human may add *“Remember: apply all AGENTS.md rules”* to re-weight rule tokens against context dilution.
## Claude Code hooks
Hooks enforce gates that prompts cannot. In **Claude Code**, **PreToolUse** hooks can block tools such as `git_commit` until checks pass. Adapt to this repo: e.g. `cd gitnexus && npm test` before commit.
## Context budget (Cursor / standards)
Generic “core standards” playbooks are often long and stack-specific. For this monorepo, commands and gotchas live under **Cursor Cloud specific instructions** below and in **[CONTRIBUTING.md](CONTRIBUTING.md)**. If always-on rules grow, split domain rules into **`.cursor/rules/*.mdc`** (globs). **Cursor:** project-wide rules live in **`.cursor/index.mdc`** (YAML frontmatter with `alwaysApply: true`). **Claude Code:** optionally load a **`STANDARDS.md`** only when needed (e.g. *“When writing new code, read STANDARDS.md”*) to save context.
- **Cursor:** `.cursor/index.mdc` (always-on rules); optional `.cursor/rules/*.mdc` (glob-scoped). Legacy `.cursorrules` is deprecated — see `.cursor/index.mdc`.
- **Optional local files:** `NOTES.md` (short vendor-neutral project snapshot). For handoffs, keep notes local (e.g., a scratch file outside the repo) rather than committing `HANDOFF.md`.
- **GitNexus:** skills under `.claude/skills/gitnexus/`; machine-oriented rules in the `gitnexus:start` … `gitnexus:end` block below.
## Changelog
| Date | Version | Change |
|------|---------|--------|
| 2026-04-13 | 1.3.0 | Updated GitNexus index stats after DAG refactor. |
This project is indexed by GitNexus as **GitNexus** (2298 symbols, 5501 relationships, 175 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **GitNexus** (4325 symbols, 10556 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
@@ -99,3 +162,44 @@ To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
<!-- gitnexus:end -->
## Cursor Cloud specific instructions
### Repository structure
This is a monorepo with two main products and supporting config packages:
| Component | Path | Purpose |
|-----------|------|---------|
| **GitNexus CLI/Core** | `gitnexus/` | Main product — TypeScript CLI, indexing pipeline, MCP server. Published to npm. |
| **GitNexus Web UI** | `gitnexus-web/` | React/Vite browser app — graph explorer + AI chat. Runs entirely in WASM. |
| Claude Plugin | `gitnexus-claude-plugin/` | Static config for Claude marketplace (no build). |
| Cursor Integration | `gitnexus-cursor-integration/` | Static config for Cursor editor (no build). |
- **Integration tests**: `cd gitnexus && npm run test:integration` (vitest, ~1850 tests). Two LadybugDB file-locking tests (`lbug-core-adapter`, `search-core`) may fail in containerized environments due to `/tmp` locking limitations — this is a known environment issue, not a code bug.
No separate lint command is configured; TypeScript strict checking serves as the primary static analysis.
### Gotchas
-`npm install` in `gitnexus/` triggers `prepare` (builds via `tsc`) and `postinstall` (patches tree-sitter-swift). Native tree-sitter bindings require `python3`, `make`, and `g++` to be present.
-`tree-sitter-kotlin` and `tree-sitter-swift` are optional dependencies — install warnings for these are expected and non-blocking.
- The Web UI uses `vite-plugin-wasm` and requires `Cross-Origin-Opener-Policy`/`Cross-Origin-Embedder-Policy` headers for `SharedArrayBuffer` (handled automatically by Vite dev server).
- There is no ESLint/Prettier configuration in this repo.
This repository is a **monorepo** with two main products: the **CLI / MCP package** (`gitnexus/`) and the **browser UI** (`gitnexus-web/`). Supporting folders ship editor integrations and plugins without changing the core graph engine.
## Repository layout
| Path | Role |
|------|------|
| `gitnexus/` | Published npm package `gitnexus`: CLI, MCP server (stdio), local HTTP API for bridge mode, ingestion pipeline, LadybugDB graph, embeddings (optional). |
| `gitnexus-web/` | Vite + React UI: in-browser indexing (WASM), graph visualization, optional connection to `gitnexus serve`. |
| `.claude/`, `gitnexus-claude-plugin/`, `gitnexus-cursor-integration/` | Packaged **skills** and plugin metadata so agents discover the same workflows as documented in `AGENTS.md`. |
| `eval/` | Evaluation harnesses and docs for benchmarking tool usage. |
| `.github/` | CI workflows (quality, unit, integration, E2E) and composite actions. |
## End-to-end flow: index → graph → tools
1.**Ingestion** (`gitnexus analyze`)
- Entry: `gitnexus/src/cli/analyze.ts` → `runPipelineFromRepo` in `gitnexus/src/core/ingestion/pipeline.ts`.
- The pipeline is structured as a **DAG (Directed Acyclic Graph)** of named phases (see [Pipeline Phase DAG](#pipeline-phase-dag) below).
- Output is loaded into **LadybugDB** under **`.gitnexus/`** at the repo root (`lbug/`, `meta.json`, etc.). Optional **FTS** indexes and **embeddings** attach to the same store.
- The repo is registered in **`~/.gitnexus/registry.json`** so MCP can find it from any working directory.
2.**Persistence & metadata**
-`gitnexus/src/storage/repo-manager.ts` — paths, registry, cleanup of legacy Kuzu artifacts.
- **MCP (stdio):** `gitnexus/src/cli/mcp.ts` → `startMCPServer` → `LocalBackend` (`gitnexus/src/mcp/local/local-backend.ts`) opens registered repos and serves **tools** from `gitnexus/src/mcp/tools.ts` and **resources** from `gitnexus/src/mcp/resources.ts`.
- **Bridge HTTP:** `gitnexus/src/cli/serve.ts` → Express app in `gitnexus/src/server/api.ts` (CORS-limited) exposes REST + MCP-over-HTTP for the web UI.
- **CLI tools (no MCP):** `gitnexus query`, `context`, `impact`, `cypher` in `gitnexus/src/cli/tool.ts` call the same backend for scripts and CI.
4.**Staleness**
-`gitnexus/src/mcp/staleness.ts` compares indexed `lastCommit` to `HEAD` and surfaces hints when the graph is behind git.
## MCP tools (summary)
| Tool | Purpose |
|------|---------|
| `list_repos` | Discover indexed repositories when more than one is registered. |
| `query` | Natural-language / keyword search over the graph (hybrid BM25 + optional vectors). |
| `cypher` | Ad hoc **Cypher** against the schema (see resource `gitnexus://repo/{name}/schema`). |
| `context` | Callers, callees, processes for one symbol (with disambiguation). |
| `impact` | Blast radius (upstream/downstream) with depth and risk summary. |
| `detect_changes` | Map git diffs to affected symbols and processes. |
| `rename` | Graph-assisted rename with `dry_run` preview (`graph` vs `text_search` confidence). |
| CI | `.github/workflows/*.yml`, `.github/actions/setup-gitnexus/`. |
## Pipeline Phase DAG
The ingestion pipeline is a DAG of named phases. Each phase is defined in its own file under `gitnexus/src/core/ingestion/pipeline-phases/` with explicit dependencies, typed inputs, and typed outputs.
The runner (`pipeline-phases/runner.ts`) validates the DAG at startup (detects cycles and missing deps via topological sort), then executes phases in dependency order. Each phase receives:
Metadata: version, last reviewed, scope, model policy, reference docs, changelog.
Last updated: 2026-03-22
-->
Last reviewed: 2026-04-13
**Project:** GitNexus · **Environment:** dev · **Maintainer:** repository maintainers (see GitHub)
Follow **AGENTS.md** for the canonical rules; this file adds Claude Code–specific deltas. Cursor-specific notes live only in `AGENTS.md`.
## Scope
See the **Scope** table in [AGENTS.md](AGENTS.md) for read/write/execute/off-limits boundaries. Cursor-specific workflow notes also live only in AGENTS.md.
## Model Configuration
- **Primary:** Pin per **Claude Code** / Anthropic org policy (explicit model id). Do not rely on an unversioned `latest` alias for governed workflows.
- **Fallback:** As configured in Claude Code (organization default or user override).
- **Notes:** The GitNexus CLI analyzer does not call an LLM.
## Execution Sequence (complex tasks)
Same discipline as [AGENTS.md](AGENTS.md): before large multi-step work, state which **AGENTS.md** / **GUARDRAILS.md** rules apply, current **Scope**, and planned validation commands (`npm test`, `tsc`, etc.). When pausing, summarize progress in the chat or a **local** scratch file (do not add `HANDOFF.md` to the repo), then `/clear` and resume with that summary.
## Claude Code hooks
Prefer **PreToolUse** hooks for hard gates (e.g. tests before `git_commit`). Adapt hook commands to `gitnexus/` npm scripts.
## Context budget
If always-on instructions grow, load deep conventions via conditional reads (e.g. *“When writing new code, read STANDARDS.md”*) instead of pasting long blocks here. In Cursor, prefer `.cursor/index.mdc` plus optional `.cursor/rules/*.mdc` globs (see [AGENTS.md](AGENTS.md) § Context budget).
- **GitNexus:** `.claude/skills/gitnexus/`; MCP and indexed-repo rules live only in [AGENTS.md](AGENTS.md) (`gitnexus:start` … `gitnexus:end`). See **GitNexus rules** below.
## Changelog
| Date | Version | Change |
|------|---------|--------|
| 2026-04-13 | 1.3.0 | Updated GitNexus index stats after DAG refactor. |
| 2026-03-24 | 1.2.0 | Removed duplicated gitnexus:start block and scope table; replaced with pointers to AGENTS.md. |
| 2026-03-23 | 1.1.0 | Updated agent instructions to match AGENTS.md. |
GitNexus MCP rules are in the `<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **GitNexus** (4325 symbols, 10556 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
## Always Do
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
## When Debugging
1. `gitnexus_query({query: "<error or symptom>"})` — find execution flows related to the issue
2. `gitnexus_context({name: "<suspect function>"})` — see all callers, callees, and process participation
3. `READ gitnexus://repo/GitNexus/process/{processName}` — trace the full execution flow step by step
4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed
## When Refactoring
- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`.
- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code.
- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed.
## Never Do
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
Before completing any code modification task, verify:
1. `gitnexus_impact` was run for all modified symbols
2. No HIGH/CRITICAL risk warnings were ignored
3. `gitnexus_detect_changes()` confirms changes match expected scope
4. All d=1 (WILL BREAK) dependents were updated
## Keeping the Index Fresh
After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
```bash
npx gitnexus analyze
```
If the index previously included embeddings, preserve them by adding `--embeddings`:
```bash
npx gitnexus analyze --embeddings
```
To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**
> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.
## CLI
| Task | Read this skill file |
|------|---------------------|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
<!-- gitnexus:end -->` block in **[AGENTS.md](AGENTS.md)** — load that section when working with MCP tools or the graph index.
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **GitNexus** (2298 symbols, 5501 relationships, 175 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **GitNexus** (3298 symbols, 7954 relationships, 185 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
How to propose changes, run checks locally, and open pull requests.
## License
This project uses the [PolyForm Noncommercial License 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0/). By contributing, you agree your contributions are licensed under the same terms unless stated otherwise.
## Where to discuss
- **Issues & feature ideas:** use [GitHub Issues](https://github.com/abhigyanpatwari/GitNexus/issues) for the upstream repo, or your fork’s tracker if you work from a fork.
- **Community:** see the Discord link in the root [README.md](README.md).
4. Run tests as described in [TESTING.md](TESTING.md).
## Branch and pull requests
- Use short-lived branches off the default branch of the repo you are targeting.
- **PR titles MUST follow the conventional-commit format** — `pr-labeler.yml` enforces this on every PR and auto-applies the matching label so release notes group the change correctly.
- **PR description:** what changed, why, how to verify (commands), and any risk or rollback notes.
### Pull request titles
Format: `<type>[(scope)][!]: <subject>`
Allowed types and the release-notes section each one lands in (defined in `.github/release.yml`):
Append `!` to the type (e.g. `feat(api)!: drop /v1 endpoint`) or include `BREAKING CHANGE:` in the PR body to flag a breaking change — the labeler then adds the `breaking` label and the 💥 Breaking Changes section is rendered first.
Commits within a PR may use any style — only the **merged PR title** shows up in release notes, so that's the one the convention applies to.
## Before you open a PR
- [ ] Tests pass for the packages you touched (`gitnexus` and/or `gitnexus-web`).
- [ ] Typecheck passes: `npx tsc --noEmit` in `gitnexus/` and `npx tsc -b --noEmit` in `gitnexus-web/`.
- [ ] No secrets, tokens, or machine-specific paths committed.
- [ ] Documentation updated if behavior or public CLI/MCP contract changes.
- [ ] Pre-commit hook runs clean (`.husky/pre-commit` — typecheck + unit tests for staged packages).
## Code review
Maintainers may request changes for correctness, tests, performance, or consistency with existing patterns. Keeping diffs focused makes review faster.
## GitHub Actions — Concurrency Convention
Every workflow under `.github/workflows/` MUST declare a top-level `concurrency:` block using this convention:
- **Group key** starts with `${{ github.workflow }}` so no two workflows can collide on the same group name. The discriminator that follows is chosen per event shape:
-`workflow_run` scope (e.g. `ci-report.yml`): `${{ github.workflow }}-${{ github.event.workflow_run.pull_requests[0].number || format('{0}/{1}', github.event.workflow_run.head_repository.full_name, github.event.workflow_run.head_branch) }}` — the fork fallback must be stable across reruns (never `workflow_run.id`, which is per-run-unique and defeats serialization).
- Global single-slot (manual dispatch utilities): `${{ github.workflow }}`
- **Reusable workflows invoked via `workflow_call`:** do NOT use `${{ github.workflow }}` in the group key — in called-workflow context its evaluation is ambiguous and can resolve to the caller's name, which would deadlock against the caller's own group. Use a hardcoded literal prefix and a `github.event_name`-aware expression that falls through to `github.run_id` for reusable invocations (see `ci.yml` for the canonical form).
- **Merge queue (`merge_group`)**: when this event is added, use `${{ github.workflow }}-${{ github.event.merge_group.head_ref }}` with `cancel-in-progress: false` (every queue entry is a distinct ref; never cancel).
- **`cancel-in-progress` policy:**
| Event | `cancel-in-progress` | Why |
|-------|----------------------|-----|
| `pull_request` CI run | `true` | New push supersedes old run |
| `push` to `main` | `false` | Every main commit gets validated |
| Tag push (`v*` publish) | `false` | Never cancel mid-publish |
| `push` to `main` for release-candidate | `false` | Never cancel mid-RC publish |
- For workflows that serve multiple events at once (e.g. `ci.yml` handles `pull_request`, `push`, and `workflow_call`), make `cancel-in-progress` event-aware:
- When adding a new workflow, copy the concurrency block from an existing workflow of the same event shape.
## AI-assisted contributions
If you use coding agents, follow project context files (e.g. `AGENTS.md`, `CLAUDE.md`) and avoid drive-by refactors unrelated to the issue. Prefer incremental, test-backed changes.
## Releases
Two publish workflows ship `gitnexus` to npm:
- **Stable** (`.github/workflows/publish.yml`) — triggered by pushing any `v*`
tag. Publishes to the `latest` dist-tag with a changelog-backed GitHub
release. Maintainers are expected to tag from `main` as a convention; the
workflow itself does not enforce branch reachability.
- **Release Candidate** (`.github/workflows/release-candidate.yml`) — runs on
every push to `main` (typically a merged PR) plus manual dispatch. Docs-only
changes are skipped via `paths-ignore`. Publishes to the `rc` dist-tag with
version `X.Y.Z-rc.N` and a GitHub prerelease, where:
- `X.Y.Z` is selected automatically. On push (and on dispatch with
`bump: auto`, the default) the workflow **continues the active rc cycle**:
if the registry already has `X.Y.Z-rc.*` versions with `X.Y.Z` > current
`latest`, it reuses the highest such base; otherwise it patch-bumps
from `latest`. Dispatching with `bump: patch|minor|major` **resets**
the cycle from `latest`.
- `N` is auto-incremented against existing `X.Y.Z-rc.*` entries on the
registry. First rc for a given base is `rc.1`.
Idempotency: the workflow pushes an `rc/<HEAD_SHA>` marker tag and a
`v<RC>` release tag **atomically, before** calling `npm publish`. The guard
refuses to re-run once the marker exists, so a post-publish failure will
not mint a duplicate rc for the same commit. The `v<RC>` tag points at a
detached release commit whose `package.json` matches the npm tarball
exactly (traceable releases). Recovery after a partial failure:
```bash
git push --delete origin rc/<HEAD_SHA> v<RC>
# then redispatch the workflow with force: true
```
The rc workflow never moves `latest`. To verify after a change, inspect dist-tags:
Rules for **human contributors** and **AI agents** working on this codebase or publishing artifacts. These complement `AGENTS.md` / `CLAUDE.md` (which focus on GitNexus-in-GitNexus workflows).
## Scope (typical agent session)
When automating changes in this repository, treat scope as **least privilege**:
- **Read:** Source, tests, docs, public config as needed for the task.
- **Write:** Only files required for the requested fix or feature; avoid unrelated formatting or refactors.
- **Execute:** Tests, typecheck, and documented CLI commands; do not run destructive commands on user data outside the repo without explicit approval.
- **Off-limits:** Other people’s machines, production deployments you don’t own, and credentials you didn’t receive permission to use.
Adjust explicitly if the maintainer defines a different scope for a task.
---
## Non-negotiables
1.**Never commit secrets** — API keys, tokens, `.env` with real values, private URLs, or session cookies. Use `.env.example` with placeholders only.
2.**Never rename symbols with blind find-and-replace** when working in a GitNexus-indexed project — use the **`rename` MCP tool** with **`dry_run: true` first**, then review `graph` vs `text_search` edits. (There is no separate `gitnexus rename` CLI; renaming goes through MCP or editor integration.)
3.**Run impact analysis before editing shared symbols** — use **`impact`** (upstream) for functions/classes/methods others call; do not ignore **HIGH** / **CRITICAL** risk without maintainer sign-off.
4.**Prefer `detect_changes` before commit** — confirm diffs map to expected symbols/processes when the graph is available.
5.**Preserve embeddings** — if `.gitnexus/meta.json` shows embeddings, run `npx gitnexus analyze --embeddings` when refreshing the index; plain `analyze` can drop them.
---
## Signs (recurring failure patterns)
Use this format: **Trigger → Instruction → Reason**.
Append new Signs here when the same mistake repeats (e.g. CI broken twice the same way).
### Sign: Stale graph after edits
- **Trigger:** MCP or resources warn the index is behind `HEAD`, or code search doesn’t match latest commit.
- **Instruction:** Run `npx gitnexus analyze` from the repo root (plus `--embeddings` if the project used them).
- **Reason:** Tools query LadybugDB built at last analyze; git changes are invisible until re-indexed.
### Sign: Embeddings vanished after analyze
- **Trigger:** Semantic search quality drops; `stats.embeddings` in `.gitnexus/meta.json` is 0 after a refresh.
- **Reason:** Embedding generation is opt-in; analyze without the flag does not preserve prior vectors.
### Sign: MCP lists no repos
- **Trigger:** MCP stderr says no indexed repos.
- **Instruction:** Run `npx gitnexus analyze` in the target repository; verify `npx gitnexus list` shows it.
- **Reason:** The MCP server discovers repos via `~/.gitnexus/registry.json`, populated by analyze.
### Sign: Wrong repo in multi-repo setups
- **Trigger:** Query/impact results clearly belong to another project.
- **Instruction:** Call `list_repos`, then pass **`repo`** on subsequent tools (or use per-workspace MCP config).
- **Reason:** Default target may be ambiguous when multiple repos are registered.
### Sign: LadybugDB lock / “database busy”
- **Trigger:** Errors opening `.gitnexus/lbug` while MCP and analyze both run.
- **Instruction:** Stop overlapping processes; one writer at a time. Retry analyze or restart MCP.
- **Reason:** Embedded DB expects single-process ownership of the store.
---
## Publishing & supply chain
- **npm:** Do not publish from unreviewed automation; follow maintainer release process. Bump version intentionally; tag releases to match `package.json`.
- **Dependencies:** Prefer minimal, auditable changes to `package.json`; run tests and CI after lockfile updates.
- **License:** This project ships under **PolyForm Noncommercial 1.0.0** — do not relicense or imply a different license in docs or metadata without maintainer approval.
---
## Escalation
Stop and ask a **human maintainer** when:
- Impact analysis shows **HIGH** / **CRITICAL** risk and the task still requires the change.
- You need to alter **CI**, **release**, or **security-sensitive** config.
- Requirements conflict (e.g. “speed up analyze” vs “must keep all embeddings on huge repo”).
- You are unsure whether data loss is acceptable (`clean`, forced migrations, schema changes).
---
## Related docs
- [ARCHITECTURE.md](ARCHITECTURE.md) — components and data flow.
- [RUNBOOK.md](RUNBOOK.md) — commands for recovery.
- [CONTRIBUTING.md](CONTRIBUTING.md) — PR and commit expectations.
GitNexus is available as an **enterprise offering** - either as a fully managed **SaaS** or a **self-hosted** deployment. Also available for **commercial use** of the OSS version with proper licensing.
Short, copy-paste operations for **local development**, **MCP**, and **CI**. Commands assume a Unix shell; on Windows use Git Bash or equivalent paths.
- From repo root, install and build the CLI package:
```bash
cd gitnexus
npm install
npm run build
```
Use `npx gitnexus …` from any path after global/published install, or `node dist/cli/index.js …` when developing from `gitnexus/` with a local build.
---
## Index out of date / “stale” tools
**Symptom:** MCP or resources warn the index is behind `HEAD`, or results don’t reflect recent commits.
**Fix (from the target repo root):**
```bash
npx gitnexus analyze
```
**Force full rebuild** (same commit but suspect corruption or changed ignore rules):
```bash
npx gitnexus analyze --force
```
**Check status:**
```bash
npx gitnexus status
```
**List what MCP knows about:**
```bash
npx gitnexus list
```
---
## Embeddings
**First time with vectors** (slower, more disk/RAM):
```bash
npx gitnexus analyze --embeddings
```
**Important:** If you already had embeddings, **always** pass `--embeddings` on later analyzes, or they can be dropped. See `stats.embeddings` in `.gitnexus/meta.json` (0 means none).
**Large repos:** Analyze may skip or limit embedding work when node counts are very high; watch CLI output.
---
## MCP: no repos / empty tools
**Symptom:**`GitNexus: No indexed repos yet` on stderr when starting MCP.
**Fix:** In each project you want indexed:
```bash
cd /path/to/repo
npx gitnexus analyze
```
Restart the editor MCP session if needed. The server **refreshes the registry lazily**; new analyzes are picked up without necessarily reinstalling MCP.
**Symptom:** Wrong repo when multiple are indexed — pass `repo` on tools or use `list_repos` first.
---
## Clean slate (corrupt or huge `.gitnexus`)
**Current repo only** (prompts for confirmation):
```bash
npx gitnexus clean
```
**Skip confirmation:**
```bash
npx gitnexus clean --force
```
**All registered repos:**
```bash
npx gitnexus clean --all --force
```
Then re-run `npx gitnexus analyze` (and `--embeddings` if you need vectors).
---
## Local bridge for the web UI
```bash
cd gitnexus
npx gitnexus serve
# default http://127.0.0.1:4747 — see serve --help for port/host
```
Use when the browser UI should talk to **local** indexed repos instead of WASM-only mode.
**Note:** Pushes that touch only certain markdown paths may be skipped by `paths-ignore` in CI — see workflow file for exact patterns.
---
## Memory / analyze crashes
Analyze re-execs Node with a **large old-space heap** when needed (`analyze.ts`). If you still OOM on huge repos, close other processes, avoid `--embeddings` for a first pass, or analyze a smaller path if supported by your workflow.
---
## LadybugDB / lock errors
Only one process should open a repo’s `.gitnexus/lbug` store at a time. If MCP and a second `analyze` run conflict, stop one process, then retry `analyze` or restart MCP.
Skip with `git commit --no-verify` (use sparingly).
## Test categories
- **Unit** — Pure logic, parsers, graph/query helpers; fast; no network.
- **Integration** — Real combinations (filesystem, MCP wiring, larger pipelines) as already organized under `gitnexus/test/integration`.
- **Eval-style / golden sets** — For agent- or classification-style behavior, keep labeled inputs and expected outputs (JSON or table-driven tests) and run them in CI when relevant.
- **E2E (web)** — Critical user paths only; prefer `data-testid` attributes for stable selectors. Tests run against real backend (`gitnexus serve`) and Vite dev server.
## Performance metrics (targets)
Set targets to match team expectations, then tune to this repo’s CI reality:
- **`ci-e2e.yml`** — Playwright E2E tests, gated on `gitnexus-web/**` changes
Local checks before pushing:
```bash
cd gitnexus && npx tsc --noEmit && npm test
cd ../gitnexus-web && npx tsc -b --noEmit && npm test
```
Or rely on the pre-commit hook which runs these automatically for staged files.
## User acceptance / beta (optional)
For staged releases or UI betas: deploy to a staging environment, collect structured feedback, watch errors and latency, then iterate before a wider release.
GitNexus indexes COBOL codebases using a **regex-only extraction** strategy, bypassing tree-sitter entirely. This document explains why, how the pipeline works, and links to detailed sub-documents.
## Why Regex-Only?
The tree-sitter-cobol grammar (v0.0.1) has three critical limitations that make it unusable for production indexing:
| Issue | Impact | Severity |
|-------|--------|----------|
| External scanner hangs on ~5% of files | No timeout mechanism exists for the C scanner; the process blocks indefinitely | **Blocking** |
| Only ~15% of paragraph headers detected | Most procedure-division paragraphs are invisible to the grammar | High |
| Patch markers in cols 1-6 cause parse errors | Enterprise COBOL uses non-standard sequence area content (e.g., `mzADD`, `estero`, `#FIX`) | High |
Because the external scanner hang cannot be interrupted (there is no `setTimeoutMicros` equivalent for tree-sitter), using tree-sitter-cobol would hang the indexing pipeline on a non-trivial fraction of real-world files.
The regex-only approach provides:
- **Speed**: ~1ms per file average extraction time
- **Reliability**: zero hangs, zero crashes across 13,000+ files
- **Coverage**: captures all critical symbols -- program name, paragraphs, sections, CALL, PERFORM, COPY, data items (01-77, 88-level), file declarations, FD entries, EXEC SQL/CICS blocks, ENTRY points, and MOVE statements
## Architecture
```mermaid
flowchart TD
A[Repository Scan] --> B{File Detection}
B -->|Extension match| C[COBOL file]
B -->|GITNEXUS_COBOL_DIRS match| C
B -->|No match| Z[Skip]
C --> D{Copybook?}
D -->|Yes| E[Add to Copybook Map]
D -->|No| F[Source Program]
E --> G[COPY Expansion Engine]
F --> G
G -->|Inline copybook content| H[Expanded Source]
H --> I[Patch Marker Cleanup]
I --> J[Regex State Machine]
J --> K[Extracted Symbols]
K --> L[Graph Model Builder]
L --> M[Knowledge Graph]
subgraph "Per-Chunk Processing"
G
H
I
J
K
L
end
subgraph "Post-Processing"
M --> N[Community Detection]
M --> O[Process Detection]
M --> P[Contract Detection]
end
style J fill:#e8f5e9,stroke:#2e7d32
style G fill:#e3f2fd,stroke:#1565c0
```
## COBOL vs Tree-Sitter Languages
| Feature | COBOL (Regex) | Tree-Sitter Languages |
The COPY statement is COBOL's include mechanism -- analogous to `#include` in C or `import` in modern languages. GitNexus expands COPY statements **before** regex extraction so that symbols defined inside copybooks (data items, paragraphs, etc.) are visible in the program's extracted graph.
## Supported Syntax
### Basic COPY
```cobol
COPY CPSESP.
COPY "WORKGRID.CPY".
```
Inlines the content of the named copybook, replacing the COPY line(s).
Pipeline->>Pipeline: Replace file content with expanded content
end
```
The return type `CopyExpansionResult` contains `expandedContent` and `copyResolutions`. The `expansionDepth` field has been removed from the return type (it was unused by callers).
COPY statement line numbers in `CopyResolution` are 1-based (consistent with the preprocessor's line numbering). The splice operation that replaces COPY lines with expanded content adjusts for 0-based array indexing internally.
## Cycle Detection
Circular COPY references (e.g., copybook A includes copybook B which includes copybook A) are detected and handled:
1. Each expansion chain maintains a `visited` set of resolved copybook paths
2. If a copybook path is already in the visited set, the expansion is skipped
3. A `warnedCircular` set (internal to `expandCopies()`, not a parameter) deduplicates warning messages within a single file expansion
Known circular copybooks in PROJECT-NAME: `ANAZI`, `ANDIP`, `QDIPE` (self-referential includes).
## Max Depth
Nested COPY expansion is limited to **10 levels** (`DEFAULT_MAX_DEPTH`). If a COPY chain exceeds this depth, a warning is logged and the remaining COPY statements are left unexpanded.
## Max Total Expansions
A breadth amplification guard caps the total number of COPY expansions across all branches within a single file to **500** (`MAX_TOTAL_EXPANSIONS`). This prevents exponential blowup from diamond-shaped COPY graphs where N copybooks each include N other copybooks. Once the limit is reached, further COPY statements in that file are left unexpanded and a single warning is logged.
## REPLACING Application Detail
The REPLACING engine works by scanning all COBOL identifiers (matching `\b[A-Z][A-Z0-9-]*\b`) in the copybook content and applying each replacement rule:
```
Original copybook content:
05 ESP-NAME PIC X(30).
05 ESP-CODE PIC X(10).
05 KPSESPL-FLAG PIC X(01).
After REPLACING LEADING "ESP-" BY "LK-ESP-" LEADING "KPSESPL" BY "LK-KPSESPL":
05 LK-ESP-NAME PIC X(30).
05 LK-ESP-CODE PIC X(10).
05 LK-KPSESPL-FLAG PIC X(01).
```
For LEADING replacements, the engine checks if each identifier starts with the `from` prefix (case-insensitive) and replaces only the prefix portion, preserving the rest of the identifier.
For TRAILING replacements, the same logic applies to suffixes.
For EXACT replacements, only identifiers that match the `from` value exactly (case-insensitive) are replaced.
## Copybook Resolution
The resolver tries multiple strategies to match a COPY target name to a copybook file:
1.**Exact match**: `COPY CPSESP` resolves to copybook named `CPSESP`
2.**Strip extension**: `COPY WORKGRID.CPY` strips `.CPY` and resolves to `WORKGRID`
3.**Add extension**: `COPY CPSESP` tries `CPSESP.CPY` and `CPSESP.COPY`
If no match is found, the COPY statement is left in place (unexpanded) and a resolution record with `resolvedPath: null` is created.
## Pipeline Integration
The expansion runs **per chunk**, after file content is read but before dispatch to worker threads:
1. All copybook files are read upfront (they are typically small, collectively under 100MB)
2. Per chunk, the copybook map is merged with chunk content (in case a chunk contains copybooks)
3. Only programs (not copybooks themselves) undergo expansion
4. The expanded content replaces the original content in-place before worker dispatch
## Inline Comment Handling
The copy expander's `stripInlineComment()` helper is quote-aware: pipe characters (`|`) inside single- or double-quoted strings are preserved. This matches the same quote-aware logic used by the preprocessor.
The stack maintains items where each entry's level is strictly less than the next. When a new item arrives with a level <= the top of stack, items are popped until the stack top has a smaller level. A `CONTAINS` edge is created from the stack top to the new item.
For 88-level condition names, the parent is the immediately preceding non-88 data item (found by scanning backwards).
A maximum of **500 data items per file** (`MAX_DATA_ITEMS_PER_FILE`) are processed. Some COBOL programs (especially after COPY expansion) can have 10,000+ data items, which would cause graph bloat and push the V8 relationship Map past its 16.7M entry limit across thousands of files.
The cap applies after extraction: the first 500 items in source order are kept. Since 01-level records appear first, critical top-level structure is preserved.
## EXEC SQL
EXEC SQL blocks are accumulated across lines between `EXEC SQL` and `END-EXEC`, then parsed as a unit.
### Operation Classification
The first SQL keyword determines the operation:
| First Keyword | Operation |
|---------------|-----------|
| `SELECT` | SELECT |
| `INSERT` | INSERT |
| `UPDATE` | UPDATE |
| `DELETE` | DELETE |
| `DECLARE` | DECLARE |
| `OPEN` | OPEN |
| `CLOSE` | CLOSE |
| `FETCH` | FETCH |
| *(anything else)* | OTHER |
### Table Extraction
Tables are extracted from SQL clauses:
| Clause Pattern | Example |
|----------------|---------|
| `FROM <table>` | `SELECT * FROM EMPLOYEES` |
| `INSERT INTO <table>` | `INSERT INTO EMPLOYEES` |
| `UPDATE <table>` | `UPDATE EMPLOYEES SET ...` |
| `JOIN <table>` | `LEFT JOIN DEPARTMENTS ON ...` |
Note: The `INTO` pattern is restricted to `INSERT INTO` to avoid false positives from `FETCH ... INTO :host-var` and `SELECT ... INTO :host-var` statements, where `INTO` introduces host variables rather than table names.
This distinction allows queries to find bulk field-by-field moves separately from simple variable assignments.
## GO TO DEPENDING ON
The `GO TO` statement with multiple targets and a `DEPENDING ON` clause is a computed branch:
```cobol
GOTOPARA-1PARA-2PARA-3
DEPENDINGONWK-SELECTOR.
```
All target paragraph names are extracted and emitted as separate `gotos` entries. Each target produces a `CALLS` edge in the graph (same semantics as PERFORM). The `DEPENDING ON` variable is not currently tracked as a data-flow dependency.
## SORT INPUT/OUTPUT PROCEDURE
SORT and MERGE statements can specify procedural entry points instead of file-based I/O:
```cobol
SORTSORT-FILEONASCENDINGKEYSORT-KEY
INPUTPROCEDUREISPREPARE-INPUT
OUTPUTPROCEDUREISFORMAT-OUTPUT.
```
`INPUT PROCEDURE IS` and `OUTPUT PROCEDURE IS` targets are extracted as control-flow targets (same as PERFORM). They produce `performs` entries and corresponding `CALLS` edges in the graph.
## Fixed-Format Literal Continuation
In fixed-format COBOL, string literals can span multiple lines using the continuation indicator (`-` in column 7). When a continuation line starts with a quote character, the extractor joins it with the predecessor by removing the trailing quote from the previous line and the opening quote from the continuation:
```
Line N: MOVE "THIS IS A LONG STRI
Line N+1 (cont): - "NG VALUE" TO WK-FIELD.
Merged: MOVE "THIS IS A LONG STRING VALUE" TO WK-FIELD.
```
The trailing `"` on line N and the opening `"` on line N+1 are both removed, producing a seamless literal. If no matching quote is found on the predecessor line, the continuation is appended as-is.
## Source Files
-`gitnexus/src/core/ingestion/cobol-preprocessor.ts` -- All extraction logic, clause parsers, EXEC block parsers
GitNexus detects COBOL files through two mechanisms: extension-based mapping and directory-based override for extensionless files. This document covers both, plus the copybook/program classification logic.
| `.open` / `.OPEN` | Copybook | File OPEN fragment |
| `.close` / `.CLOSE` | Copybook | File CLOSE fragment |
| `.ini` / `.INI` | Copybook | Initialization fragment |
| `.def` / `.DEF` | Copybook | Definition fragment |
All extension matching is case-sensitive in `getLanguageFromFilename` (the extensions above are matched as written, including uppercase variants like `.GNM`).
Many enterprise COBOL repositories use extensionless files -- the filename alone identifies the program (e.g., `s/BGTABFL` is the source for program `BGTABFL`). GitNexus handles this via the `GITNEXUS_COBOL_DIRS` environment variable.
### Configuration
Set `GITNEXUS_COBOL_DIRS` to a comma-separated list of directory names:
```bash
# Files in s/, c/, and wfproc/ directories (at any depth) are treated as COBOL
exportGITNEXUS_COBOL_DIRS=s,c,wfproc
```
The matching is **case-insensitive** and checks all path segments:
The path segment check splits the full path on `/` and tests each segment against the cached set.
## Copybook vs Program Classification
After a file is identified as COBOL, it must be classified as either a **program** (to be parsed for symbols) or a **copybook** (to be loaded into the copybook map for COPY expansion).
### Classification Rules
A COBOL file is classified as a **copybook** if ANY of these conditions is true:
1. It has a recognized copybook extension (`.cpy`, `.copy`, `.gnm`, `.fd`, `.wrk`, `.sel`, `.open`, `.close`, `.ini`, `.def`)
2. It is an extensionless file whose path contains a directory segment matching one of: `c`, `copy`, `copybooks`, `copylib`, `cpy`
A file is classified as a **program** if:
1. It has a program extension (`.cbl`, `.cob`, `.cobol`), OR
2. It is extensionless and does NOT match any copybook directory pattern
### Copybook Name Resolution
Copybook names are derived from the filename:
- Strip the extension (if any)
- Convert to uppercase
Examples:
-`c/CPSESP` -- name: `CPSESP`
-`copy/workgrid.cpy` -- name: `WORKGRID`
-`c/ANAZI.GNM` -- name: `ANAZI`
This name is used to resolve `COPY CPSESP.` statements during expansion.
This document describes the graph nodes and edges that GitNexus creates for COBOL codebases. The COBOL graph model is richer than most tree-sitter languages because it captures domain-specific constructs: file declarations, FD entries, data hierarchies, SQL tables, CICS maps, and cross-program contracts.
## Entity-Relationship Diagram
```mermaid
erDiagram
File ||--o{ Module : DEFINES
File ||--o{ Function : DEFINES
File ||--o{ Namespace : DEFINES
File ||--o{ Record : DEFINES
File ||--o{ Property : DEFINES
File ||--o{ Const : DEFINES
File ||--o{ CodeElement : DEFINES
File ||--o{ Constructor : DEFINES
File }o--o{ File : IMPORTS
Module ||--o{ Record : CONTAINS
Module ||--o{ Constructor : CONTAINS
Module }o--o{ CodeElement : ACCESSES
Module }o--o{ Module : CALLS
Module }o--o{ Module : CONTRACTS
Module }o--o{ Property : RECEIVES
Record ||--o{ Property : CONTAINS
Record ||--o{ Const : CONTAINS
Record }o--o{ Record : REDEFINES
Property ||--o{ Property : CONTAINS
Property ||--o{ Const : CONTAINS
Property }o--o{ Property : REDEFINES
Property }o--o{ CodeElement : RECORD_KEY_OF
Property }o--o{ CodeElement : FILE_STATUS_OF
CodeElement ||--o{ CodeElement : CONTAINS
CodeElement ||--o{ Record : CONTAINS
Function }o--o{ Function : CALLS
```
## Node Types
| Node Type | COBOL Concept | Created From | Example |
The worker pool splits each worker's chunk into sub-batches to bound peak memory per `postMessage` serialization. COBOL repos use a smaller sub-batch size than the default:
| Per sub-batch timeout | 120s | 120s (configurable) |
**Why 200?** COBOL regex extraction + preprocessing takes ~1ms per file on average, but with COPY expansion and deep indexing the effective time is ~150ms per file. At sub-batch size 1500, that would be ~225s per sub-batch, exceeding the 120s timeout.
COBOL mode is activated automatically when `GITNEXUS_COBOL_DIRS` is set:
Workers default to `min(8, cpus - 1)`. For COBOL repos, this is usually sufficient since regex extraction is CPU-bound but fast. The bottleneck is typically KuzuDB write, not extraction.
For COBOL-only repos, worker startup is faster because tree-sitter native modules are loaded lazily (skipped entirely if only COBOL files are present).
## Data Item Cap
### Configuration
```typescript
constMAX_DATA_ITEMS_PER_FILE=500;
```
This constant appears in both `parse-worker.ts` (worker path) and `parsing-processor.ts` (sequential fallback).
### Rationale
Some COBOL programs, especially after COPY expansion, can have 10,000+ data items. At that scale:
- The in-memory relationship Map (for CONTAINS, REDEFINES, etc.) approaches the V8 16.7M entry limit across thousands of files
- KuzuDB write time increases linearly with edge count
- Most deep-nested items (level 20+) are rarely queried individually
### Impact
The cap truncates data items beyond the 500th in source order. Since 01-level Records appear first in COBOL source, the cap preserves:
- All 01-level record definitions
- The most important 02-49 level items (those closest to the record root)
- 88-level conditions associated with early items
To increase the cap for specific needs, modify the `MAX_DATA_ITEMS_PER_FILE` constant in both files.
## Memory Management
### COPY Expansion Breadth Guard
A per-file `MAX_TOTAL_EXPANSIONS = 500` limit prevents exponential blowup from diamond-shaped COPY graphs (e.g., N copybooks each containing N COPY statements). Once the limit is reached, further COPY statements in that file are left unexpanded. See [copy-expansion.md](copy-expansion.md) for details.
### COPY Expansion Memory
All copybook content is loaded upfront into a Map before chunk processing begins. For PROJECT-NAME:
- 2,976 copybooks, typically under 100MB total
- The Map is shared (read-only) across chunk iterations
- Per-chunk, the copybook map is merged with chunk file content (in case a chunk contains copybooks not in the pre-loaded set)
- After all chunks are processed, the copybook map is freed (`cobolCopybookContents = undefined`)
### Chunk Budget
Source files are grouped into chunks of max 20MB (`CHUNK_BYTE_BUDGET`). Each chunk's lifecycle:
This ensures only ~20MB of source + ~200-400MB of working memory (ASTs, extracted records, serialization) is active at any time.
### Shared Warning Deduplication
The `warnedCircular` set (used by the COPY expansion engine) is shared across all files in a chunk. This prevents the same circular copybook warning (e.g., `ANAZI includes itself`) from being logged thousands of times.
| tree-sitter-cobol hangs on ~5% of files | Cannot use tree-sitter for COBOL | Regex-only extraction (current approach) |
| Data item cap (500/file) | May miss deeply nested items in large programs | Increase `MAX_DATA_ITEMS_PER_FILE` in source |
| Circular copybooks (ANAZI, ANDIP, QDIPE) | Self-referential includes cannot be expanded | Detected and skipped with warning |
| wfproc/ files may not be pure COBOL | Workflow files may produce extraction noise | Exclude `wfproc` from `GITNEXUS_COBOL_DIRS` if problematic |
| No MOVE DATA_FLOW edges yet | Data flow between variables not in graph | Reserved for future release |
| Continuation line handling | Some complex multi-line continuations (especially in string literals spanning 3+ lines) may not merge correctly | Known edge case; affects <0.1% of lines |
| Single-line EXEC blocks | `EXEC SQL SELECT ... END-EXEC` on one line is handled, but pathological nesting is not | Extremely rare in practice |
| Extension case sensitivity | `.GNM` and `.gnm` are matched differently | Use the exact case from the codebase |
## Troubleshooting
### "COPY expansion failed"
```
[pipeline] COPY expansion failed for s/BGTABFL: Cannot read properties of null
```
**Cause:** A copybook referenced by a COPY statement cannot be found.
**Fix:**
1. Verify `GITNEXUS_COBOL_DIRS` includes the directory containing copybooks (typically `c`)
2. Check that copybook filenames match the COPY target (case-insensitive, after stripping extensions)
3. Ensure copybook files are not in `.gitignore`
### Worker sub-batch timeout
```
Worker 3 sub-batch timed out after 120s (chunk: 200 items)
```
**Cause:** A sub-batch took longer than the timeout. Typically happens when one file is extremely large (50,000+ lines after COPY expansion).
For very large repos (>500MB source), consider `--max-old-space-size=32768`.
### Concurrent analyze corruption
**Rule:** Only ONE `gitnexus analyze` process should run at a time per repository. Concurrent writes to KuzuDB corrupt the database.
If corruption occurs:
```bash
# Remove the KuzuDB directory and re-index
rm -rf .gitnexus/kuzu
gitnexus analyze --force
```
### Slow KuzuDB write phase
The KuzuDB write phase (132s for PROJECT-NAME) is the bottleneck for large COBOL repos. This is proportional to the number of nodes and edges being written. Reducing `MAX_DATA_ITEMS_PER_FILE` or excluding non-essential directories from `GITNEXUS_COBOL_DIRS` can help.
### Verbose output
Enable verbose logging to see per-phase timing and statistics:
The `extractCobolSymbolsWithRegex()` function in `cobol-preprocessor.ts` performs single-pass, state-machine-driven extraction of all COBOL symbols. This document describes the state machine, line processing flow, and every regex pattern used.
## State Machine: Division Tracking
The extractor tracks which COBOL division is currently being processed. Division transitions are detected by the `RE_DIVISION` pattern.
Within the ENVIRONMENT DIVISION, the `currentEnvSection` tracks whether we are in `INPUT-OUTPUT` or `CONFIGURATION` section. SELECT statement accumulation only occurs in `INPUT-OUTPUT`.
## Line Processing Flow
Each raw source line goes through this pipeline:
```
Raw line
|
v
Length < 7? ---------> Skip (flush pending if any)
|
v
Indicator col 7
|
+-- '*' or '/' -----> Comment: skip entirely
|
+-- '-' ------------> Continuation: append to pending line
|
+-- other ----------> Normal: flush pending, strip inline comments (|),
buffer as new pending logical line
```
After all lines are processed, the final pending line is flushed, along with any accumulated SELECT statement, SORT/MERGE accumulator, and any open EXEC block (truncated file without `END-EXEC`).
### Inline Comment Stripping
Enterprise COBOL (particularly Italian dialect) uses the pipe character `|` as an inline comment marker. The `stripInlineComment()` helper is **quote-aware**: it tracks whether the scan position is inside a single- or double-quoted string and only treats `|` as a comment marker when outside quotes. Pipe characters inside string literals are preserved.
Free-format `*>` inline comment stripping uses the same quote-aware approach: the scanner walks character by character, toggling quote state, and only recognizes `*>` as a comment marker when not inside a quoted string.
### Patch Marker Handling
The `preprocessCobolSource()` function (run before extraction in the worker) replaces non-standard content in columns 1-6. Standard COBOL expects spaces or digit sequence numbers in this area. If any letter or `#` character is found, the entire sequence area is replaced with 6 spaces:
```
Before: mzADD MOVE WK-AMT TO WK-TOTAL
After: MOVE WK-AMT TO WK-TOTAL
```
This preserves exact line count for position mapping.
## Regex Pattern Reference
All patterns are compiled once as module-level constants and reused across calls.
SELECT statements are accumulated across multiple lines until a period terminator is found, then parsed for ASSIGN, ORGANIZATION, ACCESS, RECORD KEY, and FILE STATUS clauses.
| `RE_PROC_USING` | `\bPROCEDURE\s+DIVISION\s+USING\s+([\s\S]*?)(?:\.\|$)` | USING parameters | `PROCEDURE DIVISION USING WK-PARAM` |
| `RE_ENTRY` | `\bENTRY\s+"([^"]+)"(?:\s+USING\s+([\s\S]*?))?(?:\.\|$)` | ENTRY point | `ENTRY "SUBPROG" USING WK-DATA` |
| `RE_MOVE` | `\bMOVE\s+((?:CORRESPONDING\|CORR)\s+)?([A-Z][A-Z0-9-]+)\s+TO\s+(.+)` | MOVE statement (supports CORR abbreviation and multi-target) | `MOVE WK-NAME TO OUT-NAME`, `MOVE CORR WK-IN TO WK-OUT` |
The USING parameter list (`RE_PROC_USING`) is split on `\bRETURNING\b` before tokenization -- any RETURNING clause and everything after it is excluded from the parameter list (`.split(/\bRETURNING\b/i)[0]`).
Note: `RE_PROC_SECTION` and `RE_PROC_PARAGRAPH` require exactly 7 spaces of leading indentation (COBOL area A starting at column 8). This is the standard COBOL paragraph indentation.
### All-Division Patterns
These patterns are checked regardless of current division:
| `SORT_CLAUSE_NOISE` | Set of SORT/MERGE clause keywords filtered from USING/GIVING file lists: `ON`, `ASCENDING`, `DESCENDING`, `KEY`, `WITH`, `DUPLICATES`, `IN`, `ORDER`, `COLLATING`, `SEQUENCE`, `IS`, `THROUGH`, `THRU`, `INPUT`, `OUTPUT`, `PROCEDURE` |
SORT and MERGE statements are accumulated across multiple lines (like SELECT) until a period terminator is found, then parsed for USING/GIVING file lists and INPUT/OUTPUT PROCEDURE targets. The `flushSort()` helper encapsulates the flush-and-parse logic, mirroring the existing `flushSelect()` pattern. Both helpers are called at EOF to handle truncated files.
### GO TO Multi-Target
`RE_GOTO` captures all paragraph names in a `GO TO` statement, including the multi-target form `GO TO p1 p2 p3 DEPENDING ON x`. The captured group contains all target names (space-separated), which are split into individual targets. Each target produces a separate `gotos` entry.
### PROGRAM-ID Detection
PROGRAM-ID is detected regardless of the current division state. This handles sibling programs that appear after `END PROGRAM` and omit the `IDENTIFICATION DIVISION` header -- the extractor will still capture the PROGRAM-ID and push a new program boundary.
| `RE_END_EXEC` | `\bEND-EXEC\b` | End of EXEC block | `END-EXEC` |
EXEC blocks accumulate all lines between `EXEC SQL/CICS` and `END-EXEC`, then delegate to `parseExecSqlBlock()` or `parseExecCicsBlock()` for detailed extraction.
## Excluded Paragraph Names
The following names are excluded from paragraph detection to avoid false positives from division/section headers:
```
DECLARATIVES, END, PROCEDURE, IDENTIFICATION,
ENVIRONMENT, DATA, WORKING-STORAGE, LINKAGE,
FILE, LOCAL-STORAGE, COMMUNICATION, REPORT,
SCREEN, INPUT-OUTPUT, CONFIGURATION
```
Additionally, paragraph candidates containing `DIVISION` or `SECTION` as substrings are excluded.
## MOVE Skip List (Figurative Constants)
MOVE statements where the source is a figurative constant are skipped:
```
SPACES, ZEROS, ZEROES, LOW-VALUES, LOW-VALUE,
HIGH-VALUES, HIGH-VALUE, QUOTES, QUOTE, ALL
```
## Source Files
-`gitnexus/src/core/ingestion/cobol-preprocessor.ts` -- `preprocessCobolSource()`, `extractCobolSymbolsWithRegex()`, all regex constants
- CALL USING supports mixed modes: `USING BY REFERENCE WS-A BY CONTENT WS-B BY VALUE WS-C`
- CALL USING `ADDRESS OF` and `OMITTED` must be filtered from parameter lists
- EXEC DLI can have multiple SEGMENT levels in hierarchical retrieval (use matchAll)
- DECLARATIVES can have multiple USE sections (one per file + catch-all for INPUT/OUTPUT/I-O/EXTEND)
- INSPECT TALLYING can have multiple counters in a single statement
- STRING/UNSTRING can span multiple lines (need accumulator pattern)
---
# Complete COBOL Language Feature Coverage
## Overview
Implement the remaining 25 unhandled COBOL language features and fix 10 partial features to achieve ~95% coverage (up from 71.9%). The goal is to build the richest possible knowledge graph from COBOL codebases, enabling a future `modernize` MCP command (out of scope for this plan) that would use the graph to assist with COBOL-to-modern-language migration.
## Problem Statement
The COBOL processor currently handles 54 of 89 applicable language features (71.9%). The 25 unhandled features represent real data loss in the knowledge graph:
- **Cross-program data flow** is invisible (CALL ... USING parameters not extracted)
- **IMS/DB programs** produce empty graphs (EXEC DLI not recognized)
- **String transformation logic** is invisible (STRING/UNSTRING/INSPECT not tracked)
- **SQL copybook dependencies** are missing (EXEC SQL INCLUDE not mapped)
- **Error handling flows** are lost (DECLARATIVES/USE AFTER not captured)
## Proposed Solution
Implement features in 4 phases, ordered by graph value density (edges created per LOC of implementation). Each phase is independently shippable and testable.
## Technical Approach
### Phase 1: High-Value Data Flow Edges (~150 LOC, ~8 new edge types)
The highest-ROI features: they create new ACCESSES and IMPORTS edges that directly improve impact analysis.
**Critical research finding**: Multi-line statement accumulation is the dominant challenge. CALL USING, STRING/UNSTRING, and multi-line data item clauses all span multiple lines in production COBOL. The free-format path processes each line independently — these features need statement accumulators (like SORT/SELECT) or the free-format path needs multi-line awareness. Estimated LOC increased from 110 to 150 to account for accumulator infrastructure.
- **What:** After capturing CALL target, scan for USING clause. Extract parameter names (reuse USING_KEYWORDS filter). Store as `calls[].parameters: string[]`
- **Interface:** Add `parameters?: string[]` to calls array type in CobolRegexResults
- Mixed modes: `CALL 'PGM' USING BY REFERENCE WS-A BY CONTENT WS-B BY VALUE WS-C`
- Pointer passing: `CALL 'PGM' USING ADDRESS OF WS-A`
- Placeholder: `CALL 'PGM' USING OMITTED WS-B`
- Filter keywords: add `ADDRESS`, `OMITTED`, `LENGTH` to USING_KEYWORDS (already has BY/VALUE/REFERENCE/CONTENT)
- **Impact tool enhancement:** CALL-USING edges enable BFS traversal through parameter data flow — single most impactful edge type for COBOL impact analysis
#### 1.3 STRING/UNSTRING data flow -> ACCESSES edges
- **File:** `cobol-preprocessor.ts` (new section in extractProcedure)
- **What:** Accumulate multi-line STRING/UNSTRING until period or END-STRING/END-UNSTRING. Extract sources and INTO targets.
- IBM allows `OCCURS 0 TO n DEPENDING ON` (zero minimum) and `OCCURS UNBOUNDED DEPENDING ON` (V6.4)
- Subscripted controlling fields: `DEPENDING ON WS-COUNT(WS-IDX)` — strip subscripts before storing
- **Pre-existing gap**: Multi-line data item clauses without continuation indicator are NOT captured. `05 WS-TABLE\n OCCURS 100\n DEPENDING ON WS-COUNT.` — the current RE_DATA_ITEM only gets the first line, `rest` is empty. Fixing properly requires a data item accumulator (like SELECT). **Defer full fix to Phase 3; implement same-line capture now.**
- KEY IS fields: `ASCENDING KEY IS WS-KEY-1 WS-KEY-2` — capture for SEARCH ALL resolution
- INDEXED BY: `INDEXED BY IDX-1 IDX-2` — capture for SET/SEARCH context
- Numeric with sign/decimal: `VALUE -123.45`, `VALUE +1`
-`VALUE IS` optional — both `VALUE 'A'` and `VALUE IS 'A'` valid
- **Decimal vs period ambiguity**: `VALUE 100.` — is `.` decimal or terminator? `parseDataItemClauses` already strips trailing period, so this is handled
- IBM V6.4: floating-point `VALUE 1.0E5` — extend numeric regex if needed
- Implementation: use a pragmatic `extractValue(rest)` function, not a single complex regex
- **Graph:** CodeElement node + ACCESSES edge to `<ims>:<segmentName>` Record node with reason `dli-{verb}`; ACCESSES edges to INTO/FROM data areas; PSB ACCESSES for SCHD
- **Tests:** `EXEC DLI GU USING PCB(1) SEGMENT(CUSTOMER) INTO(WS-CUST) END-EXEC`
**Research insights (dual IMS interface):**
- **EXEC DLI**: Embedded command interface for CICS-DL/I programs only
- **CBLTDLI CALL**: Batch interface via `CALL 'CBLTDLI' USING function-code PCB io-area SSA1..SSA15`
- CBLTDLI is already captured as a CALL to 'CBLTDLI' — enrich with USING parameter semantics later
- Multiple SEGMENT levels in hierarchical retrieval — use `matchAll` on segment regex
- **Graph:** ACCESSES read on inspected field always; write if REPLACING/CONVERTING. Write edges for tally counters. Reason: `cobol-inspect-read`/`cobol-inspect-write`/`cobol-inspect-tally`
- **Tests:** `INSPECT WS-FIELD TALLYING WS-COUNT FOR ALL 'A'` -> read on WS-FIELD, write on WS-COUNT
**Research insights (INSPECT forms by frequency):**
- REPLACING (~60%): `INSPECT WS-STR REPLACING ALL 'A' BY 'B'`
- TALLYING (~25%): `INSPECT WS-STR TALLYING WS-CNT FOR ALL 'A'` — multiple counters possible
- CONVERTING (~10%): `INSPECT WS-STR CONVERTING 'abc' TO 'ABC'`
- Combined (~5%): TALLYING + REPLACING in single statement
- **Needs multi-line accumulator** — INSPECT frequently spans 3-5 lines in production
- Extract tally counters with `([A-Z][A-Z0-9-]+)\s+FOR\b` matchAll pattern
- Filter figurative constants (SPACES, ZEROS) using existing MOVE_SKIP set
### Phase 3: Completeness Fixes (~60 LOC)
Fix the 10 partial features and small gaps.
#### 3.1 CALL ... RETURNING extraction
- Extend RE_CALL processing to capture RETURNING target after the USING clause
- Store as `calls[].returning?: string`
- Graph: ACCESSES write edge with reason `cobol-call-returning`
#### 3.2 SELECT OPTIONAL flag preservation
- Store `isOptional: boolean` in FileDeclaration interface
- Include in Record node description
#### 3.3 ALTERNATE RECORD KEY extraction
- Add regex in parseSelectStatement: `/\bALTERNATE\s+RECORD\s+KEY\s+(?:IS\s+)?([A-Z][A-Z0-9-]+)/i`
- [ ] Integration test assertions updated with exact counts per phase
- [ ] Benchmark run after each phase to track graph growth
## Dependencies & Risks
### Dependencies
- None. All changes are additive to existing COBOL processor code.
- No LanguageProvider changes needed.
- No graph schema changes needed (all new constructs map to existing node labels + edge types).
### Risks
- **preprocessor.ts size**: Currently 1326 LOC. Phase 1+2 adds ~200 LOC -> 1526 LOC. May need to extract helpers into a separate `cobol-data-flow.ts` module if it exceeds 1500.
- **REPLACE statement** (Phase 3.7) is the most complex feature — requires tracking text substitution state across logical lines. Consider deferring to a separate PR if it takes >100 LOC.
- **EXEC DLI** (Phase 2.1) is only testable against IMS codebases. Need fixture data or synthetic test cases.
## Graph Value Ranking by MCP Tool Impact
Research agent analyzed all 5 MCP tools (query, context, impact, detect_changes, rename) against planned edge types:
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Fix 4 HIGH-priority issues from PR #626 code review before merge.
**Architecture:** Minimal targeted fixes — each task is independent. TDD: tests first, then implementation. No refactoring beyond what's needed.
**Paths:** All file paths are relative to the monorepo root (`GitNexus/`). Git commands run from the root. The `gitnexus/` prefix is a package subdirectory, not a separate repo.
---
### Task 1: Path Traversal — Validate Group Name
**Files:**
- Modify: `gitnexus/src/core/group/storage.ts:17-19` (getGroupDir) and `:63-68` (createGroupDir)
- [ ]**Step 1: Write failing tests for validateGroupName**
In `gitnexus/test/unit/group/storage.test.ts`, add `createGroupDir` and `validateGroupName` to the existing import from `'../../../src/core/group/storage.js'` (line 6-11). Then add these describe blocks at the end of the outer `describe('Group storage', ...)`:
- [ ]**Step 2: Run tests to verify `vendor` and `target` tests fail**
Run: `cd gitnexus && npx vitest run test/unit/group/service-boundary-detector.test.ts`
Expected: `test_detect_skips_vendor_directory` and `test_detect_skips_target_directory` FAIL (vendor/target not excluded). Other new tests may pass since dotfile exclusion already exists.
- [ ]**Step 3: Add EXCLUDED_DIRS constant and update both walking functions**
In `gitnexus/src/core/group/service-boundary-detector.ts`:
- CLI smoke: one integration test that hits `getGroupDir` / `createGroupDir` (e.g. `group create` or `group add`) with an invalid name proves wiring for every subcommand that resolves a group through storage
### CLI/API entry points accepting groupName
All paths flow through `getGroupDir` (which validates), so coverage is implicit. For reference:
**`listGroups`:** Reads directory names from disk without validation. Not a write path, so no traversal risk. May surface manually-created directories with non-conforming names — accepted as-is, not in scope.
**Risk:**`serviceRe = /service\s+(\w+)\s*\{([^}]*)}/gs` stops at first `}`. Proto services with `google.api.http` annotations inside RPCs contain nested `{ }` blocks.
1. Use regex only to find `service <Name> {` start positions (regex consumes the opening `{`)
2. Initialise depth to 1 immediately after the opening `{`
3. Scan forward char by char: `{` -> depth++, `}` -> depth--; collect into body
4. Stop when depth reaches 0 (the matching closing `}`)
5. Return name + body pairs
Inner `rpcRe` regex remains unchanged — it operates on the already-extracted body.
**Malformed input:** If EOF is reached before `depth` returns to 0, skip the incomplete service (do not add to results). Lock this in the test.
**Scope limitation (v1):** Brace-depth only — no lexer for string literals or comments containing `{`/`}`. Sufficient for `google.api.http` annotations. Known false positive: braces inside `//` comments or quoted strings within proto options. Accepted for v1; a proper proto lexer is out of scope.
### Tests
- Proto with single service, no nesting (regression)
- Proto with `google.api.http` nested braces inside RPC options
- Proto with multiple services
- Proto with nested `option` blocks inside RPC (e.g. `google.api.http`)
- Malformed proto with unclosed brace (graceful handling)
---
## Fix 3: Directory Exclusions in Service Boundary Detector
**Risk:** Walks entire repo tree, only skipping dotfiles and `node_modules`. Extremely slow on repos with `vendor/`, `target/`, `__pycache__/`, `.venv/`.
### Solution
Create `EXCLUDED_DIRS` as a `Set<string>` (alongside existing `SERVICE_MARKERS`, `SOURCE_EXTENSIONS`), for example:
```text
node_modules, vendor, target, build, dist,
__pycache__, .venv, venv, .tox, .mypy_cache,
.gradle, .mvn, out, bin
```
(Implement as `new Set([...])` — the list above is the membership, not a string literal.)
Apply in both:
-`walkForBoundaries` (line 77-78) — replace current inline `=== 'node_modules'` check with `EXCLUDED_DIRS.has(entry.name)`
-`hasSourceFilesInSubdirs` (line 130) — replace `entry.name !== 'node_modules'` with `!EXCLUDED_DIRS.has(entry.name)`
Note: remove the old `=== 'node_modules'` literal from both locations — it is covered by `EXCLUDED_DIRS`.
Dotfile exclusion (`.` prefix) remains as a separate check since it's a pattern, not a name.
Exclusions apply only to `isDirectory()` entries — file names are never checked against `EXCLUDED_DIRS`.
**Tradeoff:** Rare layouts that keep source under names like `out/` or `bin/` will be skipped; accepted for performance on typical monorepos.
**Case sensitivity:**`Set.has` is case-sensitive (matches current `=== 'node_modules'` behavior). Windows case-insensitive FS not handled — accepted as-is, consistent with existing code.
Set `GITNEXUS_EVAL_DEBUG=1` to include full Python tracebacks in run summaries and logs. By default, errors are sanitized to avoid leaking host paths or stack traces.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.